核心课程 / 视觉入门课 / EXP-07
EXP-07 · 第 3 阶段 · 多目标跟踪 · 设计性 · 4 学时

DeepSORT 多目标跟踪:给每一辆车发一个不变的身份编号

检测只回答「这一帧里有车、在哪个框里」。可路口统计要的不是这个,而是「这一辆车从哪来、到哪去」。中间差的是一条跨帧的连线:先按运动猜它跑到哪,再比对外观确认是同一个,最后发一个只增不减的 ID。

难度 ⭐⭐⭐ · 4 学时 yolov8n 检测卡尔曼预测 级联匹配ReID 外观
01 颜色02 轮廓03 二维码04 检测05 分类06 分割07 跟踪08 人脸09 骨骼10 3D11 体感12 手势

01目标:每辆车一个固定编号,交叉时也不会换人

🎯 实验目标 · 学完你将能
  • 说清检测与跟踪的分工:检测给「这一帧有什么」,跟踪给「跨帧的它是谁」,以及为什么后者不能靠框重叠硬扛
  • 跑通 python src/app.py --video test.mp4,让每辆车带上黄色 ID N car 标签,并读得懂终端那行 tracks=13
  • 调出两种失败:把 max_age 调小 → 遮挡几帧就换号;把 max_dist 调小 → 同一辆车被认成两辆
⚠️ 重点与难点 重点:四步链路 —— 预测、外观代价、级联配对、发号与淘汰。
难点:直觉是「框重叠最多的就是同一辆」,实际是先看长相(余弦距离)、位置只做门限;因为两辆车擦身而过时,重叠度最高的往往不是同一辆。
INPUT · 你给它
test.mp4 高速路车流

1280×720 的固定机位视频。也可以换 --ros 的相机画面。

→
OUTPUT · 你拿到
ID 12 car

每辆车一个黄框 + 不变的身份编号,终端每 30 帧打印 tracks=13 表示当前有 13 条活跃轨迹。

本实验的目标:画面里每辆车都带一个 ID,车走到画面另一边、被前车挡住几秒,回来还是同一个 ID。

02问题:检测给了框,可它不知道「这还是刚才那辆」

路口装一台相机,想统计「今天过了多少辆车、每辆往哪走」。你可能想:检测不是已经能框出车了吗,数一数框不就完了?

数不了。检测是「一帧一帧独立看」的:这一帧 3 个框,下一帧还是 3 个框,它说不出哪个框接着哪个框。车往前开 20 像素,对检测来说就是「一辆全新的车」。用框数当日流量,同辆车会被数几十次。

检测:只有框,没有身份 frame 100 下一帧:3 个框,谁是谁? ? 跟踪:框 + 不变的编号 frame 101 ID 5 car ID 8 car ID 11 car 下一帧:同一辆车,还是 ID 5 多出来的不是框,是多了一列「身份」—— 它必须跨帧存在
图 2-1|同一帧的两种输出。左边每帧重来,右边要回答「这个框和上一帧那个框是不是同一辆车」。
对比平时你看到的(黑盒)机器必须做的(白盒)
画面里有什么车、人、路牌,一眼就分清只有几千个格子里的分数,和几个框
它是不是刚才那辆你看一眼就认得? —— 没有「记忆」,也没有「长相」的概念
被挡住几秒又出现你记得它,接着看? —— 中间那几帧没有框,凭什么接上

把问号拆成三个具体问题:

① 车在动,框也在动,凭什么把这两个框认成同一辆?

② 两辆车交叉、擦身而过,怎么保证 ID 不互换?

③ 画面里的 ID 会涨到 30 多,这编号是随机的吗?

检测回答「这一帧有什么」,跟踪回答「它是谁」。多出来的那件事,是跨帧的身份。

03现象:同一辆车十几秒后还是 ID 12

🎬 现象速览 · 30 秒先看效果

