MkItYs

MkItYs > AI・交渉・物語の自動生成 > 

images

AI で生成した<動くキャラ>と、VR/MR で対面する:ComfyUI, GIMP, Virtual Desktop, Quest

images

- 2024.04.20

画像生成モデルで動画化したキャラを、ステレオグラムで立体化し、VR/MR で対面してみました:



※1
生成画像は、次のモデルを使用しています:
https://huggingface.co/WarriorMama777/OrangeMixs

関連


ComfyUI で画像生成 〜 なぜそこにつなぐのか:ComfyUI, Stable Diffusion
ComfyUI を、クラウドのコンテナに設置する:ComfyUI, Docker, GCP
ComfyUI の API を使う:ComfyUI, API, JSON, Python, Docker, GCP

検証:修正版


アプリ
画像生成:ComfyUI
VR/MR
PC〜HMD間ストリミーング:Virtual Desktop
HMD:Quest 2
サーバ
クラウド:GCP
コンテナ:Docker
ホスト:Ubuntu 22.04
ゲスト:Ubuntu 22.04

検証:通常版


アプリ
画像生成:ComfyUI
画像編集:GIMP
VR/MR
PC〜HMD間ストリミーング:Virtual Desktop
HMD:Quest 2
クライアント
コンテナ:Docker Desktop
ホスト:macOS
ゲスト:Ubuntu 22.04
サーバ
クラウド:GCP
コンテナ:Docker
ホスト:Ubuntu 22.04
ゲスト:Ubuntu 22.04

概要


いまは、一枚の画像から深度を推定する技術が進歩し、立体視化もそれほど違和感のないレベルに達しています。また画像生成モデルだけでも、あるていどの動画生成は可能になっています。

ここでは、キャラの動画を立体視できるようにして、VR/MR(パススルー)で対面してみます。

前提


ComfyUI では、次のカスタムノード群を使います:


動画生成(AnimateDiff)
https://github.com/Kosinkadink/ComfyUI-AnimateDiff-Evolved

文章(プロンプト)から生成される画像を、さらに動画化します。


物体検出(Rembg/U2-Net)[※1]
https://github.com/cubiq/ComfyUI_essentials[※2]
https://github.com/Jcd1230/rembg-comfyui-node[※3]

動画のフレーム(画像)群から物体(キャラ)を検出し、背景と切り離しますーーこれで、目的の対象だけを表示できるようになります。


深度推定(ControlNet Auxiliary Preprocessors/MiDaS)[※4]
https://github.com/Fannovel16/comfyui_controlnet_aux

動画のフレーム(画像)群から奥行きを推定し、深度マップ群を作ります。


立体視化(NegiTools/Stereo Image Generator)
https://github.com/natto-maki/ComfyUI-NegiTools[※5]

動画のフレーム(画像)群とその深度マップ群から、ステレオグラムのフレーム群を作成しますーーこれで、VR/MR で、対象を立体視できるようになります。



※1
Rembg は、汎用の物体検出モデル U2-Net を使うコードの総称です。
※2
このカスタムノードは、バッチ処理に対応し、アニメ調を検出できるモデルも使うことができます。ただしパッケージの構成は、物体検出のノード以外にもさまざまなノードがふくまれたものです(アプリ本体の構成によっては、依存関係に問題が出るかもしれません)。
※3
このカスタムノードは、バッチ処理に対応しておらず、アニメ調を検出できるモデルも使えません。ただしパッケージの構成は、物体検出のノードのみですーーこのノードでアニメ調のキャラを使うなら、フラットな塗りだと誤検出も多いので、厚塗り系と呼ばれる生成モデルを使う方がいいかもしれません(それでもフラットな塗りのキャラを使いたいならーーたとえば、ここでの処理は深度マップを生成するので、GIMP などを使い、一定の深さの箇所(背景)を黒の画像と合成する、といったやり方があるかも)。
※4
ここでは深度マップの生成に、MiDaS / control_v11f1p_sd15_depth を使っていますーー生成は軽く速いものの、推定の精度はそれなりですーーキャラがリアル調なら Marigold、アニメ調ならLine2Depth (Line2Normalmap) などを使うと、より精度の高い深度マップが生成されるはずです:
https://github.com/kijai/ComfyUI-Marigold
https://huggingface.co/toyxyz/Line2Depth_sd1.5
※5
このカスタムノードは、バッチ処理に対応していません。また(おそらく入力される深度マップの想定が違うため、ここでの使用のしかただと)平行法(left-right)のステレオグラムを作成するのに、交差法(R-L)を選択する必要がありますーーコード自体は、次の A1111 / Stable Diffusion web UI 向けプラグインのラッパです:
https://github.com/thygate/stable-diffusion-webui-depthmap-script

