
简介这份PDF文档面向智能交通、计算机视觉方向的学习者与开发者系统讲解如何用YOLOv11实现车辆速度计算与轨迹跟踪。内容从智能交通管理概述、YOLO系列算法演进与网络结构讲起逐步深入到车辆运动模型、卡尔曼滤波与粒子滤波等跟踪算法、帧间位移速度计算原理并完整覆盖数据集准备与标注、模型训练与优化、部署量化、检测与跟踪实现步骤、实验结果分析以及城市路口、高速公路、停车场、交通执法等应用场景最后讨论技术挑战与未来方向。文档共44页含1个PDF文件压缩包约2.25MB支持目录章节跳转、阅读器左侧大纲显示与章节快速定位文字图表显示正常。已有79人学习。读者可借此掌握从目标检测到速度轨迹输出的完整技术链路获得可参考的算法选型、评估指标与工程落地思路适合作为课程设计、项目实践与科研入门的学习资料。1. 智能交通管理里的 YOLOv11车辆速度与轨迹跟踪到底怎么落地卡口摄像头拍到的画面里一辆车从画面左侧驶入、右侧驶出中间跨越了几十帧。你要回答两个问题它跑多快它走的什么路线。这件事听起来简单做起来全是细节——检测框抖动一下速度就跳十几公里ID 一切换轨迹就断成两截。YOLOv11 在这里承担的是最前端的角色把每一帧里的车框出来后面才有资格谈速度与轨迹。它相比前代在骨干网络和检测头上做了调整小目标召回和推理速度都更适合交通场景这种「车多、目标小、帧率高」的活儿。这篇笔记面向的是想用 YOLOv11 搭一套车辆测速与轨迹跟踪管线的工程师从环境配置、检测推理、多目标跟踪、像素到物理世界的标定一路讲到避坑和验证。新手能照着命令跑通熟手能直接看参数边界和翻车点。2. 用 YOLOv11 跑通车辆检测环境配置与推理最小闭环2.1 环境配置ultralytics 装完先验证再谈训练交通场景的第一步不是训练是先把官方权重跑起来确认整条推理链路是通的。ultralytics 这个包把模型定义、推理、导出、训练都收在一个接口里装它比手动拼 PyTorch 省事得多。我一般用 conda 建独立环境避免和系统里的 torch 版本打架。# 建一个干净的 Python 3.10 环境交通项目里 3.10 兼容性最稳 conda create -n traffic_yolo python3.10 -y conda activate traffic_yolo # 装 ultralytics它会自动拉匹配的 torch/torchvision pip install ultralytics # 验证打印版本并跑一次内置推理确认 CUDA 可用 yolo version python -c import torch; print(torch.cuda.is_available(), torch.__version__)逻辑说明conda create隔离依赖是血泪经验交通项目常要同时装 opencv、numpy、scipy混装极易出现 numpy 版本冲突导致推理直接崩。yolo version确认命令行工具可用torch.cuda.is_available()返回 True 才说明 GPU 能被调用否则后面推理会退回 CPU帧率掉到个位数测速根本没法做。参数说明Python 版本建议 3.93.113.12 上部分 torch 轮子还不全。如果机器没有 GPUpip install ultralytics装的是 CPU 版 torch能跑但慢测速场景基本不可用建议至少一张 8G 显存的卡。2.2 推理最小闭环把一帧视频里的车框出来环境通了先别急着上视频。拿一张交通图片跑单帧推理确认模型能正确识别 car、bus、truck 这几类再往视频流上套。from ultralytics import YOLO import cv2 # 加载官方预训练权重n 版最轻适合高帧率交通流 model YOLO(yolo11n.pt) # 只保留车辆相关类别2car, 5bus, 7truckCOCO 类别索引 VEHICLE_CLASSES [2, 5, 7] img cv2.imread(frame.jpg) results model.predict( sourceimg, conf0.35, # 交通场景目标密集阈值别设太高 iou0.5, # NMS 的 IoU 阈值车挨车时适当调低 classesVEHICLE_CLASSES, verboseFalse, ) for box in results[0].boxes: x1, y1, x2, y2 box.xyxy[0].tolist() cls_id int(box.cls[0]) conf float(box.conf[0]) print(fclass{cls_id} conf{conf:.2f} box({x1:.0f},{y1:.0f},{x2:.0f},{y2:.0f}))逻辑说明classes参数直接把非车辆类别过滤掉省得后面再写判断也减少误检对跟踪的干扰。conf0.35是交通场景的常用起点设到 0.5 以上会漏掉远处小车和部分遮挡车辆设到 0.2 以下又会引入大量误检跟踪器会被噪声带偏。参数说明iou控制非极大值抑制的合并力度车流密集、车辆相互遮挡时0.5 是平衡点如果发现相邻两辆车被合并成一个框往下调到 0.4。yolo11n.pt是 nano 版速度最快精度最低如果卡口画面车辆占比小、需要更强的小目标能力换yolo11s.pt或yolo11m.pt代价是帧率下降。2.3 视频流推理与结果保存别让 IO 拖垮帧率单帧通了接下来是视频。交通测速对帧率敏感因为速度是靠帧间位移算的丢帧会直接让速度算错。写视频时用cv2.VideoWriter要留意编码器和帧率设置。import cv2 from ultralytics import YOLO model YOLO(yolo11n.pt) cap cv2.VideoCapture(traffic.mp4) fps cap.get(cv2.CAP_PROP_FPS) # 原始帧率测速必须用真实值 w int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) h int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) # mp4v 编码兼容性好写出的文件体积可控 writer cv2.VideoWriter(out.mp4, cv2.VideoWriter_fourcc(*mp4v), fps, (w, h)) while True: ret, frame cap.read() if not ret: break results model.predict(frame, conf0.35, classes[2, 5, 7], verboseFalse) annotated results[0].plot() # 自带画框和标签 writer.write(annotated) cap.release() writer.release()逻辑说明cap.get(cv2.CAP_PROP_FPS)拿到的帧率是后续速度换算的分母必须用视频真实帧率不能想当然写 25 或 30。results[0].plot()是 ultralytics 内置的可视化直接返回带框的 numpy 数组省去手动画框。写视频用mp4v是因为它在多数环境下不需要额外编解码器。参数说明如果原始视频帧率是 25但你的推理只能跑到 10 帧每秒不要用VideoWriter强行按 25 写那样输出视频会「快进」。正确做法是记录每帧的真实时间戳速度计算用时间戳差值而不是帧序号差值。这一点后面测速章节会展开。3. 从检测框到轨迹多目标跟踪的选型与接入3.1 为什么检测之外必须加跟踪ID 才是轨迹的骨架YOLOv11 每帧独立输出检测框帧与帧之间没有任何关联。你拿到的是「第 1 帧有 5 个框第 2 帧有 5 个框」但不知道哪个框对应哪辆车。轨迹跟踪要解决的就是这个对应关系——给每个目标分配一个稳定 ID把同一 ID 的框按时间串起来才形成轨迹。没有 ID速度也无从谈起因为你不知道第 2 帧的框是不是第 1 帧那辆车的。常见做法是检测加跟踪的两段式管线YOLOv11 负责检测跟踪算法负责关联。跟踪算法里 ByteTrack 在交通场景用得最多它的思路是把高分框和低分框分两轮匹配低分框往往是遮挡或运动模糊导致的直接丢掉会断轨捡回来能显著减少 ID 切换。另一个选择是 BoT-SORT它额外用了相机运动补偿适合摄像头本身有轻微晃动的场景。我一般先用 ByteTrack 跑基线ID 切换率下不来再换 BoT-SORT。3.2 用 ultralytics 内置跟踪器接入 ByteTrackultralytics 已经把跟踪器集成进model.track()不用自己写关联逻辑这是最省事的接入方式。from ultralytics import YOLO model YOLO(yolo11n.pt) # persistTrue 让跟踪器在帧之间保持状态这是轨迹连续的关键 results model.track( sourcetraffic.mp4, trackerbytetrack.yaml, # 内置配置也可换成 botsort.yaml conf0.3, iou0.5, classes[2, 5, 7], persistTrue, verboseFalse, ) for r in results: if r.boxes.id is None: # 某些帧可能没有跟踪结果 continue ids r.boxes.id.int().tolist() boxes r.boxes.xyxy.tolist() for tid, box in zip(ids, boxes): print(fframe track_id{tid} box{box})逻辑说明persistTrue是整段代码里最容易漏的参数。不加它跟踪器每帧重新初始化ID 每帧都变轨迹根本连不起来。tracker参数指向内置的 yaml 配置ByteTrack 的匹配阈值、缓冲帧数都在那个文件里需要调的时候复制一份改。参数说明conf0.3比纯检测时更低因为跟踪器需要低分框来维持遮挡期间的身份。r.boxes.id是跟踪器分配的 ID可能为 None必须判空。ID 是整数但类型是 tensor用.int().tolist()转成 Python 列表方便后续处理。3.3 轨迹数据结构每个 ID 维护一条历史队列拿到 ID 和框之后要自己维护轨迹。核心是一个字典键是 track_id值是这个 ID 的历史位置队列。队列长度决定你能回溯多久也决定速度计算的平滑程度。from collections import defaultdict, deque # 每个 ID 保存最近 30 帧的中心点和时间戳 tracks defaultdict(lambda: deque(maxlen30)) def update_tracks(results, frame_idx, fps): if results.boxes.id is None: return ids results.boxes.id.int().tolist() boxes results.boxes.xyxy.tolist() for tid, (x1, y1, x2, y2) in zip(ids, boxes): cx, cy (x1 x2) / 2, (y1 y2) / 2 # 用框中心代表车辆位置 t frame_idx / fps # 时间戳单位秒 tracks[tid].append((cx, cy, t))逻辑说明用deque(maxlen30)自动丢弃过老的点避免内存无限增长。中心点比框角点稳定因为框的宽高会随遮挡变化中心点抖动更小。时间戳用frame_idx / fps计算前提是视频帧率恒定如果是变帧率视频要改用cv2.CAP_PROP_POS_MSEC读真实时间。参数说明maxlen30对应约 1 秒30fps 下的历史足够算瞬时速度又能平滑噪声。如果车辆在画面里停留时间短可以降到 15如果要算较长时段的平均速度加到 60 以上但要注意队列里的点跨越的时间太长速度意义会变模糊。4. 像素位移换算真实速度标定、透视与单位转换4.1 像素不是米为什么直接拿像素差算速度是错的检测框中心点在图像上移动 100 像素这辆车实际跑了多少米答案取决于它在画面里的位置。近处的车移动 100 像素可能只走了 2 米远处的车移动 100 像素可能走了 20 米。这就是透视效应——图像是三维世界在二维平面上的投影像素和物理距离之间不是线性关系。直接拿像素差除以时间得到的「速度」单位是像素每秒没有任何物理意义换个摄像头分辨率就全变了。要得到真实速度必须做标定建立图像像素坐标到路面物理坐标的映射。交通场景常用两种方法一是已知参照物比如车道线标准宽度、路面上的标志线间距二是透视变换用四个已知路面点求出单应性矩阵把图像区域映射成俯视图俯视图里像素和米就是线性关系了。4.2 用四点透视变换把路面拉成俯视图假设你能在画面里找到一块矩形路面区域知道它实际的长和宽就可以用cv2.getPerspectiveTransform求变换矩阵。import cv2 import numpy as np # 图像里选四个点顺序左上、右上、右下、左下对应路面矩形 src_pts np.float32([[320, 480], [960, 480], [1200, 700], [80, 700]]) # 这四个点对应的真实路面尺寸单位米 ROAD_W, ROAD_H 7.0, 20.0 dst_pts np.float32([[0, 0], [ROAD_W * 100, 0], [ROAD_W * 100, ROAD_H * 100], [0, ROAD_H * 100]]) # 单应性矩阵图像坐标 - 俯视图坐标单位厘米 H cv2.getPerspectiveTransform(src_pts, dst_pts) def to_birdseye(pt): p np.array([pt[0], pt[1], 1.0]) q H p return q[0] / q[2], q[1] / q[2] # 归一化得到俯视图坐标逻辑说明src_pts是你在图像里手动或自动选的路面四点dst_pts是它们对应的真实世界坐标。这里把俯视图单位设成厘米乘 100是为了让后续速度换算直接得到米。to_birdseye把任意图像点映射到俯视图映射后的两点欧氏距离就是真实距离。参数说明src_pts的选取精度直接决定测速精度选点误差几个像素远处车辆的速度误差可能到 10% 以上。选点要选在清晰、固定的路面标记上别选在车辆上。ROAD_W和ROAD_H必须实测不能估。如果路面有坡度单应性假设平面会失效误差随距离增大。4.3 速度计算滑动窗口平滑与单位换算有了俯视图坐标速度就是距离除以时间。但逐帧算出来的速度抖动很大因为检测框本身有噪声。用滑动窗口对最近几帧做线性拟合斜率就是速度。import numpy as np def estimate_speed(track, H): track: deque of (cx, cy, t)返回 km/h if len(track) 5: # 点太少不估速 return None pts [to_birdseye((cx, cy)) for cx, cy, _ in track] ts np.array([t for _, _, t in track]) xs np.array([p[0] for p in pts]) ys np.array([p[1] for p in pts]) # 对 x、y 分别做一次线性拟合斜率即速度分量cm/s vx np.polyfit(ts, xs, 1)[0] vy np.polyfit(ts, ys, 1)[0] speed_cm_s np.hypot(vx, vy) return speed_cm_s / 100 * 3.6 # cm/s - m/s - km/h逻辑说明用np.polyfit一次拟合比逐帧差分稳得多因为它用了窗口内所有点噪声被平均掉。分别拟合 x 和 y 再合成比直接拟合距离更合理因为车辆可能斜向行驶。len(track) 5时不估速点太少拟合不可靠。参数说明窗口长度就是deque的maxlen30 帧在 30fps 下是 1 秒适合瞬时速度。如果要测区间平均速度用车辆进入和离开画面两个时刻的位置差除以时间差更准。3.6是 m/s 到 km/h 的换算系数别写错。速度超过 200 km/h 基本可以判定是标定或跟踪出错加个上限过滤。5. 避坑与排查车辆测速跟踪里最容易翻车的五件事5.1 ID 频繁切换导致轨迹断裂现象同一辆车在画面里跑着跑着ID 从 3 变成 17轨迹被拆成两段速度算出来忽高忽低。原因车辆相互遮挡、检测框抖动、或者跟踪器的匹配阈值太严导致关联失败。交通场景里大车挡小车是常态遮挡几帧后跟踪器就丢了目标。解决把conf降到 0.250.3让低分框参与匹配在 ByteTrack 配置里增大track_buffer默认 30可加到 60给遮挡留更多恢复时间如果摄像头有晃动换 BoT-SORT 并开启相机运动补偿。实在不行在轨迹层加一个简单的插值把断裂前后的轨迹按位置和时间接上。5.2 速度数值离谱动辄几百公里现象算出来的速度经常出现 300、500 km/h 这种不可能的值。原因多半是标定错误或单位换算错误。src_pts选错、ROAD_W填错、或者俯视图单位没统一都会让距离放大几十倍。另一个常见原因是把像素坐标直接当俯视图坐标用了忘了过to_birdseye。解决先验证标定——在图像里选两个已知实际距离的点过一遍to_birdseye算距离和真实值对比误差超过 5% 就重新选点。再检查单位链路俯视图是厘米除以 100 得米乘 3.6 得 km/h每一步都打印中间值确认。5.3 远处车辆检测不到轨迹从画面中间才开始现象车辆在画面远端时没有框开到中段才被检测到轨迹缺了前半段。原因YOLOv11 对小目标的能力有限远处车辆只有几十个像素低于模型的可靠检测尺度。conf设太高也会把远处的小框滤掉。解决换更大的模型yolo11s或yolo11m或者对画面远端区域做裁剪后单独推理再映射回原图。conf适当降低但要注意误检。另一个思路是提高输入分辨率model.predict(imgsz1280)代价是推理变慢需要权衡帧率。5.4 帧率不稳导致速度系统性偏差现象速度整体偏大或偏小但轨迹看起来正常。原因速度计算用了帧序号除以标称帧率但实际推理帧率和视频帧率不一致或者视频本身是变帧率的。用帧序号当时间时间轴就是错的。解决永远用真实时间戳。视频文件用cv2.CAP_PROP_POS_MSEC读每帧的毫秒时间实时流用time.time()记录处理时刻。速度计算里所有时间都来自这个时间戳不要用frame_idx / fps。如果推理速度跟不上视频帧率要么降分辨率提帧率要么接受丢帧但用真实时间戳补偿。5.5 跟踪器内存持续增长跑久了卡死现象程序跑十几分钟后越来越慢最后卡死或 OOM。原因tracks字典只增不减车辆离开画面后对应的轨迹还在内存里。长时间运行的交通监控场景ID 会累积到几万个。解决给每个轨迹加一个「最后更新时间」定期清理超过 N 秒没更新的 ID。N 取车辆穿过画面最长时间的 1.5 倍比如 10 秒。清理逻辑放在每帧处理完之后用dict遍历删除或者用带 TTL 的缓存结构。# 每帧结束后清理超时轨迹 now frame_idx / fps expired [tid for tid, tr in tracks.items() if not tr or now - tr[-1][2] 10.0] for tid in expired: del tracks[tid]逻辑说明tr[-1][2]是该轨迹最后一个点的时间戳超过 10 秒没更新就判定为已离开。这个清理必须做否则内存只涨不降。参数 10.0 根据画面里车辆最长停留时间调整卡口短画面可以降到 5。6. 进阶技巧用轨迹做超速判定与验证测速精度6.1 从速度序列到超速事件加滞回避免反复触发单帧速度超过阈值就报警会在阈值附近反复触发。正确做法是加滞回速度超过上限比如 80触发降到下限比如 75以下才解除。这样一次超速只报一次。class SpeedMonitor: def __init__(self, high80.0, low75.0): self.high, self.low high, low self.state {} # track_id - bool 是否已报警 def update(self, tid, speed_kmh): if speed_kmh is None: return False if tid not in self.state: self.state[tid] False if not self.state[tid] and speed_kmh self.high: self.state[tid] True return True # 新触发一次超速 if self.state[tid] and speed_kmh self.low: self.state[tid] False return False逻辑说明state记录每个 ID 当前是否处于「已报警」状态只有从 False 变 True 的那一刻才返回 True避免同一辆车连续帧重复报警。滞回区间80 到 75给了速度波动缓冲。参数说明high和low的差值一般取限速的 5%10%。差值太小起不到防抖作用太大则超速车辆降速后迟迟不解除状态。state字典同样需要随轨迹清理否则也会累积。6.2 验证测速精度用已知速度的参照物做标定校验测速系统上线前必须验证。最直接的办法是找一段有已知速度参照的视频比如自己开车以恒定 60 km/h 通过或者用画面里已知间距的路面标记加时间戳反推。把系统输出和真值对比算平均绝对误差。验证方法真值来源适用场景预期误差定速车辆通过车辆仪表或 GPS有可控车辆5% 以内路面标记间距实测标记距离固定摄像头8% 以内双摄像头交叉验证另一套独立系统高要求场景10% 以内误差超过 10% 时优先查标定选点其次查时间戳。我踩过的坑是标定选点选在了有坡度的路面上近处准远处偏后来换成平坦路段重新标定误差从 15% 降到 6%。6.3 一个我常用的习惯先存原始数据再调参调参最忌讳反复跑视频。我一般会在第一次推理时把每帧的track_id、框坐标、时间戳全部落盘成 CSV之后调速度算法、调阈值、调平滑窗口都直接读 CSV不再跑模型。这样一次推理能支撑几十次算法迭代省下大量时间。import csv with open(tracks.csv, w, newline) as f: writer csv.writer(f) writer.writerow([frame, time, track_id, x1, y1, x2, y2]) # 在推理循环里逐帧写入 # writer.writerow([frame_idx, t, tid, x1, y1, x2, y2])逻辑说明CSV 是纯文本用 pandas 读进来做任何分析都方便也不依赖模型环境。落盘的数据要包含时间戳否则后面没法复现速度计算。这个习惯让我在标定和调参之间来回切换时不用每次都等模型推理。参数说明CSV 体积随帧数和目标数增长一小时 30fps 的视频大概几十万行几十 MB完全可接受。如果嫌大存成 parquet 更省空间但 CSV 胜在通用。落盘时记得 flush 或及时关闭文件程序崩溃时数据不至于全丢。这套管线我从检测一路搭到超速判定最深的体会是模型只是入口真正决定测速准不准的是标定和跟踪的稳定性。YOLOv11 换个版本、调个 conf对最终速度的影响远不如把src_pts选准、把时间戳用对来得大。希望帮到你。本文还有配套的精品资源点击获取