首页
/ Supervision 车辆速度估算实战:基于检测 + ByteTrack 与透视变换的实时测速指南

Supervision 车辆速度估算实战:基于检测 + ByteTrack 与透视变换的实时测速指南

2026-09-07 17:32:31作者:宣海椒Queenly

本篇技术指南基于 supervision 仓库中的官方速度估算示例(examples/speed_estimation)编写。它演示了如何将目标检测、ByteTrack 多目标跟踪与单应性透视变换结合起来,从普通道路交通视频中估算车辆速度,并通过 supervision 的标注工具实时绘制"每车实时 km/h"标签与运动轨迹。读完本文,你将掌握该示例的核心标定原理、四种检测后端(RF-DETR / Roboflow Inference / Ultralytics / 遗留 YOLO-NAS)的完整运行方式,以及每个命令行参数与关键源码逻辑的含义,并能够把同样的"点位移 + 时间 + 换算"套路迁移到自己的测速或计数项目中。

示例的整体思路:速度不是"测"出来的,而是"算"出来的

单目摄像头没有深度信息,无法直接从像素位移换算出物理速度。因此该示例采用的是一条工程上成熟的路线:先在视频帧中检测并跟踪车辆,把每个跟踪 ID 的锚点(车辆底部中心)投影到真实世界的地平面坐标系,再用地平面坐标的位移除以时间得到速度

整体流程可以拆分为以下环节:

  1. 逐帧推理:由目标检测模型输出 Detections(检测框 + 类别 + 置信度);
  2. 区域过滤:只保留落在多边形区域 PolygonZone(即画面中靠近摄像机的路面车道区)内的检测;
  3. 多目标跟踪:用 sv.ByteTrack 为每个车辆分配稳定 ID;
  4. 坐标投影:取每个检测框的 BOTTOM_CENTER 锚点,经单应矩阵投影到"虚拟俯视地面坐标系";
  5. 滑动缓冲:以 defaultdict(lambda: deque(maxlen=fps)) 为每个 tracker_id 保存最近约 1 秒的投影纵坐标历史;
  6. 速度换算:用历史首尾点的距离差 / 经历时间 × 3.6 得到 km/h;
  7. 可视化:把测得的 #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 计算设备,可选 cpumpscuda
--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)强调:如果要把脚本用于你自己的视频,SOURCETARGET 必须针对每一个摄像头视角单独重新标定。这是整个测速管线中最关键也最容易被忽视的一步。

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 的单应矩阵上:

  1. cv2.getPerspectiveTransform(source, target) 由四组对应点解出 3×3 透视变换矩阵 m——这是把斜视角路面"矫正"为俯视图的核心假设,即路面近似为平面;
  2. cv2.perspectiveTransform 把任意像素坐标批量投影到目标地平面坐标系;
  3. 空输入直接短路返回,避免空检测帧触发底层异常。

在推理循环中,投影的输入点取的是检测框底部中心锚点(车辆与地面接触点):

points = detections.get_anchors_coordinates(anchor=sv.Position.BOTTOM_CENTER)
points = view_transformer.transform_points(points=points).astype(int)

get_anchors_coordinatesDetections 的标准锚点方法(见 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 为例,一个完整的可视化栈由四层组成:

  1. 自适应文字与线宽sv.calculate_optimal_line_thicknesssv.calculate_optimal_text_scale 根据 video_info.resolution_wh 自动推导合适尺寸(实现见 src/supervision/draw/utils.py),避免 4K 视频上出现过细线、低清视频上出现大字的比例失衡;
  2. 逐帧的推理 → 过滤 → 跟踪 → 标注循环
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)。

  1. 三个标注器叠加
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 画检测框,LabelAnnotatorBOTTOM_CENTER 位置把 #ID 速度 km/h 文本贴在框底,TraceAnnotator 用最近 fps × 2 帧的锚点画出轨迹拖尾,让人一眼看出车辆运动方向。

  1. 写盘与实时预览sv.VideoSink(target_video_path, video_info) 按源视频的帧率与分辨率把标注帧编码为输出文件;当没有目标路径时(RF-DETR 变体)则改用 sv.ImageWindow("frame") 弹出实时窗口,按 q 键退出。

把示例改造成你自己的测速项目

想把这个示例用在自有摄像头视角上,只需做四处调整,且大部分改动都不涉及 supervision 内部:

  1. 重新标定 SOURCE:在你的视频首帧上框选一段真实路面(建议选取车辆以大致恒定速度通过、且长度可通过实测量得的一段路面),得到四个图像坐标,替换到每个脚本顶部的常量中;
  2. 修正地平面比例:确认 TARGET 的宽高比与这段路面的真实长宽成比例,必要时修改 TARGET_WIDTH / TARGET_HEIGHT,使投影后的纵向位移更贴近真实米数;
  3. 确认类别列表:若用 RF-DETR 变体,按需增删 VEHICLE_CLASS_IDS(COCO 中 3=car、4=motorcycle、6=bus、8=truck),例如只测私家车可改为 [3]
  4. 按硬件选择 --device 与模型:有 GPU 用 cuda,Mac 可试 mps;追求精度把置信度下限从 0.3 调高,画面内车辆过密时适当降低 --iou_threshold 以抑制重叠误检。

将测得的每车速度用于超速告警、路段平均车速统计或流量分析时,建议理解一个前提:该方案的速度精度上限取决于标定四边形与真实路段的贴合度、跟踪 ID 在遮挡下是否稳定,以及车辆是否近似直线通过标定区域——任何破坏"地平面假设"的强坡度路面或频繁变道都会放大误差。因此官方教程也强调 SOURCE/TARGET 必须按每个摄像头视角单独调整(见 README 中的 IMPORTANT 提示)。

许可证说明

该示例集成了多个独立组件,各自遵循不同许可证,动手集成前需要分别确认:

  • RF-DETR(推荐变体所采用的检测模型):遵循宽松的 Apache-2.0 许可证;
  • Ultralytics / YOLOv8ultralytics 变体):遵循 AGPL-3.0 许可证,商业闭源使用前需评估其传染性条款;
  • supervision(本示例中承担跟踪、区域过滤、标注与视频处理的基础库):遵循 MIT 许可证,可自由用于商业项目;
  • YOLO-NAS 与 super-gradients 作为遗留参考依赖,仅存在于需要运行 yolo_nas_example.py 的场景。

README 同时建议把 examples/speed_estimation/README.md 视为使用入口,其中保留着官方配套的视频演示与逐步讲解指引;仓库根目录的 LICENSE.mdREADME.md 则提供了 supervision 库本身的完整授权与生态说明。

登录后查看全文
热门项目推荐
相关项目推荐