一、代码解析
覆盖 deep_ocsort/ 核心包(移植自 Deep-OC-SORT-main 的 integrated_ocsort_embedding 变体)中本工程实际使用的类、方法与模块级函数------源码里未被调用的 filterpy 遗留方法与预留接口不列入。分析以本工程实际代码为准。
🎯 1. EmbeddingComputer类(外观特征提取器:裁剪预处理 → ckpt.t7 推理 → L2 归一化,支持整框/网格两模式)
文件: deep_ocsort/embedding.py(重写适配层)
| 类初始化 | 输入 | 默认值 | 作用 |
|---|---|---|---|
__init__ |
model_path: 权重路径 | None(用包内 deep/checkpoint/ckpt.t7) | 记录路径,模型惰性加载 |
| grid_off: 网格开关 | True | True 整框单特征,False 上中下三条 | |
| max_batch: 推理批大小 | 256 | 分批前向上限 | |
| use_cuda: 允许用 GPU | True | 与 torch.cuda.is_available() 共同决定设备 |
| 类属性 | 类型 | 输入 | 作用 |
|---|---|---|---|
| self.model | Net/None | None | ReID 网络(首次用时 _initialize_model 加载) |
| self.grid_off | bool | grid_off 入参 | 网格模式开关 |
| self.max_batch | int | max_batch 入参 | 推理批大小 |
| self.crop_size | 元组 | (64, 128) | 裁剪 resize 目标(宽64 高128,Market1501 约定) |
| self.device | torch.device | cuda 可用且 use_cuda 时 cuda,否则 cpu | 推理设备(源码硬编码 cuda+half,此处自适应) |
| self.model_path | 字符串 | 解析后的权重路径 | ckpt.t7 位置 |
| 类定义方法 | 输入 | 输出 | 作用 |
|---|---|---|---|
| _initialize_model | 无 | 无 | Net(reid=True) 加载 ckpt.t7 权重(兼容包 net_dict 键),to(device) 并 eval |
| _crop_to_tensor | img: 原图(BGR) x1,y1,x2,y2: 框坐标 | 1xCxHxW 张量 | 裁剪→BGR 转 RGB→resize 64x128→/255→ImageNet 均值方差归一化→CHW 张量(空块黑图兜底) |
| _get_horizontal_split_patches | img: 原图 box: 检测框 | 3xCxHxW 张量 | 框横切上中下三等分,各走一遍 _crop_to_tensor |
| compute_embedding | img: 原图 bbox: Nx4 框 | Nx512(grid_off)/ Nx3x512(网格)数组 | ① 惰性加载模型 ② 逐框产 patch(整框 1 块或网格 3 块) ③ cat 后按 max_batch 分批前向 ④ L2 归一化 ⑤ 网格模式 reshape 回 (N,3,512) ⑥ 转 cpu numpy |
🎯 2. CMCComputer类(相机运动补偿器:估计相邻帧仿射变换,供轨迹整体搬移)
文件: deep_ocsort/cmc.py(重写适配层)
| 类初始化 | 输入 | 默认值 | 作用 |
|---|---|---|---|
__init__ |
minimum_features: 最少匹配点数 | 10 | 低于它不改仿射(返回单位矩阵) |
| method: 估计方法 | "sparse" | sparse 稀疏光流 / sift 特征匹配 |
| 类属性 | 类型 | 输入 | 作用 |
|---|---|---|---|
| self.minimum_features | int | minimum_features 入参 | 最少匹配点数门槛 |
| self.prev_img | 数组/None | None | 上一帧灰度图(光流法基准) |
| self.prev_desc | 光流角点或 SIFT 描述子/None | None | 上一帧特征(光流法为角点数组,SIFT 法为 kp, desc) |
| self.sparse_flow_param | dict | 预设 | goodFeaturesToTrack 参数(maxCorners=3000、qualityLevel=0.01、minDistance=1、blockSize=3) |
| self.comp_function | 函数 | 按 method 选 | 仿射估计函数(_affine_sparse_flow / _affine_sift) |
| 类定义方法 | 输入 | 输出 | 作用 |
|---|---|---|---|
| compute_affine | img: 当前帧(BGR) bbox: Nx4 本帧检测框 | 2x3 仿射矩阵 | ① 转灰度 ② 检测框区域内掩码置 0(运动目标区域不取点) ③ 调 comp_function 估计仿射 |
| _affine_sparse_flow | frame: 灰度图 mask: 取点掩码 | 2x3 仿射矩阵 | ① goodFeaturesToTrack 取角点 ② 首帧只记录返回单位矩阵 ③ calcOpticalFlowPyrLK 跟踪上帧角点 ④ 匹配点 > minimum_features 时 estimateAffinePartial2D(RANSAC) 估刚体变换,失败兜底单位矩阵 ⑤ 记录本帧角点 |
| _affine_sift | frame: 灰度图 mask: 取点掩码 | 2x3 仿射矩阵 | ① SIFT 提取描述子 ② 暴力匹配 + Lowe 0.7 比值筛好点 ③ 好点足够时 RANSAC 估仿射,不足返回单位矩阵 ④ 记录本帧描述子 |
🎯 3. OCSort类(主跟踪器:每帧分拣 → 特征 → CMC → 两轮关联 → 建档/冻结 → 输出清理)
文件: deep_ocsort/ocsort.py
| 类初始化 | 输入 | 默认值 | 作用 |
|---|---|---|---|
__init__ |
det_thresh: 检测置信度门槛 | - | ≤ 它的检测框直接丢弃,不设低分二次关联 |
| max_age: 丢失判死帧数 | 30 | time_since_update 超过它即删轨迹 | |
| min_hits: 输出资格线 | 3 | 连续命中达到才有输出 | |
| iou_threshold: 关联 IoU 下限 | 0.3 | 两轮关联的终审阈值 | |
| delta_t: VDC 回看帧数 | 3 | 传给 KalmanBoxTracker | |
| asso_func: 关联相似度函数名 | "iou" | 从 ASSO_FUNCS 取 iou/giou/diou/ciou/ct_dist | |
| inertia: VDC 权重 | 0.2 | 方向一致性项的缩放系数 | |
| w_association_emb: 外观权重 | 0.75 | 外观代价的基准权重(AW 在它附近浮动) | |
| alpha_fixed_emb: EMA 基准系数 | 0.95 | 检测分越高越接近它,分低趋近 1 | |
| aw_param: AW 差异上限 | 0.5 | compute_aw_new_metric 的 max_diff | |
| embedding_off: 关外观 | False | True 时不算检测特征 | |
| cmc_off: 关 CMC | False | True 时不算仿射校正 | |
| aw_off: 关 AW | False | True 时外观权重固定为 w_association_emb | |
| new_kf_off: 关 8 维 KF | False | True 时轨迹用 7 维 KF | |
| grid_off: 关网格 | False | True 时整框单特征(Nx512),False 上中下三条(Nx3x512) | |
| embedder: 外观特征提取器 | None | 源码无此参数(构造函数内部自建 FastReID 版);本工程新增,None 时自建适配版 EmbeddingComputer,也可由封装层传入实例 | |
| cmc: 相机运动补偿器 | None | 源码无此参数(构造函数内部自建);本工程新增,None 时自建适配版 CMCComputer,也可由封装层传入实例 |
| 类属性 | 类型 | 输入 | 作用 |
|---|---|---|---|
| self.max_age / min_hits / iou_threshold | 数值 | 同名构造参数 | 判死线 / 输出资格线 / 关联终审阈值 |
| self.trackers | list | \[\] | 存活轨迹(KalmanBoxTracker)列表 |
| self.frame_count | int | 0 | 已处理帧数 |
| self.det_thresh | 数值 | det_thresh | 检测门槛 |
| self.delta_t | int | delta_t | VDC 回看帧数 |
| self.asso_func | 函数 | ASSO_FUNCSasso_func | 关联相似度函数 |
| self.inertia | 数值 | inertia | VDC 权重 |
| self.w_association_emb / alpha_fixed_emb / aw_param | 数值 | 同名构造参数 | 外观权重 / EMA 基准 / AW 上限 |
| self.embedder | EmbeddingComputer | embedder 入参 | 外观特征提取器 |
| self.cmc | CMCComputer | cmc 入参 | 相机运动补偿器 |
| self.embedding_off / cmc_off / aw_off / new_kf_off / grid_off | bool | 同名构造参数 | 五个新增模块的开关 |
| 类定义方法 | 输入 | 输出 | 作用 |
|---|---|---|---|
| update | output_results: Nx5 检测数组 img_numpy: 当前帧原图(BGR) | Kx5 数组 x1,y1,x2,y2,id | 一帧执行步骤: ① 帧计数加 1,按第 5 列取置信度,只留 > det_thresh 的框 ② 未关外观且非空时 embedder.compute_embedding 提取检测特征 ③ 未关 CMC 时 cmc.compute_affine 估计仿射,全体轨迹 apply_affine_correction ④ 算每个检测的 EMA 系数 dets_alpha(分低趋 1 信历史) ⑤ 全体轨迹 predict 出预测框 trks 与特征 trk_embs,NaN 坏轨迹删除 ⑥ 备 velocities / last_boxes / k_observations ⑦ 第一轮 associate(IoU+VDC+外观AW) ⑧ 配上的 update(bbox) + update_emb(emb, dets_alpha) ⑨ 第二轮 OCR:落单检测 vs 落单轨迹 last_boxes 纯 IoU 重匹配,配上的同样 update + update_emb ⑩ 仍落单轨迹 update(None) 冻结 ⑪ 仍落单检测建新轨迹(带特征、按开关选 KF) ⑫ 倒序遍历输出(本帧配上且 hit_streak≥min_hits 或冷启动期)与判死(time_since_update>max_age) |
🎯 4. KalmanBoxTracker类(轨迹类:一条轨迹的运动状态、观测历史与外观特征)
文件: deep_ocsort/ocsort.py
| 类初始化 | 输入 | 默认值 | 作用 |
|---|---|---|---|
__init__ |
bbox: 建档检测框 x1,y1,x2,y2,conf(5 元素数组) | - | 用检测框初始化卡尔曼状态,并登记计数器与观测记录 |
| delta_t: VDC 速度方向估计回看帧数 | 3 | 存入 self.delta_t 供 update 取历史观测用 | |
| orig: True 用 filterpy 原版 KF | False | False 用本文件 KalmanFilterNew(含 ORU 机制) | |
| emb: 该检测框的外观特征(512 维或 3x512 维数组) | - | 新轨迹的初始特征 | |
| alpha: 初始 EMA 系数 | - | 兼容入参,构造体内未使用(EMA 在 update_emb 时才生效) | |
| new_kf: True 用 8 维 KF | - | False 退回 7 维 KF(中心+面积+宽高比建模) |
| 类属性 | 类型 | 输入 | 作用 |
|---|---|---|---|
| self.kf | KalmanFilterNew | 构造时按 new_kf 建 8 维或 7 维 | 卡尔曼滤波器(F/H/P/R/Q 按 KF 类型配置) |
| self.new_kf | bool | new_kf 入参 | 记住 KF 类型,决定 update/predict 走哪套坐标转换与噪声 |
| self.bbox_to_z_func | 函数 | 按 new_kf 选 convert_bbox_to_z_new / convert_bbox_to_z | 检测框转观测向量的函数 |
| self.x_to_bbox_func | 函数 | 按 new_kf 选 convert_x_to_bbox_new / convert_x_to_bbox | 状态向量转检测框的函数 |
| self.time_since_update | int | 0 | 距上次配上检测的帧数(predict 加 1、配上归 0) |
| self.id | int | 类计数 count 自增后取值 | 轨迹全局编号(从 0 起,输出时 +1) |
| self.history | list | \[\] | predict 过程的预测框历史(每次 update 清空) |
| self.hits | int | 0 | 累计配上检测次数 |
| self.hit_streak | int | 0 | 连续配上检测次数(漏一帧即清 0,输出资格线) |
| self.age | int | 0 | 经历的 predict 次数(帧年龄,观测字典的键) |
| self.last_observation | 数组 | -1,-1,-1,-1,-1 占位 | 最近一次真实观测框(OCR 找回轮的锚点;全 -1 表示还没配上过) |
| self.history_observations | list | \[\] | 全部真实观测框的时序列表 |
| self.observations | dict | {} | 帧年龄 → 观测框 的字典(VDC 取 Δt 前观测用) |
| self.velocity | 数组/None | None | 由观测对估计的归一化速度方向 vy, vx(VDC 用) |
| self.delta_t | int | delta_t 入参 | VDC 回看帧数 |
| self.emb | 数组 | emb 入参 | 轨迹外观特征(EMA 维护,第一轮关联的外观锚点) |
| self.frozen | bool | False | 漏检冻结标记(冻结时 predict 把宽高速度清零) |
| count | 类属性 | 0(OCSort 构造时清零) | 轨迹总数计数器,给新轨迹发号 |
| 类定义方法 | 输入 | 输出 | 作用 |
|---|---|---|---|
| update | bbox: 配上的检测框或 None | 无 | ① 框非空:置 frozen=False,从 observations 取 Δt 前观测与 last_observation 算速度方向,登记 last_observation / observations / history_observations,time_since_update 归 0、hits 与 hit_streak 加 1,卡尔曼修正(8 维按目标尺寸算 R) ② 框为 None:KF.update(None) 触发 ORU 冻结快照,置 frozen=True |
| update_emb | emb: 检测特征 alpha: 本次 EMA 系数 | 无 | 轨迹特征 EMA 更新:emb = alpha*旧 + (1-alpha)*新,再 L2 归一化 |
| get_emb | 无 | 数组 | 返回当前轨迹特征 |
| apply_affine_correction | affine: 2x3 仿射矩阵 | 无 | 相机运动补偿:把本轨迹记住的框整体搬进新相机坐标系 ① 更新 self.last_observation:4------OCR 找回轮的锚点框,按角点仿射(sum>0 守门,全 -1 占位不搬) ② 更新 self.observationsage-Δt...age 中有记录帧的观测框------VDC 速度方向的历史依据 ③ 转发 self.kf.apply_affine_correction------KF 状态/协方差/冻结快照随之搬移(见 🎯5 同名方法) 总结:三份记忆搬到同一坐标系后,predict 与两轮关联比较的坐标才同帧,差值纯粹是目标自身运动 |
| predict | 无 | 1x4 预测框 | ① 8 维时负宽高保护(w+vw≤0 则 vw=0,h+vh 同理) ② frozen 时把宽高速度清零 ③ 8 维按当前宽高算 Q,KF.predict 外推 ④ age 加 1,time_since_update>0 时 hit_streak 清 0、time_since_update 加 1 ⑤ 预测框入 history 并返回 |
| get_state | 无 | 1x4 框 | 状态向量转 x1,y1,x2,y2(新建轨迹未配上时的输出用) |
🎯 5. KalmanFilterNew类(卡尔曼滤波器:标准 predict/update + ORU 冻结回滚 + CMC 仿射校正)
文件: deep_ocsort/kalmanfilter.py(自 filterpy KalmanFilter 扩展)
| 类初始化 | 输入 | 默认值 | 作用 |
|---|---|---|---|
__init__ |
dim_x: 状态维数(8 维 KF 为 8,7 维为 7) | - | 按维数建零初始化的 x/P/Q/F/H/R 等矩阵 |
| dim_z: 观测维数(4) | - | 观测向量维数 | |
| dim_u: 控制输入维数 | 0 | 未使用 |
| 类属性 | 类型 | 输入 | 作用 |
|---|---|---|---|
| self.x / self.P | 数组 | zeros / eye(dim_x) | 状态向量与协方差(由 KalmanBoxTracker 构造后覆写 F/H/P/Q) |
| self.F / self.H / self.Q / self.R | 数组 | eye / zeros | 状态转移 / 观测映射 / 过程噪声 / 观测噪声 |
| self.K / self.y / self.S / self.SI | 数组 | zeros | 卡尔曼增益 / 残差 / 系统不确定度及其逆(update 时算出) |
| self.x_prior / P_prior、x_post / P_post | 数组 | x/P 副本 | 预测后 / 修正后的状态与协方差快照 |
| self.inv | 函数 | np.linalg.inv | S 求逆函数(可换伪逆) |
| self.history_obs | list | \[\] | 观测序列(None 表示漏检帧,ORU 用) |
| self.attr_saved | dict/None | None | freeze() 存的整个属性字典快照 |
| self.observed | bool | False | 上一帧是否有观测(False→True 触发 unfreeze) |
| self.last_measurement | 数组/None | None | 冻结前的最后一次观测(ORU 虚拟轨迹起点) |
| 类定义方法 | 输入 | 输出 | 作用 |
|---|---|---|---|
| predict | u/B/F/Q 可选覆盖 | 无 | 标准预测:x=Fx,P=FPF'+Q(8 维 KF 每帧传入按当前宽高算的 Q) |
| update | z: 观测向量或 None R: 观测噪声覆盖 H: 观测映射覆盖 new_kf: KF 类型 | 无 | ① z=None 且刚有观测:记 last_measurement、freeze() 存快照,置 observed=False ② z 非 None 且 observed 为 False:unfreeze(new_kf) 做在线平滑找回 ③ 标准修正:y=z-Hx、S=HPH'+R、K=PH'S⁻¹、x=x+Ky、P=(I-KH)P(I-KH)'+KRK' ④ z 入 history_obs |
| freeze | 无 | 无 | deepcopy 整个 dict 存 attr_saved(ORU 快照) |
| unfreeze | new_kf: KF 类型 | 无 | ORU 找回重放:① 恢复 freeze 时的属性字典(history_obs 保留并去掉最后一个 None) ② 取冻结前最后观测 box1 与找回观测 box2(7 维先换算 w/h) ③ 按帧距线性插值出虚拟轨迹(匀速假设,每步 x/y/w/h 等差) ④ 逐点 update(+predict) 重放洗参数 |
| apply_affine_correction | m: 2x2 线性部分 t: 2x1 平移 new_kf: KF 类型 | 无 | 相机运动补偿:KF 内部状态整体搬进新相机坐标系 8 维:① 更新 self.x------big_m=kron(I4,m) 全量变换(4 对 (值,速度) 各按 m 旋转缩放),t 只加在中心 x,y ② 更新 self.P------协方差共轭变换 big_m·P·big_mᵀ ③ 冻结中(observed=False 且有快照)更新 self.attr_saved------快照 x/P 同①②,快照 last_measurement 前 2 位(中心)仿射、后 2 位(宽高)只旋转缩放 7 维:① 更新 self.x:2(+t 旋转)与 x4:6(vx,vy 旋转),s、r 不动(缩放尝试已注释) ② 更新 self.P 的 :2,:2 与 4:6,4:6 两个子块 ③ 冻结中同样只搬快照的 x:2/x4:6、P 两子块、last_measurement 前 2 位 总结:状态、不确定度、ORU 冻结快照三者同步搬移,保证找回后 unfreeze 回放也在新坐标系展开 |
🎯 6. BasicBlock类(残差基础块:ReID 骨干的组成单元)
文件: deep_ocsort/deep/model.py
| 类初始化 | 输入 | 默认值 | 作用 |
|---|---|---|---|
__init__ |
c_in: 输入通道数 | - | 建两层 3x3 卷积(下采样块首层步长 2)+ BN + ReLU |
| c_out: 输出通道数 | - | 通道变换目标 | |
| is_downsample: 空间下采样 | False | True 时旁路 1x1 步长 2 投影;通道数不等时自动加 1x1 投影并置 True |
| 类属性 | 类型 | 输入 | 作用 |
|---|---|---|---|
| self.is_downsample | bool | 入参或通道不等时自动置 True | forward 是否走旁路投影 |
| self.conv1 / bn1 / conv2 / bn2 | 层 | 构造时建 | 主路两层卷积各带 BN |
| self.relu | 层 | ReLU(True) | 激活 |
| self.downsample | Sequential/无 | 需要时建 | 1x1 卷积 + BN 的旁路投影 |
| 类定义方法 | 输入 | 输出 | 作用 |
|---|---|---|---|
| forward | x: 输入张量 | 残差输出张量 | 主路 conv1→bn1→relu→conv2→bn2,需要时 x 走 downsample 投影,最后 relu(x+y) |
🎯 7. Net类(ReID 网络:resnet18 风格骨干,reid=True 输出 512 维 L2 归一化特征)
文件: deep_ocsort/deep/model.py
| 类初始化 | 输入 | 默认值 | 作用 |
|---|---|---|---|
__init__ |
num_classes: 分类头类别数 | 751 | 分类分支输出维(reid 模式不用) |
| reid: 是否取特征 | False | True 输出 512 维归一化特征,False 走分类头 |
| 类属性 | 类型 | 输入 | 作用 |
|---|---|---|---|
| self.conv | Sequential | 预设 | 首层 3→64 卷积 + BN + ReLU + 3x3 最大池化 |
| self.layer1 | Sequential | make_layers(64,64,2,False) | 64 通道不降采样残差组 |
| self.layer2 | Sequential | make_layers(64,128,2,True) | 128 通道降采样残差组 |
| self.layer3 | Sequential | make_layers(128,256,2,True) | 256 通道降采样残差组 |
| self.layer4 | Sequential | make_layers(256,512,2,True) | 512 通道降采样残差组 |
| self.avgpool | 层 | AdaptiveAvgPool2d(1) | 全局平均池化 → 512 维 |
| self.reid | bool | reid 入参 | 前向分支选择 |
| self.classifier | Sequential | 512→256→num_classes | 分类头(reid 模式不用) |
| 类定义方法 | 输入 | 输出 | 作用 |
|---|---|---|---|
| forward | x: Bx3x128x64 输入张量 | reid=True: Bx512 归一化特征 reid=False: Bxnum_classes 得分 | conv→layer1~4→avgpool→展平→reid 时 L2 归一化直接返回,否则过分类头 |
📍 8. k_previous_obs函数(取 Δt 帧前的历史观测)
文件: deep_ocsort/ocsort.py
| 函数 | 输入 | 输出 | 作用 | 执行步骤 |
|---|---|---|---|---|
| k_previous_obs | observations: 帧年龄→观测字典 cur_age: 当前帧年龄 k: 回看帧数 | 观测框(5 元素数组),空历史返回全 -1 占位 | 找当前帧 age 往前第 k 帧内最近的有效观测(VDC 的参考点) | ① 历史为空返回 -1,-1,-1,-1,-1 ② 从 dt=k 往下试到 1,第一个命中 cur_age-dt 的观测返回 ③ 都没有返回最老一条 |
📍 9. convert_bbox_to_z函数(7 维 KF:检测框转观测向量)
文件: deep_ocsort/ocsort.py
| 函数 | 输入 | 输出 | 作用 | 执行步骤 |
|---|---|---|---|---|
| convert_bbox_to_z | bbox: x1,y1,x2,y2 检测框 | 4x1 数组 x, y, s, r | 检测框转 7 维 KF 的观测向量(中心+面积+宽高比) | ① 算宽 w、高 h ② 中心 x=x1+w/2、y=y1+h/2 ③ s=w*h、r=w/(h+1e-6) ④ reshape 成 4x1 |
📍 10. convert_bbox_to_z_new函数(8 维 KF:检测框转观测向量)
文件: deep_ocsort/ocsort.py
| 函数 | 输入 | 输出 | 作用 | 执行步骤 |
|---|---|---|---|---|
| convert_bbox_to_z_new | bbox: x1,y1,x2,y2 检测框 | 4x1 数组 x, y, w, h | 检测框转 8 维 KF 的观测向量(中心+宽高,直接建模尺寸) | ① 算宽高 ② 中心坐标 ③ reshape 成 4x1 |
📍 11. convert_x_to_bbox_new函数(8 维 KF:状态向量转检测框)
文件: deep_ocsort/ocsort.py
| 函数 | 输入 | 输出 | 作用 | 执行步骤 |
|---|---|---|---|---|
| convert_x_to_bbox_new | x: 8 维状态列向量 | 1x4 数组 | 8 维状态 x,y,w,h,... 转回 x1,y1,x2,y2 | ① 取前 4 位 x/y/w/h ② 中心加减半宽半高 |
📍 12. convert_x_to_bbox函数(7 维 KF:状态向量转检测框)
文件: deep_ocsort/ocsort.py
| 函数 | 输入 | 输出 | 作用 | 执行步骤 |
|---|---|---|---|---|
| convert_x_to_bbox | x: 7 维状态列向量 score: 置信度(可选) | 1x4(或 1x5)数组 | 7 维状态 x,y,s,r,... 转回 x1,y1,x2,y2(可附 score) | ① w=sqrt(s*r)、h=s/w 由面积与宽高比反解宽高 ② 中心加减半宽半高得角点 ③ 带 score 时拼成 5 元 |
📍 13. speed_direction函数(单轨迹速度方向估计)
文件: deep_ocsort/ocsort.py
| 函数 | 输入 | 输出 | 作用 | 执行步骤 |
|---|---|---|---|---|
| speed_direction | bbox1: 参考观测框 bbox2: 当前观测框 | 2 元数组 vy, vx(归一化) | 两观测框中心连线方向(KalmanBoxTracker.update 维护 VDC 惯性方向用) | ① 算两框中心 ② 中心差向量归一化 |
📍 14. new_kf_process_noise函数(8 维 KF 过程噪声)
文件: deep_ocsort/ocsort.py
| 函数 | 输入 | 输出 | 作用 | 执行步骤 |
|---|---|---|---|---|
| new_kf_process_noise | w, h: 当前目标宽高 p: 位置噪声系数 1/20 v: 速度噪声系数 1/160 | 8x8 对角阵 | 按目标实际尺寸生成 Q(大目标噪声绝对值大,相对比例一致) | ① 位置 4 项方差 (pw)² / (p h)² ② 速度 4 项方差 (vw)² / (v h)² ③ np.diag 拼对角阵 |
📍 15. new_kf_measurement_noise函数(8 维 KF 观测噪声)
文件: deep_ocsort/ocsort.py
| 函数 | 输入 | 输出 | 作用 | 执行步骤 |
|---|---|---|---|---|
| new_kf_measurement_noise | w, h: 当前目标宽高 m: 观测噪声系数 1/20 | 4x4 对角阵 | 按目标尺寸生成 R(每次 update 重算) | ① w 方差 (mw)²、h 方差 (m h)² ② x,y,w,h 对角排列 |
📍 16. iou_batch函数(IoU 矩阵)
文件: deep_ocsort/association.py
| 函数 | 输入 | 输出 | 作用 | 执行步骤 |
|---|---|---|---|---|
| iou_batch | bboxes1: Nx4 bboxes2: Mx4 | NxM 矩阵 | 两组框两两 IoU(默认关联相似度) | ① 广播算交集角点与宽高 ② 交面积 / 并面积 |
📍 17. giou_batch函数(GIoU 矩阵)
文件: deep_ocsort/association.py
| 函数 | 输入 | 输出 | 作用 | 执行步骤 |
|---|---|---|---|---|
| giou_batch | bboxes1: Nx4 bboxes2: Mx4 | NxM 矩阵(0~1) | 广义 IoU:不相交时按外接框差距给负分(先算 IoU 再折到 0~1) | ① 算 IoU ② 算最小外接框面积 ③ giou = iou - (外接-交)/外接,再 (g+1)/2 折到 0~1 |
📍 18. diou_batch函数(DIoU 矩阵)
文件: deep_ocsort/association.py
| 函数 | 输入 | 输出 | 作用 | 执行步骤 |
|---|---|---|---|---|
| diou_batch | bboxes1: Nx4 bboxes2: Mx4 | NxM 矩阵(0~1) | 距离 IoU:中心距惩罚版 | ① 算 IoU 与两框中心距平方 ② 算外接框对角线平方 ③ diou = iou - 中心距/外接对角线,折到 0~1 |
📍 19. ciou_batch函数(CIoU 矩阵)
文件: deep_ocsort/association.py
| 函数 | 输入 | 输出 | 作用 | 执行步骤 |
|---|---|---|---|---|
| ciou_batch | bboxes1: Nx4 bboxes2: Mx4 | NxM 矩阵(0~1) | 完整 IoU:DIoU 再加宽高比一致性惩罚 | ① 算 DIoU 部分 ② arctan 宽高比差算 v 与 alpha ③ ciou = diou - alpha*v,折到 0~1 |
📍 20. ct_dist函数(中心距相似度)
文件: deep_ocsort/association.py
| 函数 | 输入 | 输出 | 作用 | 执行步骤 |
|---|---|---|---|---|
| ct_dist | bboxes1: Nx4 bboxes2: Mx4 | NxM 矩阵(0~1) | 中心欧氏距离转相似度(源码注明:粗糙实现,对帧率与速度敏感,不推荐单独用) | ① 算中心距 ② 按全矩阵 max 归一 ③ 取 max-距离 折成越大越像 |
📍 21. speed_direction_batch函数(VDC 方向矩阵)
文件: deep_ocsort/association.py
| 函数 | 输入 | 输出 | 作用 | 执行步骤 |
|---|---|---|---|---|
| speed_direction_batch | dets: Nx5 检测 tracks: Mx5 参考框(last_observation) | (Y, X) 两个 MxN 矩阵 | 参考框中心指向各检测中心的方向(归一化 dy、dx),VDC 与惯性方向求夹角用 | ① 算两组中心 ② 差向量归一化 ③ 返回 (dy, dx)(注意行列顺序为 轨迹x检测) |
📍 22. linear_assignment函数(全局最优分配)
文件: deep_ocsort/association.py
| 函数 | 输入 | 输出 | 作用 | 执行步骤 |
|---|---|---|---|---|
| linear_assignment | cost_matrix: 代价矩阵(越小越好) | Kx2 匹配对数组 | 匈牙利算法求全局最优指派 | ① 优先 lap.lapjv(extend_cost=True) ② 无 lap 库时回退 scipy.linear_sum_assignment |
📍 23. compute_aw_new_metric函数(AW 自适应加权矩阵)
文件: deep_ocsort/association.py
| 函数 | 输入 | 输出 | 作用 | 执行步骤 |
|---|---|---|---|---|
| compute_aw_new_metric | emb_cost: 外观相似度矩阵 w_association_emb: 基准权重 0.75 max_diff: 差异上限 0.5 | 同形状权重矩阵 | 按行列的"最像与第二像差距"给外观权重发奖金:差距大(区分度高)话语权加重 | ① 全阵初始化为基准权重、奖金阵置 0 ② 每行(检测视角):top1 与 top2 相似度差(封顶 max_diff)的一半加到该行奖金 ③ 每列(轨迹视角)同样发一半奖金 ④ 返回 基准 + 奖金 |
📍 24. split_cosine_dist函数(网格特征分条余弦相似度)
文件: deep_ocsort/association.py
| 函数 | 输入 | 输出 | 作用 | 执行步骤 |
|---|---|---|---|---|
| split_cosine_dist | dets: Nx3x512 检测网格特征 trks: Mx3x512 轨迹网格特征 affinity_thresh: 0.55 pair_diff_thresh: 0.6(未用) hard_thresh: True | NxM 相似度矩阵 | 网格模式外观比对:检测与轨迹的上中下三条特征分别比,任何一条不像即判 0(硬阈值) | ① 逐对算 3x3 余弦相似度阵 ② 检测每条取与轨迹三条的最大值(patch_affinity) ③ 硬阈值:三条全过 affinity_thresh 才取 max,否则 0 |
📍 25. associate函数(第一轮关联:IoU + VDC + 外观 AW 总代价)
文件: deep_ocsort/association.py
| 函数 | 输入 | 输出 | 作用 | 执行步骤 |
|---|---|---|---|---|
| associate | detections: Nx5 检测 trackers: Mx5 预测框 det_embs: 检测特征 trk_embs: 轨迹特征 iou_threshold: 0.3 velocities: 惯性方向 previous_obs: k_observations 参考点 vdc_weight: 0.2 w_assoc_emb: 0.75 aw_off / aw_param / emb_off / grid_off: 开关与参数 | (matches, unmatched_dets, unmatched_trks) 三元组 | Deep-OC-SORT 核心关联:运动 + 方向 + 外观三路代价融合后全局最优分配 | ① speed_direction_batch 算参考点→检测方向,与惯性方向点积得夹角余弦,折算 diff_angle 成本(π/2 内为正奖励) ② previous_obs 第 5 位 <0(无有效观测)的轨迹方向项置 0(valid_mask) ③ iou_batch 算 IoU 矩阵 ④ angle_diff_cost = valid_mask×diff_angle×vdc_weight,转置后乘检测置信度 ⑤ 未关外观:整框模式算 det_embs @ trk_embs.T 点积相似度,网格模式走 split_cosine_dist ⑥ IoU 超阈值且无竞争走快路径直接配 ⑦ 否则 AW 开时 emb_cost × compute_aw_new_metric 权重、关时 × 固定 w_assoc_emb ⑧ final_cost = -(iou + angle_diff_cost + emb_cost),匈牙利分配 ⑨ 低 IoU 匹配对拆回落单 |
📍 26. make_layers函数(残差组搭建)
文件: deep_ocsort/deep/model.py
| 函数 | 输入 | 输出 | 作用 | 执行步骤 |
|---|---|---|---|---|
| make_layers | c_in: 输入通道 c_out: 输出通道 repeat_times: 重复块数 is_downsample: 首块是否降采样 | nn.Sequential | 给 Net 拼一组 BasicBlock | ① 首块 c_in→c_out(可选降采样) ② 其余块 c_out→c_out ③ Sequential 串接 |
二、轨迹跟踪步骤
OCSort.update() 每帧执行一次,加上开头的数据准备与收尾的输出清理,共可分七步:数据准备与分拣 → 外观特征与相机运动补偿 → 批量预测备料 → 第一轮关联 → OCR 找回轮 → 冻结与建档 → 输出与清理。一条轨迹在跟、丢失还是死亡,全部由 time_since_update(漏检计数)和 hit_streak(连续命中计数)两个计数器表达,收尾一个倒序循环同时完成输出与判死。
2.1 第一步:数据准备与分拣
| 任务 | 变量 | 实例说明 |
|---|---|---|
| ① 帧计数与判空 | self.frame_count |
self.frame_count += 1:帧计数每帧加 1;output_results 为 None 时直接返回 0×5 空数组 例:上一帧 3 → 本帧 4(收尾的冷启动期判断 frame_count ≤ min_hits 用它) |
| ② 拆分数组 | scores、bboxes |
输入是 N×5 时直接拆列:scores 取第 5 列、bboxes 取前 4 列(6 列输入走 obj×cls 联合置信度路线,本工程封装层传 5 列,不走) 例:3 个检测框 → scores = [0.85, 0.35, 0.05],bboxes = [[100,200,150,300], [400,300,450,380], [600,100,650,180]] |
| ③ 拼回 N×5 | dets |
np.concatenate((bboxes, scores)) 拼成 x1,y1,x2,y2,置信度(封装层传入的已是原图坐标,无需换算) 例:dets = [[100,200,150,300,0.85], [400,300,450,380,0.35], [600,100,650,180,0.05]] |
| ④ 单路分拣 | dets |
只有 scores > det_thresh(默认 0.3) 一条门槛:0.3 以上的留主池,≤0.3 的全部直接丢弃------本算法不做低分二次关联:门槛以下的框不进任何一轮比对 例:dets = [[100,200,150,300,0.85], [400,300,450,380,0.35]](两框都 > 0.3 全留),0.05 那框直接丢弃 |
这一步的产出 :主池高分框 dets------后续特征提取、两轮关联、建档全部只用它;0.3 以下的框一步淘汰,没有备用通道,保证进入关联的每个框都带着可信的外观特征。
2.2 第二步:外观特征提取与相机运动补偿
| 任务 | 变量 | 实例说明 |
|---|---|---|
| ① 检测特征提取 | dets_embs |
先给占位 np.ones((N,1))(关外观时保住后续数组运算的形状);embedding_off=False 且主池非空时 embedder.compute_embedding(img, dets[:,:4]) 批量提取 ReID 特征 例:默认网格模式,2 个框 → dets_embs 形状 (2, 3, 512)(每框上中下三条各 512 维);--emb_off 时占位不提取 |
| ② CMC 仿射估计 | transform |
cmc.compute_affine(img, dets[:,:4]):检测框区域遮掉后,从背景角点的光流估 2×3 仿射(首帧或特征不足给单位阵,等于不动) 例:相机右移 3 像素 → transform 平移列 ≈ -3, 0 |
| ③ 全轨迹记忆搬家 | trk.apply_affine_correction(transform) |
逐条轨迹把三份记忆一起搬进新相机坐标系:last_observation(OCR 锚)、observations 回看窗(VDC 依据)、KF 状态/协方差/冻结快照 例:下轮预测与关联比较时坐标已同帧,差值只剩目标自身运动 |
| ④ EMA 系数备料 | dets_alpha |
trust = (conf−0.3)/0.7 把置信度折成 0~1 信任度,dets_alpha = 0.95 + 0.05×(1−trust) 例:conf=0.85 → alpha≈0.961(新特征占约 3.9%);conf=0.35 → alpha≈0.996(几乎全信历史,防低质框污染轨迹特征) |
这一步的产出 :dets_embs(第一轮外观比对的检测端)、坐标系统一的全部轨迹、逐检测的 EMA 系数 dets_alpha。
2.3 第三步:批量预测与备料
| 任务 | 变量 | 实例说明 |
|---|---|---|
| ① 逐轨迹外推 | trks |
全零 M×5 数组,逐条调 KalmanBoxTracker.predict():卡尔曼 x=Fx 外推(8 维 KF 先做负宽高保护、冻结轨迹把宽高速度清零、按当前宽高算 Q),预测框填入对应行(第 5 列恒 0 占位) 例:某轨迹预测框 [104,206,154,306] 填进 trks |
| ② 坏轨迹清理 | to_del、trks、trk_embs |
预测框含 NaN(如面积项变负后开根号)的轨迹:trks 压缩掉坏行、trackers 弹出本体(倒序删防下标位移);轨迹特征 trk_embs 只收好轨迹,与压缩后的 trks 行行对齐 例:轨迹 7 的预测框含 NaN → trks 删第 7 行,trackers 弹出第 7 条,trk_embs 不收它 |
| ③ 备三组轨迹数据 | velocities |
每条轨迹的惯性方向 (dy,dx)(KalmanBoxTracker.update 算好存的),从没配上过观测的轨迹用 (0,0) 例:velocities = [[-0.707, 0.707], [0, 0]](轨迹0 往右上走,轨迹1 还没观测过) |
last_boxes |
每条轨迹最近一次真实观测框 x1,y1,x2,y2,置信度,没观测过的是 -1×5 占位(OCR 找回轮的比对锚) 例:last_boxes = [[102,198,152,298,0.82], [-1,-1,-1,-1,-1]] |
|
k_observations |
每条轨迹约 delta_t(3) 帧前的观测框(k_previous_obs 从 observations 字典回查),作 VDC 的方向参考点;没观测过的是 -1×5 例:k_observations = [[96,204,146,304,0.88], [-1,-1,-1,-1,-1]] |
这一步的产出:trks 预测框与 trk_embs 轨迹特征(第一轮关联比对的两端)、三组轨迹数据,坏轨迹已移除。
2.4 第四步:第一轮关联(高分框 vs 预测框,IoU + VDC + 外观)
用谁配谁:主池高分检测框 vs 全部存活轨迹的卡尔曼预测框,一对一全局分配;配对代价由 IoU、VDC 方向分、外观分三路合成,三路都高的对才配得上。
| 任务 | 变量 | 实例说明 |
|---|---|---|
| ① 进 associate | matched、unmatched_dets、unmatched_trks |
associate(dets, trks, dets_embs, trk_embs, iou_threshold=0.3, velocities, k_observations, inertia=0.2, ...) 返回三个下标数组:K×2 配对表(每行=检测下标, 轨迹下标)、落单检测下标、落单轨迹下标 |
| ② 算三路相似度 | iou_matrix、angle_diff_cost、emb_cost |
iou_batch 算 N×M IoU 矩阵;VDC 链:轨迹参考点看检测的方向与惯性方向点积转方向分(同向 +0.5 / 垂直 0 / 反向 −0.5),×inertia(0.2)、转置、×检测置信度;外观链:整框模式 dets_embs @ trk_embs.T 余弦点积,网格模式 split_cosine_dist 上中下三条全过 0.55 才认 例(1 检测 × 2 轨迹):iou = [0.72, 0.05],外观相似度 = 0.83, 0.31 |
| ③ 外观加权 | emb_cost |
AW 开(默认):emb_cost × compute_aw_new_metric,权重按"最像与第二像的差距"在基准 0.75 附近上浮(奖金封顶 0.5)------外观相似度 0.83, 0.31 区分度高,轨迹0 那格的话语权加码;AW 关(--aw_off):×固定 0.75 |
| ④ 分配 | matched_indices |
候选图(IoU 超阈记 1)每行每列至多 1 个候选时直接取格子(快路径,跳过求解器);否则匈牙利分配 −(IoU + 方向分 + 外观分) 求全局最优 例:匈牙利配出 [[0,0]] |
| ⑤ 逐对终审 | matched、unmatched_dets、unmatched_trks |
配好的每一对按 IoU 阈值 0.3 复核,不够的拆散、双方并入落单 例:0.72 那对通过 → matched = [[0,0]] |
| ⑥ 兑现配对 | self.trackers |
遍历 matched:trackers[m[1]].update(dets[m[0], :]) 卡尔曼修正、登记观测;紧跟 update_emb(dets_embs[m[0]], alpha=dets_alpha[m[0]]) 按本检测的 EMA 系数更新轨迹特征------运动与外观双修正 例:轨迹0 以检测0 的框执行 update,轨迹特征按约 96:4 混入检测0 的新特征再归一化 |
这一步的产出 :配上轨迹完成修正、延续跟踪;落单检测 unmatched_dets 与落单轨迹 unmatched_trks 带去第五步找回轮。
2.5 第五步:OCR 找回轮(纯 IoU)
用谁配谁 :落单高分框 vs 落单轨迹的 last_boxes(最近真实观测框,不是预测框)------轨迹丢失期间预测框会漂走,目标重现的位置更接近它上次被看见的地方。
| 任务 | 变量 | 实例说明 |
|---|---|---|
| ① 进门条件 | unmatched_dets、unmatched_trks |
双方都有落单才进 |
| ② 纯 IoU | iou_left |
dets[unmatched_dets] vs last_boxes[unmatched_trks],asso_func 纯 IoU(无 VDC、无外观------代码里切了落单双方的特征但没用上,TODO 注明 maybe use embeddings here) 例:检测1 vs 轨迹1 的 last_boxes=-1,-1,-1,-1,-1(轨迹1 刚建档没观测过)→ IoU 全是垃圾值 |
| ③ 门卫与分配 | rematched_indices |
iou_left.max() > 0.3 才继续;匈牙利分配 −IoU;逐对终审,低于 0.3 的拆散 |
| ④ 兑现配对 | self.trackers |
配上的轨迹 update(高分框) + update_emb(双下标回译 unmatched_dets[m[0]]、unmatched_trks[m[1]]),检测、轨迹两侧名单都划掉 例:轨迹1 是占位框,找回失败 → 检测1、轨迹1 仍落单;若轨迹1 曾有过真实观测、检测1 恰好出现在附近,IoU 达标即找回------OCR 轮的价值就在这 |
这一步的产出:重现目标在"消失处附近"被找回,ID 不断;仍落单的双方带去第六步。
2.6 第六步:冻结(漏检)与建档(新目标)
| 任务 | 变量 | 实例说明 |
|---|---|---|
| ① 落单轨迹冻结 | self.trackers[m].update(None) |
仍落单的轨迹逐条 update(None):KF 收到 None 观测,第一帧漏检触发 freeze 把滤波参数整套快照进 attr_saved(ORU 起点)、frozen=True;此后每帧只 predict 不修正(惯性外推),冻结期 predict 把宽高速度清零防尺寸漂移 例:轨迹1 → attr_saved 存下漏检前的干净参数,等找回后 unfreeze 回放 |
| ② 落单高分框建档 | trk、self.trackers |
仍落单的高分框逐个 KalmanBoxTracker(dets[i,:], delta_t=3, emb=dets_embs[i], alpha=dets_alpha[i], new_kf=True) 建新轨迹:建 8 维卡尔曼、首框填初始状态、检测特征建档为初始外观、领全局 id,append 进轨迹名单 例:检测1 100,200,150,300,0.85 → 新轨迹 id=12(带初始特征),下一帧起参加关联 |
新轨迹当帧不输出(hit_streak=0,冷启动期 frame_count ≤ 3 除外),要连续配满 min_hits(3) 帧才有输出资格。
2.7 第七步:输出与清理(收尾)
倒序遍历全部轨迹,一趟同时完成输出与判死(倒序是因为边遍历边 pop,从尾往头删不挪还没遍历到的下标)。
| 任务 | 变量 | 实例说明 |
|---|---|---|
| ① 选输出坐标 | d |
last_observation.sum() < 0(从没真实观测过)用 get_state() 卡尔曼估计框;否则用 last_observation[:4] 最近真实观测框 例:轨迹0 本帧刚配上 → 输出 102,198,152,298(就是本帧检测框本身) |
| ② 输出资格门 | ret |
time_since_update < 1(本帧刚配上)且(hit_streak ≥ 3 或 frame_count ≤ 3 冷启动)→ 拼 x1,y1,x2,y2,id+1 挂进 ret(id+1:MOT 规范要求 ID 从 1 起) 例:轨迹0 streak=5、id=6 → 输出 [102,198,152,298,7];漏检中的轨迹(time_since_update ≥ 1)不满足输出资格,本帧不输出 |
| ③ 判死 | self.trackers |
time_since_update > max_age(30) 的轨迹 pop(i) 删除,编号不回收 例:连丢 31 帧的轨迹删除(输出条件 <1 与判死条件 >30 互斥,不会删到本帧刚输出的) |
这一步的产出 :concatenate(ret) 成 K×5 返回 x1,y1,x2,y2,id,就是本帧跟踪结果。至此一帧闭环:分拣(2.1)→ 特征与补偿(2.2)→ 预测备料(2.3)→ 第一轮关联(2.4)→ OCR 找回(2.5)→ 冻结与建档(2.6)→ 输出与清理(2.7)。
三、轨迹算法整体流程图(每帧 update())
#mermaid-svg-fQrJKiFoG7qP2a0q{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fQrJKiFoG7qP2a0q .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fQrJKiFoG7qP2a0q .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fQrJKiFoG7qP2a0q .error-icon{fill:#552222;}#mermaid-svg-fQrJKiFoG7qP2a0q .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fQrJKiFoG7qP2a0q .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fQrJKiFoG7qP2a0q .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fQrJKiFoG7qP2a0q .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fQrJKiFoG7qP2a0q .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fQrJKiFoG7qP2a0q .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fQrJKiFoG7qP2a0q .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fQrJKiFoG7qP2a0q .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fQrJKiFoG7qP2a0q .marker.cross{stroke:#333333;}#mermaid-svg-fQrJKiFoG7qP2a0q svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fQrJKiFoG7qP2a0q p{margin:0;}#mermaid-svg-fQrJKiFoG7qP2a0q .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fQrJKiFoG7qP2a0q .cluster-label text{fill:#333;}#mermaid-svg-fQrJKiFoG7qP2a0q .cluster-label span{color:#333;}#mermaid-svg-fQrJKiFoG7qP2a0q .cluster-label span p{background-color:transparent;}#mermaid-svg-fQrJKiFoG7qP2a0q .label text,#mermaid-svg-fQrJKiFoG7qP2a0q span{fill:#333;color:#333;}#mermaid-svg-fQrJKiFoG7qP2a0q .node rect,#mermaid-svg-fQrJKiFoG7qP2a0q .node circle,#mermaid-svg-fQrJKiFoG7qP2a0q .node ellipse,#mermaid-svg-fQrJKiFoG7qP2a0q .node polygon,#mermaid-svg-fQrJKiFoG7qP2a0q .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fQrJKiFoG7qP2a0q .rough-node .label text,#mermaid-svg-fQrJKiFoG7qP2a0q .node .label text,#mermaid-svg-fQrJKiFoG7qP2a0q .image-shape .label,#mermaid-svg-fQrJKiFoG7qP2a0q .icon-shape .label{text-anchor:middle;}#mermaid-svg-fQrJKiFoG7qP2a0q .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fQrJKiFoG7qP2a0q .rough-node .label,#mermaid-svg-fQrJKiFoG7qP2a0q .node .label,#mermaid-svg-fQrJKiFoG7qP2a0q .image-shape .label,#mermaid-svg-fQrJKiFoG7qP2a0q .icon-shape .label{text-align:center;}#mermaid-svg-fQrJKiFoG7qP2a0q .node.clickable{cursor:pointer;}#mermaid-svg-fQrJKiFoG7qP2a0q .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fQrJKiFoG7qP2a0q .arrowheadPath{fill:#333333;}#mermaid-svg-fQrJKiFoG7qP2a0q .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fQrJKiFoG7qP2a0q .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fQrJKiFoG7qP2a0q .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fQrJKiFoG7qP2a0q .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fQrJKiFoG7qP2a0q .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fQrJKiFoG7qP2a0q .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fQrJKiFoG7qP2a0q .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fQrJKiFoG7qP2a0q .cluster text{fill:#333;}#mermaid-svg-fQrJKiFoG7qP2a0q .cluster span{color:#333;}#mermaid-svg-fQrJKiFoG7qP2a0q div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fQrJKiFoG7qP2a0q .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fQrJKiFoG7qP2a0q rect.text{fill:none;stroke-width:0;}#mermaid-svg-fQrJKiFoG7qP2a0q .icon-shape,#mermaid-svg-fQrJKiFoG7qP2a0q .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fQrJKiFoG7qP2a0q .icon-shape p,#mermaid-svg-fQrJKiFoG7qP2a0q .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fQrJKiFoG7qP2a0q .icon-shape .label rect,#mermaid-svg-fQrJKiFoG7qP2a0q .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fQrJKiFoG7qP2a0q .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fQrJKiFoG7qP2a0q .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fQrJKiFoG7qP2a0q :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 置信度 > 0.3
置信度 ≤ 0.3
配上的
落单的高分框
落单的轨迹
配上的
轨迹仍落单
惯性外推超过 max_age 30 帧
高分框仍落单
本帧配上 且 连续命中 ≥ 3 或冷启动期
漏检中或资格不够
检测器输出本帧全部检测框
第一步:按置信度分拣
只留 > 0.3 的框
主池 dets
直接丢弃
不参与任何关联与建档
第二步:外观特征与相机补偿
embedder 提取检测特征 dets_embs
cmc 估仿射,全部轨迹记忆搬家
按置信度算每个检测的 EMA 系数 dets_alpha
第三步:全部轨迹批量预测
逐条 predict 外推预测框
含 NaN 的坏轨迹直接删除
第四步 第一轮关联:高分框 vs 预测框
IoU + VDC 方向分 + 外观分,阈值 0.3
轨迹执行 update + update_emb
卡尔曼修正 + 特征 EMA 更新
第五步 OCR 找回轮:落单高分框 vs 落单轨迹
最近真实观测框 last_boxes,纯 IoU,阈值 0.3
第六步 冻结:update 传 None
ORU 快照 freeze,惯性外推等找回
第七步 判死 pop 删除
编号不回收
第六步 建档:建新轨迹
8 维卡尔曼 + 初始外观特征,领新 ID
连配 min_hits 3 帧才有输出资格
第七步:输出与清理,倒序遍历全部轨迹
输出 x1,y1,x2,y2,id
id 加 1 从 1 起
不输出,继续等下帧
流程图文字说明:
第一步:按置信度分拣。检测框按置信度分两路:>0.3 进主池参与关联;≤0.3 直接淘汰,没有备用通道------门槛以下的框置信度不足、外观特征不可靠,直接舍弃以保证进关联的每个框都可信。坐标已是原图坐标系,无需换算。
第二步:外观特征与相机补偿。主池检测框批量提取 ReID 特征(默认网格模式:上中下三条各 512 维);CMC 从背景估 2×3 仿射,全部轨迹把 last_observation、observations 回看窗、KF 状态/协方差/冻结快照一起搬进新相机坐标系;再按检测置信度算每个检测的 EMA 系数 dets_alpha(分高信新特征、分低信历史)。
第三步:批量预测。全部存活轨迹逐条卡尔曼外推(x=Fx,位置各加一次速度;冻结轨迹宽高速度清零),预测框凑成 trks、轨迹特征凑成 trk_embs;预测出 NaN 的坏轨迹连行带本体一起删除。同时备好三组数据:惯性方向 velocities、最近真实观测 last_boxes、k 帧前观测 k_observations。
第四步 第一轮关联。高分框 vs 预测框,三路代价融合:IoU + VDC 方向分(同向加分、反向减分,×0.2 权重、×检测置信度)+ 外观分(余弦相似度 ×0.75 基准权重,AW 开时按区分度上浮、关时固定),匈牙利求全局最优(无竞争时走快路径跳过求解器),逐对按 IoU ≥ 0.3 终审。配上的轨迹 update 修正运动、update_emb 按 EMA 系数混入新特征------运动与外观双修正。
第五步 OCR 找回轮。落单高分框 vs 落单轨迹的 last_boxes(最近真实观测,不是漂走的预测框),纯 IoU、阈值 0.3(配前 max 预检、配后逐对复核),配上的恢复跟踪并同样做特征 EMA------这是观测中心思想的找回通道,目标重现于消失处附近时 ID 不断。
第六步 冻结与建档。仍落单的轨迹 update(None):第一帧漏检 ORU 快照冻结,进入惯性外推。仍落单的高分框建新轨迹:8 维卡尔曼 + 检测特征作初始外观、领新 ID,当帧不输出,连续配满 3 帧才有资格。
第七步 输出与清理。倒序遍历:本帧配上且(连续命中 ≥ 3 或冷启动期)的轨迹输出 x1,y1,x2,y2,id(ID 从 1 起,坐标优先用最近真实观测框);惯性外推超过 30 帧的判死删除。输出条件(<1)与判死条件(>30)互斥,不会删到刚输出的轨迹。
两轮关联对比详情:
| 对比项 | 第一轮(主关联轮) | 第二轮(OCR 找回轮) |
|---|---|---|
| 场景 | 每帧常态:目标按预期运动,预测框贴合检测框 | 目标漏检若干帧后重现,重现位置靠近消失位置(预测框已惯性漂走) |
| 谁 vs 谁 | 全部高分检测框(>0.3)vs 全部存活轨迹的卡尔曼预测框(trks) | 落单高分检测框 vs 落单轨迹的最近真实观测框(last_boxes) |
| 方法 | IoU + VDC 方向分(×0.2 权重、再 ×检测置信度)+ 外观分(余弦 ×0.75,AW 按区分度上浮)合成代价,匈牙利求全局最优(无竞争走快路径),逐对按 IoU ≥ 0.3 终审 | 纯 IoU + 匈牙利,阈值 0.3(配前 max 预检、配后逐对复核) |
| 作用 | 常态配对、运动与外观双修正;VDC 防交叉运动换 ID,外观分防同貌目标换 ID | 用真实观测锚找回被漂移预测框甩掉的配对,保持 ID 不断 |
| 触发条件 | 主池检测框与存活轨迹都非空才执行(轨迹为空时全部检测直接建档) | 落单检测框与落单轨迹两侧都非空才执行 |
| 比对锚 | 卡尔曼预测框:补偿帧间运动,快速目标不掉队 | 最近真实观测框:不用漂走的预测框,承载"目标在消失处重现"假设 |
| 配上后轨迹的动作 | update 修正滤波参数、刷新 last_observation、time_since_update 归 0;update_emb 按 dets_alpha EMA 更新轨迹特征 | 同主轮:update + update_emb 双修正 |
| 落单去向 | 检测框:转入 OCR 轮;轨迹:先争取 OCR 找回,仍落单则 update(None) 冻结外推 | 检测框:建档领新 ID(带初始特征,当帧不输出,连配 3 帧才有输出资格);轨迹:update(None) 惯性外推等待,超 30 帧判死 |
| 描述 | 链接 |
|---|---|
| 项目 | 基于 Ultralytics +Deep-OC-SORT多目标跟踪 |
| 往期回顾 | 链接 |
| 数据集 | 数据集汇总 |
| 软件开发 | 深度学习软件开发汇总 |