2 Commits e4b70558d2 ... 17194d2578

Autor SHA1 Mensagem Data
  dev 17194d2578 姿态识别配置页新增屏蔽区域编辑 + Web API há 1 semana atrás
  dev c7a009bf6b 姿态识别: 最小面积过滤 + 屏蔽区域排除误识别 há 1 semana atrás
5 ficheiros alterados com 158 adições e 24 exclusões
  1. 12 2
      config/config.json
  2. 96 13
      src/main.cpp
  3. 2 2
      src/pose_detector.cpp
  4. 4 4
      src/pose_detector.h
  5. 44 3
      web/index.html

+ 12 - 2
config/config.json

@@ -74,10 +74,20 @@
     "pose_core_mask": 1,
     "pose_crouch": true,
     "pose_enabled": true,
+    "pose_exclude_zones": {
+        "0": [
+            [
+                0,
+                100,
+                160,
+                450
+            ]
+        ]
+    },
     "pose_hand_raise": true,
     "pose_hand_to_mouth": true,
     "pose_input_size": 640,
-    "pose_min_body_h": 60.0,
+    "pose_min_area": 3000,
     "pose_model_path": "models/yolov8n-pose_rk3588_fp.rknn",
     "pose_nms_thresh": 0.45,
     "pose_recline": true,
@@ -188,4 +198,4 @@
         "username": "admin"
     },
     "web_port": 8080
-}
+}

+ 96 - 13
src/main.cpp

