本节摘要:跟踪给运动目标一个持续的身份,避免每帧重跑昂贵的检测。本节从 MeanShift 的直方图迭代讲到 CSRT 的判别式相关滤波,给出 OpenCV Tracker 家族的选型对比,重点讲"检测初始化、跟踪保持、丢失重检测"的工程闭环与目标外观变化、遮挡两大挑战的应对。这是本教程的收官一节。
阅读完本节,你应当能够:
MeanShift——经典的颜色跟踪:
import cv2 hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask = cv2.inRange(hsv, (0, 60, 60), (10, 255, 255)) # 红色区间示例 hist = cv2.calcHist([hsv], [0], mask, [180], [0, 180]) cv2.normalizeHist = None hist = cv2.normalize(hist, hist).flatten() back_proj = cv2.calcBackProject([hsv], [0], hist, [0, 180], 1) ret, box = cv2.meanShift(back_proj, (x, y, w, h), (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 1))
初始化时算目标区域的色调直方图,此后每帧做反向投影(把图变成"哪里像目标的颜色热度图"),MeanShift 在热度图上迭代地把框推向密度中心。CamShift 是它的升级版,框大小随目标远近自适应。
现代跟踪器——CSRT 一族的标准三步:
tracker = cv2.legacy.TrackerCSRT_create() # 或 TrackerCSRT_create 视版本 tracker.init(frame, (x, y, w, h)) # 第一帧框定目标 ok, box = tracker.update(frame) # 每帧更新 if ok: p1 = (int(box[0]), int(box[1])) p2 = (int(box[0]+box[2]), int(box[1]+box[3])) cv2.rectangle(frame, p1, p2, (0, 255, 0), 2)
初始化一次、每帧 update、按返回值判定成败——所有 Tracker 系列跟踪器共用这个接口。新版本 OpenCV 把部分跟踪器挪到 legacy 或 contrib 包,名字对不上时查一下当前版本的模块归属(第 1 章第 2 节的版本意识在此再次应验)。
第三种"跟踪器"其实不是跟踪器:第 5 章第 2 节的光流法,goodFeaturesToTrack 在目标框内取点、LK 光流逐帧跟点、点的位移平均即目标运动——结构最透明,也最容易定制。
检测器每帧全图扫描,CPU 上百毫秒到秒级;跟踪器只围着上一帧的目标位置小范围找,毫秒级。算力账之外还有质量账:检测输出的是"这一帧的框",两个目标交错后身份会互换;跟踪器维护的是"同一个目标的连续历史",遮挡后恢复时身份仍在。所以工程标配是 detect then track:低频检测(每 N 帧或跟踪失败时)、高频跟踪(每帧)。检测贵而准、跟踪便宜而脆,二者互补。
反向投影把每帧变成"目标颜色热度图"后,MeanShift 做的事纯粹:从当前框位置出发,反复计算框内加权重心、把框中心移向重心,直到收敛。局部爬山注定两个短板:目标快速移动超出搜索范围就跟丢;框尺寸不更新,目标靠近变大就"框不住"(CamShift 修正了这一点)。适合背景颜色与目标差异明显的慢速目标,比如白墙前的彩色球。
KCF(核相关滤波)的思路:把跟踪表述为"在上一帧学一个能把目标从背景里分离出来的滤波器",学习时不仅用目标正样本,还利用循环移位生成大量负样本(周围的背景),判别力因此强于纯模板匹配。定位快、实现巧,是速度与精度的经典平衡点。
CSRT(通道与空间可靠性判别相关滤波)是 OpenCV 里精度档的代表:多通道特征加空间可靠性图,边界处理更讲究,能更好地应对形变与部分遮挡,代价是速度明显慢于 KCF。家族选型速查:
| 跟踪器 | 精度 | 速度 | 特点 | 适用 |
|---|---|---|---|---|
| Boosting/MIL | 低 | 中 | 老算法,教学意义为主 | 不推荐新项目 |
| KCF | 中 | 快 | 循环移位负样本,性价比高 | 实时、目标变化不剧烈 |
| CSRT | 高 | 慢 | 多通道加空间可靠性 | 精度优先、可容忍低帧率 |
| MedianFlow | 中 | 快 | 正反向一致性评估,遮挡即报失败 | 遮挡频繁需要快速知道失败的场景 |
| 光流 LK | 中 | 快 | 透明可控、易于定制 | 与其他方案组合使用 |
目标外观会变(转身、变形、光照改变),跟踪器学到的模型必须更新——但更新太快,遮挡物会被学进模型("模型污染");更新太慢,外观变化就跟不上。跟踪失败判定(update 返回 ok 为假)因此是工程上的关键设计:与其让框飘走误导下游,不如尽早宣告失败、触发重检测。MedianFlow 的正反向校验(帧 A 跟到 B、再从 B 反跟回 A,位置对不上即失败)就是为这个设计服务的。
把本章三节串成收官方案:MOG2 背景减除产出运动候选框(第 5 章第 2 节),新目标出现时初始化跟踪器;每帧 tracker.update 跟随(本节);每 N 帧或 update 失败时用背景减除的候选框校正或重新初始化。这套组合在 CPU 上轻松实时,且身份连续、抗短时遮挡。若目标类别重要(要区分人和车),把背景减除换成第 4 章的轻量检测器即可,骨架不变。
多人场景要开多个跟踪器并维护"目标表"(编号、跟踪器、丢失计数)。新检测框与现有跟踪框做 IoU 匹配(第 4 章第 2 节的 IoU 在此复用):匹配上则校正对应跟踪器,匹配不上的检测框开新目标,连续多帧未匹配的跟踪目标移除。丢失计数的容错(比如连续 15 帧丢失才移除)能避免目标被短暂遮挡就"注销重入"造成计数虚高。
跟踪器 init 时给的框不干净(框进了背景或只框住半张脸),学到的模型天生带病,后面怎么调参都救不回来。宁可人工确认第一帧的框,也不要拿一个勉强的自动检测结果直接 init。这个原则与第 4 章第 3 节"数据质量决定上限"一脉相承。
⚠️ 常见坑:跟踪失败后不重置直接继续 update,框在画面里"鬼漂"——看起来还在跟踪,实际输出的是垃圾。永远检查返回值,失败即走重检分支。另一个坑是拿视频里某帧的跟踪器去跟另一段视频,模型与目标不匹配,自然失败;跟踪器是有状态的,生命周期跟着目标走。
💡 关键直觉:跟踪器的强弱不在"跟得多紧"而在"失败时说不说真话"。快速报告失败的跟踪器(MedianFlow 类)配一个可靠的重检测,实战效果常好于"死不承认失败"的高精度跟踪器。工程上,失败判定与恢复路径的设计比跟踪器本身的选型更值得花时间。

