Browse Source

姿态识别新增3个动作: 半躺/抽烟/打哈欠

- RECLINE 坐姿腿前伸半躺: 腿前伸(膝/踝远离髋) + 躯干倾斜
- SMOKING 抽烟: 手腕靠近鼻部(nose)
- YAWN 打哈欠: 手腕靠近嘴 + 头部无前倾(单帧近似)
- 各自独立开关 pose_recline/pose_smoking/pose_yawn, 前端配置页可选
- 报警历史标签颜色: 半躺紫/抽烟橙/打哈欠青
- 实测: 打哈欠(score 0.48) 抽烟(score 0.53) 均能检测
dev 1 tuần trước cách đây
mục cha
commit
a48dd14854
5 tập tin đã thay đổi với 170 bổ sung và 7 xóa
  1. 5 2
      config/config.json
  2. 31 1
      src/main.cpp
  3. 83 0
      src/pose_detector.cpp
  4. 12 0
      src/pose_detector.h
  5. 39 4
      web/index.html

+ 5 - 2
config/config.json

@@ -184,5 +184,8 @@
         "password": "admin",
         "username": "admin"
     },
-    "web_port": 8080
-}
+    "web_port": 8080,
+    "pose_recline": true,
+    "pose_smoking": true,
+    "pose_yawn": true
+}

+ 31 - 1
src/main.cpp

