Roboflow Supervision 学习笔记:YOLO 识别、追踪、区域判断、视频流处理

1. supervision 是什么

supervision 是 Roboflow 开源的 Python 计算机视觉工具库。

它本身不是一个 YOLO 模型,也不是必须依赖云端的服务。

可以把它理解成:

1
2
3
4
5
6
7
YOLO / 其他检测模型

输出检测框、类别、置信度

supervision.Detections

过滤、追踪、画框、计数、区域判断、视频保存

也就是说:

1
2
模型负责识别
supervision 负责把识别结果变成可用的业务逻辑

例如 YOLO 每一帧会输出:

1
2
3
bbox: 框坐标
class_id: 类别 ID
confidence: 置信度

supervision 可以继续帮你做:

1
2
3
4
5
6
7
8
9
画框
显示标签
显示轨迹
给目标分配 tracker_id
判断是否进入某个区域
判断是否越过某条线
统计数量
保存视频
处理摄像头 / RTSP / 本地视频

2. 使用 uv 创建项目

1
2
3
4
5
mkdir supervision-demo
cd supervision-demo

uv init
uv add ultralytics supervision opencv-python numpy

如果你没有自己的模型,可以先用官方 YOLO 小模型测试:

1
yolov8n.pt

如果你已经训练好了模型,直接把:

1
YOLO("yolov8n.pt")

改成:

1
YOLO("best.pt")

3. supervision 常用功能总览

功能 作用 典型场景
画框 / 标签 / 轨迹 / 热力图 把检测结果画到画面上 调试、监控画面、录像回放
过滤检测结果 按类别、置信度、面积、框大小、区域过滤 只看某类物品、过滤误检、小目标过滤
ByteTrack 追踪 给连续帧里的同一个物体分配固定 ID 人员追踪、车辆追踪、商品追踪
LineZone 越线计数 判断物体是否穿过一条线,并统计进出数量 门口进出、传送带计数、车辆通行
PolygonZone 区域判断 判断物体是否在一个多边形区域内 货架区域、危险区域、工作区检测
InferenceSlicer 切图推理 把大图切成小块推理,再合并结果 小物体检测、高清摄像头画面
DetectionsSmoother 平滑框 减少检测框抖动 画面更稳定,追踪显示更自然
视频工具 读视频帧、写视频、统计 FPS 离线视频测试、保存检测后视频
数据集处理 / 格式转换 COCO、VOC、YOLO 等格式导入导出 训练数据整理、模型评估
模型评估指标 mAP、Precision、Recall、F1 等 比较不同 YOLO 模型效果

4. 核心数据结构:sv.Detections

YOLO 原始输出不能直接做很多业务判断,所以通常要先转成 sv.Detections

1
2
3
import supervision as sv

detections = sv.Detections.from_ultralytics(result)

转完后,常用字段有:

1
2
3
4
5
detections.xyxy        # 框坐标,格式是 [x1, y1, x2, y2]
detections.class_id # 类别 ID
detections.confidence # 置信度
detections.tracker_id # 追踪 ID,需要经过 ByteTrack 后才有
detections.area # 检测框面积

完整流程通常是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
frame

YOLO 推理

sv.Detections.from_ultralytics(result)

过滤 detections

ByteTrack 追踪

区域判断 / 越线计数

画框 / 标签 / 轨迹

显示或保存

5. 最小核心处理函数

建议先写一个统一的 process_frame(),以后摄像头、RTSP、本地视频都可以复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import cv2
import numpy as np
import supervision as sv
from ultralytics import YOLO


# 1. 加载模型
# 测试用 yolov8n.pt
# 自己训练好的模型改成 best.pt
model = YOLO("yolov8n.pt")


# 2. 初始化追踪器和标注器
tracker = sv.ByteTrack()
smoother = sv.DetectionsSmoother()

box_annotator = sv.BoundingBoxAnnotator()
label_annotator = sv.LabelAnnotator()
trace_annotator = sv.TraceAnnotator()


# 3. 可配置参数
CONFIDENCE_THRESHOLD = 0.35

# 只看指定类别时使用,例如 COCO 里 person 是 0
# 不限制类别就设为 None
CLASS_WHITELIST = None
# CLASS_WHITELIST = [0]


