Clarify face recognition semantics and API action logging

This commit is contained in:
tian 2026-04-16 09:18:27 +08:00
parent 0d510e5d9f
commit 87c24bce05
15 changed files with 132 additions and 28 deletions

View File

@ -34,7 +34,7 @@ inline std::string BuildFaceRecogDebugSummaryLine(
const std::string& node_id, uint64_t frame_id, const FaceRecogItem& item) {
const float sim_margin = item.best_sim - item.second_sim;
const std::string candidate = item.candidate_name.empty() ? "n/a" : item.candidate_name;
const std::string status = item.unknown ? "unknown" : "known";
const std::string status = FaceRecogStateName(item.state);
std::ostringstream oss;
oss << "[ai_face_recog] match"

View File

@ -9,6 +9,25 @@
namespace rk3588 {
enum class FaceRecogState {
Uncertain,
Known,
};
inline const char* FaceRecogStateName(FaceRecogState state) {
switch (state) {
case FaceRecogState::Known:
return "known";
case FaceRecogState::Uncertain:
default:
return "uncertain";
}
}
inline bool FaceRecogStateIsKnown(FaceRecogState state) {
return state == FaceRecogState::Known;
}
struct Point2f {
float x = 0.0f;
float y = 0.0f;
@ -32,11 +51,11 @@ struct FaceDetResult {
struct FaceRecogItem {
Rect bbox{};
int person_track_id = -1;
FaceRecogState state = FaceRecogState::Uncertain;
int best_person_id = -1;
std::string best_name;
float best_sim = 0.0f;
bool unknown = true;
float second_sim = 0.0f;
int candidate_person_id = -1;
std::string candidate_name;

View File

@ -1113,10 +1113,10 @@ private:
item.candidate_person_id = sr.best_person_id;
item.candidate_name = sr.best_name;
item.best_person_id = accept ? sr.best_person_id : -1;
item.best_name = accept ? sr.best_name : "unknown";
item.best_name = accept ? sr.best_name : "";
item.best_sim = sr.best_sim;
item.second_sim = sr.second_sim;
item.unknown = !accept;
item.state = accept ? FaceRecogState::Known : FaceRecogState::Uncertain;
if (cfg->emit_embedding) item.embedding = emb;
rr.items.push_back(std::move(item));

View File

@ -9,8 +9,26 @@
namespace rk3588 {
enum class FaceAlarmStatus {
Uncertain,
Known,
Unknown,
};
inline const char* FaceAlarmStatusName(FaceAlarmStatus status) {
switch (status) {
case FaceAlarmStatus::Known:
return "known";
case FaceAlarmStatus::Unknown:
return "unknown";
case FaceAlarmStatus::Uncertain:
default:
return "uncertain";
}
}
struct FaceAlarmMatch {
bool unknown = true;
FaceAlarmStatus status = FaceAlarmStatus::Uncertain;
int candidate_person_id = -1;
std::string candidate_name;
float best_sim = 0.0f;

View File

@ -116,6 +116,28 @@ const SimpleJson* FindByDottedPath(const SimpleJson& root, const std::string& pa
} // namespace
std::string BuildExternalApiResultLogLine(const std::string& prefix,
const std::string& alarm_content,
const std::string& pic_url,
const std::string& video_url,
long http_code,
const std::string& err) {
std::ostringstream oss;
oss << "[ExternalApiAction] " << prefix
<< " http=" << http_code
<< " alarm_content=" << alarm_content;
if (!pic_url.empty()) {
oss << " pic_url=" << pic_url;
}
if (!video_url.empty()) {
oss << " video_url=" << video_url;
}
if (!err.empty()) {
oss << " error=" << err;
}
return oss.str();
}
ExternalApiAction::~ExternalApiAction() {
Drain();
Stop();
@ -256,6 +278,8 @@ void ExternalApiAction::WorkerLoop() {
long http_code = 0;
std::string send_err;
if (SendMessageWithToken(job, token, http_code, send_err)) {
LogInfo(BuildExternalApiResultLogLine(
"send ok", job.alarm_content, job.pic_url, job.video_url, http_code, ""));
break;
}
@ -275,7 +299,8 @@ void ExternalApiAction::WorkerLoop() {
LogWarn("[ExternalApiAction] refresh token failed: " + terr);
}
LogWarn("[ExternalApiAction] send failed: " + send_err + " (http=" + std::to_string(http_code) + ")");
LogWarn(BuildExternalApiResultLogLine(
"send failed", job.alarm_content, job.pic_url, job.video_url, http_code, send_err));
}
}
@ -307,6 +332,7 @@ bool ExternalApiAction::EnsureToken(std::string& err) {
if (!FetchToken(token, err)) {
return false;
}
LogInfo("[ExternalApiAction] token fetched successfully");
std::lock_guard<std::mutex> tl(token_mu_);
token_ = std::move(token);

View File

@ -12,6 +12,13 @@
namespace rk3588 {
std::string BuildExternalApiResultLogLine(const std::string& prefix,
const std::string& alarm_content,
const std::string& pic_url,
const std::string& video_url,
long http_code,
const std::string& err);
class ExternalApiAction : public IAlarmAction {
public:
~ExternalApiAction() override;

View File

@ -77,7 +77,7 @@ void LogAction::Execute(AlarmEvent& event, std::shared_ptr<Frame> /*frame*/) {
if (i > 0) oss << ", ";
const float sim_margin = match.best_sim - match.second_sim;
const std::string candidate = match.candidate_name.empty() ? "n/a" : match.candidate_name;
oss << "{status=" << (match.unknown ? "unknown" : "known")
oss << "{status=" << FaceAlarmStatusName(match.status)
<< " candidate=" << candidate
<< " candidate_id=" << match.candidate_person_id
<< " best_sim=" << std::fixed << std::setprecision(2) << match.best_sim

View File

@ -136,7 +136,7 @@ FaceTrackDecision UpdateFaceTrackState(
}
PruneKnownHitTimes(state, cfg, now_ms);
if (!item.unknown) {
if (FaceRecogStateIsKnown(item.state)) {
const bool same_identity =
state.best_known_person_id == item.best_person_id &&
state.best_known_name == item.best_name;
@ -692,10 +692,10 @@ private:
}
if (rule.kind == FaceRule::Kind::Unknown) {
if (!it.unknown) return false;
if (FaceRecogStateIsKnown(it.state)) return false;
if (it.best_sim < rule.min_sim) return false;
} else {
if (it.unknown) return false;
if (!FaceRecogStateIsKnown(it.state)) return false;
if (it.best_sim < rule.min_sim) return false;
if (!rule.persons.empty()) {
bool ok = false;
@ -830,7 +830,9 @@ private:
dets.push_back(std::move(d));
trigger_keys.push_back(key);
FaceAlarmMatch fm;
fm.unknown = it.unknown;
fm.status = (rule.kind == FaceRule::Kind::Unknown)
? FaceAlarmStatus::Unknown
: (FaceRecogStateIsKnown(it.state) ? FaceAlarmStatus::Known : FaceAlarmStatus::Uncertain);
fm.candidate_person_id = it.candidate_person_id;
fm.candidate_name = it.candidate_name;
fm.best_sim = it.best_sim;

View File

@ -744,7 +744,7 @@ private:
rw = Clamp(rw, 1, w - x);
rh = Clamp(rh, 1, h - y);
im_rect rect{x, y, rw, rh};
const Color color = it.unknown ? Color{255, 0, 0} : Color{0, 255, 0};
const Color color = FaceRecogStateIsKnown(it.state) ? Color{0, 255, 0} : Color{255, 0, 0};
if (imrectangleTask(job, dst, rect, PackColorArgb(color), line_width_) <= 0) {
ok = false;
break;
@ -790,7 +790,7 @@ private:
int x2 = static_cast<int>(it.bbox.x + it.bbox.w);
int y2 = static_cast<int>(it.bbox.y + it.bbox.h);
const Color color = it.unknown ? Color{255, 0, 0} : Color{0, 255, 0};
const Color color = FaceRecogStateIsKnown(it.state) ? Color{0, 255, 0} : Color{255, 0, 0};
if (do_cpu_bbox) {
DrawRect(data, w, h, stride, fmt, x1, y1, x2, y2, line_width_, color);
@ -798,7 +798,7 @@ private:
if (do_cpu_text) {
char label[96];
const char* name = it.best_name.empty() ? (it.unknown ? "unknown" : "") : it.best_name.c_str();
const char* name = it.best_name.empty() ? FaceRecogStateName(it.state) : it.best_name.c_str();
snprintf(label, sizeof(label), "%s %.2f", name, it.best_sim);
int text_y = y1 - 8 * font_scale_;
if (text_y < 0) text_y = y1 + 2;

View File

@ -50,6 +50,7 @@ add_executable(rk3588_gtests
test_face_recog_debug.cpp
test_face_track_association.cpp
test_face_track_alarm.cpp
test_external_api_action.cpp
test_infer_backend.cpp
test_image_processor.cpp
test_codec_backend.cpp

View File

@ -0,0 +1,32 @@
#include <gtest/gtest.h>
#include <string>
#include "../plugins/alarm/actions/external_api_action.h"
namespace rk3588 {
namespace {
TEST(ExternalApiActionTest, BuildsSuccessLogLineWithContext) {
const std::string line = BuildExternalApiResultLogLine(
"send ok", "known_person:reg_001", "http://example/pic.jpg", "http://example/clip.mp4", 200, "");
EXPECT_NE(line.find("send ok"), std::string::npos);
EXPECT_NE(line.find("http=200"), std::string::npos);
EXPECT_NE(line.find("alarm_content=known_person:reg_001"), std::string::npos);
EXPECT_NE(line.find("pic_url=http://example/pic.jpg"), std::string::npos);
EXPECT_NE(line.find("video_url=http://example/clip.mp4"), std::string::npos);
}
TEST(ExternalApiActionTest, BuildsFailureLogLineWithError) {
const std::string line = BuildExternalApiResultLogLine(
"send failed", "unknown_face", "", "", 500, "timeout");
EXPECT_NE(line.find("send failed"), std::string::npos);
EXPECT_NE(line.find("http=500"), std::string::npos);
EXPECT_NE(line.find("error=timeout"), std::string::npos);
EXPECT_NE(line.find("alarm_content=unknown_face"), std::string::npos);
}
} // namespace
} // namespace rk3588

View File

@ -29,22 +29,21 @@ TEST(FaceRecogDebugTest, ParsesDebugConfigWithDefaultsAndOverrides) {
EXPECT_EQ(parsed.min_log_interval_ms, 350);
}
TEST(FaceRecogDebugTest, BuildsSummaryLineForUnknownCandidate) {
TEST(FaceRecogDebugTest, BuildsSummaryLineForUncertainCandidate) {
FaceRecogItem item;
item.bbox = Rect{10.0f, 20.0f, 30.0f, 40.0f};
item.candidate_person_id = 7;
item.candidate_name = "alice";
item.best_name = "unknown";
item.best_sim = 0.44f;
item.second_sim = 0.41f;
item.unknown = true;
item.state = FaceRecogState::Uncertain;
const std::string line = BuildFaceRecogDebugSummaryLine("face_recog", 42, item);
EXPECT_NE(line.find("[ai_face_recog] match"), std::string::npos);
EXPECT_NE(line.find("id=face_recog"), std::string::npos);
EXPECT_NE(line.find("frame=42"), std::string::npos);
EXPECT_NE(line.find("status=unknown"), std::string::npos);
EXPECT_NE(line.find("status=uncertain"), std::string::npos);
EXPECT_NE(line.find("candidate=alice"), std::string::npos);
EXPECT_NE(line.find("candidate_id=7"), std::string::npos);
EXPECT_NE(line.find("best_sim=0.44"), std::string::npos);

View File

@ -26,18 +26,18 @@ FaceRecogItem MakeKnownFace(int track_id, int person_id, const std::string& name
item.candidate_name = name;
item.best_sim = best_sim;
item.second_sim = 0.20f;
item.unknown = false;
item.state = FaceRecogState::Known;
return item;
}
FaceRecogItem MakeUnknownFace(int track_id, int candidate_person_id, const std::string& candidate_name, float best_sim) {
FaceRecogItem MakeUncertainFace(int track_id, int candidate_person_id, const std::string& candidate_name, float best_sim) {
FaceRecogItem item;
item.person_track_id = track_id;
item.candidate_person_id = candidate_person_id;
item.candidate_name = candidate_name;
item.best_sim = best_sim;
item.second_sim = best_sim - 0.02f;
item.unknown = true;
item.state = FaceRecogState::Uncertain;
return item;
}
@ -75,7 +75,7 @@ TEST(FaceTrackAlarmTest, IgnoresLowQualityUntrackedFace) {
FaceTrackState state;
std::unordered_map<int, uint64_t> known_identity_last_alarm_ms;
FaceRecogItem item = MakeUnknownFace(-1, -1, "", 0.40f);
FaceRecogItem item = MakeUncertainFace(-1, -1, "", 0.40f);
const FaceTrackDecision decision = UpdateFaceTrackState(cfg, state, known_identity_last_alarm_ms, item, 1000);
EXPECT_FALSE(decision.trigger_known);
@ -145,7 +145,7 @@ TEST(FaceTrackAlarmTest, DoesNotEmitUnknownForKnownPersonScoreWobble) {
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 FaceRecogItem wobble = MakeUncertainFace(9, 1, "reg_001", 0.42f);
const FaceTrackDecision decision = UpdateFaceTrackState(
cfg, state, known_identity_last_alarm_ms, wobble, 2600);
EXPECT_FALSE(decision.trigger_known);
@ -169,7 +169,7 @@ TEST(FaceTrackAlarmTest, DoesNotEmitUnknownWhileTrackIsKnownLeaning) {
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 FaceRecogItem wobble = MakeUncertainFace(10, 1, "alice", 0.42f);
const FaceTrackDecision decision = UpdateFaceTrackState(
cfg, state, known_identity_last_alarm_ms, wobble, 2600);
EXPECT_FALSE(decision.trigger_known);
@ -268,7 +268,7 @@ TEST(FaceTrackAlarmTest, KnownLeaningTrackDoesNotFireUnknownRule) {
FaceRecogItem known = MakeKnownFace(41, 5, "alice", 0.88f);
known.bbox = Rect{0.0f, 0.0f, 20.0f, 20.0f};
FaceRecogItem wobble = MakeUnknownFace(41, 5, "alice", 0.42f);
FaceRecogItem wobble = MakeUncertainFace(41, 5, "alice", 0.42f);
wobble.bbox = known.bbox;
EXPECT_EQ(node.Process(MakeFaceFrame(1, known)), NodeStatus::OK);
@ -427,7 +427,7 @@ TEST(FaceTrackAlarmTest, SuppressedKnownReentryTrackDoesNotAgeIntoUnknownAfterSu
EXPECT_FALSE(UpdateFaceTrackState(cfg, reentry_state, known_identity_last_alarm_ms, reentry_track, 4000).trigger_known);
EXPECT_FALSE(UpdateFaceTrackState(cfg, reentry_state, known_identity_last_alarm_ms, reentry_track, 4500).trigger_known);
FaceRecogItem wobble = MakeUnknownFace(92, 5, "alice", 0.42f);
FaceRecogItem wobble = MakeUncertainFace(92, 5, "alice", 0.42f);
const FaceTrackDecision wobble_decision = UpdateFaceTrackState(
cfg, reentry_state, known_identity_last_alarm_ms, wobble, 7000);
EXPECT_FALSE(wobble_decision.trigger_unknown);

View File

@ -102,7 +102,7 @@ TEST(FaceTrackAssociationTest, DebugLineIncludesPersonTrackId) {
item.candidate_name = "alice";
item.best_sim = 0.89f;
item.second_sim = 0.74f;
item.unknown = false;
item.state = FaceRecogState::Known;
const std::string line = BuildFaceRecogDebugSummaryLine("face_recog", 42, item);

View File

@ -32,7 +32,7 @@ TEST(LogActionTest, IncludesFaceRecognitionDetailsInAlarmLog) {
event.detections.push_back(det);
FaceAlarmMatch match;
match.unknown = true;
match.status = FaceAlarmStatus::Unknown;
match.candidate_person_id = 7;
match.candidate_name = "alice";
match.best_sim = 0.44f;