diff --git a/.gitignore b/.gitignore index 36cc700..b7534d6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ ## Tooling outputs /compile_commands.json /.cache/ +__pycache__/ +*.py[cod] ## Binaries and objects *.o diff --git a/docs/config_guide.md b/docs/config_guide.md index fcc5253..8003031 100644 --- a/docs/config_guide.md +++ b/docs/config_guide.md @@ -300,7 +300,8 @@ pre_rgb -> face_det -> person_det -> person_trk -> face_recog -> ... - `known`:满足 `threshold.accept` 和 `threshold.margin`,可以作为“已知人”证据。 - `uncertain`:像某个已知人,但证据不足;不会直接当作陌生人。 -- `unknown`:保留给明确陌生人语义。当前人脸识别节点主要输出 `known` / `uncertain`,陌生人告警由 alarm 结合 track 聚合、质量门槛和“没有已知人证据”来判断。 + +人脸识别模型输出的是 embedding,不是带有 `unknown` 类别的分类结果。因此人脸识别节点只输出 `known` / `uncertain`。`unknown_face` 属于告警规则语义,由 alarm 结合 track 聚合、质量门槛和“没有已知人证据”来判断。 日志分析: @@ -779,9 +780,9 @@ python tools/analyze_face_recog_log.py .\logs\media-server_latest.log - `face_track_aggregation.known.min_hits` 是否过高。测试触发链路可用 `1`,正式打卡建议 `2` 或 `3`。 - `known_person.cooldown_ms` 和 `known.reentry_cooldown_ms` 是否正在抑制重复上报。 -### Q5: unknown 和 uncertain 有什么区别? +### Q5: unknown_face 和 uncertain 有什么区别? -`uncertain` 是单帧识别状态,表示“没有稳定确认是已知人”。它本身不能直接当作陌生人。`unknown_face` 是告警规则,使用开放集识别思路:人脸质量足够、不是 known、`best_sim < unknown_face.max_known_sim`、同一人体 track 持续存在并累计足够质量帧后,才认为是陌生人。这样可以避免远处小脸、合成人脸或短暂模糊帧被误报成陌生人。 +`uncertain` 是识别节点的单帧状态,表示“没有稳定确认是已知人”。它本身不能直接当作陌生人。`unknown_face` 是告警规则,使用开放集识别思路:人脸质量足够、不是 `known`、`best_sim < unknown_face.max_known_sim`、同一人体 track 持续存在并累计足够质量帧后,才认为是陌生人。这样可以避免远处小脸、合成人脸或短暂模糊帧被误报成陌生人。 --- diff --git a/docs/design/FaceRecognition_TrackAware_Alarm_Design.md b/docs/design/FaceRecognition_TrackAware_Alarm_Design.md index afb4545..c65ce94 100644 --- a/docs/design/FaceRecognition_TrackAware_Alarm_Design.md +++ b/docs/design/FaceRecognition_TrackAware_Alarm_Design.md @@ -9,14 +9,14 @@ 现有链路已经具备以下能力: - 从 gallery 中识别已知人员 -- 基于单帧识别结果判定 `known` 或 `unknown` +- 基于单帧 embedding 匹配结果输出 `known` 或 `uncertain` - 生成 `known_person` 和 `unknown_face` 告警 - 通过现有 alarm action 链路上传截图和视频片段 但当前告警行为仍然主要由单帧人脸识别结果驱动。在车间测试中,同一个已知人员可能出现: - 近距离、高质量帧触发 `known_person` -- 远距离、小脸、低质量帧触发 `unknown_face` +- 远距离、小脸、低质量帧被误当作陌生人候选并触发 `unknown_face` 这不符合目标场景要求。对当前车间场景来说: @@ -85,7 +85,7 @@ 2. 每张识别出的人脸在同帧内关联到一个人体框 3. 将关联后的人体 `track_id` 写入人脸识别结果 4. `alarm` 按 `person_track_id` 聚合多帧识别证据 -5. 身份判定改为三态: +5. 告警层身份判定改为三态: - `known` - `unknown` - `uncertain` @@ -95,7 +95,7 @@ 这样可以保持当前 DAG 架构和插件边界不变: - tracker 只负责跟踪人体 -- 人脸插件只负责输出识别证据 +- 人脸插件只负责输出识别证据。识别节点没有 `unknown` 分类,只输出 `known` / `uncertain` - alarm 节点负责业务层面的身份确认、去重和抑制 ## 5. 数据流设计 @@ -154,7 +154,7 @@ ### 7.1 状态定义 -每个 `person_track_id` 只能处于以下三种状态之一: +每个 `person_track_id` 在 alarm 聚合层只能处于以下三种状态之一: - `uncertain` - 质量不足,或证据不足 @@ -163,6 +163,8 @@ - `unknown` - 已经稳定确认无法归属到已知人员 +注意:`unknown` 不是人脸识别模型或 `ai_face_recog` 节点的单帧输出。人脸识别模型输出 embedding,识别节点只判断当前帧是否可作为 `known` 证据;无法确认已知人时统一输出 `uncertain`。`unknown` 只在 alarm 层经过质量门控、持续跟踪和低已知相似度验证后形成。 + ### 7.2 为什么必须引入 `uncertain` `uncertain` 是本方案的关键。 diff --git a/tests/test_analyze_face_recog_log.py b/tests/test_analyze_face_recog_log.py index f9ee39b..21c1139 100644 --- a/tests/test_analyze_face_recog_log.py +++ b/tests/test_analyze_face_recog_log.py @@ -50,6 +50,7 @@ class FaceRecogLogAnalysisTest(unittest.TestCase): self.assertEqual(summary.alarm_counts["known_person:reg_001"], 1) self.assertEqual(summary.external_send_counts["ok"], 1) self.assertEqual(summary.quantiles["known"]["best_sim"]["max"], 0.58) + self.assertNotIn("unknown", summary.quantiles) self.assertEqual(summary.unknown_candidate_total, 2) self.assertEqual(summary.unknown_candidate_reject_counts["min_face_area_ratio"], 1) self.assertEqual(summary.unknown_candidate_gate_counts["waiting_track_age"], 1) diff --git a/tools/analyze_face_recog_log.py b/tools/analyze_face_recog_log.py index 5176e12..93a40a2 100644 --- a/tools/analyze_face_recog_log.py +++ b/tools/analyze_face_recog_log.py @@ -91,7 +91,6 @@ class LogSummary: status_counts: Counter[str] = field(default_factory=Counter) candidate_counts: Counter[str] = field(default_factory=Counter) known_candidate_counts: Counter[str] = field(default_factory=Counter) - unknown_candidate_counts: Counter[str] = field(default_factory=Counter) track_id_missing: int = 0 known_track_id_missing: int = 0 assoc_total: int = 0 @@ -245,8 +244,6 @@ def analyze_lines(lines: Iterable[str]) -> LogSummary: summary.candidate_counts[item.candidate] += 1 if item.status == "known": summary.known_candidate_counts[item.candidate] += 1 - if item.status == "unknown": - summary.unknown_candidate_counts[item.candidate] += 1 if item.track_id < 0: summary.track_id_missing += 1 if item.status == "known": @@ -262,7 +259,7 @@ def analyze_lines(lines: Iterable[str]) -> LogSummary: summary.first_frame = matches[0].frame summary.last_frame = matches[-1].frame - for status in ("known", "uncertain", "unknown"): + for status in ("known", "uncertain"): status_items = [item for item in matches if item.status == status] summary.quantiles[status] = { "best_sim": _quantiles([item.best_sim for item in status_items]), @@ -346,7 +343,7 @@ def format_summary(summary: LogSummary) -> str: lines.append(f"- faces_in_sum: {summary.faces_in_sum}") lines.append(f"- recog_items_sum: {summary.recog_items_sum}") lines.append(f"- match_total: {summary.match_total}") - for status in ("known", "uncertain", "unknown"): + for status in ("known", "uncertain"): count = summary.status_counts.get(status, 0) lines.append(f"- {status}: {count} ({_pct(count, summary.match_total)})") lines.append(f"- person_track_id=-1: {summary.track_id_missing} ({_pct(summary.track_id_missing, summary.match_total)})") @@ -355,10 +352,9 @@ def format_summary(summary: LogSummary) -> str: lines.append("Candidates:") lines.append(f"- top candidates: {summary.candidate_counts.most_common(10)}") lines.append(f"- known candidates: {summary.known_candidate_counts.most_common()}") - lines.append(f"- unknown candidates: {summary.unknown_candidate_counts.most_common()}") lines.append("") lines.append("Quality Quantiles:") - for status in ("known", "uncertain", "unknown"): + for status in ("known", "uncertain"): lines.append(f"- {status} best_sim: {summary.quantiles.get(status, {}).get('best_sim', {})}") lines.append(f"- {status} sim_margin: {summary.quantiles.get(status, {}).get('sim_margin', {})}") lines.append(f"- {status} bbox_area: {summary.quantiles.get(status, {}).get('bbox_area', {})}")