def detect(frame):
"""
对单帧图像做 YOLO 检测,并转成 supervision.Detections
"""
result = model(frame, verbose=False)[0]

detections = sv.Detections.from_ultralytics(result)

# 按置信度过滤
detections = detections[detections.confidence >= CONFIDENCE_THRESHOLD]

# 按类别过滤
if CLASS_WHITELIST is not None and len(detections) > 0:
detections = detections[np.isin(detections.class_id, CLASS_WHITELIST)]

return detections


def build_labels(detections):
"""
给每个框生成显示文字
"""
labels = []

for class_id, confidence, tracker_id in zip(
detections.class_id,
detections.confidence,
detections.tracker_id,
):
class_name = model.names[int(class_id)]

if tracker_id is None:
label = f"{class_name} {confidence:.2f}"
else:
label = f"#{tracker_id} {class_name} {confidence:.2f}"

labels.append(label)

return labels


def process_frame(frame):
"""
输入一帧图像,输出标注后的图像和 detections
"""
detections = detect(frame)

# 追踪,生成 tracker_id
detections = tracker.update_with_detections(detections)

# 平滑检测框,减少视频画面抖动
# 注意:DetectionsSmoother 依赖 tracker_id
detections = smoother.update_with_detections(detections)

labels = build_labels(detections)

annotated_frame = frame.copy()

# 画轨迹
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,
)

return annotated_frame, detections

6. 怎么传入摄像头

摄像头最常见就是 OpenCV:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import cv2

cap = cv2.VideoCapture(0)

if not cap.isOpened():
print("无法打开摄像头")
exit()

while True:
ret, frame = cap.read()

if not ret:
print("读取摄像头失败")
break

annotated_frame, detections = process_frame(frame)

cv2.imshow("Camera Demo", annotated_frame)

# 按 q 退出
if cv2.waitKey(1) & 0xFF == ord("q"):
break

cap.release()
cv2.destroyAllWindows()

运行:

1
uv run python main.py

7. 怎么传入 RTSP 流

RTSP 也是用 OpenCV 的 VideoCapture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import cv2

rtsp_url = "rtsp://username:password@192.168.1.100:554/stream1"

cap = cv2.VideoCapture(rtsp_url)

if not cap.isOpened():
print("无法打开 RTSP 流")
exit()

while True:
ret, frame = cap.read()

if not ret:
print("RTSP 读取失败,可能是网络断开或地址错误")
break

annotated_frame, detections = process_frame(frame)

cv2.imshow("RTSP Demo", annotated_frame)

if cv2.waitKey(1) & 0xFF == ord("q"):
break

cap.release()
cv2.destroyAllWindows()

边缘设备上如果没有桌面环境,cv2.imshow() 可能不能用,可以改成:

1
2
3
4
1. 保存视频
2. 推送到 Web 页面
3. 推送到 RTSP
4. 只输出检测事件到后端

8. 怎么传入本地视频并保存结果

本地视频推荐用 supervision 自带的视频工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import supervision as sv

SOURCE_VIDEO_PATH = "source.mp4"
TARGET_VIDEO_PATH = "result.mp4"

video_info = sv.VideoInfo.from_video_path(SOURCE_VIDEO_PATH)
frames_generator = sv.get_video_frames_generator(SOURCE_VIDEO_PATH)

with sv.VideoSink(
target_path=TARGET_VIDEO_PATH,
video_info=video_info,
) as sink:
for frame in frames_generator:
annotated_frame, detections = process_frame(frame)
sink.write_frame(frame=annotated_frame)

print("处理完成:", TARGET_VIDEO_PATH)

这个适合:

1
2
3
4
测试模型效果
离线分析视频
保存识别后的视频
做 demo 演示

9. 用 sv.process_video 简化本地视频处理

如果只是本地视频输入、本地视频输出,也可以用 sv.process_video()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import numpy as np
import supervision as sv


def callback(frame: np.ndarray, index: int) -> np.ndarray:
annotated_frame, detections = process_frame(frame)
return annotated_frame


sv.process_video(
source_path="source.mp4",
target_path="result.mp4",
callback=callback,
)

这个写法更简洁。

注意:

1
2
sv.process_video 更适合本地视频文件
摄像头 / RTSP 实时流更建议用 cv2.VideoCapture

10. 保存摄像头或 RTSP 流处理结果

摄像头和 RTSP 没有固定的视频文件信息,所以需要自己从第一帧取宽高和 FPS。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import cv2
import supervision as sv

SOURCE = 0
# SOURCE = "rtsp://username:password@192.168.1.100:554/stream1"

cap = cv2.VideoCapture(SOURCE)

if not cap.isOpened():
print("无法打开视频源")
exit()

fps = cap.get(cv2.CAP_PROP_FPS)
if fps is None or fps <= 1:
fps = 25

ret, frame = cap.read()
if not ret:
print("无法读取第一帧")
cap.release()
exit()

height, width = frame.shape[:2]

video_info = sv.VideoInfo(
width=width,
height=height,
fps=int(fps),
total_frames=None,
)

with sv.VideoSink(
target_path="stream_result.mp4",
video_info=video_info,
) as sink:
while True:
annotated_frame, detections = process_frame(frame)

sink.write_frame(frame=annotated_frame)

cv2.imshow("Stream Save Demo", annotated_frame)

if cv2.waitKey(1) & 0xFF == ord("q"):
break

ret, frame = cap.read()
if not ret:
break

cap.release()
cv2.destroyAllWindows()

11. 功能一:画框 / 标签 / 轨迹 / 热力图

11.1 画框和标签

1
2
3
4
5
6
7
8
9
10
11
12
13
box_annotator = sv.BoundingBoxAnnotator()
label_annotator = sv.LabelAnnotator()

annotated_frame = box_annotator.annotate(
scene=frame.copy(),
detections=detections,
)

annotated_frame = label_annotator.annotate(
scene=annotated_frame,
detections=detections,
labels=labels,
)

11.2 画轨迹

轨迹需要先有 tracker_id,所以要先经过 ByteTrack。

1
2
3
4
5
6
7
8
9
tracker = sv.ByteTrack()
trace_annotator = sv.TraceAnnotator()

detections = tracker.update_with_detections(detections)

annotated_frame = trace_annotator.annotate(
scene=frame.copy(),
detections=detections,
)

11.3 热力图

热力图适合分析一段时间内物体经常出现的位置。

1
2
3
4
5
6
heatmap_annotator = sv.HeatMapAnnotator()

annotated_frame = heatmap_annotator.annotate(
scene=frame.copy(),
detections=detections,
)

典型场景:

1
2
3
4
门店顾客停留区域
教室人员活动区域
车辆经常经过的位置
工厂高频作业区域

12. 功能二:过滤检测结果

YOLO 可能会识别出很多类别,但业务上通常只关心一部分。

12.1 按置信度过滤

1
detections = detections[detections.confidence > 0.5]

12.2 按单个类别过滤

例如只保留 class_id == 0 的目标:

1
detections = detections[detections.class_id == 0]

如果用 COCO 模型,通常:

1
2
3
4
person = 0
car = 2
bus = 5
truck = 7

12.3 按多个类别过滤

1
2
3
4
import numpy as np

selected_classes = [0, 2, 3]
detections = detections[np.isin(detections.class_id, selected_classes)]

12.4 按检测框面积过滤

过滤太小的误检框:

1
detections = detections[detections.area > 1000]

12.5 按框宽高过滤

1
2
3
4
w = detections.xyxy[:, 2] - detections.xyxy[:, 0]
h = detections.xyxy[:, 3] - detections.xyxy[:, 1]

detections = detections[(w > 50) & (h > 50)]

12.6 混合过滤

例如只保留:

1
2
3
置信度 > 0.6
类别是 person
面积 > 2000
1
2
3
4
5
detections = detections[
(detections.confidence > 0.6)
& (detections.class_id == 0)
& (detections.area > 2000)
]

13. 功能三:ByteTrack 追踪

ByteTrack 的作用是:

1
2
3
4
第 1 帧看到一个人
第 2 帧这个人移动了一点
第 3 帧这个人继续移动
系统知道这是同一个目标

使用方式:

1
2
3
4
tracker = sv.ByteTrack()

detections = sv.Detections.from_ultralytics(result)
detections = tracker.update_with_detections(detections)

