外拓绘图 I - Controlnet 版本

社区文章 发布于 2024 年 4 月 23 日

我所知道的至少有三种外拓绘图方法,每种方法都有不同的变体和步骤。这是第一种使用 ControlNet 的方法,你可以在这里阅读其他方法:

使用 ControlNet 进行外拓绘图需要使用蒙版,因此这种方法只适用于你可以在想要扩展的区域周围绘制白色蒙版的情况。使用此方法无需事先准备区域,但它受到图像大小的限制,只能是你的 VRAM 允许的大小。

1.- 原始图像

在这种情况下,我将使用 Laidawang 在此评论中提供的狼图像。

2.- 外拓绘图

起始提示词是 `a wolf playing basketball`,我将使用 Juggernaut V9 模型。

destitech 训练了一个用于图像修复的 SDXL ControlNet 模型,名为 controlnet-inpaint-dreamer-sdxl。这是一个早期 Alpha 版本,但我认为它在大多数情况下效果很好。

controlnet = ControlNetModel.from_pretrained(
    "destitech/controlnet-inpaint-dreamer-sdxl", torch_dtype=torch.float16, variant="fp16"
)

这个 ControlNet 模型非常容易使用,你只需要将想要替换的部分涂成白色,所以在这种情况下,我将把图像的透明部分涂成白色。

为了将图像的 alpha 通道涂成白色,我使用这段代码

response = requests.get("https://huggingface.co/datasets/OzzyGT/testing-resources/resolve/main/outpainting/313891870-adb6dc80-2e9e-420c-bac3-f93e6de8d06b.png?download=true")

control_image = Image.open(BytesIO(response.content))
new_controlnet_image = Image.new("RGBA", control_image.size, "WHITE")
new_controlnet_image.alpha_composite(control_image)

这张图片的大小是 720x720 像素,SDXL 最好使用 1024x1024 的图片,所以生成的图片会被放大,你可以使用更小的图片,但可能会得到质量更低的图片。

条件尺度会影响原始图像的保留程度,由于这是外拓绘图,使用较高的值是安全的;对于内绘图和复杂图像,最好使用 0.5 左右的较低值。

pipeline = StableDiffusionXLControlNetPipeline.from_pretrained(
    "RunDiffusion/Juggernaut-XL-v9",
    torch_dtype=torch.float16,
    variant="fp16",
    controlnet=controlnet,
).to("cuda")

image = pipeline(
    prompt=prompt,
    negative_prompt=negative_prompt,
    height=1024,
    width=1024,
    guidance_scale=6.5,
    num_inference_steps=25,
    generator=generator,
    image=new_controlnet_image,
    controlnet_conditioning_scale=0.9,
    control_guidance_end=0.9,
).images[0]

使用相同的种子,我得到了这些结果

0.2 0.5 0.7 0.9 1.0
image/png image/png image/png image/png image/png

我总是倾向于让模型有一些自由度,这样它就可以调整微小的细节,使图像更连贯,所以在这种情况下,我将使用 `0.9`。

3.- 使用 IP Adapter 更好地外拓绘图

目前我认为我们已经达到了其他解决方案的水平,但是假设我们希望狼看起来就像原始图像一样,为此,我想给模型更多关于狼的上下文以及我希望它在哪里出现,所以我将为此使用 IP Adapter。

有一个对我很有用的小技巧,那就是我使用我想要的基础生成图像,并在其上绘制狼的蒙版,然后将其用作 IP Adapter 的注意力蒙版。

我生成蒙版的过程是这样的

基础图像 狼形蒙版 背景
image/png image/png image/png

对我来说,用 GIMP 这样的工具做这个花不了我一分钟。

我正在使用 IP Plus Adapter,比例为 0.4,有了这些设置,我们现在得到了这些结果。

image/png image/png

我猜测这种方法之所以有效,是因为我们绘制了狼的形状并为其提供了狼的输入图像,模型会尝试保持形状、位置和连贯性。简而言之,我们为模型提供了更多的上下文,使其能够生成最终图像,现在它看起来更像原始图像了。

例如,如果我们不使用带有这个 IP Adapter 的蒙版,结果会是这样的

image/png image/png

4.- 更好的提示词

现在图像看起来很不错了,但我总是想要更多,所以我们用提示词来改进它。

我将把提示词改为这样:

“一只狼打篮球的高质量照片,高度细节,专业,戏剧性的环境光,电影般的,动态背景,焦点”

同时,我将给 ControlNet 更多的自由度,设置 `control_guidance_end=0.9`,这样它就可以在不受限制的情况下完成细节。

image/png image/png

5.- 图像到图像的转换

最后,如果你曾经做过图像或视频合成,你会知道应用滤镜到整个合成以统一最终外观是常见的做法。对于稳定扩散也是如此,它有时能更好地隐藏接缝,如果存在的话。为此,我将对整个图像进行最后一次图像到图像的转换。

Diffusers 允许仅用一行代码更改管道,同时保留加载的模型,这正是我们这里需要的,我们只需要卸载 ControlNet 模型。

pipeline_img2img = AutoPipelineForImage2Image.from_pipe(pipeline, controlnet=None)