你会看到:高速路上每辆车套一个黄框,左上角写 ID 12 car。终端刷 [track] 30帧平均帧率: 16.4 tracks=13。下面三张是在板上真实跑出来的画面(同一段 test.mp4,不同时刻)。

跟踪画面之一:近处红色轿车标注 ID 8 car,黑色轿车 ID 12 car,远处车辆 ID 1/3/4/5/7/11
① 较早时刻:近车 ID 8、黑车 ID 12,远处 1/3/4/5/7/11
另一时刻的跟踪画面:多辆车各自带 ID 5/7/12/8/11/3/1
② 同一批车:ID 一个没变
同一视频十几秒后:画面里的车变成 ID 26 到 ID 30
③ 十几秒后:换了一批车,ID 已涨到 26~30

怎么跑出来(两条命令):第一条进目录确认模型与视频,第二条跑视频模式,浏览器打开推流地址看实时结果。

cd ~/ros2_ws/src/xflz_eai_car1/vision/07_multi_object_tracking
ls models/ test.mp4
models/: yolov8n.rknn deepsort.rknn yolov8n.onnx deepsort.onnx test.mp4
python src/app.py --video test.mp4
[track] mode=deepsort [track] 30帧平均帧率: 16.4 tracks=13 ← 浏览器打开 http://<板IP>:8080/track.mjpg 看画面

启动时也会刷几行 W RKNN: … not match 与 dynamic range 提示,属静态 shape 模型的正常输出,不影响结果。

🔍 看什么:三件事,看完再看下一节。

  • 同一辆车的编号不变 —— 红车从画面这头开到那头,标签一直是同一个数字,不会因为框移动就换号。
  • 框会「跟丢后补上」 —— 车被前车挡一下,框闪一下再回来,回来还是原编号。这是卡尔曼预测在撑着。
  • 编号只增不减 —— 第 ③ 张编号已经到 30 了,可画面只有 5 辆车。编号是「历史上第几个被确认的目标」,不是「现在第几个」。

这就是本实验最终要呈现的现象。先照做、看懂它;第 05 节会带你亲手复现这三张图,并补上单图与实时两路。

04原理:四步连线题:先猜位置、再看长相、整体配对、发号淘汰

CORE IDEA · 一句话原理

每一帧都在做一道连线题:把上一帧留下的「老轨迹」和这一帧检出的「新框」连起来,连上就沿用旧编号,连不上就发新编号。

打个比方:像在人群里找刚才那个人 —— 先按他走路的速度猜他现在大概在哪(预测),走到那片区域再认脸(外观),脸对上了才算同一个人。揭个盖:人眼是「猜位置 + 认脸」一起完成的,机器必须拆成两步、还给每步定一条数值门限,于是才有了下面这些参数。

四个步骤回答第 02 节的三问:① 凭什么认同一辆 → 步骤 2 与 3;② 交叉不换人 → 步骤 3;③ 编号为什么涨 → 步骤 4。

① 这一帧有哪些框 yolov8n 9 输出 只有框,没有身份 ② 老轨迹先猜下一步 (cx,cy,a,h) 被挡住也照样往前推 ③ 两边做一道连线题 cos 距离 + 马氏门限 → 总代价最小配对 剩下的按 IoU 再配 max_dist / max_iou 先看长相,位置只做门限 ④ 发号 / 淘汰 连上 → 沿用 ID 连不上 → 新 ID 连续 3 帧才确认 丢 70 帧就删除 编号只增不减 ② 是纯数学预测(不需要检测),③ 才是「认人」,④ 决定你看到的编号 这一整套每帧只跑一次,都在 CPU;NPU 里只有第 ① 步的检测 整帧 13~16 FPS 的瓶颈:检测约 60ms,跟踪与绘制约 5ms
图 4-1|四步链路。注意第 ② 步不依赖检测结果 —— 这就是为什么车被挡住一两帧,框还能接着预测位置。

