图像到图像变换技术详解

摘要

本文深入探讨了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)实现的:

  1. 用户绘制遮罩标记需要修改的区域
  2. 系统将遮罩区域替换为噪声
  3. 生成过程只在遮罩区域内进行

遮罩处理的实现如下:

# 处理遮罩
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 参数调优策略

  1. 去噪强度(Denoising Strength)

    • 低值(0.2-0.4):轻微修改,保持原图结构
    • 中值(0.4-0.6):适度变化,保留主要元素
    • 高值(0.6-0.8):显著改变,创造新内容
    • 很高值(0.8-1.0):几乎全新生成
  2. CFG Scale

    • 较低值(5-7):更多创造性,贴近提示词
    • 中等值(7-12):平衡创造性和准确性
    • 较高值(12+):严格遵循提示词,可能过度饱和

4.2 遮罩技巧

  1. 柔和边缘:使用遮罩模糊功能创建自然过渡
  2. 分层处理:多次应用img2img逐步完善结果
  3. 精确控制:使用黑白遮罩精确控制修改区域

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过程中需要同时存储原始图像和生成图像,对显存要求较高。可以通过以下方式优化:

  1. 合理设置图像尺寸
  2. 使用合适的VAE编码方法
  3. 及时清理不需要的张量

6.2 计算效率

  1. 选择适当的采样器和步数
  2. 使用潜在空间处理而非像素空间
  3. 合理利用缓存机制

总结

图像到图像变换技术是Stable Diffusion WebUI中强大而灵活的功能,它结合了传统图像编辑的精确性和AI生成的创造性。通过深入理解其工作原理和实现机制,用户可以更好地利用这一工具进行各种创意工作。

从基础的图像修改到复杂的修补和风格迁移,img2img提供了丰富的可能性。随着对参数和技巧的熟练掌握,创作者可以实现越来越复杂和精美的视觉效果。未来,随着模型和算法的进一步发展,img2img技术还将带来更多惊喜和创新应用。

掌握img2img不仅是学会使用一个工具,更是理解如何将人工智能与人类创意相结合的过程。通过不断地实践和探索,用户可以开拓出属于自己的AI艺术创作之路。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CarlowZJ

我的文章对你有用的话,可以支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值