4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

画像処理・画像解析Advent Calendar 2023

Day 25

controlnet inpaintで画像の一部を入れ替えたり、表情を変えたり、描き加えて拡張したり

Last updated at Posted at 2024-07-22

この記事はdiffusers(Stable Diffusion)のcontrolnet inpaint機能を使って既存の画像に色んな加工をする方法について説明します。

inpaintとは

インペイントinpaint)というのは画像の一部を修正することです。これはStable Diffusionだけの用語ではなく、opencvなど従来の画像編集ライブラリーや他の生成AIもインペイント機能を持っています。

Stable Diffusionでinpaintをする方法は主に2つに分けられています。

  • (従来の)inpaintを使う
  • controlnet inpaintを使う

従来のinpaintについてはこの前の記事の最後の部分で説明しました。

しかしそこに書いた通り、あまり使い物にならなくて使うのを諦めました。

それに対しcontrolnet inpaintはかなり使い勝手がよくて凄いものです。だから今回の記事でcontrolnet inpaintの使い方について説明したいです。

controlnet inpaintとは

controlnet inpaintはcontrolnetの沢山ある機能の一つです。controlnetとは、2023年2月に中国の張呂敏チャン リュミン(lllyasviel)等によって発表された、ある画像を基準として生成する画像の結果をコントロールするStable Diffusionの機能です。コントロールする方式は色々あって多様ですが、inpaintはその中でもかなり使い方は独特です。

diffusersでは従来のinpaintを使う時にStableDiffusionInpaintPipelineクラスを使います。そして普通のcontrolnetを使う時に
StableDiffusionControlNetPipelineクラスを使います。しかしcontrolnet inpaintを使う時にStableDiffusionControlNetInpaintPipelineというクラスを使います。(名前長すぎ!)

つまりcontrolnetでありながらinpaintですね。inpaintもどきのcontrolnetとも、controlnet版のinpaintとも言えます。独自の呼び方があるわけではないが、ここではとりあえず「controlnet inpaint」と呼びます。区別するためにStableDiffusionInpaintPipelineを使うinpaintは「従来のinpaint」と呼びます。

他のcontrolnetも今後の記事で説明する予定ですが、まず一番わかりやすいcontrolnet inpaintから説明します。

controlnet inpaintと従来のinpaintとの違い

controlnet inpaintの使い方は基本的に従来のinpaintとほぼ同じですが、使うモデルは違います。

controlnet inpaintに使うモデルはここにあります。(diffusersで使う場合from_pretrainedで使うことができるから直接ダウンロードする必要はない)

これは作者のlllyasviel氏が2024年4月に発表したcontrolnet 1.114つのモデルの中の一つです。

ただしこのモデル単体でインペイントをするわけでなく、ベースとするモデルも必要です。txt2imgとimg2imgで使う時と同じチェックポイントモデルです。controlnetモデル自体はあくまでコントロールのためであり、どんな絵を描くかはベースモデル次第です。

これは従来のinpaintとの決定的な違いとも言えます。ベースモデルも使うということはベースモデルによって結果は違って自由に調整できて汎用性が高いです。

自由に調整できないのは従来のinpaintの問題でもあると思います。特にアニメの画像に使う時は全然駄目です。恐らくそのように学習されていないため。だから自由度が高いcontrolnet inpaintの方は段違いで望ましい結果が期待できます。

基本的な使い方

では実際に使うコードを載せて説明します。

まず例としてこの画像を使います。可愛い緑色ツインテイルの猫耳メイドですね。

midorinekomimi.jpg

今このように笑っていますが、controlnet inpaintを使ってこの子の表情を変えてみます。

顔の部分だけ白で塗るマスクの画像を準備します。

kao_mask.png

ベースモデルとしてはここでIrisMixを使います。

実装のコードは以下です。

import torch
from PIL import Image
from diffusers import StableDiffusionControlNetInpaintPipeline,ControlNetModel,EulerAncestralDiscreteScheduler
from translate import Translator
import numpy as np

# コントロール画像を作成するための関数
def img_ctrl_tsukuru(img,img_mask):
    img = np.float32(img)/255
    img[np.array(img_mask.convert('L'))>127] = -1
    return torch.from_numpy(img[None,:].transpose(0,3,1,2))

