“扩散器图像填充”指南

社区文章 发布于2024年9月13日

这份指南是我一直以来的一个想法,但 pietrobolcato 在此处 提出了要求,所以我最终决定在它变得过时或我忘记之前完成它。

因此,基本思想是进行简单的物体移除或填充图像中要更改的选定部分,为此我们将使用ControlNet和一些简单的技术。

为了实现这一点,我们需要使用两个关键模型,一个是 ControlNetPlus Promax,另一个是使用光照模型,在本例中,由于我想要实现照片级真实感,我将使用 RealVis 5.0 Lighting

ControlNet不是Diffusers核心的一部分,但官方仓库提供了所有使其工作的说明,你需要`StableDiffusionXLControlNetUnionPipeline`。

我还建立了一个空间作为本指南的PoC,为此,我做了一个自定义管道,只包含了我们使其工作所需的部分。你可以在这里测试它,如果你在本地使用该应用,你可以看到图像如何生成和填充蒙版的酷炫效果。

首先我们需要一张图片,我从unsplash.com下载了一些。让我们用山中的汽车作为演示,原图在这里,由jeffersonsees拍摄。

由于本指南使用了带有自定义ControlNet的自定义管道,而不是核心部分,我无法发布完整的代码,否则它会太大,因此我将尝试给出使其工作的关键部分。此外,我将简化流程,假设图像为1024x1024的正方形,这在实际场景中并不理想,应调整为可与任何宽高比和分辨率的任何图像一起使用。

我将使用Pillow来避免进行过多的格式转换,所以让我们把图像变成正方形

from PIL import Image

from diffusers.utils import load_image


source_image = load_image(
    "https://huggingface.co/datasets/OzzyGT/testing-resources/resolve/main/diffusers_fill/jefferson-sees-OCQjiB4tG5c-unsplash.jpg"
)

width, height = source_image.size
min_dimension = min(width, height)

left = (width - min_dimension) / 2
top = (height - min_dimension) / 2
right = (width + min_dimension) / 2
bottom = (height + min_dimension) / 2

final_source = source_image.crop((left, top, right, bottom))
final_source = final_source.resize((1024, 1024), Image.LANCZOS)

然后我们需要一个遮罩,你可以使用任何方法获得它,你可以使用SAM2、BiRefNet或任何允许你遮罩物体的较新模型,也可以手动完成,因为这不涉及遮罩,我将使用图像修复遮罩生成器来生成一个。

image/png

现在我们有了两张图片,我们需要做的是从原始图片中删除被遮罩的部分,结果就是我们要输入ControlNet的图片。

mask = load_image(
    "https://huggingface.co/datasets/OzzyGT/testing-resources/resolve/main/diffusers_fill/car_mask.png"
).convert("L")

inverted_mask = ImageChops.invert(mask)
cnet_image = final_source.copy()
cnet_image.putalpha(inverted_mask)

这是该技术的第一部分,第二部分是使用光照模型,其步骤比非蒸馏模型少,并且在整个步骤中以全强度使用ControlNet的瓦片模式,以便尽可能多地保留原始图像。

对于这部分,我将假设以下内容

  • 您已下载ControlNetModel_Union模型python文件并将其放在与脚本相同的目录中。
  • 您已在本地下载了ControlNet模型权重并相应地重命名了文件。

第二个原因是官方存储库没有Promax版本模型的易于使用的格式,如果您想了解如何直接从Hub加载它,您可以阅读官方存储库或查看空间中的应用程序代码。

import torch
from controlnet_union import ControlNetModel_Union
from diffusers import AutoencoderKL, StableDiffusionXLControlNetPipeline, TCDScheduler

vae = AutoencoderKL.from_pretrained("madebyollin/sdxl-vae-fp16-fix", torch_dtype=torch.float16).to("cuda")

controlnet_model = ControlNetModel_Union.from_pretrained(
    "./controlnet-union-sdxl-1.0",
    torch_dtype=torch.float16,
)

pipe = StableDiffusionXLControlNetPipeline.from_pretrained(
    "SG161222/RealVisXL_V5.0_Lightning",
    torch_dtype=torch.float16,
    vae=vae,
    custom_pipeline="OzzyGT/pipeline_sdxl_fill",
    controlnet=controlnet_model,
    variant="fp16",
).to("cuda")
pipe.scheduler = TCDScheduler.from_config(pipe.scheduler.config)

prompt = "high quality"
(
    prompt_embeds,
    negative_prompt_embeds,
    pooled_prompt_embeds,
    negative_pooled_prompt_embeds,
) = pipe.encode_prompt(prompt, "cuda", True)

image = pipe(
    prompt_embeds=prompt_embeds,
    negative_prompt_embeds=negative_prompt_embeds,
    pooled_prompt_embeds=pooled_prompt_embeds,
    negative_pooled_prompt_embeds=negative_pooled_prompt_embeds,
    image=cnet_image,
)

有了这些,我们得到了这样的图像

我故意做了一个糟糕的遮罩,它留下了一些原始汽车的细节,使得生成效果很奇怪或很糟糕,有时我们会得到汽车的边缘,或者像这样的东西,我甚至得到了一头水牛!