@@ -506,18 +506,36 @@ int main(int argc, char* argv[]) {
     bool pose_crouch = cfg.value("pose_crouch", true);
     bool pose_recline = cfg.value("pose_recline", true);
     bool pose_hand_to_mouth = cfg.value("pose_hand_to_mouth", true);
-    float pose_min_body_h = cfg.value("pose_min_body_h", 60.0f);
+    float pose_min_area = cfg.value("pose_min_area", 3000.0f);
+    // 姿态检测屏蔽区域: {"通道id": [[x1,y1,x2,y2], ...]} 相对原始分辨率
+    map<int, vector<cv::Rect>> pose_exclude_zones;
+    if (cfg.contains("pose_exclude_zones") && cfg["pose_exclude_zones"].is_object()) {
+        for (auto& [k, v] : cfg["pose_exclude_zones"].items()) {
+            int cid = atoi(k.c_str());
+            if (!v.is_array()) continue;
+            vector<cv::Rect> zones;
+            for (auto& z : v) {
+                if (z.is_array() && z.size() >= 4) {
+                    zones.push_back(cv::Rect(z[0].get<int>(), z[1].get<int>(),
+                                             z[2].get<int>() - z[0].get<int>(),
+                                             z[3].get<int>() - z[1].get<int>()));
+                }
+            }
+            if (!zones.empty()) pose_exclude_zones[cid] = zones;
+        }
+    }
     fallAnalyzer.setHandRaiseEnabled(pose_hand_raise);
     fallAnalyzer.setCrouchEnabled(pose_crouch);
     fallAnalyzer.setReclineEnabled(pose_recline);
     fallAnalyzer.setHandToMouthEnabled(pose_hand_to_mouth);
-    fallAnalyzer.setMinBodyHeight(pose_min_body_h);
+    fallAnalyzer.setMinBodyArea(pose_min_area);
     cout << "[Pose] 动作类型: 跌倒/倒地(固定)"
          << " 举手呼救=" << (pose_hand_raise ? "开" : "关")
          << " 异常蹲坐=" << (pose_crouch ? "开" : "关")
          << " 半躺=" << (pose_recline ? "开" : "关")
          << " 手近嘴部=" << (pose_hand_to_mouth ? "开" : "关")
-         << " 距离阈值=" << pose_min_body_h << "px" << endl;
+         << " 最小面积=" << pose_min_area << "px"
+         << " 屏蔽区=" << pose_exclude_zones.size() << "路" << endl;
     bool pose_ok = false;
     if (pose_enabled) {
         if (poseDetector.load(pose_model_path, pose_input_size,
@@ -1629,7 +1647,17 @@ int main(int argc, char* argv[]) {
         r["pose_crouch"] = pose_crouch;
         r["pose_recline"] = pose_recline;
         r["pose_hand_to_mouth"] = pose_hand_to_mouth;
-        r["pose_min_body_h"] = pose_min_body_h;
+        r["pose_min_area"] = pose_min_area;
+        {
+            json zj = json::object();
+            for (auto& [cid, zones] : pose_exclude_zones) {
+                json arr = json::array();
+                for (auto& z : zones)
+                    arr.push_back({z.x, z.y, z.x + z.width, z.y + z.height});
+                zj[to_string(cid)] = arr;
+            }
+            r["pose_exclude_zones"] = zj;
+        }
         json chs = json::array();
         {
             lock_guard<mutex> lk(channels_mtx);
@@ -1695,12 +1723,12 @@ int main(int argc, char* argv[]) {
                 fallAnalyzer.setHandToMouthEnabled(v);
                 full["pose_hand_to_mouth"] = v;
             }
-            if (j.contains("pose_min_body_h")) {
-                float v = j["pose_min_body_h"].get<float>();
-                if (v >= 20.0f && v <= 500.0f) {
-                    pose_min_body_h = v;
-                    fallAnalyzer.setMinBodyHeight(v);
-                    full["pose_min_body_h"] = v;
+            if (j.contains("pose_min_area")) {
+                float v = j["pose_min_area"].get<float>();
+                if (v >= 500.0f && v <= 500000.0f) {
+                    pose_min_area = v;
+                    fallAnalyzer.setMinBodyArea(v);
+                    full["pose_min_area"] = v;
                 }
             }
             if (j.contains("pose_enabled")) {
@@ -1721,6 +1749,24 @@ int main(int argc, char* argv[]) {
                         }
                 }
             }
+            if (j.contains("pose_exclude_zones") && j["pose_exclude_zones"].is_object()) {
+                pose_exclude_zones.clear();
+                for (auto& [k, v] : j["pose_exclude_zones"].items()) {
+                    int cid = atoi(k.c_str());
+                    if (!v.is_array()) continue;
+                    vector<cv::Rect> zones;
+                    for (auto& z : v) {
+                        if (z.is_array() && z.size() >= 4) {
+                            int x1 = z[0].get<int>(), y1 = z[1].get<int>();
+                            int x2 = z[2].get<int>(), y2 = z[3].get<int>();
+                            if (x2 > x1 && y2 > y1)
+                                zones.push_back(cv::Rect(x1, y1, x2 - x1, y2 - y1));
+                        }
+                    }
+                    if (!zones.empty()) pose_exclude_zones[cid] = zones;
+                }
+                full["pose_exclude_zones"] = j["pose_exclude_zones"];
+            }
             saveJson(config_path, full);
             cout << "[Pose] 配置已更新: 阈值=" << fall_thresh
                  << " 连续" << fall_frames << "帧" << endl;
@@ -2004,9 +2050,22 @@ int main(int argc, char* argv[]) {
                 if (pose_ok && ch->pose_running) {
                     vector<PersonPose> falls;
                     // 对每个人做多动作分析, 取最高分动作
+                    // 先过滤屏蔽区域内的人体(地图等误识别)
+                    auto zit = pose_exclude_zones.find(ch->id);
+                    vector<PersonPose> valid_poses;
+                    for (auto& p : poses) {
+                        bool excluded = false;
+                        if (zit != pose_exclude_zones.end()) {
+                            cv::Point center(p.bbox.x + p.bbox.width / 2,
+                                             p.bbox.y + p.bbox.height / 2);
+                            for (auto& z : zit->second)
+                                if (z.contains(center)) { excluded = true; break; }
+                        }
+                        if (!excluded) valid_poses.push_back(p);
+                    }
                     PoseAction bestAction = PoseAction::NONE;
                     float bestScore = 0.0f;
-                    for (auto& p : poses) {
+                    for (auto& p : valid_poses) {
                         if (pose_alarm_classes.empty() ||
                             find(pose_alarm_classes.begin(),
                                  pose_alarm_classes.end(),
@@ -2025,10 +2084,34 @@ int main(int argc, char* argv[]) {
                     if (hit) ch->pose_fall_count++;
                     else ch->pose_fall_count = 0;
                     bool pose_rising = (ch->pose_fall_count == fall_frames);
+                    if (pose_rising && !poses.empty()) {
+                        // 诊断: 打印命中人体面积
+                        cout << "[Pose] CH" << ch->id << " 命中" << poseActionName(bestAction)
+                             << " 面积=" << (poses[0].bbox.width * poses[0].bbox.height)
+                             << " bbox=" << poses[0].bbox << endl;
+                    }
                     {
                         lock_guard<mutex> lk(ch->mtx);
-                        ch->poses = poses;
-                        ch->fall_poses = hit ? poses : vector<PersonPose>{};
+                        // 面积过小(太远)的人体不显示; 屏蔽区域内的人体也不显示
+                        vector<PersonPose> vis_poses;
+                        auto zit = pose_exclude_zones.find(ch->id);
+                        for (auto& p : poses) {
+                            float area = (float)p.bbox.width * p.bbox.height;
+                            if (area < pose_min_area) continue;
+                            // 屏蔽区判定: bbox中心落入任一排除矩形则忽略
+                            bool excluded = false;
+                            if (zit != pose_exclude_zones.end()) {
+                                cv::Point center(p.bbox.x + p.bbox.width / 2,
+                                                 p.bbox.y + p.bbox.height / 2);
+                                for (auto& z : zit->second) {
+                                    if (z.contains(center)) { excluded = true; break; }
+                                }
+                            }
+                            if (excluded) continue;
+                            vis_poses.push_back(p);
+                        }
+                        ch->poses = vis_poses;
+                        ch->fall_poses = hit ? vis_poses : vector<PersonPose>{};
                         ch->pose_action = hit ? poseActionName(bestAction) : "";
                         ch->pose_score = bestScore;
                     }

+ 2 - 2
src/pose_detector.cpp

@@ -348,8 +348,8 @@ PoseActionResult FallAnalyzer::analyze(const PersonPose& p) const {
     const auto& b = p.bbox;
     float bw = (float)b.width, bh = (float)b.height;
     if (bw < 1.0f || bh < 1.0f) return r;
-    // 距离阈值: 人体包围盒高度低于阈值视为太远, 不做动作判定
-    if (bh < min_body_h_) return r;
+    // 距离阈值: 人体包围盒面积低于阈值视为太远, 不做动作判定
+    if (bw * bh < min_body_area_) return r;
 
     // ===== 1. 跌倒/倒地 =====
     float fall = score(p);

+ 4 - 4
src/pose_detector.h

@@ -72,9 +72,9 @@ public:
     // 多动作分析: 返回最匹配的动作及得分
     PoseActionResult analyze(const PersonPose& p) const;
 
-    // 人体大小阈值(像素): 包围盒高度低于 min_h 视为太远, 不做动作判定
-    void setMinBodyHeight(float h) { min_body_h_ = h; }
-    float minBodyHeight() const { return min_body_h_; }
+    // 人体面积阈值(像素): 包围盒面积低于 min_area 视为太远, 不做动作判定
+    void setMinBodyArea(float a) { min_body_area_ = a; }
+    float minBodyArea() const { return min_body_area_; }
 
     // 开关
     void setHandRaiseEnabled(bool e) { hand_raise_en_ = e; }
@@ -88,7 +88,7 @@ public:
 
 private:
     float thr_ = 0.5f;
-    float min_body_h_ = 60.0f;   // 默认: 人体包围盒高度<60px 判定太远
+    float min_body_area_ = 3000.0f;  // 默认: 人体包围盒面积<3000px 判定太远
     bool hand_raise_en_ = true;
     bool crouch_en_ = true;
     bool recline_en_ = true;

+ 44 - 3
web/index.html

@@ -260,7 +260,7 @@ border-left:3px solid #e94560;padding:8px 10px;margin-bottom:8px;cursor:pointer}
           <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
             <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="cfgMinBodyH" min="20" max="500" value="60" title="低于此高度的人体视为太远, 不判断动作"></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>
           <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>
@@ -274,6 +274,13 @@ border-left:3px solid #e94560;padding:8px 10px;margin-bottom:8px;cursor:pointer}
             <div id="poseActionList" style="display:grid;grid-template-columns:1fr 1fr;gap:8px 16px;font-size:13px"></div>
           </div>
         </div>
+        <div class="card">
+          <h3>屏蔽区域</h3>
+          <div class="form-group">
+            <label>每路排除误识别区域, 格式: x1,y1,x2,y2 (多个用;分隔, 不同路用行分隔)</label>
+            <div id="excludeZoneList"></div>
+          </div>
+        </div>
       </div>
     </section>
 
@@ -1130,7 +1137,7 @@ 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;
-    if (document.getElementById('cfgMinBodyH')) document.getElementById('cfgMinBodyH').value = pd.pose_min_body_h || 60;
+    if (document.getElementById('cfgMinArea')) document.getElementById('cfgMinArea').value = pd.pose_min_area || 3000;
     // 可报警姿态: 由后端开关决定勾选
     renderPoseActions();
     const enabled = [];
@@ -1142,11 +1149,44 @@ async function loadPoseConfig() {
     enabled.push('跌倒');
     document.querySelectorAll('.pose-act-ck').forEach(c =>
       c.checked = enabled.includes(c.value));
+    // 屏蔽区域
+    const ez = pd.pose_exclude_zones || {};
+    const zlist = document.getElementById('excludeZoneList');
+    if (zlist) {
+      const rows = Object.keys(ez).map(cid => {
+        const zones = ez[cid] || [];
+        const txt = zones.map(z => z.join(',')).join(';');
+        return `<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px">
+          <span style="width:50px;font-size:12px;color:#888">CH${cid}</span>
+          <input type="text" class="exclude-zone" data-ch="${cid}" value="${txt}" placeholder="x1,y1,x2,y2;x1,y1,x2,y2" style="flex:1;padding:6px;background:#0e1426;border:1px solid #0f3460;color:#eee;font-size:13px">
+        </div>`;
+      }).join('');
+      // 补充未配置的通道(最多8路)
+      const chs = (window.CHANNELS || []).map(c => String(c.id));
+      const extra = chs.filter(cid => !ez[cid]).map(cid =>
+        `<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px">
+          <span style="width:50px;font-size:12px;color:#888">CH${cid}</span>
+          <input type="text" class="exclude-zone" data-ch="${cid}" value="" placeholder="x1,y1,x2,y2;x1,y1,x2,y2" style="flex:1;padding:6px;background:#0e1426;border:1px solid #0f3460;color:#eee;font-size:13px">
+        </div>`).join('');
+      zlist.innerHTML = rows + extra;
+    }
   } catch (e) {}
 }
 async function savePoseConfig() {
   const checks = {};
   document.querySelectorAll('.pose-act-ck').forEach(c => checks[c.value] = c.checked);
+  // 收集屏蔽区域
+  const ez = {};
+  document.querySelectorAll('.exclude-zone').forEach(inp => {
+    const txt = inp.value.trim();
+    if (!txt) return;
+    const zones = [];
+    txt.split(';').forEach(part => {
+      const n = part.split(',').map(x => parseInt(x.trim())).filter(x => !isNaN(x));
+      if (n.length === 4 && n[2] > n[0] && n[3] > n[1]) zones.push(n);
+    });
+    if (zones.length) ez[inp.dataset.ch] = zones;
+  });
   const body = {
     pose_enabled: document.getElementById('cfgPoseEnabled').checked,
     fall_thresh: parseFloat(document.getElementById('cfgFallThresh').value),
@@ -1155,7 +1195,8 @@ async function savePoseConfig() {
     pose_crouch: !!checks['异常蹲坐'],
     pose_recline: !!checks['半躺'],
     pose_hand_to_mouth: !!checks['手近嘴部'],
-    pose_min_body_h: parseInt(document.getElementById('cfgMinBodyH').value) || 60
+    pose_min_area: parseInt(document.getElementById('cfgMinArea').value) || 3000,
+    pose_exclude_zones: ez
   };
   try {
     const r = await fetch('/api/pose/config', {method:'POST',