现象
IdentityEngine 里所有依赖「本帧是否真有检测命中」的闸,在出厂配置下判据恒为 True。coasting(人已离开 / 跟丢后纯 Kalman 预测残留)的 track 被当成本帧真实检测到的人,6 个消费点全部失效。
链路
1. 产出(正确)
identity/deep_sort.py:180 — "detected_this_frame": (tr.time_since_update == 0)。该函数只按 is_confirmed 过滤(:166),coasting track 会照常输出,带 bbox = tr.bbox(纯预测位置)。字段就是为区分这种残留框而加的。
identity/sort.py:449 — "detected_this_frame": True。这条路在 :431 已先 skip 掉 time_since_update >= 1,coasting 根本不输出,恒 True 是对的。
2. 丢弃
identity/tracking_service.py:_build_response(:53,构造在 :81)把 tracker dict 投影成 TrackedObject,只保留 xyxy / class_id / id,detected_this_frame 连同 confidence / hits / age / time_since_update 一起丢掉。
TrackedObject(engine/types.py:112)只有 type / face_id / track_id / box_info,TrackingBoxInfo(:106)只有 frame_index / boxes —— schema 层面就装不下这个字段。
3. 重造(不带该字段)
identity/identity.py:_to_tracking_dicts(:131,调用点 :84)把 TrackedObject 反折成 dict,docstring 明写字段集为 id / class_id / bbox / xyxy / confidence,没有 detected_this_frame。
4. 消费(吃默认值)
identity/engine.py:518 — self._detected_this_frame[tid] = bool(tr.get("detected_this_frame", True))。缺字段时默认 True,于是每个 active track 恒 True。
git log -S'detected_this_frame' -- engine/types.py identity/tracking_service.py identity/identity.py 为空:该字段从引入起就没穿过这层边界,不是回归。
影响面
生产走的正是会输出 coasting track 的那条路:config/settings.yaml:123 设 tracking_service_mode: "deep_sort"(perception/engine/config.py:120 的 dataclass 默认是 "mock",但出厂 settings 覆盖成 deep_sort),create_tracking_service(tracking_service.py:288)据此建 DeepSortTrackingService。"real" / SORT 路径不受影响(那条路 coasting 不输出,字段冗余)。
6 个消费点,全部按「恒 True」运行:
| 位置 |
闸的本意(注释原话) |
恒 True 下的实际行为 |
engine.py:588-593 |
抗遮挡 IoA 只纳入本帧真检测框,「coasting 纯 Kalman 残留框不对应本帧真人, 算进去会造成"假遮挡"误杀」 |
残留框进入 overlap 集合 → 真人可被 E6 判为被遮挡,tier_c 不写库 |
engine.py:630-634 |
coasting track 当窗口不进 omni 候选,「bbox 已不对应本帧真人,让 omni 看到只会催生背景误判」 |
continue 永不触发,残留框进 fused prompt |
engine.py:721-724 |
bbox_norm 只对真检测 track 填,「避免给名册注入幻影位置」 |
恒填。IdentityTarget.bbox_xyxy_norm(types.py:166-169)文档里 None = 「名册退化为纯名」这一语义在生产不可达 |
engine.py:1077-1082 |
只有本帧真检测到的 confirmed 真人才解除 no_person 抑制区 |
残留框可解除静态误检抑制区 |
engine.py:1090-1097 |
抑制区内全新 track 预标 no_person |
残留的全新 track 也被预标 |
engine.py:1468-1472 |
tier_c 入队门 D:「coasting 残留框不入」 |
门永不触发;(D · 本帧无检测命中/coasting) 这行 log 在生产不可能出现 |
另外 engine.py:534-535 的注释「入队检测门(D)仍不收 coasting 窗的帧、不会写 Kalman 幻影框」是「已移除 coasting 清零」那个决定的兜底论据,在当前出厂配置下不成立。
(未测量线上实际发生频率,上表只写代码层面的确定性后果。)
为什么测试没抓到
现有用例都在丢字段的接缝下面注入:
tests/perception/engine/identity/test_engine_tier_u_wiring.py:110 直接给手造 dict 设 tr["detected_this_frame"] = False,再喂 engine.process;
tests/perception/engine/identity/test_no_person.py:177 起直接写 eng._detected_this_frame[...]。
两者都绕过 identity.py::_to_tracking_dicts,所以「tracker → tracking_service → identity → engine」这条真实接线没有任何覆盖。
修法(列选项,未定)
A. 把字段补进契约:TrackedObject(或 TrackingBoxInfo)加 detected_this_frame,_build_response 填、_to_tracking_dicts 传。改动最小、与现有结构一致,代价是要动被多处消费的 dataclass。
B. 绕开 TrackedObject:在 TrackingResponse 上并列一份 tracker 原始 dict(类似 main_det_boxes 的做法),engine 直接吃,不再投影→重造。避免同一份信息折两次,代价是接口面变大。
无论选哪个,建议一并做:
engine.py:518 的默认值从 True 改成 fail-closed(缺字段视为 False,或缺字段告警),否则下次再有实现漏填仍然静默恒 True;
- 补一条从
TrackingResponse 起算的用例,断言 coasting track 不进 omni 候选、不进 tier_c。
相关但可拆分:同一接缝还丢了 confidence
_to_tracking_dicts 把 confidence 硬编码成 1.0(identity.py:157)、class_id 硬编码成 0(:154),原因同上 —— TrackedObject 也装不下检测置信。后果:engine.py:1239 的 detector_conf=float(tr.get("confidence", 0.0)) 恒为 1.0,tier_u 侧 CropEntry.detector_conf 恒 1.0 → identity/tier_u.py:689 的 detector_conf < detector_conf_min(默认 0.4)永不拒,tier_u.py:160 的 det_norm 与 identity/extractor.py:565-584 打分里这一项退化成常数。根因与本 issue 同一个,可以一并修,也可以另开。
顺带一处笔误:sort.py:447 的注释写「在 L419 已 pre-filter」,实际 pre-filter 在 :431。
现象
IdentityEngine里所有依赖「本帧是否真有检测命中」的闸,在出厂配置下判据恒为True。coasting(人已离开 / 跟丢后纯 Kalman 预测残留)的 track 被当成本帧真实检测到的人,6 个消费点全部失效。链路
1. 产出(正确)
identity/deep_sort.py:180—"detected_this_frame": (tr.time_since_update == 0)。该函数只按is_confirmed过滤(:166),coasting track 会照常输出,带bbox = tr.bbox(纯预测位置)。字段就是为区分这种残留框而加的。identity/sort.py:449—"detected_this_frame": True。这条路在:431已先 skip 掉time_since_update >= 1,coasting 根本不输出,恒 True 是对的。2. 丢弃
identity/tracking_service.py:_build_response(:53,构造在:81)把 tracker dict 投影成TrackedObject,只保留xyxy/class_id/id,detected_this_frame连同confidence/hits/age/time_since_update一起丢掉。TrackedObject(engine/types.py:112)只有type/face_id/track_id/box_info,TrackingBoxInfo(:106)只有frame_index/boxes—— schema 层面就装不下这个字段。3. 重造(不带该字段)
identity/identity.py:_to_tracking_dicts(:131,调用点:84)把TrackedObject反折成 dict,docstring 明写字段集为id / class_id / bbox / xyxy / confidence,没有detected_this_frame。4. 消费(吃默认值)
identity/engine.py:518—self._detected_this_frame[tid] = bool(tr.get("detected_this_frame", True))。缺字段时默认True,于是每个 active track 恒 True。git log -S'detected_this_frame' -- engine/types.py identity/tracking_service.py identity/identity.py为空:该字段从引入起就没穿过这层边界,不是回归。影响面
生产走的正是会输出 coasting track 的那条路:
config/settings.yaml:123设tracking_service_mode: "deep_sort"(perception/engine/config.py:120的 dataclass 默认是"mock",但出厂 settings 覆盖成 deep_sort),create_tracking_service(tracking_service.py:288)据此建DeepSortTrackingService。"real"/ SORT 路径不受影响(那条路 coasting 不输出,字段冗余)。6 个消费点,全部按「恒 True」运行:
engine.py:588-593engine.py:630-634continue永不触发,残留框进 fused promptengine.py:721-724bbox_norm只对真检测 track 填,「避免给名册注入幻影位置」IdentityTarget.bbox_xyxy_norm(types.py:166-169)文档里None= 「名册退化为纯名」这一语义在生产不可达engine.py:1077-1082engine.py:1090-1097engine.py:1468-1472(D · 本帧无检测命中/coasting)这行 log 在生产不可能出现另外
engine.py:534-535的注释「入队检测门(D)仍不收 coasting 窗的帧、不会写 Kalman 幻影框」是「已移除 coasting 清零」那个决定的兜底论据,在当前出厂配置下不成立。(未测量线上实际发生频率,上表只写代码层面的确定性后果。)
为什么测试没抓到
现有用例都在丢字段的接缝下面注入:
tests/perception/engine/identity/test_engine_tier_u_wiring.py:110直接给手造 dict 设tr["detected_this_frame"] = False,再喂engine.process;tests/perception/engine/identity/test_no_person.py:177起直接写eng._detected_this_frame[...]。两者都绕过
identity.py::_to_tracking_dicts,所以「tracker → tracking_service → identity → engine」这条真实接线没有任何覆盖。修法(列选项,未定)
A. 把字段补进契约:
TrackedObject(或TrackingBoxInfo)加detected_this_frame,_build_response填、_to_tracking_dicts传。改动最小、与现有结构一致,代价是要动被多处消费的 dataclass。B. 绕开
TrackedObject:在TrackingResponse上并列一份 tracker 原始 dict(类似main_det_boxes的做法),engine 直接吃,不再投影→重造。避免同一份信息折两次,代价是接口面变大。无论选哪个,建议一并做:
engine.py:518的默认值从True改成 fail-closed(缺字段视为False,或缺字段告警),否则下次再有实现漏填仍然静默恒 True;TrackingResponse起算的用例,断言 coasting track 不进 omni 候选、不进 tier_c。相关但可拆分:同一接缝还丢了
confidence_to_tracking_dicts把confidence硬编码成1.0(identity.py:157)、class_id硬编码成0(:154),原因同上 ——TrackedObject也装不下检测置信。后果:engine.py:1239的detector_conf=float(tr.get("confidence", 0.0))恒为 1.0,tier_u 侧CropEntry.detector_conf恒 1.0 →identity/tier_u.py:689的detector_conf < detector_conf_min(默认 0.4)永不拒,tier_u.py:160的det_norm与identity/extractor.py:565-584打分里这一项退化成常数。根因与本 issue 同一个,可以一并修,也可以另开。顺带一处笔误:
sort.py:447的注释写「在 L419 已 pre-filter」,实际 pre-filter 在:431。