LingBot-Vision 接进已有视觉流水线 / 先固定输入输出契约

文章导读
接 LingBot-Vision 到已有视觉流水线,真正容易出错的不是模型调用本身,而是两侧对张量格式和输出字段的理解不一致:上游按老模型的习惯送 BGR、0-255,LingBot-Vision 可能按 RGB、0-1 接收;老代码拿到的是 xywh 归一化框,新模型吐的是 xyxy 像素框。稳妥的做法是先把契约冻结下来,再写一层适配把差异收口,最后才动调用点。这样上游预处理和下游后处理不需要同
📋 目录
  1. A 把现有流水线的输入输出契约写下来
  2. B 对齐输入张量的格式与取值范围
  3. C 写一层适配代码隔离两侧差异
  4. D 用同一批图做新旧输出对照
  5. E 固定契约后替换调用点
A A

接 LingBot-Vision 到已有视觉流水线,真正容易出错的不是模型调用本身,而是两侧对张量格式和输出字段的理解不一致:上游按老模型的习惯送 BGR、0-255,LingBot-Vision 可能按 RGB、0-1 接收;老代码拿到的是 xywh 归一化框,新模型吐的是 xyxy 像素框。稳妥的做法是先把契约冻结下来,再写一层适配把差异收口,最后才动调用点。这样上游预处理和下游后处理不需要同时改,出问题时也只需要回查一处。

适用场景:已有视觉流水线要换用 LingBot-Vision,但张量格式、坐标系、输出字段尚未对齐。处理动作:先以现有接口为基准写出输入输出契约表,再写一层适配函数承接转换与字段映射,用同一批图做新旧输出对照,确认无系统性偏移后才替换调用点。验证方式:契约表逐项对照,对照脚本输出字段级差异清单,切换后看日志中的形状、空结果条数和异常计数。风险边界:若新模型本身缺少老模型具备的输出字段(例如某类掩码),适配层无法凭空补齐,需要在契约里显式标注为不支持。

把现有流水线的输入输出契约写下来

不要一上来就读新模型文档,先记录现在这条流水线的边界。以现有接口为基准写契约表,好处是替换时有一个稳定的参照物,而不是两边各改一点、最后对不上。需要记录的内容包括:输入形状、数值取值范围、通道顺序、批次维度含义、调用时序,以及输出的每个字段名、维度、坐标系和编号体系。

  • 输入形状:是 (N,C,H,W) 还是 (N,H,W,C),是否含 batch 维,H/W 是否固定。
  • 取值范围:uint8 0-255、float32 0-1,还是按 ImageNet 均值方差归一化后的值。
  • 输出字段:框、分数、类别、掩码分别叫什么,是否同一个数组打包返回。
  • 调用时序:同步还是异步,是否允许并发,输入尺寸变化是否需要重建会话。

建议把契约写成一段可执行的结构定义,而不是只写在文档里。下面是一份通用骨架,字段名可按实际项目调整:

# contracts.py —— 两侧共用的契约定义
from dataclasses import dataclass
import numpy as np

@dataclass
class DetInput:
    images: np.ndarray      # (N, H, W, 3) uint8, RGB, 0-255
    orig_sizes: list        # [(w, h), ...] 原图尺寸,用于坐标回投

@dataclass
class DetOutput:
    boxes_xyxy: np.ndarray  # (M, 4) float32, 原图像素坐标, 左上原点
    scores: np.ndarray      # (M,)  float32
    labels: np.ndarray      # (M,)  int64, 沿用现有 label_map 编号

调用时序的记录格式建议统一成一行结构化日志,至少包含:请求 id、输入形状、dtype、通道顺序、耗时毫秒、输出条目数、异常类型。这样对照阶段可以直接拿日志做差集,不需要靠人工回忆。

LingBot-Vision 接进已有视觉流水线 / 先固定输入输出契约

对齐输入张量的格式与取值范围

确认两侧预处理差异落在哪一步,比笼统地说“格式不对”更有用。常见差异集中在三处:通道顺序、归一化参数、尺寸缩放方式。把它们列成对照项逐条比对,差异清单会自然浮现。

  • 通道顺序:现有流水线可能是 OpenCV 读图的 BGR,而 LingBot-Vision 通常按 RGB 解释。顺序错了不会报错,只会让颜色相关的判断整体偏移。
  • 归一化:是否需要除以 255,是否再减均值除标准差,均值方差取自哪一组常量。先用同一张纯色图跑一遍,看输出分数是否稳定,能快速暴露归一化问题。
  • 尺寸缩放:等比缩放加 padding,还是直接拉伸。若做了 padding,输出坐标回投时必须先减去 padding 再除以缩放比,这一步经常被漏掉。

