AI 图像 inpaint 改动了蒙版外的像素:排查

你只蒙了一块做 inpaint,蒙版外的像素也被改了。强制 paste-back 合成(apply_overlay)、用硬边蒙版、再用像素 diff 验证。

你 inpaint 了一小块——比如把咖啡马克杯换成酒杯——蒙版内的补丁效果很好,但蒙版外的一切都微妙地变了。肤色读起来稍偏暖,桌面木纹横移了一两个像素,墙面漆色也跳了。这就是”inpaint 溢出(inpaint bleed)“,是 diffusion 编辑里最坑的陷阱之一:表面上”成功了”,直到你做一次像素 diff,才发现整张图都动了。

最快的修法:把结果合成回原图,让蒙版外的像素根本无法改变。在 Diffusers 里只要一行——pipeline.image_processor.apply_overlay(mask, init_image, result)。在 AUTOMATIC1111/Forge 里是 Inpaint area: Only masked。在 ComfyUI 里是图末加一个 paste-by-mask 节点。然后用像素 diff 证明它(步骤 6)。下面的内容是给”这还不够”或你想知道为什么会发生的情况准备的。

为什么会发生(以及哪种是你的情况)

最常见、也最反直觉的一点,官方 Diffusers inpainting 文档(截至 2026 年 6 月)已确认:inpaint 专用 checkpoint 是被刻意训练去让蒙版内外自然过渡的,这意味着它们比普通 checkpoint “更容易改动你的蒙版外区域”。模型在做它该做的事;只是管线没有保护外部区域。用下表对号入座。

像素 diff 上的表现可能原因修法
到处都是大块、有结构的变化全图重生成,没有 paste-back步骤 1
紧贴蒙版边缘的一条软变化带羽化 / 模糊蒙版步骤 2
蒙版外 8-64 像素的锐利变化带inpaint padding / 裁切边距步骤 3
整张图像”重画”过strength / denoise 太高步骤 4
跑第二次 pass 之前外部是稳的SDXL refiner 不感知蒙版步骤 5
到处是微小随机噪声(0-255 内 +/- 2 以内)VAE 往返(无害)接受,或步骤 1
导出后整张图偏移被重存成了 JPEG,不是溢出步骤 6,存 PNG

常见原因

按频率排。

1. 管线在做全图 inpaint,而不是仅蒙版 paste-back

很多管线有两种行为:

  • 仅蒙版(paste-back):只有蒙版内的像素会变;蒙版外按字节从原图复制。
  • 全图带蒙版条件:模型重新生成整张图,把蒙版只当作引导提示。

第二种会溢出,而且在很多 SDXL inpaint workflow 里它是事实上的默认——因为 inpaint checkpoint 会通过动一动周边来抹平接缝。

如何识别:把输出和原图在蒙版外做 diff。任何像素不同 = 你在全图模式。

2. 蒙版有羽化 / 软边

羽化蒙版(高斯模糊)告诉模型”从内到外渐变混合”。这条混合带被部分重生成,所以你以为”在外面”的像素被悄悄改了。在 Diffusers 里这是 mask_processor.blur()blur_factor;在 AUTOMATIC1111/Forge 里是 Mask blur 滑块(默认 4)。

如何识别:打开蒙版文件。边缘是灰色渐变 = 有羽化;锐利黑白边 = 无羽化。

3. inpaint padding / 裁切边距扩大了处理区

大多管线会把蒙版按 N 像素扩展,对这片边距做处理来融合接缝。这片边距从你视角是”蒙版外”,从管线视角是”蒙版内”。各工具参数名不同:Diffusers padding_mask_crop,AUTOMATIC1111/Forge Only masked padding, pixels,ComfyUI VAE Encode (for Inpainting) 节点上的 grow_mask_by

如何识别:查那个值。如果是 3264,预期蒙版外 32-64 像素会被动。

4. strength / denoising strength 太高

基于 img2img 的 inpaint 里,strength(在 AUTOMATIC1111/Forge 里叫 Denoising strength)接近 1.0 实际上就是带弱条件的文生图,模型会重新生成一切。0 返回原图不变;1 完全忽略源图。

如何识别:输出整体看起来”不一样”了,不只是蒙版内。降到 0.7 重测。

5. SDXL refiner pass 处理了整张图

底模 inpaint 之后,SDXL refiner 第二阶段可能在整张图上跑,而某些实现里 refiner 不感知蒙版。

如何识别:只跑底模(关闭 refiner)。蒙版外像素稳定 = refiner 是元凶。

6. VAE 解码有损