device = 'mps' # 又は'cuda'
# controlnetモデル
controlnet = ControlNetModel.from_pretrained(
    'lllyasviel/control_v11p_sd15_inpaint',
    torch_dtype=torch.float16
).to(device)
# ベースモデルでパイプラインを使ってそれでcontrolnetモデルも入れる
pipe = StableDiffusionControlNetInpaintPipeline.from_single_file(
    'stadifmodel/IrisMix-v5.safetensors',
    controlnet=controlnet,
    torch_dtype=torch.float16
).to(device)
pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe.scheduler.config)

seed = 25022
generator = torch.Generator(device=device).manual_seed(seed)
img0 = Image.open('midorinekomimi.jpg') # 元画像
img_mask = Image.open('kao_mask.png') # マスク画像
img_ctrl = img_ctrl_tsukuru(img0,img_mask) # コントロール画像

honyaku = Translator('en','ja').translate
prompt = honyaku('泣いている少女')
img = pipe(
    prompt,
    image=img0,
    mask_image=img_mask,
    control_image=img_ctrl,
    num_inference_steps=20,
    generator=generator
).images[0]
img.save('midorinekomimi_naku.jpg')

結果はこうです。

midorinekomimi_泣いている少女.jpg

表情は完全に変わりましたね。笑っている顔も泣いている顔どっちも可愛いです。

ここでcontrolnetのモデルはControlNetModelに入れて、ベースモデルはStableDiffusionControlNetInpaintPipelineに入れてパイプラインを作ります。

パイプラインを実行する時に準備した元画像とマスク画像の他に、コントロール画像も必要です。これは元画像とマスク画像から簡単に作れます。ここではわかりやすいように関数を定義しておきました。関数の定義の部分を読んですぐわかると思いますが、コントロール画像は元画像をマスクの部分を-1で埋めてtorchテンソルにするだけのものです。

因みにインペイントの結果はベースモデルによって違うので、リアル系のモデルを使ったらどうなるかも試してみたいですね。例えばこのモデルを使ってみます。

そうしたらこうなります。

midorinekomimi_bra.jpg

これはこれで可愛いとは思いますが、なんか可笑しいですね OTL。画風に合うモデルを選ぶのはやっぱり大事です。

表情が変わっていく少女

以上の例では泣いている顔にしたのですが、他の表情もできます。色々試したのでここに結果を載せておきたいです。

以下は使うプロンプトとできた画像です。ここでプロンプトは日本語ですが、実際にそのまま解釈されるのではなく、自動的に英語に翻訳されるということは留意しておきましょう。

怒っている少女

midorinekomimi_怒っている少女.jpg

照れている少女

midorinekomimi_照れている少女.jpg

じっと目している少女

midorinekomimi_じっと目している少女.jpg

げらげらしている少女

midorinekomimi_げらげらしている少女.jpg

目を瞑っている少女

midorinekomimi_目を瞑っている少女.jpg

ウィンクしている少女

midorinekomimi_ウィンクしている少女.jpg

苛立っている少女

midorinekomimi_苛立っている少女.jpg

怖がっている少女

midorinekomimi_怖がっている少女.jpg

驚いている少女

midorinekomimi_驚いている少女.jpg

くしゃみしている少女

midorinekomimi_くしゃみしている少女.jpg

疲れている少女

midorinekomimi_疲れている少女.jpg

ゴミを見ているような目をしている少女

midorinekomimi_ゴミを見ているような目をしている少女.jpg

本当に表情豊かですね。

strengthの調整で変化の重さ

以上の例では元の画像のマスクされた部分が完全に無視されて新しく塗られるのですが、実はcontrolnet inpaintもimg2imgと同じようにstrengthというパラメータがあります。

strengthはどれくらいその部分を変更するかを決める大事な数値です。規定では1、つまり完全に新しいものに入れ替えるのですが、0~1の値を指定して元の画像の形を保つ程度を決めることができます。

strengthを変えて違いを比べる例をしてみます。今回はこの元画像を使います。

hitodeshoujo.jpg

少女の領域を塗ったマスクの画像。

hitodeshoujo_mask.png

プロンプトなしで行ってみます。

import torch
from PIL import Image
from diffusers import StableDiffusionControlNetInpaintPipeline,ControlNetModel,EulerAncestralDiscreteScheduler
import matplotlib.pyplot as plt
import numpy as np

def img_ctrl_tsukuru(img,img_mask):
    img = np.float32(img)/255
    img[np.array(img_mask.convert('L'))>127] = -1
    return torch.from_numpy(img[None,:].transpose(0,3,1,2))

