Fix face track alarm aggregation ordering
This commit is contained in:
parent
f2141ab689
commit
afd428f77c
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user