简介该资源将YOLOv5实时检测、OpenCV DNN模型部署与卡尔曼滤波状态估计融为一体面向自动驾驶、无人机导航、视频监控等真实场景的开发者重点解决目标短暂丢失后的持续跟踪与位置预测问题。压缩包共34个文件、约47.43MB包含Python源码py、ONNX模型、OpenCV配置文件xml、类别文件names、演示图片jpg以及C工程cpp/h/CMakeLists既有可直接运行的YOLOv5检测与卡尔曼跟踪主脚本也有模型转换脚本和滤波器封装模块便于按需修改。已有4495人学习下载。通过这些代码读者可以掌握从YOLOv5模型加载、目标检测到卡尔曼滤波初始化、预测与更新的一整套实施流程并借助附带的ONNX模型快速复现实验非常适合需要落地深度学习视觉方案的初中级开发者参考。1. 为什么是 yolov5 dnn 卡尔曼滤波先看这套组合解决什么问题监控画面里一个行人被货架挡住两三秒再出现时检测框的 ID 已经不是原来那个了或者车开过路灯底下框突然抖了一下又弹回来。这种「检测结果不稳定、轨迹断掉、身份错乱」的场景就是 yolov5 dnn 卡尔曼滤波Kalman最典型的使用场景。yolov5 负责每帧给出目标位置dnn 负责把训练好的模型在脱离 PyTorch 运行时的情况下做轻量级推理卡尔曼滤波则负责在检测帧之间把位置状态平滑起来并在目标短暂消失时按运动模型做预测。这套组合不依赖重框架代码量能压得很小适合树莓派、RK3568 这类资源有限的边缘板子也适合想做目标轨迹预测而不是只看单帧框的开发者。如果你已经有检测模型接下来想从「每帧几个框」变成「连续、可预测的运动轨迹」这篇就是给你准备的。2. 先跑通检测把 yolov5 模型导出成 OpenCV 能吃的 ONNXOpenCV 的 dnn 模块不直接吃.pt权重你得先把训练好的 yolov5 模型转成 ONNX再用cv2.dnn.readNetFromONNX加载。这一步能不能顺利跑通基本决定了后面所有工作的心情。导出看起来是一行命令的事但中间埋着不少坑尤其是算子版本和输入尺寸这两个点几乎每个人都会踩一遍。2.1 导出 ONNX 时这几个参数别乱改常见做法是用 yolov5 自带的导出脚本把.pt转成.onnx我一般固定这样干python export.py --weights yolov5s.pt --include onnx --opset 11 --img 640 --batch 1这里几个参数先说死--img 640是固定输入尺寸不要用动态尺寸OpenCV 的 dnn 模块碰到动态 shape 会直接报错--opset 11是算子集版本OpenCV 4.5 到 4.8 对 opset 11 的支持最稳opset 12 以上有些算子它不认--batch 1保证输入是固定1×3×640×640导出后在 dnn 里一次只喂一帧图。导出完先别急着接卡尔曼先用 OpenCV 试加载一次确认模型没在转换这一步坏掉import cv2 net cv2.dnn.readNetFromONNX(yolov5s.onnx) print(net.getLayerNames()) # 能打印出层名说明加载成功这里有个容易翻车的细节有些版本的 export.py 默认把 NMS 也导进模型里了。如果 ONNX 里带了 NMS 层OpenCV dnn 加载会慢而且输出结构会变成不定长列表后处理极难写。我一般导出时会把--nms关掉让模型只输出裸的1×25200×85张量NMS 这块自己在代码里做后面查问题也好查。提示拿到的.onnx文件大小如果跟.pt相差太多比如 30MB 以上很可能是把一些训练专用的 BatchNorm 层也带进来了推理没问题但速度会慢。导出脚本一般会自动融合这些层不用手动处理。2.2 DNN 推理的预处理和后处理代码模型加载好之后每帧图片要经过预处理、推理、后处理三个环节我直接给你一套能用的流程代码import cv2 import numpy as np CONF_THRESH 0.4 # 检测置信度阈值 IOU_THRESH 0.45 # NMS 的 IOU 阈值 INPUT_SIZE 640 # 必须和导出时一致 def preprocess(frame): blob cv2.dnn.blobFromImage( frame, scalefactor1/255.0, size(INPUT_SIZE, INPUT_SIZE), mean(0, 0, 0), swapRBTrue, cropFalse ) return blob def postprocess(outputs, frame_shape): # outputs shape: (1, 25200, 85) # 85 80个类别 4个坐标 1个obj置信度 preds outputs[0][0] # (25200, 85) boxes, scores, class_ids [], [], [] for det in preds: obj_conf det[4] if obj_conf CONF_THRESH: continue class_scores det[5:] class_id np.argmax(class_scores) conf obj_conf * class_scores[class_id] if conf CONF_THRESH: continue # 解出中心点坐标和宽高转成左上右下格式 cx, cy, w, h det[0:4] * INPUT_SIZE x1 int(cx - w / 2) y1 int(cy - h / 2) x2 int(cx w / 2) y2 int(cy h / 2) boxes.append([x1, y1, x2, y2]) scores.append(float(conf)) class_ids.append(class_id) if len(boxes) 0: return np.empty((0, 4)), np.empty((0,)), np.empty((0,), dtypeint) idxs cv2.dnn.NMSBoxes(boxes, scores, CONF_THRESH, IOU_THRESH) result_boxes np.array([boxes[i] for i in idxs]) result_scores np.array([scores[i] for i in idxs]) result_classes np.array([class_ids[i] for i in idxs]) return result_boxes, result_scores, result_classes预处理这边有几个参数需要说明scalefactor1/255.0是把像素值归一化到 0 到 1yolov5 训练时就是这么归一化的漏了会导致检测率大幅下降swapRBTrue是因为 OpenCV 读图是 BGR 通道而模型训练时用的是 RGBcropFalse表示保持宽高比较缩放图像不会被裁掉。后处理代码里最关键的是坐标换算模型输出的是归一化坐标0 到 1 之间所以要乘以INPUT_SIZE映射回输入图像的尺寸。如果你的输入图不是 640×640 而是缩放后填了黑边这里还要再按缩放比例还原到原图画面上否则框的位置会对不上原图。我在实际项目里这个坐标还原踩过好几次坑导出时固定尺寸、填入时保持比例后处理才能简单。注意cv2.dnn.NMSBoxes在 OpenCV 4.5 以上返回的是 numpy 数组个别旧版本会返回列表记得np.array()包一下否则后面索引容易出奇怪的IndexError。3. 卡尔曼滤波不是黑匣子状态向量、预测与更新检测器已经能稳定输出每帧的目标框了但如果你直接把相邻两帧的框连起来当轨迹很快会发现框在抖、ID 在跳、目标稍微遮挡一下就彻底丢。卡尔曼滤波在这里做的是对每个目标单独维护一个「运动状态」用上一帧的状态预测这一帧目标应该在哪等检测结果出来后在预测位置附近找匹配再用真实检测值修正预测误差。这样即使某一帧检测框偏了一截最终输出的轨迹也不会跟着乱跳。3.1 状态向量怎么设位置加速度还是带加速度状态向量的设计直接影响跟踪效果但很多人一上来就套公式结果调了半天不知道问题出在哪。我一般用 8 维状态中心点坐标、宽高、以及它们的速度分量状态向量: [cx, cy, w, h, vx, vy, vw, vh] 观测向量: [cx, cy, w, h]为什么带速度因为卡尔曼滤波的预测能力全靠状态转移矩阵里的速度项没有速度的话目标运动时你的预测位置永远滞后一拍。那要不要加加速度我试过在帧率稳定25 FPS 以上时加加速度项会让滤波对噪声更敏感稍微有点抖动就被当成加速运动反而更容易发散。帧率低于 10 FPS 的极端场景可以试试但一般情况下 8 维状态已经够用。vw和vh这两个速度项很多人会删掉理由是目标在画面里的宽高变化确实不明显。但删掉之后有一个副作用目标朝向镜头走近时宽高在快速变大如果没有这两个速度项卡尔曼预测的框会跟不上导致关联失败。所以我的习惯是保留但把它们的噪声调得很小让它们只在真实需要变的时候才起作用。3.2 KalmanFilter 类实现与数据关联状态转移矩阵和观测矩阵按匀速运动模型来写OpenCV 自带cv2.KalmanFilter实现起来非常直接import cv2 import numpy as np class Track: def __init__(self, bbox, track_id): self.id track_id self.kf cv2.KalmanFilter(8, 4) # 状态8维观测4维 # 状态转移矩阵 F: 匀速运动模型 self.kf.transitionMatrix np.array([ [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], ], dtypenp.float32) # 观测矩阵 H: 只观测位置不看速度 self.kf.measurementMatrix np.array([ [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, 0, 0, 0, 0], ], dtypenp.float32) self.kf.statePre np.array([ [bbox[0]], [bbox[1]], [bbox[2]], [bbox[3]], [0], [0], [0], [0] ], dtypenp.float32) self.kf.statePost self.kf.statePre.copy() self.kf.errorCovPre * 1e-2 # 协方差初值 def predict(self): predicted self.kf.predict() return predicted[:4].flatten() def update(self, bbox): measured np.array([ [bbox[0]], [bbox[1]], [bbox[2]], [bbox[3]] ], dtypenp.float32) self.kf.correct(measured)这段代码里几个关键点要说明。transitionMatrix是匀速模型前 4 行的意思是新位置 旧位置 速度 × 1帧间隔按 1 处理后 4 行表示速度保持不变。如果你视频帧率波动很大把这里的 1 换成实际的帧间隔时间不然预测会偏。measurementMatrix表示卡尔曼滤波器只拿位置和宽高当观测值速度是隐含估计出来的。这样即使检测框偶尔跳一下速度也不会剧烈震荡。statePre初始化的位置用第一帧检测框速度初始为 0这是比较保守的做法避免刚开始跟踪时滤波器乱冲。接下来是数据关联核心思路是每个轨迹先用卡尔曼预测这一帧的位置再把预测框和当前帧的检测框做 IOU 匹配距离最近的配对成功。简单实现def iou(box1, box2): x1, y1, x2, y2 box1 x3, y3, x4, y4 box2 inter_x1 max(x1, x3) inter_y1 max(y1, y3) inter_x2 min(x2, x4) inter_y2 min(y2, y4) inter_w max(0, inter_x2 - inter_x1) inter_h max(0, inter_y2 - inter_y1) inter_area inter_w * inter_h area1 (x2 - x1) * (y2 - y1) area2 (x4 - x3) * (y4 - y3) return inter_area / (area1 area2 - inter_area 1e-6) def associate(tracks, detections, iou_threshold0.3): assignments [] used_dets set() used_tracks set() for track in tracks: pred_box track.predict() best_iou iou_threshold best_det -1 for det_idx, det_box in enumerate(detections): if det_idx in used_dets: continue score iou(pred_box, det_box) if score best_iou: best_iou score best_det det_idx if best_det ! -1: assignments.append((track, detections[best_det])) used_dets.add(best_det) return assignments这个贪心匹配在目标数量少小于 20时性能完全够用而且逻辑一目了然。如果画面里目标很多、相互遮挡严重贪心匹配可能选不到全局最优我一般会换成scipy.optimize.linear_sum_assignment跑匈牙利算法代价矩阵也简单就是1 - iou。3.3 手把手调 Q 和 R这两组协方差是滤波器的灵魂Q过程噪声协方差和 R观测噪声协方差这两个参数我一开始也觉得玄学调多了总结下来是有规律可循的。OpenCV 的 KalmanFilter 默认把 Q 初始化为单位矩阵R 也是全 1 矩阵直接跑的结果就是预测框死板、跟丢目标或者抖动不收敛。参数含义初值建议取值过大取值过小Q 前 4 维位置位置过程噪声1e-2 ~ 1e-3框跟着检测值剧烈跳预测过僵跟不上突变运动Q 后 4 维速度速度过程噪声1e-3 ~ 1e-4目标丢失后乱冲速度估计过慢转弯跟不上R观测噪声检测框的信任程度1e-2 ~ 1e-1预测框反应迟钝几乎完全跟检测框失去平滑理解这套逻辑的方式很简单Q 表示你对运动模型的信任Q 越小表示你越相信目标在匀速运动R 表示你对检测结果的信任R 越小表示你越相信检测框是准的。我的初始配置是processNoiseCov的对角线前 4 个设 1e-3、后 4 个设 1e-4measurementNoiseCov对角线设 1e-2。在这个基础上如果目标机动性强就增大前 4 个 Q如果检测框容易抖动就减小 R特别好记。提示调参时不要直接改数字然后看结果那样效率太低。先把 R 固定成 1e-2只调 Q 的速度项速度项能稳住跟踪后再调位置项。每次只改一个参数记录下画面表现才能最快摸清这套模型在你的场景里的脾气。4. 避坑与常见问题dnn 推理和卡尔曼联调的 5 个坑正文就是干活负数不干活会输。我把消耗时间最多的坑按「现象 → 原因 → 解决」写给你看每一条都是从实际运行里拆出来的。4.1 检测框正常但卡尔曼预测框越飞越远现象目标已经静止不动了但卡尔曼输出的预测框还在缓慢往一个方向漂移几秒后彻底脱离真实目标。 原因Q 矩阵速度项设得太大滤波器对「目标在运动」这个假设过于信任于是把检测框的微小偏差逐步累加成不小的速度静止目标的真实速度是 0但滤波器不肯往下修正。 解决把速度项的过程噪声降到 1e-4 量级同时把观测噪声 R 降到 1e-2让滤波器在静止目标上更相信检测框。如果目标确实会长时间静止还可以在correct()之后对速度项做一点点衰减相当于一个隐藏的摩擦力。4.2 OpenCV dnn 加载 ONNX 时报错或输出全为 0现象readNetFromONNX不报错但 forward 出来的数组全是 0 或者 shape 不对。 原因一部分是导出时带了 NMS 层输出结构变了另一部分是 ONNX 里含有 OpenCV dnn 不支持的算子常见于 opset 版本过高或某些上采样算子。 解决优先把导出参数里的--opset降到 11去掉--end2end这类带后处理的选项重新导出。如果问题还在用net cv2.dnn.readNetFromONNX(onnx_path)然后打印层名对照模型网络结构找出不支持的那一层手动替换。实在解决不了就换onnxruntime做推理但这就失去了 dnn 模块的轻量优势属于最后选项。4.3 跟踪 ID 频繁切换同一个目标被认成两个轨迹现象目标走到镜头稍微暗一点的位置检测框置信度降低跟踪 ID 就变了甚至一个目标出现两个 ID。 原因IOU 关联阈值设得太严比如 0.5目标一卡顿预测框和检测框重叠率降低匹配不上然后被当成新目标。 解决把关联阈值放宽到 0.2 ~ 0.3同时引入预测框和检测框中心点的欧氏距离作为补充匹配条件。我常用的办法是 IOU 和中心距离各占一半权重先按 IOU 匹配IOU 不达标但中心距离小于一个框宽时也允许匹配。对长时间遮挡恢复的场景这个改动比调 Q 有效得多。4.4 树莓派或 RK3568 上跑起来很卡帧率只有个位数现象用 dnn 推理还能跑但接上卡尔曼后帧率明显下降板卡发热严重。 原因卡尔曼滤波本身的矩阵运算在 CPU 上不贵8 维矩阵乘法很便宜真正卡的是 dnn 推理在大网络上的耗时yolov5s 在树莓派 4B 的 CPU 上单帧要上百毫秒。 解决优先做模型轻量化yolov5n 或者对 rk3568 平台走 rknn 量化通道把模型量化成 int8 再部署。另一个思路是降低输入尺寸到 320检测速度几乎翻倍但卡尔曼的 Q 矩阵要按帧率变化做补偿——尺寸变小后像素抖动幅度变大位置 Q 要适当调小。我试过在 RK3568 上跑量化后的 yolov5n dnn帧率能到 20 FPS卡尔曼开销只占不到 1 毫秒完全不是瓶颈。4.5 目标被完全遮挡后重新出现跟踪不回来现象目标被挡了 1 到 2 秒卡尔曼预测框还在按原速度往前走但目标和预测框对不上了重新出现时被当成了新目标。 原因遮挡期间没有观测更新卡尔曼完全靠预测往前推但目标可能已经减速、转弯甚至停下匀速模型的外推结果跟实际位置偏差越来越大。 解决给轨迹增加「连续未匹配帧计数」计数小于阈值时继续用预测框维持超过阈值后标记为丢失并删除轨迹。阈值的设置可以看你的遮挡场景商超场景我一般设 5 到 10 帧交通场景设 10 到 15 帧。另外可以把运动模型升级成恒加速度模型但代价是调参工作量上去了。5. 进阶技巧遮挡恢复验证与轨迹平滑输出跟踪调通只是第一步真正判断这套方案能不能上线要看遮挡恢复能力和轨迹的平滑度。我先说一个我常用的验证方法用一段固定机位的视频人为在中间遮掉目标几帧观察卡尔曼滤波的预测框在无观测期间能撑多远不丢。具体做法是模拟漏检代码很简单# 模拟在第 30 到 40 帧之间完全不输出检测框 for frame_idx in range(50): detections get_detections(frame_idx) if 30 frame_idx 40: detections [] # 模拟遮挡 trackers.update(detections)跑完之后回放轨迹重点看两个指标一是遮挡期间预测框有没有滑开二是目标重现时跟踪 ID 有没有变。前者暴露的是 Q 和 R 匹配问题后者暴露的是关联阈值问题。我用这个方法在多个场景里找出过不少问题——比如行人转身时检测框突然缩小导致 ID 切换后来通过放宽 IOU 阈值解决了。轨迹平滑输出是另一个很实用的技巧。卡尔曼修正后的位置已经比检测框平滑不少但如果目标在做高频抖动运动比如跑步输出轨迹还是会有点毛糙。我一般会再做一层轻量级加权平均输出轨迹点只取最近三帧的位置按[0.5, 0.3, 0.2]加权代价很小效果却相当于把高频噪声滤掉了一层。最后说一个贯穿始终的习惯所有参数不要一次性写死至少把 Q、R、关联阈值、丢失容忍帧数提取成配置项在实际视频上跑 20 分钟再根据表现微调。我现在拿到新场景的素材第一件事不是改代码而是先导出 ONNX 跑一遍裸检测确认检测框的稳定性再决定卡尔曼的 R 该偏向「跟检测」还是「跟模型」——这决定了后面所有调参的起点。这套联动方案走到今天最值得投入的永远不是某个公式的精度而是整个链路的可观测性。你按这个节奏走下来会发现目标的「预测」不再是玄学而是每一帧都有依据的推算。希望帮到你。本文还有配套的精品资源点击获取