From 5156d099c43f6bcb4d97d8c11388c370aaa61878 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Fri, 17 Apr 2026 11:06:12 +0800 Subject: [PATCH] Add unknown face candidate diagnostics --- configs/full_pipeline_1080p_test_alarm.json | 4 + plugins/alarm/alarm_node.cpp | 217 ++++++++++++++++++-- tests/test_face_track_alarm.cpp | 34 +++ 3 files changed, 241 insertions(+), 14 deletions(-) diff --git a/configs/full_pipeline_1080p_test_alarm.json b/configs/full_pipeline_1080p_test_alarm.json index c342417..ee79963 100644 --- a/configs/full_pipeline_1080p_test_alarm.json +++ b/configs/full_pipeline_1080p_test_alarm.json @@ -368,6 +368,10 @@ "min_quality_hits": 4 } }, + "face_debug": { + "log_unknown_candidates": true, + "unknown_candidate_interval_ms": 0 + }, "face_rules": [ { "name": "unknown_face", diff --git a/plugins/alarm/alarm_node.cpp b/plugins/alarm/alarm_node.cpp index 3217eb0..74a9d51 100644 --- a/plugins/alarm/alarm_node.cpp +++ b/plugins/alarm/alarm_node.cpp @@ -5,9 +5,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -281,6 +283,10 @@ public: ParseFaceRules(config); track_agg_cfg_ = ParseFaceTrackAggregationConfig(config); + const FaceDebugConfig face_debug = ParseFaceDebugConfig(config); + face_debug_log_unknown_candidates_ = face_debug.log_unknown_candidates; + face_debug_unknown_candidate_interval_ms_ = face_debug.unknown_candidate_interval_ms; + last_unknown_candidate_log_ms_ = 0; face_track_states_.clear(); known_identity_last_alarm_ms_.clear(); @@ -463,6 +469,7 @@ public: // Parse face rules (independent from detection rules) std::vector new_face_rules = ParseFaceRulesFromConfig(new_config); FaceTrackAggregationConfig new_track_agg_cfg = ParseFaceTrackAggregationConfig(new_config); + const FaceDebugConfig new_face_debug = ParseFaceDebugConfig(new_config); // Packet ring window settings int pre_sec = 5; @@ -552,6 +559,9 @@ public: rule_engine_ = std::move(new_engine); face_rules_ = std::move(new_face_rules); track_agg_cfg_ = new_track_agg_cfg; + face_debug_log_unknown_candidates_ = new_face_debug.log_unknown_candidates; + face_debug_unknown_candidate_interval_ms_ = new_face_debug.unknown_candidate_interval_ms; + last_unknown_candidate_log_ms_ = 0; face_last_trigger_.clear(); face_person_last_trigger_.clear(); face_track_states_.clear(); @@ -637,6 +647,36 @@ private: float min_sim = 0.0f; }; + struct FaceRuleEval { + bool matched = false; + std::string reject_reason; + std::string detail; + double face_area_ratio = 0.0; + double face_aspect = 0.0; + }; + + struct FaceDebugConfig { + bool log_unknown_candidates = false; + uint64_t unknown_candidate_interval_ms = 0; + }; + + static std::string Fixed3(double value) { + std::ostringstream oss; + oss << std::fixed << std::setprecision(3) << value; + return oss.str(); + } + + static FaceDebugConfig ParseFaceDebugConfig(const SimpleJson& config) { + FaceDebugConfig out; + const SimpleJson* debug = config.Find("face_debug"); + if (!debug || !debug->IsObject()) return out; + + out.log_unknown_candidates = debug->ValueOr("log_unknown_candidates", out.log_unknown_candidates); + out.unknown_candidate_interval_ms = static_cast(std::max( + 0, static_cast(debug->ValueOr("unknown_candidate_interval_ms", 0)))); + return out; + } + static std::vector ParseFaceRulesFromConfig(const SimpleJson& config) { std::vector rules; const SimpleJson* fr = config.Find("face_rules"); @@ -651,10 +691,13 @@ private: 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_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)); + r.min_face_area_ratio = static_cast( + item.ValueOr("min_face_area_ratio", static_cast(r.min_face_area_ratio))); + r.min_face_aspect = static_cast( + item.ValueOr("min_face_aspect", static_cast(r.min_face_aspect))); + r.max_face_aspect = static_cast( + item.ValueOr("max_face_aspect", static_cast(r.max_face_aspect))); + r.min_sim = static_cast(item.ValueOr("min_sim", static_cast(r.min_sim))); if (const SimpleJson* persons = item.Find("persons"); persons && persons->IsArray()) { for (const auto& p : persons->AsArray()) { @@ -678,25 +721,60 @@ private: static bool FaceItemMatchesRule( const FaceRule& rule, const FaceRecogItem& it, - double img_area) { + double img_area, + FaceRuleEval* eval = nullptr) { + FaceRuleEval local; + FaceRuleEval& out = eval ? *eval : local; + out = FaceRuleEval{}; + 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)) return false; + out.face_area_ratio = r; + if (r < static_cast(rule.min_face_area_ratio)) { + out.reject_reason = "min_face_area_ratio"; + out.detail = "area_ratio=" + Fixed3(r) + " min=" + Fixed3(rule.min_face_area_ratio); + return false; + } } 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)) return false; - if (rule.max_face_aspect > 0.0f && aspect > static_cast(rule.max_face_aspect)) return false; + out.face_aspect = aspect; + if (rule.min_face_aspect > 0.0f && aspect < static_cast(rule.min_face_aspect)) { + out.reject_reason = "min_face_aspect"; + out.detail = "aspect=" + Fixed3(aspect) + " min=" + Fixed3(rule.min_face_aspect); + return false; + } + if (rule.max_face_aspect > 0.0f && aspect > static_cast(rule.max_face_aspect)) { + out.reject_reason = "max_face_aspect"; + out.detail = "aspect=" + Fixed3(aspect) + " max=" + Fixed3(rule.max_face_aspect); + return false; + } } if (rule.kind == FaceRule::Kind::Unknown) { - if (FaceRecogStateIsKnown(it.state)) return false; - if (it.best_sim < rule.min_sim) return false; + if (FaceRecogStateIsKnown(it.state)) { + out.reject_reason = "known_state"; + out.detail = "status=known"; + return false; + } + if (it.best_sim < rule.min_sim) { + out.reject_reason = "min_sim"; + out.detail = "best_sim=" + Fixed3(it.best_sim) + " min=" + Fixed3(rule.min_sim); + return false; + } } else { - if (!FaceRecogStateIsKnown(it.state)) return false; - if (it.best_sim < rule.min_sim) return false; + if (!FaceRecogStateIsKnown(it.state)) { + out.reject_reason = "not_known"; + out.detail = std::string("status=") + FaceRecogStateName(it.state); + return false; + } + if (it.best_sim < rule.min_sim) { + out.reject_reason = "min_sim"; + out.detail = "best_sim=" + Fixed3(it.best_sim) + " min=" + Fixed3(rule.min_sim); + return false; + } if (!rule.persons.empty()) { bool ok = false; for (const auto& p : rule.persons) { @@ -705,11 +783,22 @@ private: break; } } - if (!ok) return false; + if (!ok) { + out.reject_reason = "person_filter"; + out.detail = "best_name=" + it.best_name; + return false; + } } } - return it.person_track_id >= 0; + if (it.person_track_id < 0) { + out.reject_reason = "missing_track_id"; + out.detail = "person_track_id=-1"; + return false; + } + + out.matched = true; + return true; } void EvaluateFaceRulesLocked(const FramePtr& frame) { @@ -779,6 +868,26 @@ private: have_track_decision[i] = true; } + if (face_debug_log_unknown_candidates_) { + for (const auto& rule : face_rules_) { + if (rule.kind != FaceRule::Kind::Unknown) continue; + for (size_t i = 0; i < frame->face_recog->items.size(); ++i) { + FaceRuleEval eval; + const bool rule_matched = FaceItemMatchesRule( + rule, frame->face_recog->items[i], img_area, &eval); + LogUnknownCandidateDebug( + rule, + frame, + i, + img_area, + eval, + rule_matched, + have_track_decision[i] ? &track_decisions[i] : nullptr, + now_epoch_ms); + } + } + } + for (const auto& rule : face_rules_) { if (rule.per_person_cooldown_ms <= 0) { const auto it_last = face_last_trigger_.find(rule.name); @@ -869,6 +978,83 @@ private: } } + void LogUnknownCandidateDebug( + const FaceRule& rule, + const FramePtr& frame, + size_t item_index, + double img_area, + const FaceRuleEval& eval, + bool rule_matched, + const FaceTrackDecision* decision, + uint64_t now_epoch_ms) { + if (face_debug_unknown_candidate_interval_ms_ > 0 && + last_unknown_candidate_log_ms_ > 0 && + now_epoch_ms > last_unknown_candidate_log_ms_ && + (now_epoch_ms - last_unknown_candidate_log_ms_) < face_debug_unknown_candidate_interval_ms_) { + return; + } + last_unknown_candidate_log_ms_ = now_epoch_ms; + + const auto& it = frame->face_recog->items[item_index]; + const auto state_it = face_track_states_.find(it.person_track_id); + const FaceTrackState* state = (state_it != face_track_states_.end()) ? &state_it->second : nullptr; + const uint64_t track_age_ms = (state && state->first_seen_ms > 0 && now_epoch_ms >= state->first_seen_ms) + ? (now_epoch_ms - state->first_seen_ms) + : 0; + const int quality_hits = state ? state->quality_hits : 0; + const bool known_leaning = state && + (state->reported_known || state->known_hit_times.size() >= static_cast(track_agg_cfg_.known_min_hits)); + + std::string gate = "rule_rejected"; + if (rule_matched && !decision) { + gate = "no_track_decision"; + } else if (decision && decision->trigger_unknown) { + gate = "trigger_unknown"; + } else if (state && state->reported_unknown) { + gate = "already_reported"; + } else if (known_leaning) { + gate = "known_leaning"; + } else if (rule_matched && track_age_ms < static_cast(track_agg_cfg_.unknown_min_track_age_ms)) { + gate = "waiting_track_age"; + } else if (rule_matched && quality_hits < track_agg_cfg_.unknown_min_quality_hits) { + gate = "waiting_quality_hits"; + } + + const double area_ratio = eval.face_area_ratio > 0.0 + ? eval.face_area_ratio + : ((img_area > 0.0) ? (static_cast(it.bbox.w) * static_cast(it.bbox.h) / img_area) : 0.0); + const double aspect = eval.face_aspect > 0.0 + ? eval.face_aspect + : (static_cast(it.bbox.w) / std::max(1e-6, static_cast(it.bbox.h))); + + std::ostringstream oss; + oss << "[alarm] unknown_candidate" + << " rule=" << rule.name + << " frame=" << frame->frame_id + << " item=" << item_index + << " status=" << FaceRecogStateName(it.state) + << " track_id=" << it.person_track_id + << " candidate_id=" << it.candidate_person_id + << " candidate=" << (it.candidate_name.empty() ? "-" : it.candidate_name) + << " best_sim=" << Fixed3(it.best_sim) + << " second_sim=" << Fixed3(it.second_sim) + << " margin=" << Fixed3(static_cast(it.best_sim) - static_cast(it.second_sim)) + << " bbox=(" << Fixed3(it.bbox.x) << "," << Fixed3(it.bbox.y) + << "," << Fixed3(it.bbox.w) << "," << Fixed3(it.bbox.h) << ")" + << " area_ratio=" << Fixed3(area_ratio) + << " aspect=" << Fixed3(aspect) + << " rule_matched=" << (rule_matched ? "true" : "false") + << " reject_reason=" << (eval.reject_reason.empty() ? "-" : eval.reject_reason) + << " reject_detail=" << (eval.detail.empty() ? "-" : eval.detail) + << " track_age_ms=" << track_age_ms + << " quality_hits=" << quality_hits << "/" << track_agg_cfg_.unknown_min_quality_hits + << " min_track_age_ms=" << track_agg_cfg_.unknown_min_track_age_ms + << " reported_known=" << (state && state->reported_known ? "true" : "false") + << " reported_unknown=" << (state && state->reported_unknown ? "true" : "false") + << " gate=" << gate; + LogInfo(oss.str()); + } + static std::string BuildFaceTrackKey(const FaceRule& rule, int track_id) { return rule.name + "#track:" + std::to_string(track_id); } @@ -921,6 +1107,9 @@ private: FaceTrackAggregationConfig track_agg_cfg_; std::map face_track_states_; std::unordered_map known_identity_last_alarm_ms_; + bool face_debug_log_unknown_candidates_ = false; + uint64_t face_debug_unknown_candidate_interval_ms_ = 0; + uint64_t last_unknown_candidate_log_ms_ = 0; std::shared_ptr packet_buffer_; std::vector> actions_; diff --git a/tests/test_face_track_alarm.cpp b/tests/test_face_track_alarm.cpp index e43ec52..d776111 100644 --- a/tests/test_face_track_alarm.cpp +++ b/tests/test_face_track_alarm.cpp @@ -250,6 +250,40 @@ TEST(FaceTrackAlarmTest, UntrackedQualifiedFaceDoesNotTriggerAlarm) { EXPECT_EQ(node.alarm_count_, 0u); } +TEST(FaceTrackAlarmTest, UnknownRuleDiagnosticsExplainMinSimRejection) { + AlarmNode::FaceRule rule; + rule.name = "unknown_face"; + rule.kind = AlarmNode::FaceRule::Kind::Unknown; + rule.min_sim = 0.35f; + + FaceRecogItem item = MakeUncertainFace(101, 7, "reg_007", 0.20f); + item.bbox = Rect{0.0f, 0.0f, 20.0f, 20.0f}; + + AlarmNode::FaceRuleEval eval; + EXPECT_FALSE(AlarmNode::FaceItemMatchesRule(rule, item, 10000.0, &eval)); + EXPECT_FALSE(eval.matched); + EXPECT_EQ(eval.reject_reason, "min_sim"); + EXPECT_NE(eval.detail.find("best_sim=0.200"), std::string::npos); +} + +TEST(FaceTrackAlarmTest, ParsesFaceDebugUnknownCandidateLogging) { + AlarmNode node; + NodeContext ctx; + ctx.input_queue = std::make_shared>(4, QueueDropStrategy::DropOldest); + + const SimpleJson config = ParseConfig(R"({ + "id": "alarm", + "face_debug": { + "log_unknown_candidates": true, + "unknown_candidate_interval_ms": 123 + } + })"); + + ASSERT_TRUE(node.Init(config, ctx)); + EXPECT_TRUE(node.face_debug_log_unknown_candidates_); + EXPECT_EQ(node.face_debug_unknown_candidate_interval_ms_, 123); +} + TEST(FaceTrackAlarmTest, KnownLeaningTrackDoesNotFireUnknownRule) { AlarmNode node; node.track_agg_cfg_.known_min_hits = 3;