From 4399ecd60122c5110878d1f76a25136e803fb216 Mon Sep 17 00:00:00 2001 From: sladro Date: Tue, 20 Jan 2026 16:59:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- configs/sample_cam1.json | 28 +++-- ..._cam1_strict_minio_alarm_5ch_rtsp_hls.json | 8 +- plugins/alarm/alarm_node.cpp | 101 ++++++++++++++++-- plugins/alarm/rule_engine.cpp | 101 ++++++++++++++++-- plugins/alarm/rule_engine.h | 18 ++++ 5 files changed, 228 insertions(+), 28 deletions(-) diff --git a/configs/sample_cam1.json b/configs/sample_cam1.json index e377a1a..4e570c0 100644 --- a/configs/sample_cam1.json +++ b/configs/sample_cam1.json @@ -41,7 +41,7 @@ "model_path": "./third_party/rknpu2/examples/rknn_yolov5_demo/model/RK3588/yolov5s-640-640.rknn", "model_version": "v5", "num_classes": 80, - "conf": 0.25, + "conf": 0.35, "nms": 0.45, "class_filter": [] }, @@ -51,7 +51,7 @@ "role": "filter", "enable": true, "model_path": "./models/RetinaFace_mobile320.rknn", - "conf": 0.6, + "conf": 0.7, "nms": 0.4, "max_faces": 10, "output_landmarks": true, @@ -100,14 +100,14 @@ "id": "gate_face_cam1", "type": "gate", "role": "filter", - "enable": false, + "enable": true, "state_key": "cam1_sample_full_pipeline", "class_ids": [0], "require_confirmed": true, - "min_count": 1, - "max_age_ms": 1000, - "min_interval_ms": 200, - "min_box_area_ratio": 0.02 + "min_count": 2, + "max_age_ms": 1500, + "min_interval_ms": 300, + "min_box_area_ratio": 0.03 }, { "id": "pre_face_cam1", @@ -175,8 +175,14 @@ "name": "person_in_view", "class_ids": [0], "roi": { "x": 0.0, "y": 0.0, "w": 1.0, "h": 1.0 }, - "min_duration_ms": 0, - "cooldown_ms": 3000 + "min_score": 0.4, + "min_box_area_ratio": 0.02, + "require_track_id": true, + "min_duration_ms": 1500, + "min_hits": 3, + "hit_window_ms": 1500, + "cooldown_ms": 5000, + "per_track_cooldown_ms": 5000 } ], "face_rules": [], @@ -240,8 +246,8 @@ "labels": [], "rules": [], "face_rules": [ - { "name": "unknown_face", "type": "unknown", "cooldown_ms": 5000, "min_sim": 0.0 }, - { "name": "known_person", "type": "person", "cooldown_ms": 5000, "min_sim": 0.5 } + { "name": "unknown_face", "type": "unknown", "cooldown_ms": 7000, "min_sim": 0.35, "min_hits": 2, "hit_window_ms": 1500, "min_face_area_ratio": 0.01, "min_face_aspect": 0.6, "max_face_aspect": 1.6 }, + { "name": "known_person", "type": "person", "cooldown_ms": 7000, "min_sim": 0.6, "min_hits": 2, "hit_window_ms": 1500, "min_face_area_ratio": 0.01, "min_face_aspect": 0.6, "max_face_aspect": 1.6 } ], "actions": { "log": { "enable": false, "level": "info" }, diff --git a/configs/stress_cam1_strict_minio_alarm_5ch_rtsp_hls.json b/configs/stress_cam1_strict_minio_alarm_5ch_rtsp_hls.json index f3286f6..a4a6cc9 100644 --- a/configs/stress_cam1_strict_minio_alarm_5ch_rtsp_hls.json +++ b/configs/stress_cam1_strict_minio_alarm_5ch_rtsp_hls.json @@ -85,8 +85,14 @@ "name": "person_in_view", "class_ids": [0], "roi": { "x": 0.0, "y": 0.0, "w": 1.0, "h": 1.0 }, + "min_score": 0.4, + "min_box_area_ratio": 0.02, + "require_track_id": true, "min_duration_ms": 500, - "cooldown_ms": 15000 + "min_hits": 3, + "hit_window_ms": 1500, + "cooldown_ms": 15000, + "per_track_cooldown_ms": 15000 } ], "actions": { diff --git a/plugins/alarm/alarm_node.cpp b/plugins/alarm/alarm_node.cpp index c2bf3a2..a022093 100644 --- a/plugins/alarm/alarm_node.cpp +++ b/plugins/alarm/alarm_node.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -446,6 +447,12 @@ private: Kind kind = Kind::Unknown; std::vector persons; // used when kind==Person; empty => any known person int cooldown_ms = 5000; + int per_person_cooldown_ms = 0; + int min_hits = 1; + int hit_window_ms = 0; + float min_face_area_ratio = 0.0f; + float min_face_aspect = 0.0f; + float max_face_aspect = 0.0f; float min_sim = 0.0f; }; @@ -462,6 +469,12 @@ private: for (auto& c : type) c = static_cast(std::tolower(static_cast(c))); r.kind = (type == "person") ? FaceRule::Kind::Person : FaceRule::Kind::Unknown; r.cooldown_ms = std::max(0, item.ValueOr("cooldown_ms", r.cooldown_ms)); + r.per_person_cooldown_ms = std::max(0, item.ValueOr("per_person_cooldown_ms", r.per_person_cooldown_ms)); + r.min_hits = std::max(1, item.ValueOr("min_hits", r.min_hits)); + r.hit_window_ms = std::max(0, item.ValueOr("hit_window_ms", r.hit_window_ms)); + r.min_face_area_ratio = item.ValueOr("min_face_area_ratio", static_cast(r.min_face_area_ratio)); + r.min_face_aspect = item.ValueOr("min_face_aspect", static_cast(r.min_face_aspect)); + r.max_face_aspect = item.ValueOr("max_face_aspect", static_cast(r.max_face_aspect)); r.min_sim = item.ValueOr("min_sim", static_cast(r.min_sim)); if (const SimpleJson* persons = item.Find("persons"); persons && persons->IsArray()) { @@ -478,6 +491,8 @@ private: void ParseFaceRules(const SimpleJson& config) { face_rules_ = ParseFaceRulesFromConfig(config); face_last_trigger_.clear(); + face_person_last_trigger_.clear(); + face_vote_history_.clear(); } void EvaluateFaceRulesLocked(const FramePtr& frame) { @@ -485,6 +500,9 @@ private: if (!frame || !frame->face_recog || frame->face_recog->items.empty()) return; const auto now = std::chrono::steady_clock::now(); + const int img_w = frame->face_recog->img_w > 0 ? frame->face_recog->img_w : frame->width; + const int img_h = frame->face_recog->img_h > 0 ? frame->face_recog->img_h : frame->height; + const double img_area = (img_w > 0 && img_h > 0) ? static_cast(img_w) * static_cast(img_h) : 0.0; for (const auto& rule : face_rules_) { bool matched = false; std::string matched_name; @@ -492,10 +510,21 @@ private: dets.reserve(3); for (const auto& it : frame->face_recog->items) { + if (rule.min_face_area_ratio > 0.0f && img_area > 0.0) { + const double a = static_cast(it.bbox.w) * static_cast(it.bbox.h); + const double r = a / img_area; + if (r < static_cast(rule.min_face_area_ratio)) continue; + } + if (rule.min_face_aspect > 0.0f || rule.max_face_aspect > 0.0f) { + const double h = std::max(1e-6, static_cast(it.bbox.h)); + const double aspect = static_cast(it.bbox.w) / h; + if (rule.min_face_aspect > 0.0f && aspect < static_cast(rule.min_face_aspect)) continue; + if (rule.max_face_aspect > 0.0f && aspect > static_cast(rule.max_face_aspect)) continue; + } + if (rule.kind == FaceRule::Kind::Unknown) { if (!it.unknown) continue; if (it.best_sim < rule.min_sim) continue; - matched = true; } else { if (it.unknown) continue; if (it.best_sim < rule.min_sim) continue; @@ -509,10 +538,26 @@ private: } if (!ok) continue; } - matched = true; - matched_name = it.best_name; } + const std::string key = BuildFaceVoteKey(rule, it); + if (rule.per_person_cooldown_ms > 0) { + auto it_last = face_person_last_trigger_.find(key); + if (it_last != face_person_last_trigger_.end()) { + const auto elapsed = std::chrono::duration_cast(now - it_last->second).count(); + if (elapsed < rule.per_person_cooldown_ms) { + continue; + } + } + } + + if (!CheckFaceVote(rule, key, now)) { + continue; + } + + matched = true; + if (rule.kind == FaceRule::Kind::Person) matched_name = it.best_name; + Detection d; d.cls_id = -1; d.score = it.best_sim; @@ -524,14 +569,21 @@ private: if (!matched) continue; - const auto it_last = face_last_trigger_.find(rule.name); - if (it_last != face_last_trigger_.end() && rule.cooldown_ms > 0) { - const auto elapsed = std::chrono::duration_cast(now - it_last->second).count(); - if (elapsed < rule.cooldown_ms) { - continue; + if (rule.per_person_cooldown_ms > 0) { + for (const auto& d : dets) { + const std::string key = BuildFaceVoteKey(rule, d.track_id, matched_name); + face_person_last_trigger_[key] = now; } + } else { + const auto it_last = face_last_trigger_.find(rule.name); + if (it_last != face_last_trigger_.end() && rule.cooldown_ms > 0) { + const auto elapsed = std::chrono::duration_cast(now - it_last->second).count(); + if (elapsed < rule.cooldown_ms) { + continue; + } + } + face_last_trigger_[rule.name] = now; } - face_last_trigger_[rule.name] = now; RuleMatchResult fake; fake.matched = true; @@ -543,6 +595,35 @@ private: TriggerAlarm(fake, frame); } } + + static std::string BuildFaceVoteKey(const FaceRule& rule, const FaceRecogItem& item) { + if (rule.kind == FaceRule::Kind::Person) { + if (item.best_person_id >= 0) { + return rule.name + "#" + std::to_string(item.best_person_id); + } + return rule.name + "#" + item.best_name; + } + return rule.name + "#unknown"; + } + + static std::string BuildFaceVoteKey(const FaceRule& rule, int person_id, const std::string& name) { + if (rule.kind == FaceRule::Kind::Person) { + if (person_id >= 0) return rule.name + "#" + std::to_string(person_id); + return rule.name + "#" + name; + } + return rule.name + "#unknown"; + } + + bool CheckFaceVote(const FaceRule& rule, const std::string& key, const std::chrono::steady_clock::time_point& now) { + if (rule.min_hits <= 1 || rule.hit_window_ms <= 0) return true; + auto& dq = face_vote_history_[key]; + const auto window = std::chrono::milliseconds(rule.hit_window_ms); + while (!dq.empty() && (now - dq.front()) > window) { + dq.pop_front(); + } + dq.push_back(now); + return static_cast(dq.size()) >= rule.min_hits; + } void WorkerLoop() { while (worker_running_.load()) { AlarmJob job; @@ -586,6 +667,8 @@ private: RuleEngine rule_engine_; std::vector face_rules_; std::map face_last_trigger_; + std::map face_person_last_trigger_; + std::map> face_vote_history_; std::shared_ptr packet_buffer_; std::vector> actions_; diff --git a/plugins/alarm/rule_engine.cpp b/plugins/alarm/rule_engine.cpp index 2b7b591..7147148 100644 --- a/plugins/alarm/rule_engine.cpp +++ b/plugins/alarm/rule_engine.cpp @@ -43,8 +43,14 @@ bool RuleEngine::Init(const SimpleJson& rules_config, const std::vector("name", "unnamed_rule"); + rule.min_score = rule_json.ValueOr("min_score", 0.0f); + rule.min_box_area_ratio = rule_json.ValueOr("min_box_area_ratio", 0.0f); + rule.require_track_id = rule_json.ValueOr("require_track_id", false); rule.min_duration_ms = rule_json.ValueOr("min_duration_ms", 0); + rule.min_hits = rule_json.ValueOr("min_hits", 1); + rule.hit_window_ms = rule_json.ValueOr("hit_window_ms", 0); rule.cooldown_ms = rule_json.ValueOr("cooldown_ms", 5000); + rule.per_track_cooldown_ms = rule_json.ValueOr("per_track_cooldown_ms", 0); rule.schedule = rule_json.ValueOr("schedule", ""); // Parse class_ids @@ -81,7 +87,12 @@ bool RuleEngine::Init(const SimpleJson& rules_config, const std::vector& frame) { continue; } - // Check cooldown - if (!CheckCooldown(rule.name)) { - continue; + if (rule.per_track_cooldown_ms <= 0) { + // Check rule-level cooldown + if (!CheckCooldown(rule.name)) { + continue; + } } // Find matching detections @@ -124,17 +137,45 @@ RuleMatchResult RuleEngine::Evaluate(const std::shared_ptr& frame) { continue; } + if (!PassQuality(rule, det, static_cast(img_w) * static_cast(img_h))) { + continue; + } + matched.push_back(det); } - bool currently_matched = !matched.empty(); + if (matched.empty()) { + CheckDuration(rule.name, false); + continue; + } - // Check duration + std::vector qualified; + qualified.reserve(matched.size()); + const auto now = std::chrono::steady_clock::now(); + for (const auto& det : matched) { + if (rule.per_track_cooldown_ms > 0) { + if (!CheckPerTrackCooldown(rule.name, det.track_id, rule.per_track_cooldown_ms)) { + continue; + } + } + if (!CheckVote(rule.name, det.track_id, rule.min_hits, rule.hit_window_ms)) { + continue; + } + qualified.push_back(det); + } + + const bool currently_matched = !qualified.empty(); if (CheckDuration(rule.name, currently_matched)) { result.matched = true; result.rule_name = rule.name; - result.matched_detections = matched; - TriggerCooldown(rule.name); + result.matched_detections = qualified; + if (rule.per_track_cooldown_ms > 0) { + for (const auto& det : qualified) { + if (det.track_id >= 0) TriggerPerTrackCooldown(rule.name, det.track_id); + } + } else { + TriggerCooldown(rule.name); + } duration_start_.erase(rule.name); return result; } @@ -146,6 +187,8 @@ RuleMatchResult RuleEngine::Evaluate(const std::shared_ptr& frame) { void RuleEngine::Reset() { duration_start_.clear(); last_trigger_.clear(); + per_track_last_trigger_.clear(); + vote_history_.clear(); } bool RuleEngine::IsInRoi(const Rect& bbox, const RoiRect& roi, int img_w, int img_h) const { @@ -231,4 +274,48 @@ void RuleEngine::TriggerCooldown(const std::string& rule_name) { last_trigger_[rule_name] = std::chrono::steady_clock::now(); } +bool RuleEngine::CheckVote(const std::string& rule_name, int track_id, int min_hits, int hit_window_ms) { + if (min_hits <= 1 || hit_window_ms <= 0) return true; + const auto now = std::chrono::steady_clock::now(); + const std::string key = MakeKey(rule_name, track_id); + auto& dq = vote_history_[key]; + const auto window = std::chrono::milliseconds(hit_window_ms); + while (!dq.empty() && (now - dq.front()) > window) { + dq.pop_front(); + } + dq.push_back(now); + return static_cast(dq.size()) >= min_hits; +} + +bool RuleEngine::CheckPerTrackCooldown(const std::string& rule_name, int track_id, int per_track_ms) { + if (per_track_ms <= 0) return true; + if (track_id < 0) return true; + const std::string key = MakeKey(rule_name, track_id); + auto it = per_track_last_trigger_.find(key); + if (it == per_track_last_trigger_.end()) return true; + const auto now = std::chrono::steady_clock::now(); + const auto elapsed = std::chrono::duration_cast(now - it->second).count(); + return elapsed >= per_track_ms; +} + +void RuleEngine::TriggerPerTrackCooldown(const std::string& rule_name, int track_id) { + if (track_id < 0) return; + per_track_last_trigger_[MakeKey(rule_name, track_id)] = std::chrono::steady_clock::now(); +} + +bool RuleEngine::PassQuality(const AlarmRule& rule, const Detection& det, double img_area) const { + if (rule.require_track_id && det.track_id < 0) return false; + if (rule.min_score > 0.0f && det.score < rule.min_score) return false; + if (rule.min_box_area_ratio > 0.0f && img_area > 0.0) { + const double a = static_cast(det.bbox.w) * static_cast(det.bbox.h); + const double r = a / img_area; + if (r < static_cast(rule.min_box_area_ratio)) return false; + } + return true; +} + +std::string RuleEngine::MakeKey(const std::string& rule_name, int track_id) { + return rule_name + "#" + std::to_string(track_id); +} + } // namespace rk3588 diff --git a/plugins/alarm/rule_engine.h b/plugins/alarm/rule_engine.h index a84da34..b9f46ea 100644 --- a/plugins/alarm/rule_engine.h +++ b/plugins/alarm/rule_engine.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -22,8 +23,14 @@ struct AlarmRule { std::string name; std::set class_ids; RoiRect roi; + float min_score = 0.0f; + float min_box_area_ratio = 0.0f; + bool require_track_id = false; int min_duration_ms = 0; + int min_hits = 1; + int hit_window_ms = 0; int cooldown_ms = 5000; + int per_track_cooldown_ms = 0; std::string schedule; // "HH:MM-HH:MM" format, empty = always active }; @@ -45,6 +52,11 @@ private: bool CheckDuration(const std::string& rule_name, bool currently_matched); bool CheckCooldown(const std::string& rule_name); void TriggerCooldown(const std::string& rule_name); + bool CheckVote(const std::string& rule_name, int track_id, int min_hits, int hit_window_ms); + bool CheckPerTrackCooldown(const std::string& rule_name, int track_id, int per_track_ms); + void TriggerPerTrackCooldown(const std::string& rule_name, int track_id); + bool PassQuality(const AlarmRule& rule, const Detection& det, double img_area) const; + static std::string MakeKey(const std::string& rule_name, int track_id); std::vector rules_; std::vector labels_; @@ -54,6 +66,12 @@ private: // Cooldown tracking: rule_name -> last trigger time std::map last_trigger_; + + // Per-track cooldown tracking: rule_name#track_id -> last trigger time + std::map per_track_last_trigger_; + + // Vote history: rule_name#track_id -> timestamps within window + std::map> vote_history_; }; } // namespace rk3588