所以现在我们知道遮罩对结果影响很大,我将使用GIMP制作一个更详细的遮罩,因为遮罩不会是纯白色和黑色的遮罩,我们需要将其转换为二值遮罩。

mask = load_image(
    "https://huggingface.co/datasets/OzzyGT/testing-resources/resolve/main/diffusers_fill/car_mask_good.png"
).convert("L")

binary_mask = mask.point(lambda p: 255 if p > 0 else 0)
inverted_mask = ImageChops.invert(binary_mask)

我的自定义管道在底层做了很多你通常需要做的事情,这意味着设置ControlNet的步数、比例以及适当的模式和图像。如果你愿意,可以使用它,但请记住,它非常严格,主要用于我在本指南中的用途。

另请注意,我为此使用了TCD调度器,因为我认为它与光照模型配合得最好,我还尝试使用PAG,但由于某种原因,它使结果变得更糟。

现在我们得到这样的结果

本指南的最后一步是,如果你仔细观察,图像仍然会发生变化,如果原始图像像这张一样质量很好,你会看到它质量下降,一些较小的细节变得模糊,所以为了解决这个问题,我们只需将原始的alpha图像粘贴上去,这项技术的美妙之处在于它能无缝融合,大多数人如果你不告诉他们,他们不会知道这是经过修复的(我测试过)。

生成 合并
image/png image/png

在这个例子中,由于灌木丛有很多细节,如果你仔细观察,你可以看到过渡,所以根据你的使用情况,最好不要进行最终的粘贴,但再次强调,大多数人甚至不会注意到这一点。

以下是一些更多示例(您也可以使用该空间进行测试)

原始 填充
image/jpeg image/png
image/jpeg image/png

图片来源

第一张:原图 Leonardo Iribe拍摄

第二张:原图Raymond Petrik拍摄

还有一个额外的好处,我计划将其用于一个新的外画指南,那就是你可以扩展图像,所以这对于生成一个我们可以稍后添加细节的临时背景非常理想。

原始 已扩展
image/png image/png

为了改善效果,我鼓励您使用一些更高级的技术,例如

  • 使用差分扩散将接缝与原始图像融合
  • 放大遮罩后的最终生成图像,将其与img2img结合使用以添加更多细节,然后将其粘贴回原始图像上。
  • 当ControlNets达到如此出色的效果时,将其与更好的模型(SD3或Flux)结合使用。

本指南到此结束,我希望它能帮助您学习如何使用这个出色的ControlNet,并为您在工作中获得高质量图像提供一个良好的开端。

最终完整代码,包含最终合并

import torch
from PIL import Image, ImageChops

from controlnet_union import ControlNetModel_Union
from diffusers import AutoencoderKL, StableDiffusionXLControlNetPipeline, TCDScheduler
from diffusers.utils import load_image


source_image = load_image(
    "https://huggingface.co/datasets/OzzyGT/testing-resources/resolve/main/diffusers_fill/jefferson-sees-OCQjiB4tG5c-unsplash.jpg"
)

width, height = source_image.size
min_dimension = min(width, height)

left = (width - min_dimension) / 2
top = (height - min_dimension) / 2
right = (width + min_dimension) / 2
bottom = (height + min_dimension) / 2

final_source = source_image.crop((left, top, right, bottom))
final_source = final_source.resize((1024, 1024), Image.LANCZOS).convert("RGBA")

mask = load_image(
    "https://huggingface.co/datasets/OzzyGT/testing-resources/resolve/main/diffusers_fill/car_mask_good.png"
).convert("L")

binary_mask = mask.point(lambda p: 255 if p > 0 else 0)
inverted_mask = ImageChops.invert(binary_mask)

alpha_image = Image.new("RGBA", final_source.size, (0, 0, 0, 0))
cnet_image = Image.composite(final_source, alpha_image, inverted_mask)

vae = AutoencoderKL.from_pretrained("madebyollin/sdxl-vae-fp16-fix", torch_dtype=torch.float16).to("cuda")

controlnet_model = ControlNetModel_Union.from_pretrained(
    "./controlnet-union-sdxl-1.0",
    torch_dtype=torch.float16,
)

pipe = StableDiffusionXLControlNetPipeline.from_pretrained(
    "SG161222/RealVisXL_V5.0_Lightning",
    torch_dtype=torch.float16,
    vae=vae,
    custom_pipeline="OzzyGT/pipeline_sdxl_fill",
    controlnet=controlnet_model,
    variant="fp16",
).to("cuda")
pipe.scheduler = TCDScheduler.from_config(pipe.scheduler.config)

prompt = "high quality"
(
    prompt_embeds,
    negative_prompt_embeds,
    pooled_prompt_embeds,
    negative_pooled_prompt_embeds,
) = pipe.encode_prompt(prompt, "cuda", True)

image = pipe(
    prompt_embeds=prompt_embeds,
    negative_prompt_embeds=negative_prompt_embeds,
    pooled_prompt_embeds=pooled_prompt_embeds,
    negative_pooled_prompt_embeds=negative_pooled_prompt_embeds,
    image=cnet_image,
)

image = image.convert("RGBA")
cnet_image.paste(image, (0, 0), binary_mask)

cnet_image.save("final_generation.png")

社区

image (23).webp

注册登录发表评论