VAE 解码是一个有损过程,所以每次我们编码或解码时,我们都会丢失细节并降低图像质量,为了防止这种情况,我们需要尽可能多地留在潜在空间中。

如果你将 `output_type="latent"` 传递给管道,Diffusers 就允许这样做。然后我们将潜在向量输入到图像到图像的管道中,但在此之前,我还想给它一个更电影化的外观,所以我将再次更改提示词。

提示词 = "一只狼打篮球的电影剧照,高度细节,高预算好莱坞电影,宽银幕,史诗,华丽,胶片颗粒"

这应该只是一个快速通道,所以我将步数设置为 30,强度设置为 0.2。

这些是最终结果

image/png image/png
image/png image/png

在我看来,这些图像与使用其他 UI 完成的外拓绘图一样好甚至更好,希望它们能帮助人们更好地理解 Diffusers 的功能。

6.- 使用 IP Adapter 的外拓绘图技巧

作为奖励,这里有一个巧妙的小技巧。之前我使用带有蒙版的 IP Adapter 为生成提供更多的初始图像。然而,这也可以不使用蒙版。这样做的效果是为模型提供了更多的上下文,使其能够更好地猜测图像的其余部分,例如仅使用提示词“高质量”。

原始图片 不使用 IP Adapter 使用 IP Adapter
image/png image/png image/png

所以,如果我像之前那样做,但除了 IP Adapter 蒙版之外,最终结果是这样的:

image/png image/png

别问我埃菲尔铁塔为什么在那里 ^^

完整代码

import random
from io import BytesIO

import requests
import torch
from PIL import Image

from diffusers import (
    AutoPipelineForImage2Image,
    ControlNetModel,
    DPMSolverMultistepScheduler,
    StableDiffusionXLControlNetPipeline,
)
from diffusers.image_processor import IPAdapterMaskProcessor
from diffusers.utils import load_image, logging
from diffusers.utils.logging import set_verbosity


set_verbosity(logging.ERROR)  # to not show cross_attention_kwargs..by AttnProcessor2_0 warnings

controlnet = ControlNetModel.from_pretrained(
    "destitech/controlnet-inpaint-dreamer-sdxl", torch_dtype=torch.float16, variant="fp16"
)

pipeline = StableDiffusionXLControlNetPipeline.from_pretrained(
    "RunDiffusion/Juggernaut-XL-v9",
    torch_dtype=torch.float16,
    variant="fp16",
    controlnet=controlnet,
).to("cuda")

pipeline.scheduler = DPMSolverMultistepScheduler.from_config(pipeline.scheduler.config)
pipeline.scheduler.config.use_karras_sigmas = True

pipeline.load_ip_adapter(
    "h94/IP-Adapter",
    subfolder="sdxl_models",
    weight_name="ip-adapter-plus_sdxl_vit-h.safetensors",
    image_encoder_folder="models/image_encoder",
)
pipeline.set_ip_adapter_scale(0.4)

ip_wolf_image = load_image(
    "https://huggingface.co/datasets/OzzyGT/testing-resources/resolve/main/outpainting/ip_wolf_source.png?download=true"
)

ip_mask = load_image(
    "https://huggingface.co/datasets/OzzyGT/testing-resources/resolve/main/outpainting/wolf_position_mask.png?download=true"
)

processor = IPAdapterMaskProcessor()
ip_masks = processor.preprocess(ip_mask, height=1024, width=1024)

response = requests.get(
    "https://huggingface.co/datasets/OzzyGT/testing-resources/resolve/main/outpainting/313891870-adb6dc80-2e9e-420c-bac3-f93e6de8d06b.png?download=true"
)
control_image = Image.open(BytesIO(response.content))
new_controlnet_image = Image.new("RGBA", control_image.size, "WHITE")
new_controlnet_image.alpha_composite(control_image)

prompt = "high quality photo of a wolf playing basketball, highly detailed, professional, dramatic ambient light, cinematic, dynamic background, focus"
negative_prompt = ""

seed = random.randint(0, 2**32 - 1)
generator = torch.Generator(device="cpu").manual_seed(seed)

latents = pipeline(
    prompt=prompt,
    negative_prompt=negative_prompt,
    height=1024,
    width=1024,
    guidance_scale=6.5,
    num_inference_steps=25,
    generator=generator,
    image=new_controlnet_image,
    controlnet_conditioning_scale=0.9,
    control_guidance_end=0.9,
    ip_adapter_image=ip_wolf_image,
    cross_attention_kwargs={"ip_adapter_masks": ip_masks},
    output_type="latent",
).images[0]

pipeline_img2img = AutoPipelineForImage2Image.from_pipe(pipeline, controlnet=None)

prompt = "cinematic film still of a wolf playing basketball, highly detailed, high budget hollywood movie, cinemascope, epic, gorgeous, film grain"

image = pipeline_img2img(
    prompt=prompt,
    negative_prompt=negative_prompt,
    guidance_scale=3.0,
    num_inference_steps=30,
    generator=generator,
    image=latents,
    strength=0.2,
    ip_adapter_image=ip_wolf_image,
    cross_attention_kwargs={"ip_adapter_masks": ip_masks},
).images[0]

image.save("result.png")

社区

注册登录 发表评论