📖

ComfyUI-AdvancedLivePortraitを使ってみる(No WebUI)(Google Colab)

2024/09/10に公開

はじめに

AIにより生成された画像に対して、顔の傾きや、目線、口、笑顔などの微調整をしたいと思ったことはありませんか?

実は、すでにできるんです!

こちらの「ひろちゅ〜」様のポストの通り、パラメータを変更させることで、目線や、目を開ける大きさ、口、ウインク、顔の向きなど様々な要素を変更することができるため、AIにより人の顔を一旦生成できれば、あとは手元で画像を見ながら、パラメータを調整して、好きな画像を作ることができます。

そして、ポストにもありますが、上記の技術は「ComfyUI-Advanced-LivePortait」という技術が使われているそうです。
こちらは「ComfyUI」というStable DiffusionのWebUIの一つで利用できる拡張機能のようなものだと思います。

実際に下記のように、ComfyUIを利用することで、この技術を簡単に利用できるそうです。

私もこの技術を使ってみたいですが、私は宗教上の理由で画像生成AIのWebUIを利用できない()ので、上記の技術をWebUIを使わずに、PythonスクリプトとしてGoogle Colabで実行してみたいと思います。

実装の方向性としては、「ComfyUI」の拡張機能として実装された「ComfyUI-Advanced-LivePortait」を「ComfyUI」の依存関係なしに実行することを目標に、元のコードなどを修正しています。

成果物

下記のリポジトリをご覧ください。
https://github.com/personabb/colab_AI_sample/tree/main/colab_AdvancedLivePortrait_sample

事前準備

リポジトリのクローン

まずは上記のリポジトリをcloneしてください。

./
git clone https://github.com/personabb/colab_AI_sample.git

その後、cloneしたフォルダの中である「colab_AI_sample/colab_AdvancedLivePortrait_sample」をマイドライブの適当な場所においてください。

編集元の画像を用意する

編集元の画像を用意してinputsフォルダにinputs/inputs001.png格納してください。

今回の実験では、下記のモデルにより生成されたAI画像を利用します
https://civitai.com/models/129830/fudukimix

実際に利用したのは下記の画像です

想定されるディレクトリ構造

Google Driveのディレクトリ構造は下記を想定します。

MyDrive/
    └ colab_AI_sample/
          └ colab_AdvancedLivePortrait_sample/
                  ├ configs/
                  |    └ config.ini
                  ├ inputs/
                  |    └ inputs001.png
                  ├ outputs/
                  ├ module/
                  |    ├ utils/
                  |    |   ├ __init__.py
                  |    |   └ checkpoint_pickle.py
                  |    |  
                  |    └ advanced_live_portrait.py
                  |    
                  └ AdvancedLivePortrait.ipynb

使い方

実行方法

AdvancedLivePortrait.ipynbをGoogle Colabratoryアプリで開いて、後述するパラメータを設定したあと、一番上のセルから順番に一番下まで実行すると、画像が1枚「outputs」フォルダに生成されます。

また、最後まで実行後、パラメータを変更して再度実行する場合は、[5]セル目のパラメータを変更したあと、[5][6]セル目だけを実行してください(再起動は不要ですが、画像の上書きには注意してください)

パラメータの変更

AdvancedLivePortrait.ipynbの5セル目が該当します。

input_image_path = "./inputs/inputs001.png"  # 編集したい画像のパスを指定
output_image_path = "./outputs/edited_image.png"  # 編集後の画像を保存するファイル名

# 表情を変更するための設定
rotate_pitch = 0  # 顔の縦方向の回転(前後)
rotate_yaw = 0    # 顔の横方向の回転(左右)
rotate_roll = 0   # 顔の傾き
blink = 0         # 瞬きの度合い
eyebrow = 0       # 眉毛の動き
wink = 0          # 片目のウインク
pupil_x = 0       # 瞳の左右の動き
pupil_y = 0       # 瞳の上下の動き
aaa = 0           # 口を開ける動作
eee = 0           # 口を「イ」と発音する動作
woo = 0           # 口を「ウ」と発音する動作
smile = 0       # 笑顔の度合い(0.5 = 50%笑顔)

