换掉示例相机后要不要改分辨率、内参、颜色通道,以及指令文本该怎么写,判断标准其实只有一个:观测输入进入模型前的最后一份张量和字符串,是否和你以为的一致。比较稳妥的顺序是先把图像预处理和相机参数的落点核清楚,再看指令文本怎么拼、在哪一层被截断,最后固定同一帧做改动前后的输出比对。图像这一层没对齐时,后面指令怎么调都会把误差混在一起,很难判断是哪一步出的问题。
对齐自家相机的顺序建议是:先在构造观测的函数里定位图像、状态、指令三处来源与拼接位置,再用断言打印确认分辨率、dtype 和通道顺序没有被预处理改写,然后确认内参外参是从配置读取还是硬编码在代码里,接着检查指令文本的拼接与截断层级,最后用同一帧固定输入对比改动前后的输出向量。适用场景是替换示例相机或迁移到自采数据;风险边界在于不同分支、不同任务配置的预处理可能各不相同,需要逐个配置确认,不能凭默认值推断。
拆开观测输入,看它由哪几部分拼成
动手前先找到构造观测的那个函数,通常名字里带 build_observation、make_obs、prepare_input 或 dataset 的 collate 方法。可以从模型推理入口反着搜,也可以直接搜图像处理函数被调用的位置。定位到以后,把图像、状态、指令三者的来源和拼接位置列出来:图像来自哪一路相机、是否经过 resize 或归一化;状态是关节角、末端位姿还是夹爪开合,量纲和顺序是什么;指令是原始字符串还是已经套过模板的文本。这一步的意义在于,后面所有改动都能对应到具体一行,而不是靠猜。
def build_observation(cam_frame, joint_state, instruction, cfg):
img = preprocess_image(cam_frame, cfg.image) # 分辨率、通道、归一化都在这里
state = normalize_state(joint_state, cfg.state) # 量纲与维度顺序
text = build_instruction(instruction, cfg.prompt) # 模板拼接
return {'image': img, 'state': state, 'text': text}
这份骨架只是说明结构,实际项目里三者可能分散在 dataloader、环境封装和推理脚本里。建议把每个字段的来源写在注释里,例如“图像来自 front 相机,cv2 读取,默认 BGR”。可以先在一帧真实数据上把这三个返回值原样打印出来,长度、形状、首尾几个值都记下来,作为后面比对的基线。
核分辨率与颜色通道有没有被预处理改写
通道顺序和缩放是替换相机后最容易出错的地方,而且出错时模型不会报错,只会输出跑偏。常见误用表现有几种:用 OpenCV 读图后直接送进期望 RGB 的模型,画面偏色但 shape 完全正常;模型期望 HWC 而预处理输出 CHW,或者反过来;resize 时把宽高写反,得到 480×640 和 640×480 的差别;裁剪用了中心裁剪,把目标裁到画面外;uint8 没有归一化,或者归一化用了非 0-255 的均值方差。这些都不会抛异常,只能靠断言和肉眼比对发现。
import numpy as np
def check_image(img, expect_hw=(480, 640)):
print('shape', img.shape, 'dtype', img.dtype)
assert img.shape[:2] == expect_hw, f'分辨率不符: {img.shape[:2]}'
assert img.dtype in (np.uint8, np.float32), img.dtype
if img.dtype == np.uint8:
print('channel mean', img.reshape(-1, 3).mean(axis=0))
print('min/max', float(img.min()), float(img.max()))
判断通道顺序可以拿一个已知颜色的物体做参照:画面里放一块纯红物体,分别打印三个通道的均值,均值最高的那个通道就是红色所在位置,由此反推是 RGB 还是 BGR。这个断言建议放在预处理函数出口,而不是放在读取原图之后,否则核到的是输入而不是模型真正吃到的张量。需要结合环境确认的一点是,不同分支的预处理参数可能不同,配置文件里改过 image 段之后要重新跑一次这段断言。
确认相机参数在代码里的落点
内参外参是从配置读还是硬编码,决定了改相机的成本。先看配置目录有没有 camera 相关的 yaml 或 json,字段名通常围绕 fx、fy、cx、cy、width、height,外参一般是一个 4×4 的变换矩阵,含义要确认是 base 到 cam 还是 cam 到 base,这两者互为逆,写反了投影结果会整体错位。如果配置文件存在但代码里没读,那就是有人在代码里写了常量。可以用 grep 在源码里搜 fx、cx、intrinsic 这类关键词确认。
import yaml, numpy as np
def load_camera(cfg_path):
cfg = yaml.safe_load(open(cfg_path))
c = cfg['camera']
K = np.array([[c['fx'], 0, c['cx']],
[0, c['fy'], c['cy']],
[0, 0, 1]])
return K, c.get('dist'), np.array(c['extrinsic_base_cam'])
字段说明建议写在配置旁边:fx、fy 是焦距像素值,cx、cy 是主点,dist 是畸变系数,没有畸变就填零;extrinsic_base_cam 记录基座到相机的刚体变换。改动位置有两个,配置文件和加载函数里的键名,如果加载函数对字段做了重命名,只改配置文件不生效。改完之后打印一次加载出来的 K 矩阵,和标定结果逐项对照,比只看代码可靠。
指令文本的写法与长度约束
指令跑偏多半不是措辞问题,而是拼装和截断问题。截断可能发生在三个地方:tokenizer 调用时传了 max_length 和 truncation,超出部分被静默切掉;模板拼接时后面的字段覆盖了前面的指令,或者把指令套进了不适合的模板;数据集读取时对字段做了截取。排查方法是把最终送入模型的那段字符串原文打印出来,而不是看原始输入的那句指令。
samples = ['把红色方块放进左侧盒子', 'pick up the cube and place it in the box on the left']
for s in samples:
ids = tok(s)['input_ids'] if isinstance(tok(s), dict) else tok.encode(s)
print(len(s), len(ids), s[:30])
观察方法有两点:一是对比字符数和 token 数的增长是否异常,中文和英文的比值不同,明显偏低通常意味着编码器或截断出了问题;二是故意写一条超过上限的长指令,看输出里的字符串尾部是否完整。长度上限需要结合模型自身配置确认,不要照搬示例脚本里的数值。指令写法建议保持单一动作、单一目标,避免在一条指令里塞入多步操作,这类任务通常需要由上层拆解。
用同一帧做改动前后对比
改动是否真的生效,只看代码改没改不够,要看输出变没变、怎么变。做法是先固定一帧图像、一组状态和一条指令,把所有随机源固定住,跑一次推理并把输出向量存下来;改完配置或预处理后再跑同一条输入,存第二份向量,逐元素比对差异。如果差异为零,说明改动没有进入计算路径,多半是配置没被读到或者走的是缓存分支;如果差异集中在动作向量的某些维度,可以进一步判断是图像通道问题还是状态量纲问题。
np.save('out_before.npy', out_before)
# 改动配置或预处理后,同一条输入再跑一次
np.save('out_after.npy', out_after)
d = np.abs(out_before - out_after)
print('max', d.max(), 'mean', d.mean(), 'argmax', int(d.argmax()))
print('head', d[:8])
print('tail', d[-8:])
比对时建议一次只改一项:先只改通道顺序,看差异;再单独改内参,再单独改指令文本。多项一起改会让差异无法归因。固定输入这一步要在同一个进程或同一份落盘数据上做,避免相机实时取帧引入额外变化。如果两次输出完全一致且预期应该有变化,优先回头核第二、三节的断言是否真的在执行的代码路径上,而不是继续调指令。