2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Stable Diffusionのlatentlabs360を使って360度球体パノラマを生成する方法

Posted at

この記事ではStable Diffusionを使って球体パノラマを生成する方法と注意点について紹介します。

私は普段diffusersComfyUIどれも使っていて、使い方はかなり違うので、今回はそれぞれ説明します。

はじめに

球体パノラマとは、球体の表面を360度全部が書かれる長方形の画像のことです。

3D分野でもよく使われています。球体の表面に貼って背景を作ったり、ピカピカの物体の映り込みを作るのに使ったりします。

これは完成品の例です。

latentlabs360_00051_.jpg

そしてこの画像を使ってこのような表面の映り込みを作ることができます。

fig.jpg

ちょっと余談ですが、この映り込み物体の画像は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')

結果はこのようになります。

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')

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を使っても構いません)

latentlabs360_comfyui.jpg

今回使ったチェックポイントモデルはリアル系のAbsoluteRealityです。

結果の画像はこれです。この画像をドラッグしてワークフローをロードすることができます。(ただしSave Image Plus for ComfyUI又はExtended Save Image for ComfyUIプラグインをインストールしている必要があります)

latentlabs360_00003_.jpg

ワークフロー

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と一緒に使う方法

プラグインのインストール

この方法はかなり沢山のノードを使う必要があるのでかなり複雑になります。

追加でインストールする必要があるプラグインは以下です。

ノードの組み方

組んでみたらこんな感じです。

latentlabs360_comfyui2.png

このワークフローでできた画像。

latentlabs360_00163_.jpg

以上のワークフローは細かすぎて詳細まで表示されないので、この画像を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"
  }
}
2
5
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
2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?