1这一帧到底有哪些框?

吃什么:一帧图。做什么:跑 yolov8n(9 输出,检测模型,没有掩码分支)。吐什么:若干个框 + 类别 + 分数,再用 min_confidence: 0.6 筛一遍。

这一步与 EXP-06 的步骤 1~3 同款,唯一区别:配置是 classes: [2](COCO 里的 car),所以画面上只出现车,人和巴士都被挡在外面。这也解释了一个常见疑惑:拿一张只有巴士的图去跑,会得到 tracks=0。

动它会怎样:min_confidence 调低(0.3)→ 弱检测也进跟踪,框变小、编号到处乱飘;调高 → 远处的车干脆不参与。

2它跑到哪去了?—— 没检测到也能猜

每条轨迹自带一个卡尔曼滤波器,状态是 8 个数:框中心 cx, cy、宽高比 a、高度 h,再加上它们各自的变化速度。所以轨迹「记得住」这辆车正在往哪开、开多快。

每帧先做一次 predict():不看这一帧有没有检测,先把框按速度往前推一格。这就是第 02 节第一问的一半答案 —— 车被挡住的那一两帧,画面上的框不是「停在上个位置」,而是「按速度推出来的位置」。

为什么用高度 h 做尺度:远处的车小、近处的大,用高度当基准,噪声参数能随框大小自动缩放,近处宽容、远处严格。

动它会怎样:这一步没有可调参数。但预测只对未来几帧有效 —— 推太久(比如丢失 30 帧还按老速度飞)会飞出去,所以必须有步骤 4 的淘汰。

3两个框,凭什么说是同一辆车?

这是第 02 节第一、二问的核心。做法分三层,顺序是关键:

  • 先看长相:把每个框裁出来,缩到 64×128、按 ImageNet 归一化,过 ReID 模型得到 512 维向量。两枚向量夹角越小越像,用余弦距离衡量。同时用马氏距离卡一道门:位置离预测太远的配对直接作废(阈值取 CHI2INV95[2],统计上的 95% 置信界)。
  • 再按「丢了多久」分级:刚丢 1 帧的先配、丢 2 帧的再配……一直到丢 70 帧,这叫级联匹配。为什么顺序重要?刚丢的轨迹位置预测最准,让它们优先挑,才不会被「丢了很久、已经飘了」的旧轨迹抢走检测。
  • 最后兜底:还没配上的、以及还没被确认的新轨迹,用纯 IoU 配一次(max_iou_distance: 0.8)。

为什么不是「重叠最多的就是同一辆」:两车并行或交叉时,IoU 最高的那一对经常是错的 —— 能区分它们的是长相。所以外观优先、IoU 兜底。

动它会怎样:max_dist(默认 0.2)是长相门限,调小 → 遮挡后回来被当成新车;调大 → 两辆同款同色的车可能被认成一辆。max_iou_distance 调小 → 移动中的车容易丢轨迹(配置注释写明 0.7 会丢,所以取 0.8)。

4编号为什么能涨到 30 多?

这是第 02 节第三问的答案,也是画面上最容易让人误解的一个数。

  • 连上了:轨迹继承旧编号,只更新位置与外观特征库(nn_budget: 100 条,超了丢最旧的)。
  • 没连上:这个检测新建一条轨迹,编号 = _next_id,然后计数器 +1。计数器从 1 开始、永不回退。
  • 新轨迹要先「试用」:连续命中 n_init: 3 帧才被确认、才会画出来。闪过一帧的误检不占编号。
  • 淘汰:连续 max_age: 70 帧没配上就删除轨迹。注意删除不回收编号。

动它会怎样:max_age 调小(10)→ 遮挡几帧就换号,编号涨得飞快;调大(200)→ 抗遮挡更强,但开走的车轨迹会在画面外飘很久,容易被别的检测接上变成错号。n_init 调大 → 编号更干净,但新目标要慢几帧才显示。

