ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Supervision:从YOLO输出到视频车流统计的工程化工具箱

2026/9/23 3:19:41 拓冰建站 浏览量
Supervision:从YOLO输出到视频车流统计的工程化工具箱 前阵子接了个小需求对一段路口监控视频做车流统计要求画出检测框、过滤误检、跟踪同一个目标别重复计数最后输出带标注的视频和统计结果。模型选型很快定了YOLO系列跑起来也不慢真正让我卡住的反而是模型输出之后那一大堆“脏活”要把检测框画得专业、要按置信度过滤、要跨帧判断是不是同一辆车、还要把越线次数累加起来。用OpenCV硬写我也写过无非是for循环套if判断配合一堆坐标运算和手写NMS代码越写越长换个项目基本没法复用。后来才意识到一个问题OpenCV是像素世界的瑞士军刀但模型输出的目标框、掩码、类别、轨迹已经是另一层“对象世界”的东西。处理这层东西有专门的工具箱就是Roboflow开源的Supervision。这篇文章就围绕它对视觉工程项目化的价值展开适合已经用OpenCV和YOLO跑通过检测demo、想系统化做大项目或者正在为“检测框后处理”写了一堆重复代码的人。1. 从模型输出到工程落地中间缺的就是这一层1.1 模型推理只是开始工程化才是大头很多人把目标检测项目想得太简单加载权重、跑一帧、画个框demo就完事了。真实业务需求往往是另一套逻辑——既要画框还要按置信度过滤掉低质量预测既要统计类别数量还要区分是“新出现的目标”还是“上一帧已经看过的目标”既要输出视频还要把每小时的检测数据落库更别提训练前还要做数据格式转换、抽样可视化、样本清洗。这些工作如果用OpenCV从零写每一件都要自己造轮子。手动实现一次置信度过滤不难难的是你要维护一整套“框的后处理逻辑”NMS要自己写还是调别的库同一目标的跨帧ID怎么分配画框的时候标签文字大小和框宽高怎么自适应越线计数的逻辑是中心点法还是IOU法这些问题和具体业务息息相关但几乎每个视觉项目都会遇到正是重复劳动最集中的地方。1.2 OpenCV解决像素问题Supervision解决对象问题我习惯打个比方OpenCV处理的是一张图里的“像素排列”而Supervision处理的是模型已经从像素里“提取出来的语义对象”。一个检测结果里有几十个框每个框有坐标、类别、置信度这些数据已经脱离了单纯像素的范畴更像是一张结构化表格。你把OpenCV比作“图像工具库”Supervision更接近“检测结果操作平台”——它帮你把表格整理、筛选、关联、统计再画回图像上。两者不是替代关系而是互补关系。OpenCV负责底层图像读取、颜色空间转换、形态学处理、透视变换这些像素操作Supervision负责模型输出之后的对象级处理包括Box标注、Mask标注、跟踪、越线计数、热力图、数据集格式转换。下面这张表能说明我平时划分功能的习惯任务类型用谁典型场景图像读取、缩放、颜色转换OpenCV视频帧预处理、数据增强检测框/掩码可视化Supervision模型结果画框、画Mask、画标签置信度过滤、NMSSupervision后处理管线跨帧目标ID分配SupervisionByteTrack目标跟踪越线计数、区域统计SupervisionLineZone/PolygonZone安防、交通流统计数据集YOLO/COCO格式互转SupervisionDetectionDataset训练数据准备透视变换、图像融合OpenCV图像拼接、特效1.3 适合什么场景不适合什么场景我用了大半年之后给Supervision划了一个明确的“业务边界”。适合它出场的场景有这些目标检测、实例分割、姿态估计项目里的结果可视化视频监控类应用的目标跟踪和数据统计模型训练前数据集格式转换和样本检查批量跑模型后的结果巡检和误检分析。在行人检测、缺陷检测、车流统计这类任务里它几乎能覆盖检测结果之后的所有环节。不适合它出场的场景也很明确纯像素级图像增强、图像滤波、形态学操作这些仍然要回到OpenCV去做。Supervision也不会帮你训练模型它不负责loss和反向传播。还有一点容易让人误解——它不是推理框架不带YOLO权重模型还得你自己加载、自己往前传播它只在模型输出之后接管。2. 环境与首次上手Supervision在第一帧里做了什么2.1 安装和依赖没什么坑但有版本意识安装很简单pip install supervision它依赖numpy、opencv-python、pydantic这些常见包一般情况下不会和已有环境打架。但有一个很现实的问题Supervision的迭代速度非常快API变动也频繁我在0.19版本写的代码到0.21版本已经有一部分函数改名、参数调整。建议项目一上来就锁定版本号比如pip install supervision0.21.0我在Windows和Linux环境下都装过在conda环境里用也没有碰到过编译问题因为它本身是纯Python加少量C扩展不需要像OpenCV那样自己编译。如果你跑的是老项目网上搜到某段源码结果发现ImportError或者AttributeError大概率不是环境坏了而是API改了这点后面会专门展开。2.2 最小的可用示例五步画出第一个带标注的视频我拿ultralytics的YOLO模型举例这是目前最常见的搭配。最小可用的完整流程大概是这样的import supervision as sv from ultralytics import YOLO model YOLO(yolo11n.pt) video_info sv.VideoInfo.from_video_path(input.mp4) box_annotator sv.BoundingBoxAnnotator() label_annotator sv.LabelAnnotator() with sv.VideoSink(output.mp4, video_info) as sink: for frame in sv.get_video_frames_generator(input.mp4): results model(frame)[0] detections sv.Detections.from_ultralytics(results) annotated box_annotator.annotate(frame.copy(), detections) annotated label_annotator.annotate(annotated, detections) sink.write_frame(annotated)这段代码干的事情很清楚逐帧读取视频跑模型把模型输出转成Supervision的Detections结构叠加BoundingBox标注和标签标注再写回视频。注意我在标注前用了frame.copy()因为annotator默认是在传入的图像上直接绘制如果你不想污染原始帧最好传入副本。2.3 Detections理解Supervision的钥匙第一帧实验跑通之后我建议你干一件事把Detections的内容打印出来看一眼这能帮你理解整个库的设计哲学。detections sv.Detections.from_ultralytics(results) print(detections) print(detections.class_id, detections.confidence, detections.xyxy)Detections本质上一个数据容器核心是几个numpy数组xyxy形状(N, 4)的目标框坐标按[x1, y1, x2, y2]排列mask可选的目标分割掩码形状(N, H, W)confidence置信度数组形状(N,)class_id类别ID数组形状(N,)tracker_id跟踪ID默认是空的调用跟踪器后才填充data字典可以塞任意自定义字段这个设计最让我舒服的地方是不管模型来自YOLO还是DETR还是Detectron2最终都能归一化成同一个Detections结构。以后你要换模型只需要换一行转换函数后面的处理逻辑完全不用改。这也是为什么我说Detections是理解Supervision的钥匙——因为一切操作标注、过滤、跟踪、统计都是围绕这个结构展开的。3. 工具箱逐个拆解标注、过滤、跟踪、统计、数据集3.1 标注器画框只是基本功它还提供一堆进阶画法Supervision里最常用的一类工具就是annotator。我用得最多的是BoundingBoxAnnotator和LabelAnnotator前者负责画出目标框后者负责在框边上写类别和置信度。如果你做的是实例分割项目可以用MaskAnnotator直接叠加分割掩码效果比把mask转成多边形再用OpenCV画要省事一个量级。还值得一提的几个偏进阶的标注器标注器作用适合场景TraceAnnotator绘制目标运动轨迹轨迹研判、运动分析HeatMapAnnotator生成热力图人群密度、逗留区域分析DotAnnotator绘制关键点姿态估计结果可视化PixelateAnnotator对目标区域做马赛克隐私保护这些标注器有一个共同点API高度统一都是annotator.annotate(scene, detections)的格式。我最早写OpenCV画轨迹的代码得手动维护一个轨迹字典每帧更新每个目标的历史坐标再用cv2.line逐段画换成TraceAnnotator之后两三行代码就搞定了。3.2 过滤和后处理切片、NMS、类别筛选Detections的另一个优势是它支持类似numpy的布尔索引你可以非常自然地做过滤操作# 按置信度过滤只保留大于0.5的检测结果 filtered_detections detections[detections.confidence 0.5] # 按类别过滤只保留personCOCO中person的class_id是0 person_detections detections[detections.class_id 0] # 组合条件置信度大于0.3的car car_detections detections[(detections.class_id 2) (detections.confidence 0.3)]NMS也不需要自己写了一行调用detections sv.nms(detections, threshold0.5)这些操作如果全部用OpenCV手写倒不是说写不出来而是每写一次都要重新处理坐标格式、列表拼接、类型转换这些琐碎细节代码很难优雅。Supervision把这些高频操作变成了标准库级别的接口项目代码的可维护性提升非常明显。3.3 跟踪跨帧ID分配的“正规军”监控类项目最容易忽略的一个需求是同一个目标在连续多帧里出现它到底算一个目标还是新的目标如果直接按帧统计一辆车停在画面里10秒就会被统计成几百次。要解决这个问题就得给每个目标分配一个跨帧不变的ID这就是跟踪器做的事。sv.ByteTrack在工程里足够顺滑tracker sv.ByteTrack( track_activation_threshold0.25, minimum_matching_threshold0.8, lost_track_buffer50 ) detections tracker.update_with_detections(detections)执行之后detections的tracker_id字段会被填充同一个目标在不同帧里会拿到同一个ID。你可以在画框时把这个ID一起显示出来方便人工检查跟踪效果。我踩过的一个坑是track_activation_threshold设得太高。比如设成0.5一个目标要连续若干帧都有较高的置信度才会被激活分配ID这本来是为了抑制误检但有些光线环境下的目标置信度波动大会导致ID频繁激活又丢失统计就不准了。后来我调到0.25左右结合后续业务逻辑再做过滤整体效果稳健多了。3.4 统计越线计数和区域统计监控需求最常用的两个组件LineZone是Supervision里最有实用价值的组件之一。它解决的是“目标是否越过了画面中某条线”的问题常用来做进店人数统计、车流量统计。line_zone sv.LineZone( startsv.Point(0, 400), endsv.Point(1280, 400) ) line_zone_annotator sv.LineZoneAnnotator() for frame in sv.get_video_frames_generator(input.mp4): results model(frame)[0] detections sv.Detections.from_ultralytics(results) detections tracker.update_with_detections(detections) line_zone.trigger(detections) annotated line_zone_annotator.annotate(frame.copy(), line_zone)LineZone内部会自动判断目标的中心点是否跨越了这条线并且会结合tracker_id做去重同一个目标只计数一次就算目标在线上来回移动也不会反复触发。这里有一个底层逻辑值得注意如果检测结果没有tracker_idLineZone可能把同一目标在连续帧里的位置变化误判成多次越线。所以我一般都会在触发LineZone之前先做跟踪再触发统计。如果你要统计的不只是越线还包括目标出现在某个多边形区域内可以用PolygonZoneAPI风格类似适合做“人是否闯入了禁入区域”这类需求。3.5 数据集工具训练之前的数据格式噩梦它也能管除了在线推理Supervision还提供了一整套数据集相关的工具。我最早低估了这部分直到有一次要把一批YOLO格式的标注数据转成COCO格式才真正体会到它的实用性dataset sv.DetectionDataset.from_yolo( images_directory_pathimages/, annotations_directory_pathlabels/, data_yaml_pathdata.yaml ) dataset.as_coco( images_directory_pathcoco_images/, annotations_pathcoco_annotations.json )它还支持把数据集里的样本随机抽出来逐张可视化这在训练前检查标注质量时特别有用。标注错误、框偏移、类别标错这些问题靠肉眼扫一遍抽样图比看枯燥的txt文件高效得多。4. 实战案例搭一个实时车流统计小系统前面讲了一堆组件现在把它们组装起来做一个完整的车流统计Demo。这个案例我建议你亲手跑一遍在项目里算法选型往往不是最折腾人的把这些组件按照业务逻辑串成一条流畅的管线才是真正见功夫的地方。4.1 整体流程设计先明确业务逻辑统计画面中指定方向的车辆通过数量输出带标注的视频同时在控制台实时打印计数结果。我设计了这样的处理链逐帧读取视频用YOLO模型检测目标只保留车辆类目标car、bus、truck过滤置信度低于阈值的目标用ByteTrack做跨帧跟踪得到唯一ID用LineZone设置两条虚拟线统计车辆进出方向叠加检测框、标签、轨迹和热力图用VideoSink输出视频4.2 代码实现import numpy as np import supervision as sv from ultralytics import YOLO # 模型和视频 model YOLO(yolo11n.pt) SOURCE_VIDEO traffic.mp4 TARGET_VIDEO traffic_annotated.mp4 # 车辆类别在COCO数据集中为2(bus), 5(bus), 7(truck)按你的需求调整 VEHICLE_CLASS_IDS [2, 5, 7] # 视频信息 video_info sv.VideoInfo.from_video_path(SOURCE_VIDEO) # 标注器 box_annotator sv.BoundingBoxAnnotator() label_annotator sv.LabelAnnotator(text_thickness1, text_scale0.5) trace_annotator sv.TraceAnnotator(trace_length20) heat_map_annotator sv.HeatMapAnnotator(positionsv.Position.CENTER) # 跟踪器 tracker sv.ByteTrack( track_activation_threshold0.25, minimum_matching_threshold0.8 ) # 统计线和标注器 line_zone sv.LineZone( startsv.Point(0, int(video_info.height * 0.6)), endsv.Point(video_info.width, int(video_info.height * 0.6)) ) line_zone_annotator sv.LineZoneAnnotator() with sv.VideoSink(TARGET_VIDEO, video_info) as sink: for frame in sv.get_video_frames_generator(SOURCE_VIDEO): result model(frame)[0] detections sv.Detections.from_ultralytics(result) # 只保留车辆类 mask np.isin(detections.class_id, VEHICLE_CLASS_IDS) detections detections[mask] # 过滤低置信度 detections detections[detections.confidence 0.4] # 跟踪 detections tracker.update_with_detections(detections) # 触发越线统计 line_zone.trigger(detections) # 叠加标注 annotated frame.copy() annotated trace_annotator.annotate(annotated, detections) annotated box_annotator.annotate(annotated, detections) annotated label_annotator.annotate(annotated, detections) annotated line_zone_annotator.annotate(annotated, line_zone) annotated heat_map_annotator.annotate(annotated, detections) sink.write_frame(annotated) # 控制台实时输出 print(fin_count{line_zone.in_count} out_count{line_zone.out_count})4.3 为什么这样配置这个流程里有几个选择值得说明。第一先过滤类别再做跟踪而不是先跟踪再过滤原因是为了减轻跟踪器的负担。每一帧可能检测出几十个目标如果你把墙、红绿灯、行人全丢给跟踪器会增加ID分配的计算量和误匹配概率。第二LineZone放在跟踪之后触发是为了拿到tracker_id做去重。第三line_zone画在画面高度60%的位置基本模拟一条横向虚拟线不容易被车辆互相遮挡干扰。第四我特意把HeatMapAnnotator也加进去了它能帮你直观看出车辆最常走的轨迹区域在展览、商场这类场景里特别有价值。我实际跑这个流程的时候1080p视频每帧推理加标注大概耗时100毫秒左右也就是说大概10FPS做离线处理完全没有问题做实时的话需要配合跳帧或者降低推理分辨率。5. 常见坑与真实经验版本变更、坐标体系、性能瓶颈5.1 版本差异是最大的隐形坑这个必须放在第一个说。我前面提到Supervision迭代快API变动频繁这不是随便说说的。拿标注器举例早期版本里叫sv.BoxAnnotator新版本改成了sv.BoundingBoxAnnotatorLabelAnnotator的一些画文字参数也从统一的thickness、text_scale调整成了更细粒度的text_thickness、text_scale。如果你在网上搜代码很可能会搜到旧版本写法复制到新环境里直接报错。我的处理方法是写进requirements.txt时锁定版本哪个版本写的代码就固定用哪个版本换版本后先跑一遍官方Example确认API变化遇到ImportError先去官方文档确认类名。不要依赖脑子里记的API这个库还在活跃开发期API稳定是之后的事。5.2 BGR还是RGB颜色不对别甩锅给算法这是一个非常隐蔽的坑。OpenCV读取图片的默认通道顺序是BGR但很多深度学习框架训练时用的是RGB。如果你的pipeline是“模型输出检测框然后直接传给Supervision画标注”颜色一般是对的因为推理库内部通常自己做了转换。但如果你手动做过cv2.cvtColor或者模型输入前自己改了通道顺序就要特别注意。有一次我用一个自训练的模型推理检测框位置全部正确但画出来的Mask区域完全错位排查了半天最后发现问题出在我预处理时用cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)转了RGB模型输出的是RGB图上的坐标但我又拿原始的BGR帧去叠加Mask导致像素位置完全对不上。解决方式是标注叠加的时候保证输入给annotator的图像和输入给模型的图像是同一个通道顺序。5.3 性能瓶颈逐帧全流程不是最优解跑视频处理的时候很多人习惯逐帧推理、逐帧标注、逐帧输出但实际项目中逐帧全流程往往不是最优解。“检测加跟踪加统计”全链路在每帧都执行推理耗时是最大的瓶颈后面的标注反而开销较小。几个实用的优化思路跳帧处理比如每2帧或者每3帧推理一次中间帧直接沿用上一帧的检测结果适合对实时性要求不高的离线场景缩小推理尺寸把帧缩放到640或者320再输入模型检测框坐标在映射回原图时才需要乘一个缩放系数Supervision在from_ultralytics里也会帮你保持对应只对ROI区域做推理如果画面中只有某个区域是业务关心的先用OpenCV裁剪出这个区域检测完再把坐标偏移回原图用sv.FPSMonitor观察性能瓶颈在哪一环fps_monitor sv.FPSMonitor() for frame in sv.get_video_frames_generator(SOURCE_VIDEO): fps_monitor.tick() fps fps_monitor()不要一上来就追求完美画质先把流程跑通再用性能分析工具定位瓶颈是更务实的路径。5.4 什么时候需要回退到OpenCV虽然Supervision覆盖了对象级处理但它的绘图能力更像是一套预设好规则的标注器灵活性有限。如果你想画虚线框、写特殊样式的文字、做透视变换之后的多路视频拼接、或者把标注结果和AR特效叠加这些还是得回到OpenCV去实现。另外批量图像预处理、色彩校正、形态学噪声过滤这些像素操作Supervision本来就不管老老实实交给OpenCV。我的经验是在一个视觉项目里Supervision管的是90%的对象级重复劳动OpenCV管的是剩下10%的定制化像素操作。两者配合起来用比单纯依赖某一个都舒服。6. 进阶思路把Detections融进你自己的工程体系6.1 自定义字段让检测结果不再是“哑数据”很多场景下目标框、类别、置信度这些字段还不够。你可能想给每个目标附加一个速度估计、一个优先级标签、或者一个从GPS坐标映射过来的位置。Detections.data这个字典字段就是为这种自定义信息准备的。import numpy as np detections.data[priority] np.array([1, 3, 2])之后你在自定义标注器里读取这个字段再画到图上逻辑完全不打扰原有pipeline。我做缺陷检测项目的时候经常把“缺陷等级”塞进data里然后用不同颜色的框表示不同等级比单纯输出一个类别ID灵活很多。6.2 多模型结果统一检测加分割不再需要手写坐标对齐真实项目里经常出现多模型协同先检测目标位置再用分割模型抠出目标的精细轮廓甚至再接一个分类模型判断属性。以前这种需求我都是自己写一堆坐标对齐和Mask拼接的逻辑容易出bug。Supervision提供了一些模型转换工具核心思路还是那句老话所有模型输出统一转成Detections后续处理全走一套接口。from supervision import Detections # 假设det是目标检测模型转出的Detectionsmask是从分割模型得到的掩码 detections Detections( xyxydet.xyxy, maskmask_array, confidencedet.confidence, class_iddet.class_id )如果你手头有多个不同架构的模型可以先用各自的转换函数适配到Detections再在统一的数据结构上做后处理。这种解耦带来的可维护性提升用一次就回不去了。6.3 我自己的工程习惯最后分享几个实打实的习惯。第一所有项目不管多小都先定义好检测、过滤、跟踪、统计这套管线即使当前用不到跟踪和统计也先占位方便后加需求第二所有标注器的参数统一维护在一个配置类里方便调试比如画框粗细、字体大小、颜色全部集中管理第三每个功能模块都用一个小型纯数据Demo而不是真实视频来验证跑通之后再接真实数据定位问题会快很多第四看到“新版本API”先看release note别急着改代码确认废弃项和新增项再动手不然一次升级可能要改好几个模块。这套工具箱现在已经成为我视觉工程项目的标配。它不会替代OpenCV也不会替代模型框架但它补上了模型输出到业务落地之间最容易被低估的那一层。如果你也在为目标框后处理、跟踪、统计这些重复劳动头疼我建议你先拿一段自己手头的视频照着前面几个例子跑一遍很快就能感受到差别。