经过追踪后,detections.tracker_id 会有值。

例如标签可以这样写:

1
2
3
4
5
labels = [
f"#{tracker_id} {model.names[int(class_id)]} {confidence:.2f}"
for class_id, confidence, tracker_id
in zip(detections.class_id, detections.confidence, detections.tracker_id)
]

显示效果类似:

1
2
3
#1 person 0.86
#2 cup 0.72
#3 bottle 0.80

注意:

1
2
3
4
ByteTrack 是视频内追踪
不是跨天识别身份
不是人脸识别
不是长期身份识别

如果程序重启,ID 通常会重新分配。


14. 功能四:LineZone 越线计数

LineZone 用来判断目标是否穿过一条线。

典型场景:

1
2
3
4
门口进出人数
车辆进出
传送带物品计数
工厂流水线计数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import cv2
import supervision as sv

line_zone = sv.LineZone(
start=sv.Point(100, 400),
end=sv.Point(1100, 400),
)

tracker = sv.ByteTrack()


def process_frame_with_line(frame):
result = model(frame, verbose=False)[0]
detections = sv.Detections.from_ultralytics(result)

detections = tracker.update_with_detections(detections)

# 更新越线计数
crossed_in, crossed_out = line_zone.trigger(detections)

labels = build_labels(detections)

annotated_frame = frame.copy()
annotated_frame = box_annotator.annotate(
scene=annotated_frame,
detections=detections,
)
annotated_frame = label_annotator.annotate(
scene=annotated_frame,
detections=detections,
labels=labels,
)

# 手动画线和显示计数
cv2.line(
annotated_frame,
(100, 400),
(1100, 400),
(0, 255, 255),
2,
)

cv2.putText(
annotated_frame,
f"in: {line_zone.in_count} out: {line_zone.out_count}",
(30, 50),
cv2.FONT_HERSHEY_SIMPLEX,
1,
(0, 255, 255),
2,
)

return annotated_frame, detections

重点:

1
2
LineZone 依赖 tracker_id
所以要先 ByteTrack,再 line_zone.trigger(detections)

15. 功能五:PolygonZone 区域判断

PolygonZone 用来判断目标是否在指定多边形区域内。

典型场景:

1
2
3
4
货架区域有没有商品
危险区域有没有人
教室某个座位区域有没有人
仓库某个区域是否有叉车

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import numpy as np
import supervision as sv

# 画一个四边形区域
polygon = np.array([
[100, 100],
[600, 100],
[600, 500],
[100, 500],
])

# 假设视频分辨率是 1280x720
zone = sv.PolygonZone(
polygon=polygon,
frame_resolution_wh=(1280, 720),
)

zone_annotator = sv.PolygonZoneAnnotator(zone=zone)


def process_frame_with_polygon(frame):
result = model(frame, verbose=False)[0]
detections = sv.Detections.from_ultralytics(result)

detections = tracker.update_with_detections(detections)

# 判断哪些目标在区域内
mask = zone.trigger(detections=detections)

# 只保留区域内的目标
detections_in_zone = detections[mask]

labels = build_labels(detections_in_zone)

annotated_frame = frame.copy()

annotated_frame = box_annotator.annotate(
scene=annotated_frame,
detections=detections_in_zone,
)

annotated_frame = label_annotator.annotate(
scene=annotated_frame,
detections=detections_in_zone,
labels=labels,
)

annotated_frame = zone_annotator.annotate(
scene=annotated_frame,
label=f"count: {zone.current_count}",
)

return annotated_frame, detections_in_zone

注意:

1
2
PolygonZone 默认通常使用检测框的底部中心点判断目标是否在区域内
适合判断人、车、物体是否进入某个地面区域

如果是教室座位检测,可以给每个座位画一个 PolygonZone。

例如:

1
2
3
seat_01 区域有人 -> 学生 1 到课
seat_02 区域有人 -> 学生 2 到课
seat_03 区域有人 -> 学生 3 到课

16. 功能六:InferenceSlicer 切图推理

InferenceSlicer 适合小目标检测。

例如:

1
2
3
4
4K 摄像头画面
货架上的小商品
工业零件
远处的人或车

直接整张图送 YOLO,目标可能太小,容易漏检。

