
简介这是一套基于Python与TensorFlow实现的卡尔曼滤波目标追踪项目源码适合熟悉Python基础、希望深入理解状态估计与滤波算法的学习者也适用于无人机、视频监控等目标跟踪场景的算法验证与二次开发。资源共包含28个文件压缩包大小946KB主要文件类型有16个Python脚本、4个XML工程配置、1个TensorFlow事件日志及若干图片与说明文档其中kalman.py、kalmatensorflow.py、KF.py分别对应滤波器核心实现、TensorFlow版本实现与基础滤波类。项目完整覆盖初始化、预测、更新、协方差计算及循环追踪流程并附带目标检测与可视化模块便于初学者对照源码理解每一个数学步骤。已有657人学习下载适合作为入门卡尔曼滤波的实战参考借助代码运行与调试快速掌握状态向量、协方差矩阵更新及多传感器数据融合的应用思路。1. 卡尔纳曼滤波目标追踪的 Python 实现为什么值得自己写一遍把“卡尔纳曼滤波”输进搜索引擎的人多数想要的东西其实叫卡尔曼滤波Kalman Filter。“卡尔纳曼”是常见口误但不影响这个标题指向一个明确的诉求用 Python 写一套能跑起来的目标追踪程序。目标追踪的经典架构是“检测 跟踪”检测器比如 YOLO、OpenCV 的背景分割负责在每一帧里找出目标跟踪器负责把前后帧的目标关联成同一条轨迹。卡尔曼滤波在中间扮演的角色是“预测 平滑”用前一帧的状态预测当前帧目标在哪等检测结果回来后再修正。这个机制解决的核心痛点是检测器偶尔丢帧、结果抖动、目标短时被遮挡时轨迹断裂。Python 生态里 OpenCV 自带跟踪接口但那些接口封得太死看不清滤波在干什么出了问题也无从调试。自己实现一遍卡尔曼滤波适合正在做视觉项目的工程师和想深入理解状态估计原理的开发者整个过程两三百行代码改造成本不高收益却是对整套追踪系统的每个环节都有掌控力。2. 卡尔曼滤波的状态建模先把目标追踪问题翻译成数学语言2.1 目标追踪里的状态向量怎么设计位置与速度还是带上加速度卡尔曼滤波处理的是状态估计问题目标追踪里“状态”就是目标在图像坐标系中的运动参数。最常见的做法是把状态向量设计成四维# 状态向量: [x, y, vx, vy] # x, y : 目标框中心点在图像中的像素坐标 # vx, vy : 目标在相邻帧间的像素位移速度这里的速度单位是“像素/帧”而不是“像素/秒”因为卡尔曼滤波的预测步长是帧间隔视频里每两帧之间的时间差基本恒定直接用帧做时间单位能省掉时间戳对齐的麻烦代码也更直观。要不要把加速度也加进去取决于目标的运动模式。匀速运动模型CV够用的情况是视频帧率稳定、目标没有频繁加减速如果真的在做舵机云台追踪那种场景目标可能是直线移动的无人机或车辆CV 模型就足够。加速度模型CA会引入两个额外状态维度预测准确性在目标真正做匀加速运动时更好但代价是状态协方差矩阵更容易发散滤波器对噪声更敏感。2.2 状态转移矩阵与观测矩阵驱动追踪循环的两个关键矩阵状态转移矩阵 F 描述的是“没有测量输入时状态如何随时间演化”。对匀速模型F 的形态是固定的import numpy as np dt 1.0 # 帧间隔按帧为单位取 1 F np.array([ [1, 0, dt, 0], [0, 1, 0, dt], [0, 0, 1, 0], [0, 0, 0, 1], ])这个矩阵的含义是新一帧的 x 坐标等于上一帧的 x 加上速度 vx 乘以时间步长速度本身保持不变。它描述的是“目标继续按当前速度滑行”的物理假设。观测矩阵 H 解决的是“传感器的测量值如何映射到状态空间”。检测器输出的是目标框的 x, y 坐标没有直接输出速度所以观测矩阵要做的就是把四维状态压缩成二维H np.array([ [1, 0, 0, 0], [0, 1, 0, 0], ])预测阶段的核心代码如下def predict(self): self.x self.F self.x # 状态预测 self.P self.F self.P self.F.T self.Q # 协方差预测Q 是过程噪声 return self.x逻辑说明状态预测直接套矩阵乘法协方差预测中的F P F.T表示不确定性随运动模型扩散加上 Q 表示运动模型本身就不是精确的——目标可能突然加速或转弯这些模型误差要通过过程噪声吸收。更新阶段则把检测结果 z 融合进来def update(self, z): y z - self.H self.x # 新息衡量预测和测量的差异 S self.H self.P self.H.T self.R # 新息协方差 K self.P self.H.T np.linalg.inv(S) # 卡尔曼增益 self.x self.x K y # 状态修正 self.P (np.eye(4) - K self.H) self.P # 协方差更新 return self.x逻辑说明新息 y 是检测值和预测值的差S 是这个差的不确定性卡尔曼增益 K 决定“预测和测量谁更可信”。当测量噪声 R 很大时K 变小滤波器更信任预测当过程噪声 Q 很大时K 变大滤波器更愿意跟随测量。P 是状态协方差矩阵跟踪对角线元素就能看到滤波器对每个状态量的置信程度。2.3 过程噪声 Q 与测量噪声 R卡尔曼滤波唯二需要调的参数Q 和 R 是卡尔曼滤波的“性格开关”。R 由检测器的精度决定用 YOLO 类目标检测模型时检测框的中心点抖动幅度通常有几个像素R 的主对角线设 5 到 20 都合理用背景差分等传统方法时检测结果更粗糙R 要适当放大。# 一个能直接跑的初始参数组合 Q np.eye(4) * 0.1 # 过程噪声假设目标运动接近匀速 R np.eye(2) * 10.0 # 测量噪声检测框中心抖动约 -3 像素Q 的经验取值与目标运动急缓强相关。目标运动机动性强时 Q 要放大到 0.5 甚至 1.0让滤波器对预测不那么自信这样目标突然转弯时滤波结果能较快跟上目标运动很平滑时 Q 保持小值轨迹更稳定但响应变慢。R 过大表现为轨迹严重滞后于真实目标位置R 过小表现为滤波后的轨迹仍然起伏很大几乎起不到平滑作用。常见调参策略是先固定 R 为经验值再调整 Q观察预测框和检测框的重叠程度。提示以上公式用的是标准卡尔曼滤波形式适合线性高斯场景。图像坐标中的目标运动近似线性实际项目里四维状态 匀速模型已经覆盖大多数视觉追踪需求。如果目标运动有明显的曲线轨迹就该换扩展卡尔曼滤波EKF或无损卡尔曼滤波UKF那要改动的是状态转移函数而非整个框架。3. 用 Python 手写一个可用于目标追踪的卡尔曼滤波类3.1 完整代码结构一个类封住预测与更新两个接口import numpy as np class KalmanBoxTracker: 用于目标追踪的卡尔曼滤波器 状态: [x, y, vx, vy] 观测: [x, y] def __init__(self, bbox): # bbox: [x1, y1, x2, y2]检测框左上角和右下角 x (bbox[0] bbox[2]) / 2.0 # 中心点 x y (bbox[1] bbox[3]) / 2.0 # 中心点 y self.x np.array([x, y, 0, 0], dtypenp.float64) self.P np.eye(4) * 10.0 # 初始协方差速度不确定性设大 self.F np.array([ [1, 0, 1, 0], [0, 1, 0, 1], [0, 0, 1, 0], [0, 0, 0, 1], ], dtypenp.float64) self.H np.array([ [1, 0, 0, 0], [0, 1, 0, 0], ], dtypenp.float64) self.Q np.eye(4) * 0.1 self.R np.eye(2) * 10.0 self.id 0 self.hits 0 # 连续匹配成功次数 self.no_loss_count 0 # 连续丢失检测的次数 def predict(self): self.x self.F self.x self.P self.F self.P self.F.T self.Q return self.x def update(self, bbox): z np.array([(bbox[0] bbox[2]) / 2.0, (bbox[1] bbox[3]) / 2.0]) y z - self.H self.x S self.H self.P self.H.T self.R K self.P self.H.T np.linalg.inv(S) self.x self.x K y self.P (np.eye(4) - K self.H) self.P self.hits 1参数说明初始协方差 P 设为对角值 10表示对初始状态不自信特别是速度分量完全未知这一点很关键——初始化时把速度设为 0 是无奈之举必须给协方差足够大的值让滤波器在头几帧尽快收敛。hits和no_loss_count两个计数用于后文的多目标管理判断一个目标是否值得保留轨迹。3.2 最小可运行的追踪循环把滤波接到单目标视频上将上面的类用在单目标场景时代码的整体结构是读帧、预测、检测、更新四步import cv2 tracker None first_frame True cap cv2.VideoCapture(test_video.mp4) while True: ret, frame cap.read() if not ret: break # 用最简单的方式做检测帧差法 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) if first_frame: prev_gray gray first_frame False continue diff cv2.absdiff(prev_gray, gray) _, thresh cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY) contours, _ cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) detections [] for cnt in contours: if cv2.contourArea(cnt) 500: # 过滤噪声 x, y, w, h cv2.boundingRect(cnt) detections.append([x, y, x w, y h]) if len(detections) 0: bbox max(detections, keylambda b: (b[2]-b[0]) * (b[3]-b[1])) if tracker is None: tracker KalmanBoxTracker(bbox) else: tracker.predict() tracker.update(bbox) x1, y1, x2, y2 map(int, tracker.x[:2] - 0.0) # 这里只取位置分量 # 绘制时要取回完整的框可结合检测框宽高扩展为中心点绘制 cv2.circle(frame, (int(tracker.x[0]), int(tracker.x[1])), 5, (0, 255, 0), -1) elif tracker is not None: tracker.predict() # 未检测到目标时只预测不更新 prev_gray gray cv2.imshow(track, frame) if cv2.waitKey(30) 0xFF ord(q): break逻辑说明这个例子用帧差法代替深度模型目的只有一个——把卡尔曼滤波的循环单独暴露出来。if tracker is None负责初始化检测到目标后先 predict 再 update顺序不能颠倒目标丢失时只 predict 不 update让滤波器用运动模型维持输出。这样处理之后检测短暂中断几帧时输出坐标不会跳到 0而是沿着已有速度平滑外推。3.3 状态向量与检测框宽度和高度的取舍为什么只追踪中心点上面的滤波类只追踪中心点坐标检测框的宽高被丢弃了。大多数卡尔曼目标追踪实现会追踪完整四维状态 [cx, cy, w, h] 或 [cx, cy, s, r]s 是面积r 是宽高比这来自经典 Tracking-by-Detection 框架的设计。中心点 速度的方案足够覆盖中心点为一阶导数的简单场景但在目标快速靠近摄像头时宽高变化剧烈只靠中心点在遮挡恢复后会丢失尺度信息。实践中如果检测框是 YOLO 等模型输出的宽高本身比较稳定可以把状态扩展到六维# 六维状态: [cx, cy, w, h, vcx, vcy] # 状态转移矩阵对应扩展为 6x6观测矩阵取前四维单位阵这个方案的优点在于目标匹配时可以同时参考位置和尺度比只用中心点在处理重叠目标时更鲁棒。代价是协方差矩阵从 4x4 涨到 6x6运算量小幅度上升而目标追踪的实时性瓶颈通常在检测网络不在滤波这一步。4. 把卡尔曼滤波接入多目标追踪数据关联是绕不开的工程问题4.1 检测与跟踪的匹配机制从暴力匹配到匈牙利算法单目标场景不存在匹配问题检测框直接喂给滤波器就行。多目标场景的核心问题是当前帧检测出 N 个框已有 M 条轨迹怎么知道哪个检测框对应哪条轨迹不关联就没法更新或者错误关联会导致轨迹 ID 切换这是多目标追踪里最典型的错误形态。最简单的关联方法计算所有轨迹预测框和检测框之间的 IoU交并比大于阈值的配对生效。IoU 的计算朴素直接def iou(bbox1, bbox2): x1 max(bbox1[0], bbox2[0]) y1 max(bbox1[1], bbox2[1]) x2 min(bbox1[2], bbox2[2]) y2 min(bbox1[3], bbox2[3]) inter max(0, x2 - x1) * max(0, y2 - y1) area1 (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1]) area2 (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1]) union area1 area2 - inter return inter / union if union 0 else 0IoU 大于阈值就认为预测框和检测框对应同一个目标。但多对多场景直接暴力匹配会出问题一个检测框可能和两条轨迹的 IoU 都大于阈值这时候需要全局最优分配。常见解法是把代价矩阵丢给匈牙利算法scipy.optimize.linear_sum_assignment在“整体代价最小”的意义上做一对一分配。from scipy.optimize import linear_sum_assignment cost_matrix np.zeros((len(trackers), len(detections))) for i, trk in enumerate(trackers): pred_box trk.predict() # 得到预测中心点 for j, det in enumerate(detections): cost_matrix[i, j] -iou(pred_box_to_bbox(pred_box), det) # 取负号求最小化 row_ind, col_ind linear_sum_assignment(cost_matrix)参数说明linear_sum_assignment返回的是一组行列索引配对默认求最小代价因此代价矩阵取 IoU 的负值。这个方案的问题是对漏检和遮挡没有记忆——某一帧检测不到某个目标对应轨迹就分不到检测下一帧检测恢复时又需要重新匹配。解决思路是引入“未匹配”状态和存活计数连续未命中超过max_age帧的轨迹才删除给目标留出短暂被遮挡的缓冲期。4.2 马氏距离作为补充度量利用卡尔曼滤波的协方差信息IoU 只度量空间重叠没有使用卡尔曼滤波内部的状态不确定性。如果目标沿着预测方向快速移动检测框和预测框可能只有少量重叠但确实是同一个目标IoU 阈值设得稍高就会漏配。这时可以用马氏距离替代或联合 IoUdef mahalanobis_distance(z, x, S_inv): delta z - x return np.sqrt(delta.T S_inv delta)马氏距离把协方差矩阵 S 纳入度量S 描述预测的不确定性距离在“预测不太确定的方向”上会被放大容忍度。实际项目中常见做法是两级匹配先用马氏距离做硬条件过滤超过卡方分布阈值的配对直接排除再对剩余候选算 IoU 并执行匈牙利算法。这套流程和经典多目标追踪框架的处理逻辑一致只不过那个框架里用的是更复杂的级联匹配而本文这套更适合中小项目自研。4.3 管理轨迹的生命周期创建、更新、删除的策略多目标追踪工程里滤波只是状态估计的子模块真正决定追踪效果的是目标管理逻辑。策略通常有三条检测框首次出现时立即创建新轨迹但要连续命中几帧才对外输出避免单帧误检产生幽灵轨迹轨迹连续丢失超过 N 帧比如 30 帧直接删除避免长期空转占用计算资源被遮挡期间轨迹只 predict 不 update因为此时没有可信的测量输入。# 每帧结束时统一清理 for trk in trackers: if trk.no_loss_count max_age: trackers.remove(trk)参数说明max_age是经验值10 到 30 之间比较常用。设太小遮挡稍长轨迹就断了设太大会留下大量空预测轨迹后续匹配时产生错误关联。一个可用的初始化参数是min_hits 3max_age 20。这两个值应该暴露为配置项在实际视频上观察轨迹断裂率和 ID Switch 次数来微调。OpenCV 里自带的追踪器接口如cv2.TrackerKCF没有暴露这套生命周期管理机制这也是手写方案更具可调试性的原因之一。提示基于 stm32 与 opencv 的多模式舵机云台目标追踪这类硬件项目里卡尔曼滤波的输出不是直接给用户看的而是作为云台的期望角输入。决策模块需要的是位置与速度状态向量中的 vx, vy 此时就是很有价值的信号——它们能用来预测目标接下来往哪儿移动让云台提前转向而不是等目标出了画面才追过去。5. 卡尔曼滤波追踪的参数调优与验证一个调试技巧解决大部分轨迹跳动问题5.1 用模拟轨迹验证滤波器实现是否有误先别急着上真实视频追踪效果不好时第一步要确认滤波实现本身有没有问题。用受控的模拟数据可以快速验证生成一条匀速直线运动的轨迹加上高斯白噪声作为检测结果把带噪数据喂给卡尔曼滤波看输出是否接近真实轨迹。# 生成模拟轨迹真实位置 高斯噪声 np.random.seed(42) frames 100 true_x np.linspace(0, 640, frames) true_y np.linspace(100, 300, frames) det_x true_x np.random.normal(0, 5, frames) det_y true_y np.random.normal(0, 5, frames) # 滤波过程 x_est, y_est [], [] filter KalmanBoxTracker([det_x[0], det_y[0], det_x[0], det_y[0]]) for i in range(1, frames): filter.predict() filter.update([det_x[i], det_y[i], det_x[i], det_y[i]]) x_est.append(filter.x[0]) y_est.append(filter.x[1]) # 计算均方根误差 rmse np.sqrt(np.mean((np.array(x_est) - true_x[1:])**2)) print(fRMSE: {rmse:.2f} 像素)验证逻辑匀速直线运动条件下卡尔曼滤波的 RMSE 应该显著低于直接使用检测坐标的 RMSE。如果滤波后的误差反而更大基本可以确定 F、H、Q、R 中至少有一个矩阵写错了。检查顺序是先看 F 和 H 维度是否匹配再看 Q 和 R 是否出现数量级错误最后确认 predict 和 update 的调用顺序。5.2 Q 与 R 的具体调参手法看残差曲线比盯着画面更有效手动调参数时最常见的动作是盯着滤波轨迹看“平不平滑”这依赖直觉但效率很低。更系统的方法是记录每一帧的新息innovation序列——也就是 update 步骤中y z - H x的值。卡尔曼滤波的理论假设是新息为零均值白噪声如果新息曲线持续有偏说明模型有系统性偏差比如目标在做匀加速运动而滤波器用的是匀速模型如果新息噪声幅度远超 R 的标准差说明 R 设小了滤波器过度信任检测结果。# 在 update 中记录新息 def update_with_log(self, bbox, log_list): z np.array([(bbox[0] bbox[2]) / 2.0, (bbox[1] bbox[3]) / 2.0]) y z - self.H self.x log_list.append(np.linalg.norm(y)) # 新息的模长 # ... 后续更新逻辑不变新息模长的均值可以用来反馈调整 Q均值过大说明预测和检测偏离严重模型跟不上目标运动需要增大 Q 让滤波器更快适应测量均值虽然小但曲线高频震荡则说明测量噪声影响过大可以适当增大 R 换取更稳定的输出。这种数据驱动的方式比肉眼调参准确得多尤其在参数要复用到不同视频场景的时候。5.3 最后一个实用技巧用卡尔曼速度分量做检测失效时的外推追踪过程中检测器偶尔会连续几帧没有输出这时滤波器不能等死。把no_loss_count大于 0 时的预测结果直接作为追踪输出配合 vx、vy 速度分量做线性外推能明显减少目标短暂出画面或遮挡后的轨迹断裂# 检测丢失时用预测结果绘制位置 if len(detections) 0: pred tracker.predict() out_x, out_y pred[0], pred[1] # 限制外推帧数超过阈值后停止输出避免目标早已大变向 tracker.no_loss_count 1 if tracker.no_loss_count 3: # 超过 3 帧仍无检测把预测结果置信度标记为低 pass这个技巧的核心是一个简单的信任衰减逻辑丢失的帧数越少外推结果的可用性越高超过 3 帧后匀速假设越来越不可靠输出虽然还在但需要降级处理——比如在画面上降低绘制透明度、云台追踪时减小控制增益。当检测恢复时update 方法会自动把状态拉回真实位置这就是卡尔曼滤波相比纯预测平滑方案的优势所在。调参时优先调整no_loss_count的阈值配合 Q 的大小这两个参数协同工作基本能覆盖目标遮挡恢复的绝大多数场景。本文还有配套的精品资源点击获取