def seisei(strength):
    controlnet = ControlNetModel.from_pretrained(
        'lllyasviel/control_v11p_sd15_inpaint',
        torch_dtype=torch.float16
    ).to(device)
    
    pipe = StableDiffusionControlNetInpaintPipeline.from_single_file(
        'stadifmodel/IrisMix-v5.safetensors',
        controlnet=controlnet,
        torch_dtype=torch.float16
    ).to(device)
    pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe.scheduler.config)
    
    generator = torch.Generator(device=device).manual_seed(seed)
    img_ctrl = img_ctrl_tsukuru(img0,img_mask) # コントロール画像
    
    return pipe(
        '', # プロンプトは入れない
        image=img0,
        mask_image=img_mask,
        control_image=img_ctrl,
        num_inference_steps=20,
        strength=strength,
        generator=generator
    ).images[0]

device = 'mps'
seed = 35022
img0 = Image.open('hitodeshoujo.jpg')
img_mask = Image.open('hitodeshoujo_mask.png')
plt.figure(figsize=[6,15.5],dpi=100)
for i in range(10):
    strength = 0.1+i*0.1
    img = seisei(strength)
    plt.subplot(5,2,i+1,title='strength = %.1f'%strength)
    plt.imshow(img)
    plt.axis('off')
    torch.mps.empty_cache()

plt.tight_layout(pad=0.5)
plt.savefig('hitodeshoujo_nakunaru.jpg')
plt.close()

hitodeshoujo_nakunaru.jpg

プロンプトを入れない場合はマスクの部分が勝手にそれらしい内容で埋められるようです。だからstrength=0.8以上の時少女は完全に消失しますが、その分背景が予測されていますね。違和感も感じますが、随分とそれっぽく感じます。少女の姿は突然消えるように見えますが、面白いのは抱いている海星ひとで(?)です。姿が変わっていって、まるでメタモンです。

修正したい部分だけ拡大して処理する

次は使ってみた時に直面した問題と対策について説明します。

例は前回の記事でインペイントを説明したのと同じ、神社で後ろを向いている少女画像です。

jinja_akagami.jpg

インペイント用のマスク画像。埋めたいプロンプトは「こっちを見て笑っている黒髪和服少女」。

jinja_masuku.png

これは従来のinpaintで失敗した例で、今回はcontrolnet inpaintで挑戦することにしました。

今回使ったモデルはTMND-Mixにしました。

しかし上の例と同じように普通にインペイントをしようとしたら上手くいかなかったです。

jinja_kurokami_houkai.jpg

従来のinpaintよりましになったが、それでも顔などが崩壊していて、全然いい結果とは言えなくてがっかりしました。

理由は、埋めたい領域が小さすぎるからです。そもそもStable Diffusionがこんな小さい領域の中で人間全体を描くことが苦手です。

だとすると解決方法は、その領域だけ切り取って適切なサイズに拡大してインペイントして、最後に修正した後の画像を又縮小して元の場所に繕うという手段です。

一般的にStable Diffusionモデルが得意のは512×512です。人物を描く場合は768×512がいける場合も多いので、今回は768×512にします。

このように書きます。

import torch
from PIL import Image
from diffusers import StableDiffusionControlNetInpaintPipeline,ControlNetModel,EulerAncestralDiscreteScheduler
from translate import Translator
import numpy as np

# コントロール画像を作成するための関数
def img_ctrl_tsukuru(img,img_mask):
    img = np.float32(img)/255
    img[np.array(img_mask.convert('L'))>127] = -1
    return torch.from_numpy(img[None,:].transpose(0,3,1,2))

device = 'mps'
controlnet = ControlNetModel.from_pretrained(
    'lllyasviel/control_v11p_sd15_inpaint',
    torch_dtype=torch.float16
).to(device)
pipe = StableDiffusionControlNetInpaintPipeline.from_single_file(
    'stadifmodel/tmndMix_tmndMixSPRAINBOW.safetensors',
    controlnet=controlnet,
    torch_dtype=torch.float16
).to(device)
pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe.scheduler.config)