现在回看第 03 节第 ③ 张图:编号涨到 30 但画面只有 5 辆车,不是乱编 —— 是历史上已经确认过 30 个目标,前 25 个都开走了。

先认识 5 个词

轨迹 track

一条「跨帧的目标」,自带编号、卡尔曼状态、外观特征库。什么时候失效:连续 max_age 帧没配上就被删除。

卡尔曼预测

按目标当前速度推算下一帧大概在哪。什么时候失效:急停急转时预测会偏,靠步骤 3 的外观拉回来。

级联匹配 cascade

按「丢了多久」分轮配对,刚丢的先挑。什么时候失效:密集场景里同款车多,长相也会撞车。

余弦距离

两枚 512 维外观向量的夹角,越小越像,范围 0~2。什么时候失效:逆光、夜间低对比的目标,特征会失真。

ReID

专门把「一个目标」压成向量的模型,与检测模型不是同一个文件(deepsort.rknn)。什么时候失效:缺失时自动退回 sort 纯 IoU 模式并告警。

什么时候会翻车:五个真实边界选读 1 分钟
  • 密集同款车 —— 一排白色轿车排队通过,长相几乎一样,外观也会指错。此时只能靠位置与速度,需要适度收紧 max_iou_distance。
  • 长时间遮挡 —— 超过 max_age 帧还没重现,轨迹被删,回来一定是新编号。这不是 bug,是「愿意等多久」的取舍。
  • 出画面再进来 —— 同一辆车从左边出去、右边进来,中间完全消失过,就只有新编号。跨镜头还需要专门的 ReID 检索,不在本实验范围。
  • 相机自身在动 —— 卡尔曼假设背景静止。相机一晃,所有预测都偏,配对大面积失败。
  • ReID 缺失 —— 会打印 [warn] ReID load failed …; falling back to SORT,退化成纯 IoU:交叉换人、遮挡换号都会出现。
步数据变成什么为什么非它不可 / 少了会怎样谁在干动参数会怎样
1一帧图 → N 个框(类 + 分数)框都没有,跟踪无从谈起detect.decodescore_thr↑ 漏检;classes 决定追谁
2每条轨迹 → 1 个预测框(8 维状态)不做预测,遮挡 1 帧就断线Tracker.predict无参数(噪声固定)
3预测框 × 检测框 → 配对表(外观 + IoU 两道)只按重叠配,交叉时必换人_cascade / _iou_matchmax_dist↓ 换号、↑ 认错车;max_iou_distance↓ 丢轨迹
4配对表 → 带 ID 的轨迹集合(新增 / 更新 / 删除)不发号就没有跨帧身份;不淘汰会无限增长Track / _next_idmax_age↓ 易换号;n_init↑ 显示更慢但更干净

这张表只看第 2、5 两列就够用:第 2 列是数据形态,第 5 列是你能拧的旋钮。函数名在第四列,追代码时再看。

05实操:单图 / 视频 / 实时三条命令

先看「敲的命令」和「原理第几步」怎么对上号 —— 每一行都是第 04 节某一格的落地。

