你 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。
如何识别:查那个值。如果是 32 或 64,预期蒙版外 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_factor、Mask blur)。 - inpaint padding / 裁切值(
padding_mask_crop、Only masked padding, pixels、grow_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 masked 和 Masked 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)vsblend-edit(软蒙版、较大 padding)vsfull-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 content 是 original 而不是 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) 节点