seed = 21801
generator = torch.Generator(device=device).manual_seed(seed)
img0 = Image.open('jinja_akagami.jpg') # 元画像
img0_crop = img0.crop([149,191,363,512]).resize([512,768]) # 切り取ってリサイズした元画像
img_mask = Image.open('jinja_masuku.png') # マスク画像
img_mask_crop = img_mask.crop([149,191,363,512]).resize([512,768]) # 切り取ってリサイズしたマスク画像
img_ctrl = img_ctrl_tsukuru(img0_crop,img_mask_crop) # コントロール画像
honyaku = Translator('en','ja').translate
prompt = 'こっちを見て笑っている黒髪和服少女、アニメ風'
img = pipe(
    honyaku(prompt),
    image=img0_crop,
    mask_image=img_mask_crop,
    control_image=img_ctrl,
    num_inference_steps=25,
    generator=generator
).images[0]

# 修正した画像で繕う
imga = np.array(img0)
mx,my = np.meshgrid(range(214),range(321))
w = np.clip(np.min([mx,213-mx,my],0)/10,0,1)[:,:,None]
imga[191:512,149:363] = (1-w)*imga[191:512,149:363]+w*np.array(img.resize([214,321]))
Image.fromarray(imga).save('jinja_kurokami.jpg')

jinja_kurokami.jpg

このようにやっと上手くいったようです。とはいえ実は境界線が合わなくて不自然に感じる結果の方が多いです。ガチャだから何度も試してこれは漸く最もいい一枚です。

最後の繕う部分のコードでは境界線のところにちょっと拘りがあります。実はインペイントで修正した後の画像はマスクの外の部分でも完全に元と同じではなく、微妙に変化があるのです。だからそのままただ修正した後のもので入れ替えたら境界線のところに色の違いで浮いているように見えて違和感が発生してしまいます。ここで重みをかけて少しずつ元の画像と修正した後の画像の成分を入れ替えていくことで自然に繕うことができます。

使いやすいように自動的に切り取る範囲を決める関数と繕う時の使う関数を準備しておいたらいいと思います。

もう一つ例を挙げます。今回はその画像の中の猫を猫耳少女に入れ替えたいです。

nekoshiro.jpg

マスク画像。

nekoshiro_mask.png

モデルはここでMeinaMixを使います。

実装のコード

import torch
from PIL import Image
from diffusers import StableDiffusionControlNetInpaintPipeline,ControlNetModel,EulerAncestralDiscreteScheduler
from translate import Translator
import numpy as np

# コントロール画像を作成するための関数
def img_ctrl_tsukuru(img,img_mask):
    img = np.float32(img)/255
    img[np.array(img_mask.convert('L'))>127] = -1
    return torch.from_numpy(img[None,:].transpose(0,3,1,2))

# 切り取る範囲を決める関数
def crop_kimeru(img,sf=1.2):
    arr = np.array(img)>127
    x0 = arr.max(0).argmax()
    x1 = arr.shape[1]-arr[:,::-1].max(0).argmax()
    y0 = arr.max(1).argmax()
    y1 = arr.shape[0]-arr[::-1].max(1).argmax()
    dx = x1-x0
    dy = y1-y0
    dd = int(max(dx,dy)*sf*0.5)
    xc = np.clip(int((x1+x0)*0.5),dd,arr.shape[1]-dd)
    yc = np.clip(int((y1+y0)*0.5),dd,arr.shape[0]-dd)
    return (x0,y0,x1,y1),(xc-dd,yc-dd,xc+dd,yc+dd)

# 修正した部分で元画像を繕う関数
def tsukurou(img0,img_crop,bb,cc):
    img = np.array(img0)
    mx,my = np.meshgrid(range(cc[0],cc[2]),range(cc[1],cc[3]))
    w = np.clip(np.nanmin([
        (mx-cc[0]) / (bb[0]-cc[0]),
        (my-cc[1]) / (bb[1]-cc[1]),
        (cc[2]-1-mx) / (cc[2]-bb[2]),
        (cc[3]-1-my) / (cc[3]-bb[3])
    ],0),0,1)[:,:,None]
    img[cc[1]:cc[3],cc[0]:cc[2]] = (1-w)*img[cc[1]:cc[3],cc[0]:cc[2]] + w*np.array(img_crop.resize([cc[2]-cc[0],cc[3]-cc[1]]))
    return Image.fromarray(img)

