Add unknown face candidate diagnostics
This commit is contained in:
parent
498a8216fe
commit
5156d099c4
@ -368,6 +368,10 @@
|
||||
"min_quality_hits": 4
|
||||
}
|
||||
},
|
||||
"face_debug": {
|
||||
"log_unknown_candidates": true,
|
||||
"unknown_candidate_interval_ms": 0
|
||||
},
|
||||
"face_rules": [
|
||||
{
|
||||
"name": "unknown_face",
|
||||
|
||||
@ -5,9 +5,11 @@
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
@ -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<FaceRule> 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<bool>("log_unknown_candidates", out.log_unknown_candidates);
|
||||
out.unknown_candidate_interval_ms = static_cast<uint64_t>(std::max<int64_t>(
|
||||
0, static_cast<int64_t>(debug->ValueOr<int>("unknown_candidate_interval_ms", 0))));
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::vector<FaceRule> ParseFaceRulesFromConfig(const SimpleJson& config) {
|
||||
std::vector<FaceRule> 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<int>("cooldown_ms", r.cooldown_ms));
|
||||
r.per_person_cooldown_ms = std::max(0, item.ValueOr<int>("per_person_cooldown_ms", r.per_person_cooldown_ms));
|
||||
r.min_face_area_ratio = item.ValueOr<double>("min_face_area_ratio", static_cast<double>(r.min_face_area_ratio));
|
||||
r.min_face_aspect = item.ValueOr<double>("min_face_aspect", static_cast<double>(r.min_face_aspect));
|
||||
r.max_face_aspect = item.ValueOr<double>("max_face_aspect", static_cast<double>(r.max_face_aspect));
|
||||
r.min_sim = item.ValueOr<double>("min_sim", static_cast<double>(r.min_sim));
|
||||
r.min_face_area_ratio = static_cast<float>(
|
||||
item.ValueOr<double>("min_face_area_ratio", static_cast<double>(r.min_face_area_ratio)));
|
||||
r.min_face_aspect = static_cast<float>(
|
||||
item.ValueOr<double>("min_face_aspect", static_cast<double>(r.min_face_aspect)));
|
||||
r.max_face_aspect = static_cast<float>(
|
||||
item.ValueOr<double>("max_face_aspect", static_cast<double>(r.max_face_aspect)));
|
||||
r.min_sim = static_cast<float>(item.ValueOr<double>("min_sim", static_cast<double>(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<double>(it.bbox.w) * static_cast<double>(it.bbox.h);
|
||||
const double r = a / img_area;
|
||||
if (r < static_cast<double>(rule.min_face_area_ratio)) return false;
|
||||
out.face_area_ratio = r;
|
||||
if (r < static_cast<double>(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<double>(it.bbox.h));
|
||||
const double aspect = static_cast<double>(it.bbox.w) / h;
|
||||
if (rule.min_face_aspect > 0.0f && aspect < static_cast<double>(rule.min_face_aspect)) return false;
|
||||
if (rule.max_face_aspect > 0.0f && aspect > static_cast<double>(rule.max_face_aspect)) return false;
|
||||
out.face_aspect = aspect;
|
||||
if (rule.min_face_aspect > 0.0f && aspect < static_cast<double>(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<double>(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<size_t>(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<uint64_t>(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<double>(it.bbox.w) * static_cast<double>(it.bbox.h) / img_area) : 0.0);
|
||||
const double aspect = eval.face_aspect > 0.0
|
||||
? eval.face_aspect
|
||||
: (static_cast<double>(it.bbox.w) / std::max(1e-6, static_cast<double>(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<double>(it.best_sim) - static_cast<double>(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<int, FaceTrackState> face_track_states_;
|
||||
std::unordered_map<int, uint64_t> 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<PacketRingBuffer> packet_buffer_;
|
||||
std::vector<std::unique_ptr<IAlarmAction>> actions_;
|
||||
|
||||
|
||||
@ -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<SpscQueue<FramePtr>>(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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user