Add face alarm reentry suppression

This commit is contained in:
tian 2026-04-15 16:39:02 +08:00
parent da36345a90
commit 68d7c9edcf
3 changed files with 152 additions and 21 deletions

View File

@ -358,6 +358,18 @@
"per_track_cooldown_ms": 0
}
],
"face_track_aggregation": {
"known": {
"min_hits": 3,
"hit_window_ms": 3000,
"reentry_cooldown_ms": 300000
},
"unknown": {
"min_track_age_ms": 2000,
"min_quality_hits": 4,
"reentry_cooldown_ms": 300000
}
},
"face_rules": [
{
"name": "unknown_face",

View File

@ -10,6 +10,7 @@
#include <mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
#include "utils/logger.h"
@ -32,8 +33,10 @@ namespace {
struct FaceTrackAggregationConfig {
int known_min_hits = 3;
int known_hit_window_ms = 3000;
int known_reentry_cooldown_ms = 0;
int unknown_min_track_age_ms = 2000;
int unknown_min_quality_hits = 4;
int unknown_reentry_cooldown_ms = 0;
int state_expire_ms = 5000;
};
@ -90,17 +93,21 @@ FaceTrackAggregationConfig ParseFaceTrackAggregationConfig(const SimpleJson& con
cfg.known_min_hits = std::max(1, agg->ValueOr<int>("known_min_hits", cfg.known_min_hits));
cfg.known_hit_window_ms = std::max(0, agg->ValueOr<int>("known_hit_window_ms", cfg.known_hit_window_ms));
cfg.known_reentry_cooldown_ms = std::max(0, agg->ValueOr<int>("known_reentry_cooldown_ms", cfg.known_reentry_cooldown_ms));
cfg.unknown_min_track_age_ms = std::max(0, agg->ValueOr<int>("unknown_min_track_age_ms", cfg.unknown_min_track_age_ms));
cfg.unknown_min_quality_hits = std::max(1, agg->ValueOr<int>("unknown_min_quality_hits", cfg.unknown_min_quality_hits));
cfg.unknown_reentry_cooldown_ms = std::max(0, agg->ValueOr<int>("unknown_reentry_cooldown_ms", cfg.unknown_reentry_cooldown_ms));
cfg.state_expire_ms = std::max(0, agg->ValueOr<int>("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<int>("min_hits", cfg.known_min_hits));
cfg.known_hit_window_ms = std::max(0, known->ValueOr<int>("hit_window_ms", cfg.known_hit_window_ms));
cfg.known_reentry_cooldown_ms = std::max(0, known->ValueOr<int>("reentry_cooldown_ms", cfg.known_reentry_cooldown_ms));
}
if (const SimpleJson* unknown = agg->Find("unknown"); unknown && unknown->IsObject()) {
cfg.unknown_min_track_age_ms = std::max(0, unknown->ValueOr<int>("min_track_age_ms", cfg.unknown_min_track_age_ms));
cfg.unknown_min_quality_hits = std::max(1, unknown->ValueOr<int>("min_quality_hits", cfg.unknown_min_quality_hits));
cfg.unknown_reentry_cooldown_ms = std::max(0, unknown->ValueOr<int>("reentry_cooldown_ms", cfg.unknown_reentry_cooldown_ms));
}
return cfg;
@ -109,6 +116,7 @@ FaceTrackAggregationConfig ParseFaceTrackAggregationConfig(const SimpleJson& con
FaceTrackDecision UpdateFaceTrackState(
const FaceTrackAggregationConfig& cfg,
FaceTrackState& state,
std::unordered_map<int, uint64_t>& known_identity_last_alarm_ms,
const FaceRecogItem& item,
uint64_t now_ms) {
FaceTrackDecision decision;
@ -145,8 +153,21 @@ FaceTrackDecision UpdateFaceTrackState(
if (!state.reported_known &&
static_cast<int>(state.known_hit_times.size()) >= std::max(1, cfg.known_min_hits)) {
if (cfg.known_reentry_cooldown_ms > 0 && state.best_known_person_id >= 0) {
const auto it_last = known_identity_last_alarm_ms.find(state.best_known_person_id);
if (it_last != known_identity_last_alarm_ms.end() &&
now_ms >= it_last->second &&
(now_ms - it_last->second) < static_cast<uint64_t>(cfg.known_reentry_cooldown_ms)) {
state.reported_known = true;
state.reported_unknown = false;
return decision;
}
}
state.reported_known = true;
state.reported_unknown = false;
if (state.best_known_person_id >= 0) {
known_identity_last_alarm_ms[state.best_known_person_id] = now_ms;
}
decision.trigger_known = true;
}
return decision;
@ -256,6 +277,7 @@ public:
ParseFaceRules(config);
track_agg_cfg_ = ParseFaceTrackAggregationConfig(config);
face_track_states_.clear();
known_identity_last_alarm_ms_.clear();
// Optional evaluation throttle (for alarm side). 0 = disabled.
eval_interval_ms_ = std::max<int64_t>(0, static_cast<int64_t>(config.ValueOr<int>("eval_interval_ms", 0)));
@ -528,6 +550,7 @@ public:
face_last_trigger_.clear();
face_person_last_trigger_.clear();
face_track_states_.clear();
known_identity_last_alarm_ms_.clear();
packet_buffer_ = std::move(new_packet_buffer);
old_actions = std::move(actions_);
actions_ = std::move(new_actions);
@ -644,6 +667,7 @@ private:
face_last_trigger_.clear();
face_person_last_trigger_.clear();
face_track_states_.clear();
known_identity_last_alarm_ms_.clear();
}
static bool FaceItemMatchesRule(
@ -745,7 +769,8 @@ private:
if (!qualifying_observations[i]) continue;
const auto& it = frame->face_recog->items[i];
auto& state = face_track_states_[it.person_track_id];
track_decisions[i] = UpdateFaceTrackState(track_agg_cfg_, state, it, now_epoch_ms);
track_decisions[i] = UpdateFaceTrackState(
track_agg_cfg_, state, known_identity_last_alarm_ms_, it, now_epoch_ms);
have_track_decision[i] = true;
}
@ -888,6 +913,7 @@ private:
std::map<std::string, std::chrono::steady_clock::time_point> face_person_last_trigger_;
FaceTrackAggregationConfig track_agg_cfg_;
std::map<int, FaceTrackState> face_track_states_;
std::unordered_map<int, uint64_t> known_identity_last_alarm_ms_;
std::shared_ptr<PacketRingBuffer> packet_buffer_;
std::vector<std::unique_ptr<IAlarmAction>> actions_;

View File

@ -2,6 +2,7 @@
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
#include "node.h"
@ -72,10 +73,11 @@ AlarmNode::FaceRule MakeKnownRule() {
TEST(FaceTrackAlarmTest, IgnoresLowQualityUntrackedFace) {
FaceTrackAggregationConfig cfg;
FaceTrackState state;
std::unordered_map<int, uint64_t> known_identity_last_alarm_ms;
FaceRecogItem item = MakeUnknownFace(-1, -1, "", 0.40f);
const FaceTrackDecision decision = UpdateFaceTrackState(cfg, state, item, 1000);
const FaceTrackDecision decision = UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, item, 1000);
EXPECT_FALSE(decision.trigger_known);
EXPECT_FALSE(decision.trigger_unknown);
}
@ -86,16 +88,42 @@ TEST(FaceTrackAlarmTest, EmitsKnownAfterStableHitsOnSameTrack) {
cfg.known_hit_window_ms = 3000;
FaceTrackState state;
std::unordered_map<int, uint64_t> known_identity_last_alarm_ms;
const FaceRecogItem item = MakeKnownFace(5, 1, "reg_001", 0.62f);
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, item, 1000).trigger_known);
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, item, 1500).trigger_known);
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, item, 1000).trigger_known);
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, item, 1500).trigger_known);
const FaceTrackDecision decision = UpdateFaceTrackState(cfg, state, item, 2000);
const FaceTrackDecision decision = UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, item, 2000);
EXPECT_TRUE(decision.trigger_known);
EXPECT_FALSE(decision.trigger_unknown);
}
TEST(FaceTrackAlarmTest, SuppressesKnownReentryInsideConfiguredCooldown) {
FaceTrackAggregationConfig cfg;
cfg.known_min_hits = 2;
cfg.known_hit_window_ms = 3000;
cfg.known_reentry_cooldown_ms = 5000;
std::unordered_map<int, uint64_t> known_identity_last_alarm_ms;
const FaceRecogItem initial_track = MakeKnownFace(8, 1, "alice", 0.66f);
FaceTrackState first_track_state;
EXPECT_FALSE(UpdateFaceTrackState(cfg, first_track_state, known_identity_last_alarm_ms, initial_track, 1000).trigger_known);
EXPECT_TRUE(UpdateFaceTrackState(cfg, first_track_state, known_identity_last_alarm_ms, initial_track, 1500).trigger_known);
FaceTrackState reentry_state;
FaceRecogItem reentry_track = initial_track;
reentry_track.person_track_id = 18;
EXPECT_FALSE(UpdateFaceTrackState(cfg, reentry_state, known_identity_last_alarm_ms, reentry_track, 4000).trigger_known);
const FaceTrackDecision suppressed = UpdateFaceTrackState(
cfg, reentry_state, known_identity_last_alarm_ms, reentry_track, 4500);
EXPECT_FALSE(suppressed.trigger_known);
EXPECT_TRUE(reentry_state.reported_known);
EXPECT_EQ(known_identity_last_alarm_ms.at(1), 1500u);
}
TEST(FaceTrackAlarmTest, DoesNotEmitUnknownForKnownPersonScoreWobble) {
FaceTrackAggregationConfig cfg;
cfg.known_min_hits = 3;
@ -104,14 +132,16 @@ TEST(FaceTrackAlarmTest, DoesNotEmitUnknownForKnownPersonScoreWobble) {
cfg.unknown_min_quality_hits = 1;
FaceTrackState state;
std::unordered_map<int, uint64_t> known_identity_last_alarm_ms;
const FaceRecogItem known = MakeKnownFace(9, 1, "reg_001", 0.63f);
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known, 1000).trigger_known);
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known, 1500).trigger_known);
EXPECT_TRUE(UpdateFaceTrackState(cfg, state, known, 2000).trigger_known);
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, known, 1000).trigger_known);
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, known, 1500).trigger_known);
EXPECT_TRUE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, known, 2000).trigger_known);
const FaceRecogItem wobble = MakeUnknownFace(9, 1, "reg_001", 0.42f);
const FaceTrackDecision decision = UpdateFaceTrackState(cfg, state, wobble, 2600);
const FaceTrackDecision decision = UpdateFaceTrackState(
cfg, state, known_identity_last_alarm_ms, wobble, 2600);
EXPECT_FALSE(decision.trigger_known);
EXPECT_FALSE(decision.trigger_unknown);
}
@ -124,16 +154,18 @@ TEST(FaceTrackAlarmTest, DoesNotEmitUnknownWhileTrackIsKnownLeaning) {
cfg.unknown_min_quality_hits = 1;
FaceTrackState state;
std::unordered_map<int, uint64_t> known_identity_last_alarm_ms;
const FaceRecogItem known = MakeKnownFace(10, 1, "alice", 0.63f);
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known, 1000).trigger_known);
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known, 1500).trigger_known);
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, known, 1000).trigger_known);
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, known, 1500).trigger_known);
EXPECT_EQ(state.best_known_person_id, 1);
EXPECT_EQ(state.best_known_name, "alice");
EXPECT_EQ(state.known_hit_times.size(), 2u);
const FaceRecogItem wobble = MakeUnknownFace(10, 1, "alice", 0.42f);
const FaceTrackDecision decision = UpdateFaceTrackState(cfg, state, wobble, 2600);
const FaceTrackDecision decision = UpdateFaceTrackState(
cfg, state, known_identity_last_alarm_ms, wobble, 2600);
EXPECT_FALSE(decision.trigger_known);
EXPECT_FALSE(decision.trigger_unknown);
}
@ -144,14 +176,15 @@ TEST(FaceTrackAlarmTest, ResetsKnownStateWhenIdentityChangesOnSameTrack) {
cfg.known_hit_window_ms = 3000;
FaceTrackState state;
std::unordered_map<int, uint64_t> known_identity_last_alarm_ms;
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, known_identity_last_alarm_ms, alice, 1000).trigger_known);
EXPECT_TRUE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, alice, 1500).trigger_known);
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, bob, 2000).trigger_known);
EXPECT_TRUE(UpdateFaceTrackState(cfg, state, bob, 2500).trigger_known);
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, bob, 2000).trigger_known);
EXPECT_TRUE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, bob, 2500).trigger_known);
}
TEST(FaceTrackAlarmTest, ExpiresStateAfterTrackInactivity) {
@ -161,13 +194,14 @@ TEST(FaceTrackAlarmTest, ExpiresStateAfterTrackInactivity) {
cfg.state_expire_ms = 1000;
FaceTrackState state;
std::unordered_map<int, uint64_t> known_identity_last_alarm_ms;
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, known_identity_last_alarm_ms, item, 1000).trigger_known);
EXPECT_TRUE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, item, 1500).trigger_known);
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, item, 4000).trigger_known);
EXPECT_TRUE(UpdateFaceTrackState(cfg, state, item, 4500).trigger_known);
EXPECT_FALSE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, item, 4000).trigger_known);
EXPECT_TRUE(UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, item, 4500).trigger_known);
}
TEST(FaceTrackAlarmTest, DisqualifiedFramesDoNotAdvanceTrackAggregation) {
@ -277,6 +311,63 @@ TEST(FaceTrackAlarmTest, OverlappingRulesDoNotDoubleCountSingleFrameEvidence) {
EXPECT_TRUE(it_second->second.reported_known);
}
TEST(FaceTrackAlarmTest, KnownPersonDoesNotReAlarmWhileTrackStaysActive) {
AlarmNode node;
node.track_agg_cfg_.known_min_hits = 2;
node.track_agg_cfg_.known_hit_window_ms = 5000;
auto rule = MakeKnownRule();
rule.cooldown_ms = 0;
node.face_rules_.push_back(rule);
FaceRecogItem item = MakeKnownFace(61, 5, "alice", 0.88f);
item.bbox = Rect{0.0f, 0.0f, 20.0f, 20.0f};
EXPECT_EQ(node.Process(MakeFaceFrame(1, item)), NodeStatus::OK);
EXPECT_EQ(node.alarm_count_, 0u);
EXPECT_EQ(node.Process(MakeFaceFrame(2, item)), NodeStatus::OK);
EXPECT_EQ(node.alarm_count_, 1u);
EXPECT_EQ(node.Process(MakeFaceFrame(3, item)), NodeStatus::OK);
EXPECT_EQ(node.alarm_count_, 1u);
EXPECT_EQ(node.Process(MakeFaceFrame(4, item)), NodeStatus::OK);
EXPECT_EQ(node.alarm_count_, 1u);
}
TEST(FaceTrackAlarmTest, KnownPersonReEntryInsideCooldownOnNewTrackDoesNotReAlarm) {
AlarmNode node;
node.track_agg_cfg_.known_min_hits = 2;
node.track_agg_cfg_.known_hit_window_ms = 5000;
node.track_agg_cfg_.known_reentry_cooldown_ms = 1000;
auto rule = MakeKnownRule();
rule.cooldown_ms = 0;
node.face_rules_.push_back(rule);
FaceRecogItem first_track = MakeKnownFace(71, 5, "alice", 0.88f);
first_track.bbox = Rect{0.0f, 0.0f, 20.0f, 20.0f};
FaceRecogItem second_track = MakeKnownFace(72, 5, "alice", 0.88f);
second_track.bbox = first_track.bbox;
EXPECT_EQ(node.Process(MakeFaceFrame(1, first_track)), NodeStatus::OK);
EXPECT_EQ(node.alarm_count_, 0u);
EXPECT_EQ(node.Process(MakeFaceFrame(2, first_track)), NodeStatus::OK);
EXPECT_EQ(node.alarm_count_, 1u);
ASSERT_EQ(node.known_identity_last_alarm_ms_.size(), 1u);
const auto first_alarm_it = node.known_identity_last_alarm_ms_.find(5);
ASSERT_NE(first_alarm_it, node.known_identity_last_alarm_ms_.end());
EXPECT_EQ(node.Process(MakeFaceFrame(3, second_track)), NodeStatus::OK);
EXPECT_EQ(node.alarm_count_, 1u);
EXPECT_EQ(node.Process(MakeFaceFrame(4, second_track)), NodeStatus::OK);
EXPECT_EQ(node.alarm_count_, 1u);
EXPECT_EQ(node.known_identity_last_alarm_ms_.at(5), first_alarm_it->second);
}
TEST(FaceTrackAlarmTest, IgnoresLegacyDisableFlagAndStillUsesTrackAggregation) {
AlarmNode node;
NodeContext ctx;
@ -296,7 +387,8 @@ TEST(FaceTrackAlarmTest, IgnoresLegacyDisableFlagAndStillUsesTrackAggregation) {
"enable": false,
"known": {
"min_hits": 2,
"hit_window_ms": 5000
"hit_window_ms": 5000,
"reentry_cooldown_ms": 60000
}
}
})");
@ -304,6 +396,7 @@ TEST(FaceTrackAlarmTest, IgnoresLegacyDisableFlagAndStillUsesTrackAggregation) {
ASSERT_TRUE(node.Init(config, ctx));
EXPECT_EQ(node.track_agg_cfg_.known_min_hits, 2);
EXPECT_EQ(node.track_agg_cfg_.known_hit_window_ms, 5000);
EXPECT_EQ(node.track_agg_cfg_.known_reentry_cooldown_ms, 60000);
FaceRecogItem item = MakeKnownFace(31, 5, "alice", 0.88f);
item.bbox = Rect{0.0f, 0.0f, 20.0f, 20.0f};