环节函数(文件)吃哪个参数对应原理第几步
取流cv2.VideoCapture / Ros2Camera.read(app.py / ros2_source.py)--video --ros链外
检测detect.decode(detect.py)detect.score_thr detect.classes步骤 1
外观特征ReIDBackend.__call__(reid.py)→ 512 维model.reid_rknn步骤 3
预测与配对Tracker.update / _cascade(tracker.py)tracker.* 六个阈值步骤 2、3
发号与绘制Track 生命周期 + draw_tracks(app.py)n_init max_age步骤 4
推流MjpegServer.set_frame(mjpeg_server.py)server.port stream_name链外
  1. 先用单图确认「只追车」约 3 分钟

    单图模式只跑一帧、只起 1 个 worker。因为 classes: [2] 只保留 car,拿一张只有巴士和行人的图去跑,会得到 tracks=0 —— 不是坏了,是过滤生效了。

    cd ~/ros2_ws/src/xflz_eai_car1/vision/07_multi_object_tracking
    python src/app.py --image ../06_semantic_segmentation/images/bus.jpg --save out.jpg
    [track] mode=deepsort [track] tracks=0 saved -> out.jpg

    把 classes 改成 [5](bus)再跑一次,tracks 就变成 1。这一步确认:过滤发生在检测之后、跟踪之前。

  2. 视频模式:看编号稳住约 5 分钟

    视频模式起 3 个 worker 的检测池,并把结果推到 MJPEG。

    python src/app.py --video test.mp4
    [track] mode=deepsort [track] 30帧平均帧率: 14.7 tracks=15 [track] 30帧平均帧率: 16.4 tracks=13 ← 浏览器打开 http://<板IP>:8080/track.mjpg

    盯近处那辆车的标签:它从画面底部开到顶部,编号一直不变。tracks 会上下浮动,那是同时存在的车辆数在变。

  3. 实时相机:deepsort 与 sort 对照约 8 分钟

    板上标准走法是 ROS 话题。先起驱动再跑程序,两个终端 ROS_DOMAIN_ID 必须一致(N 取 0~101 任一整数,每台设备各选各的)。

    终端一 · 起相机驱动

    source /opt/ros/humble/setup.bash && source ~/ros2_ws/install/setup.bash
    export ROS_DOMAIN_ID=N
    ros2 launch ascamera hp60c.launch.py
    日志持续滚动,无 ERROR

    终端二 · 跑跟踪

    source /opt/ros/humble/setup.bash && source ~/ros2_ws/install/setup.bash
    export ROS_DOMAIN_ID=N
    cd ~/ros2_ws/src/xflz_eai_car1/vision/07_multi_object_tracking
    python src/app.py --ros
    [track] mode=deepsort [track] 30帧平均帧率: xx.x tracks=N ← http://<板IP>:8080/track.mjpg

    对照实验:把 tracker.mode 改 sort 重跑(不动模型),再让人从别人身后穿过 —— sort 会在交叉处换号,deepsort 不会。这就是「长相」那一层的价值。

🔁 现象呼应这三步跑完出现的画面,就是第 03 节那三张:黄框 + ID N car、编号沿用不跳、编号只增不减。两处对上了,说明取流、检测、外观、配对、发号五段已完整走通。

你该看到的画面

画面 A · 正常
编号跟着车走

同一辆车穿过画面,标签一直是同一个数字;框偶尔闪一下(预测补位),编号不变。

画面 B · sort 模式
交叉就换号

两车擦身过后编号互换,说明外观那一层被关掉了。

画面 C · 编号暴涨
每隔几帧 +1

max_age 太小或 max_dist 太小:轨迹一丢就删,回来就是新号。

异常 · tracks=0
一个框都没有

先看 classes:[2] 只追 car。再看 min_confidence 是否太高。

踩坑速查

现象原因怎么办
画面里一个框都没有classes: [2] 只追 car,而画面里没有轿车改成要追的 COCO 编号,或写 [] 追全部
编号每隔几帧就变轨迹被过早删除,或长相门限太严max_age 调大(70 → 90),max_dist 放宽到 0.3
两辆车共用一个编号外观门限太松,两辆同款车被判成同一辆max_dist 收紧到 0.15;确认 deepsort.rknn 已加载
启动打印 falling back to SORTReID 模型缺失或加载失败确认 models/deepsort.rknn 存在;出现这条告警时交叉必换号
帧率只有个位数只开了 1 个 worker,或 ReID 拖慢pool.workers: 3;core_mask 可试 round_robin
--ros 报 cannot open ros topic驱动没起、两终端 ROS_DOMAIN_ID 不一致,或话题被 remap先起 ascamera;两边 export 同一个 N;话题保持原生名
提示 python: command not found没激活板上的 venvsource ~/ros2_venv/bin/activate
进阶 1 · 在画面里画出运动轨迹约 20 分钟