設置〜作成


カスタムノード群の設置とステレオグラムの作成は、次の2とおりの手順を示しています:

修正版……設置がノーマルでない代わりに、作成はノンストップになります[※1]
通常版……作成がノンストップでない代わりに、設置はノーマルになります
※1
ステレオグラムのカスタムノードがバッチ処理に対応していないので、これを修正したものを使いますーーこれですべての処理を、ワークフロー上で完結させることができます。

設置:修正版


この手順は、設置がノーマルでない代わりに、作成はノンストップになります。

次の手順で、物体検出/立体視化のカスタムノード群を設置します:

物体検出(Rembg/U2-Net)

カスタムノードのフォルダを作成します:

$ cd ${directory_project}
$ mkdir ComfyUI_essentials_002
$ cd ${directory_project}/ComfyUI/custom_nodes
$ ln -s ${directory_project}/ComfyUI_essentials_002 ComfyUI_essentials_002

カスタムノードのファイルを作成します:

ComfyUI_essentials_002/__init__.py
import torch
import torchvision.transforms.v2 as T

def p(image):
    return image.permute([0,3,1,2])

def pb(image):
    return image.permute([0,2,3,1])

class RemBGSession:
    @classmethod
    def INPUT_TYPES(s):
        return {
            "required": {
                "model": (["u2net: general purpose", "u2netp: lightweight general purpose", "u2net_human_seg: human segmentation", "u2net_cloth_seg: cloths Parsing", "silueta: very small u2net", "isnet-general-use: general purpose", "isnet-anime: anime illustrations", "sam: general purpose"],),
                "providers": (['CPU', 'CUDA', 'ROCM', 'DirectML', 'OpenVINO', 'CoreML', 'Tensorrt', 'Azure'],),
            },
        }

    RETURN_TYPES = ("REMBG_SESSION",)
    FUNCTION = "execute"
    CATEGORY = "essentials"

    def execute(self, model, providers):
        from rembg import new_session as rembg_new_session

        model = model.split(":")[0]
        return (rembg_new_session(model, providers=[providers+"ExecutionProvider"]),)

class ImageRemoveBackground:
    @classmethod
    def INPUT_TYPES(s):
        return {
            "required": {
                "rembg_session": ("REMBG_SESSION",),
                "image": ("IMAGE",),
            },
        }

    RETURN_TYPES = ("IMAGE", "MASK",)
    FUNCTION = "execute"
    CATEGORY = "essentials"

    def execute(self, rembg_session, image):
        from rembg import remove as rembg

        image = p(image)
        output = []
        for img in image:
            img = T.ToPILImage()(img)
            img = rembg(img, session=rembg_session)
            output.append(T.ToTensor()(img))

        output = torch.stack(output, dim=0)
        output = pb(output)
        mask = output[:, :, :, 3] if output.shape[3] == 4 else torch.ones_like(output[:, :, :, 0])

        return(output[:, :, :, :3], mask,)

NODE_CLASS_MAPPINGS = {
    "RemBGSession+": RemBGSession,
    "ImageRemoveBackground+": ImageRemoveBackground
}

NODE_DISPLAY_NAME_MAPPINGS = {
    "RemBGSession+": "🔧 RemBG Session",
    "ImageRemoveBackground+": "🔧 Image Remove Background"
}

ライブラリ群を設置します(ComfyUI が動く環境で):[※1]

$ pip install rembg[gpu]
$ pip install kornia
立体視化(Stereo Image Generator)

カスタムノードのフォルダを作成します:

$ cd ${directory_project}
$ mkdir ComfyUI-NegiTools_002
$ cd ${directory_project}/ComfyUI/custom_nodes
$ ln -s ${directory_project}/ComfyUI-NegiTools_002 ComfyUI-NegiTools_002

ラッパ元のソースコードを取得しますーーステレオグラムの生成に必要なのは、次のファイルのみです:

https://github.com/thygate/stable-diffusion-webui-depthmap-script/blob/main/src/stereoimage_generation.py
$ cd ${directory_project}/ComfyUI-NegiTools_002
$ mkdir -p negi dependencies/stable-diffusion-webui-depthmap-script/src
$ cd ${directory_project}/ComfyUI-NegiTools_002/dependencies/stable-diffusion-webui-depthmap-script/src
$ wget https://raw.githubusercontent.com/thygate/stable-diffusion-webui-depthmap-script/main/src/stereoimage_generation.py

初期化ファイルを作成します:

ComfyUI-NegiTools_002/__init__.py
from .negi.stereo_image_generator_002 import StereoImageGenerator_002

NODE_CLASS_MAPPINGS = {
  "NegiTools_StereoImageGenerator_002": StereoImageGenerator_002
}
NODE_DISPLAY_NAME_MAPPINGS = {
  "NegiTools_StereoImageGenerator_002": "StereoImageGenerator_002 🧅"
}

カスタムノードを修正します:

https://github.com/natto-maki/ComfyUI-NegiTools/blob/master/negi/stereo_image_generator.py

これは、[1]バッチ処理ができるようコードを追加し、[2]深度マップを補正するコードを削除したものですーーこの修正により、(API などを介することなく)ノンストップで動画生成から立体視化まで行え、ステレオグラムも平行法(L-R)が使えるようになります(なお(VR/MR には不要ということもあり)、平行法以外のステレオグラム生成のコードも削除しています):

ComfyUI-NegiTools_002/negi/stereo_image_generator_002.py
import importlib
import numpy as np
import torch
import torchvision

_dependency_dir = "dependencies"
_repository_name = "stable-diffusion-webui-depthmap-script"

class StereoImageGenerator_002:
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "l_image": ("IMAGE",),
                "l_depth_image": ("IMAGE",),
                "divergence": ("FLOAT", {
                    "default": 5.0, "min": 0.05, "max": 10.0, "step": 0.01, "round": 0.001, "display": "slider"
                }),
                "stereo_offset_exponent": ("FLOAT", {
                    "default": 1.0, "min": 0.1, "max": 3.0, "step": 0.1, "round": 0.01, "display": "slider"
                }),
                "fill_technique": ([
                    "polylines_sharp", "polylines_soft", "naive", "naive_interpolating", "none"
                ],),
            }
        }

    RETURN_TYPES = ("IMAGE",)
    RETURN_NAMES = ("STEREO_IMAGE",)
    FUNCTION = "doit"
    OUTPUT_NODE = False
    CATEGORY = "Generator"

    def doit(self, l_image, l_depth_image, divergence, stereo_offset_exponent, fill_technique):
        m = importlib.import_module(
            "." + ".".join([_dependency_dir, _repository_name, "src", "stereoimage_generation"]),
            ".".join(__name__.split(".")[:-2]))
        modes = ["left-right"]

        l = []
        for (i, _) in enumerate(l_image):
            image = torchvision.transforms.functional.to_pil_image(torch.permute(l_image[i], (2, 0, 1)))
            depth_map = l_depth_image[i].to('cpu').detach().numpy()[:, :, 0]
            images = m.create_stereoimages(
                image, depth_map, divergence, modes=modes,
                stereo_offset_exponent=stereo_offset_exponent, fill_technique=fill_technique)
            l.append(torchvision.transforms.functional.to_tensor(images[0]).permute(1, 2, 0).unsqueeze(0))

        return (torch.cat(l),)

※1
物体検知のモデル U2-Net は、ホームディレクトリに配置されます:
${HOME}/.u2net/u2net.onnx

作成:修正版


この手順は、設置がノーマルでない代わりに、作成はノンストップになります。

次の手順で、動画のステレオグラム群を作成します:

次のフローを実行すれば、動画のステレオグラム(GIF 形式)がノード上に生成されますーーこのノードから、ファイルをローカルに保存します。:

${directory_development}/cnf/try004_workflow_stereogram.json

設置:通常版


この手順は、作成がノンストップでない代わりに、設置はノーマルになります。

次の手順で、物体検出/立体視化のカスタムノード群を設置します:

物体検出(Rembg/U2-Net)

カスタムノードを取得します:

$ cd ${directory_project}
$ git clone --depth=1 https://github.com/Jcd1230/rembg-comfyui-node.git
$ cd ${directory_project}/ComfyUI/custom_nodes
$ ln -s ${directory_project}/rembg-comfyui-node rembg-comfyui-node

ライブラリ群を設置します(ComfyUI が動く環境で):[※1]

$ pip install rembg[gpu]
$ pip install kornia
立体視化(Stereo Image Generator)

カスタムノードのフォルダを作成します:[※2]

$ cd ${directory_project}
$ mkdir ComfyUI-NegiTools_002
$ cd ${directory_project}/ComfyUI/custom_nodes
$ ln -s ${directory_project}/ComfyUI-NegiTools_002 ComfyUI-NegiTools_002

カスタムノードのファイルを取得しますーーステレオグラムの生成に必要なのは、次のファイルのみです:

https://github.com/natto-maki/ComfyUI-NegiTools/blob/master/negi/stereo_image_generator.py
https://github.com/natto-maki/ComfyUI-NegiTools/blob/master/negi/noise_image_generator.py
$ cd ${directory_project}/ComfyUI-NegiTools_002
$ mkdir negi
$ cd ${directory_project}/ComfyUI-NegiTools_002/negi
$ wget https://raw.githubusercontent.com/natto-maki/ComfyUI-NegiTools/master/negi/stereo_image_generator.py
$ wget https://raw.githubusercontent.com/natto-maki/ComfyUI-NegiTools/master/negi/noise_image_generator.py

初期化ファイルを作成します:

ComfyUI-NegiTools_002/__init__.py
from .negi.stereo_image_generator import StereoImageGenerator

NODE_CLASS_MAPPINGS = {
  "NegiTools_StereoImageGenerator": StereoImageGenerator
}
NODE_DISPLAY_NAME_MAPPINGS = {
  "NegiTools_StereoImageGenerator": "Stereo Image Generator 🧅"
}

※1
物体検知のモデル U2-Net は、ホームディレクトリに配置されます:
${HOME}/.u2net/u2net.onnx
※2
ノードが最初に使われるときに、ラッパするライブラリを取得するため、次のリポジトリからフォルダ内に、コード群が配置されます:
https://github.com/thygate/stable-diffusion-webui-depthmap-script

作成:通常版


この手順は、作成がノンストップでない代わりに、設置はノーマルになります。

次の手順で、動画のステレオグラム群を作成します:

まず、動画のフレーム群を生成しますーー最初は、Save Image ノードは無効にしておいた方がいいかもしれません。

生成結果を動画(GIF )で確認しつつ、プロンプトやシードを決めていきます。決まったら、履歴からその状態に戻り、Save Image ノードを有効にします。ふたたび生成を実行すると、動画のフレームになる画像群が、output フォルダに保存されます:

${directory_development}/cnf/try004_workflow_animatediff.json
images

フォルダ output に生成されたフレーム群を、フォルダ input に移します:

$ cd ${directory_comfyui}/input
$ mv ${directory_comfyui}/output/??????_???_?????_.png .

ComfyUI の API を使って、動画のフレーム群をステレオグラムのフレーム群に変換します。

元になるワークフローは、画像1枚ごとに、[1]背景を抜く〜[2]深度マップを作る〜[3]ステレオグラムのフレームを作る、ように構成します(なお、Rembg ノードが出力する背景は透過状態(アルファチャネルでの指定)になるので、Split Image with Alpha ノードを介して(Stable Diffusion があつかえる)通常の画像のマスクに変換します)。

