Skip to main content

アニメのキャプチャ画像から線画を作る

はじめに

OpenCVを用いてアニメのキャプチャ画像から線画を生成してみました。鉛筆で書いたような味のある線画が作れました。

線画生成は最近では深層学習の手法を使うものもありますが、この記事では古典的な画像処理の方法で作ってみました。アニメに限らずいわゆるアニメ風のイラストであれば記事の方法で同様に適用できると思います。

線画抽出のロジック

イラストの線画抽出については、グレースケールで読み込み -> 1回収縮 -> 収縮前との差を取る -> 白黒反転という方法でそれなりに綺麗なものが作れることが知られています(例えば:そこそこな線画を目指す OpenCV - Qiita)。収縮の際のカーネルは4近傍(2x2の行列)か8近傍(3x3の行列)を用います。シンプルですが結構きれいに線画が抽出できる優れた方法です。

この方法を出発点に、より綺麗に線画が抽出できる方法を探ってみました。

結論としては、グレースケールで読み込み -> 適応的ヒストグラム平坦化 -> 1回収縮 -> 収縮前との差を取る -> 白黒反転 -> Non-local Means Denoising -> ガンマ変換で綺麗な線画が作れました。パラメータなどは決め打ちしたのでもっといい方法はあると思います。

環境

  • Windows 10
  • python 3.10.0
  • opencv-python 4.5.5.64

実装

きんいろモザイクの第1期12話のこちらの画像から線画を作ってみます。

import cv2
import numpy as np

画像を読み込みます。

image_original = cv2.imread("image_original/kinmosa.jpg")

こちらがベースラインとなる「グレースケールで読み込み -> 1回収縮 -> 収縮前との差を取る -> 白黒反転」のロジックです。

image = cv2.cvtColor(image_original, cv2.COLOR_BGR2GRAY)
image_dilate = cv2.dilate(image, np.ones((3, 3), np.uint8), iterations=1)
image = cv2.absdiff(image, image_dilate)
image = cv2.bitwise_not(image)

この段階で十分きれいで驚きました。ただ、2つ改善したい点があります。

  • 一番右のあややの左手と胴の間の線や、セーターとスカートの間の線、セーターのしわがうまく抽出できていません。
    • 元の画像と見比べると分かりますが、これらはセーターの濃紺の中にある黒い線ですから、単純な収縮では線を取り出しづらいのかと思います。
  • 全体的に線が薄いため、線にメリハリを付けたいです。

1点目はコントラストを平坦化するのがよさそうです。特に、画像の小さな領域ごとにヒストグラムを平坦化する適応的ヒストグラム平坦化(cv2.createCLAHE)が効きそうな感じがします。というわけで、グレースケール化した後に適応的ヒストグラム平坦化をかけてみます。

また、2点目については、ガンマ変換をかけることにします。

ガンマ変換前の画像のnp.ndarrayをxとするとき、単にx/255**gamma*255でガンマ変換できますが、xの全ての画素についてこの計算をするのは計算負荷が大きいです。0から255までの整数値をaとしたときに、ガンマ変換後の値ya/255**gamma*255で与えられますから、ガンマ変換を実装する上では、aとyのマッピングテーブルを用意しておき、このテーブルのaをxで読み替えるのが賢い方法です。

なお、ガンマ変換をかけると線の周辺や真っ白の領域に黒いモスキートノイズが浮かび上がってしまうため、ガンマ変換の前に何らかのフィルターをかけてノイズを軽減する必要があります。このタイプのノイズを取るには、Bilateral Filter(cv2.bilateralFilter)、Adaptive Bilateral Filter(cv2>=3.0.0で削除された)、Non-local Means Denoising(cv2.fastNlMeansDenoising)などがあると思います。ここではNon-local Means Denoisingを試してみました。

以上を実装してみます。なお、画像処理の各種パラメータは私が色々試してみて良さそうだと感じたものを適当に採用しています。

def contrast_equalization(img: np.ndarray, clip_limit: float, tile_grid_size: int) -> np.ndarray:
    clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=(tile_grid_size, tile_grid_size))
    res = clahe.apply(img)
    return res

def gamma_transformation(img: np.ndarray, gamma: float) -> np.ndarray:
    x = np.arange(0, 256)
    look_up_table = (x / 255) ** gamma * 255
    look_up_table = look_up_table.astype(np.uint8)
    return cv2.LUT(img, look_up_table)

image = cv2.cvtColor(image_original, cv2.COLOR_BGR2GRAY)
# 追加
image = contrast_equalization(image, 2.0, 8)
image_dilate = cv2.dilate(image, np.ones((3, 3), np.uint8), iterations=1)
image = cv2.absdiff(image, image_dilate)
image = cv2.bitwise_not(image)
# 追加
image = cv2.fastNlMeansDenoising(image, h=3, templateWindowSize=7, searchWindowSize=21)
# 追加
image = gamma_transformation(image, 1.5)

以上に挙げた点がそれなりに解消していますね。ノイズは若干残ってしまっています。モスキートノイズに効果があるらしいAdaptive Bilateral Filterを使ってみたいので元の論文(Zhang and Allebach, 2008)を読んで実装したい…。

ガンマ変換はパラメータによってはノイズが出過ぎるので、やらなくてもいいかもしれません。

他のキャプチャ画像でも色々線画を作ってみましたが、少なくとも私が試してみた範囲では、コード中のパラメータはそのままで問題なさそうです。

スローループの海凪小春ちゃん

kawaii!コントラストがはっきりしている画像だと線画が綺麗に取り出せますね。

線画から動画を作る

このロジックとffmpegを用いて、アニメの動画を線画化することができます。

流れは以下の通りです。1〜5をシェルスクリプトで書いて3のpythonコードをシェルスクリプト内で読み込むようにしました。

  1. (ffmpeg)元の動画から静止画を取り出す
  2. (ffmpeg)元の動画から音声を取り出す
  3. (上のpythonコード)1で取り出した静止画を線画に変換する
  4. (ffmpeg)3で作った線画を動画にする
  5. (ffmpeg)4で作った動画に2の音声を合わせて音声ありの動画にする

きんいろモザイク1期のOPの冒頭です。ただし5の音声は付けていません。

おわりに

いい感じに線画が作れました。

画像処理は素人なので多少勉強をしたのですが、こちらの本が参考になりました。画像処理のアルゴリズムを一通り解説しているものです。何も分からずにOpenCVの関数を使うのではなく、その背後にあるアルゴリズムを多少なりとも把握できるとより面白く感じられました。

ディジタル画像処理 改訂第二版

参考