見出し画像

画像の切り替えというか補完をVAEで - Stable DiffusionとPythonの画像処理と自動化(5)

前回の記事で、背景画像の切り替え、みんなどうやってんの的なことお聞きして終わってしまいましたが、自己解決です。なんとなく。
それがこちら。

ところで攻殻機動隊は草薙素子の声優の田中敦子さん、R.I.P.
あなたの少佐の声は、いつまでも僕のゴーストの中で囁いています。

前回、Python でクロスフェードみたいなことさせる画像処理のコード書きましたとお話しましたが、僕の記事を読んでいただいている方は見覚えがあるこちらのウィンクしてくれるお姉さんの、ウィンクもそうなんですね。

でもこれって単に、2つの画像の間のピクセルの輝度を、重みを付けて変化させてるだけなんですよね。細かいこと言うと、線形じゃなくて時間に対して正弦波のイージングカーブのような感じで重みが変化するようにはしてはいますが(もちろん線形にすることも可能というかむしろ計算式は簡単)

でもなんかこー、途中のフレームみると、結局2枚の画像の重ね合わせを重み付けして重ねてる感がどうしてもぬぐえない。
画像生成の AI というものが世の中にあるのにこれじゃあ単なる画像処理。
この辺のフレームがとても分かりやすいですね。

最初と最後の画像が重なってる感じ

というか、もうなんなら、全然構図が違う画像同士の補完をしちゃいたいんですよ。これくらい違う画像間の。

この画像からはじめて
この画像に至らせたい

この2つの画像は、Seed が違うだけで同じプロンプトと同じパラメータで Stable Diffusion で生成したんですが、生成 AI を使ってこういう2つの画像間の補完がどうにかできないものかと、AnimateDiff や Deforum をつかってみるんですが、どうにもこうにもうまくいかない。やってみていただければわかるんですが、途中の画像がぐっちゃぐちゃになるんですよ。
すいません、やり方わかる人、教えてくれたら、フォローでもサブスクライブでもなんでもしますんで教えて!

でもまあ、まだウィンクするお姉さんくらいならともかく、こんなに構図が違う画像間で補完させようなんて無茶もいいところ。
普通、関連性がある動画同士を補完させたり、ダンスするモーション映像で ControlNet つかうとか、Deforum でフレーム毎にプロンプト指定してどうにかしますよね。

実際問題、Deforum も ControlNet も面白いです。特にざすこさんのこちらの動画は、何度もみて真似をしました。みなさんもぜひ参考にしてチャンネル登録しちゃいましょう。

こちらは応用編なので、慣れてない方は基礎編から見た方がよいでしょう。

ただ僕は、短い動作がある映像が作りたいわけでも、先々の映像で何ができるかわからないような映像が作りたいわけでもなく、プロンプトトラベルしたいわけでもなく、これと思うわがままな2つの画像の間を補完し、さらにはそれの繰り返しで長い映像に発展させる過程を、自動化したいのです。

画像処理的アプローチで複数の画像を作って重ね合わせるんじゃなくて、生成 AI 風なアプローチで、画像の一貫性あるいはテーマを保った映像の生成を可能なかぎり自動化したい。

で、結局 Python を書いちゃう俺。
今回は diffusers ライブラリで Stable Diffusion の API も使いませんでした。
はい、ソースコードは早速こちら。

import torch
from diffusers import AutoencoderKL
from PIL import Image
import numpy as np
import os
import shutil
import math

def interpolate_images(first_image, last_image, output_dir, num_steps=29, model_id="stable-diffusion-v1-5/stable-diffusion-v1-5", model_image_size=512, mode='sin'):

    if torch.cuda.is_available():
        print("GPU is available. Device:", torch.cuda.get_device_name(0))
        device = "cuda"
    else:
        print("GPU is not available. Using CPU.")
        device = "cpu"

    os.makedirs(output_dir, exist_ok=True)

    print(f"Using {first_image} as the first image and {last_image} as the last image.")

    base_name = os.path.splitext(first_image)[0]

    vae = AutoencoderKL.from_pretrained(model_id, subfolder="vae", torch_dtype=torch.float16).to(device)

    def image_to_latent(image_path):
        original_image = Image.open(image_path).convert("RGB")
        original_size = original_image.size
        resized_image = original_image.resize((model_image_size, model_image_size))
        image = np.array(resized_image).astype(np.float32) / 255.0
        image = (image * 2.0) - 1.0
        image = torch.from_numpy(image).permute(2, 0, 1).unsqueeze(0).to(device)
        image = image.half()
        with torch.no_grad():
            latent = vae.encode(image).latent_dist.sample()
        return latent * vae.config.scaling_factor, original_size

    def latent_to_image(latent, original_size):
        latent = 1 / vae.config.scaling_factor * latent
        with torch.no_grad():
            image = vae.decode(latent).sample
        image = (image / 2 + 0.5).clamp(0, 1)
        image = image.cpu().permute(0, 2, 3, 1).float().numpy()
        image = (image * 255).round().astype("uint8")[0]
        pil_image = Image.fromarray(image)
        return pil_image.resize(original_size, Image.LANCZOS)

    def interpolate_latents(latent1, latent2, num_steps, mode='sin'):
        if mode == 'linear':
            for i in range(num_steps + 1):
                t = i / num_steps
                interpolated_latent = (1 - t) * latent1 + t * latent2
                yield interpolated_latent
        else:
            for i in range(num_steps + 1):
                adjusted_t = None
                if mode == 'quad':
                    t = (i + 1.0) / (num_steps + 1) if num_steps > 0 else i
                    adjusted_t = t ** 2
                elif mode == 'sin':
                    t = (i + 1.0) / (num_steps + 1) if num_steps > 0 else i
                    eased_t = (math.sin((t - 0.5) * math.pi) + 1) / 2
                    adjusted_t = eased_t ** 1.5
                else:
                    raise ValueError(f"Invalid mode: {mode}")

                interpolation_factor = max(0.0000000001, min(0.9999999999, adjusted_t))
                
                interpolated_latent = (1 - interpolation_factor) * latent1 + interpolation_factor * latent2
                yield interpolated_latent

    latent1, original_size = image_to_latent(first_image)
    latent2, _ = image_to_latent(last_image)

    first_output_name = f"{base_name.split(os.sep)[-1]}_interpolated_0000.png"
    shutil.copy(first_image, os.path.join(output_dir, first_output_name))
    print(f"Copied original first image: {os.path.join(output_dir, first_output_name)}")

    for i, interpolated_latent in enumerate(interpolate_latents(latent1, latent2, num_steps, mode), start=1):
        image = latent_to_image(interpolated_latent, original_size)
        output_name = f"{base_name.split(os.sep)[-1]}_interpolated_{i:04d}.png"
        output_path = os.path.join(output_dir, output_name)
        image.save(output_path)
        print(f"Saved: {output_path}")

    last_output_name = f"{base_name.split(os.sep)[-1]}_interpolated_{num_steps+1:04d}.png"
    shutil.copy(last_image, os.path.join(output_dir, last_output_name))
    print(f"Copied original last image: {os.path.join(output_dir, last_output_name)}")

    print("Image interpolation completed.")

if __name__ == "__main__":
    dir_name = input("Enter the name of the input directory in 'test_images': ")
    input_dir = os.path.join("test_images", dir_name)
    image_files = sorted([ os.path.join(input_dir, f) for f in os.listdir(input_dir) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif'))])
    output_dir = os.path.join("interpolated_images", dir_name)
    
    interpolate_images(image_files[0], image_files[1], output_dir, num_steps=59, model_id="stable-diffusion-v1-5/stable-diffusion-v1-5")

分かる人が読んだらわかりますね。
Stable Diffusion のモデルの VAE だけ使って、輝度の重み付けではなく、潜在表現に重み付けして画像に変換しているわけです。
冒頭で紹介した映像はこちらでできております。
そしてウィンクしてくれるお姉さんを作り直してみました。

ぶっちゃけ輝度の重み付けで補完したこととどんだけ違いがあるん?と言われたらなかなかに微妙なんですが、ここで比較してみましょう。
左が画像処理的、右が潜在表現への重み付をした AI 的な結果です。

そしてここまできたらもー、漢としてロマンを追求するまで。
女性の方は無視して進んでください。






おっぱいをゆらしたい!!!

ふうおおおおおおおおほほほほお!!
ゆれているうううううう!!
そんなにゆらしてくれてもいいんですよ!!!!


すいません。

2 枚の微妙におっぱいの高さが違う画像を作ってくれたiムラ君ありがとう。
地味に、おっぱいが揺れる潜在空間を動かす関数は、重力の物理法則なんだからただの2次関数でいいじゃんということにたどり着くまで約 20 分。
お前のゴールは結局これなんじゃないかというそこのあなた。
そうかもしれない。

いずれこのスクリプトも、Stable Diffusion の Stable Diffusion Meganex Helper Script に入れちゃおうか、それとも拡張機能を作ろうか、ともかく何か考えてます。

ちなみに曲の Kaiber.ai で作ったコンセプトムービーはこちら。

前回同様また宣伝。
こちらの MV は、ぱっとみ何か特別なことはなにもないのですが、実はなかなかに面白い AI ( Claude と Stable Diffusion ) の使い方をして作ったので、そのうち紹介しようと思います。

以上!

この記事が気に入ったらサポートをしてみませんか?