この記事は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.1の14つのモデルの中の一つです。
ただしこのモデル単体でインペイントをするわけでなく、ベースとするモデルも必要です。txt2imgとimg2imgで使う時と同じチェックポイントモデルです。controlnetモデル自体はあくまでコントロールのためであり、どんな絵を描くかはベースモデル次第です。
これは従来のinpaintとの決定的な違いとも言えます。ベースモデルも使うということはベースモデルによって結果は違って自由に調整できて汎用性が高いです。
自由に調整できないのは従来のinpaintの問題でもあると思います。特にアニメの画像に使う時は全然駄目です。恐らくそのように学習されていないため。だから自由度が高いcontrolnet inpaintの方は段違いで望ましい結果が期待できます。
基本的な使い方
では実際に使うコードを載せて説明します。
まず例としてこの画像を使います。可愛い緑色ツインテイルの猫耳メイドですね。
今このように笑っていますが、controlnet inpaintを使ってこの子の表情を変えてみます。
顔の部分だけ白で塗るマスクの画像を準備します。
ベースモデルとしてはここで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')
結果はこうです。
表情は完全に変わりましたね。笑っている顔も泣いている顔どっちも可愛いです。
ここでcontrolnetのモデルはControlNetModel
に入れて、ベースモデルはStableDiffusionControlNetInpaintPipeline
に入れてパイプラインを作ります。
パイプラインを実行する時に準備した元画像とマスク画像の他に、コントロール画像も必要です。これは元画像とマスク画像から簡単に作れます。ここではわかりやすいように関数を定義しておきました。関数の定義の部分を読んですぐわかると思いますが、コントロール画像は元画像をマスクの部分を-1で埋めてtorchテンソルにするだけのものです。
因みにインペイントの結果はベースモデルによって違うので、リアル系のモデルを使ったらどうなるかも試してみたいですね。例えばこのモデルを使ってみます。
そうしたらこうなります。
これはこれで可愛いとは思いますが、なんか可笑しいですね OTL。画風に合うモデルを選ぶのはやっぱり大事です。
表情が変わっていく少女
以上の例では泣いている顔にしたのですが、他の表情もできます。色々試したのでここに結果を載せておきたいです。
以下は使うプロンプトとできた画像です。ここでプロンプトは日本語ですが、実際にそのまま解釈されるのではなく、自動的に英語に翻訳されるということは留意しておきましょう。
怒っている少女
照れている少女
じっと目している少女
げらげらしている少女
目を瞑っている少女
ウィンクしている少女
苛立っている少女
怖がっている少女
驚いている少女
くしゃみしている少女
疲れている少女
ゴミを見ているような目をしている少女
本当に表情豊かですね。
strengthの調整で変化の重さ
以上の例では元の画像のマスクされた部分が完全に無視されて新しく塗られるのですが、実はcontrolnet inpaintもimg2imgと同じようにstrength
というパラメータがあります。
strength
はどれくらいその部分を変更するかを決める大事な数値です。規定では1
、つまり完全に新しいものに入れ替えるのですが、0~1の値を指定して元の画像の形を保つ程度を決めることができます。
strength
を変えて違いを比べる例をしてみます。今回はこの元画像を使います。
少女の領域を塗ったマスクの画像。
プロンプトなしで行ってみます。
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()
プロンプトを入れない場合はマスクの部分が勝手にそれらしい内容で埋められるようです。だからstrength=0.8
以上の時少女は完全に消失しますが、その分背景が予測されていますね。違和感も感じますが、随分とそれっぽく感じます。少女の姿は突然消えるように見えますが、面白いのは抱いている海星(?)です。姿が変わっていって、まるでメタモンです。
修正したい部分だけ拡大して処理する
次は使ってみた時に直面した問題と対策について説明します。
例は前回の記事でインペイントを説明したのと同じ、神社で後ろを向いている少女画像です。
インペイント用のマスク画像。埋めたいプロンプトは「こっちを見て笑っている黒髪和服少女」。
これは従来のinpaintで失敗した例で、今回はcontrolnet inpaintで挑戦することにしました。
今回使ったモデルはTMND-Mixにしました。
しかし上の例と同じように普通にインペイントをしようとしたら上手くいかなかったです。
従来の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')
このようにやっと上手くいったようです。とはいえ実は境界線が合わなくて不自然に感じる結果の方が多いです。ガチャだから何度も試してこれは漸く最もいい一枚です。
最後の繕う部分のコードでは境界線のところにちょっと拘りがあります。実はインペイントで修正した後の画像はマスクの外の部分でも完全に元と同じではなく、微妙に変化があるのです。だからそのままただ修正した後のもので入れ替えたら境界線のところに色の違いで浮いているように見えて違和感が発生してしまいます。ここで重みをかけて少しずつ元の画像と修正した後の画像の成分を入れ替えていくことで自然に繕うことができます。
使いやすいように自動的に切り取る範囲を決める関数と繕う時の使う関数を準備しておいたらいいと思います。
もう一つ例を挙げます。今回はその画像の中の猫を猫耳少女に入れ替えたいです。
マスク画像。
モデルはここで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')
結果。猫ちゃんが見事に猫耳になりました。
ちょっと複雑になりますが、こうやって小さい部分に人間の姿を綺麗に入れることができます。
外へ描き加えるoutpaint
「インペイント」と似ているものとして「アウトペイント」(outpaint)という概念があります。これは対義語ではなく、寧ろ実質的同じものです。インペイントというと内側を埋めることが多いが、外側を埋める場合はアウトペイントと呼ぶのです。だから勿論、アウトペイントもcontrolnet inpaintで行えます。
例えばこのようにある部分しかない512×200pxの画像があって下の部分を描き加えて512×512pxにしたい場合です。
今回準備するマスク画像は下の方全域が白となる画像です。コードで簡単に作れるので予め準備する必要がないでしょう。
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')
結果はこう。
描き加えた部分はなんかどうしてもそこまで合わなくて違和感多いですね。小さな領域から無理矢理大きな画像を拡張しようとするからやはり難しいでしょう。それでも下にある少女の身体と町並みを合わせようとする努力を感じます。もっと工夫したら更に上手くいけるでしょう。
終わりに
controlnetはとりあえず凄いものです。controlnetが現れたのはStable Diffusionの大きな進歩です。controlnet inpaintだけでなく、他のcontrolnetも使い熟せたらとても恐ろしい武器になりそうです。今後も色々試して紹介してみたいです。
参考
- diffusers で ControNet の inpaint を試す
- ControlNet 版 Inpaint をやってみる
- 【SDXL 1.0】ControlNet と Inpaint を組み合わせると何ができるか?
- Stablediffusionで、表情差分のイラストを作成する。
- 表情の描写に関するプロンプト【Stable Diffusion web UI】
- 【Stable Diffusion Web UI】ControlNet Inpaintの使い方
- 完全なキャラ固定!in paintを使った差分作りが最強すぎた!イラストAI Stable Diffusion Novel AI
- Stable Diffusion『ControlNet』の使い方ガイド!導入・更新方法も