给每条轨迹存最近 30 个中心点,用 cv2.polylines 连成一条尾巴 —— 一眼看出「谁在往哪走」,也是流向统计的最简版本。

观察点:max_age 调大时,尾巴会在目标消失后继续往前画,这正是「预测还在推进」的可见证据。

进阶 2 · 数一数「过线车数」约 25 分钟

在画面中间画一条虚拟线,某条轨迹的中心点从一侧跨到另一侧时计数 +1(比较上一帧与这一帧的 y 符号)。这是卡口计数、门店客流的基本功。

必须用轨迹 ID 去重,不能按框数累加 —— 否则同一辆车被数几十次,正是第 02 节开头那个坑。

🧪 Vibe Coding · 自己把这段代码读明白约 20 分钟

你要产出三样:模块职责表 → 控制流流程图 → 时序图。提示词给全了,产出后与真源码逐条对照。

提示词 1 · 模块职责分析

请阅读这个多目标跟踪项目的七个模块:src/app.py、src/detect.py、src/preprocess.py、src/tracker.py、src/track.py、src/reid.py、src/rknnpool.py。
它们共同完成「读一帧 → 检测 → 匹配 → 画编号」。
请输出一张表,每行一个模块,四列:模块名 / 一句话职责 / 被谁调用 / 从哪个配置项取值。
要求指出关键函数的所在行号;说不清谁调谁的地方请标出来,不要猜。

提示词 2 · 控制流流程图

请按「主线程处理一帧」的视角,为 src/app.py 的主循环画一张流程图。
必须包含:检测池 put/get 的进出;tracker.predict 与 tracker.update 的先后;没有检测结果时如何处理;ReID 缺失时如何退回 sort 模式。
每个框写函数名,判断框写条件,不要用「处理」「分析」这类空词。

提示词 3 · 时序图

请为视频模式画一张时序图,参与者用真实名字:主线程、RKNNPool 的 worker 线程、ReIDPool 的 worker 线程、MjpegServer。
按顺序标出:读帧、检测入队、worker 推理、结果出队、裁剪目标送 ReID、Tracker.update、绘制、推流。
并标注哪些环节是并行流水线,哪些是同步等待。

自查三问:每个模块都能指到真实文件与行号?流程图里有「无检测」和「ReID 失败」两条分支?时序图里并行与串行分得清?

三处必然的差异(对答案用):

差异点AI 版常见写法工程版(本仓)为什么工程这么写
匹配顺序对全部轨迹和全部检测算一次 IoU,取最大先按外观级联(丢得少的先配),再用 IoU 兜底(tracker.py:81-89)IoU 在交叉、并行时会把两辆车配错;外观才是同一目标的判据
遮挡怎么续没检测到就把框留着不动,或直接丢掉每帧先 predict() 按速度推进,超 max_age 才删(tracker.py:103-105)静止保留会「卡住」,直接丢会断号;预测让框跟着车速走
ReID 怎么调在主循环里同步调用一次模型独立 ReIDPool 2 worker × 每线程独立 RKNN 实例,按提交序号回收保序NPU 上下文不能跨线程共享;外观推理必须与检测并行,否则帧率掉一半

改一处、先预测再验证:把 tracker.max_dist 从 0.2 改成 0.05,先写下你认为画面会发生什么,再跑一次对答案。

06互动:拖 max_age 和 max_dist,看遮挡后还是不是原 ID

📌 这一节是「掌握检查」:亲手改一个参数,看现象是否朝你预期的方向变,还要能说出为什么 —— 说得出,才算掌握。

第 04 节讲了四步,真正的判据只有两条线:「愿意等多久」= max_age,「像不像」= max_dist。命令跑一次只能看一种组合,这里做成能拖的时间轴。

