From afd428f77c4c36fe8f3adc4e9d674074cd0e0283 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Wed, 15 Apr 2026 15:15:43 +0800 Subject: [PATCH] Fix face track alarm aggregation ordering --- plugins/alarm/alarm_node.cpp | 82 ++++++++++++++++++------ tests/test_face_track_alarm.cpp | 108 ++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+), 19 deletions(-) diff --git a/plugins/alarm/alarm_node.cpp b/plugins/alarm/alarm_node.cpp index fcd37ad..b8ec961 100644 --- a/plugins/alarm/alarm_node.cpp +++ b/plugins/alarm/alarm_node.cpp @@ -36,6 +36,7 @@ struct FaceTrackAggregationConfig { int known_hit_window_ms = 3000; int unknown_min_track_age_ms = 2000; int unknown_min_quality_hits = 4; + int state_expire_ms = 5000; }; struct FaceTrackState { @@ -55,6 +56,13 @@ struct FaceTrackDecision { bool trigger_unknown = false; }; +void ResetFaceTrackState(FaceTrackState& state, int track_id, uint64_t now_ms) { + state = FaceTrackState{}; + state.track_id = track_id; + state.first_seen_ms = now_ms; + state.last_seen_ms = now_ms; +} + FaceTrackAggregationConfig ParseFaceTrackAggregationConfig(const SimpleJson& config) { FaceTrackAggregationConfig cfg; const SimpleJson* agg = config.Find("face_track_aggregation"); @@ -66,6 +74,7 @@ FaceTrackAggregationConfig ParseFaceTrackAggregationConfig(const SimpleJson& con cfg.known_hit_window_ms = std::max(0, agg->ValueOr("known_hit_window_ms", cfg.known_hit_window_ms)); cfg.unknown_min_track_age_ms = std::max(0, agg->ValueOr("unknown_min_track_age_ms", cfg.unknown_min_track_age_ms)); cfg.unknown_min_quality_hits = std::max(1, agg->ValueOr("unknown_min_quality_hits", cfg.unknown_min_quality_hits)); + cfg.state_expire_ms = std::max(0, agg->ValueOr("state_expire_ms", cfg.state_expire_ms)); if (const SimpleJson* known = agg->Find("known"); known && known->IsObject()) { cfg.known_min_hits = std::max(1, known->ValueOr("min_hits", cfg.known_min_hits)); @@ -85,26 +94,32 @@ FaceTrackDecision UpdateFaceTrackState( const FaceRecogItem& item, uint64_t now_ms) { FaceTrackDecision decision; - if (cfg.require_person_track && item.person_track_id < 0) return decision; if (item.person_track_id < 0) return decision; - if (state.track_id != item.person_track_id) { - state = FaceTrackState{}; - state.track_id = item.person_track_id; + const uint64_t expire_ms = static_cast(std::max(0, cfg.state_expire_ms)); + const bool expired = + expire_ms > 0 && + state.last_seen_ms > 0 && + now_ms > state.last_seen_ms && + (now_ms - state.last_seen_ms) > expire_ms; + if (state.track_id != item.person_track_id || expired) { + ResetFaceTrackState(state, item.person_track_id, now_ms); + } else if (state.first_seen_ms == 0) { + state.first_seen_ms = now_ms; + state.last_seen_ms = now_ms; + } else { + state.last_seen_ms = now_ms; } - if (state.first_seen_ms == 0) state.first_seen_ms = now_ms; - state.last_seen_ms = now_ms; - const auto known_window_ms = static_cast(std::max(0, cfg.known_hit_window_ms)); if (!item.unknown) { const bool same_identity = state.best_known_person_id == item.best_person_id && state.best_known_name == item.best_name; if (!same_identity) { + ResetFaceTrackState(state, item.person_track_id, now_ms); state.best_known_person_id = item.best_person_id; state.best_known_name = item.best_name; - state.known_hit_times.clear(); } if (known_window_ms > 0) { @@ -634,17 +649,33 @@ private: 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; - std::vector track_decisions(frame->face_recog->items.size()); - if (track_agg_cfg_.enable) { - for (size_t i = 0; i < frame->face_recog->items.size(); ++i) { - const auto& item = frame->face_recog->items[i]; - if (item.person_track_id < 0) continue; - auto& state = face_track_states_[item.person_track_id]; - track_decisions[i] = UpdateFaceTrackState(track_agg_cfg_, state, item, now_epoch_ms); + if (track_agg_cfg_.enable && track_agg_cfg_.state_expire_ms > 0) { + const uint64_t expire_ms = static_cast(track_agg_cfg_.state_expire_ms); + for (auto it = face_track_states_.begin(); it != face_track_states_.end();) { + const auto& state = it->second; + const bool expired = + state.last_seen_ms > 0 && + now_epoch_ms > state.last_seen_ms && + (now_epoch_ms - state.last_seen_ms) > expire_ms; + if (expired) { + it = face_track_states_.erase(it); + } else { + ++it; + } } } for (const auto& rule : face_rules_) { + if (rule.per_person_cooldown_ms <= 0) { + 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; + } + } + } + bool matched = false; std::string matched_name; std::vector dets; @@ -686,7 +717,15 @@ private: } } - const std::string key = BuildFaceVoteKey(rule, it); + const bool can_use_track_aggregation = + track_agg_cfg_.enable && it.person_track_id >= 0; + if (track_agg_cfg_.enable && track_agg_cfg_.require_person_track && it.person_track_id < 0) { + continue; + } + + const std::string key = can_use_track_aggregation + ? BuildTrackAwareFaceVoteKey(rule, it) + : 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()) { @@ -697,8 +736,9 @@ private: } } - if (track_agg_cfg_.enable) { - const FaceTrackDecision& decision = track_decisions[i]; + if (can_use_track_aggregation) { + auto& state = face_track_states_[it.person_track_id]; + const FaceTrackDecision decision = UpdateFaceTrackState(track_agg_cfg_, state, it, now_epoch_ms); if (rule.kind == FaceRule::Kind::Person && !decision.trigger_known) continue; if (rule.kind == FaceRule::Kind::Unknown && !decision.trigger_unknown) continue; } else { @@ -753,10 +793,14 @@ private: } } - static std::string BuildFaceVoteKey(const FaceRule& rule, const FaceRecogItem& item) { + static std::string BuildTrackAwareFaceVoteKey(const FaceRule& rule, const FaceRecogItem& item) { if (item.person_track_id >= 0) { return rule.name + "#track:" + std::to_string(item.person_track_id); } + return BuildFaceVoteKey(rule, item); + } + + 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); diff --git a/tests/test_face_track_alarm.cpp b/tests/test_face_track_alarm.cpp index 54db1ba..d19ece9 100644 --- a/tests/test_face_track_alarm.cpp +++ b/tests/test_face_track_alarm.cpp @@ -1,9 +1,13 @@ #include #include +#include +#include #include "node.h" +#include "utils/simple_json.h" +#define private public #undef REGISTER_NODE #define REGISTER_NODE(NodeClass, NodeTypeStr) @@ -36,6 +40,37 @@ FaceRecogItem MakeUnknownFace(int track_id, int candidate_person_id, const std:: return item; } +FramePtr MakeFaceFrame(uint64_t frame_id, const FaceRecogItem& item, int img_w = 100, int img_h = 100) { + auto frame = std::make_shared(); + frame->frame_id = frame_id; + frame->width = img_w; + frame->height = img_h; + frame->face_recog = std::make_shared(); + frame->face_recog->img_w = img_w; + frame->face_recog->img_h = img_h; + frame->face_recog->items.push_back(item); + return frame; +} + +AlarmNode::FaceRule MakeKnownRule() { + AlarmNode::FaceRule rule; + rule.name = "known_person"; + rule.kind = AlarmNode::FaceRule::Kind::Person; + rule.persons = {"alice"}; + rule.cooldown_ms = 0; + return rule; +} + +AlarmNode::FaceRule MakeLegacyKnownRule() { + AlarmNode::FaceRule rule; + rule.name = "known_person"; + rule.kind = AlarmNode::FaceRule::Kind::Person; + rule.cooldown_ms = 0; + rule.min_hits = 2; + rule.hit_window_ms = 5000; + return rule; +} + TEST(FaceTrackAlarmTest, IgnoresLowQualityUntrackedFace) { FaceTrackAggregationConfig cfg; FaceTrackState state; @@ -83,5 +118,78 @@ TEST(FaceTrackAlarmTest, DoesNotEmitUnknownForKnownPersonScoreWobble) { EXPECT_FALSE(decision.trigger_unknown); } +TEST(FaceTrackAlarmTest, ResetsKnownStateWhenIdentityChangesOnSameTrack) { + FaceTrackAggregationConfig cfg; + cfg.known_min_hits = 2; + cfg.known_hit_window_ms = 3000; + + FaceTrackState state; + const FaceRecogItem alice = MakeKnownFace(7, 1, "alice", 0.80f); + const FaceRecogItem bob = MakeKnownFace(7, 2, "bob", 0.82f); + + EXPECT_FALSE(UpdateFaceTrackState(cfg, state, alice, 1000).trigger_known); + EXPECT_TRUE(UpdateFaceTrackState(cfg, state, alice, 1500).trigger_known); + + EXPECT_FALSE(UpdateFaceTrackState(cfg, state, bob, 2000).trigger_known); + EXPECT_TRUE(UpdateFaceTrackState(cfg, state, bob, 2500).trigger_known); +} + +TEST(FaceTrackAlarmTest, ExpiresStateAfterTrackInactivity) { + FaceTrackAggregationConfig cfg; + cfg.known_min_hits = 2; + cfg.known_hit_window_ms = 3000; + cfg.state_expire_ms = 1000; + + FaceTrackState state; + const FaceRecogItem item = MakeKnownFace(8, 1, "alice", 0.80f); + + EXPECT_FALSE(UpdateFaceTrackState(cfg, state, item, 1000).trigger_known); + EXPECT_TRUE(UpdateFaceTrackState(cfg, state, item, 1500).trigger_known); + + EXPECT_FALSE(UpdateFaceTrackState(cfg, state, item, 4000).trigger_known); + EXPECT_TRUE(UpdateFaceTrackState(cfg, state, item, 4500).trigger_known); +} + +TEST(FaceTrackAlarmTest, DisqualifiedFramesDoNotAdvanceTrackAggregation) { + AlarmNode node; + node.track_agg_cfg_.enable = true; + node.track_agg_cfg_.known_min_hits = 2; + node.track_agg_cfg_.known_hit_window_ms = 5000; + auto rule = MakeKnownRule(); + rule.min_face_area_ratio = 0.03f; + node.face_rules_.push_back(rule); + + FaceRecogItem small = MakeKnownFace(11, 1, "alice", 0.85f); + small.bbox = Rect{0.0f, 0.0f, 5.0f, 5.0f}; + FaceRecogItem valid = MakeKnownFace(11, 1, "alice", 0.85f); + valid.bbox = Rect{0.0f, 0.0f, 20.0f, 20.0f}; + + EXPECT_EQ(node.Process(MakeFaceFrame(1, small)), NodeStatus::OK); + EXPECT_EQ(node.alarm_count_, 0u); + + EXPECT_EQ(node.Process(MakeFaceFrame(2, valid)), NodeStatus::OK); + EXPECT_EQ(node.alarm_count_, 0u); + + EXPECT_EQ(node.Process(MakeFaceFrame(3, valid)), NodeStatus::OK); + EXPECT_EQ(node.alarm_count_, 1u); +} + +TEST(FaceTrackAlarmTest, LegacyFallbackKeepsPersonVoteAcrossTrackChanges) { + AlarmNode node; + node.track_agg_cfg_.enable = false; + node.face_rules_.push_back(MakeLegacyKnownRule()); + + FaceRecogItem first = MakeKnownFace(21, 5, "alice", 0.88f); + first.bbox = Rect{0.0f, 0.0f, 20.0f, 20.0f}; + FaceRecogItem second = first; + second.person_track_id = 22; + + EXPECT_EQ(node.Process(MakeFaceFrame(1, first)), NodeStatus::OK); + EXPECT_EQ(node.alarm_count_, 0u); + + EXPECT_EQ(node.Process(MakeFaceFrame(2, second)), NodeStatus::OK); + EXPECT_EQ(node.alarm_count_, 1u); +} + } // namespace } // namespace rk3588