【Bug已解决】SD1.5 - pipeline_controlnet_img2img and pipeline_controlnet_inpaint are mixing variables “im
【Bug已解决】SD1.5 - pipeline_controlnet_img2img and pipeline_controlnet_inpaint are mixing variables image and control_image 解决方案一、现象长什么样用 SD1.5 的 ControlNet img2img / inpaint pipeline 时控制和生成结果明显不对from diffusers import StableDiffusionControlNetImg2ImgPipeline, ControlNetModel import PIL.Image as Image controlnet ControlNetModel.from_pretrained(lllyasviel/sd-controlnet-canny) pipe StableDiffusionControlNetImg2ImgPipeline.from_pretrained( runwayml/stable-diffusion-v1-5, controlnetcontrolnet ) init_image Image.open(photo.png) # 原图img2img 的输入 control_image Image.open(edge.png) # 边缘图control 信号 out pipe( prompta room, imageinit_image, # 期望img2img 去噪的起点 control_imagecontrol_image, # 期望控制结构 )现象生成的图完全跟着 control_image 走init_image 被忽略——看起来像 control_image 被当成了 img2img 的初始图或者反过来control 失效图就是普通 img2img边缘约束没起作用偶尔直接RuntimeError: image and control_image have different sizes或形状错。最迷惑的是参数都传对了image和control_image分明是两个变量但 pipeline 内部把两者搞混了——要么用control_image当image去初始化潜变量要么把image喂给了 ControlNet 当控制条件。这是典型的「变量名/语义混淆」bug。二、背景ControlNet pipeline 里有两个语义完全不同的图像输入imageimg2img / inpaint 流程里它是待去噪/修复的初始图像img2img 的init_imageinpaint 的带遮挡图。它会被 VAE 编码成潜变量作为生成起点。control_imageControlNet 的条件图像如边缘、姿态、深度用来引导生成结构不参与初始潜变量。这两个必须严格区分。但 ControlNet 的 img2img / inpaint 变体在处理输入时因为代码是从StableDiffusionImg2ImgPipeline/StableDiffusionInpaintPipeline改造而来改造时把image既用于初始化潜变量、又被错误地复用去构造 ControlNet 条件或反过来control_image被错误地塞进 VAE 编码当初始图。具体混淆形式img2imginit_latents vae.encode(image)是对的但 ControlNet 条件应该是control_image代码却写成controlnet(..., control_imageimage)——于是控制条件用的是原图而非边缘图。或者control_image被赋给image变量后后续既当初始图又当控制两者彻底串。根子是pipeline 内部image初始图与control_image控制条件的变量语义混淆导致初始潜变量或 ControlNet 条件用了错误的图像。三、根因根因一句话SD1.5 的 ControlNet img2img / inpaint pipeline 在内部把image初始/待修复图和control_image控制条件的变量语义混淆导致初始潜变量编码或 ControlNet 条件用了错误的图像控制失效或结果错位。三点展开变量串用control_image被塞进image变量或反之后续两者共用一个值。潜变量来源错用control_image而非image去 VAE 编码当初始潜变量。ControlNet 条件错用image而非control_image当 ControlNet 输入控制结构丢失。不是参数传错用户传对了是「pipeline 内部变量混淆」。四、最小可运行复现不依赖真实模型模拟「image 与 control_image 混淆」def run_controlnet_img2img(image, control_image, buggyFalse): if buggy: # 错误把 control_image 当成初始图去编码潜变量 init_latents_source control_image # 同时 ControlNet 条件又用了 image本应是 control_image control_source image else: init_latents_source image control_source control_image return init_latents_source, control_source img photo.png ctl edge.png ok_init, ok_ctl run_controlnet_img2img(img, ctl, buggyFalse) print(正确: 初始图, ok_init, 控制, ok_ctl) # photo / edge bad_init, bad_ctl run_controlnet_img2img(img, ctl, buggyTrue) print(混淆: 初始图, bad_init, 控制, bad_ctl) # edge / photo反了跑出来buggy 版本初始图用了edge、控制用了photo两者完全反了。这就是「image/control_image 混淆」的精确复现。五、解决方案第一层最小直接修复最小修复在 pipeline 内部严格分离image与control_image两个变量——image只用于 VAE 编码初始潜变量control_image只用于构造 ControlNet 条件互不串用。from diffusers import StableDiffusionControlNetImg2ImgPipeline import torch # 修复后的核心逻辑示意 def _run(self, prompt, image, control_image, **kw): # 1) image - 初始潜变量img2img 去噪起点 init_latents self.vae.encode( self.image_processor.preprocess(image) ).latent_dist.sample() init_latents init_latents * self.vae.config.scaling_factor # 2) control_image - ControlNet 条件绝不混入 image control_image self.control_image_processor.preprocess(control_image) control_image control_image.to(deviceself.device, dtypeinit_latents.dtype) # 3) ControlNet 只吃 control_image controlnet_cond control_image down_block_res, mid_block_res self.controlnet( init_latents, timesteps, encoder_hidden_states, controlnet_condcontrol_image, # 注意是 control_image不是 image return_dictFalse, ) # 4) 去噪从 init_latents来自 image开始 latents init_latents ... return self.decode(latents)要点image只走 VAE 编码成init_latents作去噪起点。control_image只走 ControlNet 条件绝不进init_latents。两变量名在 pipeline 内全程区分不互相赋值覆盖。这一步单独就让 ControlNet img2img / inpaint 的「初始图」与「控制条件」各司其职。六、解决方案第二层结构性改进第一层是「在 pipeline 内改两处」。但 img2img / inpaint / 其它 ControlNet 变体都可能混淆容易漏。更稳的做法把「image 与 control_image 的语义边界」收敛成单一守卫。from dataclasses import dataclass, field from typing import Any, Optional dataclass class ControlImageVariableGuard: ControlNet pipeline 中 image / control_image 语义隔离的单一守卫。 # 二者绝不允许共用同一个对象引用 def segregate(self, image: Any, control_image: Any) - tuple: if image is control_image: raise ValueError(image 与 control_image 不能是同一对象) # 确保两者都经各自处理器不互相覆盖 return image, control_image def route(self, image: Any, control_image: Any, mode: str): 按 mode 决定各自用途杜绝串用。 if mode img2img: init_source, control_source image, control_image elif mode inpaint: init_source, control_source image, control_image else: raise ValueError(f未知 mode {mode}) # 断言初始潜变量来源 ! 控制条件来源引用不同 assert init_source is not control_source, image/control_image 串用 return {init_source: init_source, control_source: control_source} def validate_sizes(self, image: Any, control_image: Any): 可选断言两图尺寸一致ControlNet 通常要求。 if hasattr(image, size) and hasattr(control_image, size): if image.size ! control_image.size: raise ValueError( fimage {image.size} 与 control_image {control_image.size} 尺寸不一致) return True # 用法 guard ControlImageVariableGuard() img, ctl guard.segregate(user_image, user_control) routed guard.route(img, ctl, modeimg2img) init_latents vae_encode(routed[init_source]) # 用 image control_cond preprocess(routed[control_source]) # 用 control_image结构收益单一守卫image/control_image 的语义隔离、路由、尺寸校验集中。防串用segregate拒绝同一对象route断言两者引用不同。可扩展新 ControlNet 变体depth/pose/seg复用同一守卫。七、解决方案第三层断言 / CI 守护写 pytest 守三条(1) image 与 control_image 不被同一对象(2) 初始潜变量来源是 image 不是 control(3) 尺寸不一致被校验。import pytest from your_lib import ControlImageVariableGuard def test_same_object_rejected(): g ControlImageVariableGuard() with pytest.raises(ValueError): g.segregate(photo.png, photo.png) # 同一对象 def test_route_uses_image_for_init(): g ControlImageVariableGuard() img, ctl g.segregate(photo.png, edge.png) r g.route(img, ctl, modeimg2img) assert r[init_source] photo.png assert r[control_source] edge.png def test_route_asserts_no_share(): g ControlImageVariableGuard() class Obj: pass o Obj() with pytest.raises(AssertionError): g.route(o, o, modeinpaint) # 串用同一对象 def test_size_mismatch_detected(): g ControlImageVariableGuard() class Img: def __init__(self, s): self.size s with pytest.raises(ValueError): g.validate_sizes(Img((512,512)), Img((256,256)))CI 常驻跑这四条后任何「image/control_image 又混淆」「串用同一对象」的回归都会立刻爆红。八、排查清单ControlNet img2img/inpaint 控制错位时按顺序查先确认是不是「control 失效 / 初始图被忽略」——是的话定位变量混淆。打印 pipeline 内init_latents的来源确认是image而非control_image。打印 ControlNet 的controlnet_cond确认是control_image而非image。确保image与control_image在 pipeline 内是两个独立变量不互相赋值覆盖。用ControlImageVariableGuard做隔离断言引用不同 尺寸一致。img2img / inpaint / 其它 ControlNet 变体都过同一守卫。升级 diffusers 后跑「带 control_image 的生成」冒烟断言控制生效、初始图被用。九、小结SD1.5 ControlNet img2img/inpaint 控制错位根子是 pipeline 内部把image初始/待修复图与control_image控制条件变量语义混淆导致初始潜变量或 ControlNet 条件用了错误的图像。修复三层次第一层在 pipeline 内严格分离两变量image只编码初始潜变量、control_image只作 ControlNet 条件第二层用ControlImageVariableGuarddataclass 把语义隔离/路由/尺寸校验收敛为单一守卫第三层用 pytest 守「非同一对象」「初始来源是 image」「尺寸一致」。工程启示任何带「多个图像输入」的 pipelineControlNet、IP-Adapter、img2imgcontrol都必须把每个图像输入的语义边界显式化杜绝变量串用。ControlNet 的image去噪起点和control_image结构条件是性质完全不同的两个东西一旦在重构时共用一个变量控制就废了——用守卫层做引用隔离能从根上防这种静默错位。