167
143

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[訓練・データ不要]ディープラーニングフレームワークを使ったアニメの線画の自動生成

Last updated at Posted at 2019-07-01

ディープラーニングのフレームワークを使って、アニメ本編から線画を生成します。ただし、一切訓練や訓練データを与えていません。アニメ1話分全4.3万フレームの線画化を1時間程度で終わらせることができました。

あらまし

このようにアニメ本編から自動的に線画が生成できます。ディープラーニングのフレームワークを使うとできます。ただし、一切訓練をしていません。

元ネタ

そこそこな線画を目指す OpenCV
https://qiita.com/khsk/items/6cf4bae0166e4b12b942

こちらの記事のアルゴリズムを使います。もともとはOpenCVの実装ですが、ディープラーニングの関数に変換することができます。

OpenCV→ディープラーニングへの置き換え

OpenCVを使った線画化のアルゴリズムは以下の処理の構成です。

  1. カラー画像のグレースケール化
  2. 1の画像を8近傍で膨張処理を1回
  3. 膨張処理した画像と1の画像の差分の絶対値を取る
  4. 3の画像の白黒反転させる

膨張処理をどうにかすれば、あとは四則演算や行列積でできそうな気がしますよね。OpenCVのモルフォロジー変換のドキュメントを見ると次の式が書かれていました。

$$dst(x,y)=\max_{(x',y'):\rm{element} (x',y')\neq0}src(x+x',y+y') $$

この式をよく見ると、MaxPoolingっぽくないですか? ただし、普通のMaxPoolingは解像度が下がってしまうので、入力と出力の解像度が変わらないように「pool_size=(3,3), stride=1, padding="same"」という特殊なプーリングを行います(GoogLe NetのInceptionブロックでこういう使い方しています)。

つまり、ディープラーニングの処理でOpenCVの線画化の処理を置き換えると次のようになります。

  1. カラー画像のグレースケール化 → 行列積 or 1x1畳み込み
  2. 1の画像を8近傍で膨張処理を1回 → stride=1, padding="same"の3x3 MaxPooling
  3. 膨張処理した画像と1の画像の差分の絶対値を取る → ただの絶対値+引き算
  4. 3の画像の白黒反転させる → 1.0-画像

OpenCVの処理を無事にディープラーニングの文脈に置き換えることができました。あとは実装するだけです。

何が美味しいの?

ディープラーニングフレームワークに置き換えてなにが美味しいの?という疑問があるかもしれません。理由はいくつかあります。

  • GPUブーストが簡単に使える
    OpenCVの処理をGPU上で簡単に行うことができます。KerasならCPUと同じコードでできます。
  • ディープラーニング上で「線画」の概念が扱える
    もともと自分が欲しかったのはこっちで、損失関数内で線画の損失関数を定義したかったのです。この方法なら、カラー画像→線画への変換をディープラーニングのモデル内で行えます。

ディープラーニングフレームワークによる実装

元画像

元の記事と同じように「響け!ユーフォニアム」の画像を使います。これを「eupho.jpg」とします。

eupho.jpg

Keras

Kerasの場合は以下のような処理になります。ディープラーニングのフレームワークでは、画像は4階テンソルとして扱うため前後にテンソル変換を挟んでいます(load_tensor, show_tensor)。

import numpy as np
from PIL import Image
import keras.backend as K
import matplotlib.pyplot as plt

def load_tensor():
    with Image.open("eupho.jpg") as img:
        array = np.asarray(img, np.float32) / 255.0 # [0, 1]
        array = np.expand_dims(array, axis=0) # KerasはNHWC
        return K.variable(array)

def show_tensor(input_image_tensor):
    img = K.eval(input_image_tensor * 255.0)
    img = img.astype(np.uint8)[0,:,:,0]    
    plt.imshow(img, cmap="gray")
    plt.show()

def linedraw():
    # データの読み込み
    x = load_tensor()
    # Y = 0.299R + 0.587G + 0.114B でグレースケール化
    gray_kernel = K.variable(
        np.array([0.299, 0.587, 0.114], np.float32).reshape(3, 1))
    x = K.dot(x, gray_kernel) # dot積でOK
    # 3x3カーネルで膨張1回(膨張はMaxPoolと同じ)
    dilated = K.pool2d(x, pool_size=(3, 3), strides=(1, 1), padding="same")
    # 膨張の前後でL1の差分を取る
    diff = K.abs(x-dilated)  
    # ネガポジ反転
    x = 1.0 - diff
    # 結果表示
    show_tensor(x)

if __name__ == "__main__":
    linedraw()

keras.png

うまくいきました。

