资讯详情 基于YOLO11的目标速度估算:从检测、跟踪到透视变换的完整实战
📅 2026/10/20 12:29:25
把YOLO11和速度估算放在一起听起来像是两个不太相关的技术硬凑CP——一个做目标检测一个做运动学计算。但实际项目里这种组合非常常见路口车流测速、园区行人异常奔跑预警、体育训练中的跑动速度分析、工厂传送带上的物料流速统计本质上都是同一套思路。核心链条并不复杂先用YOLO11把目标检测出来再用跟踪算法把相邻帧里的同一个目标关联起来接着通过透视变换把像素位移换算成真实世界距离最后用“距离 ÷ 时间差”算出速度。这篇文章适合谁看如果你正在做智能交通、安防监控、运动分析或者任何需要“算出目标移动快慢”的视觉项目而且想找一个能快速落地、又不用从零手写检测和跟踪的方案那这篇内容应该能帮你省掉不少弯路。我会把从模型选型、坐标系标定到代码实现、踩坑排查的完整过程写出来全部是基于我实际跑通的经验。1. 项目整体设计与技术思路拆解1.1 为什么非要用YOLO11而不是继续用YOLOv8先说结论如果你已经熟练使用YOLOv8那YOLO11并不是一次颠覆性升级但它带来的一些细节提升恰恰是速度估算这类连续帧处理任务比较看重的。YOLO11是Ultralytics推出的新一代检测模型它延续了anchor-free的设计思路但在骨干网络和颈部结构上做了调整。我实际测试下来最直观的感受是两点第一小目标的召回率有一定提升。速度估算场景下目标往往从画面边缘进入距离摄像头很远时可能只有几十个像素YOLOv8在这些小目标上的漏检率相对偏高YOLO11的C3k2模块对这种中等偏小尺寸的目标更友好。第二推理速度更快在同样的硬件条件下YOLO11s的推理帧率比YOLOv8s大约能快10%到15%。但这里有个容易忽略的点速度估算不是只看检测精度更重要的是检测的稳定性。如果某帧检测到了、下一帧丢了跟踪就会被切断速度计算结果直接跳变。YOLO11在检测稳定性上的表现比单纯看mAP指标的提升更明显这个在后面的跟踪环节会展开讲。1.2 技术链路的全貌从像素到速度的五个环节速度估算看起来是个单一功能实际拆开来看是一条完整的视觉处理流水线。我按处理顺序把它拆成五个环节每个环节都有独立的技术选型目标检测定位画面中目标的位置和类别输出边界框。这里选YOLO11具体用哪个尺寸的模型取决于你的算力。多目标跟踪给每一帧检测到的目标分配稳定的ID关联相邻帧中的同一目标。推荐ByteTrack它是Ultralytics生态内置的跟踪方案和YOLO11配合得很顺手。坐标映射把目标在图像中的像素坐标映射到地面上的真实世界坐标。这一步是整个项目的关键因为像素距离不能直接当真实距离用不换算的话算出来的速度没有任何物理意义。速度计算基于相邻关键帧之间的位移和时间差计算瞬时速度并通过平滑算法消除抖动。可视化与统计在画面上标注速度值输出统计结果或报警信号。这五个环节里最容易让小白翻车的是第三环坐标映射也就是透视变换。很多人做完检测和跟踪发现算出来的速度忽快忽慢甚至方向都是反的问题几乎都出在坐标系没对齐。后面的章节我会单独讲这块。在模型选型上我做过的几个项目里有两种典型倾向算力充足比如后端GPU服务器就用YOLO11m或YOLO11l精度更高小目标表现更好边缘设备比如Jetson盒子、工控机就用YOLO11n或YOLO11s保证帧率。速度估算对帧率很敏感如果帧率低于15FPS目标在两帧之间移动距离过大跟踪关联容易出现错误所以保底帧率至少要在15以上。2. 核心关键技术解析2.1 检测为什么“框得准”不等于“测速准”YOLO11输出的是目标边界框左上角坐标、宽高、类别和置信度。很多人以为检测精度越高测速一定越准。我一开始也是这么想的直到实际比对之后才发现对速度估算影响最大的其实不是框的定位精度而是“框的稳定性”。什么叫框的稳定性打个比方一个行人站在画面里没动理想情况下每一帧检测到的框应该是完全重合的。但实际上由于画面噪声、光照变化、模型本身的预测波动框的中心点会在真实位置附近小幅抖动可能左右偏移几个像素。如果是静止目标这个抖动叠加到速度计算上就会算出“每秒0.5米”的荒谬结果。所以YOLO11在实际使用中对置信度阈值的设定要特别小心。阈值设得太高比如0.7以上目标稍微遮挡一下就被丢弃跟踪断开速度中断阈值设得太低比如0.15大量误检框会把背景里的电线杆、阴影当成目标跟踪器被干扰。我实际经验是在大多数道路上0.35到0.45之间是个比较合适的区间。这个阈值最好做成配置项不同场景下去现场调。再说框的定位质量。YOLO11默认输出的是水平边界框对视频中侧面行驶的车辆、走动的行人来说已经完全够用。但要注意边界框并不严格貼合目标的实际轮廓框的中心和目标的“物理中心”之间有一个固定的偏移。这个偏移在速度计算中是可以接受的因为我们要的是位移差只要每帧取的参考点一致固定偏差会被差分运算减掉。2.2 跟踪速度计算数据的生命力所在检测只回答“这一帧里目标在哪”的问题但速度需要回答“同一个目标这一帧和上一帧之间动了多远”。这中间必须有一层负责把时间上离散的检测结果串成一条轨迹这就是多目标跟踪要做的事。Ultralytics在YOLO11生态里内置了ByteTrack和BOTSORT两种跟踪方案。我实测下来在普通场景下ByteTrack和BOTSORT的差距不是很大但ByteTrack的参数更少、更容易调出稳定结果对新手更友好。BOTSORT多了一个基于外观特征的ReID行人重识别模块理论上在遮挡严重、目标长时消失后重出现的场景下表现更好但代价是推理速度下降、内存占用增加。我的建议是没有特别复杂的遮挡场景先用ByteTrack跑通后续需要再升级。ByteTrack的核心思想用一句话概括“高置信度框用于轨迹关联低置信度框用于保留轨迹。”传统跟踪算法比如SORT只处理置信度高于阈值的检测框目标一旦被遮挡导致置信度下降轨迹就丢了。ByteTrack把低置信度框也作为候选只要它们和已有轨迹的位置匹配率足够高就继续维持轨迹。这个设计对车辆测速特别友好因为车辆经过树荫、路灯杆后面时经常会有两三帧被遮住置信度骤降但位置大概率还在可以预测的范围内。使用跟踪器时有几个参数需要现场调。track_high_thresh默认0.5决定高置信度框的门槛track_low_thresh默认0.1是低置信度框的门槛。我建议把high_thresh降到0.35到0.45和前面说的检测置信度阈值保持一致让更多有效目标进入跟踪体系。track_buffer这个参数很微妙它控制轨迹在目标丢失后保留多少帧。设大了目标短暂遮挡后还能续上但可能出现两条轨迹错误合并设小了遮挡一下就断速度值跳变。我一般先设默认值30帧然后看效果如果轨迹频繁断就往大调。2.3 透视变换把二维像素变成三维世界坐标的关键魔法这是整个项目里最核心也最容易出错的一环。直接说为什么需要它摄像头画面是透视投影的结果同样一米长的距离在画面近处可能占200个像素在画面远处可能只占50个像素。直接用像素距离除以时间算速度会导致同一辆车在画面近处“跑得慢”、远处“跑得快”纯粹是几何畸变在骗人。解决办法是用透视变换矩阵把图像平面上的任意一个点映射到地面上对应点的真实世界坐标。这里依赖的关键假设是目标在地面上运动或者运动平面基本平行于地面。对车辆测速、行人测速来说这个假设在大多数监控场景下是成立的因为人眼看到的路面就是目标贴着地面在走。数学上这是一个单应性变换Homography可以用一个3乘3的矩阵H来表示。给定图像平面坐标(u, v)和世界平面坐标(x, y)满足关系式[x, y, 1]^T H * [u, v, 1]^T也就是说两组平面点之间的映射可以用一个矩阵统一描述。要解出这个矩阵至少需要4组对应的点对图像坐标 ↔ 世界坐标。实际操作中我们会取4个以上的点对用最小二乘法求最优解。选点的原则需要特别注意。我见过很多人标定时随便在路上点4个点矩阵也求出来了但测出来的速度偏差很大。原因是选的点几乎在一条直线上或者分布范围太小导致矩阵病态。好的做法是在画面上选取路面一块矩形区域的四个角点作为标定点让这四个点在图像平面上围成一个尽可能大的四边形并且大致覆盖目标最常出现的区域。关于标定坐标系的取法我自己有一个比较省事的做法以画面中一个明显的道路标线交点作为原点以路沿方向作为X轴以垂直方向作为Y轴用尺子实际量出四个角点在真实空间中的相对距离。不需要绝对经纬度只需要相对坐标因为速度算的是位移差。比如某段路面上横向每隔1米画一条线纵向每隔2米画一条线四个交点的真实坐标直接标成(0,0)、(2,0)、(2,1)、(0,1)这类数值就行。2.4 速度计算的数学推导与平滑处理拿到目标在连续帧中的真实世界坐标之后速度计算本身变得很简单。但简单归简单里面藏着的坑一点都不少。瞬时速度的计算公式是v sqrt((x_t - x_{t-1})^2 (y_t - y_{t-1})^2) / (t_t - t_{t-1})其中(x_t, y_t)是当前帧目标的世界坐标(x_{t-1}, y_{t-1})是上一帧的世界坐标t是帧的时间戳。这里有个细节相邻两帧只有几十毫秒位移量小四舍五入产生的相对误差会很大。所以我建议不要每一帧都算一次速度而是把时间窗口拉大一点比如每隔5帧或10帧计算一次平均速度。窗口拉大之后偶然发生的检测跳跃和坐标抖动会被平均掉数值稳定很多。平滑处理的方法我推荐两种EMA指数移动平均设定一个平滑因子alpha新速度 alpha * 当前帧速度 (1 - alpha) * 历史平均速度。alpha在0.3到0.6之间越大越灵敏越小越平滑。卡尔曼滤波如果你对状态估计比较熟可以对轨迹坐标做卡尔曼滤波再基于滤波后的坐标计算速度。这个方法理论更优雅但调参复杂度高普通项目里EMA足够了。坐标抖动的另一个来源是跟踪框的中心点选取。对车辆和行人我一般取框底部中心点作为参考点。原因很简单目标框的下边缘在图像中更接近目标与地面的接触位置而目标站立位置就是和地面接触的点。如果你取框中心目标身高差异、姿态变化都会引起框中心上下移动这部分误差会被引入速度计算导致明明走得不快算出来的速度却很高。3. 完整实操过程与核心代码实现3.1 环境准备与依赖安装我用的环境是Python 3.9PyTorch 2.xUltralytics最新版8.3.x以上YOLO11在8.3.0之后正式提供OpenCV。如果你的GPU内存有限CPU也能跑只是帧率会明显下降。安装依赖很简单pip install ultralytics opencv-python numpy确认YOLO11可用from ultralytics import YOLO model YOLO(yolo11n.pt) # 首次运行会自动下载预训练权重 results model(bus.jpg) # 随便拿一张图测试这里有个小提示YOLO11的预训练权重后缀和YOLOv8很像但模型结构文件不能混用。如果你从网上下载了别人改过的yaml文件匹配版本的时候要特别小心不然会报结构不匹配的错。3.2 标定数据采集与透视矩阵计算标定是速度估算项目里最“扣细节”的环节。我强烈建议在正式写测速逻辑之前先把标定这一步做扎实。操作步骤固定摄像头录制一段包含路面标志物的视频。从视频中截取一帧清晰的画面找出至少4个已知真实距离关系的点。在画面中用鼠标记录这4个点的像素坐标用卷尺或已知参照物记录它们对应的真实坐标。用cv2.getPerspectiveTransform或cv2.findHomography计算变换矩阵。我写了一个简易标定脚本配合鼠标点击取点import cv2 import numpy as np # 按顺序点击画面中的4个点记录像素坐标 # 同时在代码里预先输入对应的真实坐标 image_points [] # 鼠标点击依次填入 world_points [] # 预先测量好的真实坐标 def click_event(event, x, y, flags, param): if event cv2.EVENT_LBUTTONDOWN: image_points.append([x, y]) print(f图像坐标: ({x}, {y})) cv2.imshow(calibration, frame) cv2.setMouseCallback(calibration, click_event) cv2.waitKey(0) # 假设真实坐标为 # world_points [[0, 0], [width, 0], [width, height], [0, height]] # 单位米 image_points np.array(image_points, dtypenp.float32) world_points np.array(world_points, dtypenp.float32) matrix cv2.getPerspectiveTransform(image_points, world_points) np.save(homography_matrix.npy, matrix)关于选点的经验我再补充一个细节如果你在画面上找到的路面参照物不够也可以自己动手在路面上贴纸或者放交通锥然后量出它们之间的实际距离。这样做虽然麻烦一点但标定效果比随机选点强十倍。3.3 主程序检测、跟踪、测速一体实现下面这段代码是把整个流程串起来的核心。我做了一个精简版本去掉了一些统计输出的逻辑便于你看清主干。import cv2 import numpy as np import time from collections import defaultdict from ultralytics import YOLO # 参数配置区 MODEL_PATH yolo11n.pt VIDEO_PATH traffic.mp4 CALIB_MATRIX_PATH homography_matrix.npy TRACK_HIGH_THRESH 0.4 TRACK_LOW_THRESH 0.1 CONF_THRESH 0.35 SPEED_WINDOW 5 # 每隔5帧计算一次速度 EMA_ALPHA 0.5 # 平滑系数 PIXEL_TO_WORLD np.load(CALIB_MATRIX_PATH) # model YOLO(MODEL_PATH) # 初始化字节跟踪器Ultralytics内置 # 实际使用可以直接调用 model.track 接口 cap cv2.VideoCapture(VIDEO_PATH) # 保存每个ID的历史信息 track_history defaultdict(lambda: []) # 存储上一帧时间戳用于计算时间差 prev_frame_time None def pixel_to_world_point(center_x, center_y): 把像素坐标转为地面坐标 point np.array([[[center_x, center_y]]], dtypenp.float32) world cv2.perspectiveTransform(point, PIXEL_TO_WORLD) return world[0][0] while cap.isOpened(): ret, frame cap.read() if not ret: break current_time time.time() if prev_frame_time is None: prev_frame_time current_time continue dt current_time - prev_frame_time # 检测 跟踪直接使用YOLO11的track接口 results model.track( frame, persistTrue, confCONF_THRESH, trackerbytetrack.yaml, verboseFalse ) if results[0].boxes is None or results[0].boxes.id is None: prev_frame_time current_time continue # 获取检测框、跟踪ID和类别 boxes results[0].boxes.xyxy.cpu().numpy() track_ids results[0].boxes.id.cpu().numpy().astype(int) cls_names results[0].boxes.cls.cpu().numpy().astype(int) for box, track_id, cls in zip(boxes, track_ids, cls_names): x1, y1, x2, y2 box # 取框底部中心作为参考点 center_x (x1 x2) / 2 center_y y2 # 像素坐标 - 世界坐标 world_x, world_y pixel_to_world_point(center_x, center_y) hist track_history[track_id] hist.append((world_x, world_y, current_time)) # 至少保留SPEED_WINDOW 1个点才计算速度 if len(hist) SPEED_WINDOW 1: prev_x, prev_y, prev_t hist[-SPEED_WINDOW - 1] dx world_x - prev_x dy world_y - prev_y dt_win current_time - prev_t if dt_win 0: speed np.sqrt(dx * dx dy * dy) / dt_win # 米/秒 speed_kmh speed * 3.6 # EMA平滑 if ema_speed not in globals(): ema_speed speed_kmh else: ema_speed EMA_ALPHA * speed_kmh (1 - EMA_ALPHA) * ema_speed # 在画面上绘制 cv2.putText( frame, fID:{track_id} {ema_speed:.1f}km/h, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2 ) # 只保留最近N个坐标避免内存无限增长 hist hist[-20:] cv2.imshow(YOLO11 Speed Estimation, frame) if cv2.waitKey(1) 0xFF ord(q): break prev_frame_time current_time cap.release() cv2.destroyAllWindows()这段代码是有实际运行背景的我拿一段道路上行驶车辆的测试视频跑过。需要注意EMA平滑变量的作用域在循环外如果不care全局限定用字典按ID存ema_speed更规范。上面的简版在只有一个目标的时候可以工作如果同时出现多个目标需要把每个ID的ema_speed存到字典里我实际项目中就是这么做的。3.4 参数调优实验记录我贴一份自己实测的参数配置记录方便你有个起点。测试视频是普通城市道路场景车辆在画面中大约占30到200像素高相机高度约5米俯视角度约25度。参数项初始值调优后说明检测置信度 conf0.250.40初始值误检多路灯和白色标线频繁被当成目标跟踪 high_thresh0.500.40与检测阈值对齐轨迹保活率明显提高跟踪 low_thresh0.100.10保持默认速度窗口 SPEED_WINDOW353帧时抖动明显5帧后曲线稳定EMA alpha0.70.50.7太灵敏速度值跳动0.5更平滑这里要重点讲一下为什么speed_window不能无限大。窗口越大速度曲线越平滑但实时性越差响应延迟越高。对车辆超速报警来说延迟两三秒是可以接受的但对行人奔跑预警来说延迟两秒可能就失去意义了。所以speed_window的选取本质上是在平滑度和实时性之间找平衡点。我建议起步用5帧然后根据你的业务容忍度往两头调。4. 常见问题与排查技巧实录4.1 跟踪ID大幅跳变轨迹无法持续现象同一个车明明一直出现在画面里但ID从1跳到30再跳到2速度计算断断续续。排查思路先确认检测置信度是否稳定。如果车辆在画面远处置信度低近处置信度高说明模型本身在观望。把conf阈值降下来同时把track_high_thresh也降下来让更多低置信度框进入关联。另外检查track_buffer——如果你设置的track_buffer太小目标被遮挡几帧后轨迹就被删了重新出现时会分配一个新ID。我遇到的一个典型情况是摄像头画面里有一段区域有电线杆遮挡车辆经过时被遮挡两帧track_buffer默认值30帧足够扛过去但conf阈值太高导致目标在被遮挡前就已经掉了轨迹长度连不起来。把conf从0.6降到0.4之后问题直接消失。4.2 标定后速度值严重偏大或偏小现象所有目标算出来的速度都在200km/h以上或者都是5km/h以下。排查思路先检查世界坐标单位。如果你是手工量的真实距离单位要统一成米如果你用的是某张图纸上的像素坐标而没有做等比换算真实坐标会比实际小得多算出来的速度就会偏大。再检查标定点分布——如果4个点挨得很近透视矩阵在远离标定点的区域外推时会严重失真目标一旦出现在标定区域外速度就会暴涨或暴跌。我实际项目中踩过这样一个坑标定的时候选了路面上一块交通标志牌区域的4个点结果目标在画面左侧行驶时速度正常快到右侧时就变成200km/h。原因是右侧区域完全在标定区域外矩阵外推产生了夸张的畸变。解决办法是扩大标定范围让4个点尽可能覆盖目标可能出现的全部画面区域。4.3 速度结果在0附近来回抖静止目标被算出速度现象路边有一辆停着的车屏幕上它的速度在5km/h和-3km/h之间跳动。排查思路这是坐标抖动叠加的典型症状。即使目标静止由于检测框中心点波动和透视变换后坐标的微小变化连续几帧计算出的世界坐标也会有小幅波动。当窗口里刚好有一次检测框偏移较大时差分值除以时间差就得到个假速度。解决方法有两个方向第一加大speed_window到10帧以上让静止目标的随机抖动在差分中被抵消第二引入速度死区——当某帧计算出的速度低于某个阈值比如2km/h时直接归零。这个阈值要结合实际业务场景设定对车辆测速我一般用3km/h对行人测速用1km/h。4.4 速度估算精度如何验证做速度估算项目最后肯定绕不过一个问题我算得准不准我的做法是拿一台真实车辆做对照实验。具体操作找一辆车速稳定的车让副驾驶用GPS或手机测速App记录真实速度同时视频测速系统记录计算速度。让车以20km/h、40km/h、60km/h分别匀速通过画面每个速度跑三次。拿真实速度和计算速度做对比算平均误差。就我自己的实测结果来说在标定质量较好的前提下YOLO11s做检测配合ByteTrack和5帧窗口速度误差在10%以内是可以做到的。如果误差超过15%优先排查标定区域是否覆盖不全或者跟踪框中心参考点是否选得不对——比如你把框中心改成框顶部中心目标姿态一变速度就会跟着波动。4.5 常见问题速查表问题现象可能原因推荐处理方式测速值梯度感强烈忽快忽慢EMA系数过大或速度窗口过短调大speed_window或降低alpha到0.3画面左侧正常、右侧异常标定点区域未覆盖右侧重新标定扩大四点范围所有目标速度都偏大真实坐标系单位被当成像素矩阵外推失真检查单位换算重新标定目标走走停停但ID不稳conf阈值过高或track_buffer过短降低conf到0.35左右调大track_buffer行人识别正常车辆漏检严重模型类别支持问题、遮挡严重检查类别名称尝试更大的YOLO11模型测速结果在目标靠近相机时飙升透视矩阵在近区畸变严重检查标定点是否包含近区必要时增加点对做非线性校正4.6 几个容易被忽略的细节最后一个部分我把自己踩过的坑整理成清单按“会不会踩”的程度排序时间戳要用系统时间不要用帧序号代替。视频抽帧可能不均匀帧序号间隔不等于固定时间。我用视频文件测试时曾因为用帧序号算时间差导致速度值出现周期性波动后来改成time.time()才稳定。透视变换矩阵保存为npy后换摄像头位置或转角后必须重新标定。这听起来像是废话但我真的见过有人摄像头复位后忘了重新标定拿着旧矩阵跑了半天所有速度值都异常。多个目标同时出现时每个ID的EMA速度要独立保存。不要用一个全局变量存所有目标的速度否则不同车辆之间会产生“速度串扰”。要做实际部署的话YOLO11的跟踪接口里最好加上classes参数只检测你关心的类别比如车辆测速就只保留car、bus、truck行人测速只保留person。这样可以降低误检率也能提升推理速度。如果摄像头本身带运动补偿或电子防抖务必关闭。我自己在测试一台带EIS功能的运动相机时画面中静止标志物的坐标会缓慢漂移导致所有速度结果都附加上一个虚假的速度分量。我个人实际操作中的体会是速度估算项目的成败百分之六十在标定百分之三十在参数调节只有百分之十在模型本身。YOLO11把检测这件事做得足够好后续的技术难点其实已经转移到了几何映射和处理策略上。这篇文章里写的标定细节、跟踪参数和排查方法都是我在好几个项目里反复试错积累下来的你照着搭一套基础版本不难想把它调到能够稳定上线的精度还是得拿着自己的真实视频反复打磨。