supervision 中 DetectionsSmoother 深度解析:基于滑动窗口实现跨帧检测框平滑
DetectionsSmoother 是 supervision(Roboflow 开源的计算机视觉后处理工具库,口号为 "We write your reusable computer vision tools")提供的检测平滑工具,位于检测工具集 sv.detection.tools 中。它针对视频追踪场景下目标检测框逐帧抖动的问题,为每个追踪目标维护一段固定长度的历史窗口,并对窗口内的坐标做均值平滑,从而让标注框、下游统计结果更加稳定。读完本文,你将掌握它的完整 API、逐帧更新与轨道管理的工作机制、置信度与旋转框(OBB)的特殊处理规则,以及如何把它接入 Ultralytics / RF-DETR 等检测 + 追踪流水线。
工具定位:平滑解决什么问题
在视频目标检测中,即使模型输出稳定,检测框的坐标也会因每帧预测误差而轻微跳动;若再叠加遮挡、短暂漏检再重现等情况,视觉上的"抖动"会更明显。DetectionsSmoother 的思路是:不再单独信任当前帧的检测框,而是取该追踪目标(track)最近 length 帧检测框的均值作为平滑后的输出。
类定义见 DetectionsSmoother,并通过包级入口导出,可直接以 sv.DetectionsSmoother 使用,见 supervision 包初始化文件(from supervision.detection.tools.smoother import DetectionsSmoother 及 __all__ 中的 "DetectionsSmoother" 条目)。官方文档页入口为 docs/detection/tools/smoother.md,追踪流程的完整用法可在 追踪目标指南 的 "Bonus: Smoothing" 一节中看到。
官方文档给出了三条必须注意的前提约束:
- 必须提供
tracker_id:平滑以追踪 ID 为单位组织历史,没有追踪 ID 就无法区分"同一个目标在不同帧的检测"。若检测到tracker_id is None,update_with_detections会发出SupervisionWarnings警告并原样返回输入,跳过平滑(源码 smoother.py 第 148–155 行)。 - 不兼容分割模型:该类只处理
xyxy框(以及可选的旋转框角点),不适用于 mask 类输出。 - 置信度不一致时的降级规则:当同一帧中部分追踪携带置信度、部分不携带时,该帧所有平滑检测的
confidence会被统一置为None,以避免合并时的字段冲突(源码 smoother.py 第 244–249 行)。
快速上手:最小示例与数值验证
官方文档附带的交互式示例(doctest)是理解其数学行为的最快方式:用 length=3 创建一个 smoother,送入两个带 tracker_id 的检测,观察平滑结果。
import numpy as np
import supervision as sv
smoother = sv.Detectionssmoother if False else sv.DetectionsSmoother(length=3)
detections_1 = sv.Detections(
xyxy=np.array([[0, 0, 10, 10]]),
confidence=np.array([0.5]),
tracker_id=np.array([1]),
)
detections_2 = sv.Detections(
xyxy=np.array([[2, 2, 12, 12]]),
confidence=np.array([0.7]),
tracker_id=np.array([1]),
)
smoothed = smoother.update_with_detections(detections_1)
print(smoothed.xyxy) # array([[ 0., 0., 10., 10.]]) —— 窗口内只有 1 帧,即原框
smoothed = smoother.update_with_detections(detections_2)
print(smoothed.xyxy) # array([[ 1., 1., 11., 11.]]) —— 两帧坐标的均值
print(smoothed.confidence) # array([0.6]) —— 两帧置信度的均值
可以看到两点规律:
- 框坐标是窗口内所有有效帧
xyxy的逐元素均值(第一帧输出等于自身,第二帧是两帧均值(0+2)/2, (0+2)/2, (10+12)/2, (10+12)/2)。 - 置信度同样在窗口内取均值,但只统计"携带置信度"的帧;若整个窗口都没有置信度则保持
None。
完整流水线:接入检测 + 追踪
官方文档给出的标准用法是"检测模型 → 追踪器 → smoother → 标注器"四级流水线,以下示例基于 RF-DETR 与内置 ByteTrack:
import supervision as sv
from rfdetr import RFDETRMedium
video_info = sv.VideoInfo.from_video_path(video_path="<SOURCE_FILE_PATH>")
frame_generator = sv.get_video_frames_generator(source_path="<SOURCE_FILE_PATH>")
model = RFDETRMedium()
tracker = sv.ByteTrack(frame_rate=video_info.fps)
smoother = sv.DetectionsSmoother()
box_annotator = sv.BoxAnnotator()
with sv.VideoSink("<TARGET_FILE_PATH>", video_info=video_info) as sink:
for frame in frame_generator:
detections = model.predict(frame[:, :, ::-1])
detections = tracker.update_with_detections(detections) # 先分配 tracker_id
detections = smoother.update_with_detections(detections) # 再做平滑
annotated_frame = box_annotator.annotate(frame.copy(), detections)
sink.write_frame(annotated_frame)
顺序很关键:必须先过追踪器拿到 tracker_id,再送入 smoother。同样的模式也出现在关键点追踪场景,见 docs/how_to/track_objects.md 中 Ultralytics / Inference 两套示例,均为 tracker.update_with_detections(...) 之后紧跟 smoother.update_with_detections(...),再交给 BoxAnnotator 与 TraceAnnotator 画框和轨迹。
一个需要留意的版本信息:示例中使用的内置 sv.ByteTrack 自 supervision-0.28.0 起标记为弃用,并计划在 0.31.0 移除(建议改用 trackers 包的 ByteTrackTracker,且更新方法由 update_with_detections() 改名为 update()),说明见 ByteTrack 类文档。本文示例忠实保留当前文档给出的写法,若你使用 0.28.0 之后的版本,建议将追踪器替换为外部实现,其余平滑代码不变。
API 详解
__init__(length: int = 5)
唯一构造参数:
length:参与平滑的最大帧数,即每个追踪历史的滑动窗口长度,默认 5。
从源码看,length 通过 defaultdict(lambda: deque(maxlen=length)) 落实为每个追踪 ID 一条独立、有上限的 deque 队列,见 smoother.py 第 103–111 行。因此:
length越大,平滑越强、输出滞后越大,框对目标真实位置的反应越慢;length越小(如 1–2),输出越贴近原始检测,但抖动抑制有限;- 每个轨道的历史相互独立,窗口满后最早帧自动滑出,不占用额外内存。
update_with_detections(detections) -> Detections
核心更新入口,每处理一帧调用一次。它在 smoother.py 第 140–174 行的实现分四步:
- 前置校验:
detections.tracker_id is None时发出SupervisionWarnings警告并原样返回输入; - 写入历史:逐条检测取出
tracker_id,用detections.select(detection_idx)复制出单目标Detections后追加进该轨道的队列(select保证每帧历史都是独立副本,见 Detections.select); - 缺席补记:对本帧未出现的历史轨道追加一个
None占位;若某轨道整个窗口全是None(长时间未再出现),则从缓存中删除该轨道,防止内存无限增长; - 输出:调用
get_smoothed_detections(track_ids=当前帧活跃轨道),只输出本帧活跃轨道的平滑结果——缺席轨道保留历史但不输出"幽灵框"。
其中第 3 步的活跃 ID 判断使用集合成员检查而非逐轨道扫描,这是仓库变更日志中记录的一次性能优化(无输出变化)。
get_track(track_id) -> Detections | None
返回单个轨道的平滑 Detections,规则(源码第 176–226 行):
- 取窗口内所有非
None帧,对xyxy逐元素求均值; confidence仅在携带它的帧上求均值;全部缺失时为None;- 其余字段(如
class_id)取自窗口中最早的有效帧; - 轨道未知或整窗为空时返回
None。
get_smoothed_detections(track_ids=None) -> Detections
将多条轨道的平滑结果用 Detections.merge 合并为一个对象(见 Detections.merge)。两个细节值得注意:
track_ids用于过滤"本帧活跃轨道":缺席轨道的历史仍保留在缓存中,只是不参与本次输出;- 若任一轨道的
confidence为None,则全部平滑检测的confidence统一置None——这是为了规避Detections.merge对可选字段"全有或全无"的合并约束(源码第 244–249 行的注释明确说明了这一点); - 结果为空时会补一个
tracker_id=np.array([], dtype=int),保证返回对象字段完整。
reset()
清空所有轨道的历史,但保留配置的 length,让同一个实例可以复用于不同视频流而不携带上一流的残帧。变更日志显示该方法是近期为与 TraceAnnotator、HeatMapAnnotator 保持接口一致而补充的。
深入实现:置信度、缺席轨道与旋转框
缺席与重现:历史不丢,输出不撒谎
测试 tests/detection/tools/test_smoother.py 的 test_smoother_reappearing_track_keeps_history 验证了这一行为:第 1 帧有检测、第 2 帧空(轨道缺席)、第 3 帧目标重新出现。期望结果是——第 2 帧平滑输出长度为 0(不输出幽灵框),而第 3 帧输出的坐标 [1, 1, 11, 11] 与置信度 0.6 是跨越缺席帧、对第 1 帧与第 3 帧历史求均值得到的。这说明 None 占位只用于淘汰整窗空轨道,并不打断平滑连续性,对"短暂漏检后重现"的目标尤其友好。test_smoother_does_not_emit_missing_tracks 则从另一角度断言了"缺席不输出"这一点。
置信度的三种窗口情形
test_smoother_confidence_scenarios 用参数化用例覆盖了三种情形:
| 窗口内置信度分布 | 平滑结果 |
|---|---|
| 两帧均有(0.5、0.7) | 均值 0.6 |
| 两帧均无 | None |
| 混合(0.5、无) | 仅对存在的帧取均值,得 0.5 |
test_smoother_window_full_averages_all_frames 进一步验证满窗(length=3,坐标 0/3/6、置信度 0.3/0.6/0.9)时是对全部 3 帧而非最近 2 帧求平均,输出恰为中间帧的坐标 [3, 3, 13, 13] 与置信度 0.6——这正是"均值平滑 = 移动平均滤波"的直观体现。
旋转框(OBB)的平滑
仓库较新的一个能力是:当 Detections.data 中带有旋转框角点(ORIENTED_BOX_COORDINATES)时,smoother 会把角点与轴对齐框一起平滑,保证两者描述同一位置。实现要点:
- 仅当窗口内所有有效帧都携带形状一致的角点时才平滑角点,否则丢弃该字段、回退为普通
xyxy平滑(test_mixed_oriented_box_metadata_is_dropped验证混合窗口不残留陈旧 OBB 数据); - 求均值前先做角点对齐:不同帧的角点起点与绕行方向可能不同,
_align_oriented_corners(smoother.py 第 13–25 行)通过枚举 4 个循环移位 × 2 个方向(正序/逆序)共 8 种候选,选取与参考帧欧氏距离最小的对齐方式,test_corner_order_is_aligned_before_averaging专门验证了循环移位等价角点不会把正方形"搅成"交叉形; - 平滑角点算出后,
xyxy由角点包络(xyxyxyxy_to_xyxy)重新派生,保证两者严格一致(test_corners_agree_with_the_smoothed_box、test_rotated_rectangle_keeps_xyxy_and_obb_envelopes_consistent)。测试类注释说明这修复的问题:此前其余字段都拷贝自窗口最早帧,导致角点与平滑后的轴对齐框位置互相矛盾; - 普通轴对齐检测不受影响,不会凭空多出 OBB 键(
test_detections_without_oriented_boxes_are_unaffected)。
重置语义
test_reset_clears_track_history 验证 reset() 之后输出不再受旧框污染(新轨道首帧输出即原始框);test_reset_preserves_window_length 验证重置后新轨道的 deque.maxlen 仍等于构造时的 length(2),即重置只清数据、不清配置。
实践建议与适用边界
综合源码与测试,使用 DetectionsSmoother 时的几点建议:
length与帧率的换算:默认 5 帧,在 30 fps 视频中约覆盖 1/6 秒的历史。目标运动越快,越应减小length以免标注框"拖在"目标后面;目标基本静止(如固定设备计数)时则可适当加大。- 追踪器是前置依赖:没有稳定
tracker_id时平滑无从谈起,建议先调好追踪器参数(如激活阈值、丢失缓冲帧数)再叠加平滑。 - 注意输出滞后:均值平滑本质上引入滞后,若下游需要"当前时刻"的精确位置(如测速),应评估滞后带来的偏差,或缩短窗口。
- 不适用场景:分割 mask 输出(官方明确不兼容)、以及需要逐帧实时响应的低延迟场景。
- 多路复用:同一实例复用于不同流前调用
reset(),避免上一流的轨迹污染新一流的平滑结果。
DetectionsSmoother 以极小的 API 面(构造、逐帧更新、按轨查询、重置)实现了"滑窗均值 + 缺席容忍 + 字段一致性"三件事,是 supervision 检测工具集中用于压制视频标注抖动的标准组件;其每帧 O(n_tracks) 的更新开销主要来自活跃 ID 的集合判断与各轨道一次窗口均值,窗口长度 length 又通常很小,因此叠加进检测管线的成本可以忽略。
参考路径
- 文档入口:docs/detection/tools/smoother.md
- 核心实现:src/supervision/detection/tools/smoother.py
- 单元测试(含置信度、缺席轨道、重置、旋转框各场景):tests/detection/tools/test_smoother.py
- 追踪 + 平滑的完整用法:docs/how_to/track_objects.md
- 合并/选取等基础
Detections方法:src/supervision/detection/core.py
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 StartedRust0622
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