⚡ 参数实验台 · 遮挡 N 帧后重现,还认不认得出
8
70
0.20
默认遮挡 8 帧、max_age = 70、max_dist = 0.20:轨迹没被删,外观漂移也在门限内,重现时沿用 ID 7。
互动 6-1|时间轴上一格是一帧:绿实线 = 检出到,黄色虚线 = 只在预测(被挡住);重现后的编号由两条门限共同决定。外观漂移按遮挡时长线性增长(0.06 + 0.012×遮挡帧数),用来模拟「挡得越久、长相记得越模糊」,属教学简化,不是模型真实输出。

怎么读出规律:先把 max_dist 拉到最大、只动 max_age —— 看到「等多久」这条线;再把 max_age 拉到最大、只动 max_dist —— 看到「像不像」这条线。任何一条断了,编号都会断。

遮挡 8 帧 · 默认
原 ID 保住

8 ≤ 70,且漂移 0.156 ≤ 0.20,恢复 ID 7。

max_age 拖到 5
轨迹被删

8 > 5,还没重现轨迹就没了,只能是新号。这就是「遮挡即换号」。

max_dist 拖到 0.10
认不出脸

位置在、轨迹也在,但长相过不了门限,照样判成新目标。

遮挡拉到 30
两条线一起紧

漂移涨到 0.42,超过默认 0.20 —— 现实中「挡太久」本来就会换号。

编号断不断,不取决于模型多聪明,取决于你敢等多久、敢认多像。max_age 管时间,max_dist 管长相,两条都要过。

07验证:三道小测

✅ 目标对账 · 验证掌握得有多好

回页首对照「实验目标」:① 检测与跟踪的分工讲得清吗 → 试 Q1;② --video 跑通、tracks=13 能否解释 → 回第 05 节自查;③ 两种失败调出来了没有 → 试 Q2 与 Q3,并回第 06 节把两条滑杆各推到极端看一眼。三条全 ✓,这一课就完成了。

Q1两辆车在画面里交叉而过,程序为什么不会把它们的 ID 互换?
因为卡尔曼预测的位置一定准
因为配对先比外观(512 维余弦距离 + 马氏门限),位置只用来卡门限,不是主要依据
因为 ID 是随机分配的,互换也看不出来
对。交叉时两车框严重重叠,靠 IoU 必然出错 —— 那正是 sort 模式的表现。工程版先按长相配对,再用 IoU 兜底,所以交叉不换人。第一个选项是常见误解:卡尔曼只是「猜大概在哪」,车急停急转时预测就会偏。
Q2画面上编号每隔几帧就换一个新数字,最该先调哪个参数?
把 detect.score_thr 调到 0.9
把 tracker.max_age 调大(或把 max_dist 放宽),让轨迹不要被过早删掉 / 判生
把 pool.workers 改成 1
对。编号暴涨只有两种成因:轨迹删得太快(max_age 太小),或外观过不了门限被当成新目标(max_dist 太小)。调 score_thr 只会让检测更少、编号更乱;workers 只影响帧率。
Q3画面里只有 5 辆车,编号却已经涨到 30 多。这说明什么?
程序出 bug 了,编号重复分配
编号是「历史上第几个被确认的目标」,只增不减,前面那些车已经开出画面了
每帧都给同一辆车重新发号
对。_next_id 每出现一个「配不上的检测」就 +1,删除轨迹时不回收号码。所以编号大只表示「历史上确认过很多目标」,与当前画面里有几辆车无关。要数当前车辆数,看终端那个 tracks=。

08章节总结:检测管「在哪」,跟踪管「还是谁」

这页讲了什么,它们怎么串起来

