首页
/ supervision 中的 VLM 工具详解:将视觉语言模型输出解析为 Detections

supervision 中的 VLM 工具详解:将视觉语言模型输出解析为 Detections

2026-09-06 15:40:48作者:庞眉杨Will

本文基于 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_paligemmafrom_qwen_2_5_vlfrom_florence_2 等),它们被 src/supervision/detection/core.py 中的 Detections.from_vlm(及其已弃用的前身 Detections.from_lmm)按模型分发调用。edit_distancefuzzy_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)按顺序执行四步:

  1. 若传入字符串,先尝试 VLM(vlm.lower()),失败则报 “Invalid vlm value”;
  2. isinstance(result, RESULT_TYPES[vlm]) 检查模型输出的类型——这是 Florence-2 / Moondream(dict)与其他模型(str)最直观的差异;
  3. 遍历 REQUIRED_ARGUMENTS[vlm],缺失任一必需参数即抛 ValueError: Missing required argument: ...
  4. 遍历传入的 kwargs,任何不在 ALLOWED_ARGUMENTS[vlm] 中的参数都会抛 ValueError: Argument ... is not allowed for ...,防止把 classes 误传给 Moondream 之类的模型。

resolution_wh 还有一层数值校验:_validate_resolutionsrc/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)
)

解析链条相当讲究容错:

  1. re.sub 剥掉开头的 ```json(大小写不敏感)和结尾的 ```,再截取第一个 [ 到最后一个 ]
  2. 先试 json.loads;失败则调用 recover_truncated_qwen_2_5_vl_response 修复被截断的输出——它定位首个 [ 和最后一个 },删除尾逗号后补上 ] 再解析,专门应对模型生成到一半被 max_tokens 截断的场景(vlm.py);
  3. 再失败则退回 ast.literal_eval,兼容单引号的 Python 风格字面量;
  4. 坐标换算采用双坐标系: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_2vlm.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_2vlm.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),其中 masksobb_boxes(四边形)按任务可为 None<REGION_TO_SEGMENTATION> 分支依赖 polygon_to_maskpolygon_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 在此之上支持可选的 maskconfidence 字段,返回五元组 (xyxy, class_id, class_name, confidence, masks)vlm.py):

  • maskdata:image/png;base64, 前缀的 PNG 数据 URI;解码后经 PIL 转灰度、双线性缩放到检测框尺寸,再贴回 (h, w) 布尔掩码的对应位置;任何解码失败或畸形 mask 都会写入全零掩码,并保持逐检测数组对齐;
  • confidence 按 item 收集,某 item 缺失该字段时整列置 None,与掩码的降级策略一致;
  • from_google_gemini_3_5 因输出格式与 2.5 相同,直接委托给 from_google_gemini_2_5vlm.py)。

from_moondream:0–1 浮点坐标

Moondream 返回 dictobjects 键下是 {"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_2dlabel 字段的 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) 对齐类别表。

登录后查看全文
热门项目推荐
相关项目推荐