Supervision Draw Utils 完全指南:掌握底层绘图 API,用代码亲手画出检测与跟踪可视化
本文以 supervision 项目中的绘图工具模块为核心,系统梳理 supervision.draw.utils 与 supervision.draw.color 提供的全部底层绘制原语(线段、矩形、多边形、文字、图像叠加等)与颜色体系。读完本文,你将掌握每个 API 的参数语义、默认值与在 BGR 帧(NumPy 数组)上的就地绘制行为,并能基于这些原语构建自己的自定义可视化,或深入理解上层 Annotator 的工作机理。
从参考文档说起:本文覆盖的 API 清单
本仓库的 docs/utils/draw.md 是一份 MkDocs 自动生成的 API 参考页,它通过 :::supervision.draw.utils.xxx 与 :::supervision.draw.color.xxx 指令将源码中的 docstring 渲染为文档。因此,该页面的真实内容即源码中的完整说明与示例。按文档骨架,本文要讲解的对象分为三类:
- 绘制函数(utils.py):
draw_line、draw_rectangle、draw_filled_rectangle、draw_polygon、draw_filled_polygon、draw_text、draw_image; - 分辨率自适应辅助函数:
calculate_optimal_text_scale、calculate_optimal_line_thickness; - 颜色基础类(color.py):
Color与ColorPalette。
这些符号在包的顶层 src/supervision/init.py(from supervision.draw.utils import ...、from supervision.draw.color import Color, ColorPalette,见第 117–127 行与第 185、243–257 行)中被统一重导出,因此日常使用时既可以写 sv.draw_line(...),也可以像官方示例那样从子模块精确导入。
动手前的三个基础概念
1. 场景(scene):绘制目标就是 NumPy 图像数组
所有绘制函数的第一个参数都叫 scene,类型为 npt.NDArray[np.uint8]。最典型的场景是 OpenCV 读取的 BGR 彩色帧,shape 为 (H, W, 3);docstring 同时声明灰度图((H, W))也可作为 draw_text 的输入。构建一张空白测试场景最直接的方式就是 NumPy 零数组:
import numpy as np
scene = np.zeros((100, 100, 3), dtype=np.uint8)
2. 坐标系与几何基元:Point 与 Rect
draw_line 的起点终点、draw_text 的锚点都是 Point;draw_rectangle、draw_filled_rectangle、draw_image 的绘制区域则是 Rect。它们定义在 geometry/core.py:
Point(x, y):持有浮点坐标,通过as_xy_int_tuple()转成(int x, int y)供 OpenCV 使用(第 31–61 行);Rect(x, y, width, height):以左上角 + 宽高定义矩形,提供top_left、bottom_right角点属性、pad(padding)(四边同时外扩)以及as_xyxy_int_tuple()(转(x_min, y_min, x_max, y_max)),见第 146–209 行。
例如画矩形所需的区域与文字背景框计算都会用到 Rect 的角点与 pad 能力。
3. 颜色:永远是 RGBA,交给 OpenCV 时自动转 BGR
supervision 的颜色模型以 RGBA 表示,而在内部调用 OpenCV 时通过 color.as_bgr() 转成 BGR 元组(例如 utils.py 中 cv2.line(..., color.as_bgr(), ...))。下面的小节详解颜色体系。
Color:RGBA 颜色对象与构造/转换方法
Color 是一个 @dataclass(color.py 第 63–413 行),字段为 r, g, b(0–255)以及默认 255(不透明)的 a。直接在 Color(...) 构造时,__post_init__ 会校验四个通道都必须落在 0–255,否则抛出 ValueError。
预设颜色常量
| 常量 | Hex 值 | RGB 元组 |
|---|---|---|
WHITE |
#FFFFFF |
(255, 255, 255) |
BLACK |
#000000 |
(0, 0, 0) |
GREY |
#808080 |
(128, 128, 128) |
RED |
#FF0000 |
(255, 0, 0) |
GREEN |
#00FF00 |
(0, 255, 0) |
BLUE |
#0000FF |
(0, 0, 255) |
YELLOW |
#FFFF00 |
(255, 255, 0) |
ROBOFLOW |
#A351FB |
(163, 81, 251) |
这些常量都是 classproperty(见 color.py 第 366–396 行的 WHITE/BLACK/GREY/RED/GREEN/BLUE/YELLOW/ROBOFLOW 定义),使用时无需实例化:
>>> import supervision as sv
>>> sv.Color.WHITE
Color(r=255, g=255, b=255)
值得注意的是,几乎所有绘制函数的颜色参数默认值都是 Color.ROBOFLOW(品牌紫色 #A351FB),这与 supervision 默认标注配色保持一致。
从 Hex 字符串构造(支持 3/4/6/8 位缩写)
Color.from_hex(color_hex) 接受带或不带 # 前缀的十六进制字符串。校验规则见 _validate_color_hex(color.py 第 55–60 行):只能包含合法十六进制字符,长度必须是 3、4、6、8 之一。3/4 位短码会将每个字符翻倍展开成完整 hex,因此 #f0f 等价于 #ff00ff,#f0f8 等价于 #ff00ff88:
>>> sv.Color.from_hex('#ff00ff')
Color(r=255, g=0, b=255)
>>> sv.Color.from_hex('#f0f')
Color(r=255, g=0, b=255)
>>> sv.Color.from_hex('#ff00ff80') # 8 位时末两位是 alpha
Color(r=255, g=0, b=255, a=128)
从元组构造与转换输出
除 Hex 外,还提供一组对称的构造器与转换器:
- 构造器:
from_rgb_tuple((r, g, b))、from_bgr_tuple((b, g, r))、from_rgba_tuple((r, g, b, a))、from_bgra_tuple((b, g, r, a)),均会校验通道范围(越界抛ValueError); - 转换器:
as_hex()(alpha 非 255 时输出#RRGGBBAA)、as_rgb()、as_bgr()、as_rgba()、as_bgra(); __hash__/__eq__基于四个通道实现,因此Color可作为集合元素或字典键,便于按颜色聚合标注结果。
>>> sv.Color.from_rgb_tuple((255, 255, 0))
Color(r=255, g=255, b=0)
>>> sv.Color(r=255, g=255, b=0).as_bgr()
(0, 255, 255)
>>> sv.Color(r=255, g=0, b=255, a=128).as_hex()
'#ff00ff80'
ColorPalette:成组取色与按索引循环取色
当需要为多个检测框、多个类别或轨迹点分配不同颜色时,使用 ColorPalette(color.py 第 416–570 行),内部即 colors: list[Color]。
内置三套色板
ColorPalette.DEFAULT:21 色默认色板,源码常量DEFAULT_COLOR_PALETTE(color.py 第 8–30 行),首色即#A351FB;ColorPalette.ROBOFLOW:6 色 Roboflow 紫色系渐变色板,常量ROBOFLOW_COLOR_PALETTE(第 52 行),从#C28DFC渐变到#4D049A;ColorPalette.LEGACY:17 色旧版色板,常量LEGACY_COLOR_PALETTE(第 32–50 行),供迁移期兼容使用。
构造与使用
>>> import supervision as sv
>>> colors = ['#ff0000', '#00ff00', '#0000ff']
>>> color_palette = sv.ColorPalette.from_hex(colors)
>>> color_palette.by_idx(1) # 按索引取色
Color(r=0, g=255, b=0)
by_idx(idx)(color.py 第 537–561 行)的实现细节值得注意:它用 idx % len(self.colors) 取模,因此负数索引与越界索引都会被自动回绕,比如调色板只有 3 色时 by_idx(3) 会取回第 0 号颜色。这在为数量不确定的目标循环分配颜色时非常安全。空调色板调用会抛 ValueError。
此外还有 ColorPalette.from_matplotlib(palette_name, color_count)(color.py 第 489–535 行):内部延迟导入 matplotlib.pyplot(注释明确说明是为了「import 颜色工具时不引入 matplotlib」),从指定 colormap 等距采样 color_count 个颜色。若 color_count < 1 会抛 ValueError;对 viridis 之类非 ListedColormap 对象按 0–1 等距位置取色,单色请求则取调色板起始色以避免除零。
线段与矩形绘制:draw_line、draw_rectangle、draw_filled_rectangle
draw_line —— 画线段
draw_line(scene, start: Point, end: Point,
color: Color = Color.ROBOFLOW, thickness: int = 2) -> scene
在场景上从 start 到 end 绘制线段。内部调用 cv2.line(scene, start.as_xy_int_tuple(), end.as_xy_int_tuple(), color.as_bgr(), thickness=thickness)(utils.py 第 47–53 行)。
>>> import numpy as np
>>> from supervision.draw.utils import draw_line
>>> from supervision.draw.color import Color
>>> from supervision.geometry.core import Point
>>> scene = np.zeros((100, 100, 3), dtype=np.uint8)
>>> scene = draw_line(
... scene, start=Point(x=10, y=10), end=Point(x=90, y=90), color=Color.RED
... )
>>> scene.shape
(100, 100, 3)
Point 的浮点坐标会被截断取整后传给 OpenCV;result is scene,即函数就地修改并返回同一个数组对象(测试 test_draw_line_modifies_scene 断言了 result is scene 与像素确实变化,见 tests/draw/test_utils.py 第 153–165 行)。
draw_rectangle —— 画空心矩形边框
draw_rectangle(scene, rect: Rect, color: Color = Color.ROBOFLOW,
thickness: int = 2) -> scene
以 Rect 为区域绘制矩形边框,内部用 rect.top_left / rect.bottom_right 两个角点调用 cv2.rectangle(utils.py 第 89–95 行)。thickness 指定边框像素宽度,测试表明 1 像素边框不会污染矩形内部(tests/draw/test_utils.py 第 200–210 行断言内点保持 [0,0,0])。
>>> from supervision.draw.utils import draw_rectangle
>>> from supervision.geometry.core import Rect
>>> scene = np.zeros((100, 100, 3), dtype=np.uint8)
>>> rect = Rect(x=10, y=10, width=50, height=30)
>>> scene = draw_rectangle(scene, rect, color=Color.RED)
draw_filled_rectangle —— 填充矩形(支持透明度)
draw_filled_rectangle(scene, rect: Rect, color: Color = Color.ROBOFLOW,
opacity: float = 1) -> scene
多了一个 opacity 参数(0–1,默认 1 表示完全不透明)。其实现(utils.py 第 131–150 行)体现了两种路径:
opacity == 1:直接cv2.rectangle(..., thickness=-1)实心填充,效率最高;opacity < 1:先把填充画到scene.copy()的副本上,再用cv2.addWeighted(scene_with_annotations, opacity, scene, 1 - opacity, gamma=0, dst=scene)做加权混合,dst=scene保证结果仍写回原数组。
>>> scene = draw_filled_rectangle(scene, rect, color=Color.RED, opacity=0.5)
测试 test_draw_filled_rectangle_fills_interior(opacity=1 时内点变纯白)与 test_draw_filled_rectangle_opacity_blends(opacity=0.5 时混合像素既不是黑也不是白)分别验证了这两条路径(tests/draw/test_utils.py 第 218–242 行)。
补充说明:源码中还有
draw_rounded_rectangle(scene, rect, color, border_radius)(utils.py 第 155–234 行),即带圆角的实心矩形,实现上先按min(border_radius, min(width, height)//2)对半径做钳制,再画两个矩形加四个圆角圆。它虽不在 docs/utils/draw.md 参考页的清单中,但已被上层标注器使用(见下节),并配套了圆角像素级测试(tests/draw/test_utils.py 第 86–145 行)。
多边形绘制:draw_polygon 与 draw_filled_polygon
draw_polygon —— 闭合多边形轮廓
draw_polygon(scene, polygon: npt.NDArray[np.int_],
color: Color = Color.ROBOFLOW, thickness: int = 2) -> scene
polygon 是顶点数组,例如 np.array([[x1, y1], [x2, y2], ...])(整数类型)。内部调用 cv2.polylines(scene, [polygon], isClosed=True, color=..., thickness=...),isClosed=True 意味着首尾顶点自动相连构成闭合轮廓(utils.py 第 267–269 行)。
>>> import numpy as np
>>> from supervision.draw.utils import draw_polygon
>>> scene = np.zeros((100, 100, 3), dtype=np.uint8)
>>> polygon = np.array([[10, 10], [90, 10], [90, 90], [10, 90]])
>>> scene = draw_polygon(scene, polygon, color=Color.RED)
>>> scene.shape
(100, 100, 3)
draw_filled_polygon —— 填充多边形(支持透明度)
draw_filled_polygon(scene, polygon: npt.NDArray[np.int_],
color: Color = Color.ROBOFLOW, opacity: float = 1) -> scene
与 draw_filled_rectangle 保持完全一致的设计:opacity == 1 时直接 cv2.fillPoly 实心填充;否则先在副本上 fillPoly,再用 cv2.addWeighted 按透明度混合回原图(utils.py 第 303–310 行)。这一 API 同形性让使用者无需记忆两套透明度语义。
>>> scene = draw_filled_polygon(scene, polygon, color=Color.RED, opacity=0.5)
典型应用:半透明多边形常用来标注 ROI 区域(如禁入区、统计区),既能看清区域边界又不完全遮挡底下的画面内容。
draw_text:带可选背景框与抗锯齿的文字绘制
draw_text(
scene, text: str, text_anchor: Point,
text_color: Color = Color.BLACK,
text_scale: float = 0.5,
text_thickness: int = 1,
text_padding: int = 10,
text_font: int = cv2.FONT_HERSHEY_SIMPLEX,
background_color: Color | None = None,
) -> scene
参数语义(utils.py 第 315–395 行):
text_anchor:文字的锚点位置,文字会被以锚点为中心放置(见下方几何计算);text_color:文字颜色,默认黑色;text_scale:字号缩放,默认 0.5,通常结合分辨率自适应函数(见后文)取值;text_thickness:笔画粗细,默认 1;text_padding:当绘制背景框时文字四周留白(像素),默认 10;text_font:OpenCV 字体,默认cv2.FONT_HERSHEY_SIMPLEX;background_color:非None时先绘制实心背景矩形,再叠文字,实现「标签条」效果。
其底层几何流程非常值得拆解(utils.py 第 364–394 行):
- 用
cv2.getTextSize(text, fontFace, fontScale, thickness)量出文字宽高; - 以
text_anchor为中心构造文字矩形Rect(x=ax - text_width//2, y=ay - text_height//2, width=text_width, height=text_height),然后.pad(text_padding)四边外扩得到背景框; - 若
background_color非空,调用上文的draw_filled_rectangle画背景; cv2.putText使用lineType=cv2.LINE_AA(抗锯齿)绘制文字。
>>> import numpy as np
>>> from supervision.geometry.core import Point
>>> from supervision.draw.utils import draw_text
>>> scene = np.zeros((100, 100, 3), dtype=np.uint8)
>>> text_anchor = Point(x=50, y=50)
>>> scene = draw_text(scene=scene, text="Hello, world!", text_anchor=text_anchor)
>>> scene.shape
(100, 100, 3)
# 带背景色的典型标注写法
>>> from supervision.draw.color import Color
>>> scene = draw_text(
... scene=scene, text="person 0.92", text_anchor=Point(x=50, y=20),
... text_scale=1.0, text_color=Color.WHITE, background_color=Color.RED,
... )
测试验证了调用后场景被修改且 shape 保持不变(tests/draw/test_utils.py 第 250–272 行)。
draw_image:把图片(文件或数组)按透明度叠加进场景
draw_image(scene, image: str | npt.NDArray[np.uint8],
opacity: float, rect: Rect) -> scene
用于把另一张图片作为水印/角标/信息卡片叠加到场景指定区域,例如在画面角落贴 logo 或统计信息小图。
输入与校验(utils.py 第 436–465 行)——这是函数中防御逻辑最密集的一处:
- 若
image是路径字符串:文件不存在抛FileNotFoundError;存在但解码失败(cv2.imread返回None)抛OSError;测试用例test_draw_image_invalid_path_raises_oserror用伪图片文件验证了这一分支(tests/draw/test_utils.py 第 18–31 行); image_np.ndim != 3或通道数不在(3, 4)中抛ValueError("Image must have 3 or 4 channels.")——灰度图(2 维)会被拒绝,测试同样覆盖了「文件路径」与「内存数组」两种灰度输入(第 53–83 行);opacity不在[0.0, 1.0]抛ValueError;- 目标
rect越出场景边界(x<0、y<0、右下角超出scene.shape)抛ValueError("Invalid rectangle dimensions.")。
混合算法(utils.py 第 467–487 行):
- 用
cv2.resize把待叠加图缩放到rect的宽高; - 若图片是 4 通道 RGBA/BGRA,则取第 4 通道作为天然 alpha 蒙版;3 通道图则生成全 255 的 alpha 平面;
- 用
cv2.convertScaleAbs(alpha_channel * opacity)把「图片自身 alpha × 用户 opacity」合并成最终透明度; - 在场景对应 ROI 上逐像素做 alpha 混合后写回原图。
因此 draw_image 既支持直接传内存数组(测试中通过 np.full((40, 40, 3), 255) 模拟),也支持传文件路径,且对带透明通道的 PNG 能自动保留其透明信息。
分辨率自适应辅助:两个 calculate_optimal_* 函数
当可视化画面分辨率变化时(如 720p/1080p 视频流),硬编码的字号和线宽会导致小图糊成一片、大图又看不清。supervision 为此提供了两个启发式函数,都只依赖分辨率较短边:
calculate_optimal_text_scale(resolution_wh: tuple[int, int]) -> float
实现为 min(resolution_wh) * 1e-3(utils.py 第 514 行),即字号与较短边成正比:
>>> import supervision as sv
>>> sv.calculate_optimal_text_scale((1920, 1080))
1.08
>>> sv.calculate_optimal_text_scale((640, 480))
0.48
calculate_optimal_line_thickness(resolution_wh: tuple[int, int]) -> int
实现为分段常量:较短边 < 1080 时返回 2,否则返回 4(utils.py 第 539–541 行):
>>> sv.calculate_optimal_line_thickness((1920, 1080))
4
>>> sv.calculate_optimal_line_thickness((640, 480))
2
典型用法是取帧后先算一次,再传给各绘制函数,实现「分辨率不变式」的标注体验:
h, w = frame.shape[:2]
text_scale = sv.calculate_optimal_text_scale((w, h))
thickness = sv.calculate_optimal_line_thickness((w, h))
源码级统一设计:这些原语如何支撑上层标注器
纵观 utils.py 的全部实现,可以总结出四条贯穿性的设计约定(均有源码依据):
- 就地绘制、返回同一数组:所有函数直接修改传入的
scene并return scene(测试多次用result is scene断言),适合流式视频处理——逐帧就地叠加标注,无需反复拷贝大数组; - 颜色统一走
Color/as_bgr():从不在业务代码里直接写裸 BGR 三元组,保证颜色语义(RGBA 世界观)与 OpenCV 落地(BGR)解耦; - 透明统一走
opacity + addWeighted:四个「填充/叠加」类函数共享同一套透明度混合心智模型; - 对外只暴露 NumPy 图像接口:不感知具体推理框架的
Detections对象,因此既可在顶层直接使用,也可作为其他模块的积木。
关于最后一点,可在上层标注器代码中直接看到证据: annotators/core.py 第 38–40 行从 supervision.draw.utils 导入了 draw_polygon、draw_rounded_rectangle、draw_text,从 supervision.draw.color 导入 Color、ColorPalette,并在内部(如第 720 行画多边形、第 3599 行画文字)调用它们实现各类 Box/多边形标注器。换言之,本文讲解的函数就是全套标注器可视化的最小公共原语——理解它们,等于理解了所有 Annotator 的绘制内核。
进阶组合示例:一个完整的自定义标注流程
综合全篇 API,拼一段接近实战的「检测框 + 类别标签 + 置信度 + 半透明 ROI」标注逻辑(BGR 场景帧来自任何检测流程):
import numpy as np
import supervision as sv
from supervision.geometry.core import Point, Rect
frame = np.zeros((720, 1280, 3), dtype=np.uint8) # 模拟一帧 720p 场景
w, h = 1280, 720
text_scale = sv.calculate_optimal_text_scale((w, h))
thickness = sv.calculate_optimal_line_thickness((w, h))
# 1) 为多个目标从色板循环取色
palette = sv.ColorPalette.ROBOFLOW
for i, (xyxy, label) in enumerate(detections_for_frame):
x1, y1, x2, y2 = xyxy
color = palette.by_idx(i)
# 2) 检测框 + 文字标签
sv.draw_rectangle(frame, Rect(x=x1, y=y1, width=x2 - x1, height=y2 - y1),
color=color, thickness=thickness)
sv.draw_text(frame, text=label, text_anchor=Point(x=(x1 + x2) // 2, y=y1),
text_scale=text_scale, text_color=sv.Color.WHITE,
background_color=color)
# 3) 半透明禁入区域
roi = np.array([[x1, y1], [x2, y1], [x2, y2], [x1, y2]])
sv.draw_filled_polygon(frame, roi, color=sv.Color.ROBOFLOW, opacity=0.3)
(detections_for_frame 在此仅为示意数据源,实际检测框坐标可从任意检测器得到。)这段代码演示了本文全部核心要素:Color/ColorPalette 取色、按索引回绕、Point/Rect 几何构造、空心与填充绘制的透明度控制、抗锯齿带背景文字,以及两个分辨率自适应函数的实际组合。
小结
本文基于 docs/utils/draw.md 参考页骨架,逐一展开 supervision 绘图工具箱中 7 个绘制函数、2 个自适应辅助函数与 Color/ColorPalette 两个颜色类。它们统一以 NumPy 数组为场景、以 Point/Rect 描述几何、以 RGBA 世界观管理颜色、就地修改并返回原数组,是项目全部高层标注器共享的底层绘制内核。想要继续深入,可对照 src/supervision/draw/utils.py 与 src/supervision/draw/color.py 的完整实现,以及 tests/draw/test_utils.py、tests/draw/test_color.py 中逐像素级的测试断言;想观察这些原语在「大图标注」场景下的成品效果,则可参考 docs/detection/annotators.md 等上层标注器文档。
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 StartedRust0627
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