把 Mage-Flow 接进一套已经跑得通的出图流程,第一步要解决的是接口两端对不对得上:上游交出来的是不是 NCHW 的浮点张量、通道顺序是 RGB 还是 BGR、取值范围是 [0,1] 还是 [0,255]、下游要的是像素还是编码后的潜变量。其中任何一项不一致,下游节点要么直接抛出形状错误,要么勉强跑通却出偏色、错位的图。先花半小时把这些字段记清楚,通常比反复调整模型参数更有效。
判断:接入卡点多数在接口边界,不在模型本身。建议先确认对接层(文件路径、张量、潜变量),把两端的形状、通道顺序、取值范围、dtype 记录成一张对照清单;再写一个只做格式转换的适配函数,把所有改动限制在转换层;最后用同一张固定输入跑端到端,逐段保存中间结果,与改造前比对。若两端语义本就不同(一边是像素、一边是潜变量),中间需要编解码环节,不能靠形状变换硬凑。
确认已有流程需要的输入是文件路径、张量还是编码后的潜变量
先确认对接发生在哪一层,这决定了后面要改什么。常见的三种情况差别很大:
- 文件路径层:上游把结果写入磁盘,下游按路径读文件。此时主要核对位深、色彩配置和 EXIF 方向标记,张量形状往往不是问题。
- 张量层:上游直接把内存里的数组交给下游。需要核对维度顺序(NCHW 还是 NHWC)、通道顺序、取值范围、dtype 四项。
- 潜变量层:下游接收的是编码之后的潜变量,尺寸通常远小于像素图(按空间压缩倍数缩小),数值分布也偏向零附近对称。如果上游送来的是像素张量,形状和范围都不匹配,中间缺的是一个编码步骤,而不是简单变形。
记录方式建议固定成一个字典,两侧各填一份,字段不要随手增减。值是程序里实际打印出来的,不要凭印象填。
upstream = {
"layer": "tensor", # path / tensor / latent
"shape": (1, 3, 1024, 1024),
"dim_order": "NCHW",
"channel_order": "RGB", # 潜变量可填 n/a
"value_range": (0.0, 1.0),
"dtype": "float32",
"batch_semantics": "dim0",
}
downstream = {
"layer": "latent",
"shape": (1, 4, 128, 128),
"dim_order": "NCHW",
"channel_order": "n/a",
"value_range": (-1.0, 1.0),
"dtype": "float16",
"batch_semantics": "dim0",
}
两份记录摆在一起,差异点就是适配函数要处理的范围。如果 layer 字段本身就不一致,先别写转换代码,需要补上中间环节再谈。
写一个只做格式转换的适配函数并用一张图验证
适配函数只做三件事:维度排布、通道置换、取值范围归一化。缩放、裁剪、色彩空间换算这类会改变画面内容的操作,尽量放到流程原有位置,不要混进转换层,否则出问题时说不清是哪一步造成的。
def adapt_to_target(x, src_spec, dst_spec):
"""x: 上游张量;src_spec/dst_spec: 上一步填的两份记录。
只做格式转换,不改画面内容。"""
# 1. 入口形状检查:维度数、通道维位置、批维语义
if len(x.shape) != len(dst_spec["shape"]):
raise ValueError(f"维度数不一致: {tuple(x.shape)} vs {dst_spec['shape']}")
if tuple(x.shape) == tuple(dst_spec["shape"]):
pass # 形状已对齐,跳过变形
# 2. 维度顺序:NCHW / NHWC 之间用 transpose,不要用 reshape 硬凑
if src_spec["dim_order"] != dst_spec["dim_order"]:
x = transpose_to(x, dst_spec["dim_order"])
# 3. 通道顺序:RGB / BGR 用索引置换,比逐通道反转更不容易写错
if src_spec.get("channel_order") and dst_spec.get("channel_order"):
if src_spec["channel_order"] != dst_spec["channel_order"]:
x = x[:, CHANNEL_INDEX_PLACEHOLDER] # 按目标顺序取通道
# 4. 取值范围归一化:先减下限再按区间长度缩放,避免越界
lo_s, hi_s = src_spec["value_range"]
lo_d, hi_d = dst_spec["value_range"]
x = (x - lo_s) / (hi_s - lo_s) * (hi_d - lo_d) + lo_d
# 5. dtype 放在最后,避免中间计算掉精度
x = x.to(dst_spec["dtype"])
# 6. 出口再断言一次,形成双向检查
assert x.shape[-3] == dst_spec["shape"][-3]
return x
函数放在上游输出和下游入口之间的粘合位置,两边原有逻辑一行不动。transpose_to、通道索引、区间端点都按上一步的记录填。验证时不要用复杂的自然图,先拿一张纯色图或棋盘格图跑一遍:通道顺序错了,红色区域会变蓝;范围错了,纯色会变成灰或全黑;dtype 错了,容易出现条纹。纯色图能把这三类问题一次性暴露出来。
检查尺寸与通道数在下游节点是否被重新解释
「能跑通但画面不对」是最容易被忽略的一类。适配函数的输出符合预期,不代表下游节点按同样的理解在使用它。在下游入口第一个处理函数之前加一行探针,是最省事的定位手段:
def tap(name, x):
print(f"[{name}] shape={tuple(x.shape)} dtype={x.dtype} "
f"min={float(x.min()):.4f} max={float(x.max()):.4f} "
f"mean={float(x.mean()):.4f}")
# 调用位置:适配函数出口、下游第一个节点入口、下游最后一个节点出口
tap("after_adapt", y)
tap("downstream_in", y)
tap("downstream_out", out)
把三处打印放在同一次运行里对照,能判断数值是在哪一段被改写的。常见的错位现象和对应检查项如下:
- 颜色偏红或偏蓝:通道顺序在某一侧被当成另一种解释,核对 RGB / BGR 与通道索引。
- 整体发灰、对比度偏低:范围被压缩,例如 0–255 的数被按 0–1 读入。
- 全黑或全白:范围端点不匹配后被裁剪,或归一化时除错了区间长度。
- 出现条纹、噪点:dtype 溢出或位宽不一致,检查 float16 / float32 / uint8 的转换时机。
- 图像被拉伸或裁掉一块:下游按固定边长或整除倍数假设处理尺寸,核对下游对尺寸的取整方式。
- 上下颠倒或左右镜像:坐标原点约定不同,检查下游是否有翻转操作。
- 通道维被当成批维:维度语义错位,核对 NCHW 里哪一维是 batch。
这些现象多数能从前面的三次打印里直接看出线索:形状对但 mean 异常,多半是范围问题;形状不对但数值正常,多半是排布问题。
用同一张输入跑通端到端并对比改造前后结果
最后一步是确认适配函数没有在转换过程中悄悄改动画面内容。做法是控制变量,只换流程本身,不换输入。
- 固定一张输入图,尺寸选常见的正方形,下采样后的潜变量尺寸要能被整除,避免触发下游的补齐逻辑。
- 如果流程内部有随机采样,先固定随机种子,否则两次结果不可比。
- 在原流程和接入 Mage-Flow 后的流程里,各保存一份中间结果:上游输出、适配函数输出、下游第一层输入、下游最终输出。落盘时用同一套命名,方便并排看。
- 逐段比对,先比形状和 dtype,再比 min / max / mean,最后看图像本身。判定标准是:适配函数前后的形状、通道、范围应符合记录表;下游第一层输入若与原流程对应位置一致或在数值容差内,说明转换层没有引入变化。
- 如果差异只出现在下游内部,转换层通常可以先排除,转去检查下游参数;如果差异从适配函数出口就开始出现,回到第 2 节的骨架逐项核对。
改造前后不要求逐像素完全一致,模型内部计算顺序变化、dtype 精度不同都可能带来细微差别。判断的重点是差异有没有超出预期范围,以及差异从哪一段开始出现。把这一步的中间图留档,后续改动时可以直接复用同一套比对方式。