InferenceSlicer 会:

1
2
3
4
5
6
7
8
9
大图

切成多个小图

每个小图单独推理

把检测框合并回原图

做 NMS 去重

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import numpy as np
import supervision as sv
from ultralytics import YOLO

model = YOLO("best.pt")


def callback(image_slice: np.ndarray) -> sv.Detections:
result = model(image_slice, verbose=False)[0]
return sv.Detections.from_ultralytics(result)


slicer = sv.InferenceSlicer(
callback=callback,
slice_wh=(640, 640),
overlap_ratio_wh=(0.2, 0.2),
iou_threshold=0.5,
thread_workers=1,
)


def process_frame_with_slicer(frame):
detections = slicer(frame)

detections = detections[detections.confidence > 0.35]

labels = [
f"{model.names[int(class_id)]} {confidence:.2f}"
for class_id, confidence
in zip(detections.class_id, detections.confidence)
]

annotated_frame = box_annotator.annotate(
scene=frame.copy(),
detections=detections,
)

annotated_frame = label_annotator.annotate(
scene=annotated_frame,
detections=detections,
labels=labels,
)

return annotated_frame, detections

注意:

1
2
3
切图推理会更准,但会更慢
适合小目标,不一定适合强实时场景
边缘设备上要测试 FPS

17. 功能七:DetectionsSmoother 平滑框

YOLO 在视频里有时会出现框抖动:

1
2
3
这一帧框偏左
下一帧框偏右
再下一帧框变大

DetectionsSmoother 可以让框更稳定。

1
2
3
4
5
tracker = sv.ByteTrack()
smoother = sv.DetectionsSmoother()

detections = tracker.update_with_detections(detections)
detections = smoother.update_with_detections(detections)

注意:

1
2
DetectionsSmoother 依赖 tracker_id
所以必须先 ByteTrack

常见顺序:

1
2
3
4
5
6
7
8
9
10
11
YOLO 检测

Detections

过滤

ByteTrack

DetectionsSmoother

画框

18. 功能八:FPS 统计

边缘设备部署时,一定要看 FPS。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import cv2
import supervision as sv

fps_monitor = sv.FPSMonitor()

cap = cv2.VideoCapture(0)

while True:
ret, frame = cap.read()
if not ret:
break

annotated_frame, detections = process_frame(frame)

fps_monitor.tick()
fps = fps_monitor()

cv2.putText(
annotated_frame,
f"FPS: {fps:.1f}",
(30, 50),
cv2.FONT_HERSHEY_SIMPLEX,
1,
(0, 255, 0),
2,
)

cv2.imshow("FPS Demo", annotated_frame)

if cv2.waitKey(1) & 0xFF == ord("q"):
break

cap.release()
cv2.destroyAllWindows()

边缘设备上一般要关注:

1
2
3
4
5
6
输入分辨率
模型大小
是否使用 GPU
是否使用 TensorRT / ONNX / OpenVINO
是否需要切图
是否每帧都推理

19. 数据集处理 / 格式转换

这个功能主要用于训练前后的数据管理。

例如你有 YOLO 格式数据集,可以用 supervision 读取,然后做拆分、合并、导出等操作。

概念流程:

1
2
3
4
5
6
7
YOLO 数据集

supervision.DetectionDataset

数据集检查 / 合并 / 拆分

导出为 YOLO / COCO / VOC

这个和实时边缘部署关系不大,但是和模型训练关系很大。

如果你现在重点是摄像头识别和追踪,可以先不深入这个部分。


20. 模型评估指标

模型训练完成后,可以用指标比较不同模型效果:

1
2
3
4
mAP
Precision
Recall
F1 Score

简单理解:

1
2
3
4
Precision:识别出来的结果有多少是真的
Recall:真实目标里有多少被识别出来了
mAP:综合评估检测框和类别预测质量
F1:Precision 和 Recall 的平衡

业务上常见选择:

1
2
3
误报不能太多 -> 关注 Precision
漏检不能太多 -> 关注 Recall
整体检测效果 -> 关注 mAP

例如:

1
2
3
安全帽检测:漏检很危险,更关注 Recall
商品计数:误检会影响库存,更关注 Precision
车辆检测:Precision 和 Recall 都要看