@@ -504,11 +504,20 @@ int main(int argc, char* argv[]) {
     fallAnalyzer.setThreshold(fall_thresh);
     bool pose_hand_raise = cfg.value("pose_hand_raise", true);
     bool pose_crouch = cfg.value("pose_crouch", true);
+    bool pose_recline = cfg.value("pose_recline", true);
+    bool pose_smoking = cfg.value("pose_smoking", true);
+    bool pose_yawn = cfg.value("pose_yawn", true);
     fallAnalyzer.setHandRaiseEnabled(pose_hand_raise);
     fallAnalyzer.setCrouchEnabled(pose_crouch);
+    fallAnalyzer.setReclineEnabled(pose_recline);
+    fallAnalyzer.setSmokingEnabled(pose_smoking);
+    fallAnalyzer.setYawnEnabled(pose_yawn);
     cout << "[Pose] 动作类型: 跌倒/倒地(固定)"
          << " 举手呼救=" << (pose_hand_raise ? "开" : "关")
-         << " 异常蹲坐=" << (pose_crouch ? "开" : "关") << endl;
+         << " 异常蹲坐=" << (pose_crouch ? "开" : "关")
+         << " 半躺=" << (pose_recline ? "开" : "关")
+         << " 抽烟=" << (pose_smoking ? "开" : "关")
+         << " 打哈欠=" << (pose_yawn ? "开" : "关") << endl;
     bool pose_ok = false;
     if (pose_enabled) {
         if (poseDetector.load(pose_model_path, pose_input_size,
@@ -1618,6 +1627,9 @@ int main(int argc, char* argv[]) {
         r["fall_relays"] = fall_relays;
         r["pose_hand_raise"] = pose_hand_raise;
         r["pose_crouch"] = pose_crouch;
+        r["pose_recline"] = pose_recline;
+        r["pose_smoking"] = pose_smoking;
+        r["pose_yawn"] = pose_yawn;
         json chs = json::array();
         {
             lock_guard<mutex> lk(channels_mtx);
@@ -1671,6 +1683,24 @@ int main(int argc, char* argv[]) {
                 fallAnalyzer.setCrouchEnabled(v);
                 full["pose_crouch"] = v;
             }
+            if (j.contains("pose_recline")) {
+                bool v = j["pose_recline"].get<bool>();
+                pose_recline = v;
+                fallAnalyzer.setReclineEnabled(v);
+                full["pose_recline"] = v;
+            }
+            if (j.contains("pose_smoking")) {
+                bool v = j["pose_smoking"].get<bool>();
+                pose_smoking = v;
+                fallAnalyzer.setSmokingEnabled(v);
+                full["pose_smoking"] = v;
+            }
+            if (j.contains("pose_yawn")) {
+                bool v = j["pose_yawn"].get<bool>();
+                pose_yawn = v;
+                fallAnalyzer.setYawnEnabled(v);
+                full["pose_yawn"] = v;
+            }
             if (j.contains("pose_enabled")) {
                 bool v = j["pose_enabled"].get<bool>();
                 pose_enabled = v;

+ 83 - 0
src/pose_detector.cpp

@@ -332,6 +332,9 @@ const char* poseActionName(PoseAction a) {
         case PoseAction::LYING:      return "倒地";
         case PoseAction::HAND_RAISE: return "举手呼救";
         case PoseAction::CROUCH:     return "异常蹲坐";
+        case PoseAction::RECLINE:    return "半躺";
+        case PoseAction::SMOKING:    return "抽烟";
+        case PoseAction::YAWN:       return "打哈欠";
         default:                     return "无";
     }
 }
@@ -411,5 +414,85 @@ PoseActionResult FallAnalyzer::analyze(const PersonPose& p) const {
         }
     }
 
+    // ===== 4. 坐姿腿前伸半躺: 躯干后仰/倾斜 + 腿向前水平伸出 =====
+    if (recline_en_ && has_sh && (vis(11) || vis(12))) {
+        float hip_y = 0;
+        int hn = 0;
+        if (vis(11)) { hip_y += p.keypoints[11].y; hn++; }
+        if (vis(12)) { hip_y += p.keypoints[12].y; hn++; }
+        hip_y /= hn;
+        float body = max(1.0f, (float)b.height);
+        // 躯干倾斜: 肩-髋水平距离 vs 垂直距离, 半躺时水平分量大
+        float torso_dx = 0, torso_dy = 0;
+        if (has_sh) {
+            float sh_x = (vis(5) ? p.keypoints[5].x : p.keypoints[6].x);
+            float hip_x = (vis(11) ? p.keypoints[11].x : p.keypoints[12].x);
+            torso_dx = fabsf(hip_x - sh_x);
+            torso_dy = fabsf(hip_y - sh_y);
+        }
+        // 腿前伸: 膝/踝在髋前方, 且膝-髋水平距离大(相对身体高度)
+        float leg_reach = 0.0f;
+        for (int k = 13; k <= 16; k++) {
+            if (!vis(k)) continue;
+            float hip_x = (vis(11) ? p.keypoints[11].x : p.keypoints[12].x);
+            float dx = fabsf(p.keypoints[k].x - hip_x) / body;
+            leg_reach = max(leg_reach, min(1.0f, dx / 0.3f));
+        }
+        // 躯干后仰: 肩-髋倾斜角
+        float tilt = (torso_dy > 0.01f) ? torso_dx / max(1e-3f, torso_dx + torso_dy) : 0.0f;
+        // 半躺判定: 腿明显前伸 + 躯干有一定倾斜
+        if (leg_reach >= 0.5f && tilt >= 0.30f) {
+            r.action = PoseAction::RECLINE;
+            r.score = min(1.0f, 0.5f * leg_reach + 0.5f * tilt);
+            return r;
+        }
+    }
+
+    // ===== 5. 抽烟: 手腕靠近嘴部(nose) =====
+    if (smoking_en_ && vis(0)) {
+        float nose_x = p.keypoints[0].x, nose_y = p.keypoints[0].y;
+        float best = 0.0f;
+        for (int i = 0; i < 2; i++) {
+            int w = (i == 0) ? 9 : 10;
+            if (!vis(w)) continue;
+            float dx = fabsf(p.keypoints[w].x - nose_x);
+            float dy = fabsf(p.keypoints[w].y - nose_y);
+            float dist = sqrtf(dx * dx + dy * dy) / max(1.0f, (float)b.height);
+            if (dist < 0.25f) {
+                best = max(best, min(1.0f, (0.25f - dist) / 0.15f));
+            }
+        }
+        if (best >= 0.5f) {
+            r.action = PoseAction::SMOKING;
+            r.score = best;
+            return r;
+        }
+    }
+
+    // ===== 6. 打哈欠: 手腕靠近嘴部 + 头部略后仰(近似单帧判定) =====
+    if (yawn_en_ && vis(0)) {
+        float nose_x = p.keypoints[0].x, nose_y = p.keypoints[0].y;
+        float best = 0.0f;
+        for (int i = 0; i < 2; i++) {
+            int w = (i == 0) ? 9 : 10;
+            if (!vis(w)) continue;
+            float dx = fabsf(p.keypoints[w].x - nose_x);
+            float dy = fabsf(p.keypoints[w].y - nose_y);
+            float dist = sqrtf(dx * dx + dy * dy) / max(1.0f, (float)b.height);
+            if (dist < 0.28f) {
+                best = max(best, min(1.0f, (0.28f - dist) / 0.18f));
+            }
+        }
+        // 打哈欠: 手离嘴较近(但比抽烟远一点) + 头后仰(nose 低于肩不多)
+        if (best >= 0.4f && best < 0.85f && has_sh) {
+            float head_rel = (p.keypoints[0].y - sh_y) / max(1.0f, (float)b.height);
+            if (head_rel < 0.20f) {   // 头部没明显前倾
+                r.action = PoseAction::YAWN;
+                r.score = best;
+                return r;
+            }
+        }
+    }
+
     return r;
 }

+ 12 - 0
src/pose_detector.h

@@ -47,6 +47,9 @@ enum class PoseAction {
     LYING,       // 倒地/躺卧
     HAND_RAISE,  // 举手呼救
     CROUCH,      // 异常蹲坐
+    RECLINE,     // 坐姿腿前伸半躺
+    SMOKING,     // 抽烟
+    YAWN,        // 打哈欠
 };
 
 const char* poseActionName(PoseAction a);
@@ -73,11 +76,20 @@ public:
     // 开关
     void setHandRaiseEnabled(bool e) { hand_raise_en_ = e; }
     void setCrouchEnabled(bool e) { crouch_en_ = e; }
+    void setReclineEnabled(bool e) { recline_en_ = e; }
+    void setSmokingEnabled(bool e) { smoking_en_ = e; }
+    void setYawnEnabled(bool e) { yawn_en_ = e; }
     bool handRaiseEnabled() const { return hand_raise_en_; }
     bool crouchEnabled() const { return crouch_en_; }
+    bool reclineEnabled() const { return recline_en_; }
+    bool smokingEnabled() const { return smoking_en_; }
+    bool yawnEnabled() const { return yawn_en_; }
 
 private:
     float thr_ = 0.5f;
     bool hand_raise_en_ = true;
     bool crouch_en_ = true;
+    bool recline_en_ = true;
+    bool smoking_en_ = true;
+    bool yawn_en_ = true;
 };

+ 39 - 4
web/index.html

@@ -61,6 +61,9 @@ h3.sec{color:#e94560;font-size:15px;margin:4px 0 12px}
 .vcell .tag{position:absolute;top:8px;left:8px;background:rgba(14,20,38,.85);border:1px solid #0f3460;color:#ddd;font-size:12px;padding:2px 10px;z-index:3}
 .vcell.offline::after{content:"信号离线";position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#e74c3c;font-size:16px;background:rgba(0,0,0,.55);z-index:2}
 .model-chip{position:absolute;top:10px;right:10px;z-index:6;background:rgba(14,20,38,.85);border:1px solid #0f3460;color:#aaa;font-size:12px;padding:4px 12px}
+#monitorFsBtn{position:absolute;top:10px;right:150px;z-index:6;background:rgba(14,20,38,.85);border:1px solid #0f3460;color:#eee;font-size:16px;line-height:1;padding:6px 12px;cursor:pointer;user-select:none}
+#monitorFsBtn:hover{background:#182242;border-color:#e94560;color:#e94560}
+#monitorFsBtn.on{background:#e94560;color:#fff;border-color:#e94560}
 .info-bar{position:absolute;bottom:10px;left:10px;background:rgba(14,20,38,.9);border:1px solid #0f3460;padding:6px 14px;font-size:12px;z-index:6;color:#aaa}
 .info-bar .val{color:#3498db}
 .empty-tip{height:100%;display:flex;align-items:center;justify-content:center;color:#666;font-size:15px}
@@ -177,6 +180,7 @@ border-left:3px solid #e94560;padding:8px 10px;margin-bottom:8px;cursor:pointer}
     <section id="page-monitor" class="page active">
       <div id="monitorSlot" style="position:absolute;inset:0"></div>
       <div class="model-chip" id="modelChip">RKNN | NPU</div>
+      <div id="monitorFsBtn" title="全屏">⛶</div>
     </section>
 
     <section id="page-fence" class="page">
@@ -728,7 +732,26 @@ function updateView() {
   viewOY = (canvas.height - imgH * viewS) / 2;
   draw();
 }
-window.addEventListener('resize', resizeCanvas);
+ window.addEventListener('resize', resizeCanvas);
+
+/* ================= 全屏监控 ================= */
+function toggleMonitorFullscreen() {
+  const btn = document.getElementById('monitorFsBtn');
+  const el = document.getElementById('page-monitor');
+  if (!document.fullscreenElement) {
+    const r = el.requestFullscreen || el.webkitRequestFullscreen;
+    if (r) { r.call(el); btn.classList.add('on'); }
+  } else {
+    const r = document.exitFullscreen || document.webkitExitFullscreen;
+    if (r) { r.call(document); btn.classList.remove('on'); }
+  }
+}
+document.addEventListener('fullscreenchange', () => {
+  const btn = document.getElementById('monitorFsBtn');
+  if (btn) btn.classList.toggle('on', !!document.fullscreenElement);
+});
+document.getElementById('monitorFsBtn').addEventListener('click', toggleMonitorFullscreen);
+
 
 function startDraw() {
   if (!chById(curChannel)) { alert('暂无通道'); return; }
@@ -947,6 +970,9 @@ function poseActionColor(a) {
   if (a === '跌倒') return '#e94560';
   if (a === '举手呼救') return '#f39c12';
   if (a === '异常蹲坐') return '#3498db';
+  if (a === '半躺') return '#9b59b6';
+  if (a === '抽烟') return '#e67e22';
+  if (a === '打哈欠') return '#16a085';
   return '#8892a8';
 }
 async function loadAllAlarms() {
@@ -1082,7 +1108,10 @@ async function saveConfig() {
 const POSE_ACTIONS = [
   ['跌倒', '跌倒'],
   ['举手呼救', '举手呼救'],
-  ['异常蹲坐', '异常蹲坐']
+  ['异常蹲坐', '异常蹲坐'],
+  ['半躺', '半躺'],
+  ['抽烟', '抽烟'],
+  ['打哈欠', '打哈欠']
 ];
 function renderPoseActions() {
   const el = document.getElementById('poseActionList');
@@ -1102,11 +1131,14 @@ async function loadPoseConfig() {
     if (document.getElementById('cfgPoseEnabled')) document.getElementById('cfgPoseEnabled').checked = d.pose_cfg_enabled !== false;
     if (document.getElementById('cfgFallThresh')) document.getElementById('cfgFallThresh').value = pd.fall_thresh;
     if (document.getElementById('cfgFallFrames')) document.getElementById('cfgFallFrames').value = pd.fall_frames;
-    // 可报警姿态: 固定渲染4类, 由后端开关决定勾选
+    // 可报警姿态: 由后端开关决定勾选
     renderPoseActions();
     const enabled = [];
     if (pd.pose_hand_raise !== false) enabled.push('举手呼救');
     if (pd.pose_crouch !== false) enabled.push('异常蹲坐');
+    if (pd.pose_recline !== false) enabled.push('半躺');
+    if (pd.pose_smoking !== false) enabled.push('抽烟');
+    if (pd.pose_yawn !== false) enabled.push('打哈欠');
     // 跌倒固定启用
     enabled.push('跌倒');
     document.querySelectorAll('.pose-act-ck').forEach(c =>
@@ -1121,7 +1153,10 @@ async function savePoseConfig() {
     fall_thresh: parseFloat(document.getElementById('cfgFallThresh').value),
     fall_frames: parseInt(document.getElementById('cfgFallFrames').value),
     pose_hand_raise: !!checks['举手呼救'],
-    pose_crouch: !!checks['异常蹲坐']
+    pose_crouch: !!checks['异常蹲坐'],
+    pose_recline: !!checks['半躺'],
+    pose_smoking: !!checks['抽烟'],
+    pose_yawn: !!checks['打哈欠']
   };
   try {
     const r = await fetch('/api/pose/config', {method:'POST',