supervision 中的 VLM 工具详解:将视觉语言模型输出解析为 Detections
本文基于 supervision 仓库中的 VLM Utils 文档 与对应源码展开。当你使用 PaliGemma、Qwen2.5-VL、Qwen3-VL、Google Gemini、Florence-2、DeepSeek-VL2、Moondream 等视觉语言模型(VLM)做零样本检测时,模型返回的是一段带特殊坐标标记的文本或 JSON 片段,而 supervision 的 Detections 需要标准的 xyxy 像素坐标数组。本文讲清这套“VLM 输出 → Detections”的解析管线:VLM/LMM 枚举、各模型专属解析函数、参数校验规则、坐标归一化约定,以及 edit_distance/fuzzy_match_index 两个字符串匹配工具的实现原理与用法。读完本文,你可以把任意受支持 VLM 的原始输出稳定地转换为可用于 BoxAnnotator 等标注器的检测结果。
整体架构:文档页面与源码模块的对应关系
VLM Utils 文档 是一个 mkdocs autodoc 页面,它通过 ::: 指令从四个符号的 docstring 自动渲染出 API 文档:
| 文档章节 | 源码位置 |
|---|---|
VLM |
src/supervision/detection/vlm.py |
LMM(已弃用) |
同上 |
validate_vlm_parameters(已弃用) |
同上 |
edit_distance |
src/supervision/detection/utils/vlms.py |
fuzzy_match_index |
同上 |
从源码结构看,文档只列出了“枚举 + 校验 + 字符串工具”这几个顶层符号,但真正干活的是 src/supervision/detection/vlm.py 中一组 from_* 解析函数(from_paligemma、from_qwen_2_5_vl、from_florence_2 等),它们被 src/supervision/detection/core.py 中的 Detections.from_vlm(及其已弃用的前身 Detections.from_lmm)按模型分发调用。edit_distance 与 fuzzy_match_index 则通过 src/supervision/init.py 导出为顶层 API,可直接写作 sv.edit_distance(...) 使用。
VLM 枚举:受支持的模型清单与结果类型约定
VLM 是一个 Enum,每个成员对应一个受支持的视觉语言模型,成员值是小写字符串,例如 sv.VLM.PALIGEMMA == "paligemma"。当前枚举包含以下成员(见 vlm.py):
| 枚举成员 | 字符串值 | 模型 | 结果类型 |
|---|---|---|---|
PALIGEMMA |
paligemma |
Google PaliGemma(含 PaliGemma 2) | str |
FLORENCE_2 |
florence_2 |
Microsoft Florence-2 | dict |
QWEN_2_5_VL |
qwen_2_5_vl |
阿里 Qwen2.5-VL | str |
QWEN_3_VL |
qwen_3_vl |
阿里 Qwen3-VL | str |
DEEPSEEK_VL_2 |
deepseek_vl_2 |
DeepSeek-VL2 | str |
GOOGLE_GEMINI_2_0 |
gemini_2_0 |
Google Gemini 2.0 | str |
GOOGLE_GEMINI_2_5 |
gemini_2_5 |
Google Gemini 2.5 | str |
GOOGLE_GEMINI_3_5 |
gemini_3_5 |
Google Gemini 3.5 | str |
MOONDREAM |
moondream |
Moondream | dict |
两个值得注意的类方法:
VLM.list()返回所有合法字符串值的列表,可用于生成命令行参数校验或交互式提示;VLM.from_value(value)接受VLM实例或小写字符串,内部先value.lower()再cls(value),非法值抛出ValueError。
LMM 是旧命名(Large Multimodal Models),自 supervision-0.27.0 起弃用,将在 supervision-0.31.0 移除,docstring 中明确写着 “Use VLM instead”。两者成员字符串值互为镜像,Detections.from_lmm 内部就是把 LMM 值转成同名的 VLM 再走统一分发(core.py)。新代码应直接使用 VLM。
参数校验:RESULT_TYPES、REQUIRED_ARGUMENTS 与 ALLOWED_ARGUMENTS
validate_vlm_parameters(现以私有函数 _validate_vlm_parameters 实现)是 from_vlm 入口的守门员。源码中用三张表声明式地定义了每个模型的调用契约(vlm.py):
RESULT_TYPES: dict[VLM, type] = {
VLM.PALIGEMMA: str,
VLM.FLORENCE_2: dict,
VLM.QWEN_2_5_VL: str,
# ... Moondream 为 dict,其余为 str
}
REQUIRED_ARGUMENTS: dict[VLM, list[str]] = {
VLM.PALIGEMMA: ["resolution_wh"],
VLM.QWEN_2_5_VL: ["input_wh", "resolution_wh"], # 唯一要求两个坐标空间的模型
VLM.MOONDREAM: ["resolution_wh"],
# 其余模型均只需 resolution_wh
}
ALLOWED_ARGUMENTS: dict[VLM, list[str]] = {
VLM.PALIGEMMA: ["resolution_wh", "classes"],
VLM.FLORENCE_2: ["resolution_wh"], # 不支持 classes 过滤
VLM.MOONDREAM: ["resolution_wh"], # 不支持 classes 过滤
# ...
}
校验逻辑(vlm.py)按顺序执行四步:
- 若传入字符串,先尝试
VLM(vlm.lower()),失败则报 “Invalid vlm value”; - 用
isinstance(result, RESULT_TYPES[vlm])检查模型输出的类型——这是 Florence-2 / Moondream(dict)与其他模型(str)最直观的差异; - 遍历
REQUIRED_ARGUMENTS[vlm],缺失任一必需参数即抛ValueError: Missing required argument: ...; - 遍历传入的
kwargs,任何不在ALLOWED_ARGUMENTS[vlm]中的参数都会抛ValueError: Argument ... is not allowed for ...,防止把classes误传给 Moondream 之类的模型。
resolution_wh 还有一层数值校验:_validate_resolution(src/supervision/validators/init.py)要求它是长度为 2 的整数元组且两个分量均为正数,否则抛出 Both dimensions in resolution must be positive. Got (w, h).——tests/detection/test_vlm.py 中对 (0, 480)、(640, -100) 等非法分辨率的用例正是在断言这条错误信息。
各模型解析函数:输出格式、坐标约定与解析细节
from_paligemma:<locXXXX> 标记与 1024 坐标系
PaliGemma 的输出是纯文本,每个检测形如 <locX1><locY1><locX2><locY2> 类名,坐标是 0–1024 区间内的整数(四位数零填充)。解析流程(vlm.py):
def from_paligemma(result: str, resolution_wh: tuple[int, int], classes: list[str] | None = None):
...
pattern = re.compile(
r"(?<!<loc\d{4}>)<loc(\d{4})><loc(\d{4})><loc(\d{4})><loc(\d{4})> ([\w\s\-]+)"
)
matches = pattern.findall(result)
...
xyxy_arr = np.array(matches_arr[:, [1, 0, 3, 2]], dtype=float)
xyxy_arr = xyxy_arr.astype(int) / 1024 * np.array([w, h, w, h])
要点:
- 正则用负向后顾
(?<!<loc\d{4}>)防止把五个连续<loc>标记中的错位片段误匹配为完整框,测试里 “extra loc” 用例(5 个 loc 标记)正是验证它返回空结果; - 捕获组顺序是
X1, Y1, X2, Y2,源码通过[:, [1, 0, 3, 2]]重排为xyxy; - 归一化因子是 1024:先还原 0–1024 坐标,再除以 1024 乘以目标分辨率;
- 类名允许空格、连字符和下划线(
[\w\s\-]+);提供classes时按名称精确过滤,并生成对应的class_id。
from_qwen_2_5_vl / from_qwen_3_vl:JSON 围栏、容错修复与双坐标系
Qwen2.5-VL 以 ```json 代码块包裹 JSON 数组,每个元素为 {"bbox_2d": [x1, y1, x2, y2], "label": "..."}(vlm.py):
result = """```json
[
{"bbox_2d": [139, 768, 315, 954], "label": "cat"},
{"bbox_2d": [366, 679, 536, 849], "label": "dog"}
]
```"""
xyxy, class_id, class_name = from_qwen_2_5_vl(
result, input_wh=(1000, 1000), resolution_wh=(1280, 720)
)
解析链条相当讲究容错:
- 用
re.sub剥掉开头的```json(大小写不敏感)和结尾的```,再截取第一个[到最后一个]; - 先试
json.loads;失败则调用recover_truncated_qwen_2_5_vl_response修复被截断的输出——它定位首个[和最后一个},删除尾逗号后补上]再解析,专门应对模型生成到一半被max_tokens截断的场景(vlm.py); - 再失败则退回
ast.literal_eval,兼容单引号的 Python 风格字面量; - 坐标换算采用双坐标系:
xyxy = xyxy / [in_w, in_h, in_w, in_h] * [out_w, out_h, out_w, out_h]。这是所有解析函数中唯一显式接收input_wh的,因为 Qwen 系列的归一化坐标空间由 prompt 约定决定。
from_qwen_3_vl 是一个薄封装:Qwen3-VL 的坐标空间固定为 1000×1000,所以它直接调用 from_qwen_2_5_vl(result, input_wh=(1000, 1000), ...)(vlm.py)。测试用例 single-detection-scales-from-1000x1000 验证了 [100, 200, 300, 400] 在 640×480 目标下变为 [64, 96, 192, 192](test_vlm.py)。
from_deepseek_vl_2:ref/det 标签配对与 999 坐标系
DeepSeek-VL2 的输出形如:
<|ref|>The giraffe at the back<|/ref|><|det|>[[580, 270, 999, 904]]<|/det|><|ref|>The giraffe at the front<|/det|>...
from_deepseek_vl_2(vlm.py)用两组正则分别提取 <|ref|>...<|/ref|> 标签与 <|det|>...<|/det|> 框段,并强制两者数量相等(不等即 ValueError);每个 det 段可含多个 [[x1, y1, x2, y2], ...] 框,坐标按 999 归一化(x1 / 999 * width)。一个与 PaliGemma 相反的细节:不提供 classes 时它也会返回 class_id——对出现的类名排序后自造 0 起编号,方便直接接入按类着色的标注器。
from_florence_2:任务式 dict 输出与 10 种任务
Florence-2 是唯一返回 dict 且输出为“任务 → 载荷”结构的模型。from_florence_2 要求 result 只有单一任务键,并在 SUPPORTED_TASKS_FLORENCE_2(vlm.py)白名单内,共 10 种任务,各任务的处理分支差异很大(vlm.py):
| 任务 | 返回内容 |
|---|---|
<OD>、<CAPTION_TO_PHRASE_GROUNDING>、<DENSE_REGION_CAPTION> |
bboxes + labels |
<REGION_PROPOSAL> |
仅 bboxes(labels 为空串被丢弃) |
<OCR_WITH_REGION> |
8 坐标的 quad_boxes 重排为 (n, 4, 2) 四边形,同时给出其外接 xyxy |
<REFERRING_EXPRESSION_SEGMENTATION>、<REGION_TO_SEGMENTATION> |
多边形经 polygon_to_mask 生成 (n, h, w) 布尔掩码 + 外接框 |
<OPEN_VOCABULARY_DETECTION> |
bboxes + bboxes_labels |
<REGION_TO_CATEGORY>、<REGION_TO_DESCRIPTION> |
文本前缀作为 label,<loc_XXXX> 标记按 1000 归一化还原坐标;"No object detected." 时返回空 |
返回值是四元组 (xyxy, labels, masks, obb_boxes),其中 masks 与 obb_boxes(四边形)按任务可为 None。<REGION_TO_SEGMENTATION> 分支依赖 polygon_to_mask 与 polygon_to_xyxy,因此分割任务必须传入正确的 resolution_wh 才能生成与图像尺寸一致的掩码。
from_google_gemini_2_0 / 2_5 / 3_5:box_2d 的轴序陷阱
Gemini 系输出 ```json 代码块包裹的数组,元素形如 {"box_2d": [y_min, x_min, y_max, x_max], "label": "..."},坐标按 1000 归一化。最容易踩的坑是轴序:box_2d 是 y-x-y-x,源码中有一行注释 # Gemini bbox order is [y_min, x_min, y_max, x_max],随后用 xyxy.append([box[1], box[0], box[3], box[2]]) 重排,再交给 denormalize_boxes(..., normalization_factor=1000) 完成缩放(vlm.py)。
当整段 json.loads 失败时,_recover_gemini_json_objects 会做“对象级抢救”:扫描花括号配对深度,对每一个平衡的 {...} 片段独立 json.loads,保留能解析成 dict 的对象,从而在某个元素含语法错误时仍能救回其余检测(vlm.py)。
from_google_gemini_2_5 在此之上支持可选的 mask 与 confidence 字段,返回五元组 (xyxy, class_id, class_name, confidence, masks)(vlm.py):
mask是data:image/png;base64,前缀的 PNG 数据 URI;解码后经 PIL 转灰度、双线性缩放到检测框尺寸,再贴回(h, w)布尔掩码的对应位置;任何解码失败或畸形 mask 都会写入全零掩码,并保持逐检测数组对齐;confidence按 item 收集,某 item 缺失该字段时整列置None,与掩码的降级策略一致;from_google_gemini_3_5因输出格式与 2.5 相同,直接委托给from_google_gemini_2_5(vlm.py)。
from_moondream:0–1 浮点坐标
Moondream 返回 dict,objects 键下是 {"x_min", "y_min", "x_max", "y_max"} 列表,坐标为 0–1 浮点(vlm.py)。缺少任一坐标键的 item 被跳过;最终经 denormalize_boxes(默认 normalization_factor=1)缩放到 resolution_wh。该函数只返回 xyxy,不产生类名——这也是 ALLOWED_ARGUMENTS 中 Moondream 不允许 classes 的原因。
Detections.from_vlm:统一入口与调用示例
上述解析函数通过 Detections.from_vlm 对外暴露(core.py)。分发表与各模型的参数契约:
| Name | Enum (sv.VLM) | Tasks | Required parameters | Optional parameters |
|---|---|---|---|---|
| PaliGemma / PaliGemma 2 | PALIGEMMA |
detection | resolution_wh |
classes |
| Qwen2.5-VL | QWEN_2_5_VL |
detection | resolution_wh, input_wh |
classes |
| Qwen3-VL | QWEN_3_VL |
detection | resolution_wh |
classes |
| Google Gemini 2.0 | GOOGLE_GEMINI_2_0 |
detection | resolution_wh |
classes |
| Google Gemini 2.5 | GOOGLE_GEMINI_2_5 |
detection, segmentation | resolution_wh |
classes |
| Google Gemini 3.5 | GOOGLE_GEMINI_3_5 |
detection, segmentation | resolution_wh |
classes |
| Moondream | MOONDREAM |
detection | resolution_wh |
— |
| DeepSeek-VL2 | DEEPSEEK_VL_2 |
detection | resolution_wh |
classes |
from_vlm 先用 _validate_vlm_parameters 做上文的四项校验,再按 vlm 值分发到对应 from_* 函数并组装 Detections。docstring 中的官方示例:
import supervision as sv
paligemma_result = "<loc0256><loc0256><loc0768><loc0768> cat"
detections = sv.Detections.from_vlm(
sv.VLM.PALIGEMMA,
paligemma_result,
resolution_wh=(1000, 1000),
classes=["cat", "dog"],
)
detections.xyxy
# array([[250., 250., 750., 750.]])
detections.class_id
# array([0])
detections.data
# {'class_name': array(['cat'], dtype='<U10')}
qwen_2_5_vl_result = """```json
[
{"bbox_2d": [139, 768, 315, 954], "label": "cat"},
{"bbox_2d": [366, 679, 536, 849], "label": "dog"}
]
```"""
detections = sv.Detections.from_vlm(
sv.VLM.QWEN_2_5_VL,
qwen_2_5_vl_result,
input_wh=(1000, 1000),
resolution_wh=(1280, 720),
)
对 Qwen2.5-VL,源码 docstring 还附有 prompt 建议:一般检测可用 “Detect all objects in the image and return their locations and labels.”,定向检测可用 “Detect the red object that is leading in this image and return its location and label.” 或更简洁的 “leading blue truck”,模型会以带 bbox_2d 与 label 字段的 JSON 返回(core.py)。旧入口 Detections.from_lmm 行为完全一致但已弃用,计划于 supervision-0.31.0 移除。
edit_distance 与 fuzzy_match_index:模糊标签匹配工具
这两个函数位于 src/supervision/detection/utils/vlms.py,处理 VLM 输出中常见的标签拼写漂移问题——模型可能把 cat 写成 dat,或大小写不一致。
edit_distance:双行动态规划
def edit_distance(string_1: str, string_2: str, case_sensitive: bool = True) -> int
计算将 string_1 变换为 string_2 所需的最少单字符编辑数(插入、删除、替换)。实现采用 Levenshtein 经典 DP,但只保留两行状态(prev_row/curr_row),空间复杂度 O(min(len₁, len₂)):先把较短字符串作为行方向(if len(string_1) < len(string_2) 时交换),转移方程为 curr_row[j] = min(prev_row[j] + 1, curr_row[j - 1] + 1, prev_row[j - 1] + substitution_cost),substitution_cost 为 0 或 1。case_sensitive=False 时先统一转小写。docstring 中的行为示例(均可在 tests/detection/utils/test_vlms.py 找到对应参数化用例):
>>> sv.edit_distance("hello", "hello")
0
>>> sv.edit_distance("Test", "test", case_sensitive=True)
1
>>> sv.edit_distance("abc", "xyz")
3
>>> sv.edit_distance("hello", "")
5
测试覆盖了 Unicode/emoji(("😊", "😢") 距离为 1)、100 字符长串、首尾空格等边界,确认实现按字符码点工作。
fuzzy_match_index:阈值化最近邻
def fuzzy_match_index(
candidates: list[str], query: str, threshold: int, case_sensitive: bool = True
) -> int | None
遍历 candidates,返回第一个与 query 编辑距离 <= threshold 的下标,否则返回 None。典型用法是把模型输出标签对齐到你自己的类别表:
>>> from supervision.detection.utils.vlms import fuzzy_match_index
>>> fuzzy_match_index(["cat", "dog", "rat"], "dat", threshold=1)
0
>>> fuzzy_match_index(["alpha", "beta", "gamma"], "bata", threshold=1)
1
>>> fuzzy_match_index(["one", "two", "three"], "xyz", threshold=2) is None
True
注意它是“首个命中”语义而非“最优命中”:多个候选满足阈值时返回下标最小者(测试用例 ["apple", "apply", "appla"] 查询 apple 返回 0 即验证了这一点)。
健壮性设计小结与测试依据
从 tests/detection/test_vlm.py 的 1500 余行参数化用例可以归纳出这套工具的设计取向——宁可返回空,也不抛未预期的异常:
- 纯文本、空 JSON、缺键元素、畸形坐标都会被安全地降级为空数组
(0, 4); - 部分有效即保留:PaliGemma 输出中夹杂错误框时只保留合法匹配;Qwen 截断响应能救回前面完整的对象;Gemini 坏数组能救回每个完好的 dict;
- 只有“不该宽容”的地方才抛错:
resolution_wh非正数、ref/det 标签数量不匹配、Florence-2 多任务 payload 或未知任务名; - 每个模型的坐标空间(1024 / 1000 / 999 / 0–1)都在源码中显式处理,且 Gemini 的 y-x 轴序重排单独成行——这正是把不同 VLM 接进同一管线时最常见的翻车点。
结合本文内容,你可以完成完整链路:调用自家 VLM 拿到原始文本/dict → 选择对应 from_* 函数或 sv.Detections.from_vlm → 用 classes 过滤并按 class_id 着色 → 交给 sv.BoxAnnotator / sv.LabelAnnotator 渲染;标签拼写漂移时用 sv.fuzzy_match_index(candidates, query, threshold=1) 对齐类别表。
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