パラメータの説明は、パラメータの横に記載しています。
注意点としては、基本的に全て0で設定すると、入力画像と同じような画像が出力されます。
また、smile以外のパラメータは0-1の範囲で制限されていません。10などの数値が入ることもザラです。

実行結果

時間がなくて、全く実験していないので、2つだけ例を提示します。

左の画像はAIにより生成された画像で、右の画像が本技術で目線を変更した画像になります。

こちらを見ると、ちゃんと目線の変更に成功していることがわかります。

パラメータは下記で実行しました。

# 表情を変更するための設定
rotate_pitch = 0  # 顔の縦方向の回転(前後)
rotate_yaw = 0    # 顔の横方向の回転(左右)
rotate_roll = 0   # 顔の傾き
blink = 0         # 瞬きの度合い
eyebrow = 0       # 眉毛の動き
wink = 0          # 片目のウインク
pupil_x = 8       # 瞳の左右の動き
pupil_y = 0       # 瞳の上下の動き
aaa = 0           # 口を開ける動作
eee = 0           # 口を「イ」と発音する動作
woo = 0           # 口を「ウ」と発音する動作
smile = 0       # 笑顔の度合い(0.5 = 50%笑顔)

2枚目です。
左の画像はAIにより生成された画像で、右の画像が本技術で色々変更した画像になります。

パラメータは下記で実行しました。

# 表情を変更するための設定
rotate_pitch = 0  # 顔の縦方向の回転(前後)
rotate_yaw = 0    # 顔の横方向の回転(左右)
rotate_roll = 15   # 顔の傾き
blink = 0         # 瞬きの度合い
eyebrow = 0       # 眉毛の動き
wink = 15          # 片目のウインク
pupil_x = 0       # 瞳の左右の動き
pupil_y = 0       # 瞳の上下の動き
aaa = 0           # 口を開ける動作
eee = 0           # 口を「イ」と発音する動作
woo = 0           # 口を「ウ」と発音する動作
smile = 1       # 笑顔の度合い(0.5 = 50%笑顔)

全く違う表情の画像にも変換できました。

まとめ

今回は、「ComfyUI」というWebUIでの実行が前提となっている「ComfyUI-Advanced-LivePortait」という技術を、WebUIを使わずにPythonスクリプト上で実行できるように再構築しました。

本来であれば、WebUIと同様に、スライドバーで変更できるようにするべきではありますが、フロントエンドの知識は未熟なので今回はパラメータとして指定するだけにしました。
Next.jsを利用した簡易的なフロントを作ってみました!
https://zenn.dev/asap/articles/c49551ff14d5e9

この技術は「LivePortrait」というカメラ入力動画から、静止画像を動かす技術をベースに作られているので、時間があればこちらの「LivePortrait」の技術も勉強してみたいと思います。

ではここまで読んでくださりありがとうございました!

ここからは、実際に実装したコードの解説になります。
興味のある方がご覧ください。

コード解説

ここからは、実際に実装したコードを紹介します。
方向性としては、「ComfyUI」の拡張機能として実装された「ComfyUI-Advanced-LivePortait」を「ComfyUI」の依存関係なしに実行することを目標に、元のコードなどを修正しています。

AdvancedLivePortrait.ipynb

AdvancedLivePortrait.ipynbについて解説します。

コードは下記よりご覧ください。
https://github.com/personabb/colab_AI_sample/blob/main/colab_AdvancedLivePortrait_sample/AdvancedLivePortrait.ipynb

1セル目


#Google Driveのフォルダをマウント(認証入る)
from google.colab import drive
drive.mount('/content/drive')

マイドライブのマウントを行っています。
認証が入り、一定時間認証しないとエラーになってしまうので、最初に持ってきて実行と認証をまとめて実施します。

2セル目

#必要なモジュールのインストール
%rm -r /content/ComfyUI-AdvancedLivePortrait
%cd /content/
!git clone https://github.com/PowerHouseMan/ComfyUI-AdvancedLivePortrait.git
%cd /content/ComfyUI-AdvancedLivePortrait
!pip install -r requirements.txt
%cd /content/

必要のモジュールをインストールしています。
「ComfyUI-AdvancedLivePortrait」のリポジトリをクローンして、必要なモジュールをインストールした上で、カレントディレクトリを元に戻しています。

