1. 为什么自主导航里的"跟踪"不能只靠"检测":先想清楚要解决什么问题
先从一个真实场景说起。我在做自主导航小车时,一开始觉得目标检测已经够了——每帧都能把行人、车辆框出来,导航系统拿到坐标避障不就完了?结果第一次上路实测就吃了亏:小车跟着一个行人走,人走到树后面遮住了两秒,检测框消失,等再露头时算法已经把它当成一个新目标了。小车愣在原地面面相觑,完全不知道该继续走还是重新规划。
这个教训让我明白,目标跟踪和目标检测是两回事。检测解决的是"这一帧画面里哪里有目标"的问题,而跟踪解决的是"这个目标在连续的视频帧里是谁、往哪去、下一刻会在哪"的问题。自主导航恰恰需要的是后者。你想一下,导航避障不只是要知道"现在这里有个人",更要知道"这个人正在往我这个方向移动,1.5秒后可能挡在我路径上"——这个预判能力只靠单帧检测是做不到的,必须靠跟踪把帧与帧之间的目标关联起来,建立时间维度上的连续性。
把话说得更直白一点:检测是无状态的,跟踪是有状态的。无状态意味着每帧都是孤岛,哪怕同一个行人,换个角度换个光照,模型可能给出完全不同的置信度,甚至漏检。跟踪则通过维护目标的状态信息(位置、速度、轨迹、ID),让系统能够容忍短时间的目标丢失、遮挡,并对目标的运动趋势做出预估。
在自主导航这个场景里,目标跟踪具体要解决的痛点可以归纳成四类:
- 跨帧身份保持:同一个障碍物不能因为检测框抖动就换ID,否则路径规划会因为目标数量忽多忽少而频繁重算。
- 运动预测:知道了目标的航向和速度,导航系统才有时间提前减速或绕行,而不是等目标到了跟前才急刹。
- 遮挡恢复:行人被树、广告牌、帧间暂时遮挡时,跟踪器能基于历史轨迹估计其未出现期间的位置。
- 相机运动下的目标关联:自主导航里相机是装在移动载体上的,背景本身也在动,这时候区分"相机运动造成的像素位移"和"目标自身运动"是最大的难点。
说白了,目标跟踪就是给检测结果插上时间的翅膀。后面我会详细讲如何落地。
2. 技术选型:OpenCV内置跟踪器与YOLOv11关联追踪,到底该选哪条路
目标跟踪经过这么多年的发展,路线其实非常清晰,就两大类:纯几何/外观的传统跟踪方法和检测+数据关联的跟踪方法(Tracking-by-Detection)。选型之前先把两条路线的底牌看清。
2.1 OpenCV内置的八种传统跟踪器,别指望它什么都能跟踪
OpenCV contrib库里内置了不少传统跟踪器,日常被提起的主要有这八种:
| 跟踪器 | 算法类型 | 优点 | 劣势 | 适合场景 |
|---|---|---|---|---|
| KCF | 相关滤波 | 快,约几百FPS | 无尺度变化适应,遮挡易丢 | 简单目标、高帧率 |
| CSRT | 判别式相关滤波 | 精度高,支持尺度估计 | 速度中等,约40-80FPS | 目标形变不大、精度优先 |
| MOSSE | 相关滤波 | 极快,可达数百FPS | 精度一般,容易漂移 | 低资源设备 |
| BOOSTING | 在线Adaboost | 简单 | 遮挡、光照变化几乎必丢 | 不推荐用 |
| MIL | 多实例学习 | 对目标旋转容忍度较高 | 精度偏低 | 快速原型验证 |
| TLD | 检测+跟踪融合 | 能处理遮挡后重现 | 计算量大,抖动明显 | 老项目兼容 |
| MedianFlow | 光流法 | 对刚体目标跟踪稳定 | 形变、快运动易丢 | 刚体、匀速目标 |
| GOTURN | 深度学习(离线) | 速度尚可 | 训练依赖通用数据集 | 迁移效果不稳定 |
我在实际项目中用过KCF和CSRT居多。KCF跟踪一个在画面里缓慢行走的行人,前几十帧表现还不错,可一旦行人转身,外观特征突变,框就开始漂,慢慢飘到背景去。CSRT相对稳一些,但遇到目标被完全遮挡再出现时同样救不回来——这些跟踪器没有"全局重检测"的能力,丢失就是真丢了。
而且这里有个致命限制:传统跟踪器需要手动初始化目标框。在自主导航场景里,目标随时从画面任意位置出现,你根本不可能每来一个新行人就点一下框。传统跟踪器可以做"单个目标的持续跟随",但做不到"多目标新增/离开的自动管理"。所以纯OpenCV传统跟踪器在完整自主导航系统里,顶多做个辅助角色。
2.2 Tracking-by-Detection:YOLOv11+ByteTrack/DeepSORT这条主路为什么是主流
当前工业界和学术界最主流的目标跟踪范式,就是检测+数据关联。思路一句话概括:用检测器找到每一帧的目标框,用数据关联算法把相邻帧属于同一个目标的框连接起来,再通过运动模型(通常是卡尔曼滤波)平滑轨迹。
这个范式的核心组件有三个:
基座检测器:我用的是YOLOv11(最新热词里也大量指向yolov11目标跟踪,不是没有原因的)。相比上一代,YOLOv11在检测精度和推理速度的平衡上进一步提升,尤其在COCO行人类别的召回上表现可圈可点,而且官方repo还带了定向检测、实例分割等分支,给后面扩展留了余地。
运动预测器:最常用的是卡尔曼滤波(Kalman Filter)。它根据目标的历史位置和速度,预测它在下一帧可能出现的位置。为什么需要它?因为检测器不是每帧都能命中的,检测框本身也有噪声,卡尔曼滤波能让轨迹更平滑,并且为数据关联提供"预测框"作为匹配依据。
数据关联器:经典方案有DeepSORT和ByteTrack。DeepSORT在SORT基础上加了外观特征匹配,用ReID模型提取的表观特征来辅助匹配,解决ID Switch问题。ByteTrack的思路则不同,核心是利用低置信度检测框——普遍情况是遮挡目标、远距离目标检出的置信度偏低,传统做法直接丢掉这些框,但ByteTrack认为高置信度框匹配完剩余的低置信度框仍然关联了遮挡目标的正确位置,把它们也纳入匹配流程,能显著减少遮挡引起的轨迹断裂。
我自己最终选的是YOLOv11检测器+ByteTrack关联器。理由很实际:
- DeepSORT需要额外的行人重识别模型,多一个模型就多一份算力开销和初始化时间,对Jetson这类边缘设备不友好。
- ByteTrack无需额外训练,直接吃检测器的输出,实现极简。
- 字节的消融实验和我的实测都显示,在行人和车辆这类刚性/半刚性目标上,ByteTrack的ID Switch比DeepSORT低,同时速度更快。
当然,如果项目对身份确认有更高要求(比如要记录某个特定人员的移动轨迹),那DeepSORT的多粒度外观特征就更合适。先把场景需求摆出来再选型,别拍脑袋。
2.3 我的选型结论与架构总览
最终搭建起来的跟踪模块架构是这样的:
视频帧输入 │ ▼ YOLOv11推理 ──► 检测框集合 (xyxy, conf, class) │ ▼ ByteTrack关联器 ──► 卡尔曼滤波预测下一帧位置 │ │ │ ▼ │ 匈牙利匹配算法 (IoU距离) │ │ │ ▼ │ 轨迹更新 / 新增轨迹 / 轨迹消亡 │ ▼ 输出:目标ID、历史轨迹、预测位置 ──► 导航避障模块整个链路里YOLOv11负责"看得见",ByteTrack负责"认得出",卡尔曼滤波负责"算得准"。下面我直接给完整可复现的代码实现。
3. 手把手实现:YOLOv11+ByteTrack多目标跟踪的完整链路
3.1 环境准备:版本对齐能省掉一半的报错
这一步我用的是Python 3.9 + PyTorch 2.x + ultralytics 8.3.x的组合。再强调一次,版本对齐很重要,ultralytics的API在不同小版本之间改过几次接口签名,最好统一用我列的这一套。
# 创建虚拟环境 conda create -n nav_track python=3.9 conda activate nav_track # 安装PyTorch(NVIDIA GPU用户按官网选对应CUDA版本) pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装YOLOv11所在的ultralytics包 pip install ultralytics==8.3.23 # 安装ByteTrack依赖(cython_bbox在Windows下需要编译器,Linux直接装) pip install cython pip install -e git+https://github.com/ifzhang/ByteTrack.git@main#egg=bytetrack装完以后先跑个最小验证,确认YOLOv11能调用:
from ultralytics import YOLO model = YOLO("yolo11n.pt") results = model.predict("test.jpg") print(results[0].boxes.xyxy.shape)能输出框坐标就算成功。如果卡在half precision或者显存分配,大概率是CUDA和torch版本不配,重装torch即可,别去debug底层的报错浪费时间。
3.2 检测端:YOLOv11推理的工程化处理
检测端表面上就是一句model.predict(),但工程化落地时细节很多。我封装了一个带帧率控制和预处理缓存的检测器,核心逻辑如下:
class YOLOv11Detector: def __init__(self, weights="yolo11n.pt", device=None, conf=0.25, imgsz=640): self.model = YOLO(weights) self.device = device or ("cuda:0" if torch.cuda.is_available() else "cpu") self.conf = conf self.imgsz = imgsz def detect(self, frame_bgr): # ultralytics内部会做BGR->RGB、缩放、letterbox results = self.model.predict( source=frame_bgr, device=self.device, conf=self.conf, imgsz=self.imgsz, classes=[0, 1, 2, 3], # 行人、自行车、汽车、摩托车 verbose=False, ) if not results or len(results[0].boxes) == 0: return np.empty((0, 5)) boxes = results[0].boxes # ultralytics的xyxy是相对原图的坐标,不用自己反算 xyxy = boxes.xyxy.cpu().numpy() confs = boxes.conf.cpu().numpy() # 返回 N x 5: x1, y1, x2, y2, conf return np.hstack([xyxy, confs.reshape(-1, 1)])几个实际工程点的提醒:
- 记得指定classes。自主导航一般只需要行人、车辆等少数类别,过滤掉其他类别既省算力又减少误检。
- 开启TensorRT加速(Jetson平台):在predict参数里加
device="cuda:0"后如果还是吃不满性能,可以用TensorRT导出:
然后加载engine文件。实测在Jetson Orin Nano上fps能从22涨到45左右。model.export(format="engine", imgsz=640, half=True) - 推理结果不需要手工缩放:ultralytics返回的xyxy已经是相对于原输入图像的坐标,直接把框送给跟踪器即可,别再写一遍letterbox反变换,这是新手最容易画蛇添足的地方。
3.3 跟踪端:ByteTrack数据关联的核心逻辑
ByteTrack的精髓在于**"两次匹配"**。第一次用高置信度框和现有轨迹做IoU匹配,匹配不上的轨迹先不判死,留着;第二次把低置信度框也参与匹配,补上被遮挡目标的轨迹。代码层面它封装了一个STrack类(带卡尔曼状态)和一个BYTETracker类。
这里我拆解一下ByteTrack的几个关键参数:
class BYTETrackerArgs: track_thresh: float = 0.5 # 高置信度阈值,用于第一轮匹配 track_buffer: int = 30 # 轨迹寿命:目标丢失后最多保留的帧数 match_thresh: float = 0.8 # 匹配的IoU阈值下限 aspect_ratio_thresh: float = 1.6 # 剔除长宽比极端的框(防止把柱子当行人) min_box_area: float = 10 # 最小框面积,过滤太小的噪点框 mot20: bool = False # 是否使用MOT20格式的匹配逻辑实际使用起来非常简单:
from bytetrack import BYTETracker tracker = BYTETracker(BYTETrackerArgs()) detector = YOLOv11Detector() while True: ret, frame = cap.read() if not ret: break # 1. 检测 dets = detector.detect(frame) # (N,5) 每行 = [x1,y1,x2,y2,conf] # 2. ByteTrack更新 online_targets = tracker.update( dets[:, :4], # 框坐标 dets[:, 4], # 置信度 None, # 类别信息,ByteTrack本身不区分类别 None # 这里是feat,ByteTrack用不到,传None就行 ) # 3. 取结果 for t in online_targets: tlwh = t.tlwh # 当前框(x,y,w,h) track_id = t.track_id # 这个ID就是跨帧稳定的目标标识,直接送到导航模块等等,这里有个需要特别说明的坑:ByteTrack本身不感知类别。刚才检测器检测了四类目标,输出框里混了行人、汽车、自行车,ByteTrack会把它们统一当成"可跟踪目标"来做关联。这会导致一个行人轨迹突然跳到一辆车上——虽然IoU匹配通常会阻止这种离谱跳变,但在密集场景下ID错配概率会增加。所以稳妥做法是按类别分别调tracker,每个类别独立一个对象:
# 按类别独立跟踪,避免跨类别ID串扰 trackers = {cls_id: BYTETracker(BYTETrackerArgs()) for cls_id in [0, 1, 2, 3]} dets = detector.detect_with_class(frame) # 返回 N x 6 = [x1,y1,x2,y2,conf,cls] for cls_id, trk in trackers.items(): cls_dets = dets[dets[:, 5] == cls_id] if len(cls_dets) == 0: trk.update(np.empty((0, 4)), np.empty((0, 1)), None, None) else: trk.update(cls_dets[:, :4], cls_dets[:, 4], None, None)这个细节能显著降低ID Switch,代价只是多了几个tracker对象的开销,非常划算。
3.4 导航模块怎么消费跟踪结果:不只是拿个坐标
跟踪器输出了ID、当前框、轨迹历史,但导航模块真正需要的是目标的速度矢量和预测位置。这两者都可以从轨迹历史里算出来:
def compute_target_velocity(history, max_age=10): """ history: deque of (timestamp, cx, cy),按时间正序 返回: (vx, vy) 像素/秒 """ if len(history) < 2: return (0, 0) # 只取最近一小段,避免历史速度拉平均导致延迟 recent = list(history)[-max_age:] if len(recent) < 2: return (0, 0) dt = (recent[-1][0] - recent[0][0]).total_seconds() if dt < 1e-6: return (0, 0) vx = (recent[-1][1] - recent[0][1]) / dt vy = (recent[-1][2] - recent[0][2]) / dt return (vx, vy)注意这里的总时间跨度不能太长,否则速度是"过去一段时间的平均速度",对突然加速的目标反应太慢。我实测用最近0.5秒的轨迹计算速度,在行人变速场景下,预测位置能提前0.3秒给出,足够导航做减速决策了。
有个简单的几何变换要提前做:把像素坐标的速度换算到小车坐标系。做法是事先标定相机内参和外参,再用cv2.solvePnP求地面平面上的单应矩阵H,把像素速度乘以H得到实际地面速度。这一步最简单也最容易忽略,但没做的话速度值只有相对意义,没法指导真实控制。
4. 自主导航场景下的动态跟踪:相机本身在动,怎么分离目标运动和背景运动
传统目标跟踪算法有个隐含假设:相机是静止的。OpenCV传统跟踪器基本都是这个假设。可自主导航的相机是长在小车上的,小车一开动,整个背景都在运动,这种情况下再直接比较相邻帧的IoU就会出大问题——目标的框在像素坐标系下移动了,可能只是因为相机转弯,而不是目标真的动了。
4.1 问题本质:运动来源的叠加
目标在像素坐标系的运动量近似等于两个运动的叠加:
像素位移 ≈ 目标自身运动(世界系) + 相机自运动造成的图像位移(背景场)在没有相机姿态信息的情况下,这两者完全混在一起。比如小车右转弯,画面中所有静止物体都向左移动,一个行人站着不动,看起来却像以车速向左移动。导航系统如果拿这个"表观运动"去避让,绝对会误判,甚至原地打转。
4.2 实践中最常见的错误做法
我最开始的做法是直接判断"目标靠近小车就避让",结果小车低速直行时还好,一旦转弯,因为背景运动导致的假目标速度非常大,导航系统误以为行人高速冲过来,频繁急停。后来加了速度平滑(低通滤波)依然不行——因为这是系统性的偏差,不是噪声,低通滤波只会让响应变钝,错得更离谱。
还有另一个错误是企图用光流法直接补偿背景。全局光流确实能算出相机运动场,但在目标占据大面积画面的情况下(比如近距离行人),目标本身的像素会污染背景运动估计,补偿结果时好时坏。
4.3 可行方案:地面平面假设+单应变换补偿
我最终采用的是地平面假设下的补偿方案,思路简单说:
- 假设所有目标都在地面平面上运动(行人的脚、车底与地面接触区域可以作为锚点);
- 相邻帧之间,地面平面在图像上的变换可以用一个单应矩阵H描述;
- 用连续帧之间匹配到的地面特征点,估计出这个H;
- 用H把上一帧在像素坐标系下的框坐标变换到当前帧的相机视角下,再做IoU匹配和运动计算。
这样就把"相机运动"的影响从"目标运动"里剥离了。
具体实现里,我用了cv2.findHomography加RANSAC估计H:
def estimate_homography(prev_gray, curr_gray, max_corners=200): # 用Shi-Tomasi角点检测找特征点 prev_pts = cv2.goodFeaturesToTrack( prev_gray, max_corners, 0.01, 10, mask=ground_mask ) if prev_pts is None or len(prev_pts) < 8: return None # LK光流跟踪这些特征点 curr_pts, status, _ = cv2.calcOpticalFlowPyrLK( prev_gray, curr_gray, prev_pts, None ) good_prev = prev_pts[status.flatten() == 1] good_curr = curr_pts[status.flatten() == 1] if len(good_prev) < 8: return None # 这里ground_mask是我们事先划定的地面区域掩码, # 把天空、墙体等非地面特征排除掉 H, _ = cv2.findHomography(good_prev, good_curr, cv2.RANSAC, 3.0) return Hground_mask怎么定?我是在相机安装好后,对着纯地面区域标定出一个多边形,之后所有特征点只在这个多边形内选取。这个掩码同时天然排除了远处和高处的特征点,对提升H估计的稳定性帮助巨大。
拿到H之后,把上一帧的跟踪框变换到当前帧视角:
def warp_boxes(boxes, H): """boxes: N x 4 [x1,y1,x2,y2],用单应矩阵做透视变换""" if H is None or len(boxes) == 0: return boxes # 对框的四个角点做透视变换 corners = np.array([ [boxes[:, 0], boxes[:, 1], np.ones_like(boxes[:, 0])], [boxes[:, 2], boxes[:, 1], np.ones_like(boxes[:, 0])], [boxes[:, 0], boxes[:, 3], np.ones_like(boxes[:, 0])], [boxes[:, 2], boxes[:, 3], np.ones_like(boxes[:, 0])], ]) # 4 x N x 3 # 简化写法:对每个框的中心点+宽高直接变换, # 生产环境建议对四角点做完整透视变换后重新取bbox centers = np.hstack([ (boxes[:, 0:1] + boxes[:, 2:3]) / 2, (boxes[:, 1:2] + boxes[:, 3:4]) / 2, np.ones_like(boxes[:, 0:1]), ]) warped = (H @ centers.T).T warped = warped[:, :2] / warped[:, 2:3] return warped有了补偿后的坐标,再做数据关联就可靠多了。这套东西跑通后,我的小车在转弯和加减速过程中,跟踪框依然能稳定咬住目标,避让决策也回归正常。
4.4 什么场景下这个方案会失效
地平面假设不是万能的。下面这些情况我都会明确标识为失效边界:
- 目标不在平面上:比如无人机视角下目标在楼顶,地面掩码盖不住;
- 剧烈颠簸:相机安装刚性差,roll/pitch抖动过大,单应矩阵无法描述完整运动(本质上是相机位姿六自由度变化,地平面假设强行降到了三维);
- 纯旋转运动:小车原地旋转时,地面特征点位移很小,但远处目标位移很大,单应阵对远处物体的补偿不完全准确。
在这些场景下,更严谨的路线是引入IMU里程计或者视觉惯性里程计获取相机六自由度姿态,把相机运动完全解析掉,再在"准静止相机"坐标系里做跟踪。这也是我下一步准备做的升级方向。
5. 实测踩坑记录:ID Switch、遮挡丢失、低帧率下的真实教训
代码跑通和现场稳定是两回事,我在真实的园区道路上测了大概两周,下面这几个问题是我踩得最深的,每一个都花了至少一整天排查。
5.1 遮挡丢失:走过去又回来,ID和目标全乱了
这个坑在行人被树或路灯杆短暂遮挡时最容易触发。ByteTrack的track_buffer=30本来能容忍30帧的丢失,但注意:容忍的前提是目标再次出现时位置足够接近预测轨迹。如果目标在被遮挡期间改变了运动方向(比如绕了个小弯),再出现的位置和卡尔曼预测的位置差很多,IoU匹配不上,触发器就判这个轨迹"死亡",目标被当成新目标重新编号。
解决路径有三条,按性价比排序:
- 拉长track_buffer,并适当调低
match_thresh。track_buffer=60、match_thresh=0.65可以明显减少短期遮挡导致的ID切换,代价是目标真的离开后,轨迹会多存活一会儿,造成目标数量虚高。对导航避障来说,虚高比漏检安全,这个trade-off是值得的。 - 维护轨迹的"外观特征历史":把目标框区域的简单颜色直方图存下来,在IoU匹配失败时用直方图相似度做二次匹配。完全没必要上重型ReID模型,一个HSV直方图就能挡住一半以上的遮挡场景。
- 对长时间遮挡目标,不做彻底删除,而是标记"lost"状态:让导航模块知道"这里有个目标曾经存在,往这个方向运动",如果5秒内没有新目标出现再清除。对路径规划来说,一个"幽灵目标"比一个"突然冒出的新目标"更安全。
5.2 ID Switch:最容易让导航系统崩溃的细节问题
我先说结论:ID Switch在慢速简单场景下不常见,但越是密集目标、越是目标互相靠近走动的场景,越容易出现。最典型的是两个人迎面走过,瞬间交叉,两个跟踪框重叠面积超过最小IoU阈值时,ByteTrack直接把两个ID对调了。
ID错乱为什么影响导航?因为我的导航模块是用目标ID来索引轨迹和做速度平滑的,ID一变就相当于"旧目标消失+新目标出现",速度历史全部重置,刚算出来的惧让轨迹就报废了。严重时小车会对着明明匀速走来的行人一顿一顿加速减速。
我用的可行降低方案:
- 加入外观特征二次校验:检测框里提取的颜色直方图+LBP纹理拼接成一个轻量级特征向量。IoU匹配上之后再做一次特征相似度验证,相似度低于阈值就认为可能是ID串扰,进入二次分配流程。
- 对轨迹速度做一致性约束:一个目标一帧内移动距离超过物理可能速度(比如行人超过5帧内偏移超过200像素),大概率是匹配错误,拒绝这次关联。
- 实测效果:按上面两条改进后,ID Switch从每小时二十几次降到两三次。还没做到零,但对导航决策来说,ID偶尔跳一变已经完全够用了,因为避障看的是所有目标的位置和速度,剩下那两三次错误会被平滑掉。
5.3 低帧率场景:处理速度跟不上的连锁反应
在Jetson平台上,实时推理加跟踪对算力压力很大。我开始跑的是YOLOv11s(比nano大一档),帧率只有12fps左右,结果跟踪效果急剧恶化。原因不复杂:帧率低,相邻帧之间目标的像素位移就大,IoU重叠率变小,关联失败率上升。
解决办法当然有两条路:要么提高推理速度,要么让跟踪器适应低帧率。
提高推理速度我做了三件事:
- 换模型:从YOLOv11s降级到YOLOv11n,精度掉得不多,帧率从12提高到22。
- TensorRT导出fp16:在Jetson上再把帧率往上拔到30+。
- 检测间隔+跟踪推理:对导航场景不需要每帧都跑检测,我用"每3帧跑一次检测+间帧用卡尔曼预测补充"的策略,系统帧率直接到40fps,而跟踪精度几乎没损失。
逻辑其实很简单:
frame_count = 0 DETECT_INTERVAL = 3 while True: ret, frame = cap.read() frame_count += 1 if frame_count % DETECT_INTERVAL == 0: dets = detector.detect(frame) targets = tracker.update(dets[:, :4], dets[:, 4], None, None) else: # 不用检测,仅用卡尔曼滤波做预测推进轨迹 tracker.update(np.empty((0, 4)), np.empty((0, 1)), None, None) # 此时online_targets仍然在内部predict后返回,拿到的就是预测位置注意ByteTrack内部在检测框为空时也会调用卡尔曼预测并返回预测框,正好用于间帧补位。这套模式本身不复杂,但你必须在update里传入空检测框,而不是跳过调用,否则轨迹状态不会推进。
5.4 实测得到的一组参考数据
最后把我一套配置在园区道路的实测结果放出来作参考。测试环境:Jetson Orin Nano 8GB,500万像素摄像头,中等光照,行人+非机动车混合路段,测试时长约2小时:
| 指标 | 数值 | 说明 |
|---|---|---|
| 平均跟踪精度(MOTA风格) | 68.3% | 含大量遮挡和远距离目标 |
| ID Switch次数 | 7次/小时 | 改进前约25次/小时 |
| 遮挡恢复率 | 81% | 遮挡时间<1秒场景 |
| 平均处理延迟 | 32ms | 含检测推理,不含显示 |
| 跟踪目标最大数量 | 14个 | 超过后丢帧率上升明显 |
坦白讲,这个水平还没到可以完全放手的程度,但作为自主导航的感知层,已经能让导航模块拿到稳定可靠的目标输入了。
最后说一个我自己长期实操的体会:与其花大量时间刷精度指标,不如先把跟踪结果的可信度和失败模式管理好。你的导航模块接收到一个目标消失、一个新目标出现时,到底该信任多少?该切换避障策略还是维持上一秒的决策?这些"元规则"设计好了,整个系统的稳定性会有一个质的飞跃。后面有空我再单独写一篇关于跟踪结果如何与路径规划层做交互的文章。