Fix face track alarm aggregation ordering

This commit is contained in:
tian 2026-04-15 15:15:43 +08:00
parent f2141ab689
commit afd428f77c
2 changed files with 171 additions and 19 deletions

View File

@ -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<int>("known_hit_window_ms", cfg.known_hit_window_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.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));
@ -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<uint64_t>(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<uint64_t>(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<double>(img_w) * static_cast<double>(img_h) : 0.0;
std::vector<FaceTrackDecision> 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<uint64_t>(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<std::chrono::milliseconds>(now - it_last->second).count();
if (elapsed < rule.cooldown_ms) {
continue;
}
}
}
bool matched = false;
std::string matched_name;
std::vector<Detection> 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);

View File

@ -1,9 +1,13 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <string>
#include <vector>
#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->frame_id = frame_id;
frame->width = img_w;
frame->height = img_h;
frame->face_recog = std::make_shared<FaceRecogResult>();
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