超強力汎用ControlNet「AnyTest v4」を利用する方法【diffusers】
はじめに
今までStable Diffusion XLに関してはさまざまな記事を書いてきました。
生成AIをGoogle Colaboratoryで簡単に 【Part6 画像生成AI Stable Diffusion XL編】
生成AIをGoogle Colaboratoryで簡単に 【Part6.5 画像生成AI SDXL+ANIMAGINE XL 3.1編】
生成AIをGoogle Colabで簡単に 【Part10 SDXL+ANIMAGINE +ControlNet+LoRA編(全部載せ)】
【Next.js】生成AIによる「お絵描きサポートWebアプリ」を作りたい!【全くわからない人】
その中で様々なモデルやControlNetを利用をしてきました。
しかしながら、ControlNetは用途に応じて様々なモデルを使い分けないといけないなどの課題があります。
その中で下記のような汎用ControlNetなるものを見つけました。
上記の重みは「AnyTest」というControlNetであり、「月須和・那々」様が開発したControlNetになります。
開発者様のXを見ると、このモデルの威力がわかるかと思いますので、少しだけ紹介しようと思います。
いやー、すごいですね。
Depthのような画像を入力して、構図を指定した画像を生成したり、Cannyの一部のような画像を入力して、一部分を固定して残りをAIに書かせたりと、これをたった一つの汎用ControlNetで達成しているというのが圧倒的です。
そこで私もぜひ使ってみたいと思ったのですが、記事執筆時点では、ComfyUIなどのWebUIで利用する方法しかネットに使い方がありませんでした。
私は宗教上の理由()により、画像生成AIのWebUIは使えないので、なんとかしてDiffusersライブラリで利用できるようにしたいという試みを始めました。
結論(わかっている人向け)
「AnyTest」をDiffusersライブラリで利用する場合は、from_single_file
を利用するのではなく、使いたいモデルのsafetensor
重みをダウンロードして、他のCannyなどのStable Diffusion XL のControlNetモデルのconfig.json
ファイルと一緒に、適当なフォルダに格納したのち、ControlNetModel.from_pretrained
で読み込みましょう。
今回生成した画像
下記の画像を見ていただければわかりますが、本当に強力なControlNetです。これを一つのControlNetでできているのがすごいです。
アニメ画像のフィギュア化
下記の通り、以前にFLUX.1-devで生成した画像(背景抜き)をフィギュアにしてみました
入力した画像
生成した画像
構図的にも難しいと思いますが、それでもリアルなフィギュア風の画像にできていますね。
利用したプロンプト
メインプロンプト
a photo of an anime character figure,Yellowish-white hair , red ribbon,red eyes,white hat, white costume, black microphone, bare legs,Skin color is white
ネガティブプロンプト
nsfw, lowres, (bad), text, error, fewer, extra, missing, worst quality, jpeg artifacts, low quality, watermark, unfinished, displeasing, oldest, early, chromatic aberration, signature, extra digits, artistic error, username, scan, [abstract]
一部分の修正(スカーフの追加)
また、元画像に対して一部分修正して、追加してみました。
入力した画像
生成した画像
灰色になった部分に対して、「scarf」を追加した画像をさらに表示できていますね
利用したプロンプト
メインプロンプト
scarf, Yellowish-white hair , red ribbon,red eyes,white hat, white costume, black microphone, bare legs
ネガティブプロンプト
nsfw, lowres, (bad), text, error, fewer, extra, missing, worst quality, jpeg artifacts, low quality, watermark, unfinished, displeasing, oldest, early, chromatic aberration, signature, extra digits, artistic error, username, scan, [abstract]
AnyTestをDiffusersで使う方法
ここからは、最低限「AnyTest」ControlNetをDiffusersライブラリで利用する方法について解説します。
AnyTestの重みファイルをダウンロードする
AnyTest ControlNetモデルは下記よりダウンロードできます。
おすすめのモデルは「CN-anytest_v4-marged.safetensors」です。
もしくは、「CN-anytest_v3-50000_fp16.safetensors」です。
そのほかの「dim」などが記載されているファイルは、Control LoRAと呼ばれる重みファイルで、LoRAとしてモデルにくっつけるだけで、ControlNetと同じような効果を出すことができる、非常にすごい技術です。
ちなみに「pn」はpony系のモデル、「am」は「Animagine」系のモデルと一緒に使うLoRAです。SDXLでよく使われるモデル2つで使えるのはとてもいいですね。
しかしながら、Control LoRAをDiffusersライブラリにて簡単に使う方法を見つけることはできませんでした。
下記のように、Diffusersライブラリの中でControl LoRAを利用可能にする取り組み自体はあるようですが、効果のある形で使えるようにはまだなっていないようです。
(無理やり利用する方法はあるみたいですが、今回は普通にControlNetを利用すれば良いので、無視することにします)
したがって、今回は「CN-anytest_v4-marged.safetensors」重みをダウンロードしておいてください。
適当なControlNetからconfig.jsonを取得する
続いて、「AnyTest」ControlNetを利用するために、config.jsonが必要なので、下記のファイルをダウンロードしておいてください。
両方のファイルを同じフォルダに格納する
ダウンロードした二つのファイルは同じフォルダに格納してください。
例えば下記のようなフォルダに格納しましょう。
./inputs/AnyTest-v4/
格納する際は、モデルの重みは下記のようにリネームして保存してください。
./inputs/CN-anytest_v4/diffusion_pytorch_model.safetensors
configファイルも下記のように保存してください。
./inputs/CN-anytest_v4/config.json
コードを書く
最低限下記のようなコードをかけば動くはずです。
ただし、ControlNetに入力する参照画像を"./inputs/control_image.png"
に格納しておいてください。
from diffusers import StableDiffusionXLControlNetPipeline, ControlNetModel
from diffusers.utils import load_image
import torch
cnet_path = "./inputs/AnyTest-v4/"
base_model_path = "cagliostrolab/animagine-xl-3.1"
image_path = "./inputs/control_image.png"
controlnet = ControlNetModel.from_pretrained(cnet_path,torch_dtype=torch.float16)
base = StableDiffusionXLControlNetPipeline.from_pretrained(
base_model_path,
controlnet=controlnet,
torch_dtype=torch.float16,
use_safetensors=True
)
base.to("cuda")
control_image = load_image(image_path)
prompt = "a cute girls"
negative_prompt = "bad"
image = self.base(
prompt=prompt,
negative_prompt=negative_prompt,
image=control_image,
cfg_scale=7.0,
controlnet_conditioning_scale=0.7,
num_inference_steps=28,
output_type=pil,
width = 832,
height = 1216,
).images[0]
image.save("./outputs/output_image.png".format(i))
なんで上記のようなめんどくさい方法をしないといけないのか?
そもそもとして、なんでconfig.jsonをわざわざダウンロードしないといけないのか?
普通に、HuggingfaceのControlNetのsafetensor
をリンクしてロードすればいいじゃん!
と感じる方多いと思います。
ここでは、その点について補足説明しようと思います。
Diffusersライブラリに詳しい人は、単一のsafetensor
重みからモデルをロードする関数をご存知だと思います。
そうです。from_single_file
メソッドです。
例えば下記のように書ければ、ロードできるように思えます。
cnet_path="./inputs/CN-anytest_v4-marged.safetensors"
controlnet = ControlNetModel.from_single_file(cnet_path, torch_dtype=torch.float16)
しかしながら、上記のように指定すると、下記のエラーが発生します。
エラー全文
OSError Traceback (most recent call last)
<ipython-input-8-fed346bff60b> in <cell line: 1>()
----> 1 sdxl = SDXL()
2 #指定しているControlNetで利用できる"./inputs/refer_prepared/0.png"などがすでにある場合は、下記はコメントアウトしても良い
3 sdxl.prepare_multi_referimage(input_refer_image_folder = input_refer_image_folder,output_refer_image_folder = output_refer_image_folder, low_threshold = 100, high_threshold = 200, noise_level=25, blur_radius=5)
4
<ipython-input-4-3d4e3265793f> in __init__(self, device, config_ini_path)
211
212
--> 213 self.base = self.preprepare_model()
214
215
<ipython-input-4-3d4e3265793f> in preprepare_model(self, controlnet_path)
235 if cnet_path is not None:
236 if self.single_controlnet_bool_list[model_step]:
--> 237 controlnet = ControlNetModel.from_single_file(cnet_path, torch_dtype=torch.float16)
238 else:
239 if self.controlnet_variant_bool_list[model_step]:
/usr/local/lib/python3.10/dist-packages/huggingface_hub/utils/_validators.py in _inner_fn(*args, **kwargs)
112 kwargs = smoothly_deprecate_use_auth_token(fn_name=fn.__name__, has_token=has_token, kwargs=kwargs)
113
--> 114 return fn(*args, **kwargs)
115
116 return _inner_fn # type: ignore
/content/diffusers/src/diffusers/loaders/single_file_model.py in from_single_file(cls, pretrained_model_link_or_path_or_dict, **kwargs)
266 ) # some configs contain a subfolder key, e.g. StableCascadeUNet
267
--> 268 diffusers_model_config = cls.load_config(
269 pretrained_model_name_or_path=default_pretrained_model_config_name,
270 subfolder=subfolder,
/usr/local/lib/python3.10/dist-packages/huggingface_hub/utils/_validators.py in _inner_fn(*args, **kwargs)
112 kwargs = smoothly_deprecate_use_auth_token(fn_name=fn.__name__, has_token=has_token, kwargs=kwargs)
113
--> 114 return fn(*args, **kwargs)
115
116 return _inner_fn # type: ignore
/content/diffusers/src/diffusers/configuration_utils.py in load_config(cls, pretrained_model_name_or_path, return_unused_kwargs, return_commit_hash, **kwargs)
404 )
405 except EntryNotFoundError:
--> 406 raise EnvironmentError(
407 f"{pretrained_model_name_or_path} does not appear to have a file named {cls.config_name}."
408 )
OSError: Lykon/dreamshaper-8 does not appear to have a file named config.json.
このエラーは、config.jsonファイルを取得できないことが原因です。
大前提として、safetensor
重みは、モデルの重みを辞書型で保存しています。
つまり、ファイルの中にはモデルの各層の名前(例えば、"model.diffusion_model.input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight"
)とその層での重みの値(行列)がセットで保存されています。
実際に下記のようなコードでsafetensor
重みの中を見ることができます。
重みファイルの中身を見るコード
import safetensors
from safetensors import safe_open
# safetensorファイルのパスを指定
safetensor_file_path = 'inputs/CN-anytest_v3-45000_fp16.safetensors'
# safetensorsファイルを読み込む
with safe_open(safetensor_file_path, framework="pt") as f:
tensors = {key: f.get_tensor(key) for key in f.keys()}
# keyごとに重みを表示
for key, tensor in tensors.items():
print(f"Key: {key}")
#print(f"Tensor: {tensor}")
print(f"Shape: {tensor.shape}")
print(f"Dtype: {tensor.dtype}")
print("="*50)
結果(長いので一部省略)
Key: add_embedding.linear_1.bias
Shape: torch.Size([1280])
Dtype: torch.float16
==================================================
Key: add_embedding.linear_1.weight
Shape: torch.Size([1280, 2816])
Dtype: torch.float16
==================================================
Key: add_embedding.linear_2.bias
Shape: torch.Size([1280])
Dtype: torch.float16
==================================================
・・・・・・
==================================================
Key: time_embedding.linear_1.weight
Shape: torch.Size([1280, 320])
Dtype: torch.float16
==================================================
Key: time_embedding.linear_2.bias
Shape: torch.Size([1280])
Dtype: torch.float16
==================================================
Key: time_embedding.linear_2.weight
Shape: torch.Size([1280, 1280])
Dtype: torch.float16
==================================================
その上で、Diffusersライブラリは、この重みに保存されている層の名前(Key)を元に、ロードされたモデル重みがなんのモデルなのかを把握しています。
該当するのは下記のコードです。
該当部分
・・・
CHECKPOINT_KEY_NAMES = {
"v2": "model.diffusion_model.input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight",
"xl_base": "conditioner.embedders.1.model.transformer.resblocks.9.mlp.c_proj.bias",
"xl_refiner": "conditioner.embedders.0.model.transformer.resblocks.9.mlp.c_proj.bias",
"upscale": "model.diffusion_model.input_blocks.10.0.skip_connection.bias",
"controlnet": "control_model.time_embed.0.weight",
"playground-v2-5": "edm_mean",
"inpainting": "model.diffusion_model.input_blocks.0.0.weight",
"clip": "cond_stage_model.transformer.text_model.embeddings.position_embedding.weight",
"clip_sdxl": "conditioner.embedders.0.transformer.text_model.embeddings.position_embedding.weight",
"clip_sd3": "text_encoders.clip_l.transformer.text_model.embeddings.position_embedding.weight",
"open_clip": "cond_stage_model.model.token_embedding.weight",
"open_clip_sdxl": "conditioner.embedders.1.model.positional_embedding",
"open_clip_sdxl_refiner": "conditioner.embedders.0.model.text_projection",
"open_clip_sd3": "text_encoders.clip_g.transformer.text_model.embeddings.position_embedding.weight",
"stable_cascade_stage_b": "down_blocks.1.0.channelwise.0.weight",
"stable_cascade_stage_c": "clip_txt_mapper.weight",
"sd3": "model.diffusion_model.joint_blocks.0.context_block.adaLN_modulation.1.bias",
"animatediff": "down_blocks.0.motion_modules.0.temporal_transformer.transformer_blocks.0.attention_blocks.0.pos_encoder.pe",
"animatediff_v2": "mid_block.motion_modules.0.temporal_transformer.norm.bias",
"animatediff_sdxl_beta": "up_blocks.2.motion_modules.0.temporal_transformer.norm.weight",
"animatediff_scribble": "controlnet_cond_embedding.conv_in.weight",
"animatediff_rgb": "controlnet_cond_embedding.weight",
"flux": [
"double_blocks.0.img_attn.norm.key_norm.scale",
"model.diffusion_model.double_blocks.0.img_attn.norm.key_norm.scale",
],
}
・・・
def infer_diffusers_model_type(checkpoint):
if (
CHECKPOINT_KEY_NAMES["inpainting"] in checkpoint
and checkpoint[CHECKPOINT_KEY_NAMES["inpainting"]].shape[1] == 9
):
if CHECKPOINT_KEY_NAMES["v2"] in checkpoint and checkpoint[CHECKPOINT_KEY_NAMES["v2"]].shape[-1] == 1024:
model_type = "inpainting_v2"
elif CHECKPOINT_KEY_NAMES["xl_base"] in checkpoint:
model_type = "xl_inpaint"
else:
model_type = "inpainting"
elif CHECKPOINT_KEY_NAMES["v2"] in checkpoint and checkpoint[CHECKPOINT_KEY_NAMES["v2"]].shape[-1] == 1024:
model_type = "v2"
elif CHECKPOINT_KEY_NAMES["playground-v2-5"] in checkpoint:
model_type = "playground-v2-5"
elif CHECKPOINT_KEY_NAMES["xl_base"] in checkpoint:
model_type = "xl_base"
elif CHECKPOINT_KEY_NAMES["xl_refiner"] in checkpoint:
model_type = "xl_refiner"
elif CHECKPOINT_KEY_NAMES["upscale"] in checkpoint:
model_type = "upscale"
elif CHECKPOINT_KEY_NAMES["controlnet"] in checkpoint:
model_type = "controlnet"
elif (
CHECKPOINT_KEY_NAMES["stable_cascade_stage_c"] in checkpoint
and checkpoint[CHECKPOINT_KEY_NAMES["stable_cascade_stage_c"]].shape[0] == 1536
):
model_type = "stable_cascade_stage_c_lite"
elif (
CHECKPOINT_KEY_NAMES["stable_cascade_stage_c"] in checkpoint
and checkpoint[CHECKPOINT_KEY_NAMES["stable_cascade_stage_c"]].shape[0] == 2048
):
model_type = "stable_cascade_stage_c"
elif (
CHECKPOINT_KEY_NAMES["stable_cascade_stage_b"] in checkpoint
and checkpoint[CHECKPOINT_KEY_NAMES["stable_cascade_stage_b"]].shape[-1] == 576
):
model_type = "stable_cascade_stage_b_lite"
elif (
CHECKPOINT_KEY_NAMES["stable_cascade_stage_b"] in checkpoint
and checkpoint[CHECKPOINT_KEY_NAMES["stable_cascade_stage_b"]].shape[-1] == 640
):
model_type = "stable_cascade_stage_b"
elif CHECKPOINT_KEY_NAMES["sd3"] in checkpoint:
model_type = "sd3"
elif CHECKPOINT_KEY_NAMES["animatediff"] in checkpoint:
if CHECKPOINT_KEY_NAMES["animatediff_scribble"] in checkpoint:
model_type = "animatediff_scribble"
elif CHECKPOINT_KEY_NAMES["animatediff_rgb"] in checkpoint:
model_type = "animatediff_rgb"
elif CHECKPOINT_KEY_NAMES["animatediff_v2"] in checkpoint:
model_type = "animatediff_v2"
elif checkpoint[CHECKPOINT_KEY_NAMES["animatediff_sdxl_beta"]].shape[-1] == 320:
model_type = "animatediff_sdxl_beta"
elif checkpoint[CHECKPOINT_KEY_NAMES["animatediff"]].shape[1] == 24:
model_type = "animatediff_v1"
else:
model_type = "animatediff_v3"
elif any(key in checkpoint for key in CHECKPOINT_KEY_NAMES["flux"]):
if any(
g in checkpoint for g in ["guidance_in.in_layer.bias", "model.diffusion_model.guidance_in.in_layer.bias"]
):
model_type = "flux-dev"
else:
model_type = "flux-schnell"
else:
model_type = "v1"
return model_type
上記のコードを見ると、CHECKPOINT_KEY_NAMES
に記載されているモデルの層の名前と一致した時に、infer_diffusers_model_type
関数にて、モデルの種類が決まります。
ここで、CHECKPOINT_KEY_NAMES
で指定されているどの名前の層も、ロードされたsafetensor
ファイルに存在しない場合、"v1"
がmodel_type
として選定されます。
ここで、このmodel_type
はさらに同じファイル中の下記部分にて参照されます。
該当部分
・・・
DIFFUSERS_DEFAULT_PIPELINE_PATHS = {
"xl_base": {"pretrained_model_name_or_path": "stabilityai/stable-diffusion-xl-base-1.0"},
"xl_refiner": {"pretrained_model_name_or_path": "stabilityai/stable-diffusion-xl-refiner-1.0"},
"xl_inpaint": {"pretrained_model_name_or_path": "diffusers/stable-diffusion-xl-1.0-inpainting-0.1"},
"playground-v2-5": {"pretrained_model_name_or_path": "playgroundai/playground-v2.5-1024px-aesthetic"},
"upscale": {"pretrained_model_name_or_path": "stabilityai/stable-diffusion-x4-upscaler"},
"inpainting": {"pretrained_model_name_or_path": "Lykon/dreamshaper-8-inpainting"},
"inpainting_v2": {"pretrained_model_name_or_path": "stabilityai/stable-diffusion-2-inpainting"},
"controlnet": {"pretrained_model_name_or_path": "lllyasviel/control_v11p_sd15_canny"},
"v2": {"pretrained_model_name_or_path": "stabilityai/stable-diffusion-2-1"},
"v1": {"pretrained_model_name_or_path": "Lykon/dreamshaper-8"},
"stable_cascade_stage_b": {"pretrained_model_name_or_path": "stabilityai/stable-cascade", "subfolder": "decoder"},
"stable_cascade_stage_b_lite": {
"pretrained_model_name_or_path": "stabilityai/stable-cascade",
"subfolder": "decoder_lite",
},
"stable_cascade_stage_c": {
"pretrained_model_name_or_path": "stabilityai/stable-cascade-prior",
"subfolder": "prior",
},
"stable_cascade_stage_c_lite": {
"pretrained_model_name_or_path": "stabilityai/stable-cascade-prior",
"subfolder": "prior_lite",
},
"sd3": {
"pretrained_model_name_or_path": "stabilityai/stable-diffusion-3-medium-diffusers",
},
"animatediff_v1": {"pretrained_model_name_or_path": "guoyww/animatediff-motion-adapter-v1-5"},
"animatediff_v2": {"pretrained_model_name_or_path": "guoyww/animatediff-motion-adapter-v1-5-2"},
"animatediff_v3": {"pretrained_model_name_or_path": "guoyww/animatediff-motion-adapter-v1-5-3"},
"animatediff_sdxl_beta": {"pretrained_model_name_or_path": "guoyww/animatediff-motion-adapter-sdxl-beta"},
"animatediff_scribble": {"pretrained_model_name_or_path": "guoyww/animatediff-sparsectrl-scribble"},
"animatediff_rgb": {"pretrained_model_name_or_path": "guoyww/animatediff-sparsectrl-rgb"},
"flux-dev": {"pretrained_model_name_or_path": "black-forest-labs/FLUX.1-dev"},
"flux-schnell": {"pretrained_model_name_or_path": "black-forest-labs/FLUX.1-schnell"},
}
・・・
def fetch_diffusers_config(checkpoint):
model_type = infer_diffusers_model_type(checkpoint)
model_path = DIFFUSERS_DEFAULT_PIPELINE_PATHS[model_type]
return model_path
すなわち、fetch_diffusers_config
関数の中で、infer_diffusers_model_type
関数が利用され、model_type
が"v1"
に決定され、DIFFUSERS_DEFAULT_PIPELINE_PATHS
辞書のKeyとして"v1"
を入れることで、model_path={"pretrained_model_name_or_path": "Lykon/dreamshaper-8"}
となります。
さらに、もう一段深掘ると、fetch_diffusers_config
関数は、下記の通り、from_single_file
メソッドで使われていることがわかります。
該当部分
該当するコードは下記です。
・・・
class FromOriginalModelMixin:
"""
Load pretrained weights saved in the `.ckpt` or `.safetensors` format into a model.
"""
@classmethod
@validate_hf_hub_args
def from_single_file(cls, pretrained_model_link_or_path_or_dict: Optional[str] = None, **kwargs):
・・・
if config:
if isinstance(config, str):
default_pretrained_model_config_name = config
else:
raise ValueError(
(
"Invalid `config` argument. Please provide a string representing a repo id"
"or path to a local Diffusers model repo."
)
)
else:
config = fetch_diffusers_config(checkpoint)
default_pretrained_model_config_name = config["pretrained_model_name_or_path"]
・・・
diffusers_model_config = cls.load_config(
pretrained_model_name_or_path=default_pretrained_model_config_name,
subfolder=subfolder,
local_files_only=local_files_only,
)
・・・
はい、こちらでcls.load_config
クラスメソッドが出てきましたね。エラーメッセージは下記でした。
したから3つ目のブロックの268行目をみてみるとcls.load_config
を使っており、ここでエラーが出ていますね。
・・・
/content/diffusers/src/diffusers/loaders/single_file_model.py in from_single_file(cls, pretrained_model_link_or_path_or_dict, **kwargs)
266 ) # some configs contain a subfolder key, e.g. StableCascadeUNet
267
--> 268 diffusers_model_config = cls.load_config(
269 pretrained_model_name_or_path=default_pretrained_model_config_name,
270 subfolder=subfolder,
/usr/local/lib/python3.10/dist-packages/huggingface_hub/utils/_validators.py in _inner_fn(*args, **kwargs)
112 kwargs = smoothly_deprecate_use_auth_token(fn_name=fn.__name__, has_token=has_token, kwargs=kwargs)
113
--> 114 return fn(*args, **kwargs)
115
116 return _inner_fn # type: ignore
/content/diffusers/src/diffusers/configuration_utils.py in load_config(cls, pretrained_model_name_or_path, return_unused_kwargs, return_commit_hash, **kwargs)
404 )
405 except EntryNotFoundError:
--> 406 raise EnvironmentError(
407 f"{pretrained_model_name_or_path} does not appear to have a file named {cls.config_name}."
408 )
OSError: Lykon/dreamshaper-8 does not appear to have a file named config.json.
このエラーは、fetch_diffusers_config
関数の中で、model_path={"pretrained_model_name_or_path": "Lykon/dreamshaper-8"}
と決められてしまったことから発生しています。
ここで、"Lykon/dreamshaper-8"
リポジトリをHuggingfaceで探してみましょう。
みてわかるように、config.json
が本リポジトリには存在しません。
したがって、OSError: Lykon/dreamshaper-8 does not appear to have a file named config.json.
というエラーになります。
エラーの原因がわかりました。
ここで、一番最初に何が原因だったかを思い出すと、Diffusersライブラリでは、safetensor
重みの中のモデルの層の名前から、モデルの型を決定していることから始まっています。
今回の「AnyTest」重みに保存されている層の名前が、CHECKPOINT_KEY_NAMES
に記載されていないのが問題でした。
しかし、実は、これは「AnyTest」モデルに限った話ではありません。
SDXLのControlNetとしてよく利用されている、下記の公式のモデルなども、実は同じ層の名前で保存されています。(一致することを確認しました)
つまり、普通に公式で実装されているControlNetであっても、from_single_file
はうまく動作しません。
したがって、SDXLのControlNetがfrom_single_file
メソッドをうまく利用できない形になっていることが原因です。(これはバグなのか仕様なのか)
そこで、今回は、上記のdiffusers/controlnet-canny-sdxl-1.0
リポジトリの中のconfigファイルを「AnyTest」モデルにて、無理やり一緒に利用することで、エラーを回避した形になります。
(モデルの層の名前と、各層のモデルの層の個数が完全に一致していることを確認したため、問題ないと思います)
そのために、モデルファイルとコンフィグファイルを同じフォルダ(リポジトリ)に格納し、フォルダで指定して、from_pretrained
メソッドを利用することで、フォルダ内のconfigファイルが採用されて、読み込むことできるようになりました。
使用上の注意
ここまで書いてきてあれですが、「AnyTest」を使ってみて一つだけ使いにくいなと思った点があります。それは、
色が引き継がれないこと
です。
もしかしたら私の使い方が悪いのかもしれないですが、少なくとも上記の使い方では、ControlNetに入力した構図は引き継がれますが、色に関しては、プロンプトでかなり正確に指定しないと引き継がれません。
なので、元の画像をベースに少しだけ微修正したい場合は、素直にStableDiffusionXLInpaintPipeline
を使うのが良いかなと思いました。
StableDiffusionXLInpaintPipeline
の威力は、下記記事で説明しています。
逆に言うと、色の変化の正確性がそこまで求められない場合(つまり、生成画像例くらいの色の再現性で良い場合)には、その他のモデルと一線を画すレベルで高性能なモデルです。
(色の再現性を高める方法があれば、ぜひ教えて欲しいです。多分私が知らないだけです)
まとめ
ここまでで、超強力は汎用ControlNetの「AnyTest」をDiffusersライブラリで利用する方法を記載しました。
色だけはプロンプトで指定してあげないといけないですが、形状の変換という観点では、非常に高性能なControlNetモデルだなと思いました。
(そもそも、あらゆるタスクを一つのControlNetでできるというのがすごい。とりあえず最初に試すモデルになりそう)
では、ここまで読んでくださりありがとうございました!
ここからは、実際に使ったコードの解説をしますので、興味のある方のみご覧ください。
詳細な内容
ここからは、今回の記事を書くにあたって実施した実験と、実際のコードに関しての解説を行います。かなり細かい内容なので、興味のある方のみご覧ください(ほぼ忘備録目的)
成果物
下記のリポジトリをご覧ください。
事前準備
AnyTestの重みファイルをダウンロードする
上述しました。
適当なControlNetからconfig.jsonを取得する
上述しました。
参照画像を用意する
フィギュア化の参照画像
この画像を./inputs/refer/figre/tile.png
として保存します。
部分修正の参照画像(スカーフ追加)
この画像を./inputs/refer/scarf/tile.png
として保存します。
想定されるディレクトリ構造
Google Driveのディレクトリ構造は下記を想定します。
MyDrive/
└ colab_AI_sample/
└ colab_SDXLControlNet2_sample/
├ configs/
| └ config.ini
├ inputs/
| ├ CN-anytest_v4/
| | ├ diffusion_pytorch_model.safetensors
| | └ config.json
| ├ refer/
| | ├ figre/
| | | └ tile.png
| | └ scarf/
| | | └ tile.png
| └ refer_prepared/
├ outputs/
├ module/
| └ module_sdxl.py
└ SDXLControlNet_sample.ipynb
使い方の解説
SDXLControlNet_sample.ipynb
をGoogle Colabratoryアプリで開いてください。
ファイルを右クリックすると「アプリで開く」という項目が表示されるため、そこからGoogle Colabratoryアプリを選択してください。
もし、ない場合は、「アプリを追加」からアプリストアに行き、「Google Colabratory」で検索してインストールをしてください。
Google Colabratoryアプリで開いたら、後述するパラメータとプロンプトの設定の後で、SDXLControlNet_sample.ipynb
のメモを参考にして、一番上のセルから順番に実行すると、画像が5枚「outputs」フォルダに生成されます。
また、最後まで実行後、パラメータを変更して再度実行する場合は、「ランタイム」→「セッションを再起動して全て実行する」をクリックしてください。
パラメータの変更
SDXLControlNet_sample.ipynb
の5セル目と8セル目が該当します。
パラメータの設定 5セル目
#モデルの設定を行う。
config_text = """
[SDXL]
device = auto
n_steps=28
seed=42
;from_single_file = True
;base_model_path = stabilityai/stable-diffusion-xl-base-1.0
base_model_path = Asahina2K/Animagine-xl-3.1-diffuser-variant-fp16
;base_model_path = ./inputs/pony/ponyDiffusionV6XL_v6StartWithThisOne.safetensors
;base_model_path = ./inputs/fudukiMix_v20.safetensors
;from_single_vae_file = True
;base_vae_model = ./inputs/pony/sdxl_vae.safetensors
use_controlnet = True
;controlnet_path0 = ./inputs/CN-anytest_v3-45000/
;from_single_controlnet_file0 = False
;use_controlnet_variant0 = True
controlnet_path0 = ./inputs/CN-anytest_v4/
from_single_controlnet_file0 = False
use_controlnet_variant0 = False
;controlnet_path1 = diffusers/controlnet-depth-sdxl-1.0
;from_single_controlnet_file1 = False
;use_controlnet_variant1 = True
;controlnet_path2 = xinsir/controlnet-openpose-sdxl-1.0
;from_single_controlnet_file2 = False
;use_controlnet_variant2 = True
;controlnet_path3 = diffusers/controlnet-canny-sdxl-1.0
;from_single_controlnet_file3 = False
;use_controlnet_variant3 = True
;controlnet_path4 = diffusers/controlnet-zoe-depth-sdxl-1.0
;from_single_controlnet_file4 = False
;use_controlnet_variant4 = True
;残念ながら下記は動かない
;controlnet_path0 = https://huggingface.co/2vXpSwA7/iroiro-lora/blob/main/test_controlnet2/CN-anytest_v4-marged.safetensors
;controlnet_config_repo0 = diffusers/controlnet-canny-sdxl-1.0
;from_single_controlnet_file0 = True
;use_controlnet_variant0 = False
;modeと同じ名前の参照画像が格納されているとする。(tile.pngなど)
control_mode0 = tile
;control_mode1 = depth
;control_mode2 = openpose_full
;control_mode3 = canny
;control_mode4 = zoe_depth
;以下は待機用
;control_mode0 = blur
;control_mode0 = gray
;control_mode0 = lq
;control_mode0 = openpose_face
;control_mode0 = openpose_faceonly
;control_mode0 = openpose
;lora_weight_repo0 = erohinem/LoRA
;lora_weight_path0 = Dreamyvibes-lora.safetensors
;lora_weight_repo1 = xxx
;lora_weight_path1 = xxx.safetensors
;lora_scale0 = 1.0
;lora_scale1 = 1.0
;trigger_word0 = "Dreamyvibes Artstyle"
;trigger_word1 = "xxx"
;select_solver = LCM
select_solver = DPM
;select_solver = Eulera
;select_solver = FMEuler
use_karras_sigmas = True
scheduler_algorithm_type = dpmsolver++
solver_order = 2
cfg_scale = 7.0
width = 832
height = 1216
output_type = pil
aesthetic_score = 6
negative_aesthetic_score = 2.5
save_latent_simple = False
save_latent_overstep = False
save_latent_approximation = False
save_predict_skip_x0 = False
"""
with open("configs/config.ini", "w", encoding="utf-8") as f:
f.write(config_text)
念の為の記載ですが;
はコメントアウトの記号です。実験の設定を復元可能なように過去の設定を残したりしています。
-
n_steps
- 拡散モデルのノイズ除去のstep数
- モデルごとに適切な値は異なるので確認する。
-
from_single_file
- モデルを単一のsafetensorファイルから読み込む場合はTrueにする
-
base_model_path
- 利用するモデルの指定。
- 現時点の実装では、単一ファイルからのロードでない場合は、
variant = fp16
に対応しているモデルを利用する必要がある - 対応しているかどうかは、ファイル名に
fp16
と記載があるかどうかで判断できる
-
from_single_vae_file
、base_vae_model
- VAEを指定する場合は、ここも設定する。
- この実装ではRefinerは設定できない。使わないので破棄しました
-
use_controlnet
- ControlNetを利用するかどうかのFlag
-
controlnet_path0
からcontrolnet_path4
- 利用するControlNetのモデルパス
- 一応複数指定できる
-
control_mode0
からcontrol_mode4
- 利用するControlNetのモード。
- 指定した
controlnet_path
の数字に応じて、合わせて記載する
-
use_controlnet_variant0
からuse_controlnet_variant4
-
variant=fp16
を採用するかどうか。 - 基本は、対応しているモデルが少ないので
False
で良い
-
-
lora_weight_repo0
からlora_weight_repo9
- 利用するLoRAのリポジトリ指定。複数記載すれば多重に適用可能
- HuggingFaceにあるLoRA利用する場合は、ここにリポジトリ名も指定する
-
lora_weight_path0
からlora_weight_path9
- 利用するLoRAのパスを指定する。複数記載すれば多重に適用可能
- HuggingFaceにあるLoRA利用する場合は、ここにはLoRAで利用するファイル名のみを指定する
- CivitaiなどのLoRAファイルをダウンロードして利用するファイルは、ファイルを保存した場所の相対パスを記述する('lora_weight_repo`はNoneに設定する)
-
lora_scale0
からlora_scale9
- 利用するLoRAの適用度合い
-
trigger_word0
からtrigger_word9
- 利用するLoRAで推奨されるTrigger Wordの設定
-
select_solver
- 利用するサンプラーの指定。
-
width
とheight
- 生成する画像のサイズ
-
save_latent_simple
とsave_latent_overstep
とsave_latent_approximation
- 生成途中も出力するかどうか、出力する場合どのような形式で出力するかのフラグ
-
save_latent_simple
はノイズから生成される過程を出力 -
save_latent_overstep
はある程度綺麗な画像が遷移するような過程を出力 -
save_latent_approximation
は潜在表現からの一次近似で出力
-
save_predict_skip_x0
- https://zenn.dev/asap/articles/7940b17be86da7
- 上記の記事に記載の通り、forkしたDiffusersを利用して、コードを書き換えれば利用可能
- 基本的には利用不可
- そのうち、親クラスを継承する形で、新規にカスタムパイプラインを作って実装予定
8セル目は下記です。
for i in range(5):
start = time.time()
image = sdxl.generate_image(main_prompt,neg_prompt = neg_prompt, image_path = output_refer_image_folder, controlnet_conditioning_scale = [0.8])
print("generate image time: ", time.time()-start)
image.save("./outputs/SDXL_result_{}.png".format(i))
こちらのcontrolnet_conditioning_scale = [0.8]
の部分が変更可能なパラメータです。
controlnet_conditioning_scale
は、ControlNetを利用する場合に、どの程度反映させるかを設定するパラメータです。大きい方が、制御が強くなります。
プロンプトの変更
SDXLControlNet_sample.ipynb
の6セル目が該当します。
#読み上げるプロンプトを設定する。
#figre
main_prompt = """
a photo of an anime character figure,Yellowish-white hair , red ribbon,red eyes,white hat, white costume, black microphone, bare legs,Skin color is white
"""
#scarf
#main_prompt = """
#scarf, Yellowish-white hair , red ribbon,red eyes,white hat, white costume, #black microphone, bare legs
#"""
neg_prompt="""
nsfw, lowres, (bad), text, error, fewer, extra, missing, worst quality, jpeg artifacts, low quality, watermark, unfinished, displeasing, oldest, early, chromatic aberration, signature, extra digits, artistic error, username, scan, [abstract]
"""
#コントロールネットに入力する参照画像の名前はcontrolnetのモードと一致させてください。(tile.png, depth.jpegなど)
input_refer_image_folder = "./inputs/refer/figre"
#コントロールネットに入力する変換後の参照画像の格納フォルダ。命名規則は上記と同様
output_refer_image_folder = "./inputs/refer_prepared/figre"
6セル目のmain_prompt
を変更して、8セル目を実行することで、変更されたプロンプトが反映されて実行されます。
また、タスクに応じて、参照画像を指定するフォルダのpathを正しく指定してください。
input_refer_image_folder
には変換元の画像を入れれば、実行後に、適切な画像に変換されて、output_refer_image_folder
に保存されます。
また、参照画像のファイル名は、Control_mode
に合わせる必要があります。
例えば、depth.pngやopenpose-full.webpなど
実行結果
記事上部の画像をご覧ください。
それ以外の実験は実施していません。
コードの解説
SDXLControlNet_sample.ipynb
SDXLControlNet_sample.ipynb
について解説します。
コードは下記よりご覧ください
1セル目
#Google Driveのフォルダをマウント(認証入る)
from google.colab import drive
drive.mount('/content/drive')
マイドライブのマウントを行っています。
認証が入り、一定時間認証しないとエラーになってしまうので、最初に持ってきて実行と認証をまとめて実施します。
2セル目
#SDXL で必要なモジュールのインストール
!pip install -U bitsandbytes diffusers optimum-quanto peft tensorflow-metadata transformers scikit-learn ftfy accelerate invisible_watermark safetensors controlnet-aux mediapipe timm
必要なモジュールをインストールしています。
3セル目
# カレントディレクトリを本ファイルが存在するディレクトリに変更する。
import glob
import os
pwd = os.path.dirname(glob.glob('/content/drive/MyDrive/colabzenn/colab_SDXLControlNet2_sample/SDXLControlNet_sample.ipynb', recursive=True)[0])
print(pwd)
%cd $pwd
!pwd
カレントディレクトリを、ノートブックが存在しているディレクトリに設定しています。
4セル目
#モジュールをimportする
from module.module_sdxl import SDXL
import time
モジュールをinportしています。
sdxlの実装コードはmodule/module_sdxl.py
で記述しております。
そちらも後述します
5セル目
#モデルの設定を行う。
config_text = """
[SDXL]
device = auto
n_steps=28
seed=42
;from_single_file = True
;base_model_path = stabilityai/stable-diffusion-xl-base-1.0
base_model_path = Asahina2K/Animagine-xl-3.1-diffuser-variant-fp16
;base_model_path = ./inputs/pony/ponyDiffusionV6XL_v6StartWithThisOne.safetensors
;base_model_path = ./inputs/fudukiMix_v20.safetensors
;from_single_vae_file = True
;base_vae_model = ./inputs/pony/sdxl_vae.safetensors
use_controlnet = True
;controlnet_path0 = ./inputs/CN-anytest_v3-45000/
;from_single_controlnet_file0 = False
;use_controlnet_variant0 = True
controlnet_path0 = ./inputs/CN-anytest_v4/
from_single_controlnet_file0 = False
use_controlnet_variant0 = False
;controlnet_path1 = diffusers/controlnet-depth-sdxl-1.0
;from_single_controlnet_file1 = False
;use_controlnet_variant1 = True
;controlnet_path2 = xinsir/controlnet-openpose-sdxl-1.0
;from_single_controlnet_file2 = False
;use_controlnet_variant2 = True
;controlnet_path3 = diffusers/controlnet-canny-sdxl-1.0
;from_single_controlnet_file3 = False
;use_controlnet_variant3 = True
;controlnet_path4 = diffusers/controlnet-zoe-depth-sdxl-1.0
;from_single_controlnet_file4 = False
;use_controlnet_variant4 = True
;残念ながら下記は動かない
;controlnet_path0 = https://huggingface.co/2vXpSwA7/iroiro-lora/blob/main/test_controlnet2/CN-anytest_v4-marged.safetensors
;controlnet_config_repo0 = diffusers/controlnet-canny-sdxl-1.0
;from_single_controlnet_file0 = True
;use_controlnet_variant0 = False
;modeと同じ名前の参照画像が格納されているとする。(tile.pngなど)
control_mode0 = tile
;control_mode1 = depth
;control_mode2 = openpose_full
;control_mode3 = canny
;control_mode4 = zoe_depth
;以下は待機用
;control_mode0 = blur
;control_mode0 = gray
;control_mode0 = lq
;control_mode0 = openpose_face
;control_mode0 = openpose_faceonly
;control_mode0 = openpose
;lora_weight_repo0 = erohinem/LoRA
;lora_weight_path0 = Dreamyvibes-lora.safetensors
;lora_weight_repo1 = xxx
;lora_weight_path1 = xxx.safetensors
;lora_scale0 = 1.0
;lora_scale1 = 1.0
;trigger_word0 = "Dreamyvibes Artstyle"
;trigger_word1 = "xxx"
;select_solver = LCM
select_solver = DPM
;select_solver = Eulera
;select_solver = FMEuler
use_karras_sigmas = True
scheduler_algorithm_type = dpmsolver++
solver_order = 2
cfg_scale = 7.0
width = 832
height = 1216
output_type = pil
aesthetic_score = 6
negative_aesthetic_score = 2.5
save_latent_simple = False
save_latent_overstep = False
save_latent_approximation = False
save_predict_skip_x0 = False
"""
with open("configs/config.ini", "w", encoding="utf-8") as f:
f.write(config_text)
実験の設定などを記述する部分です。
直接configs/config.ini
を書き換えても、このセルを実行したら上書きされてしまうので注意してください。
;
をつけるとコメントアウトできるので、今回の実験では不要だが、残しておきたい実験設定などはコメントアウトして残しておくことをお勧めします。
6セル目
#読み上げるプロンプトを設定する。
#figre red
#main_prompt = """
#a photo of an anime character figure,Yellowish-white hair , red ribbon,red eyes,white hat, white costume, black microphone, bare legs,Skin color is white
#"""
#scarf scketch
main_prompt = """
scarf, Yellowish-white hair , red ribbon,red eyes,white hat, white costume, black microphone, bare legs
"""
neg_prompt="""
nsfw, lowres, (bad), text, error, fewer, extra, missing, worst quality, jpeg artifacts, low quality, watermark, unfinished, displeasing, oldest, early, chromatic aberration, signature, extra digits, artistic error, username, scan, [abstract]
"""
#コントロールネットに入力する参照画像の名前はcontrolnetのモードと一致させてください。(tile.png, depth.jpegなど)
input_refer_image_folder = "./inputs/refer/sketch2"
#コントロールネットに入力する変換後の参照画像の格納フォルダ。命名規則は上記と同様
output_refer_image_folder = "./inputs/refer_prepared/sketch2"
生成するプロンプトと、ControlNetで利用する参照画像の格納場所を記述してます。
7セル目
sdxl = SDXL()
#指定しているControlNetで利用できる"./inputs/refer_prepared/tile.png"などがすでにある場合は、下記はコメントアウトしても良い
sdxl.prepare_multi_referimage(input_refer_image_folder = input_refer_image_folder,output_refer_image_folder = output_refer_image_folder, low_threshold = 100, high_threshold = 200, noise_level=25, blur_radius=5)
SDXLのクラスのクラスインスタンスを取得しています。
加えてprepare_multi_referimage
メソッドを実行しています。
このメソッドでは、今回指定されたcontrol_mode
をもとに、それに該当する画像を変換して、格納し直します。
例えばdepthのControlNetを利用する場合は、control_mode
をdepthに指定することで、普通の画像(depth.png)から深度マップ(depth.png)に変換します。
8セル目
for i in range(5):
start = time.time()
image = sdxl.generate_image(main_prompt,neg_prompt = neg_prompt, image_path = output_refer_image_folder, controlnet_conditioning_scale = [0.8])
print("generate image time: ", time.time()-start)
image.save("./outputs/SDXL_result_{}.png".format(i))
ここで指定したseed値から連続で5枚分の画像を生成し、生成にかかった時間も計測します。
生成された画像はoutputs
フォルダに格納されます。
module/module_sdxl.py
module/module_sdxl.py
について解説します。
コードは下記よりご覧ください
簡単に下記で説明します。過去の記事と重複するものは解説していません
FLUXクラスのコンストラクタ
class SDXL:
def __init__(self,device = None, config_ini_path = './configs/config.ini'):
・・・
self.use_controlnet = config_dict.get("use_controlnet", "False")
if self.use_controlnet == "False":
self.use_controlnet = False
else:
self.use_controlnet = True
self.controlnet_path_list = []
for i in range(5):
if config_dict.get(f"controlnet_path{i}", "None") != "None":
self.controlnet_path_list.append(config_dict[f"controlnet_path{i}"])
else:
self.controlnet_path_list.append(None)
self.single_controlnet_bool_list = []
for i in range(5):
if config_dict.get(f"from_single_controlnet_file{i}", "False") != "False":
self.single_controlnet_bool_list.append(True)
else:
self.single_controlnet_bool_list.append(False)
self.controlnet_variant_bool_list = []
for i in range(5):
if config_dict.get(f"use_controlnet_variant{i}", "False") != "False":
self.controlnet_variant_bool_list.append(True)
else:
self.controlnet_variant_bool_list.append(False)
self.controlnet_config_list = []
for i in range(5):
if config_dict.get(f"controlnet_config_repo{i}", "None") != "None":
self.controlnet_config_list.append(config_dict[f"controlnet_config_repo{i}"])
else:
self.controlnet_config_list.append(None)
if not self.use_controlnet and self.controlnet_path_list == []:
raise ValueError("controlnet_path is not set")
if len(self.controlnet_path_list) != len(self.single_controlnet_bool_list):
raise ValueError("controlnet_path and from_single_controlnet_file is not match")
if len(self.controlnet_path_list) != len(self.controlnet_variant_bool_list):
raise ValueError("controlnet_path and use_controlnet_variant is not match")
if len(self.controlnet_path_list) != len(self.controlnet_config_list):
raise ValueError("controlnet_path and controlnet_config_repo is not match")
#ControlNet一つ指定につき、一つのControlModeを指定する
self.control_modes_list = []
for i in range(5):
if config_dict.get(f"control_mode{i}", "None") != "None":
self.control_modes_list.append(config_dict[f"control_mode{i}"])
else:
self.control_modes_list.append(None)
if len(self.controlnet_path_list) != len(self.control_modes_list):
raise ValueError("controlnet_path and control_mode is not match")
ControlNetを複数適用できるようにするために、設定ファイルも複数用意できるようにしています。
記載しないとNoneになるようになっているので、一つだけの指定でも大丈夫です。
最大5つ指定できて、5のうちどの場所に指定しても問題ありません。
モデルの読み込み
def preprepare_model(self, controlnet_path = None):
・・・
if self.use_controlnet:
controlnets = []
for model_step, cnet_path in enumerate(self.controlnet_path_list):
if cnet_path is not None:
if self.single_controlnet_bool_list[model_step]:
if self.controlnet_config_list[model_step] is not None:
print(model_step, self.controlnet_config_list[model_step])
controlnet = ControlNetModel.from_single_file(cnet_path, config = self.controlnet_config_list[model_step], torch_dtype=torch.float16)
else:
controlnet = ControlNetModel.from_single_file(cnet_path, torch_dtype=torch.float16)
else:
if self.controlnet_variant_bool_list[model_step]:
controlnet = ControlNetModel.from_pretrained(cnet_path,
torch_dtype=torch.float16,
variant="fp16",
use_safetensors=True
)
else:
controlnet = ControlNetModel.from_pretrained(cnet_path, torch_dtype=torch.float16)
controlnets.append(controlnet)
print("loaded controlnet")
if not self.SINGLE_FILE_FLAG:
if self.VAE_FLAG:
base = StableDiffusionXLControlNetPipeline.from_pretrained(
self.base_model_path,
vae=vae,
controlnet=controlnets,
torch_dtype=torch.float16,
variant="fp16",
use_safetensors=True
)
・・・
複数のControlNetを設定できるようにしています。
また、単一ファイルから取得するのか、フォルダで指定するのか、fp16に対応しているのかなどで読み込み方が違うので、場合わけしています。
前提として、設定時に同じ数字は同じControlNetの設定だと決めて記載しています。
まとめ(2回目)
ここまで読んでくださってありがとうございました!
Discussion