会,这正是"模型污染"。缓解手段:CSRT 类跟踪器有自适应更新率参数可调低;更稳的结构性解法是把"跟踪"与"确认"分开——跟踪器只管给框,每 N 帧用检测器确认框内还是不是目标(详见本节完整方案),确认失败立即停用该跟踪器。牺牲一点算力,换来对污染的免疫。
每个目标一个实例,几十个目标就是几十个跟踪器,CPU 上 CSRT 会吃紧。规模上去之后的务实选择:目标多时换 KCF(快),或干脆退回"纯背景减除加 IoU 关联"的轻量方案——第 5 章第 2 节的候选框配上传帧间的位置关联,在很多场景已经是够用的"穷人版跟踪"。
能且推荐。跟踪器框住目标,框内跑稀疏光流,既能得到目标的运动向量(方向速度),又能在跟踪器短暂失效时用光流的位移"惯性外推"目标位置,兜住一两帧的空窗。第 5 章第 2 节的光流与本节的跟踪器在实战里经常这样互相搭桥。
把本章内容总装成一个多目标跟踪系统(入口大堂的人流持续跟踪),给出最小可用版本的设计。
状态表设计。每个目标一条记录:编号、跟踪器实例、丢失计数、上次框位置。新目标入场由背景减除的候选框触发(IoU 与现有目标都不匹配的框,本节 3.2 的匹配规则),初始化跟踪器、分配递增编号。
每帧主循环。所有跟踪器 update;成功的更新记录与画框(框上标编号),失败的递增丢失计数;丢失计数达到十五的目标注销(容错窗口,本节 3.3)。每三十帧跑一次背景减除做关联校正,防止长时间漂移。
参数初值。跟踪器选 KCF(人多时快);IoU 匹配阈值 0.3(低于它算同一目标);冷却与注销窗口按行人通过画面的时间尺度估(步行横穿约十秒,十五帧的窗口约合半秒遮挡容忍,可放宽到三十帧)。
验收实验。回放一段录像,统计三个数:编号跳变次数(身份不连续)、平均跟踪时长、每帧跟踪目标数与肉眼数的偏差。三个数都在可接受范围,系统可用;编号跳变集中在交叉走动处,就把 IoU 匹配加上运动预测(上一帧速度外推)再验。
这套系统没有用到任何本教程之外的概念——背景减除(第 5 章第 2 节)、跟踪器三步(本节)、IoU(第 4 章第 2 节)、形态学净化(第 2 章第 3 节)——五章的内容在一个一百来行的程序里各就各位。把它跑起来,你就完成了这门课从"认识函数"到"搭建系统"的跨越。
五章至此收官。从第 1 章的数组世界观,到第 2 章六件套、第 3 章特征匹配、第 4 章检测识别,再到本章的视频闭环,你已经具备搭建完整视觉应用的全部基础。下一步建议找一个真实小项目(如工位人数统计)从头到尾做完,教程里每个"动手实验清单"都是它的零件。