Supervision 车辆速度估算实战:基于检测 + ByteTrack 与透视变换的实时测速指南
本篇技术指南基于 supervision 仓库中的官方速度估算示例(examples/speed_estimation)编写。它演示了如何将目标检测、ByteTrack 多目标跟踪与单应性透视变换结合起来,从普通道路交通视频中估算车辆速度,并通过 supervision 的标注工具实时绘制"每车实时 km/h"标签与运动轨迹。读完本文,你将掌握该示例的核心标定原理、四种检测后端(RF-DETR / Roboflow Inference / Ultralytics / 遗留 YOLO-NAS)的完整运行方式,以及每个命令行参数与关键源码逻辑的含义,并能够把同样的"点位移 + 时间 + 换算"套路迁移到自己的测速或计数项目中。
示例的整体思路:速度不是"测"出来的,而是"算"出来的
单目摄像头没有深度信息,无法直接从像素位移换算出物理速度。因此该示例采用的是一条工程上成熟的路线:先在视频帧中检测并跟踪车辆,把每个跟踪 ID 的锚点(车辆底部中心)投影到真实世界的地平面坐标系,再用地平面坐标的位移除以时间得到速度。
整体流程可以拆分为以下环节:
- 逐帧推理:由目标检测模型输出
Detections(检测框 + 类别 + 置信度); - 区域过滤:只保留落在多边形区域
PolygonZone(即画面中靠近摄像机的路面车道区)内的检测; - 多目标跟踪:用
sv.ByteTrack为每个车辆分配稳定 ID; - 坐标投影:取每个检测框的 BOTTOM_CENTER 锚点,经单应矩阵投影到"虚拟俯视地面坐标系";
- 滑动缓冲:以
defaultdict(lambda: deque(maxlen=fps))为每个 tracker_id 保存最近约 1 秒的投影纵坐标历史; - 速度换算:用历史首尾点的距离差 / 经历时间 × 3.6 得到 km/h;
- 可视化:把测得的
#tracker_id speed km/h标签、检测框和轨迹叠加回原帧。
这一思路完全由 supervision 公开 API 拼装而成,不依赖任何私有逻辑;追踪 ID 由 ByteTrack 提供(见 ByteTrack 核心实现),其 update_with_detections 负责在帧间维护轨迹关联(见 core.py)。
选择检测后端:四种变体的取舍
示例目录在 examples/speed_estimation/ 下提供了四个结构几乎一致的入口脚本,方便在同一套下游代码上替换检测模型:
| 脚本 | 检测后端 | 状态 | 说明 |
|---|---|---|---|
rfdetr_example.py |
RF-DETR(RFDETRMedium) |
推荐 | predict 直接返回 Detections,无需转换步骤,并内置 VEHICLE_CLASS_IDS 过滤 + with_nms |
inference_example.py |
Roboflow Inference(get_roboflow_model) |
支持 | 需要 Roboflow API Key;默认模型 rfdetr-small |
ultralytics_example.py |
Ultralytics YOLO(默认 yolo11x.pt) |
支持 | 使用 Detections.from_ultralytics 转换结果 |
yolo_nas_example.py |
YOLO-NAS(super-gradients) | 遗留参考 | README 明确不推荐新项目使用,仅保留作为参照 |
推荐 RF-DETR 的核心原因在 README 中写得很清楚:其 predict 方法的返回结果已经是 Detections 对象,省去了从框架结果到 supervision 格式的转换适配层。而其他框架则需要显式转换,例如 Ultralytics 侧调用 sv.Detections.from_ultralytics(result)(见 ultralytics_example.py)、Inference 侧调用 sv.Detections.from_inference(results)(见 inference_example.py)。
从源码结构看,RF-DETR 变体还额外做了两件增强(见 rfdetr_example.py):
VEHICLE_CLASS_IDS = [3, 4, 6, 8]:将检测结果限定在 COCO 车辆类别(3 为 car、4 为 motorcycle、6 为 bus、8 为 truck),避免行人等非机动车目标进入测速流程;- 用
detections.with_nms(threshold=iou_threshold)做类内 NMS 后再进入区域过滤与跟踪。
这意味着:如果你想测速的目标不是车辆,或你只关心其中某一类车,改动 VEHICLE_CLASS_IDS 即可完成筛选,无需触碰任何 supervision 代码。
环境准备与依赖安装
安装步骤遵循 README 的顺序,三条命令各司其职:
# 1. 克隆仓库并进入示例目录(--depth 1 只拉最新快照)
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
cd supervision/examples/speed_estimation
# 2.(可选)创建并激活隔离的 Python 虚拟环境
uv venv
source .venv/bin/activate
# 3. 安装示例依赖
uv pip install -r requirements.txt
requirements.txt 同时包含了四套变体的依赖:
supervision:核心库,提供跟踪、标注、视频 IO 等能力;rfdetr/ultralytics/inference:分别对应 RF-DETR、Ultralytics 与 Roboflow Inference 三种检测后端;super-gradients==3.5.0:仅被遗留脚本yolo_nas_example.py使用(README 注释已标明该依赖服务于 legacy 参照),如果不需要跑 YOLO-NAS 变体,可以不安装;jsonargparse[signatures]:示例脚本统一用它基于函数签名自动生成 CLI 参数;requests/tqdm:用于下面的视频资源下载。
依赖就绪后,下载示例视频(用于测试的 vehicles.mp4):
python video_downloader.py
video_downloader.py 内部会创建 data/ 目录并调用 supervision 的资源下载能力:download_assets(VideoAssets.VEHICLES),把官方示例视频拉取到 data/vehicles.mp4。该能力的实现在 src/supervision/assets/downloader.py,视频后处理与编码均通过 supervision 的 Video API(如 src/supervision/utils/video.py)完成。
脚本参数详解:从 --device 到 --iou_threshold
示例脚本不使用手写 argparse,而是让 jsonargparse.auto_cli(main, ...) 根据 main() 的函数签名自动生成参数(见任意示例脚本的 __main__ 段)。因此每个参数都对应 main 的一个带默认值的入参,README 对各参数的定义如下:
| 参数 | 适用脚本 | 必填 | 默认值 | 含义 |
|---|---|---|---|---|
--source_video_path |
全部 | 是 | — | 待分析的输入视频路径,是全部推理与测速的数据来源 |
--target_video_path |
全部 | 否 | — | 标注结果视频的保存路径;若缺省则不落盘,改为实时窗口显示(RF-DETR 变体在 target_video_path=None 时直接进入 sv.ImageWindow 展示分支,见 rfdetr_example.py) |
--source_weights_path |
历史说明保留 | — | — | 原文档中定义为 YOLO 权重路径;当前版本各脚本已改为从依赖模型工厂自动加载权重,此参数主要面向自行指定权重的定制场景 |
--confidence_threshold |
全部 | 否 | 0.3 |
置信度过滤阈值,决定"模型需要多确信才认定目标",同时透传给 ByteTrack 的 track_activation_threshold |
--iou_threshold |
全部 | 否 | 0.7 |
NMS 的 IoU 阈值,用于抑制重叠框、区分不同目标 |
--device |
rfdetr / ultralytics | 否 | cpu |
计算设备,可选 cpu、mps、cuda |
--roboflow_api_key |
inference 变体 | 否(二选一) | 环境变量 | Roboflow API Key;不传时回退读取 ROBOFLOW_API_KEY 环境变量,两者皆无则抛错退出 |
--model_id |
inference 变体 | 否 | "rfdetr-small" |
指定 Roboflow 模型 ID |
值得注意的实现细节有两个,分别对应推理与跟踪两个阶段:
- 置信度阈值是"双重透传"的:它既传给模型推理(如 Ultralytics 的
model(frame, conf=...),见 ultralytics_example.py),又被用作 ByteTrack 的track_activation_threshold(见各示例中的sv.ByteTrack(frame_rate=video_info.fps, track_activation_threshold=confidence_threshold))。跟踪器只对高于该阈值的检测激活新轨迹,避免低置信度的碎片检测把轨迹 ID 打乱。 - RF-DETR 变体的
main返回签名略有不同:其target_video_path默认值就是None(实时预览模式),因此它天然支持"不保存、仅预览";而 inference/ultralytics/yolo_nas 变体的签名要求显式传入输出路径。运行前留意这一点即可。
关于推理设备,RF-DETR 变体通过 RFDETRMedium(device=device) 传入 cpu/mps/cuda;若机器没有 GPU,默认 cpu 即可完成全部流程,只是推理速率取决于硬件。
运行四种变体:一条命令跑通测速管线
在完成数据下载后,可直接按以下命令运行(下文默认已下载 data/vehicles.mp4,输出写入 data/vehicles-result.mp4)。
RF-DETR(推荐)
python rfdetr_example.py \
--source_video_path data/vehicles.mp4 \
--target_video_path data/vehicles-result.mp4 \
--confidence_threshold 0.3 \
--iou_threshold 0.5
Roboflow Inference
python inference_example.py \
--roboflow_api_key "ROBOFLOW_API_KEY" \
--source_video_path data/vehicles.mp4 \
--target_video_path data/vehicles-result.mp4 \
--confidence_threshold 0.3 \
--iou_threshold 0.5
不传 --roboflow_api_key 时,脚本会尝试读取 ROBOFLOW_API_KEY 环境变量;两者均缺失会直接抛出 ValueError(见 inference_example.py)。
Ultralytics(YOLOv8 / YOLO11)
python ultralytics_example.py \
--source_video_path data/vehicles.mp4 \
--target_video_path data/vehicles-result.mp4 \
--confidence_threshold 0.3 \
--iou_threshold 0.5
YOLO-NAS(遗留参照,不推荐新项目)
python yolo_nas_example.py \
--source_video_path data/vehicles.mp4 \
--target_video_path data/vehicles-result.mp4 \
--confidence_threshold 0.3 \
--iou_threshold 0.5
运行结束后,在输出视频中可以看到每个跟踪框上实时刷新的 #ID 数字 km/h 标签与两秒长度的轨迹拖尾(trace_length=int(video_info.fps * 2),约合 2 秒)。若 --target_video_path 缺省,程序会弹出一个标题为 frame 的窗口实时预览,按 q 即可结束。
标定原理:SOURCE 与 TARGET 为什么决定测速准确性
README 使用醒目提醒(IMPORTANT)强调:如果要把脚本用于你自己的视频,SOURCE 与 TARGET 必须针对每一个摄像头视角单独重新标定。这是整个测速管线中最关键也最容易被忽视的一步。
以 ultralytics_example.py 中的定义为模板:
SOURCE = np.array([[1252, 787], [2298, 803], [5039, 2159], [-550, 2159]])
TARGET_WIDTH = 25
TARGET_HEIGHT = 250
TARGET = np.array(
[
[0, 0],
[TARGET_WIDTH - 1, 0],
[TARGET_WIDTH - 1, TARGET_HEIGHT - 1],
[0, TARGET_HEIGHT - 1],
]
)
标定的物理解释:
SOURCE是图像坐标系中的四个点,通常取自路面上的一个四边形(如两条车道线在远处、近处与画面的四个交点),它应恰好覆盖车辆以接近匀速行驶通过的一段真实路面;TARGET是这四个点对应的地平面(俯视)坐标。这里把四边形"拉直"成宽 25、高 250 的竖直长条——这是一个有物理含义的选择:若该四边形对应真实世界的一条长约 25 米、横跨约 2.5 米(数值需按实际画面语义理解)的路段,则y轴恰好与行车方向对齐。
两点之间的真实长度可以在源码之外通过测量路段得到,据此推算出比例尺,投影后的 y 位移即近似真实世界中的纵向位移。测速只用了投影后的 y(见下文速度计算逻辑),因此行车方向与四边形长边一致时结果才可靠。
ViewTransformer:把像素点投影到地平面
速度计算的前提是"每个跟踪框在哪个地平面位置"。示例用一个不到二十行的 ViewTransformer 完成这件事(各脚本实现一致,参考 rfdetr_example.py):
class ViewTransformer:
def __init__(self, source: np.ndarray, target: np.ndarray) -> None:
source = source.astype(np.float32)
target = target.astype(np.float32)
self.m = cv2.getPerspectiveTransform(source, target)
def transform_points(self, points: np.ndarray) -> np.ndarray:
if points.size == 0:
return points
reshaped_points = points.reshape(-1, 1, 2).astype(np.float32)
transformed_points = cv2.perspectiveTransform(reshaped_points, self.m)
return transformed_points.reshape(-1, 2)
它的原理建立在 OpenCV 的单应矩阵上:
cv2.getPerspectiveTransform(source, target)由四组对应点解出 3×3 透视变换矩阵m——这是把斜视角路面"矫正"为俯视图的核心假设,即路面近似为平面;cv2.perspectiveTransform把任意像素坐标批量投影到目标地平面坐标系;- 空输入直接短路返回,避免空检测帧触发底层异常。
在推理循环中,投影的输入点取的是检测框底部中心锚点(车辆与地面接触点):
points = detections.get_anchors_coordinates(anchor=sv.Position.BOTTOM_CENTER)
points = view_transformer.transform_points(points=points).astype(int)
get_anchors_coordinates 是 Detections 的标准锚点方法(见 src/supervision/detection/core.py)。选 BOTTOM_CENTER 而非框中心是刻意的:同一辆车即便框高随远近变化,其底边仍贴地,投影误差远小于质心。
速度计算逻辑:位移、时间窗口与 km/h 换算
跟踪 ID 稳定后,每个 ID 的投影纵坐标被写入一个"最近 1 秒滑动窗口":
coordinates = defaultdict(lambda: deque(maxlen=int(video_info.fps)))
deque(maxlen=fps) 使得缓冲区自动只保留最近约 fps 个观测——也就是内存中始终只有每个目标最近 1 秒的运动历史,超期数据自动丢弃,不会无限膨胀。
三个变体(ultralytics/inference/yolo_nas)在主循环内使用同一套朴素算法,以 ultralytics_example.py 为例:
for tracker_id in detections.tracker_id:
if len(coordinates[tracker_id]) < video_info.fps / 2:
labels.append(f"#{tracker_id}")
else:
coordinate_start = coordinates[tracker_id][-1]
coordinate_end = coordinates[tracker_id][0]
distance = abs(coordinate_start - coordinate_end)
time = len(coordinates[tracker_id]) / video_info.fps
speed = distance / time * 3.6
labels.append(f"#{tracker_id} {int(speed)} km/h")
其中用到的常量换算关系是:
- 观测不足半秒(
< fps / 2个样本)时,只显示#tracker_id,不估算速度,避免起步/刚入镜的车辆测出瞬时噪声值; - 位移取缓冲区最后一个与第一个观测的投影纵坐标差(
deque[-1]是最新值),即最近约 1 秒的净位移; - 时间 = 样本数 ÷ fps;
- 米/秒到千米/小时需要乘以
3.6(因为 1 m/s = 3.6 km/h)。
RF-DETR 变体对这一逻辑做了更严谨的重构,把公式提取成独立函数(见 rfdetr_example.py):
def calculate_speed(distance: float, elapsed_frames: int, fps: float) -> float:
if elapsed_frames < 1:
raise ValueError("At least one elapsed frame is required to calculate speed.")
elapsed_time = elapsed_frames / fps
return distance / elapsed_time * 3.6
区别在于:RF-DETR 变体的历史缓冲保存的是 (frame_index, y) 二元组,计算时使用 history[-1][0] - history[0][0](真实帧号差)作为时间跨度,而非简单用样本数量折算。这意味着当检测存在丢帧时,RF-DETR 变体的时间估计仍能反映真实经过的帧间隔,而按样本数折算的朴素版本会把丢帧后的观测间隔当成正常间隔。仓库为此专门写了回归测试 tests/test_speed_estimation_example.py 验证三种情形:
calculate_speed(distance=14, elapsed_frames=14, fps=30) == 108.0:无丢帧时的标准换算;calculate_speed(distance=14, elapsed_frames=2, fps=30) == 756.0:同样位移、帧间隔更短,代表丢帧后速率显著上升;elapsed_frames == 0时抛出ValueError,杜绝除零。
用 supervision 组件把结果画回视频
测速标签算好后,叠加与输出完全交给 supervision 的标注器与视频组件,这一段在四种变体中几乎一致。以 ultralytics_example.py 为例,一个完整的可视化栈由四层组成:
- 自适应文字与线宽:
sv.calculate_optimal_line_thickness与sv.calculate_optimal_text_scale根据video_info.resolution_wh自动推导合适尺寸(实现见 src/supervision/draw/utils.py),避免 4K 视频上出现过细线、低清视频上出现大字的比例失衡; - 逐帧的推理 → 过滤 → 跟踪 → 标注循环:
detections = sv.Detections.from_ultralytics(result)
detections = detections[polygon_zone.trigger(detections)]
detections = byte_track.update_with_detections(detections=detections)
polygon_zone.trigger(detections) 返回布尔掩码,只保留落在路面多边形内的检测(实现见 src/supervision/detection/tools/polygon_zone.py);随后 byte_track.update_with_detections 完成跨帧关联与 ID 分配(见 src/supervision/tracker/byte_tracker/core.py)。
- 三个标注器叠加:
annotated_frame = trace_annotator.annotate(scene=annotated_frame, detections=detections)
annotated_frame = box_annotator.annotate(scene=annotated_frame, detections=detections)
annotated_frame = label_annotator.annotate(
scene=annotated_frame, detections=detections, labels=labels)
BoxAnnotator 画检测框,LabelAnnotator 以 BOTTOM_CENTER 位置把 #ID 速度 km/h 文本贴在框底,TraceAnnotator 用最近 fps × 2 帧的锚点画出轨迹拖尾,让人一眼看出车辆运动方向。
- 写盘与实时预览:
sv.VideoSink(target_video_path, video_info)按源视频的帧率与分辨率把标注帧编码为输出文件;当没有目标路径时(RF-DETR 变体)则改用sv.ImageWindow("frame")弹出实时窗口,按q键退出。
把示例改造成你自己的测速项目
想把这个示例用在自有摄像头视角上,只需做四处调整,且大部分改动都不涉及 supervision 内部:
- 重新标定
SOURCE:在你的视频首帧上框选一段真实路面(建议选取车辆以大致恒定速度通过、且长度可通过实测量得的一段路面),得到四个图像坐标,替换到每个脚本顶部的常量中; - 修正地平面比例:确认
TARGET的宽高比与这段路面的真实长宽成比例,必要时修改TARGET_WIDTH / TARGET_HEIGHT,使投影后的纵向位移更贴近真实米数; - 确认类别列表:若用 RF-DETR 变体,按需增删
VEHICLE_CLASS_IDS(COCO 中 3=car、4=motorcycle、6=bus、8=truck),例如只测私家车可改为[3]; - 按硬件选择
--device与模型:有 GPU 用cuda,Mac 可试mps;追求精度把置信度下限从 0.3 调高,画面内车辆过密时适当降低--iou_threshold以抑制重叠误检。
将测得的每车速度用于超速告警、路段平均车速统计或流量分析时,建议理解一个前提:该方案的速度精度上限取决于标定四边形与真实路段的贴合度、跟踪 ID 在遮挡下是否稳定,以及车辆是否近似直线通过标定区域——任何破坏"地平面假设"的强坡度路面或频繁变道都会放大误差。因此官方教程也强调 SOURCE/TARGET 必须按每个摄像头视角单独调整(见 README 中的 IMPORTANT 提示)。
许可证说明
该示例集成了多个独立组件,各自遵循不同许可证,动手集成前需要分别确认:
- RF-DETR(推荐变体所采用的检测模型):遵循宽松的 Apache-2.0 许可证;
- Ultralytics / YOLOv8(
ultralytics变体):遵循 AGPL-3.0 许可证,商业闭源使用前需评估其传染性条款; - supervision(本示例中承担跟踪、区域过滤、标注与视频处理的基础库):遵循 MIT 许可证,可自由用于商业项目;
- YOLO-NAS 与
super-gradients作为遗留参考依赖,仅存在于需要运行yolo_nas_example.py的场景。
README 同时建议把 examples/speed_estimation/README.md 视为使用入口,其中保留着官方配套的视频演示与逐步讲解指引;仓库根目录的 LICENSE.md 与 README.md 则提供了 supervision 库本身的完整授权与生态说明。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00