CMC:相机运动补偿
Deep-OC-SORT 的第二个通道:每帧估计相机自身 的运动,把全体轨迹的参考坐标搬到新相机坐标系下,再走正常的预测-关联流程。代码位置:deep_ocsort/cmc.py(估计器)、deep_ocsort/ocsort.py(调用与轨迹搬移)、deep_ocsort/kalmanfilter.py(KF 状态搬移)。
一、为什么需要 CMC
多目标跟踪的运动模型默认相机静止:目标在画面里的位移 = 目标自身的运动。相机一动(手持、车载、云台转动、变焦),这个等式破功:
画面位移 = 目标运动 ⊕ 相机运动 ← 两者耦合,检测框直接观测到的只有左边
相机运动的受害者清单:
| 模块 | 相机不动时的假设 | 相机动了会怎样 |
|---|---|---|
| 卡尔曼速度 | 画面位移即目标速度 | 速度估计混入相机速度,全是偏的 |
| VDC 方向 | 惯性方向是目标行进方向 | 方向整体偏转,密集处配错加剧 |
| OCR 找回锚点(last_observation) | 目标会回到"最后被看到的位置" | 锚点还留在旧坐标系,找回轮大面积失效 |
| IoU 关联 | 预测框与检测框同一坐标系 | 整体错位,IoU 集体下跌 |
典型症状:相机一抖,满屏 ID 切换、轨迹集体断裂。
CMC 的解法:先解耦再跟踪------用背景估计相机运动,把轨迹全部"搬"过去,让等式右边只剩目标运动。
二、怎么估计相机运动
2.1 核心原则:只用背景
compute_affine(img, bbox) 第一步就把本帧检测框区域掩掉:
python
mask = np.ones_like(img)
for bb in bbox: mask[bb[1]:bb[3], bb[0]:bb[2]] = 0 # 检测框内不取点
画面里"动的东西"有两类:目标(自己动)和背景(跟着相机动)。目标是干扰源------一辆车和相机同向同速行驶时它在画面里不动,反向时动得飞快,混进估计就把目标运动泄漏进"相机运动"。掩掉检测框,剩下的特征点全部假设属于背景,其位移即相机运动的像。
2.2 方法一:稀疏光流(sparse,默认)
上一帧灰度图 + 上帧角点
↓ cv2.calcOpticalFlowPyrLK(金字塔 LK 光流)
本帧角点跟踪对
↓ cv2.estimateAffinePartial2D(RANSAC)
2×3 仿射矩阵 [m | t]
- 取角点 :
goodFeaturesToTrack(maxCorners=3000, qualityLevel=0.01, minDistance=1, blockSize=3)------ Shi-Tomasi 角点,纹理丰富处密集采样; - 跟踪:LK 金字塔光流逐点算位移,两帧间小运动足够准;
- RANSAC 估刚体变换 :
estimateAffinePartial2D只估旋转+缩放+平移(4 自由度,不含剪切)------相机小幅运动的主成分就是这三个,自由度少则抗噪强;RANSAC 自动剔除跟丢/误配的离群点(正好补上掩码漏掉的运动目标); - 兜底:首帧只记录返回单位矩阵;匹配点 < minimum_features(10) 不估、返回单位矩阵;估计失败(返回 None)也回单位矩阵。
2.3 方法二:SIFT 特征匹配(sift)
上一帧 SIFT 描述子 ── 暴力匹配(k=2) ──> Lowe 比值筛选(0.7) ──> RANSAC 仿射
SIFT 对光照变化和较大位移更鲁棒(光流假设亮度恒定),代价是提取与匹配更慢。--cmc_method sift 切换。两种方法输出同构(2×3 仿射),后处理完全一致。
2.4 固定相机时的行为
固定机位下相邻帧背景不动,估出的仿射≈单位矩阵(RANSAC 在纯噪声上拟合出的旋转/缩放/平移都接近 0),轨迹坐标几乎不变------CMC 对固定相机是无害的常量开销(一次角点检测 + 一次光流,每帧固定毫秒级),因此默认开启不需要按场景手动切换。
三、拿到仿射之后:轨迹搬移
OCSort.update 每帧把仿射矩阵发给全体轨迹:
python
transform = self.cmc.compute_affine(img, dets[:, :4])
for trk in self.trackers:
trk.apply_affine_correction(transform)
KalmanBoxTracker.apply_affine_correction 搬三处:
3.1 last_observation(OCR 找回锚点)
python
ps = last_observation[:4].reshape(2, 2).T # 4 坐标排成 2x2(两个角点)
ps = m @ ps + t
x1,y1,x2,y2 重排成两列(左上角点、右下角点),各自仿射。有真实观测的轨迹才搬(占位符全 -1 跳过)。
3.2 observations 字典(VDC 参考点)
Δt 范围内(速度估计会用到的那几条)的观测同样搬------VDC 用的参考点与惯性方向必须在同一坐标系,否则夹角是错的。
3.3 KF 状态(kalmanfilter.py apply_affine_correction)
8 维(默认):
python
big_m = np.kron(np.eye(4), m) # 2x2 → 8x8 块对角
x = big_m @ x;x[:2] += t # [x,vx]、[y,vy]、[w,vw]、[h,vh] 四对各自变换
P = big_m @ P @ big_m.T # 协方差共轭变换
- 每个物理量与其速度组成 2 维子空间被 m 旋转缩放------速度向量跟着相机转,方向语义保持;
- 平移 t 只加在中心 x、y(相机平移不改尺寸与速度);
- 共轭变换保持 P 正定,不确定性椭圆跟着旋转。
7 维 (new_kf_off):只搬 [x,y] 与 [vx,vy],s(面积)、r(宽高比)是非线性量无法直接仿射,原地不动------7 维模式下 CMC 的补偿打了折扣,这是新 KF 与 CMC 配合更优的原因之一。
3.4 冻结快照同步搬移
漏检中的轨迹(attr_saved 非空且 observed 为 False)连快照一起搬:快照里的 x、P、last_measurement 同样变换------找回重放的起点也在新坐标系,ORU 虚拟轨迹不会从旧坐标出发。
四、在每帧流程中的位置
① 检测分拣(conf > track_thresh)
② 外观特征提取(embedding)
③ CMC:估仿射 → 全体轨迹搬移(last_observation / observations / KF 状态 / 冻结快照)
④ 全体轨迹 predict(在已校正的坐标系里外推)
⑤ 第一轮关联(IoU + VDC + 外观AW)→ ⑥ OCR 找回轮 → ⑦ 冻结/建档 → ⑧ 输出清理
CMC 排在 predict 之前:先搬坐标系再外推,速度估计建立在正确的基准上;排在 embedding 之后无依赖关系(源码即此顺序)。
五、参数与调参
| 参数 | 默认 | 作用 |
|---|---|---|
--cmc_off |
不传(开) | 关闭 CMC(仿射恒为单位矩阵) |
--cmc_method |
sparse | sparse 稀疏光流 / sift 特征匹配 |
| minimum_features | 10(构造参数) | 匹配点低于它不估仿射,返回单位矩阵 |
| sparse_flow_param | 预置 dict | 角点检测参数(maxCorners=3000 等) |
调参直觉:
- 固定相机:默认开即可,仿射≈单位矩阵,无害;
- 相机剧烈晃动(手持):默认 sparse 通常够用;光照突变(隧道口)时 LK 光流跟丢率高,试
--cmc_method sift; - 画面纹理极少(黑夜、空墙):角点数可能低于门槛,CMC 自动退化为不补偿(打 Warning "not enough matching points")------此时要么接受,要么给
qualityLevel降到更低(构造参数); - 纯静止机位想省这点开销:
--cmc_off。
六、局限
- 仅 4 自由度刚体模型(旋转+缩放+平移):透视效应明显的场景(相机平移近景)实际是单应变换,partial affine 只是近似------但 MOT 相邻帧位移小,近似误差有限;
- 背景纹理依赖:无纹理画面无点可估,自动退化为不补偿;
- 检测框外的运动目标仍可能漏进特征点:掩码只挡检出的框,漏检目标(检测没框出来的)会污染光流------RANSAC 能剔掉一部分,剩不多的污染由 4 自由度的强约束兜底。
| 描述 | 链接 |
|---|---|
| 项目 | 基于 Ultralytics + Deep-OC-SORT 多目标跟踪与改进 |
| 往期回顾 | 链接 |
| 数据集 | 数据集汇总 |
| 软件开发 | 深度学习软件开发汇总 |