3セル目


# カレントディレクトリを本ファイルが存在するディレクトリに変更する。
import glob
import os
pwd = os.path.dirname(glob.glob('/content/drive/MyDrive/colabzenn/colab_AdvancedLivePortrait_sample/AdvancedLivePortrait.ipynb', recursive=True)[0])
print(pwd)

%cd $pwd
!pwd

import sys
sys.path.append("/content/ComfyUI-AdvancedLivePortrait")


カレントディレクトリの設定と、ComfyUI-AdvancedLivePortraitリポジトリのPATHを追加しています。

4セル目

#モジュールをimportする
from module.advanced_live_portrait import AdvancedLivePortrait_execution

モジュールをinportしています。
AdvancedLivePortraitの実行コードはmodule/advanced_live_portrait.pyで記述しております。
そちらも後述します

5セル目

input_image_path = "./inputs/inputs001.png"  # 編集したい画像のパスを指定
output_image_path = "./outputs/edited_image.png"  # 編集後の画像を保存するファイル名


# 表情を変更するための設定
rotate_pitch = 0  # 顔の縦方向の回転(前後)
rotate_yaw = 0    # 顔の横方向の回転(左右)
rotate_roll = 0   # 顔の傾き
blink = 0         # 瞬きの度合い
eyebrow = 0       # 眉毛の動き
wink = 0          # 片目のウインク
pupil_x = 0       # 瞳の左右の動き
pupil_y = 0       # 瞳の上下の動き
aaa = 0           # 口を開ける動作
eee = 0           # 口を「イ」と発音する動作
woo = 0           # 口を「ウ」と発音する動作
smile = 0       # 笑顔の度合い(0.5 = 50%笑顔)

画像を編集する際のパラメータや、編集元の画像、編集後の画像の保存先などを指定しています。

6セル目

parameters = [rotate_pitch, rotate_yaw, rotate_roll, blink, eyebrow, wink, pupil_x, pupil_y, aaa, eee, woo, smile]
AdvancedLivePortrait_execution(input_image_path,output_image_path,parameters)

AdvancedLivePortraitを実行しています。
生成された画像はoutputsフォルダに格納されます。

module/advanced_live_portrait.py

module/advanced_live_portrait.pyについて解説します。

コードは下記よりご覧ください
https://github.com/personabb/colab_AI_sample/blob/main/colab_AdvancedLivePortrait_sample/module/advanced_live_portrait.py

基本的には、こちらnodes.pyの実装を流用しています。

ただし、上記のコードでは、import folder_pathsの部分やimport comfy.utilsの部分、またそれを利用するコードにおいて、「ComfyUI」に依存しているため、その依存部分の解消をします。

加えて、画像の入力部分やLivePortraitのimport文を微修正と、スクリプトとして利用する上で不要な部分の削除、入力画像が32の倍数じゃない場合の適切なリサイズなどを実施しています。

主に下記の部分に関して変更しています。

コードの変更箇所
advanced_live_portrait.py
import os
import sys
import numpy as np
import torch
import cv2
from PIL import Image
import time
import copy
import dill
import yaml
from ultralytics import YOLO
from module.utils import checkpoint_pickle

current_directory = "/content/ComfyUI-AdvancedLivePortrait"
models_dir = "/content/ComfyUI-AdvancedLivePortrait/models"
os.makedirs(models_dir, exist_ok=True)

import math
import struct
import safetensors.torch
import logging
import itertools

def load_torch_file(ckpt, safe_load=False, device=None):
    if device is None:
        device = torch.device("cpu")
    if ckpt.lower().endswith(".safetensors") or ckpt.lower().endswith(".sft"):
        sd = safetensors.torch.load_file(ckpt, device=device.type)
    else:
        if safe_load:
            if not 'weights_only' in torch.load.__code__.co_varnames:
                logging.warning("Warning torch.load doesn't support weights_only on this pytorch version, loading unsafely.")
                safe_load = False
        if safe_load:
            pl_sd = torch.load(ckpt, map_location=device, weights_only=True)
        else:
            pl_sd = torch.load(ckpt, map_location=device, pickle_module=checkpoint_pickle)
        if "global_step" in pl_sd:
            logging.debug(f"Global Step: {pl_sd['global_step']}")
        if "state_dict" in pl_sd:
            sd = pl_sd["state_dict"]
        else:
            sd = pl_sd
    return sd