device = 'mps'
controlnet = ControlNetModel.from_pretrained(
    'lllyasviel/control_v11p_sd15_inpaint',
    torch_dtype=torch.float16
).to(device)
pipe = StableDiffusionControlNetInpaintPipeline.from_single_file(
    'stadifmodel/meinamix_meinaV11.safetensors',
    controlnet=controlnet,
    torch_dtype=torch.float16
).to(device)
pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe.scheduler.config)

seed = 23019
generator = torch.Generator(device=device).manual_seed(seed)
img0 = Image.open('nekoshiro.jpg') # 元画像
img_mask = Image.open('nekoshiro_mask.png') # マスク画像
bb,cc = crop_kimeru(img_mask) # 切り取る部分を決める
img0_crop = img0.crop(cc).resize([512,512])
img_mask_crop = img_mask.crop(cc).resize([512,512])

img_ctrl = img_ctrl_tsukuru(img0_crop,img_mask_crop) # コントロール画像
honyaku = Translator('en','ja').translate
prompt = honyaku('座って遊んでいる桃色髪猫耳女子高校生')
img_crop = pipe(
    prompt,
    image=img0_crop,
    mask_image=img_mask_crop,
    control_image=img_ctrl,
    num_inference_steps=20,
    generator=generator
).images[0]

img = tsukurou(img0,img_crop,bb,cc)
img.save('nekomimishiro.jpg')

結果。猫ちゃんが見事に猫耳になりました。

nekomimishiro.jpg

ちょっと複雑になりますが、こうやって小さい部分に人間の姿を綺麗に入れることができます。

外へ描き加えるoutpaint

「インペイント」と似ているものとして「アウトペイント」(outpaint)という概念があります。これは対義語ではなく、寧ろ実質的同じものです。インペイントというと内側を埋めることが多いが、外側を埋める場合はアウトペイントと呼ぶのです。だから勿論、アウトペイントもcontrolnet inpaintで行えます。

例えばこのようにある部分しかない512×200pxの画像があって下の部分を描き加えて512×512pxにしたい場合です。

aotwtchan.jpg

今回準備するマスク画像は下の方全域が白となる画像です。コードで簡単に作れるので予め準備する必要がないでしょう。

import torch
from PIL import Image
from diffusers import StableDiffusionControlNetInpaintPipeline,ControlNetModel,EulerAncestralDiscreteScheduler
from translate import Translator
import numpy as np

def img_ctrl_tsukuru(img,img_mask):
    img = np.float32(img)/255
    img[np.array(img_mask.convert('L'))>127] = -1
    return torch.from_numpy(img[None,:].transpose(0,3,1,2))

device = 'mps'
controlnet = ControlNetModel.from_pretrained(
    'lllyasviel/control_v11p_sd15_inpaint',
    torch_dtype=torch.float16
).to(device)
pipe = StableDiffusionControlNetInpaintPipeline.from_single_file(
    'stadifmodel/meinamix_meinaV11.safetensors',
    controlnet=controlnet,
    torch_dtype=torch.float16
).to(device)
pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe.scheduler.config)

seed = 25019
generator = torch.Generator(device=device).manual_seed(seed)
# 元画像を読み込む。ただしとりあえず白の方は黒で拡張する
img0 = Image.open('aotwtchan.jpg').crop([0,0,512,512])
ar_mask = np.zeros([512,512],dtype=np.uint8) # 空っぽのマスク画像
ar_mask[200:] = 255 # 下の方は全部白で埋める
img_mask = Image.fromarray(ar_mask)

img_ctrl = img_ctrl_tsukuru(img0,img_mask) # コントロール画像
honyaku = Translator('en','ja').translate
prompt = honyaku('町の上で飛んでいる魔法少女')
img = pipe(
    prompt,
    image=img0,
    mask_image=img_mask,
    control_image=img_ctrl,
    num_inference_steps=20,
    generator=generator
).images[0]
img.save('aotwtmachi.jpg')

結果はこう。

aotwtmachi.jpg

描き加えた部分はなんかどうしてもそこまで合わなくて違和感多いですね。小さな領域から無理矢理大きな画像を拡張しようとするからやはり難しいでしょう。それでも下にある少女の身体と町並みを合わせようとする努力を感じます。もっと工夫したら更に上手くいけるでしょう。

終わりに

controlnetはとりあえず凄いものです。controlnetが現れたのはStable Diffusionの大きな進歩です。controlnet inpaintだけでなく、他のcontrolnetも使い熟せたらとても恐ろしい武器になりそうです。今後も色々試して紹介してみたいです。

参考

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?