Skip to content

perception/identity: detected_this_frame 未穿过 TrackedObject 边界,DeepSORT 下 6 处 coasting 闸全部空转 #494

Description

@Molly-3000

现象

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 / iddetected_this_frame 连同 confidence / hits / age / time_since_update 一起丢掉。

TrackedObjectengine/types.py:112)只有 type / face_id / track_id / box_infoTrackingBoxInfo: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:518self._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:123tracking_service_mode: "deep_sort"perception/engine/config.py:120 的 dataclass 默认是 "mock",但出厂 settings 覆盖成 deep_sort),create_tracking_servicetracking_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_normtypes.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_dictsconfidence 硬编码成 1.0identity.py:157)、class_id 硬编码成 0:154),原因同上 —— TrackedObject 也装不下检测置信。后果:engine.py:1239detector_conf=float(tr.get("confidence", 0.0)) 恒为 1.0,tier_u 侧 CropEntry.detector_conf 恒 1.0 → identity/tier_u.py:689detector_conf < detector_conf_min(默认 0.4)永不拒,tier_u.py:160det_normidentity/extractor.py:565-584 打分里这一项退化成常数。根因与本 issue 同一个,可以一并修,也可以另开。

顺带一处笔误:sort.py:447 的注释写「在 L419 已 pre-filter」,实际 pre-filter 在 :431

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions