【Stable Video Diffusion】ローカルでの使い方や料金体系、商用利用について解説

Stable-Video-Diffusion

WEELメディア事業部テックリサーチャーの中田です。

11月21日、画像から動画を生成してくれるAI「Stable Video Diffusion」がGitHubで公開され、誰でも「画像を入力とした動画生成」ができるようになりました。

これにより、任意の入力画像に沿った動画の生成が、とても簡単にできるんです…!

GitHubのスター数は、なんと11000を超えており、Stability AIが開発したAIの中でも大注目のプロジェクトです。

この記事ではStable Video Diffusionの使い方や、有効性の検証まで行います。本記事を熟読することで、SVDの凄さを実感し、Image2Videoの魅力にとりつかれるでしょう。

ぜひ、最後までご覧ください。

なお弊社では、生成AIツール開発についての無料相談を承っています。こちらからお気軽にご相談ください。
→無料相談で話を聞いてみる

目次

Stable Video Diffusionの概要

Stable Video Diffusion(SVD)は、Stability AIによって開発されたImage2Videoモデルの一種で、入力画像から動画を生成できます。

具体的には、これまでStable Diffusionにおいて、プロンプトとしてテキストを入れていた部分に、画像を入力すれば、その画像の内容に即した動画が生成されるのです。

静止画から動きを生み出すことが可能になり、さまざまなクリエイティブ作業に役立てることができるでしょう。

Stable Video Diffusionの料金体系

SVDでは、MITライセンスが採用されているので、誰でも無料で利用可能です。

なお、最先端の動画生成AIについて知りたい方はこちらの記事をご覧ください。
【Pika 1.0】頭の中のアイデアを動画にできる動画生成AI!使い方や料金、商用利用について解説

Stable Video Diffusionの商用利用は可能?

Stable Video DiffusionをリリースしたStability AIの発表によると「Stable Video Diffusionは研究用であり、現段階では商用アプリケーションでの使用を意図していない」と書かれています。

現時点では、商用利用はできないようですね。

また2023年12月にStability AIは「顧客が同社のモデルを商業利用する方法を標準化し、有償メンバーシッププランを開始する」と発表しています。有償メンバーシップは収益性とオープン性のバランスをとることを目的としており、ユーザーに商業利用する権利を付与する方法を「再定義」するものとして位置付けられています。

メンバーシップは、

  • Non-Commercial:個人・研究利用向けの無料プラン
  • Professional:機関投資家・クリエイター・開発者・新興企業向けの月額20ドルのプラン
  • Enterprise:Professinalプランよりも大きな顧客向けのカスタムプラン

の3種類が用意されています。

いずれのプランでも、SDXL(Stable Diffusion XL) TurboやStable Video Diffusion・大規模言語モデルの「Stable LM Zephyr 3B」など最新AIモデルへの早期アクセスが提供される予定ですが、商業利用が可能なのはProfessionalプランとEnterpriseプランのみとなるようです。

商用利用したい場合は、有料プランに登録する必要がありそうです。

参考記事:Stability AI メンバーシップのご紹介

Stable Video Diffusionの使い方

ここでは、無料版のGoogle Colabを用いて行います。また、ランタイムは「T4 GPU」でもOKです。また、今回は以下のツイートを参考にしています。

まずは、以下のコードを実行して、Setupを完了させましょう。

#@title Setup

!nvidia-smi

!git clone https://github.com/Stability-AI/generative-models.git

# install required packages from pypi

# !pip3 install -r generative-models/requirements/pt2.txt

# manually install only necesarry packages for colab

!wget https://gist.githubusercontent.com/mkshing/4ad40699756d996ba6b3f7934e6ca532/raw/3f0094272c7a2bd3eb5f1a0db91bed582c9e8f01/requirements.txt

!pip3 install -r requirements.txt

!pip3 install -e generative-models

!pip3 install -e git+https://github.com/Stability-AI/datapipelines.git@main#egg=sdata

!pip3 install gradio

#@title Colab hack for SVD

# !pip uninstall -y numpy

# !pip install -U numpy

!mkdir -p /content/scripts/util/detection

!ln -s /content/generative-models/scripts/util/detection/p_head_v1.npz /content/scripts/util/detection/p_head_v1.npz

!ln -s /content/generative-models/scripts/util/detection/w_head_v1.npz /content/scripts/util/detection/w_head_v1.npz

次に、以下のコードを実行して、モデルのcheckpointのダウンロードと、モデルのロードをしましょう。

# @title Download weights

import os

import subprocess

version = "svd_xt" #@param ["svd", "svd_xt"]

TYPE2PATH = {

    "svd": ["https://huggingface.co/stabilityai/stable-video-diffusion-img2vid/resolve/main/svd.safetensors", "checkpoints/svd.safetensors"],

    "svd_xt": ["https://huggingface.co/stabilityai/stable-video-diffusion-img2vid-xt/resolve/main/svd_xt.safetensors", "checkpoints/svd_xt.safetensors"]

}

download_from, download_to = TYPE2PATH[version]

# @markdown This will take several minutes. <br>

# @markdown **Reference:**

# @markdown * `svd`: [stabilityai/stable-video-diffusion-img2vid](https://huggingface.co/stabilityai/stable-video-diffusion-img2vid) for 14 frames generation

# @markdown * `svd_xt`: [stabilityai/stable-video-diffusion-img2vid-xt](https://huggingface.co/stabilityai/stable-video-diffusion-img2vid-xt) for 25 frames generation

os.makedirs("checkpoints", exist_ok=True)

if os.path.exists(download_to):

  print("Already downloaded")

else:

  print(f"download from {download_from} to {download_to}")

  subprocess.call(["wget", download_from, "-O", download_to])

#@title Load Model

import sys

from omegaconf import OmegaConf

import torch

sys.path.append("generative-models")

from sgm.util import default, instantiate_from_config

from scripts.util.detection.nsfw_and_watermark_dectection import DeepFloydDataFiltering

def load_model(

    config: str,

    device: str,

    num_frames: int,

    num_steps: int,

):

    config = OmegaConf.load(config)

    config.model.params.conditioner_config.params.emb_models[

        0

    ].params.open_clip_embedding_config.params.init_device = device

    config.model.params.sampler_config.params.num_steps = num_steps

    config.model.params.sampler_config.params.guider_config.params.num_frames = (

        num_frames

    )

    with torch.device(device):

        model = instantiate_from_config(config.model).to(device).eval().requires_grad_(False)

    filter = DeepFloydDataFiltering(verbose=False, device=device)

    return model, filter

if version == "svd":

    num_frames = 14

    num_steps = 25

    # output_folder = default(output_folder, "outputs/simple_video_sample/svd/")

    model_config = "generative-models/scripts/sampling/configs/svd.yaml"

elif version == "svd_xt":

    num_frames = 25

    num_steps = 30

    # output_folder = default(output_folder, "outputs/simple_video_sample/svd_xt/")

    model_config = "generative-models/scripts/sampling/configs/svd_xt.yaml"

else:

    raise ValueError(f"Version {version} does not exist.")

device = "cuda" if torch.cuda.is_available() else "cpu"

model, filter = load_model(

    model_config,

    device,

    num_frames,

    num_steps,

)

# move models expect unet to cpu

model.conditioner.cpu()

model.first_stage_model.cpu()

# change the dtype of unet

model.model.to(dtype=torch.float16)

torch.cuda.empty_cache()

model = model.requires_grad_(False)

ちなみに、ここで「version」変数の値を「svd_xt」としてます。これは、SVD_XTモデルを用いることを明示しているので、ここを「svd」とすれば、文字通りSVDモデルを利用できます。

次に、以下のコードを実行して、動画生成を行うための関数を定義します。

# @title Sampling function

import math

import os

from glob import glob

from pathlib import Path

from typing import Optional

import cv2

import numpy as np

import torch

from einops import rearrange, repeat

from fire import Fire

from PIL import Image

from torchvision.transforms import ToTensor

from torchvision.transforms import functional as TF

from sgm.inference.helpers import embed_watermark

from sgm.util import default, instantiate_from_config

def get_unique_embedder_keys_from_conditioner(conditioner):

    return list(set([x.input_key for x in conditioner.embedders]))

def get_batch(keys, value_dict, N, T, device, dtype=None):

    batch = {}

    batch_uc = {}

    for key in keys:

        if key == "fps_id":

            batch[key] = (

                torch.tensor([value_dict["fps_id"]])

                .to(device, dtype=dtype)

                .repeat(int(math.prod(N)))

            )

        elif key == "motion_bucket_id":

            batch[key] = (

                torch.tensor([value_dict["motion_bucket_id"]])

                .to(device, dtype=dtype)

                .repeat(int(math.prod(N)))

            )

        elif key == "cond_aug":

            batch[key] = repeat(

                torch.tensor([value_dict["cond_aug"]]).to(device, dtype=dtype),

                "1 -> b",

                b=math.prod(N),

            )

        elif key == "cond_frames":

            batch[key] = repeat(value_dict["cond_frames"], "1 ... -> b ...", b=N[0])

        elif key == "cond_frames_without_noise":

            batch[key] = repeat(

                value_dict["cond_frames_without_noise"], "1 ... -> b ...", b=N[0]

            )

        else:

            batch[key] = value_dict[key]

    if T is not None:

        batch["num_video_frames"] = T

    for key in batch.keys():

        if key not in batch_uc and isinstance(batch[key], torch.Tensor):

            batch_uc[key] = torch.clone(batch[key])

    return batch, batch_uc

def sample(

    input_path: str = "assets/test_image.png",  # Can either be image file or folder with image files

    resize_image: bool = False,

    num_frames: Optional[int] = None,

    num_steps: Optional[int] = None,

    fps_id: int = 6,

    motion_bucket_id: int = 127,

    cond_aug: float = 0.02,

    seed: int = 23,

    decoding_t: int = 14,  # Number of frames decoded at a time! This eats most VRAM. Reduce if necessary.

    device: str = "cuda",

    output_folder: Optional[str] = "/content/outputs",

):

    """

    Simple script to generate a single sample conditioned on an image `input_path` or multiple images, one for each

    image file in folder `input_path`. If you run out of VRAM, try decreasing `decoding_t`.

    """

    torch.manual_seed(seed)

    path = Path(input_path)

    all_img_paths = []

    if path.is_file():

        if any([input_path.endswith(x) for x in ["jpg", "jpeg", "png"]]):

            all_img_paths = [input_path]

        else:

            raise ValueError("Path is not valid image file.")

    elif path.is_dir():

        all_img_paths = sorted(

            [

                f

                for f in path.iterdir()

                if f.is_file() and f.suffix.lower() in [".jpg", ".jpeg", ".png"]

            ]

        )

        if len(all_img_paths) == 0:

            raise ValueError("Folder does not contain any images.")

    else:

        raise ValueError

    all_out_paths = []

    for input_img_path in all_img_paths:

        with Image.open(input_img_path) as image:

            if image.mode == "RGBA":

                image = image.convert("RGB")

            if resize_image and image.size != (1024, 576):

                print(f"Resizing {image.size} to (1024, 576)")

                image = TF.resize(TF.resize(image, 1024), (576, 1024))

            w, h = image.size

            if h % 64 != 0 or w % 64 != 0:

                width, height = map(lambda x: x - x % 64, (w, h))

                image = image.resize((width, height))

                print(

                    f"WARNING: Your image is of size {h}x{w} which is not divisible by 64. We are resizing to {height}x{width}!"

                )

            image = ToTensor()(image)

            image = image * 2.0 - 1.0

        image = image.unsqueeze(0).to(device)

        H, W = image.shape[2:]

        assert image.shape[1] == 3

        F = 8

        C = 4

        shape = (num_frames, C, H // F, W // F)

        if (H, W) != (576, 1024):

            print(

                "WARNING: The conditioning frame you provided is not 576x1024. This leads to suboptimal performance as model was only trained on 576x1024. Consider increasing `cond_aug`."

            )

        if motion_bucket_id > 255:

            print(

                "WARNING: High motion bucket! This may lead to suboptimal performance."

            )

        if fps_id < 5:

            print("WARNING: Small fps value! This may lead to suboptimal performance.")

        if fps_id > 30:

            print("WARNING: Large fps value! This may lead to suboptimal performance.")

        value_dict = {}

        value_dict["motion_bucket_id"] = motion_bucket_id

        value_dict["fps_id"] = fps_id

        value_dict["cond_aug"] = cond_aug

        value_dict["cond_frames_without_noise"] = image

        value_dict["cond_frames"] = image + cond_aug * torch.randn_like(image)

        value_dict["cond_aug"] = cond_aug

        # low vram mode

        model.conditioner.cpu()

        model.first_stage_model.cpu()

        torch.cuda.empty_cache()

        model.sampler.verbose = True

        with torch.no_grad():

            with torch.autocast(device):

                model.conditioner.to(device)

                batch, batch_uc = get_batch(

                    get_unique_embedder_keys_from_conditioner(model.conditioner),

                    value_dict,

                    [1, num_frames],

                    T=num_frames,

                    device=device,

                )

                c, uc = model.conditioner.get_unconditional_conditioning(

                    batch,

                    batch_uc=batch_uc,

                    force_uc_zero_embeddings=[

                        "cond_frames",

                        "cond_frames_without_noise",

                    ],

                )

                model.conditioner.cpu()

                torch.cuda.empty_cache()

                # from here, dtype is fp16

                for k in ["crossattn", "concat"]:

                    uc[k] = repeat(uc[k], "b ... -> b t ...", t=num_frames)

                    uc[k] = rearrange(uc[k], "b t ... -> (b t) ...", t=num_frames)

                    c[k] = repeat(c[k], "b ... -> b t ...", t=num_frames)

                    c[k] = rearrange(c[k], "b t ... -> (b t) ...", t=num_frames)

                for k in uc.keys():

                    uc[k] = uc[k].to(dtype=torch.float16)

                    c[k] = c[k].to(dtype=torch.float16)

                randn = torch.randn(shape, device=device, dtype=torch.float16)

                additional_model_inputs = {}

                additional_model_inputs["image_only_indicator"] = torch.zeros(

                    2, num_frames

                ).to(device, )

                additional_model_inputs["num_video_frames"] = batch["num_video_frames"]

                for k in additional_model_inputs:

                    if isinstance(additional_model_inputs[k], torch.Tensor):

                        additional_model_inputs[k] = additional_model_inputs[k].to(dtype=torch.float16)

                def denoiser(input, sigma, c):

                    return model.denoiser(

                        model.model, input, sigma, c, **additional_model_inputs

                    )

                samples_z = model.sampler(denoiser, randn, cond=c, uc=uc)

                samples_z.to(dtype=model.first_stage_model.dtype)

                ##

                model.en_and_decode_n_samples_a_time = decoding_t

                model.first_stage_model.to(device)

                samples_x = model.decode_first_stage(samples_z)

                samples = torch.clamp((samples_x + 1.0) / 2.0, min=0.0, max=1.0)

                model.first_stage_model.cpu()

                torch.cuda.empty_cache()

                os.makedirs(output_folder, exist_ok=True)

                base_count = len(glob(os.path.join(output_folder, "*.mp4")))

                video_path = os.path.join(output_folder, f"{base_count:06d}.mp4")

                writer = cv2.VideoWriter(

                    video_path,

                    cv2.VideoWriter_fourcc(*"MP4V"),

                    fps_id + 1,

                    (samples.shape[-1], samples.shape[-2]),

                )

                samples = embed_watermark(samples)

                samples = filter(samples)

                vid = (

                    (rearrange(samples, "t c h w -> t h w c") * 255)

                    .cpu()

                    .numpy()

                    .astype(np.uint8)

                )

                for frame in vid:

                    frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)

                    writer.write(frame)

                writer.release()

                all_out_paths.append(video_path)

    return all_out_paths

最後に、以下のコードを実行してください。

# @title Do the Run!

# @markdown Generation takes about 10 mins for `svd_xt` on T4 (Colab free plan). Please be patient...

import gradio as gr

import random

def infer(input_path: str, resize_image: bool, n_frames: int, n_steps: int, seed: str, decoding_t: int) -> str:

  if seed == "random":

    seed = random.randint(0, 2**32)

  seed = int(seed)

  output_paths = sample(

    input_path=input_path,

    resize_image=resize_image,

    num_frames=n_frames,

    num_steps=n_steps,

    fps_id=6,

    motion_bucket_id=127,

    cond_aug=0.02,

    seed=23,

    decoding_t=decoding_t,  # Number of frames decoded at a time! This eats most VRAM. Reduce if necessary.

    device=device,

  )

  return output_paths[0]

with gr.Blocks() as demo:

  with gr.Column():

    image = gr.Image(label="input image", type="filepath")

    resize_image = gr.Checkbox(label="resize to optimal size", value=True)

    btn = gr.Button("Run")

    with gr.Accordion(label="Advanced options", open=False):

      n_frames = gr.Number(precision=0, label="number of frames", value=num_frames)

      n_steps = gr.Number(precision=0, label="number of steps", value=num_steps)

      seed = gr.Text(value="random", label="seed (integer or 'random')",)

      decoding_t = gr.Number(precision=0, label="number of frames decoded at a time", value=2)

  with gr.Column():

    video_out = gr.Video(label="generated video")

  examples = [

      ["https://user-images.githubusercontent.com/33302880/284758167-367a25d8-8d7b-42d3-8391-6d82813c7b0f.png"]

  ]

  inputs = [image, resize_image, n_frames, n_steps, seed, decoding_t]

  outputs = [video_out]

  btn.click(infer, inputs=inputs, outputs=outputs)

  gr.Examples(examples=examples, inputs=inputs, outputs=outputs, fn=infer)

  demo.queue().launch(debug=True, share=True, show_error=True)

上記を実行すると、以下のような画面が出てくると思います。

「ここに画像をドロップ」のところに任意の画像をアップロードして、「Run」をクリックすると、動画が生成されます。

Stable Video Diffusionを実際に使ってみた

ここでは、冒頭の導入文にある、以下の画像を動画化してみましょう。

アップロードして、SVDで動かしてみた結果が、以下の通りです。

なんか、角度が変化しているだけに感じます。

お次はSVD-XT。

うーん、クオリティは悪くないんだけど、なんかカクカクしています。

なお、ByteDanceが開発した動画生成AIについて知りたい方はこちらの記事をご覧ください。
【MagicAnimate】AnimateAnyoneよりこっちを使え!画像を踊らせる神AIの使い方〜実践まで

Stable Video Diffusionの推しポイントである高品質な動画生成は本当なのか?

Stable Video Diffusionの威力を確かめるために、Runwayの「Gen-2」と比較してみます。

そこで、先ほどのStable Video Diffusionへのタスクを、Gen-2にも行わせて、品質を比較してみようと思います。Gen-2は、以下のページから利用できます。

参考記事:Gen-2: The Next Step Forward for Generative AI

Gen-2によって生成された動画は、以下の通りです。

細部までかなり自然ですね!ただ、人の手が歪んでいたりして、不自然な部分も見受けられました。

とはいえ、SVDよりもGen-2の方が、まだまだ精度がよさそうです。

まとめ

Stable Video Diffusion(SVD)は、Stability AIによって開発されたImage2Videoモデルの一種で、入力画像から動画を生成できます。

誰でも無料で利用可能できるAIツールであり、画像を入力すれば、その画像の内容に即した動画が生成されるのです。使い方は簡単で、本記事のコードをそのまま実行すれば、実行できます。

実際に試したところ、角度が動いているだけだったり、カクカクしていたりしました。加えて、比較検証したところ、やはりSVDよりもGen-2の方が、まだまだ精度がよさそうです。

数年後には、『アーサー・C・クラークの小説』のように、誰でもアーティストになれる時代が来るかもしれないですね。

サービス紹介資料

【無料】2023年2月版生成系AIの業務活用なら!

・生成系AIを活用したPoC開発

・生成系AIの業務活用コンサルティング

・システム間API連携

サービス紹介資料

生成系AIの業務活用なら!

・生成系AIを活用したPoC開発

・生成系AIのコンサルティング

・システム間API連携

最後に

いかがだったでしょうか?

弊社では

・マーケティングやエンジニアリングなどの専門知識を学習させたAI社員の開発
・要件定義・業務フロー作成を80%自動化できる自律型AIエージェントの開発
・生成AIとRPAを組み合わせた業務自動化ツールの開発
・社内人事業務を99%自動化できるAIツールの開発
ハルシネーション対策AIツールの開発
自社専用のAIチャットボットの開発

などの開発実績がございます。

まずは、「無料相談」にてご相談を承っておりますので、ご興味がある方はぜひご連絡ください。

➡︎生成AIを使った業務効率化、生成AIツールの開発について相談をしてみる。

生成AIを社内で活用していきたい方へ

「生成AIを社内で活用したい」「生成AIの事業をやっていきたい」という方に向けて、生成AI社内セミナー・勉強会をさせていただいております。

セミナー内容や料金については、ご相談ください。

また、弊社紹介資料もご用意しておりますので、併せてご確認ください。

投稿者

  • 中田

    データサイエンス専攻の大学院生。大学では、生成系AIの拡散モデルを用いた音楽生成について研究。 趣味は作曲、サッカー、コーヒー。

  • URLをコピーしました!
  • URLをコピーしました!
目次