21. 一个完整 main.py 示例:摄像头 / RTSP / 视频都能改

下面是一个比较完整的版本。

你只需要改 SOURCE

1
SOURCE = 0

可以用摄像头。

改成:

1
SOURCE = "rtsp://username:password@192.168.1.100:554/stream1"

可以用 RTSP。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import cv2
import numpy as np
import supervision as sv
from ultralytics import YOLO


MODEL_PATH = "yolov8n.pt"
SOURCE = 0
# SOURCE = "rtsp://username:password@192.168.1.100:554/stream1"

CONFIDENCE_THRESHOLD = 0.35
CLASS_WHITELIST = None
# CLASS_WHITELIST = [0] # 只检测 person


model = YOLO(MODEL_PATH)

tracker = sv.ByteTrack()
smoother = sv.DetectionsSmoother()

box_annotator = sv.BoundingBoxAnnotator()
label_annotator = sv.LabelAnnotator()
trace_annotator = sv.TraceAnnotator()

fps_monitor = sv.FPSMonitor()


def detect(frame):
result = model(frame, verbose=False)[0]
detections = sv.Detections.from_ultralytics(result)

detections = detections[detections.confidence >= CONFIDENCE_THRESHOLD]

if CLASS_WHITELIST is not None and len(detections) > 0:
detections = detections[np.isin(detections.class_id, CLASS_WHITELIST)]

return detections


def build_labels(detections):
labels = []

for class_id, confidence, tracker_id in zip(
detections.class_id,
detections.confidence,
detections.tracker_id,
):
class_name = model.names[int(class_id)]

if tracker_id is None:
labels.append(f"{class_name} {confidence:.2f}")
else:
labels.append(f"#{tracker_id} {class_name} {confidence:.2f}")

return labels


def process_frame(frame):
detections = detect(frame)

detections = tracker.update_with_detections(detections)
detections = smoother.update_with_detections(detections)

labels = build_labels(detections)

annotated_frame = frame.copy()

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,
)

fps_monitor.tick()
fps = fps_monitor()

cv2.putText(
annotated_frame,
f"FPS: {fps:.1f}",
(30, 50),
cv2.FONT_HERSHEY_SIMPLEX,
1,
(0, 255, 0),
2,
)

return annotated_frame, detections


def main():
cap = cv2.VideoCapture(SOURCE)

if not cap.isOpened():
print("无法打开视频源")
return

while True:
ret, frame = cap.read()

if not ret:
print("读取视频帧失败")
break

annotated_frame, detections = process_frame(frame)

cv2.imshow("Supervision Demo", annotated_frame)

if cv2.waitKey(1) & 0xFF == ord("q"):
break

cap.release()
cv2.destroyAllWindows()


if __name__ == "__main__":
main()

运行:

1
uv run python main.py

22. 完整视频文件处理示例

如果你要处理 source.mp4 并生成 result.mp4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import cv2
import numpy as np
import supervision as sv
from ultralytics import YOLO


MODEL_PATH = "yolov8n.pt"
SOURCE_VIDEO_PATH = "source.mp4"
TARGET_VIDEO_PATH = "result.mp4"

model = YOLO(MODEL_PATH)

tracker = sv.ByteTrack()
smoother = sv.DetectionsSmoother()

box_annotator = sv.BoundingBoxAnnotator()
label_annotator = sv.LabelAnnotator()
trace_annotator = sv.TraceAnnotator()


def process_frame(frame):
result = model(frame, verbose=False)[0]
detections = sv.Detections.from_ultralytics(result)

detections = detections[detections.confidence > 0.35]

detections = tracker.update_with_detections(detections)
detections = smoother.update_with_detections(detections)

labels = [
f"#{tracker_id} {model.names[int(class_id)]} {confidence:.2f}"
for class_id, confidence, tracker_id
in zip(detections.class_id, detections.confidence, detections.tracker_id)
]

annotated_frame = frame.copy()

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,
)

return annotated_frame


def main():
video_info = sv.VideoInfo.from_video_path(SOURCE_VIDEO_PATH)
frames_generator = sv.get_video_frames_generator(SOURCE_VIDEO_PATH)