PyTorch

PyTorchの場合は以下のようになります。グレースケール化で1x1畳み込みを使っているのが違うところです。

import numpy as np
from PIL import Image
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt

def load_tensor():
    with Image.open("eupho.jpg") as img:
        array = np.asarray(img, np.float32) / 255.0 # [0, 1]
        array = np.expand_dims(array, axis=0)
        array = np.transpose(array, [0, 3, 1, 2]) # PyTorchはNCHW
        return torch.as_tensor(array)

def show_tensor(input_image_tensor):
    img = input_image_tensor.numpy() * 255.0
    img = img.astype(np.uint8)[0,0,:,:]    
    plt.imshow(img, cmap="gray")
    plt.show()

def linedraw():
    # データの読み込み
    x = load_tensor()
    # Y = 0.299R + 0.587G + 0.114B でグレースケール化
    gray_kernel = torch.as_tensor(
        np.array([0.299, 0.587, 0.114], np.float32).reshape(1, 3, 1, 1))
    x = F.conv2d(x, gray_kernel) # 行列積は畳み込み関数でOK
    # 3x3カーネルで膨張1回(膨張はMaxPoolと同じ)
    dilated = F.max_pool2d(x, kernel_size=3, stride=1, padding=1)
    # 膨張の前後でL1の差分を取る
    diff = torch.abs(x-dilated)    
    # ネガポジ反転
    x = 1.0 - diff
    # 結果表示
    show_tensor(x)

if __name__ == "__main__":
    linedraw()

pytorch.png

ディープラーニングなのに訓練/データ不要

このアルゴリズムの面白いところなのですが、ディープラーニングなのに学習する係数が1個もありません。したがって、モデルを定義をした段階で訓練フェーズをすっ飛ばしてすぐ使うことができます。

アニメ1話分を全て線画に変換してみよう

応用例として、YouTubeにあるアニメ1話分を丸々線画に変換して、Before-Afterで動画として保存します。以下の流れで行います。

  • YouTubeからPyTubeで動画をダウンロード
  • フレーム単位で静止画(カラー画像)で切り出し
  • Kerasでカラー画像を全て線画に変換
  • もとのカラー画像と線画を組み合わせて動画として再エンコード

今回はKerasを使っていますが、PyTorchや他のフレームワークでもできます。暇な方はやってみてはいかがでしょうか。

ちなみにColabでやる場合はYouTubeの再生地域制限に引っかかるケースがあります。日本から動画が見れてもColabのサーバーがアメリカにあるので、アメリカからの接続と認識されてDLできないケースがあります(その場合は日本のHTTPSプロキシ経由で接続しますが説明は省略します)。ローカルにDLする場合は特に心配しなくてOKです。

必要ライブラリ

ffmpeg

ffmpegはPythonのライブラリの他に、本体のバイナリが必要なので本家から必要なバイナリをダウンロードしておきます。Windowsならffmpegのexeファイルを実行ディレクトリにおきます。

OpenH264

H264でエンコードする際に必要。以下のリポジトリから、H264の1.8.0かつ64ビットのライブラリをOSに応じてダウンロード。同じく実行ディレクトリにおいておきます。

その他ライブラリ

pip install ffmpeg-python
pip install pytube

他にインストールされていないライブラリがあったら適宜pip installでインストールします。

コード

動画はYouTubeの公式配信の侵略!イカ娘の第1話を使っています。

import ffmpeg
from pytube import YouTube
import os
import glob
import cv2
import numpy as np
from tqdm import tqdm

import keras
from keras import layers
import keras.backend as K
from PIL import Image

def download_youtube():
    if not os.path.exists("video"):
        os.mkdir("video")
    yt = YouTube("https://www.youtube.com/watch?v=JUIk6a1nzFw")
    query = yt.streams.filter(fps=30, subtype="mp4", res="360p").all()
    query[0].download("video")

def extract_images():
    if not os.path.exists("images"):
        os.mkdir("images")
    stream = ffmpeg.input(glob.glob("video/*")[0])
    stream = ffmpeg.output(stream, f"images/frame_%05d.png", f="image2", q=4)
    ffmpeg.run(stream) 

def linedraw_func(input_tensor):
    gray_kernel = K.variable(
        np.array([0.299, 0.587, 0.114], np.float32).reshape(3, 1))
    x = K.dot(input_tensor, gray_kernel) # dot積でOK
    # 1枚単位でmin-maxスケーリングしてコントラスト補正する
    mins = K.min(x, axis=(1,2,3), keepdims=True)
    maxs = K.max(x, axis=(1,2,3), keepdims=True)
    x = (x-mins) / (maxs-mins+K.epsilon())
    dilated = K.pool2d(x, pool_size=(3, 3), strides=(1, 1), padding="same")
    diff = K.abs(x-dilated)  
    x = 1.0 - diff
    return x