このワークフローを、フレームの枚数分だけ繰り返すよう、スクリプトを作成します。このスクリプトを、Python を実行できる環境から、ComfyUI が動く環境に対し、実行します:[※1]

${directory_development}/cnf/try004_workflow_api_stereogram.json
images
${directory_development}/bin/try004.py
import sys
import time
import json
from urllib import request

def reqcmf(dctflw):
  prompt = {'prompt': dctflw}
  conweb = json.dumps(prompt).encode('utf-8')
  reqweb = request.Request(f"{urltgt}prompt", data=conweb)
  request.urlopen(reqweb)

urltgt = sys.argv[1]
strflw = sys.stdin.read()
dctflw = json.loads(strflw)

l = list(range(1,17)) 
for i in l:
  dctflw['2']['inputs']['image'] = 'try004_frm_' + f'{i:05}' + '_.png'
  reqcmf(dctflw)
  time.sleep(1)
$ cat ${directory_development}/cnf/try004_workflow_api_stereogram.json | python ${directory_development}/bin/try004.py http://${server}:${port}/

作成されたステレオグラムのフレーム群を取得します:

$ cd ${directory_working}
$ scp ${user}@${server}:${directory_comfyui}/output/'??????_???_?????_.png' .

画像編集アプリの GIMP で、フレーム群から動画ファイル(GIF)を作ります:[※2]

> file open: <image_1>.png
> open as layers: <image_2>.png, ..., <image_N>.png # 一括での選択可

> filters > animation > 
  > optimize (for gif)
  > playback # 再生を確認

> file > export as
  > select file type (by extension)
    > <image>.gif
    > [export]
      > as animation: <yes>
images

※1
ここのカスタムノードの”Image batch To Image List”を使っても、たんにテンソルをリストに変換するだけでしょうし……:
https://github.com/ltdrdata/ComfyUI-Impact-Pack
※2
このステレオグラムは、平行法でみることができますーー連続した画像でも、統一した立体感が与えられていることを確認できると思います(統一感はあるものの、右足の遠近感が微妙ですね……ただこの生成画像だとこうするしかなさそうで、深度推定はがんばってると思います)。

利用


動画ファイル(GIF)を、Virtual Desktop Streamer が動く環境に移します。

HMDで Virtual Desktop を実行し、動画ファイル(GIF)を画像ビューアで開きますーーデスクトップ画面のモードを、SBS (Side-by-Side) + Transparency にすることで、生成した動くキャラと MR で対面できます:[※1][※2][※3]

> menu (bottom) > full sbs
> menu (top) > transparency

※1
動画の上下がはみ出すときは、ディスプレの向きの設定を縦にするなどし、表示できる領域を確保します。
※2
Windows 既定の画像ビューアや、その他のビューアも使えます(IrfanView, ...)。
※3
ここでクロマキーが使えればいいんですけどねーーVirtual Desktop のデスクトップ画面は、黒色を透過させるだけですーーSteamVR のメディアプレイヤなら(Virtual Desktop のゲーム内になるので)SBS でもクロマキーが使えますが、画面の位置調整が煩雑など、使い勝手がよくありませんしーー一長一短です……

感想


目の前にいるキャラは、奥行きがあり動いてもいるので、じっさいにその場にいるように感じられますーーこれがたった数文字のプロンプトから生まれた存在だと考えると、感慨深いものがありますね。[※1][※2]


※1
いまのところただの動画なので、触れてもスカスカな状態ですーーただ深度マップはもっているので、HMDの深度センサと組み合わせれば、<触れる>感触を得ることも可能ですーーこれがリアルタイムに生成できるようになれば、インタラクティブ性をもたせることもできるでしょうし、物理演算をふくめた多彩な反応が得られそうです。
※2
もちろんその数文字のプロンプトの背後には、膨大な数の創作者(絵画〜写真)の熱意と努力の成果がありますーー生成モデルの普及で、それらの利用のしかたが、<個別の作品を鑑賞・体験する>というかたちから、<作品の集合を融合・体験する>というかたちに変わってきていますーー問題のひとつは、もともとあった<出典>(作家と作品など)という構造が、フラットになってしまっている、という点です。ここをどう解決していくかが、技術側の課題といえそうです。