with sv.VideoSink(
target_path=TARGET_VIDEO_PATH,
video_info=video_info,
) as sink:
for frame in frames_generator:
annotated_frame = process_frame(frame)
sink.write_frame(frame=annotated_frame)

print("视频处理完成:", TARGET_VIDEO_PATH)


if __name__ == "__main__":
main()

运行:

1
uv run python video_demo.py

23. 边缘设备上的推荐架构

边缘设备部署时,可以这样设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
摄像头 / RTSP

OpenCV 读取帧

YOLO 本地推理

sv.Detections

过滤

ByteTrack

LineZone / PolygonZone

业务事件

本地 SQLite / HTTP 上报 / MQTT 上报

例如教室人数统计:

1
2
3
4
5
6
7
8
9
摄像头

YOLO person 检测

ByteTrack 临时追踪

PolygonZone 判断座位区域

记录 seat_01 / seat_02 / seat_03 是否有人

例如传送带商品计数:

1
2
3
4
5
6
7
8
9
摄像头

YOLO 商品检测

ByteTrack 追踪

LineZone 判断是否过线

计数 +1

例如危险区域检测:

1
2
3
4
5
6
7
摄像头

YOLO person 检测

PolygonZone 判断是否进入危险区域

触发报警

24. 常见问题

24.1 supervision 是不是必须联网?

不是。

如果你使用本地 YOLO 权重,例如:

1
model = YOLO("best.pt")

就可以本地运行。

但是如果你用 Roboflow 云端 API 或 Roboflow Inference 的云模型,可能需要 API Key。


24.2 YOLO 拉框做好后是不是就能接进来?

是的。

只要 YOLO 能输出:

1
2
3
框坐标
类别
置信度

就可以转成:

1
detections = sv.Detections.from_ultralytics(result)

然后接入 supervision 的后处理能力。


24.3 ByteTrack 是不是人脸识别?

不是。

ByteTrack 只是视频内目标追踪。

它只能判断连续帧里哪个框大概率是同一个目标。

它不能跨天识别身份,也不能知道这个人是谁。


24.4 追踪 ID 会不会一直固定?

不会。

tracker_id 通常只在当前视频流或当前程序运行期间有效。

如果程序重启,ID 可能重新分配。

如果你要长期识别同一个人,需要额外身份机制,例如:

1
2
3
4
5
6
刷卡
扫码
固定座位
老师确认
人脸识别
人体 ReID

24.5 为什么检测到了但越线没有计数?

常见原因:

1
2
3
4
5
6
7
1. 没有先做 ByteTrack
2. detections.tracker_id 是 None
3. 检测框不稳定
4. 线画的位置不对
5. 目标没有真正穿过线
6. 视频 FPS 太低
7. 置信度阈值太高导致中间帧漏检

正确顺序:

1
2
3
4
5
6
7
YOLO

Detections

ByteTrack

LineZone.trigger()

24.6 为什么 PolygonZone 判断不准?

常见原因:

1
2
3
4
1. 多边形坐标写错
2. frame_resolution_wh 和真实视频分辨率不一致
3. 默认判断点是框的底部中心点
4. 摄像头角度导致人或物体框位置和地面区域不匹配

建议先把多边形区域画出来调试。


24.7 小物体检测不好怎么办?

可以尝试:

1
2
3
4
5
6
1. 提高输入分辨率
2. 训练更适合小目标的数据集
3. 使用 InferenceSlicer 切图推理
4. 降低置信度阈值
5. 使用更大的 YOLO 模型
6. 调整摄像头距离和角度

25. 推荐学习顺序

建议按这个顺序学习:

1
2
3
4
5
6
7
8
9
10
第一步:跑通 YOLO 单帧检测
第二步:转成 sv.Detections
第三步:画框和标签
第四步:接摄像头 / RTSP
第五步:接 ByteTrack 追踪
第六步:加 PolygonZone 区域判断
第七步:加 LineZone 越线计数
第八步:保存结果视频
第九步:优化 FPS
第十步:部署到边缘设备

26. 最重要的一句话

supervision 的重点不是训练模型,而是把模型输出的检测框变成业务结果。

核心公式:

1
2
3
4
YOLO 负责识别
supervision 负责后处理
OpenCV 负责读流和显示
你的业务代码负责保存、报警、统计、上报