摘要
本文深入探讨了Stable Diffusion WebUI中的图像到图像(img2img)变换技术。我们将从基本原理、核心实现、多种模式应用以及高级技巧等方面全面解析这一重要功能,帮助开发者和使用者更好地理解和应用img2img技术。
引言
图像到图像变换(img2img)是Stable Diffusion WebUI中一项极其重要的功能,它允许用户基于现有图像进行修改和再创作,而不是从零开始生成。这项技术结合了原始图像的内容信息和用户提示词的指导,能够实现图像修复、风格迁移、细节增强等多种创意应用。相比纯文本生成图像,img2img提供了更强的可控性和实用性。
一、图像到图像变换的基本原理
1.1 核心工作机制
img2img的工作原理基于扩散模型的特性。在图像生成过程中,模型通过逐步去噪来创建图像。img2img利用这一特性,不是从纯噪声开始,而是从经过特定程度破坏的输入图像开始生成过程。去噪的程度由"Denoising strength"(去噪强度)参数控制,范围从0到1:
- 0.0: 完全保留原始图像,不做任何修改
- 1.0: 完全忽略原始图像,相当于从随机噪声开始生成
1.2 潜在空间编码
Stable Diffusion在潜在空间(latent space)中进行操作,因此输入图像首先需要通过VAE(变分自编码器)编码到潜在空间:
# 编码到潜在空间
self.init_latent = images_tensor_to_samples(image,
approximation_indexes.get(opts.sd_vae_encode_method),
self.sd_model)
这样做的好处是可以大大减少计算量,因为潜在空间的维度远小于像素空间。
1.3 噪声添加与重建
在img2img过程中,系统会根据去噪强度参数向编码后的潜在表示添加相应强度的噪声:
# 在采样过程中添加噪声
x = self.rng.next()
if self.initial_noise_multiplier != 1.0:
self.extra_generation_params["Noise multiplier"] = self.initial_noise_multiplier
x *= self.initial_noise_multiplier
然后通过反向扩散过程逐步去除噪声,生成新的图像。
二、img2img的不同模式及实现
2.1 基础img2img模式
基础img2img模式是最简单的图像变换方式,它接受一张输入图像并根据提示词对其进行修改。在[img2img.py](file:///E:/project/stable-diffusion-webui/modules/img2img.py)中可以看到模式选择的实现:
if mode == 0: # img2img
image = init_img
mask = None
elif mode == 1: # img2img sketch
image = sketch
mask = None
elif mode == 2: # inpaint
image, mask = init_img_with_mask["image"], init_img_with_mask["mask"]
mask = processing.create_binary_mask(mask)
elif mode == 3: # inpaint sketch
image = inpaint_color_sketch
orig = inpaint_color_sketch_orig or inpaint_color_sketch
pred = np.any(np.array(image) != np.array(orig), axis=-1)
mask = Image.fromarray(pred.astype(np.uint8) * 255, "L")
mask = ImageEnhance.Brightness(mask).enhance(1 - mask_alpha / 100)
blur = ImageFilter.GaussianBlur(mask_blur)
image = Image.composite(image.filter(blur), orig, mask.filter(blur))
elif mode == 4: # inpaint upload mask
image = init_img_inpaint
mask = init_mask_inpaint
2.2 图像修补模式(Inpainting)
图像修补是img2img的一个重要应用,它允许用户指定图像的某些区域进行修改,而保持其他区域不变。这是通过遮罩(Mask)实现的:
- 用户绘制遮罩标记需要修改的区域
- 系统将遮罩区域替换为噪声
- 生成过程只在遮罩区域内进行
遮罩处理的实现如下:
# 处理遮罩
if image_mask is not None:
# 将RGBA遮罩转换为二值遮罩
image_mask = create_binary_mask(image_mask, round=self.mask_round)
# 反转遮罩
if self.inpainting_mask_invert:
image_mask = ImageOps.invert(image_mask)
self.extra_generation_params["Mask mode"] = "Inpaint not masked"
# 应用遮罩模糊
if self.mask_blur_x > 0:
np_mask = np.array(image_mask)
kernel_size = 2 * int(2.5 * self.mask_blur_x + 0.5) + 1
np_mask = cv2.GaussianBlur(np_mask, (kernel_size, 1), self.mask_blur_x)
image_mask = Image.fromarray(np_mask)
if self.mask_blur_y > 0:
np_mask = np.array(image_mask)
kernel_size = 2 * int(2.5 * self.mask_blur_y + 0.5) + 1
np_mask = cv2.GaussianBlur(np_mask, (1, kernel_size), self.mask_blur_y)
image_mask = Image.fromarray(np_mask)
2.3 修补草图模式(Inpaint Sketch)
修补草图模式允许用户通过绘制彩色草图来指导修补过程。系统会自动检测用户绘制的区域作为遮罩:
elif mode == 3: # inpaint sketch
image = inpaint_color_sketch
orig = inpaint_color_sketch_orig or inpaint_color_sketch
pred = np.any(np.array(image) != np.array(orig), axis=-1)
mask = Image.fromarray(pred.astype(np.uint8) * 255, "L")
mask = ImageEnhance.Brightness(mask).enhance(1 - mask_alpha / 100)
blur = ImageFilter.GaussianBlur(mask_blur)
image = Image.composite(image.filter(blur), orig, mask.filter(blur))
三、核心实现分析
3.1 初始化处理
在[StableDiffusionProcessingImg2Img.init](file:///E:/project/stable-diffusion-webui/modules/processing.py#L1649-L1792)方法中,系统会对输入图像进行预处理:
def init(self, all_prompts, all_seeds, all_subseeds):
self.extra_generation_params["Denoising strength"] = self.denoising_strength
self.image_cfg_scale: float = self.image_cfg_scale if shared.sd_model.cond_stage_key == "edit" else None
self.sampler = sd_samplers.create_sampler(self.sampler_name, self.sd_model)
crop_region = None
image_mask = self.image_mask
# 处理遮罩
if image_mask is not None:
# 将RGBA遮罩转换为二值遮罩
image_mask = create_binary_mask(image_mask, round=self.mask_round)
# 反转遮罩
if self.inpainting_mask_invert:
image_mask = ImageOps.invert(image_mask)
self.extra_generation_params["Mask mode"] = "Inpaint not masked"
# 应用遮罩模糊
if self.mask_blur_x > 0:
np_mask = np.array(image_mask)
kernel_size = 2 * int(2.5 * self.mask_blur_x + 0.5) + 1
np_mask = cv2.GaussianBlur(np_mask, (kernel_size, 1), self.mask_blur_x)
image_mask = Image.fromarray(np_mask)
if self.mask_blur_y > 0:
np_mask = np.array(image_mask)
kernel_size = 2 * int(2.5 * self.mask_blur_y + 0.5) + 1
np_mask = cv2.GaussianBlur(np_mask, (1, kernel_size), self.mask_blur_y)
image_mask = Image.fromarray(np_mask)
# 全局修复或局部修复
if self.inpaint_full_res:
self.mask_for_overlay = image_mask
mask = image_mask.convert('L')
crop_region = masking.get_crop_region_v2(mask, self.inpaint_full_res_padding)
if crop_region:
crop_region = masking.expand_crop_region(crop_region, self.width, self.height,
mask.width, mask.height)
x1, y1, x2, y2 = crop_region
mask = mask.crop(crop_region)
image_mask = images.resize_image(2, mask, self.width, self.height)
self.paste_to = (x1, y1, x2-x1, y2-y1)
self.extra_generation_params["Inpaint area"] = "Only masked"
self.extra_generation_params["Masked area padding"] = self.inpaint_full_res_padding
else:
# 如果遮罩为空,切换到img2img模式
crop_region = None
image_mask = None
self.mask_for_overlay = None
self.inpaint_full_res = False
massage = 'Unable to perform "Inpaint Only mask" because mask is blank, switch to img2img mode.'
model_hijack.comments.append(massage)
logging.info(massage)
else:
image_mask = images.resize_image(self.resize_mode, image_mask, self.width, self.height)
np_mask = np.array(image_mask)
np_mask = np.clip((np_mask.astype(np.float32)) * 2, 0, 255).astype(np.uint8)
self.mask_for_overlay = Image.fromarray(np_mask)
self.overlay_images = []
# 处理潜在空间遮罩
latent_mask = self.latent_mask if self.latent_mask is not None else image_mask
# 设置颜色校正
add_color_corrections = opts.img2img_color_correction and self.color_corrections is None
if add_color_corrections:
self.color_corrections = []
imgs = []
for img in self.init_images:
# 保存初始图像
if opts.save_init_img:
self.init_img_hash = hashlib.md5(img.tobytes()).hexdigest()
images.save_image(img, path=opts.outdir_init_images, basename=None,
forced_filename=self.init_img_hash, save_to_dirs=False,
existing_info=img.info)
# 展平图像
image = images.flatten(img, opts.img2img_background_color)
# 调整图像大小
if crop_region is None and self.resize_mode != 3:
image = images.resize_image(self.resize_mode, image, self.width, self.height)
# 应用遮罩
if image_mask is not None:
if self.mask_for_overlay.size != (image.width, image.height):
self.mask_for_overlay = images.resize_image(self.resize_mode,
self.mask_for_overlay,
image.width, image.height)
image_masked = Image.new('RGBa', (image.width, image.height))
image_masked.paste(image.convert("RGBA").convert("RGBa"),
mask=ImageOps.invert(self.mask_for_overlay.convert('L')))
self.overlay_images.append(image_masked.convert('RGBA'))
# 裁剪区域处理
if crop_region is not None:
image = image.crop(crop_region)
image = images.resize_image(2, image, self.width, self.height)
# 填充遮罩区域
if image_mask is not None:
if self.inpainting_fill != 1:
image = masking.fill(image, latent_mask)
if self.inpainting_fill == 0:
self.extra_generation_params["Masked content"] = 'fill'
# 颜色校正
if add_color_corrections:
self.color_corrections.append(setup_color_correction(image))
# 转换为张量
image = np.array(image).astype(np.float32) / 255.0
image = np.moveaxis(image, 2, 0)
imgs.append(image)
# 批量处理图像
if len(imgs) == 1:
batch_images = np.expand_dims(imgs[0], axis=0).repeat(self.batch_size, axis=0)
if self.overlay_images is not None:
self.overlay_images = self.overlay_images * self.batch_size
if self.color_corrections is not None and len(self.color_corrections) == 1:
self.color_corrections = self.color_corrections * self.batch_size
elif len(imgs) <= self.batch_size:
self.batch_size = len(imgs)
batch_images = np.array(imgs)
else:
raise RuntimeError(f"bad number of images passed: {len(imgs)}; expecting {self.batch_size} or less")
image = torch.from_numpy(batch_images)
image = image.to(shared.device, dtype=devices.dtype_vae)
# 编码到潜在空间
if opts.sd_vae_encode_method != 'Full':
self.extra_generation_params['VAE Encoder'] = opts.sd_vae_encode_method
self.init_latent = images_tensor_to_samples(image,
approximation_indexes.get(opts.sd_vae_encode_method),
self.sd_model)
devices.torch_gc()
# 调整大小模式3处理
if self.resize_mode == 3:
self.init_latent = torch.nn.functional.interpolate(self.init_latent,
size=(self.height // opt_f, self.width // opt_f),
mode="bilinear")
# 处理遮罩
if image_mask is not None:
init_mask = latent_mask
latmask = init_mask.convert('RGB').resize((self.init_latent.shape[3],
self.init_latent.shape[2]))
latmask = np.moveaxis(np.array(latmask, dtype=np.float32), 2, 0) / 255
latmask = latmask[0]
if self.mask_round:
latmask = np.around(latmask)
latmask = np.tile(latmask[None], (self.init_latent.shape[1], 1, 1))
self.mask = torch.asarray(1.0 - latmask).to(shared.device).type(devices.dtype)
self.nmask = torch.asarray(latmask).to(shared.device).type(devices.dtype)
# 填充遮罩内容
if self.inpainting_fill == 2:
self.init_latent = self.init_latent * self.mask + create_random_tensors(
self.init_latent.shape[1:], all_seeds[0:self.init_latent.shape[0]]
) * self.nmask
self.extra_generation_params["Masked content"] = 'latent noise'
elif self.inpainting_fill == 3:
self.init_latent = self.init_latent * self.mask
self.extra_generation_params["Masked content"] = 'latent nothing'
# 设置图像条件
self.image_conditioning = self.img2img_image_conditioning(image * 2 - 1, self.init_latent,
image_mask, self.mask_round)
3.2 采样过程
img2img的采样过程在[StableDiffusionProcessingImg2Img.sample](file:///E:/project/stable-diffusion-webui/modules/processing.py#L1771-L1791)方法中实现:
def sample(self, conditioning, unconditional_conditioning, seeds, subseeds, subseed_strength, prompts):
# 生成噪声
x = self.rng.next()
# 应用噪声乘数
if self.initial_noise_multiplier != 1.0:
self.extra_generation_params["Noise multiplier"] = self.initial_noise_multiplier
x *= self.initial_noise_multiplier
# 执行采样前的脚本
if self.scripts is not None:
self.scripts.process_before_every_sampling(
p=self,
x=self.init_latent,
noise=x,
c=conditioning,
uc=unconditional_conditioning
)
# 执行图像到图像采样
samples = self.sampler.sample_img2img(self, self.init_latent, x, conditioning,
unconditional_conditioning,
image_conditioning=self.image_conditioning)
# 应用遮罩混合
if self.mask is not None:
blended_samples = samples * self.nmask + self.init_latent * self.mask
if self.scripts is not None:
mba = scripts.MaskBlendArgs(samples, self.nmask, self.init_latent,
self.mask, blended_samples)
self.scripts.on_mask_blend(self, mba)
blended_samples = mba.blended_latent
samples = blended_samples
# 清理内存
del x
devices.torch_gc()
return samples
四、高级应用技巧
4.1 参数调优策略
-
去噪强度(Denoising Strength):
- 低值(0.2-0.4):轻微修改,保持原图结构
- 中值(0.4-0.6):适度变化,保留主要元素
- 高值(0.6-0.8):显著改变,创造新内容
- 很高值(0.8-1.0):几乎全新生成
-
CFG Scale:
- 较低值(5-7):更多创造性,贴近提示词
- 中等值(7-12):平衡创造性和准确性
- 较高值(12+):严格遵循提示词,可能过度饱和
4.2 遮罩技巧
- 柔和边缘:使用遮罩模糊功能创建自然过渡
- 分层处理:多次应用img2img逐步完善结果
- 精确控制:使用黑白遮罩精确控制修改区域
4.3 批处理功能
WebUI支持批量处理图像,这对于处理大量相似图像非常有用:
def process_batch(p, input, output_dir, inpaint_mask_dir, args, to_scale=False, scale_by=1.0, use_png_info=False, png_info_props=None, png_info_dir=None):
# 处理一批图像
for i, image in enumerate(batch_images):
# 读取并处理每个图像
img = images.read(image)
p.init_images = [img] * p.batch_size
# 执行处理
proc = process_images(p)
五、实际应用场景
5.1 图像修复与增强
- 修复老照片的损坏部分
- 增强低质量图像的细节
- 移除图像中的不需要元素
5.2 风格迁移
- 将照片转换为艺术风格
- 统一多张图像的视觉风格
- 创建特定艺术家风格的作品
5.3 创意设计
- 基于草图生成完整图像
- 扩展现有图像的画布
- 创建图像序列和动画帧
六、性能优化要点
6.1 内存管理
img2img过程中需要同时存储原始图像和生成图像,对显存要求较高。可以通过以下方式优化:
- 合理设置图像尺寸
- 使用合适的VAE编码方法
- 及时清理不需要的张量
6.2 计算效率
- 选择适当的采样器和步数
- 使用潜在空间处理而非像素空间
- 合理利用缓存机制
总结
图像到图像变换技术是Stable Diffusion WebUI中强大而灵活的功能,它结合了传统图像编辑的精确性和AI生成的创造性。通过深入理解其工作原理和实现机制,用户可以更好地利用这一工具进行各种创意工作。
从基础的图像修改到复杂的修补和风格迁移,img2img提供了丰富的可能性。随着对参数和技巧的熟练掌握,创作者可以实现越来越复杂和精美的视觉效果。未来,随着模型和算法的进一步发展,img2img技术还将带来更多惊喜和创新应用。
掌握img2img不仅是学会使用一个工具,更是理解如何将人工智能与人类创意相结合的过程。通过不断地实践和探索,用户可以开拓出属于自己的AI艺术创作之路。
1278

被折叠的 条评论
为什么被折叠?



