Prechádzať zdrojové kódy

姿态识别新增: 疲劳瞌睡/长时间静止/疑似离岗

- 疲劳瞌睡(SLEEPY): 头部下垂(nose远低于肩部), 单帧判定, 可开关
- 长时间静止: 人体中心跨帧位移<3%持续N秒(默认15s), 时间戳计时, 报警+截图
- 疑似离岗: 人体消失持续N秒(默认30s), 报警+截图
- 配置: pose_sleepy/pose_still/pose_still_sec/pose_absent_sec
- 前端姿态配置页: 新增勾选+静止/离岗秒数输入
- 实测: 静止15秒/离岗30秒均正常触发
dev 1 týždeň pred
rodič
commit
8db36f46c6
5 zmenil súbory, kde vykonal 187 pridanie a 2 odobranie
  1. 5 1
      config/config.json
  2. 143 0
      src/main.cpp
  3. 14 0
      src/pose_detector.cpp
  4. 8 0
      src/pose_detector.h
  5. 17 1
      web/index.html

+ 5 - 1
config/config.json

@@ -197,5 +197,9 @@
         "password": "admin",
         "username": "admin"
     },
-    "web_port": 8080
+    "web_port": 8080,
+    "pose_sleepy": true,
+    "pose_still": true,
+    "pose_still_sec": 15,
+    "pose_absent_sec": 30
 }

+ 143 - 0
src/main.cpp

