
简介多目标跟踪是计算机视觉与信号处理领域的重要课题旨在识别并连续追踪图像序列中的多个动态目标。该MATLAB项目完整实现了从前景检测、卡尔曼滤波预测、匈牙利匹配到轨迹创建与删除的闭环流程适合正在学习目标跟踪算法或需要MATLAB参考实现的本科生、研究生及工程开发者。压缩包共3个文件包含可直接运行的MATLAB主程序、演示视频以及源码使用说明整体仅573KB轻量易用。资源已有448人学习内容精炼覆盖高斯混合模型前景分割、匀速运动模型下的卡尔曼预测、基于成本矩阵的匈牙利指派等关键知识点并附有演示场景便于对照理解轨迹管理策略。通过这套代码读者可快速搭建自己的多目标跟踪测试环境为自动驾驶、视频监控等应用打下基础。1. 多目标跟踪与 MATLAB先解决“为什么难”如果你只做过单目标跟踪第一次跑多目标跟踪算法时最先崩掉的往往不是准确率而是 ID Switch——同一个行人刚被标记为 ID 5两帧之后变成了 ID 12。另外一个更隐蔽的坑是在只有下一帧数据到达时你的程序根本来不及跟当前所有目标挨个做“谁是谁”的穷举匹配因为计算量随目标数量呈平方级增长。多目标跟踪Multi-Object Tracking, MOT就是在这种“实时性、不确定性和 ID 一致性”三条约束同时成立的前提下解决“每一帧检测到一堆框怎么把它们串成一条条干净的轨迹”的问题。MATLAB 在这个领域一直被低估Sensor Fusion and Tracking Toolbox 提供了航迹管理、GNN/JPDA/多假设跟踪MHT等现成接口而自己实现一个最小可运行的跟踪器也只要两百行左右。这篇博文面向已经会用 MATLAB 做图像处理和卡尔曼滤波的读者——我会从数据关联的内核讲起给出一个不用工具箱也能跑通的多目标跟踪 MATLAB 实现再谈参数怎么调、指标怎么算最后落到 HOTA 这类对 ID 一致性敏感的新指标的 MATLAB 计算技巧上。2. 多目标跟踪的核心机制卡尔曼滤波、匈牙利匹配与 TrackGNN2.1 先搞清楚多目标跟踪在解一个什么问题多目标跟踪的数学本质是一个“在线最优分配”问题。每一帧送入检测结果集合系统需要回答两个问题这个检测框是否属于一条已存在的轨迹如果不属于是否应该新建一条轨迹如果它和某条轨迹关联轨迹的状态位置、速度应该怎么更新这里最关键的是“关联”这一步。常见做法是先用运动模型预测每条轨迹在当前帧的位置然后计算预测框与检测框之间的相似度再用匈牙利算法求全局最优匹配。MATLAB 中预测由predict完成相似度计算通常用交并比IoU或马氏距离匹配则由assignDetectionsToTracks封装了matchpairs来实现。这套流程的组合在经典文献里常被叫作 Tracking-by-Detection它的性能上限直接由检测器质量决定——这一点我会在第 4 章详细展开。% 伪代码级流程多目标跟踪主循环 for k 1:numFrames detections runDetector(frame{k}); % 检测得到框和置信度 [tracks, assignments] updateTracks(... % 更新预测关联滤波 tracks, detections, maxNumTracks); end多目标跟踪与单目标追踪最本质的区别在于单目标追踪假设场景中只有一个目标目标消失后重新出现还是同一个目标而多目标跟踪必须处理目标的出现、消失、遮挡后重新出现——这时它是全新的轨迹而不是原来的 ID。所以任何 MOT 系统都必须包含“轨迹生命周期管理”模块。它要解决两个问题一条轨迹多久没有关联到检测就算死了一个新检测连续被关联多少次才赋予正式 ID2.2 卡尔曼滤波在多目标跟踪里到底预测什么卡尔曼滤波在 MOT 中的角色是给每一条轨迹维护一个运动状态估计用于预测下一帧目标可能出现的位置。最常见的状态向量是 7 维[cx, cy, aspect, height, vx, vy, va, vh]即中心坐标、宽高比、高度以及各自的速度。SORTSimple Online and Realtime Tracking论文里用的是匀速CV模型即使存在加速度只要帧率足够高25 FPS 以上匀速假设在短时间内外推的误差仍然远小于检测误差本身。% 定义等速模型的状态转移矩阵 dt1 A [1 0 0 0 1 0 0 0; 0 1 0 0 0 1 0 0; 0 0 1 0 0 0 1 0; 0 0 0 1 0 0 0 1; 0 0 0 0 1 0 0 0; 0 0 0 0 0 1 0 0; 0 0 0 0 0 0 1 0; 0 0 0 0 0 0 0 1];A 是状态转移矩阵。注意这里的速度单位是“像素/帧”不是“像素/秒”。在 MATLAB 中如果使用trackingKF你并不需要显式传入这个矩阵因为它默认支持多种运动模型但自己写predict步骤时这个矩阵就是全部。实际工程中最需要调的是过程噪声协方差 Q。Q 设得越大滤波器越相信检测设得越小滤波器越相信预测。多目标跟踪中的经验是检测器不稳定时适当调大 Q让轨迹能跟上检测抖动检测器非常准但帧率较低时Q 要小一些避免轨迹被误检带飞。2.3 匈牙利匹配与代价矩阵有了预测位置下一步构造代价矩阵。矩阵的每一行是一条轨迹每一列是一个检测框元素是两者之间的代价cost。代价越小说明越可能是同一个目标。选择用什么度量来填充矩阵直接决定了跟踪器在高密度场景下的表现。度量方式计算开销适用场景典型问题IoU 距离极低行人、车辆等刚体帧率≥20FPS目标快速形变时失效中心点欧氏距离极低帧率极高、目标移动慢两个目标靠近时无法区分马氏距离含速度信息低线性运动目标目标急转弯时误差大外观特征余弦距离高遮挡频繁、跨摄像头需要额外训练 ReID 网络SORT 用 IoU 距离DeepSORT 用马氏距离加外观余弦距离的加权和。MATLAB 里实现 IoU 代价矩阵最直接的做法是调用bboxOverlapRatiofunction costMatrix iouCost(predBoxes, detBoxes) iouMatrix bboxOverlapRatio(predBoxes, detBoxes); % 行预测框列检测框 costMatrix 1 - iouMatrix; % IoU 越大代价越小 % 将小于阈值的 IoU 对应的代价置为 Inf强制不允许匹配 costMatrix(iouMatrix 0.1) Inf; end这段代码里bboxOverlapRatio返回一个 m×n 矩阵元素取值范围是 [0,1]。1 - iou将相似度转成代价IoU1 时代价为 0IoU0 时代价为 1。最后一行把所有 IoU 低于 0.1 的配对全部设为 Inf相当于告诉匈牙利算法这些配对在物理上不可能属于同一个目标。得到代价矩阵之后用 MATLAB 的matchpairs函数求解全局最优匹配。需要特别注意的是matchpairs默认求解最小化总代价的匹配与我们的代价定义一致。它的第三个参数如果传min返回所有未匹配的行和列正好对应“轨迹找不到检测”和“检测找不到轨迹”两种情况。实际使用时要区分这三个返回值前一帧留下的轨迹匹配到检测则更新未匹配的轨迹要看存活时长是否超限未匹配的检测则纳入未确认轨迹集合。2.4 TrackGNN工具箱里的一个足够好的默认选择如果你不想自己维护轨迹生命周期MATLAB 的trackerGNN是一个可靠的起点。GNN 的全称是 Global Nearest Neighbor即全局最近邻本质上就是用匈牙利算法做逐帧贪心关联的滤波器组合。它的核心参数包括MaxNumTracks、AssignmentThreshold、DetectionProbability、FalseTrackRate等。值得专门一提的是GNN 是陷阱最少的多目标跟踪算法它的局限性在于密集场景中一个目标跟丢后会立刻把 ID 分配给另一个目标但在目标数小于 50 且遮挡不严重的场景它的表现稳定且调试成本最低。简而言之理解多目标跟踪的关键不是背卡尔曼公式而是理清“预测—关联—更新—管理”的循环。接下来我用一段不需要任何工具箱的纯 MATLAB 代码把这个循环完整实现出来。3. 用 MATLAB 从零写一个可跑的多目标跟踪最小实现3.1 先准备一段可复现的合成数据在正式引入真实视频之前先用合成数据验证跟踪逻辑这是开发多目标跟踪算法时效率最高的方式。我一般会生成两到三个匀速直线运动的目标每个目标持续 50 帧左右中途让两个目标交叉一次——这个交叉点是一切跟踪算法最容易被绕晕的地方。生成数据的代码如下rng(2024); numFrames 100; groundTruth struct(boxes, cell(1, numFrames)); % 三条轨迹匀速直线运动并在第 40 帧发生交叉 tracksTruth [60, 100, 0, 100; % [x, y, vx, vy] 200, 100, 0, 80; 350, 80, -40, 100]; for k 1:numFrames boxes zeros(3, 4); for j 1:3 t tracksTruth(j, :); boxes(j, :) [t(1) t(3)*k, t(2) t(4)*k, 40, 80]; end % 叠加高斯噪声模拟检测框抖动 boxes boxes randn(3, 4) * 3; groundTruth(k).boxes boxes; end这段代码生成了三条起点分布在不同位置的匀速运动轨迹。噪声标准差设为 3 像素对应检测框的定位误差。生成数据的目的是什么目的是在多目标跟踪算法跑起来之前先知道“正确答案”是什么——这样跟踪结果一出来我们立刻能判断是关联逻辑出了错还是滤波参数不匹配。3.2 最小跟踪器的完整 MATLAB 实现下面的实现由四个部分组成轨迹结构体数组、卡尔曼滤波的预测、匈牙利匹配、轨迹管理。按多目标跟踪惯用的“SORT 思路”组织总共约 200 行不依赖任何工具箱只需要基础 MATLAB 环境。function tracks runMoTracker(groundTruth) % 初始化参数 tracks struct(id, {}, x, {}, P, {}, ... age, {}, hits, {}, timeSinceUpdate, {}); nextId 1; maxInvisible 4; % 连续 4 帧未关联则删除轨迹 minHits 2; % 连续命中 2 次才确认轨迹 A [1 0 1 0; 0 1 0 1; 0 0 1 0; 0 0 0 1]; % 匀速模型 H [1 0 0 0; 0 1 0 0]; % 观测矩阵只观测 x, y Q eye(4) * 5; % 过程噪声 R eye(2) * 10; % 观测噪声 for k 1:length(groundTruth) dets groundTruth(k).boxes; numDets size(dets, 1); numTracks length(tracks); % 第一步预测所有轨迹并保存预测位置 predPos zeros(numTracks, 2); for t 1:numTracks [tracks(t).x, tracks(t).P] predictKF(..., tracks(t).x, tracks(t).P, A, Q); predPos(t, :) tracks(t).x(1:2); end % 第二步构造代价矩阵用中心点欧氏距离 detCenters [dets(:,1)dets(:,3)/2, dets(:,2)dets(:,4)/2]; costMatrix zeros(numTracks, numDets); for t 1:numTracks diff detCenters - repmat(predPos(t,:), numDets, 1); costMatrix(t, :) sqrt(sum(diff.^2, 2)); end costMatrix(costMatrix 50) Inf; % 距离超过 50 像素不匹配 % 第三步匈牙利算法求最优匹配 if numTracks 0 numDets 0 [matchIdx, unmatchedTracks, unmatchedDets] ... matchpairs(costMatrix, 30, min); else unmatchedTracks 1:numTracks; unmatchedDets 1:numDets; matchIdx zeros(0, 2); end % 第四步更新匹配上的轨迹 for i 1:size(matchIdx, 1) t matchIdx(i, 1); d matchIdx(i, 2); z detCenters(d, :); [tracks(t).x, tracks(t).P] updateKF(..., tracks(t).x, tracks(t).P, z, H, R); tracks(t).age tracks(t).age 1; tracks(t).hits tracks(t).hits 1; tracks(t).timeSinceUpdate 0; end % 第五步未匹配的轨迹标记为丢失 for t unmatchedTracks tracks(t).timeSinceUpdate tracks(t).timeSinceUpdate 1; end % 第六步未匹配的检测新建轨迹 for d unmatchedDets tracks(end1) struct(id, nextId, ... x, [detCenters(d,:), 0, 0], ... P, eye(4)*50, ... age, 1, hits, 1, timeSinceUpdate, 0); nextId nextId 1; end % 第七步删除丢失过久的轨迹 toDelete [tracks.timeSinceUpdate] maxInvisible; tracks tracks(~toDelete); % 输出当前帧的确认轨迹hits minHits confirmed tracks([tracks.hits] minHits); fprintf(Frame %3d: %d active tracks\n, k, length(confirmed)); end end这段代码缺两个函数predictKF和updateKF。它们就是标准的卡尔曼滤波两步。我给你可直接替换的写法function [xPred, PPred] predictKF(x, P, A, Q) xPred A * x; PPred A * P * A Q; end function [xUpd, PUpd] updateKF(x, P, z, H, R) y z - H * x; % 新息innovation S H * P * H R; % 新息协方差 K P * H / S; % 卡尔曼增益 xUpd x K * y; PUpd (eye(size(P)) - K * H) * P; end注意这段代码的处理顺序是所有轨迹先统一预测再做匹配再更新。为什么不能预测一条匹配一条因为匈牙利算法需要完整的代价矩阵才能求全局最优。如果逐条预测并立即匹配后面轨迹的匹配结果会受前面匹配顺序的影响这在多目标跟踪里叫作“贪心匹配”虽然速度快但冲突区域内容易出错。关于matchpairs的第三个参数 30它表示匹配代价阈值。大于 30 的代价会被当作不可匹配。更严谨的做法是先将代价大于阈值的位置设为 Inf再用matchpairs这样门控逻辑更清晰。两种方式结果等价但建议用后者原因是在后续将 IoU 代价换成马氏距离时代码更统一。3.3 为什么不用 MATLAB 自带的 trackerGNN这一节讨论一个在实际选型中一定会遇到的问题既然 Sensor Fusion and Tracking Toolbox 里有现成的trackerGNN为什么要手写两个理由。其一工具箱的航迹管理逻辑依赖objectDetection对象它要求输入必须是结构体数组且包含Measurement、MeasurementNoise字段而很多做图像处理的工程师手头的数据是 m×4 的框坐标矩阵转换成本不高但初次接触时容易踩数据格式的坑其二手写版本方便你改关联逻辑比如把中心点距离换成 IoU或者把匈牙利匹配换成贪心最近邻这对于研究和对比实验是必需的。工具箱版本也有它不可替代的价值它内置了航迹合并、航迹碎片重组、确认/删除逻辑的成熟实现。我的建议是——先手写一遍核心循环确保你理解数据流再在项目交付时切换到工具箱实现因为你维护手写版本的成本会随着目标数增加而快速上升。4. 参数调优与实战数据集上的多目标跟踪实验4.1 检测器质量决定多目标跟踪的上限多目标跟踪领域的常识是MOTA 指标的头部差异80% 来自于检测器的性能而不是跟踪器本身。一个 AP 从 70% 提到 85% 的检测器带来的 MOTA 提升远大于把匈牙利匹配换成 MHT。所以开始调跟踪器参数之前先拿到你检测器在自己场景的 Precision-Recall 曲线。当检测器的平均置信度高于 0.7 时Q 矩阵的数值可以偏小当检测器输出不稳定比如车辆检测中把阴影误检成车则需要把minHits调大到 3 或 4否则大量碎片轨迹会不断刷新 ID。还有一个常被忽略的点检测框的格式统一。你的检测器可能输出[x1, y1, x2, y2]而跟踪器内部使用[cx, cy, w, h]MATLAB 的bboxOverlapRatio默认接受[x, y, w, h]格式如果混用IoU 计算出的结果必然错误。写一个统一的转换函数放在公共目录里是所有多目标跟踪 MATLAB 项目的第一步。function boxXYWH convertBoxFormat(boxXYXY) % 从 [x1 y1 x2 y2] 转为 [x y w h] boxXYWH [boxXYXY(:,1), boxXYXY(:,2), ... boxXYXY(:,3) - boxXYXY(:,1), ... boxXYXY(:,4) - boxXYXY(:,2)]; end4.2 轨迹生命周期参数的设置原则参数含义推荐初始值调参信号maxInvisible轨迹最多丢失多少帧后被删除3~5目标频繁消失时调大ID 跳变时调小minHits新建轨迹连续命中几次才确认2~3误检多时调大低帧率时调小IoU threshold允许关联的最小 IoU0.3目标重叠场景调大到 0.4小目标场景调小到 0.2AssignmentThreshold最大允许代价30~50代价度量改变时必须同步调整参数调优的原则是训一调二即一个参数调节必须对着一个具体的失效模式。运行完跟踪器之后把轨迹可视化出来重点观察三种现象ID Switch 发生在遮挡前后会表现为同一个目标的框上方 ID 标签在几帧内跳变碎片化轨迹即一个目标被切成多条轨迹通常是因为maxInvisible太小或置信度阈值太低轨迹漂移即目标已经转弯轨迹却继续直线外推这时要检查状态模型是否包含加速度估计或者检测器的置信度阈值是否需要下调。4.3 在真实视频上跑通多目标跟踪的完整流程拿一段 MOT 社区公开的行人视频做实验流程分三步。第一步用 detector 逐帧提取检测框并保存为.mat文件字段建议用boxes和scores两个矩阵——这一步解耦检测与跟踪方便 Debug。第二步运行第 3 章手写跟踪器输入是 boxes输出是轨迹结构体。第三步可视化叠加。% 可视化轨迹按 ID 着色 colors lines(max([tracks.id])); figure; imshow(frame); hold on; for t 1:length(tracks) if tracks(t).hits 2 box tracks(t).x(1:2) - [tracks(t).x(3)/2, tracks(t).x(4)/2]; rectangle(Position, [box, tracks(t).x(3:4)], ... EdgeColor, colors(tracks(t).id, :), LineWidth, 2); text(box(1), box(2)-5, sprintf(ID %d, tracks(t).id), ... Color, colors(tracks(t).id, :), FontSize, 12); end end hold off;可视化验证是调参的最高优先级手段任何一个指标都替代不了肉眼观察。具体说你要看的是两目标交叉后 ID 是否保持目标短暂走到障碍物后面再出来时是新 ID 还是原 ID背景中的误检是否迅速被删除。观察 30 秒视频你就能得出比 MOTA 分数更直观的结论。4.4 MOTA 与 HOTA多目标跟踪的两个维度MOTA 是历史最悠久的指标它把三项错误FP、FN、ID Switch加权重组成一个总分它的缺陷是 ID Switch 在公式中只占了固定权重1/numGT即使连续切换多次同一个目标的 IDMOTA 受的惩罚也远低于检测错误。HOTA 是后来的修正版它把检测精度和关联精度分离开来对 ID 一致性更敏感尤其适合评估交叉、遮挡频繁的场景。这就是为什么我今天特意把 HOTA 的计算单独拎出来放在最后一章盲目追逐 MOTA 数值提高会让你的跟踪器在 ID 一致性上偷工减料而工程交付时最容易被用户感知到的恰恰是频繁的 ID 跳变。5. 用 MATLAB 计算 HOTA 指标并定位 ID 跳变帧HOTA 的计算核心是建立一个“预测轨迹点”和“真值轨迹点”之间的匹配然后对每个匹配点计算三件事检测是否准确Det、关联是否准确AssA、以及两者的几何平均。公式为$$HOTA \sqrt{DetA \times AssA}$$其中 DetA 就是常规的检测召回/精度综合值AssA 则是衡量在两个各自连续轨迹片段上是否一致。用 MATLAB 实现时关键是先构造匹配对再分组计算关联矩阵。function hotaScore computeHOTA(gtBoxes, trkBoxes, maxDist) % gtBoxes: cell 数组每个元素是 n_gt x 4 的真值框 % trkBoxes: cell 数组每个元素是 n_tr x 4 的跟踪框 numFrames length(gtBoxes); matches cell(1, numFrames); % 第一层匹配帧内按 IoU 匹配 for k 1:numFrames if isempty(gtBoxes{k}) || isempty(trkBoxes{k}) matches{k} zeros(0, 2); continue; end iouMat bboxOverlapRatio(gtBoxes{k}, trkBoxes{k}); cost 1 - iouMat; cost(iouMat 0.5) Inf; % IoU 0.5 不算匹配 matches{k} matchpairs(cost, 1, min); end % 第二层匹配根据匹配对分配 track ID 与 GT ID 的关联 % 统计每个 (gtID, trkID) 组合匹配的帧数得到关联矩阵 % 这个矩阵的行是 GT 轨迹 ID列是跟踪器轨迹 ID % 关联矩阵中共享相同 ID 组合的点构成一个“关联对” % 为简化示例这里假设 gtID 就是 gtBoxes 的行号 % 实际使用时需要把检测框转换为具体轨迹 ID --- 这部分代码省略 % 最终 HOTA 计算需要遍历所有匹配点 tpCount size(cat(1, matches{:}), 1); % 真实实现需基于轨迹 ID 求每个关联对的 DetA 和 AssA % 这里给出最简指示 hotaScore tpCount / max(1, sum(cellfun((x) size(x,1), gtBoxes))); end这段代码只是演示了第一层 IoU 匹配的构建方式真正的 HOTA 计算需要为每个跟踪器输出分配一个全局轨迹 ID。这正是我推荐你在跟踪器内部保存id字段的原因——没有稳定的轨迹 ID你无法评估关联精度也永远无法定位 HOTA 低效的根源。定位 ID 跳变帧的一个实用技巧是比较相邻两帧的匹配结果统计前一帧轨迹 ID 与当前帧轨迹 ID 对应关系任何一对多或多对一的关系都是可疑的 ID 跳变点。你可以把跳变帧号连同视频帧保存为图片frameID find(idSwitchFrames); for f 1:length(frameID) imwrite(extractFrame(videoObj, frameID(f)), ... sprintf(idSwitch_%04d.png, frameID(f))); end这套方法在调试 DeepSORT 或 ByteTrack 时同样有效。无论你最终选择工具箱还是手写实现把 HOTA 的评估代码保留在工程目录中持续观察它的走势——当检测器升级或场景迁移时HOTA 比 MOTA 更能揭示真实质量变化。最后给出一个实操框在matlab中跑完评估后用disp输出TD总检测数、FP、FN、IDSW四个原始计数这四个数字能告诉你数字背后的叙事比单个百分比分数可靠得多。本文还有配套的精品资源点击获取