from LivePortrait.live_portrait_wrapper import LivePortraitWrapper
from LivePortrait.utils.camera import get_rotation_matrix
from LivePortrait.config.inference_config import InferenceConfig

from LivePortrait.modules.spade_generator import SPADEDecoder
from LivePortrait.modules.warping_network import WarpingNetwork
from LivePortrait.modules.motion_extractor import MotionExtractor
from LivePortrait.modules.appearance_feature_extractor import AppearanceFeatureExtractor
from LivePortrait.modules.stitching_retargeting_network import StitchingRetargetingNetwork
from collections import OrderedDict

・・・

class LP_Engine:
・・・
    def resize_image_to_stride(self,image_tensor, stride=32):
        # 画像の高さと幅を取得
        _, _, h, w = image_tensor.shape

        # ストライドで割り切れるサイズにリサイズする
        new_h = (h // stride) * stride
        new_w = (w // stride) * stride

        # テンソルを NumPy に変換してリサイズ
        image_np = image_tensor.squeeze(0).permute(1, 2, 0).cpu().numpy()  # (B, C, H, W) -> (H, W, C)
        resized_image_np = cv2.resize(image_np, (new_w, new_h), interpolation=cv2.INTER_LINEAR)

        # リサイズされた画像をテンソルに戻す
        resized_image_tensor = torch.from_numpy(resized_image_np).permute(2, 0, 1).unsqueeze(0).float()  # (H, W, C) -> (B, C, H, W)

        return resized_image_tensor

    def get_face_bboxes(self, image_rgb):
        detect_model = self.get_detect_model()

        # 画像が正しく読み込まれているかチェック
        if image_rgb is None or image_rgb.size == 0:
            raise ValueError("The input image is empty or invalid.")

        # 画像が3チャンネル (RGB) であることを確認
        if len(image_rgb.shape) == 2:  # もしグレースケール画像の場合、3チャンネルに変換
            image_rgb = cv2.cvtColor(image_rgb, cv2.COLOR_GRAY2RGB)

        elif len(image_rgb.shape) == 3 and image_rgb.shape[2] == 1:  # 1チャンネル (モノクロ画像) も3チャンネルに変換
            image_rgb = cv2.cvtColor(image_rgb, cv2.COLOR_GRAY2RGB)

        # 画像が正しい次元か確認し、4次元に拡張 (YOLOが期待するバッチ形式)
        if len(image_rgb.shape) == 3:
            image_rgb = np.expand_dims(image_rgb, axis=0)  # (height, width, 3) -> (1, height, width, 3)

        # 次元を (batch_size, channels, height, width) の形式に変換
        image_rgb = torch.from_numpy(image_rgb).permute(0, 3, 1, 2).float()  # (1, height, width, 3) -> (1, 3, height, width)

        # 0-255の範囲のピクセル値を0-1に正規化
        image_rgb /= 255.0

        # デバッグ用にサイズを確認
        print(f"Image dimensions before YOLO: {image_rgb.shape}")

        image_rgb = self.resize_image_to_stride(image_rgb, stride=32)


        pred = detect_model(image_rgb, conf=0.7, device="")  # YOLOモデルに入力
        return pred[0].boxes.xyxy.cpu().numpy()  # 検出したバウンディングボックスを返す

・・・

    def GetMaskImg(self):
        if self.mask_img is None:
            path = os.path.join(current_directory, "LivePortrait/utils/resources/mask_template.png")
            self.mask_img = cv2.imread(path, cv2.IMREAD_COLOR)
            if self.mask_img is None:
                raise FileNotFoundError(f"Mask image not found at path: {path}")
        return self.mask_img

    def prepare_source(self, source_image, crop_factor, is_video = False, tracking = False):
        print("Prepare source...")
        engine = self.get_pipeline()
        source_image_np = (source_image * 255).byte().cpu().numpy()

・・・


def AdvancedLivePortrait_execution(input_image_path, output_image_path, parameters):
    # 入力画像のパスを指定します

    if not os.path.exists(os.path.dirname(output_image_path)):
        os.makedirs(os.path.dirname(output_image_path))

    # 顔を含む画像を読み込む
    image = Image.open(input_image_path)
    image = image.convert("RGB")
    print(image.size)
    img_tensor = pil2tensor(image).to(get_device())  # 画像をテンソルに変換し、GPUに転送

    print(img_tensor.shape)

    print("load image")
    # LP_Engineクラスのインスタンス作成
    engine = LP_Engine()

    print("load LP_Engine")
    # 顔の検出と準備
    crop_factor = 1.7  # 顔のクロップサイズ
    prepared_face = engine.prepare_source(img_tensor, crop_factor)  # 顔の準備

    # 表情を編集するためのExpressionEditorクラスのインスタンスを作成
    editor = ExpressionEditor()

    print("load ExpressionEditor")

    # 表情を変更するための設定
    rotate_pitch = parameters[0]  # 顔の縦方向の回転(前後)
    rotate_yaw = parameters[1]    # 顔の横方向の回転(左右)
    rotate_roll = parameters[2]   # 顔の傾き
    blink = parameters[3]         # 瞬きの度合い
    eyebrow = parameters[4]       # 眉毛の動き
    wink = parameters[5]          # 片目のウインク
    pupil_x = parameters[6]       # 瞳の左右の動き
    pupil_y = parameters[7]       # 瞳の上下の動き
    aaa = parameters[8]           # 口を開ける動作
    eee = parameters[9]           # 口を「イ」と発音する動作
    woo = parameters[10]          # 口を「ウ」と発音する動作
    smile = parameters[11]        # 笑顔の度合い(0.5 = 50%笑顔)

    print("start running")

    # 表情を編集し、結果の画像を取得
    result = editor.run(
        rotate_pitch, rotate_yaw, rotate_roll,
        blink, eyebrow, wink, pupil_x, pupil_y,
        aaa, eee, woo, smile,
        src_ratio=1, sample_ratio=1,
        sample_parts="All",  # 全体的な表情を編集する
        crop_factor=1.7,
        src_image=img_tensor  # 先ほど準備した顔画像を指定
    )

    # 結果を展開
    edited_img, motion_link, expression_data = result["result"]

    print("finish running")

    # 結果の画像を保存する
    edited_image_pil = tensor2pil(edited_img)  # テンソルをPIL画像に変換
    edited_image_pil.save(output_image_path)
    print(f"Edited image saved at: {output_image_path}")

特にAdvancedLivePortrait_executionが元々のnodes.pyになかった新規で実装した関数になります。

こちらの関数では、指定したパスの画像を下記部分で読み込みます。

    # 顔を含む画像を読み込む
    image = Image.open(input_image_path)
    image = image.convert("RGB")
    print(image.size)
    img_tensor = pil2tensor(image).to(get_device())  # 画像をテンソルに変換し、GPUに転送

その後、YOLOモデル(物体検出モデル)を利用して、顔の範囲を取得します。

    # LP_Engineクラスのインスタンス作成
    engine = LP_Engine()

    print("load LP_Engine")
    # 顔の検出と準備
    crop_factor = 1.7  # 顔のクロップサイズ
    prepared_face = engine.prepare_source(img_tensor, crop_factor)  # 顔の準備

その後、指定したパラメータに合わせて、ExpressionEditorクラス内のrunメソッドを利用して画像を編集します。

    # 表情を編集し、結果の画像を取得
    result = editor.run(
        rotate_pitch, rotate_yaw, rotate_roll,
        blink, eyebrow, wink, pupil_x, pupil_y,
        aaa, eee, woo, smile,
        src_ratio=1, sample_ratio=1,
        sample_parts="All",  # 全体的な表情を編集する
        crop_factor=1.7,
        src_image=img_tensor  # 先ほど準備した顔画像を指定
    )

    # 結果を展開
    edited_img, motion_link, expression_data = result["result"]

最終的に画像を保存しています。

    # 結果の画像を保存する
    edited_image_pil = tensor2pil(edited_img)  # テンソルをPIL画像に変換
    edited_image_pil.save(output_image_path)
    print(f"Edited image saved at: {output_image_path}")

まとめ

ここまで読んでくださってありがとうございました!

Discussion