@@ -68,6 +68,13 @@ struct ChannelState {
     chrono::steady_clock::time_point fall_latch_ts{};
     string pose_action;                    // 当前命中的姿态动作
     float pose_score = 0.0f;               // 当前动作得分
+    // 疲劳/静止/离岗 跨帧状态
+    int still_count = 0;                   // 静止累计帧数
+    cv::Point2f last_still_center;         // 上一帧人体中心
+    bool last_had_person = false;          // 上一帧是否有人
+    chrono::steady_clock::time_point still_since{};   // 静止起始时刻
+    chrono::steady_clock::time_point absent_since{};  // 离岗起始时刻
+    bool absent_latched = false;           // 离岗报警锁存
     cv::Mat display_frame;
     cv::Mat fence_fill;              // 围栏填充缓存(纯色图, 与帧内容无关)
     uint64_t fence_ver = 0;          // 对应 FenceEngine 版本
@@ -529,11 +536,19 @@ int main(int argc, char* argv[]) {
     fallAnalyzer.setReclineEnabled(pose_recline);
     fallAnalyzer.setHandToMouthEnabled(pose_hand_to_mouth);
     fallAnalyzer.setMinBodyArea(pose_min_area);
+    int pose_still_sec = cfg.value("pose_still_sec", 15);    // 静止多久报警
+    int pose_absent_sec = cfg.value("pose_absent_sec", 30);  // 离岗多久报警
+    bool pose_sleepy = cfg.value("pose_sleepy", true);
+    bool pose_still = cfg.value("pose_still", true);
+    fallAnalyzer.setSleepyEnabled(pose_sleepy);
+    fallAnalyzer.setStillEnabled(pose_still);
     cout << "[Pose] 动作类型: 跌倒/倒地(固定)"
          << " 举手呼救=" << (pose_hand_raise ? "开" : "关")
          << " 异常蹲坐=" << (pose_crouch ? "开" : "关")
          << " 半躺=" << (pose_recline ? "开" : "关")
          << " 手近嘴部=" << (pose_hand_to_mouth ? "开" : "关")
+         << " 疲劳瞌睡=" << (pose_sleepy ? "开" : "关")
+         << " 静止=" << pose_still_sec << "秒 离岗=" << pose_absent_sec << "秒"
          << " 最小面积=" << pose_min_area << "px"
          << " 屏蔽区=" << pose_exclude_zones.size() << "路" << endl;
     bool pose_ok = false;
@@ -1648,6 +1663,10 @@ int main(int argc, char* argv[]) {
         r["pose_recline"] = pose_recline;
         r["pose_hand_to_mouth"] = pose_hand_to_mouth;
         r["pose_min_area"] = pose_min_area;
+        r["pose_still_sec"] = pose_still_sec;
+        r["pose_absent_sec"] = pose_absent_sec;
+        r["pose_sleepy"] = pose_sleepy;
+        r["pose_still"] = pose_still;
         {
             json zj = json::object();
             for (auto& [cid, zones] : pose_exclude_zones) {
@@ -1723,6 +1742,32 @@ int main(int argc, char* argv[]) {
                 fallAnalyzer.setHandToMouthEnabled(v);
                 full["pose_hand_to_mouth"] = v;
             }
+            if (j.contains("pose_sleepy")) {
+                bool v = j["pose_sleepy"].get<bool>();
+                pose_sleepy = v;
+                fallAnalyzer.setSleepyEnabled(v);
+                full["pose_sleepy"] = v;
+            }
+            if (j.contains("pose_still")) {
+                bool v = j["pose_still"].get<bool>();
+                pose_still = v;
+                fallAnalyzer.setStillEnabled(v);
+                full["pose_still"] = v;
+            }
+            if (j.contains("pose_still_sec")) {
+                int v = j["pose_still_sec"].get<int>();
+                if (v >= 3 && v <= 600) {
+                    pose_still_sec = v;
+                    full["pose_still_sec"] = v;
+                }
+            }
+            if (j.contains("pose_absent_sec")) {
+                int v = j["pose_absent_sec"].get<int>();
+                if (v >= 5 && v <= 3600) {
+                    pose_absent_sec = v;
+                    full["pose_absent_sec"] = v;
+                }
+            }
             if (j.contains("pose_min_area")) {
                 float v = j["pose_min_area"].get<float>();
                 if (v >= 500.0f && v <= 500000.0f) {
@@ -2164,6 +2209,104 @@ int main(int argc, char* argv[]) {
                                 fnow - ch->fall_latch_ts).count() >= rearm_sec)
                             ch->fall_latched = false;
                     }
+
+                    // ===== 长时间静止检测: 人体中心跨帧几乎不动 =====
+                    if (pose_still) {
+                        bool has_person = !ch->poses.empty();
+                        if (has_person) {
+                            cv::Point2f center(
+                                ch->poses[0].bbox.x + ch->poses[0].bbox.width / 2.0f,
+                                ch->poses[0].bbox.y + ch->poses[0].bbox.height / 2.0f);
+                            if (ch->last_had_person) {
+                                float dist = cv::norm(center - ch->last_still_center);
+                                // 移动小于 bbox 高度的 3% 视为静止
+                                float th = ch->poses[0].bbox.height * 0.03f;
+                                if (dist < th) {
+                                    if (ch->still_count == 0)
+                                        ch->still_since = chrono::steady_clock::now();
+                                    ch->still_count++;
+                                } else {
+                                    ch->still_count = 0;
+                                }
+                            }
+                            ch->last_still_center = center;
+                        } else {
+                            ch->still_count = 0;
+                        }
+                        ch->last_had_person = has_person;
+                        // 静止超过阈值触发
+                        if (ch->still_count >= 3) {   // 连续3个推理帧静止
+                            auto sNow = chrono::steady_clock::now();
+                            if (chrono::duration_cast<chrono::seconds>(
+                                    sNow - ch->still_since).count() >= pose_still_sec) {
+                                ch->still_count = 0;   // 触发后重置
+                                cout << "[Pose] CH" << ch->id << " 长时间静止("
+                                     << pose_still_sec << "秒)" << endl;
+                                if (!fall_relays.empty()) {
+                                    relay.pulseChannels(fall_relays, rc.pulse_sec);
+                                    string chans;
+                                    for (auto& t : fall_relays) {
+                                        if (!chans.empty()) chans += ",";
+                                        chans += t;
+                                    }
+                                    alarmHistory.recordAction("姿态报警",
+                                        "CH" + to_string(ch->id) + " 长时间静止", chans);
+                                }
+                                std::vector<uint8_t> frgb;
+                                int fout_w, fout_h;
+                                ch->rga.processToRgb(nv12, frgb, fout_w, fout_h);
+                                if (!frgb.empty()) {
+                                    cv::Mat fshot(fout_h, fout_w, CV_8UC3, frgb.data());
+                                    cv::cvtColor(fshot, fshot, cv::COLOR_RGB2BGR);
+                                    float fs = (nv12.width > 0)
+                                               ? (float)fshot.cols / nv12.width : 1.0f;
+                                    drawPoseDets(fshot, ch->poses, ch->poses, fs);
+                                    alarmHistory.recordPoseAlarm(ch->id, "长时间静止",
+                                                                 0.9f, fshot);
+                                }
+                            }
+                        }
+                    }
+
+                    // ===== 离岗检测: 人体消失持续 N 秒 =====
+                    {
+                        if (ch->poses.empty() && !ch->absent_latched) {
+                            if (!ch->last_had_person) {
+                                auto anow = chrono::steady_clock::now();
+                                if (ch->absent_since.time_since_epoch().count() == 0)
+                                    ch->absent_since = anow;
+                                else if (chrono::duration_cast<chrono::seconds>(
+                                             anow - ch->absent_since).count() >= pose_absent_sec) {
+                                    ch->absent_latched = true;
+                                    cout << "[Pose] CH" << ch->id << " 疑似离岗("
+                                         << pose_absent_sec << "秒无人体)" << endl;
+                                    if (!fall_relays.empty()) {
+                                        relay.pulseChannels(fall_relays, rc.pulse_sec);
+                                        string chans;
+                                        for (auto& t : fall_relays) {
+                                            if (!chans.empty()) chans += ",";
+                                            chans += t;
+                                        }
+                                        alarmHistory.recordAction("姿态报警",
+                                            "CH" + to_string(ch->id) + " 疑似离岗", chans);
+                                    }
+                                    std::vector<uint8_t> frgb;
+                                    int fout_w, fout_h;
+                                    ch->rga.processToRgb(nv12, frgb, fout_w, fout_h);
+                                    if (!frgb.empty()) {
+                                        cv::Mat fshot(fout_h, fout_w, CV_8UC3, frgb.data());
+                                        cv::cvtColor(fshot, fshot, cv::COLOR_RGB2BGR);
+                                        alarmHistory.recordPoseAlarm(ch->id, "疑似离岗",
+                                                                     0.9f, fshot);
+                                    }
+                                }
+                            }
+                        } else if (!ch->poses.empty()) {
+                            ch->absent_since = chrono::steady_clock::time_point{};
+                            ch->absent_latched = false;
+                        }
+                        // 更新 last_had_person(静止检测也用它, 避免冲突)
+                    }
                 } else if (!pose_ok || !ch->pose_running) {
                     ch->pose_fall_count = 0;
                 }

+ 14 - 0
src/pose_detector.cpp

@@ -334,6 +334,8 @@ const char* poseActionName(PoseAction a) {
         case PoseAction::CROUCH:     return "异常蹲坐";
         case PoseAction::RECLINE:    return "半躺";
         case PoseAction::HAND_TO_MOUTH: return "手近嘴部";
+        case PoseAction::SLEEPY:     return "疲劳瞌睡";
+        case PoseAction::STILL:      return "长时间静止";
         default:                     return "无";
     }
 }
@@ -470,5 +472,17 @@ PoseActionResult FallAnalyzer::analyze(const PersonPose& p) const {
         }
     }
 
+    // ===== 6. 疲劳瞌睡: 头部明显下垂(nose 远低于肩部水平线) =====
+    if (sleepy_en_ && vis(0) && has_sh) {
+        float nose_y = p.keypoints[0].y;
+        float head_drop = (nose_y - sh_y) / max(1.0f, (float)b.height);
+        // 正常坐姿 nose 略低于肩(约0.05-0.15); 瞌睡时头下垂 >0.25
+        if (head_drop > 0.25f) {
+            r.action = PoseAction::SLEEPY;
+            r.score = min(1.0f, (head_drop - 0.25f) / 0.25f + 0.5f);
+            return r;
+        }
+    }
+
     return r;
 }

+ 8 - 0
src/pose_detector.h

@@ -49,6 +49,8 @@ enum class PoseAction {
     CROUCH,      // 异常蹲坐
     RECLINE,     // 坐姿腿前伸半躺
     HAND_TO_MOUTH, // 手靠近嘴部(抽烟/打哈欠等)
+    SLEEPY,      // 疲劳瞌睡(头部下垂)
+    STILL,       // 长时间静止
 };
 
 const char* poseActionName(PoseAction a);
@@ -81,10 +83,14 @@ public:
     void setCrouchEnabled(bool e) { crouch_en_ = e; }
     void setReclineEnabled(bool e) { recline_en_ = e; }
     void setHandToMouthEnabled(bool e) { hand_to_mouth_en_ = e; }
+    void setSleepyEnabled(bool e) { sleepy_en_ = e; }
+    void setStillEnabled(bool e) { still_en_ = e; }
     bool handRaiseEnabled() const { return hand_raise_en_; }
     bool crouchEnabled() const { return crouch_en_; }
     bool reclineEnabled() const { return recline_en_; }
     bool handToMouthEnabled() const { return hand_to_mouth_en_; }
+    bool sleepyEnabled() const { return sleepy_en_; }
+    bool stillEnabled() const { return still_en_; }
 
 private:
     float thr_ = 0.5f;
@@ -93,4 +99,6 @@ private:
     bool crouch_en_ = true;
     bool recline_en_ = true;
     bool hand_to_mouth_en_ = true;
+    bool sleepy_en_ = true;
+    bool still_en_ = true;
 };

