この記事ではStable Diffusionを使って球体パノラマを生成する方法と注意点について紹介します。
私は普段diffusersとComfyUIどれも使っていて、使い方はかなり違うので、今回はそれぞれ説明します。
はじめに
球体パノラマとは、球体の表面を360度全部が書かれる長方形の画像のことです。
3D分野でもよく使われています。球体の表面に貼って背景を作ったり、ピカピカの物体の映り込みを作るのに使ったりします。
これは完成品の例です。
そしてこの画像を使ってこのような表面の映り込みを作ることができます。
ちょっと余談ですが、この映り込み物体の画像はMATLABによって作られたのです。方法はこの記事に書いてあります。
このような画像をStable Diffusionで作るのに「latentlabs360」というよくできたLoRAがあります。
ただしそのまま普通のLoRAと同じように使っても意外と上手くいかなくて、更にちょっとした工夫が必要なので、この記事ではこれについて説明していきたいです。
尚、残念ながらこのLoRAはSDXLに使うことができないようでXLなしで使うしかありません。
diffusersでの使用
まずLoRAの使い方を含め、diffusersの基本的な使い方に関しては以前私が書いた入門の記事でも書いたのでこれを参考に。
latentlabs360のトリガーワードは「a 360 equirectangular panorama」なのでこれをいつもプロンプトに入れる必要があります。
チェックポイントモデルは基本的にどれでも使えます。今回はアニメ系のTMND-Mixを使います。
ではひとまずlatentlabs360をこのまま導入してみます。
from diffusers import StableDiffusionPipeline,EulerAncestralDiscreteScheduler
import torch
from translate import Translator
device = 'mps'
cp_model_file = 'stadifmodel/tmndMix_tmndMixSPRAINBOW.safetensors'
pipe = StableDiffusionPipeline.from_single_file(cp_model_file,torch_dtype=torch.float16).to(device)
pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe.scheduler.config)
# ここでlatentLabs360をロードする
lora_file = 'stadifmodel/LatentLabs360.safetensors'
pipe.load_lora_weights(".", weight_name=lora_file)
seed = 20007
generator = torch.Generator().manual_seed(seed)
honyaku = Translator('en','ja').translate
prompt = honyaku('公園の桜並木')
prompt = 'a 360 equirectangular panorama, ' + prompt
img = pipe(
prompt,
width=1024,
height=512,
num_inference_steps=25,
generator=generator,
).images[0]
img.save('llsp.jpg')
結果はこのようになります。
これはそれらしく綺麗な画像ができましたが、まだなんか違いますね。右と左の端は繋がっていないからです。このままでは360度パノラマとして使うわけにはいきません。
この問題を簡潔に解決できる方法はdiffusersにはまだ見つかりませんが、色々試したところcontrolnet inpaintで何とかできるとわかりました。
controlnet inpaintの使い方について私は以前の記事で紹介しました。
この方法を使うとなると2度も生成する必要があって複雑になりますが、結果は悪くないです。
from diffusers import StableDiffusionPipeline,ControlNetModel,StableDiffusionControlNetInpaintPipeline,EulerAncestralDiscreteScheduler
import torch
import numpy as np
from PIL import Image
from translate import Translator
# コントロール画像を作るための関数
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'
cp_model_file = 'stadifmodel/tmndMix_tmndMixSPRAINBOW.safetensors'
lora_file = 'stadifmodel/LatentLabs360.safetensors'
seed = 20007
generator = torch.Generator().manual_seed(seed)
honyaku = Translator('en','ja').translate
prompt = honyaku('公園の桜並木')
prompt = 'a 360 equirectangular panorama, ' + prompt
# 1回目生成するパイプライン
pipe1 = StableDiffusionPipeline.from_single_file(
cp_model_file,
torch_dtype=torch.float16
).to(device)
pipe1.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe1.scheduler.config)
pipe1.load_lora_weights(".", weight_name=lora_file)
# controlnet inpaintを含む再度生成のパイプライン
controlnet = ControlNetModel.from_pretrained(
'lllyasviel/control_v11p_sd15_inpaint',
torch_dtype=torch.float16
).to(device)
pipe2 = StableDiffusionControlNetInpaintPipeline.from_single_file(
cp_model_file,
controlnet=controlnet,
torch_dtype=torch.float16,
scheduler=pipe1.scheduler
).to(device)
pipe2.load_lora_weights(".", weight_name=lora_file)
# まず1回生成
img0 = pipe1(
prompt,
width=1024,
height=512,
num_inference_steps=25,
generator=generator,
).images[0]
# 左右半分分けて入れ替える
arimg = np.array(img0)
arimg = np.hstack([arimg[:,512:],arimg[:,:512]])
img0 = Image.fromarray(arimg)
# 中心部を含むマスク画像を作る
arimg_mask = np.zeros([512,1024],dtype=np.uint8)
arimg_mask[:,64:-64] = 255
img_mask = Image.fromarray(arimg_mask)
img_ctrl = img_ctrl_tsukuru(img0,img_mask)
# 中心部だけ再度生成
img = pipe2(
prompt,
image=img0,
mask_image=img_mask,
control_image=img_ctrl,
num_inference_steps=25,
strength=0.5,
generator=generator
).images[0]
img.save('llsp2.jpg')
これで綺麗で360度回せる球体パノラマができました。
ComfyUIでの使用
ComfyUIにはループする画像を作るためのノードを提供するプラグインがあるのでdiffusersみたいな工夫をしなくても簡単に360度球体パノラマを作ることができます。
ただしそのプラグインを使うことでプロンプトに従わなかったり、品質が低下したりするような気がします。だからこれを使う方法とdiffusersと同じようにcontrolnet inpaintを使うという方法をどちらも紹介します。
ループ画像を作るksamplerとVAEを使う
プラグインのインストール
ループする画像を作るプラグインは、探してみたら主に2つあります。
tiled_ksampler
ComfyUI-seamless-tiling
ノードの組み方は少し違いますが、どちらも試したところ、結果は全く同じです。ただしtiled_ksamplerの方が使いやすいから今回はこれを使う方法を紹介します。
tiled_ksamplerの使い方は、ただ普段のKSampler
ノードをAsymmetric Tiled KSampler
ノードに入れ替えて、VAEDecode
ノードをCircular VAEDecode
ノードに入れ替えるだけです。
ノードの組み方
tiled_ksamplerをlatentlabs360のLoRAと、私が以前の記事で紹介した自動翻訳DeepTranslatorCLIPTextEncodeNode
ノードとjpgで保存できるSaveImagePlus
ノードと一緒に組み立てたらこんな感じのワークフローになります。(勿論、デフォルトの翻訳無しのCLIPTextEncode
とpng保存のSaveImage
を使っても構いません)
今回使ったチェックポイントモデルはリアル系のAbsoluteRealityです。
結果の画像はこれです。この画像をドラッグしてワークフローをロードすることができます。(ただしSave Image Plus for ComfyUI又はExtended Save Image for ComfyUIプラグインをインストールしている必要があります)
ワークフロー
API形式のワークフローもここに載せておきます。これを保存してComfyUIにロードできます。
{
"1": {
"inputs": {
"ckpt_name": "absolutereality_v181.safetensors"
},
"class_type": "CheckpointLoaderSimple"
},
"2": {
"inputs": {
"width": 1024,
"height": 512,
"batch_size": 1
},
"class_type": "EmptyLatentImage"
},
"3": {
"inputs": {
"lora_name": "LatentLabs360.safetensors",
"strength_model": 1,
"strength_clip": 1,
"model": ["1", 0],
"clip": ["1", 1]
},
"class_type": "LoraLoader"
},
"4": {
"inputs": {
"from_translate": "japanese",
"to_translate": "english",
"add_proxies": false,
"service": "GoogleTranslator [free]",
"text": "a 360 equirectangular panorama, ボロボロな倉庫",
"Show proxy": "proxy_hide",
"Show authorization": "authorization_hide",
"clip": ["3", 1]
},
"class_type": "DeepTranslatorCLIPTextEncodeNode"
},
"5": {
"inputs": {
"from_translate": "japanese",
"to_translate": "english",
"add_proxies": false,
"service": "GoogleTranslator [free]",
"text": "低品質",
"Show proxy": "proxy_hide",
"Show authorization": "authorization_hide",
"clip": ["3", 1]
},
"class_type": "DeepTranslatorCLIPTextEncodeNode"
},
"6": {
"inputs": {
"seed": 231544903223482,
"tileX": 1,
"tileY": 0,
"steps": 20,
"cfg": 7,
"sampler_name": "dpmpp_2m",
"scheduler": "karras",
"denoise": 1,
"model": ["3", 0],
"positive": ["4", 0],
"negative": ["5", 0],
"latent_image": ["2", 0]
},
"class_type": "Asymmetric Tiled KSampler"
},
"7": {
"inputs": {
"samples": ["6", 0],
"vae": ["1", 2]
},
"class_type": "Circular VAEDecode"
},
"8": {
"inputs": {
"filename_prefix": "latentlabs360",
"file_type": "JPEG",
"remove_metadata": false,
"images": ["7", 0]
},
"class_type": "SaveImagePlus"
}
}
controlnet inpaintと一緒に使う方法
プラグインのインストール
この方法はかなり沢山のノードを使う必要があるのでかなり複雑になります。
追加でインストールする必要があるプラグインは以下です。
-
Image Blank
ノード:was-node-suite-comfyui -
ImageConcanate
ノード:ComfyUI-KJNodes -
InpaintPreprocessor
ノード:comfyui_controlnet_aux
ノードの組み方
組んでみたらこんな感じです。
このワークフローでできた画像。
以上のワークフローは細かすぎて詳細まで表示されないので、この画像をComfyUIにドラッグして読み込んでいいでしょう。
ワークフロー
ちょっと長いですが、API形式のjsonワークフローもここに載せておきます。
{
"1": {
"inputs": {
"ckpt_name": "absolutereality_v181.safetensors"
},
"class_type": "CheckpointLoaderSimple"
},
"2": {
"inputs": {
"width": 1024,
"height": 512,
"batch_size": 1
},
"class_type": "EmptyLatentImage"
},
"3": {
"inputs": {
"lora_name": "LatentLabs360.safetensors",
"strength_model": 1,
"strength_clip": 1,
"model": ["1", 0],
"clip": ["1", 1]
},
"class_type": "LoraLoader"
},
"4": {
"inputs": {
"from_translate": "japanese",
"to_translate": "english",
"add_proxies": false,
"service": "GoogleTranslator [free]",
"text": "a 360 equirectangular panorama, ボロボロな倉庫",
"Show proxy": "proxy_hide",
"Show authorization": "authorization_hide",
"clip": ["3", 1]
},
"class_type": "DeepTranslatorCLIPTextEncodeNode"
},
"5": {
"inputs": {
"from_translate": "japanese",
"to_translate": "english",
"add_proxies": false,
"service": "GoogleTranslator [free]",
"text": "低品質",
"Show proxy": "proxy_hide",
"Show authorization": "authorization_hide",
"clip": ["3", 1]
},
"class_type": "DeepTranslatorCLIPTextEncodeNode"
},
"6": {
"inputs": {
"seed": 877456268177392,
"steps": 20,
"cfg": 7,
"sampler_name": "dpmpp_2m",
"scheduler": "karras",
"denoise": 1,
"model": ["3", 0],
"positive": ["4", 0],
"negative": ["5", 0],
"latent_image": ["2", 0]
},
"class_type": "KSampler"
},
"7": {
"inputs": {
"samples": ["6", 0],
"vae": ["1", 2]
},
"class_type": "VAEDecode"
},
"8": {
"inputs": {
"width": 512,
"height": 512,
"x": 512,
"y": 0,
"image": ["7", 0]
},
"class_type": "ImageCrop"
},
"9": {
"inputs": {
"width": 512,
"height": 512,
"x": 0,
"y": 0,
"image": ["7", 0]
},
"class_type": "ImageCrop"
},
"10": {
"inputs": {
"direction": "right",
"match_image_size": false,
"image1": ["8", 0],
"image2": ["9", 0]
},
"class_type": "ImageConcanate"
},
"11": {
"inputs": {
"width": 448,
"height": 512,
"red": 0,
"green": 0,
"blue": 0
},
"class_type": "Image Blank"
},
"12": {
"inputs": {
"width": 128,
"height": 512,
"red": 255,
"green": 255,
"blue": 255
},
"class_type": "Image Blank"
},
"13": {
"inputs": {
"width": 448,
"height": 512,
"red": 0,
"green": 0,
"blue": 0
},
"class_type": "Image Blank"
},
"14": {
"inputs": {
"inputcount": 3,
"direction": "right",
"match_image_size": false,
"Update inputs": null,
"image_1": ["11", 0],
"image_2": ["12", 0],
"image_3": ["13", 0]
},
"class_type": "ImageConcatMulti"
},
"15": {
"inputs": {
"channel": "red",
"image": ["14", 0]
},
"class_type": "ImageToMask"
},
"16": {
"inputs": {
"grow_mask_by": 40,
"pixels": ["10", 0],
"vae": ["1", 2],
"mask": ["15", 0]
},
"class_type": "VAEEncodeForInpaint"
},
"17": {
"inputs": {
"image": ["10", 0],
"mask": ["15", 0]
},
"class_type": "InpaintPreprocessor"
},
"18": {
"inputs": {
"control_net_name": "control_v11p_sd15_inpaint_fp16.safetensors"
},
"class_type": "ControlNetLoader"
},
"19": {
"inputs": {
"strength": 1,
"start_percent": 0,
"end_percent": 1,
"positive": ["4", 0],
"negative": ["5", 0],
"control_net": ["18", 0],
"image": ["17", 0]
},
"class_type": "ControlNetApplyAdvanced"
},
"20": {
"inputs": {
"seed": 258289190362142,
"steps": 20,
"cfg": 7,
"sampler_name": "dpmpp_2m",
"scheduler": "karras",
"denoise": 1,
"model": ["3", 0],
"positive": ["19", 0],
"negative": ["19", 1],
"latent_image": ["16", 0]
},
"class_type": "KSampler"
},
"21": {
"inputs": {
"samples": ["20", 0],
"vae": ["1", 2]
},
"class_type": "VAEDecode"
},
"22": {
"inputs": {
"filename_prefix": "latentlabs360",
"file_type": "JPEG",
"remove_metadata": false,
"images": ["21", 0]
},
"class_type": "SaveImagePlus"
}
}