差异清单建议写成三列:对照项、现有流水线取值、LingBot-Vision 侧取值(按实际接口确认后填写)。凡是无法确认的项,先在适配层里做成可配置参数,用日志打印实际生效值,不要凭猜测写死。运行一次后从日志中确认,再决定是否收紧成常量。

写一层适配代码隔离两侧差异

适配层的作用是让替换只影响一个文件。上游仍然按老契约送 DetInput,下游仍然按老契约读 DetOutput,中间所有转换都在适配函数里完成。函数签名建议固定,内部实现随接口替换。

def lingbot_detect(inp: DetInput, params: dict) -> DetOutput:
    # 1) 张量转换:与第 2 节对照表保持一致
    x = inp.images.astype(np.float32) / 255.0        # TODO 按实际归一化参数替换
    x = x[..., ::-1]                                  # BGR 转 RGB, 若上游已是 RGB 则删除
    x = np.transpose(x, (0, 3, 1, 2))                 # NHWC -> NCHW, 按实际接口确认

    # 2) 调用:替换成真实 SDK 入口
    raw = lingbot_client.infer(x, **params)           # TODO 替换为实际调用方式

    # 3) 字段映射:按实际返回结构替换
    boxes = raw["boxes"]                              # TODO 若为 xywh 需转 xyxy
    scores = raw["scores"]                            # TODO 确认字段名与维度
    idx = raw["classes"]                              # TODO 确认编号体系
    labels = np.array([LABEL_MAP.get(int(i), -1) for i in idx])

    # 4) 坐标回投到原图:若做了缩放或 padding, 在此处还原
    boxes = rescale_boxes(boxes, inp.orig_sizes)      # TODO 按实际缩放方式替换
    return DetOutput(boxes_xyxy=boxes, scores=scores, labels=labels)

四处 TODO 就是需要按实际接口替换的位置:归一化参数、调用入口、返回字段名、坐标还原方式。适配层里不要混入业务逻辑,也不要顺手改阈值或 NMS 参数,否则对照阶段会分不清差异来自模型还是后处理。

LingBot-Vision 接进已有视觉流水线 / 先固定输入输出契约

用同一批图做新旧输出对照

对照样本建议覆盖几类:纯色或低纹理图、目标密集的小图、目标极大的图、目标贴近边界的图、以及历史中出现过误检的图。数量不必多,几十张即可,关键是每类都要有,且固定下来不复用其他用途。

比较指标建议选可计数的项,而不是笼统的“效果更好”:

  • 框数量差异:同一张图上新旧输出的条目数是否一致。
  • 匹配率:按 IoU 阈值做配对后,未匹配上的框有几条。
  • 类别不一致数:配对成功的框里,标签不同的有几条。
  • 分数差值:配对框的分数差的分布范围,关注是否存在整体性偏移。
  • 坐标差值:边界框四个坐标的绝对差,重点看是否有固定量级的系统性平移,那通常指向 padding 或缩放还原错误。

差异超出预期时,按这个顺序回查:通道顺序 → 归一化参数 → 尺寸缩放与 padding → 坐标回投 → 置信度阈值与 NMS 参数 → 标签映射表。前四项属于契约问题,改适配层即可;后两项属于后处理参数,需要单独确认是否本来就不一致,不要用调阈值的方式掩盖契约错误。

LingBot-Vision 接进已有视觉流水线 / 先固定输入输出契约

固定契约后替换调用点

对照通过后再动调用点。替换步骤建议拆成小步:先把原调用点复制到一个新函数保留为旧路径,再让业务代码通过配置项选择走旧路径还是适配层,跑一段时间后把旧路径标记为待删除,确认无回滚需求再清理。

  1. 在配置中增加开关,例如 vision.backend = legacy | lingbot,默认保持 legacy。
  2. 把调用点改为读取配置后分支调用,业务层其余代码不动。
  3. 灰度放开,观察日志中的输入形状、dtype、输出条目数、异常计数和耗时分位值。
  4. 确认稳定后,把默认值切到新路径,旧路径保留一个可回滚的提交点。

回滚方式就是配置项改回旧值,或者回退到切换前的提交。前提是适配层不删、旧调用点不删,所以这两处要等到观察期结束再清理。

切换后的观察项建议包括:每个请求的输入形状是否符合契约、空结果条数是否突然增多、类别分布是否与切换前明显不同、异常日志中是否出现形状不匹配或 dtype 报错。这些项都能从日志和配置直接核对,不依赖主观判断。只要其中一项出现持续异常,先回滚到旧路径再定位,不要在线上直接调参数试探。