概念它解决了什么和别的关系
检测(9 输出)把每一帧的「有什么、在哪」找出来上游吃画面,下游是跟踪唯一的输入;classes 在这里就决定追谁
卡尔曼预测没检测到时也能给出「它大概在哪」补上检测的空档;推得太久会飞,靠 max_age 收尾
ReID + 余弦距离给「是不是同一个目标」一个可比较的数值配对的主依据;模型缺失就退回纯 IoU
级联匹配让刚丢的轨迹优先挑检测,避免被旧轨迹抢走把外观与 IoU 两层串起来;门限 max_dist / max_iou_distance
n_init / max_age一个管「什么时候才算真目标」,一个管「什么时候算走了」共同决定编号的干净与稳定,也就是第 06 节那两条线
编号 track_id给跨帧目标一个唯一身份,才能统计与计数只增不减;与「当前有几辆车」无关
推理池把检测与外观推理并行起来抢帧率横向于整条链:只影响 FPS,不影响编号质量
⚠ 三个反直觉的地方 ① 直觉是框重叠最多的就是同一辆车,实际是外观才是主判据、位置只做门限 —— 否则交叉必换号;
② 直觉是编号就是「画面里第几辆车」,实际是历史计数器,只增不减,删轨迹也不回收;
③ 直觉是跟踪比检测贵很多,实际是跟踪整套(预测 + 配对 + 绘制)只占几毫秒,瓶颈仍在检测那约 60ms。
💡 四条要注意 ① max_iou_distance 用 0.8 不是 0.7:配置注释写明 0.7 时移动目标易丢轨迹,这是踩过的坑;
② max_age: 70 是帧不是秒 —— 16 FPS 下约 4.4 秒,换成 30 FPS 只剩 2.3 秒,改帧率时这个值要跟着改;
③ ReID 输出是 512 维([1,3,128,64] → [1,512]),不是老文档里的 128 维;
④ 单图模式只起 1 个 worker,别拿单图去测帧率。

前置依赖 · 缺一样就跑不起来

类型具体项缺了会怎样 / 怎么补
环境ROS 2 Humble 两级 source + 相机节点(仅 --ros 需要);板上 Python 在 ~/ros2_venv--ros 起不来会打印 cannot open ros topic;未激活 venv 则 python: command not found
Python 库numpy、opencv-python、pyyaml、scipy(匈牙利匹配)、rknnlite、mjpeg_streamer缺 scipy 无法做代价矩阵配对;缺 mjpeg_streamer 启动即退出
模型 / 资源models/yolov8n.rknn(检测,9 输出)+ models/deepsort.rknn(ReID)+ test.mp4缺 ReID → 打印 falling back to SORT,交叉换号;缺视频就没有第 03 节那三张图
配置config/track_config.yaml:classes: [2]、mode: deepsort、max_age: 70、n_init: 3、max_dist: 0.2、min_confidence: 0.6缺 key 时 load_config 抛 ConfigError;mode 拼错直接报错
硬件 / 前置实验RK3576 板(3 核 NPU)+ EXP-04 检测基础 + EXP-06 的掩码/后处理概念单核开 3 worker 不会更快;不懂 DFL/NMS 就看不懂步骤 1 的 9 输出

这套代码用在哪儿

  • 路口卡口计数:虚拟线 + 轨迹跨越判定,按 ID 去重统计车流量与方向,替代地感线圈。
  • 门店 / 电梯客流:同一个人被反复计数的问题,靠跟踪 ID 天然解决;配 classes: [0] 只追 person。
  • 机器人跟人:锁住「要跟的那个人」的 ID,输出它的框中心作为跟随目标 —— 这是 EXP-11 的上游能力。
  • 不该用它的场合:只需要「这一帧有没有人」时不必上跟踪,多花的算力没有收益;相机在动、镜头切换的场景也不适用。
核心记忆点:检测管「在哪」,跟踪管「还是谁」。max_age 决定敢等多久,max_dist 决定敢认多像 —— 两条线都过,编号才不断。

下一个实验换一个方向:跟踪告诉我们「它一直在」,但说不出它离我多远。EXP-10 会用深度图把这个距离补上。