IMG_WIDTH, IMG_HEIGHT = 640, 360

def linedraw_model():
    input = layers.Input((IMG_HEIGHT, IMG_WIDTH, 3))
    x = layers.Lambda(linedraw_func)(input)
    return keras.models.Model(input, x)

def convert_all():
    # KerasのImageDataGeneratorはroot/classes/img という構造なので、
    # ルートディレクトリをカレントディレクトリにし、サブディレクトリとして画像のあるディレクトリを1クラスとして指定
    # batch_size分の画像を一度に読み込む、シャッフルはしないようにする(連番で読まれる)
    gen = keras.preprocessing.image.ImageDataGenerator(rescale=1.0/255).flow_from_directory(
        "./", class_mode=None, classes=["images"], batch_size=256, shuffle=False, target_size=(IMG_HEIGHT, IMG_WIDTH)
    )

    # 出力ディレクトリ
    if not os.path.exists("images_line"):
        os.mkdir("images_line")

    # ファイル数
    n_files = len(glob.glob("images/*.png"))

    # 線画化の操作をモデルとして読み込む
    model = linedraw_model()
    cnt = 1

    for X in tqdm(gen):
        out = model.predict_on_batch(X) # 線画化
        # ファイルの保存
        for i in range(out.shape[0]):
            img = (out[i,:,:,0]*255.0).astype(np.uint8)
            with Image.fromarray(img) as img:
                img.save(f"images_line/frame_{cnt:05}.png")
            cnt += 1
            if cnt > n_files: return  

def encode():
    fourcc = cv2.VideoWriter_fourcc("h", "2", "6", "4")
    video = cv2.VideoWriter("output.mp4", fourcc, 29.97, (IMG_WIDTH, IMG_HEIGHT*2))
    n = len(glob.glob("images/*"))
    for i in tqdm(range(1, n + 1)):
        img_original = cv2.imread(f"images/frame_{i:05}.png")
        img_original = cv2.resize(img_original, (IMG_WIDTH, IMG_HEIGHT), cv2.INTER_LANCZOS4)
        img_convert = cv2.imread(f"images_line/frame_{i:05}.png")
        #img_convert = cv2.resize(img_convert, (IMG_WIDTH, IMG_HEIGHT), cv2.INTER_LANCZOS4)
        x = np.concatenate([img_original, img_convert], axis=0)
        video.write(x)

if __name__ == "__main__":
    encode()

これで生成したのが冒頭の動画。ただし、Twitterの埋め込み動画の画質が悪いので実際はもう少し綺麗に出てたりします1

生成結果比較

実際の静止画としての出力はこちらになります。カラー画像との比較です。

frame_13217.png
frame_13217.png
frame_04940.png
frame_04940.png

なかなか良いのではないでしょうか?

速度

ダウンロードからエンコードまでおおよそ1時間程度でした。ffmpegによるフレームの切り出しとストレージがボトルネックになるので、CPU性能とストレージ速度(HDDよりSSD)を上げると高速に出力できます。GPUはほとんど使っていませんでした(使用率でせいぜい5%程度)。もちろんこのあとDeepの訓練につなげる場合は、GPU等のデバイスがあればより高速化できます。

しかし、アニメ1話分の線画が1時間でできてしまうというのはびっくりでした。1話で4.3万フレームあったので(29.97fps × 累計秒)、アニメ本編と既存の画像処理を組み合わせればかなり簡単かつ大量にデータが作れるのかもしれません。

応用 / まとめ

これを使うと、(線画, カラー画)のペアデータが簡単に作れるので2、生成モデルとの相性が良くなります。具体的には、pix2pixなどを使えばアニメの原画~動画の工程をかなり自動化できるのではないでしょうか。

この記事のポイントは、OpenCVで書けるような従来の画像処理も、畳み込みやプーリングといったディープラーニングの操作にある程度置き換えることができるということです。これに気づくとディープラーニングの幅がぐっと広がると思われます。

  1. HTMLのvideoタグ対応していればプレイヤー付きで直にMP4再生できますが、QiitaはそもそもMP4のアップロード非対応です。また、YouTubeの埋め込みにも対応していません。したがって、低画質ながらもTwitterの埋め込みか、256色のGIFアニメぐらいしか方法がありません。あのさぁ……

  2. Qiita内で探してもpix2pixで似たようなことをやっている例はいくつかあります

167
143
3

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
167
143

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?