Clarify face unknown alarm semantics
This commit is contained in:
parent
8c910d55fc
commit
5beab86905
2
.gitignore
vendored
2
.gitignore
vendored
@ -12,6 +12,8 @@
|
||||
## Tooling outputs
|
||||
/compile_commands.json
|
||||
/.cache/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
## Binaries and objects
|
||||
*.o
|
||||
|
||||
@ -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 持续存在并累计足够质量帧后,才认为是陌生人。这样可以避免远处小脸、合成人脸或短暂模糊帧被误报成陌生人。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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` 是本方案的关键。
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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', {})}")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user