+ 17 - 1
web/index.html

@@ -261,6 +261,8 @@ border-left:3px solid #e94560;padding:8px 10px;margin-bottom:8px;cursor:pointer}
             <div class="form-group" style="margin:0"><label>跌倒阈值</label><input type="number" id="cfgFallThresh" min="0" max="1" step="0.05" value="0.5"></div>
             <div class="form-group" style="margin:0"><label>连续确认帧数</label><input type="number" id="cfgFallFrames" min="1" max="30" value="3"></div>
             <div class="form-group" style="margin:0"><label>人体最小面积(px²)</label><input type="number" id="cfgMinArea" min="500" max="500000" value="3000" title="低于此面积的人体视为太远, 不判断动作也不显示"></div>
+            <div class="form-group" style="margin:0"><label>静止报警秒数</label><input type="number" id="cfgStillSec" min="3" max="600" value="15"></div>
+            <div class="form-group" style="margin:0"><label>离岗报警秒数</label><input type="number" id="cfgAbsentSec" min="5" max="3600" value="30"></div>
           </div>
           <div class="form-group" style="margin-top:12px">
             <label style="display:flex;align-items:center;gap:5px;cursor:pointer"><input type="checkbox" id="cfgPoseEnabled"> 启用姿态识别</label>
@@ -980,6 +982,9 @@ function poseActionColor(a) {
   if (a === '异常蹲坐') return '#3498db';
   if (a === '半躺') return '#9b59b6';
   if (a === '手近嘴部') return '#e67e22';
+  if (a === '疲劳瞌睡') return '#8e44ad';
+  if (a === '长时间静止') return '#2c3e50';
+  if (a === '疑似离岗') return '#c0392b';
   return '#8892a8';
 }
 async function loadAllAlarms() {
@@ -1117,7 +1122,9 @@ const POSE_ACTIONS = [
   ['举手呼救', '举手呼救'],
   ['异常蹲坐', '异常蹲坐'],
   ['半躺', '半躺'],
-  ['手近嘴部', '手近嘴部']
+  ['手近嘴部', '手近嘴部'],
+  ['疲劳瞌睡', '疲劳瞌睡'],
+  ['长时间静止', '长时间静止']
 ];
 function renderPoseActions() {
   const el = document.getElementById('poseActionList');
@@ -1145,10 +1152,15 @@ async function loadPoseConfig() {
     if (pd.pose_crouch !== false) enabled.push('异常蹲坐');
     if (pd.pose_recline !== false) enabled.push('半躺');
     if (pd.pose_hand_to_mouth !== false) enabled.push('手近嘴部');
+    if (pd.pose_sleepy !== false) enabled.push('疲劳瞌睡');
+    if (pd.pose_still !== false) enabled.push('长时间静止');
     // 跌倒固定启用
     enabled.push('跌倒');
     document.querySelectorAll('.pose-act-ck').forEach(c =>
       c.checked = enabled.includes(c.value));
+    // 静止/离岗秒数
+    if (document.getElementById('cfgStillSec')) document.getElementById('cfgStillSec').value = pd.pose_still_sec || 15;
+    if (document.getElementById('cfgAbsentSec')) document.getElementById('cfgAbsentSec').value = pd.pose_absent_sec || 30;
     // 屏蔽区域
     const ez = pd.pose_exclude_zones || {};
     const zlist = document.getElementById('excludeZoneList');
@@ -1195,6 +1207,10 @@ async function savePoseConfig() {
     pose_crouch: !!checks['异常蹲坐'],
     pose_recline: !!checks['半躺'],
     pose_hand_to_mouth: !!checks['手近嘴部'],
+    pose_sleepy: !!checks['疲劳瞌睡'],
+    pose_still: !!checks['长时间静止'],
+    pose_still_sec: parseInt(document.getElementById('cfgStillSec').value) || 15,
+    pose_absent_sec: parseInt(document.getElementById('cfgAbsentSec').value) || 30,
     pose_min_area: parseInt(document.getElementById('cfgMinArea').value) || 3000,
     pose_exclude_zones: ez
   };