ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

supervision:一套搞定目标检测后处理、跟踪与区域统计

supervision:一套搞定目标检测后处理、跟踪与区域统计 如果你正在做目标检测项目每次模型推理完之后还要自己画框、写标签、做跟踪、统计人数、保存视频roboflow / supervision 这个库值得认真研究一遍。它不是一个新检测模型而是一套围绕检测结果设计的后处理和标注工具核心价值是把不同模型的输出统一成 Detections 结构然后提供画框、标签、目标跟踪、区域统计、视频写入等模块化组件。适合已经在用 YOLO 等模型、但不想每次重复写 OpenCV 后处理代码的人。最值得关注的是模型输出一旦转成 Detections后续所有处理都围绕同一个数据对象展开单图、视频、批量任务可以复用同一条处理链路。下面我按实际落地顺序拆一遍从环境、单图、视频、跟踪、批量到排查。1. 先搞清楚 supervision 解决的是哪一段问题1.1 目标检测流程里推理完成后才是重复劳动开始一个目标检测项目通常分为两段模型加载和推理输出推理结果的可视化、统计、落盘。前一段已经有 YOLO 系列、RT-DETR、OpenMMLab 等很多成熟方案后一段却经常是每个人自己写一套。常见的做法是拿到模型输出的坐标、置信度、类别 ID再用 OpenCV 的rectangle和putText逐帧画框。只处理一张图时代码确实不多。但项目一旦涉及视频、多个类别、跟踪、区域人数统计、结果保存重复代码会迅速膨胀坐标要转成整数类别 ID 要映射成名称置信度要格式化成字符串遮挡目标要处理视频编码要调整参数。supervision 把这一整段收敛成几个模块。Detections 负责统一数据结构BoxAnnotator 画框LabelAnnotator 画标签ByteTrack 做目标跟踪PolygonZone 和 LineZone 做区域统计VideoSink 负责写视频。你可以按需组合而不是引入一个重型框架。1.2 相比裸写 OpenCV它强在哪里第一统一数据流。来自不同检测框架的推理结果都能转换成 Detections。后续操作不关心结果来自 YOLO、YOLOv5 还是 transformers。模型切换时只需要改转换那一行后面的画框、跟踪、统计代码基本不用动。第二组件可以组合。画框、标签、跟踪、区域统计都是独立对象用哪个就实例化哪个不会把所有功能耦合在一起。这种设计对调试很友好出了问题能很快定位是哪一层。第三视频链路有封装。帧读取、视频信息、结果写入、逐帧标注由VideoInfo、get_video_frames_generator、VideoSink配合完成比每次手动设置VideoWriter更省事也少一些低级别参数错误。1.3 边界也先说清楚它不负责识别目标是什么只负责处理已经识别出来的结果。如果你还没有一个能稳定输出检测框的模型先解决模型问题再回来用它。它也不是任务调度平台适合作为图像和视频后处理层的工具。大规模分布式任务仍然需要自己设计队列、重试、分布式存储。还有一点要特别注意supervision 的 API 在版本迭代中会有调整网上教程里的代码可能对应不同版本跑不通时先检查你安装的版本而不是怀疑代码抄错了。2. 写代码前先把环境和数据流转理清楚2.1 运行环境与安装最省事的方案是在 Python 3.9 以上的虚拟环境里安装。Windows、Linux、macOS 都能用但我不建议直接装到系统 Python 里。视觉项目依赖很多OpenCV、NumPy、模型框架之间容易互相影响虚拟环境能省掉大量版本冲突问题。python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install supervision如果你的检测模型来自 Ultralytics YOLO还要同时安装pip install ultralytics安装完成后先验证一下导入是否正常python -c import supervision; print(supervision.__version__)如果 import 失败优先怀疑两个问题是不是装到了另一个 Python 环境是不是环境中已有老版本 OpenCV 导致接口冲突。不要急着重装系统先看which python和pip list | grep supervision。2.2 核心概念DetectionsDetections 是 supervision 的核心数据结构。它的本质是把一批检测结果封装起来主要包含xyxy边界框坐标格式是左上角和右下角两个点。mask语义分割或实例分割的掩膜。confidence每个目标的置信度。class_id类别索引。tracker_id跟踪器分配的目标 ID。data附加数据用于向前传递其他信息。后续的标注、跟踪、统计都读取这些字段。最常见的转换方式是import supervision as sv detections sv.Detections.from_ultralytics(result)如果你用的是其他框架也有from_yolov5、from_transformers等转换入口。不同版本支持的来源可能不同以你安装的版本为准。2.3 为什么先理清数据流转实际踩坑时最容易出问题的不是 API 调用而是没想清楚数据在三种格式之间怎么流动模型原始输出Detections标注结果。很多人拿到一段代码直接改路径就跑报错后只盯着 API 名字看却忽略了自己的模型返回格式不一样。我建议动手前先打印一下中间结果。比如用 YOLO 时先看result.boxes里有多少检测框转成 Detections 后再打印detections.xyxy、detections.class_id、detections.confidence。确认这些字段有值再继续往下写。这一步能排除大量“看起来是标注问题实际是模型输出问题”的情况。3. 第一个可运行 Demo单张图片的检测与可视化3.1 最小代码流程先跑通一张图后面的视频和批量才有基础。下面是一段完整可参考的示例import cv2 import supervision as sv from ultralytics import YOLO model YOLO(yolov8n.pt) image cv2.imread(demo.jpg) result model(image)[0] detections sv.Detections.from_ultralytics(result) box_annotator sv.BoxAnnotator() label_annotator sv.LabelAnnotator() labels [ f{model.names[class_id]} {confidence:.2f} for class_id, confidence in zip(detections.class_id, detections.confidence) ] annotated box_annotator.annotate( sceneimage.copy(), detectionsdetections ) annotated label_annotator.annotate( sceneannotated, detectionsdetections, labelslabels ) sv.plot_image(annotated)这段代码做了四件事加载模型、推理图片、把结果转成 Detections、画框和标签后展示。3.2 关键参数说明model.names是 Ultralytics 自带的类别名映射。如果你的模型来自其他框架要自己准备一个id - name的字典不能照抄这段。BoxAnnotator常用参数有thickness、color。color可以指定一个固定颜色也可以不传让标注器按类别自动分配。LabelAnnotator常用参数有text_scale、text_thickness、text_color、text_padding。文字标签默认放在框的左上角附近实际表现会受文本长度和边界框位置影响。这里有一个容易忽略的细节annotate方法的第一个参数是scene。我传入image.copy()是因为标注器默认会在原图像上直接画。如果不复制原图会被覆盖。后面如果还要用原图做其他处理必须保留一份原始数据。3.3 成功标准和验证跑通代码不代表就结束了还要检查三件事图片能不能正常显示。框是否贴合目标有没有明显偏移。类别名称和置信度是否和画面内容对应。如果图片显示出来颜色不对大概率是 RGB 和 BGR 通道问题。supervision 按 OpenCV 的 BGR 惯例处理sv.plot_image也按 BGR 显示。不要把其他地方读出来的 RGB 图直接传进去否则蓝和红会互换。如果画面里没有任何框先看模型输出不要怀疑标注器。你可以在转换前打印result.boxes或者打印detections.xyxy确认检测数量是否为零。很多时候不是代码的问题而是这张图里确实没检测到目标或者检测阈值太高。4. 从单图到视频循环、帧率与输出文件4.1 视频入口和帧迭代视频任务不是简单把图片循环放大。视频有帧率、尺寸、编码、时长这些额外信息自己用cv2.VideoCapture和cv2.VideoWriter写也不是不行但参数多容易出错。supervision 把这一层封装成了三个组件VideoInfo.from_video_path读取视频信息。get_video_frames_generator逐帧读取视频。VideoSink保存输出视频。下面是一个完整的视频处理示例import cv2 import supervision as sv from ultralytics import YOLO model YOLO(yolov8n.pt) video_info sv.VideoInfo.from_video_path(input.mp4) generator sv.get_video_frames_generator(input.mp4) box_annotator sv.BoxAnnotator() label_annotator sv.LabelAnnotator() with sv.VideoSink(output.mp4, video_info) as sink: for frame in generator: result model(frame)[0] detections sv.Detections.from_ultralytics(result) labels [ f{model.names[class_id]} {confidence:.2f} for class_id, confidence in zip(detections.class_id, detections.confidence) ] annotated box_annotator.annotate( sceneframe, detectionsdetections ) annotated label_annotator.annotate( sceneannotated, detectionsdetections, labelslabels ) sink.write_frame(annotated)这里每一帧都做了一次完整推理。如果你的视频是 1080p、30FPS、10 分钟实际要处理 18000 帧耗时取决于模型推理速度而不是 supervision 本身。4.2 为什么用 VideoSink 而不是自己写 VideoWriter自己写 OpenCV 的 VideoWriter 时经常要手动指定cv2.VideoWriter_fourcc、帧率、宽高。漏一个或写错一个输出文件就可能打不开或者尺寸和源视频不一致。VideoSink 的好处是能直接借用 VideoInfo 里的帧率、宽度、高度和编码信息减少大部分低级错误。如果你想换编码或重新指定输出参数可以先构造一个VideoInfo改好字段后再传给 VideoSink。有一点要注意输出尺寸最好和源视频保持一致。如果模型输入尺寸和输出尺寸不一致最终写出的视频尺寸仍会按 VideoInfo 走。强行改变宽高时要么在画面上做缩放要么注意是否导致文件异常。4.3 视频任务先测帧率再跑全片视频处理最容易翻车的就是一上来直接跑整段长视频。建议先取前 50 帧或前 10 秒测试统计每帧耗时再估算全片总时长。判断标准很直接如果单帧推理 100 毫秒一秒钟大约处理 10 帧一段 10 分钟的视频有 18000 帧大约需要 1800 秒也就是 30 分钟。如果你原本预期几分钟跑完这个结果就不符合预期需要先优化模型输入尺寸、推理后端或硬件配置。实时摄像头任务更严格。30FPS 视频每一帧间隔约 33 毫秒如果单帧处理超过这个时间就没法做到实时。低配机器能跑不代表能按实时帧率跑这是两件事。显存和内存也要盯。用 GPU 推理时可以用nvidia-smi看显存占用用 CPU 时看内存和 CPU 占用。如果视频处理速度越来越慢往往不是模型问题而是前面帧没有被释放或者内存不足触发了大量换页。5. 目标跟踪与区域统计从画框到业务指标5.1 用 ByteTrack 给目标分配稳定 ID画框只能表示当前帧有哪些目标。很多业务需要知道同一个目标是否持续存在比如人流量统计、车辆逗留时间、越界报警。这时要用目标跟踪器。supervision 内置了 ByteTrack。用法很简单tracker sv.ByteTrack() tracked_detections tracker.update_with_detections(detections) labels [ f#{tracker_id} {model.names[class_id]} {confidence:.2f} for tracker_id, class_id, confidence in zip( tracked_detections.tracker_id, tracked_detections.class_id, tracked_detections.confidence ) ]注意顺序必须先转成 Detections再交给 tracker。如果你把模型原始输出直接传进去会报错。跟踪是在已经检测到的目标之间做关联它不能替代检测。5.2 区域统计和越线计数人数统计和区域巡检是常见需求。如果你的业务是判断某个多边形区域内有没有目标、有多少目标可以用 PolygonZone。import numpy as np zone sv.PolygonZone( polygonnp.array([ [100, 100], [500, 100], [500, 400], [100, 400] ]), triggering_anchorssv.Position.CENTER ) zone.trigger(detectionsdetections) count zone.current_count如果要统计一条线两侧的进出数量可以用 LineZone。它根据目标中心点相对一条线的位置变化判断目标是从左边进入还是从右边出去适合出入口计数。line_zone sv.LineZone( startsv.Point(0, 400), endsv.Point(1280, 400) ) line_zone.trigger(detectionsdetections) in_count line_zone.in_count out_count line_zone.out_count这里有一个关键参数triggering_anchors。它决定用目标上的哪个点作为判断依据。默认用中心点比较稳妥。但如果目标很大中心点还没进入区域边缘已经先到了判断就会延迟。这时可以换成BOTTOM_CENTER或TOP_CENTER等锚点具体选择要看目标和业务方向。5.3 实际业务里的参数取舍检测置信度阈值不要开太低。区域统计对误检非常敏感一个误检框可能让计数多跳很多次。建议先过滤低置信度结果再进入跟踪和统计。你可以用Detections的过滤方法也可以在做区域判断前手动筛一遍。跟踪器也有参数。比如目标丢失多少帧后删除轨迹。默认值适合大多数场景但如果是高遮挡环境目标经常被临时挡住可以把丢失帧数调大如果是目标快速移动前后帧位置变化大可能需要调整匹配阈值。没有万能参数先跑一段小样本看 ID 稳定性。判断跟踪和统计效果可以固定一个目标观察三件事目标在区域内来回走动时计数是否只按预期增加。两个目标交错后ID 是否发生互换。目标离开后计数是否会停留过久。这些都要通过可视化标注加日志一起观察只看最终数字很难定位问题。6. 批量处理与功能封装从能跑到敢跑6.1 批量之前想清楚三件事测试阶段跑一张图、一段视频问题通常不大。一旦进入批量很多预想不到的问题会冒出来。我建议批量处理前先把这三件事定下来输入怎么组织图片放在一个目录视频路径写在 txt 文件里还是从接口逐个接收。输出怎么命名如何避免覆盖已有文件失败文件如何处理。稳定性怎么保证一批任务中某个文件失败是跳过、重试还是整个流程退出。很多人一上来就写一个巨大的循环感觉能用就行结果跑到第 30 个文件时崩溃前面 29 个白跑。批量任务不能只看能不能跑还要看失败重试、队列、日志和输出一致性。6.2 把处理流程封装成函数不管是单图还是视频尽量把核心逻辑封装成一个函数。函数的好处是单文件容易测试出错时可以单独调用也方便以后接入接口或并行处理。def process_image(image_path, output_path, model, box_annotator, label_annotator): image cv2.imread(image_path) if image is None: raise ValueError(fcannot read image: {image_path}) result model(image)[0] detections sv.Detections.from_ultralytics(result) labels [ f{model.names[class_id]} {confidence:.2f} for class_id, confidence in zip(detections.class_id, detections.confidence) ] annotated box_annotator.annotate( sceneimage.copy(), detectionsdetections ) annotated label_annotator.annotate( sceneannotated, detectionsdetections, labelslabels ) cv2.imwrite(output_path, annotated) return len(detections)封装之后批量循环就变成简单的遍历。函数内部单文件失败时外层能捕获异常不会让整个批量任务中断。6.3 日志、失败重试和输出命名批量场景要记录每个文件的处理结果。建议至少记录四样信息文件路径、成功还是失败、检测数量、耗时。你可以把它们写到 CSV也可以写到日志文件后面排查时能直接看到哪个文件在什么阶段出问题。失败重试不能省。很多任务失败是暂时性的比如文件被其他进程占用、输出目录不存在、显存瞬间不足。建议在循环里用try/except捕获异常把失败文件记录下来最后统一重试。不要碰到一个失败就退出整个流程。重试后仍然失败的任务要看日志。常见原因不是模型判断错误而是输入路径不存在、图片损坏、输出目录没有写权限。批量输出命名可以用“源文件名加后缀”的方式例如name_detected.jpg。如果输出目录里已经存在同名文件先确认是否允许覆盖。不允许覆盖时加上时间戳或序号。7. 我实际踩过的坑和排查顺序7.1 启动阶段最常见的问题启动阶段最容易出问题的地方不是核心逻辑而是环境。如果你安装后 import 失败先不要重装系统检查两件事当前 Python 是不是你安装时用的同一个环境。是否有多个 OpenCV 版本在干扰。你可以运行python -c import supervision; print(supervision.__version__)如果这个能通过但你的项目代码里 import 失败那就是项目解释器和命令行解释器不一致。另外一个常见问题是转换方法报错。比如Detections.from_ultralytics不存在多半是 version 差异。supervision 版本更新比较频繁不同版本对模型来源支持不完全一样先看安装版本的帮助文档再看代码。如果图片显示出来了但框上没有类别文字检查labels是否传给了LabelAnnotator以及labels的数量是否和detections数量一致。长度对不上时很多版本会直接报错有些版本则安静地不显示文字。7.2 运行中卡住、无输出、速度慢的排查链路程序运行中卡住或没有输出排查顺序很重要。我习惯这样看先看现象。是卡住不动还是一直在跑但没结果。这两种情况处理方式完全不同。再看输入。图片能不能正常读取视频路径是否存在帧生成器是不是返回了空。再看日志。程序有没有在关键节点输出信息有没有异常被吞掉。再看资源。用nvidia-smi看显存用top或任务管理器看内存和 CPU。再看参数。检测阈值、批量大小、视频输出尺寸、标注器厚度有没有明显异常。最后看版本。supervision API 在更新中可能变化老教程代码跑不通很正常。不要一上来就改模型参数。很多问题看起来像模型能力不够实际是输入文件或环境问题。7.3 别把工具限制当成项目 bug有些现象看起来像 bug其实是使用边界。比如 LabelAnnotator 对中文字体支持默认不好。如果你要画中文标签大概率需要自己处理字体或者先用类别 ID 替换。这不是项目报错而是工具的设计边界。再比如 VideoSink 保存的 mp4 在某些播放器里打不开。先换一个播放器试试再判断是不是编码器问题。OpenCV 自带的编码支持在不同系统上有差异这是常见情况。目标检测框偶尔闪烁也正常。逐帧检测本身没有利用时序信息没有加跟踪时同一目标可能在某几帧漏检导致框闪烁。这不是 supervision 的 bug而是检测模型的问题。加上 ByteTrack 后通常会稳定很多。最后留几个我会优先排查的点检测结果是不是为空Detections 里字段长度是否正常标注器是否收到了正确参数输出目录有没有写权限。很多复杂问题说到底只是某一层的小失误。
返回列表