
去年在做一个低帧率视频下的行人追踪项目输入源只有10FPS左右的网络摄像头画面。一开始我天真地以为把YOLOv8检测的置信度阈值调低一点、把跟踪器的匹配阈值放宽一点就能搞定结果ID依旧频繁跳变——一个行人走到画面中间转个身就变成了“新目标”再拐个弯又换了一个ID。翻来覆去排查问题根本不在检测器精度而是帧率。帧率一旦降下来“显示上是连续运动、实际上却是离散采样”这件事会让基于边界框匹配的追踪器全线崩溃。后来我把光流法Optical Flow引入到追踪管线里用稀疏Lucas-Kanade光流跟踪目标内部的特征点在检测帧之间不断传播边界框再拿传播结果去和下一帧检测框做关联融合。这个组合把IDSWID Switch直接砍掉了近七成MOTA也涨了一截。这篇文章就把我在这套方案里的完整思路、代码做法和踩坑记录整理出来给同样被低帧率视频困扰的人一个可以直接参考的落地样板。1. 低帧率不是“少检测几帧”那么简单问题模型与失效机制1.1 帧间隔拉大后追踪器为什么会“掉链子”所有基于检测的追踪器DeepSORT、ByteTrack、BoT-SORT等都算核心都依赖一个东西把当前帧的检测框和上一帧的轨迹预测框做匹配最常用的相似度度量就是IoU。IoU对帧率极其敏感。假设一个目标的边界框尺寸是100×100像素目标在图像平面上以每秒300像素的速度运动。当帧率是30FPS时相邻帧位移只有10像素两帧IoU大概有0.82匹配毫无压力。但帧率降到10FPS位移变成30像素IoU掉到0.55左右如果帧率只有5FPS位移直接到60像素IoU就只剩下0.25上下。IoU低于0.3时无论ByteTrack还是DeepSORT关联分数都会跌破阈值追踪器就认为这个框和原有轨迹对不上于是新建一个ID。除了IoU运动模型也在低帧率下失效。DeepSORT和BoT-SORT内部都带卡尔曼滤波用匀速或匀加速模型外推下一帧位置。高帧率下相邻帧间隔极短目标在这几十毫秒里的运动近似直线线性外推很准但低帧率意味着相隔100毫秒甚至200毫秒目标完全可能在期间完成一次转身、急停或者变向线性模型预测出来的位置和真实位置相差很远。预测框不准匹配自然不稳。1.2 检测器在低帧率下的隐性退化很多人以为降低帧率只是追踪器吃亏检测器每一帧还在干活应该没问题。实际不是这样。低帧率视频往往伴随着运动模糊加剧。同一个摄像头曝光时间不变帧率降低后目标在单帧内移动的像素距离更大拖影更明显YOLOv8对模糊目标的特征提取能力会明显下降。还有一个容易被忽略的点低帧率视频在采集时常常伴随抽帧也就是把高帧率视频中的若干帧直接丢弃。这个操作会让目标的关键姿态变化被“吞掉”比如人弯腰捡东西的动作在高帧率下能拍到完整过程抽帧后只留下“站着”和“蹲着”两帧中间没有过渡。检测器检测出来框的形状会突变追踪算法拿一个突然矮一半的框和一个突然高回去的框做匹配很容易判定它们不是同一个目标。1.3 为什么单靠YOLOv8不够需要一个“运动先验”YOLOv8本质上是观测器它回答的是“这一帧画面里目标在哪”它不回答“目标从上一帧运动到这一帧经历了什么路径”。追踪器要做的恰恰是把这种瞬时观测连接成连续轨迹。在高帧率下帧与帧之间目标运动很小你甚至可以偷懒用“上帧检测框直接当作下帧预测框”。但在低帧率下观测之间出现了巨大的“真空期”你必须填补这个运动估计。这时候有两个选项一是依赖卡尔曼滤波等参数化运动模型二是用光流法做像素级运动估计。前者参数少、快但模型过于简单后者能直接测量目标每个特征点的实际位移提供更丰富的运动先验。这也是我最终选光流法辅助追踪的根本原因——我们需要的是一个能插在相邻检测时刻中间的、足够可靠的“运动插值器”。2. 光流法这一刀切在哪里才不伤手原理、假设与选型2.1 光流的两个核心假设以及低帧率下怎么补救光流法估计的是图像中每个像素点在两帧之间的运动向量。几乎所有经典光流算法都建立在两个假设上亮度恒定目标像素点在运动中亮度不变和短时运动足够小两帧之间点的位移不超过一定范围。低帧率直接打脸第二个假设。位移大了局部窗口内的像素匹配就失效。补救的手段也很直接用图像金字塔。OpenCV里的calcOpticalFlowPyrLK就是金字塔LK光流实现先在低分辨率层估计大位移再逐层细化到原分辨率。相当于先用“缩小的图”看全局粗运动再用原图抠细节。对低帧率场景金字塔层数直接决定你能补偿多大的位移这个参数后面详谈。2.2 稀疏光流与稠密光流选型逻辑光流按计算密度分成两类稀疏光流Lucas-Kanade法为代表只跟踪被选中的特征点稠密光流Farneback、RAFT等对每个像素都计算运动向量。对目标追踪这个任务我强烈推荐稀疏光流。原因有三个。第一追踪的主体是目标内部有区分度的角点/纹理点不需要背景每个像素都算。第二稀疏光流计算量低很多一个目标跟踪几十个点在CPU上都能跑实时而稠密光流在嵌入式设备上常常要卡几帧。第三稀疏光流的结果容易做鲁棒处理。你可以分别跟踪多个特征点然后取位移中位数抵消个别点跟丢或跟错的影响。Farneback稠密光流也不是没用它适合需要整个运动场的场景比如背景全局运动估计但纯粹为目标追踪服务的话稀疏LK更划算。两个算法的比较可以参考下面这张表维度稀疏Lucas-Kanade稠密Farneback计算开销低只算特征点高全图像素都要算适用对象单个/多个目标的特征点跟随全局运动场分析、背景补偿对位移大的容忍度依赖金字塔层数可调高一般过大位移容易糊鲁棒后处理方便逐点结果可做统计不直观需要后续滤波在追踪管线里的角色目标框传播相机运动补偿2.3 LK光流关键参数怎么定calcOpticalFlowPyrLK的参数不多但每个都直接影响低帧率下的表现。我按经验值给一个起点你先跑一轮再看效果。winSize搜索窗口大小。窗口太小大位移跟不住窗口太大匹配范围过宽容易找错点。常见取值21×21到31×31之间。如果目标本身很小比如远处行人只有30×50像素窗口可以缩到11×11或15×15。maxLevel金字塔层数。这就是应对低帧率大位移的核心参数。10FPS视频目标位移30~50像素时maxLevel2-3比较合适5FPS甚至更低建议maxLevel3-4。criteria迭代终止条件通常设为(cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_COUNT, 30, 0.01)。迭代次数太少位移收敛不好太多浪费时间30次足够了。minEigThreshold最小特征值阈值。低于这个阈值的点会被认为是“弱纹理点”而丢弃避免跟丢。默认1e-4偏低在低纹理区域内希望采更多点时可以降到1e-5甚至更小。一句话总结低帧率场景的调参优先级是“金字塔层数优先窗口其次”位移越大越要增加金字塔层数而不是盲目放大窗口。3. 完整实施用光流给YOLOv8追踪“续命”的双路管线3.1 总体设计这套方案的核心思路是不要让YOLOv8每帧都做检测。检测帧之间用光流作为一个轻量级的“运动插值器”把上一帧的边界框传播到当前帧。等到下一个检测帧来了再用检测框对传播框做关联和融合。这样做有两个好处。一是低帧率视频的检测质量本来就不稳定与其强制每帧都依赖检测框不如在检测帧之间插入一个更连续的估计二是光流传播的代价远低于一次YOLO推理算力紧张时你可以把检测频率进一步降低用光流“扛住”中间帧。整个管线的流程描述如下第0帧做YOLO检测初始化轨迹并在每个目标框内采样特征点。进入中间帧不做检测只用金字塔LK光流跟踪特征点根据特征点位移计算目标框的新位置。到达下一个检测帧YOLO检测出新框同时用光流传播得到预测框。两者做关联匹配。关联成功的目标用检测框校准光流点继续传播关联失败的目标按遮挡/丢失状态处理。3.2 边界框传播的具体实现光流传框的逻辑说起来简单做起来有几个细节。先看核心代码骨架。import cv2 import numpy as np def propagate_bbox_by_flow(prev_gray, cur_gray, bbox, points): 用稀疏光流传播目标框。 bbox: [x1, y1, x2, y2] points: 目标框内的特征点Nx2 返回新的bbox以及有效光流点 if points is None or len(points) 2: return bbox, None # 上一帧特征点要求是float32 prev_pts points.astype(np.float32).reshape(-1, 1, 2) cur_pts, status, _ cv2.calcOpticalFlowPyrLK( prev_gray, cur_gray, prev_pts, None, winSize(21, 21), maxLevel3, criteria(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 30, 0.01) ) cur_pts cur_pts.reshape(-1, 2) status status.reshape(-1) good_prev points[status 1] good_cur cur_pts[status 1] if len(good_cur) 2: return bbox, None # 中位位移抗异常点 dx np.median(good_cur[:, 0] - good_prev[:, 0]) dy np.median(good_cur[:, 1] - good_prev[:, 1]) new_bbox [ bbox[0] dx, bbox[1] dy, bbox[2] dx, bbox[3] dy, ] # 尺度变化估计用点对间距离比的中位数 if len(good_cur) 4: dist_prev np.linalg.norm(good_prev[:, None, :] - good_prev[None, :, :], axis2) dist_cur np.linalg.norm(good_cur[:, None, :] - good_cur[None, :, :], axis2) mask dist_prev 5 ratio (dist_cur[mask] / (dist_prev[mask] 1e-6)) ratio ratio[np.isfinite(ratio)] if len(ratio) 0: scale np.median(ratio) scale np.clip(scale, 0.7, 1.3) w new_bbox[2] - new_bbox[0] h new_bbox[3] - new_bbox[1] cx (new_bbox[0] new_bbox[2]) / 2 cy (new_bbox[1] new_bbox[3]) / 2 new_bbox [ cx - scale * w / 2, cy - scale * h / 2, cx scale * w / 2, cy scale * h / 2, ] return new_bbox, good_cur几个值得展开的细节为什么用中位数而不是均值。光流点里偶尔会混入背景点或者匹配错误的大偏移点均值会被这些离群值拉偏。中位数对离群值的鲁棒性高一个量级。在目标内部移动不统一时比如人的手臂摆动和躯干运动方向不一致中位数也能反映主体的整体位移。尺度估计要加clip限制。单纯用特征点距离比中位数估计目标缩放很容易被少数点对干扰一次跳跃可能从0.8跳到1.5。把尺度限制在0.7到1.3这个范围内能避免框突然放大缩小。对行人追踪实际帧间尺度变化不会那么大。3.3 检测帧的关联与融合策略每次检测帧来临时光流传播框和YOLO检测框需要做关联。我这里采用的策略和ByteTrack类似但多了一个候选框来源光流传播框。具体做法是把传播框当作一个“低质量检测框”参与关联。先对高置信度检测框做IoU匹配再把未匹配的传播框和低置信度检测框做第二轮匹配最后剩下的未匹配轨迹进入挂起状态。代码层面的关联部分简化如下def associate(tracks_propagated, detections, sigma_iou0.4): # 用IoU算代价矩阵匈牙利匹配 # 第一轮高置信度检测框 传播框 matches, unmatched_tracks, unmatched_dets \ iou_associate(tracks_propagated, detections, thresholdsigma_iou) # 传播框和检测框匹配上之后融合位置 # 权重建议检测框更大因为光流传播帧数越多累积漂移越严重 for track_id, idx, det_id in matches: fused_bbox detections[det_id] * 0.7 tracks_propagated[track_id].bbox * 0.3 tracks_propagated[track_id].bbox fused_bbox # 用检测框中心附近的特征点重新采样光流点 tracks_propagated[track_id].resample_points()为什么融合权重是检测框更大因为光流传播是一个递推过程每传播一帧误差都有累积的可能性。检测框虽然在低帧率下也会抖动但它是独立的观测相当于一次“绝对定位”。融合的意义是让轨迹位置既不要太跳检测框噪声也不要累积漂移光流传播所以检测框权重高一些。3.4 目标被遮挡或被截断时的回退逻辑低帧率视频里目标遮挡会变得非常棘手。遮挡发生得快恢复也快追踪器经常反应不过来。光流在这里能帮上忙是因为遮挡前跟踪到的特征点在遮挡瞬间会有两种表现点跟丢了或者点跳到遮挡物上。我在管线里专门设了一个置信度状态机。当目标框内光流幸存点比例低于30%时把轨迹状态置为lost。lost状态下不再用传播框参与关联而是继续用光流点跟踪一小段时间比如5帧如果后续检测帧里又出现能匹配上传播框的高置信度检测框就把这个轨迹“复活”。这里有一个容易做错的地方目标被完全遮挡时特征点跟丢速度和遮挡物有关。如果遮挡物是墙体点会直接消失如果遮挡物是另一个行人点可能跳到那人身上位移中位数可能会突变。所以我额外加了一个位移合理性判断一旦当前帧点位移中位数超过上一帧位移中位数的3倍以上就判定为“疑似点跳变”立刻停止用这些点做框传播转为等待检测结果。下面这张表是我实际用的状态处置策略情况判断条件处置方式正常跟踪光流内点率高位移连续传播框 检测框融合部分遮挡内点率30%-70%位移偏大降低融合权重以检测框为主严重遮挡内点率低于30%轨迹挂起保留光流点继续试探5帧点跳变点位位移中位数骤升3倍以上直接丢弃光流结果等待检测框目标出画面传播框越界或面积小于阈值终结轨迹并清理特征点这套状态机让整个追踪器在真实摄像头数据里稳了很多尤其是行人短暂被立柱挡住、两人交错的场景。4. 实测对比低帧率场景下追踪效果的前后差异4.1 实验设置验证这套方案时我用了一个模拟项目X的低帧率视频集故意把原始30FPS视频抽帧到10FPS和5FPS模拟真实低帧率链路。检测器用YOLOv8n输入分辨率640检测间隔D分别设为1即每帧都检测、2和3。D1相当于不使用光流传帧只做低帧率下的纯检测追踪作为对照基线D2表示每隔一帧检测一次中间帧全部靠光流传播D3是把检测频率压到最低中间连续两帧靠光流撑住。需要说明的是D2、D3模式中追踪器的输出帧率仍然是视频帧率只是检测器本身偷懒了。这也是这套方案在生产环境里真正的价值追踪输出不降帧率但YOLO推理次数大幅减少。4.2 量化指标的改善在测试集上我对比了D1纯检测追踪和D2/D3光流辅助的表现。为了公平D1模式里追踪器的关联阈值经过专门的调优已经在10FPS下尽可能减少了IDSWD2/D3则使用本文上面描述的光流辅助管线。场景模式MOTAIDF1IDSWYOLO推理次数10FPSD1 纯检测追踪62.155.486100%10FPSD2 光流辅助68.863.92950%10FPSD3 光流辅助66.261.03833%5FPSD1 纯检测追踪43.638.2142100%5FPSD3 光流辅助54.751.36633%最直观的结论有两条。第一光流辅助在10FPS下把IDSW从86降到29IDF1涨了8.5个百分点D2模式甚至比每帧检测还多用一半算力。第二D3在5FPS场景下表现依然不错IDSW只略高于10FPS的D1基线但YOLO推理次数只有三成。这说明光流传播确实充当了一个不错的“轻量级追踪主力”检测器只是用来定期校准。4.3 直观案例之前ID跳变、现在线框稳定数字之外实际视频效果更能说明问题。在10FPS抽帧样本里有两个行人在画面中间错身而过。D1基线模式下两个ID在交错后完全互换了一次后面重新分配ID跟踪线框在目标身上跳来跳去。换成光流辅助的D2模式后交错过程里两个目标的特征点依然各自跟随目标主体传播框在交叉瞬间会稍微贴近但不会合并检测帧到来后再把位置拉回到真实框上。肉眼上看线框抖动变小ID一直稳定到目标消失。这就是我一直强调的点低帧率追踪最大的痛点不是“框不准”而是“ID不稳定”。在监控场景里ID一旦跳变下游的轨迹分析、人流统计、绊线检测全部乱套。5. 工程落地中容易翻车的细节清单基于项目里的踩坑记录5.1 光流点管理什么时候重采样光流点不是初始化一次就万事大吉的。目标变形、自我遮挡、光照变化都会让最初采样的点逐渐丢失。所以每次检测帧校准完成后要重新在融合后的框内采样特征点。但不是无脑全部重采那样会丢掉光流连续性的优势。我的做法是保留上一帧跟踪成功的点然后如果当前特征点数量少于目标框面积对应点数的30%时再补充采样。另外特征点数量要控制。目标框过大时goodFeaturesToTrack会返回几百个点浪费算力目标框很小时点太少又不稳定。我一般限制每个目标最多120个点最少5个点。小于5个就放弃光流传播直接用上一帧位置。5.2 相机自身运动带来的干扰很多低帧率视频来自固定摄像头但固定不代表完全静止风、振动、云台微调都会让背景运动。如果光流点里混入了背景点位移中位数会被背景带动目标框就会跟着背景“漂移”。处理这个问题有两个思路。一个是采样点的时候尽量选目标框内部远离边缘的点减少边缘背景点的混入另一个是做强背景运动估计用视频角落处的背景点拟合一个全局运动模型然后把目标光流位移减去全局分量。前者简单后者更可靠但复杂。我的经验是摄像头非常固定的场景第一种足够摄像头有轻微抖动的场景建议至少先看看光流矢量场的整体直方图再用全局位移修正。5.3 算力预算与检测间隔的动态调节固定D值虽然简单但不够聪明。目标运动慢时可以把检测间隔拉大目标运动快时需要让检测器更勤快一点。我最后在项目里用的是一种简单自适应策略每帧统计光流位移中位数当连续3帧位移中位数都小于5像素时把D上限从2调到3当位移中位数大于25像素时强制把下一帧设为检测帧。这样做的理论依据是位移小时传播误差累积慢可以省检测位移大时单个光流传播误差相对变大需要尽快用检测框校准。光流的单帧成本其实很低。用YOLOv8n在640分辨率下推理一次大概30-50ms而一次稀疏LK传播追踪50个目标、每个目标几十个点在i5级别的CPU上也只要5-8ms。也就是说检测间隔每拉大一倍就相当于省了一半以上的推理时间光流自身的开销几乎可以忽略。5.4 参数调试经验总结最后聊点调试心得。第一低帧率追踪如果表现不好先别怀疑网络先看光流点颜色对不对。把光流点的轨迹可视化出来如果点的运动方向和实际画面里目标的运动方向明显不一致优先调整maxLevel和winSize这两个参数几乎决定一切。第二调minEigThreshold时不要贪多。低阈值能采到更多点但也会采进很多纹理不明确的点跟丢率反而更高。目标纹理丰富时用默认值就好目标本身纹理弱纯色衣服、白车再考虑降低。第三检测帧融合时不要只信检测框。低帧率下检测框本身也会抖动你如果用官方默认的conf0.25经常会看到框忽大忽小。我建议检测框在参与融合前先做一次轻量级平滑比如对宽高做指数移动平均。第四多目标场景下每个目标的光流计算应该独立做。有人图省事把全图的点扔给calcOpticalFlowPyrLK一次性算结果不同目标之间的点会互相干扰尤其在两个目标靠近时点会跳来跳去。按目标分别调用函数虽然多几毫秒但稳定性好得多。根据我个人经验这套检测稀疏光流双路追踪方案不是要替代纯检测追踪而是给低帧率约束下的追踪问题提供一条很实用的逃生通道。它最大的优点是把“追踪连续性”和“检测成本”解耦你用更低频率的检测换来基本不降级的追踪质量这在边缘设备、带宽受限的摄像头链路上尤其值得一试。最近我还想顺着这个方向看看能不能把光流传播置信度直接喂给下游融合模块让业务层知道当前轨迹的“可信度”而不是只知道一个框的坐标。如果有人也在做类似方向欢迎一起交流。