哪怕模型只动蒙版内,编码-解码经过 VAE 往返不是逐字节一致,所以蒙版外区域回来时带量化噪声。

如何识别:在蒙版外做 diff。差异很小(0-255 范围内 +/- 2 以内)且像随机噪声 = VAE 往返损失,不是溢出。paste-back(步骤 1)能彻底消除它。

7. PNG inpaint 之后被重存成 JPEG

如果导出成 JPEG,JPEG 量化会改变每个像素,不只蒙版内。这不是 inpaint 溢出,是 JPEG。

如何识别:把输出存成 PNG 重做 diff。蒙版外一致了 = JPEG 是原因。

开始前准备

  • 全分辨率保存原图。
  • 同分辨率保存蒙版文件。
  • 记下你管线在用哪种 inpaint 模式(仅蒙版 vs 全图)。
  • 决定你的容忍度:蒙版外像素完美保留,还是视觉不可区分(几个单位 VAE 噪声没关系)。

需要收集的信息

  • inpaint 模式 / 区域标志(AUTOMATIC1111 里的 Inpaint area、Diffusers 里是否用 apply_overlay、ComfyUI 里是否有 paste-by-mask 节点)。
  • 蒙版文件(打开它;验证边缘是否锐利)。
  • 蒙版羽化 / 模糊值(blur_factorMask blur)。
  • inpaint padding / 裁切值(padding_mask_cropOnly masked padding, pixelsgrow_mask_by)。
  • strength / Denoising strength
  • 是否启用 SDXL refiner。
  • 输出文件格式和质量设置。

一步步修复

步骤 1:强制仅蒙版的 paste-back

这是最可靠的单一修法,而且不管你用哪个 checkpoint 都管用。

Diffusers(官方方法)。 库里自带 VaeImageProcessor.apply_overlay(),强制蒙版外区域与原图完全一致。文档点明了代价:接缝处过渡会稍硬一点,这由步骤 2 处理。

from diffusers import AutoPipelineForInpainting

repainted = pipe(prompt=prompt, image=init_image, mask_image=mask).images[0]

# 强制蒙版外区域与原图逐字节一致
final = pipe.image_processor.apply_overlay(mask, init_image, repainted)
final.save("out.png")

如果你想要显式合成(比如你不在 Diffusers 里),手动做 alpha 混合——蒙版外逐字节一致:

from PIL import Image
import numpy as np

mask_arr = np.array(mask.convert("L"))[:, :, None] / 255.0
orig_arr = np.array(init_image).astype(np.float32)
gen_arr = np.array(repainted).astype(np.float32)
final = (1 - mask_arr) * orig_arr + mask_arr * gen_arr
Image.fromarray(final.astype(np.uint8)).save("out.png")

AUTOMATIC1111 / Forge:在 Inpaint 标签设 Inpaint area: Only maskedMasked content: original。“Only masked” 会裁到蒙版、编辑、再贴回,所以画布其余部分不受影响。

ComfyUI:用 Set Latent Noise Mask(或 InpaintModelConditioning)节点驱动采样器,让只有蒙版区被去噪,然后在图末接一个 paste-by-mask / image-composite 节点,把蒙版外像素从原图还原。

步骤 2:用硬边蒙版

软边会溢出。如果需要柔过渡,先画硬蒙版,最后再加 alpha 渐变——不要让 diffusion 管线看到软蒙版。

# 把灰阶 / 羽化蒙版转成硬黑白蒙版(ImageMagick)
convert mask_soft.png -threshold 50% mask_hard.png

在 AUTOMATIC1111/Forge 里,做精确编辑时把 Mask blur 从默认 4 降到 0-2。在 Diffusers 里,跳过 mask_processor.blur() 或用很小的 blur_factor

步骤 3:减少 inpaint padding / 裁切边距

如果处理区伸到蒙版外太远:

pipe(
    prompt=prompt,
    image=init_image,
    mask_image=mask,
    strength=0.75,
    padding_mask_crop=8,   # 更小的边距;文档把 32 当作画质默认
)

4-8 像素边距能给干净接缝又不太溢出。在 AUTOMATIC1111/Forge 里把 Only masked padding, pixels 设小(比如 8);调高它会拉宽周边对填充的影响范围。

步骤 4:降低 strength

小改动(换色、换物):

strength = 0.7  # 不是 0.95

0.5 以下蒙版区几乎不变;0.85 以上溢出风险上升。0.7 是大多数编辑的好起点。在 AUTOMATIC1111/Forge 里这是 Denoising strength 滑块。

步骤 5:inpaint 时关闭 refiner

SDXL workflow 里,对蒙版编辑关掉 refiner:

inpaint_pipe(
    prompt=prompt,
    image=init_image,
    mask_image=mask,
    use_refiner=False,
)

如需精修,只在蒙版裁切区跑 refiner,然后贴回(顺序:base,然后 crop,然后 refine,然后 paste)。

步骤 6:用 PNG 输出并验证像素稳定性

始终把编辑后的图导出为 PNG(绝不用 JPEG),然后证明修复:

python -c "
from PIL import Image
import numpy as np
a = np.array(Image.open('orig.png').convert('RGB'))
b = np.array(Image.open('out.png').convert('RGB'))
m = np.array(Image.open('mask.png').convert('L')) > 128
outside_diff = int(np.abs(a[~m].astype(int) - b[~m].astype(int)).max())
print('Max outside-mask pixel diff:', outside_diff)
"

paste-back 工作正常 = 期望 0;VAE 往返 = 期望 <= 2(可接受)。再高 = 还在溢出——回到步骤 1。

步骤 7:极小编辑用裁剪-编辑-粘贴

非常小的变动(256x256 以内),裁出区域,在裁块里 inpaint,再贴回。绕过所有全图溢出:

crop = init_image.crop((x, y, x + 256, y + 256))
edited_crop = pipe(prompt, image=crop, mask_image=mask_crop).images[0]
init_image.paste(edited_crop, (x, y))

相关质量控制参见 AI 图像与提示词不符

如何确认已修好

  • 步骤 6 的像素 diff 打印 0(paste-back)或 <= 2(VAE 往返)。
  • 蒙版边界处肉眼检查:没有可见色阶或接缝线。
  • 在三张不同测试图上重跑。无溢出应该可重现,而不是某个 seed 的巧合。

长期预防

  • 永远 paste back。把全图重生成当作一种独立模式,只留给风格变换。
  • 把 inpaint workflow 存成命名预设:tight-edit(paste-back、硬蒙版、低 padding)vs blend-edit(软蒙版、较大 padding)vs full-redo(全图重做)。
  • 生产管线在 CI 里校验蒙版外像素 diff。
  • 记录你用的蒙版格式(8 位灰度、alpha、或 RGBA 蒙版通道)并坚持用。
  • inpaint 过的图绝不导出成 JPEG;总是 PNG。
  • 当编辑涉及合理影响全图的光照时,显式选择全图模式,而不是意外发现。

常见坑

  • 用高斯模糊蒙版因为它”在 UI 里看起来更柔”,没意识到软边会溢出。
  • 相信模型”尊重蒙版”,却从未做过像素 diff。
  • strength 拉到 1.0 做 inpaint,还指望蒙版外像素保留。
  • inpaint 之后让 SDXL refiner 跑而没关掉它。
  • 导出成 JPEG,然后把 JPEG 量化产生的色偏怪到模型头上。
  • 跑默认 workflow,里面 padding_mask_crop=64(或 Only masked padding, pixels: 64),结果被 64 像素溢出搞蒙圈。

FAQ

Q:我的 UI 写着”Only masked”为什么还有溢出?

检查 UI 是真的在生成后用原图做合成,还是只把蒙版区可视化。有些 UI 把”only masked”当视觉提示,但仍重生成整张画布。用硬像素 diff 确认(步骤 6),并确保 Masked contentoriginal 而不是 fill

Q:有没有内置方式保留蒙版外区域,还是我必须手动合成?

在 Diffusers 里有——pipeline.image_processor.apply_overlay(mask, init_image, result) 强制蒙版外区域与原图一致。官方文档标注了一个代价:接缝过渡稍硬,用小幅 mask blur 或最后再羽化即可解决。

Q:为什么 inpaint 专用 checkpoint 比普通的溢出更多?

是设计如此。inpaint checkpoint 被训练去混合蒙版内外以求自然接缝,所以它会刻意动周边。普通 checkpoint 对外部保留得更好,但常留下可见的蒙版轮廓。不论哪种,paste-back(步骤 1)都能让你同时拿到干净的外部inpaint 画质。

Q:inpaint 后的人脸修复会溢出吗?

会。人脸修复器(CodeFormer、GFPGAN)无视蒙版,会重做整张脸。在 inpaint 前跑人脸修复,或在 inpaint workflow 里跳过它。

Q:能在比源图更高分辨率上做 inpaint 吗?

可以——把裁切块放大、编辑、缩小、贴回。paste-back 必须缩回原分辨率,否则接缝处会有位移。

相关阅读

外部参考:Diffusers inpainting 指南ComfyUI VAE Encode (for Inpainting) 节点

标签: #排查 #ai-image #inpaint #mask #diffusion