画像から線画をDeepLearningで抽出してみた

  • 22
    いいね
  • 0
    コメント

画像から線画をDeepLearningで抽出してみた

気がつけばGWもあっという間に過ぎ去りました。今年のGWは東京(一部埼玉含む)を家族一同でやたら歩きまわりました。スカイツリーにも登りましたが、あんなに人がいるとは思ってませんでしたね。次行くことがあれば夜に行くのも面白そうでした。なんか上映とかしてるみたいですし。

6/11アップデート

ネットワーク構造、データセット内の色々を修正して、抽出できる線画を改善したので、その内容を含めてアップデートしました。

線画が欲しい

以前記事にしましたが、線画着色をDeepLearningでやってみていますが、その過程で、いくつかデータセットに課題がありそうな感じがしました。線画自体は、 http://qiita.com/khsk/items/6cf4bae0166e4b12b942 を参考にして作成したのですが、どうもいくつか問題があるようです。

  • 縮小した画像を二値化するとギザギザすぎる
    • これ自体は、二値化してから縮小することで解消できました(BiCubicを使ったりするとやっぱりシャギーが出ます)
  • 二値化しないまま利用する場合、利用されている色によっては、本来濃くなって欲しい所が薄くなるケースが多々見られる
    • 色の差の絶対値を利用している都合上、線と塗っている色間で、似た色がりようされていると、どうしても薄くなります
  • 色の塗り方によっては、余計なディティールが出る
    • 特にアニメ塗りにおける影に顕著です

とまぁ、いくつか問題がありますが、手軽(OpenCVが動けばいい)かつ高速(OpenCVが略)に作成できるので、いまでもこれで進めています。

ラフを線画に変える研究

既存の研究として、こんなものがあります。
http://hi.cs.waseda.ac.jp:8081/

こっちは論文です。
http://hi.cs.waseda.ac.jp/~esimo/publications/SimoSerraSIGGRAPH2016.pdf

これは、ラフを線画に変換するAutoEncoderについての手法です。今回は、これを参考にして作ってみました。

class AutoEncoder(object):
    """Define autoencoder"""

    def __init__(self):
        self.conv1 = Encoder(3, 48, 5, 5, strides=[1, 2, 2, 1], name='encoder1')
        self.conv1_f1 = Encoder(48, 128, 3, 3, name='encoder1_flat1')
        self.conv1_f2 = Encoder(128, 128, 3, 3, name='encoder1_flat2')
        self.conv2 = Encoder(128, 256, 5, 5, strides=[1, 2, 2, 1], name='encoder2')
        self.conv2_f1 = Encoder(256, 256, 3, 3, name='encoder2_flat1')
        self.conv2_f2 = Encoder(256, 256, 3, 3, name='encoder2_flat2')
        self.conv3 = Encoder(256, 256, 5, 5, strides=[1, 2, 2, 1], name='encoder3')
        self.conv3_f1 = Encoder(256, 512, 3, 3, name='encoder3_flat1')
        self.conv3_f2 = Encoder(512, 1024, 3, 3, name='encoder3_flat2')
        self.conv3_f3 = Encoder(1024, 512, 3, 3, name='encoder3_flat3')
        self.conv3_f4 = Encoder(512, 256, 3, 3, name='encoder3_flat4')

        self.bnc1 = op.BatchNormalization(name='bnc1')
        self.bnc1_f1 = op.BatchNormalization(name='bnc1_flat1')
        self.bnc1_f2 = op.BatchNormalization(name='bnc1_flat2')
        self.bnc2 = op.BatchNormalization(name='bnc2')
        self.bnc2_f1 = op.BatchNormalization(name='bnc2_flat1')
        self.bnc2_f2 = op.BatchNormalization(name='bnc2_flat2')
        self.bnc3 = op.BatchNormalization(name='bnc3')
        self.bnc3_f1 = op.BatchNormalization(name='bnc3_flat1')
        self.bnc3_f2 = op.BatchNormalization(name='bnc3_flat2')
        self.bnc3_f3 = op.BatchNormalization(name='bnc3_flat3')
        self.bnc3_f4 = op.BatchNormalization(name='bnc3_flat4')

        self.deconv1 = Decoder(256, 256, 4, 4, strides=[1, 2, 2, 1], name='decoder1')
        self.deconv1_f1 = Encoder(256, 128, 3, 3, name='decoder1_flat1')
        self.deconv1_f2 = Encoder(128, 128, 3, 3, name='decoder1_flat2')
        self.deconv2 = Decoder(128, 128, 4, 4, strides=[1, 2, 2, 1], name='decoder2')
        self.deconv2_f1 = Encoder(128, 128, 3, 3, name='decoder2_flat1')
        self.deconv2_f2 = Encoder(128, 48, 3, 3, name='decoder2_flat2')
        self.deconv3 = Decoder(48, 48, 4, 4, strides=[1, 2, 2, 1], name='decoder3')
        self.deconv3_f1 = Decoder(48, 24, 3, 3, name='decoder3_flat1')
        self.deconv3_f2 = Decoder(24, 1, 3, 3, name='decoder3_flat2')

        self.bnd1 = op.BatchNormalization(name='bnd1')
        self.bnd1_f1 = op.BatchNormalization(name='bnd1_flat1')
        self.bnd1_f2 = op.BatchNormalization(name='bnd1_flat2')
        self.bnd2 = op.BatchNormalization(name='bnd2')
        self.bnd2_f1 = op.BatchNormalization(name='bnd2_flat1')
        self.bnd2_f2 = op.BatchNormalization(name='bnd2_flat2')
        self.bnd3 = op.BatchNormalization(name='bnd3')
        self.bnd3_f1 = op.BatchNormalization(name='bnd3_flat1')


def autoencoder(images, height, width):
    """make autoencoder network"""

    AE = AutoEncoder()

    def div(v, d):
        return max(1, v // d)

    relu = tf.nn.relu
    net = relu(AE.bnc1(AE.conv1(images, [height, width])))
    net = relu(AE.bnc1_f1(AE.conv1_f1(net, [div(height, 2), div(width, 2)])))
    net = relu(AE.bnc1_f2(AE.conv1_f2(net, [div(height, 2), div(width, 2)])))
    net = relu(AE.bnc2(AE.conv2(net, [div(height, 2), div(width, 2)])))
    net = relu(AE.bnc2_f1(AE.conv2_f1(net, [div(height, 4), div(width, 4)])))
    net = relu(AE.bnc2_f2(AE.conv2_f2(net, [div(height, 4), div(width, 4)])))
    net = relu(AE.bnc3(AE.conv3(net, [div(height, 4), div(width, 4)])))
    net = relu(AE.bnc3_f1(AE.conv3_f1(net, [div(height, 8), div(width, 8)])))
    net = relu(AE.bnc3_f2(AE.conv3_f2(net, [div(height, 8), div(width, 8)])))
    net = relu(AE.bnc3_f3(AE.conv3_f3(net, [div(height, 8), div(width, 8)])))
    net = relu(AE.bnc3_f4(AE.conv3_f4(net, [div(height, 8), div(width, 8)])))
    net = relu(AE.bnd1(AE.deconv1(net, [div(height, 4), div(width, 4)])))
    net = relu(AE.bnd1_f1(AE.deconv1_f1(net, [div(height, 4), div(width, 4)])))
    net = relu(AE.bnd1_f2(AE.deconv1_f2(net, [div(height, 4), div(width, 4)])))
    net = relu(AE.bnd2(AE.deconv2(net, [div(height, 2), div(width, 2)])))
    net = relu(AE.bnd2_f1(AE.deconv2_f1(net, [div(height, 2), div(width, 2)])))
    net = relu(AE.bnd2_f2(AE.deconv2_f2(net, [div(height, 2), div(width, 2)])))
    net = relu(AE.bnd3(AE.deconv3(net, [height, width])))
    net = relu(AE.bnd3_f1(AE.deconv3_f1(net, [height, width])))

    net = tf.nn.sigmoid(AE.deconv3_f2(net, [height, width]))

    return net

AutoEncoderはこんな感じです。なんとなくフーンってなって貰えればいいかと・・・。論文中では、どっちかというとloss mapという手法についてフォーカスしている感じですが、Tensorflowでヒストグラムを参照するやり方がよくわからんという理由で、その部分はなんちゃって実装になってます。

やってみた

利用したネットワークは、以下のようなパラメータなどを与えて250000程回したものです。

  • batch size = 15
  • 学習画像のパッチサイズ = 128*128
    • 元となる画像から、ランダムに128*128をcropして利用
    • 上下・左右・色相をランダムに変換
  • learning rate = 0.00002
  • Optimizer = ADAM

巨大サイズは、そもそも2GiBのメモリでも乗り切らなかったのであきらめました・・・。1回縮小して抽出してから、高解像度にするためのエンコーダーを挟めば良さそうです。

それなりのサイズの画像

みこーん!塗ってみた。 | 雨雫@お仕事募集中 さん

元画像はいずれもPixivの塗ってみたから拝借しています。この画像は元線画がないもので、そもそも比較対象がないのでアレですが。このサイズでもGPUでも10秒くらいかかったので、正直CPUでやるってのは考えたくないです。

かなりしっかり出ているかと思います。データセット内の画像の平均サイズが1000*1000くらいと結構な大きさなので、ある程度大きいサイズの画像でも対応できます。ただ、残念なのは下の方に特有のギザギザが・・・これは出る時と出ない時があるのでなんとも言えないですが。
output1.png

サムネイルサイズ

256*256のサイズです。一応、OpenCVで抜いてみた版も入れます。256*256がサムネイル化というツッコミは無視します。

元絵。パッと見は以外と線が出そうな雰囲気はしますが・・・。
small_origin.jpeg

OpenCV版。細かい色調まで全部出てしまっているため、線画というかグレースケールのなりそこないな感は否めません。
small_opencv.jpeg

今回のネットワーク版。一部怪しい(というか手の部分は流石に無理ゲー)ですが、影の影響を結構ちゃんと無視できてたり、髪の部分の表現がシンプルになっていたりと、手前味噌ですが結構いい感じではないでしょうか。フリル系等の細か過ぎるディティールは流石に潰れていますが、これはもう少し学習を続ければ少しずつ解消出来そうです。
small_cnn.jpeg

データセットについて

学習に利用したデータセットは、基本的にPixivの塗ってみたカテゴリから取得しています。データセットとする上で一番大事なのは、 線画と着色画とでアスペクトが一致している ことでした。これがずれると、そもそも学習できないというようなものになりますので、一点一点チェックしながら集めました。

また、たまーにあったのが、アスペクト比は同一でも微妙に 線画と着色画がずれている というものでした。これもあると学習が進まないので、ちゃんと省く必要がありました。

弱点

塗り方によりますが、OpenCVよりも簡単な線になっており、かつディティールがそこまで失われていない感じになってるんじゃないかと思います。ただ、構造というかやってることの性質上、どうしようもないことも含めて、欠点があります。

  • 黒塗り部分がそのまま出る
    • 学習に利用したデータの中に、元線画自体に黒塗りがあったことと相まって、黒い部分はかなりの割合でそのままになります
  • 小さいサイズになるとぼやけやすい
    • これは、元になる画像が最小でも256*256くらいのサイズになっていたため、それよりも細かい特徴が会った場合に対応できていないような気がします
  • ぼやけている部分は基本諦め
    • 無理やりガウシアンフィルタとかでほやかした画像で学習させたりもしてみましたが、かなりきついです
  • 線の色とほぼ同じ色で塗られている部分はかなり曖昧になる
    • 濃い影とか塗り方によります
    • オリジナルを再現することは、基本的に無理です(線の上になんか追加されてたりするし)

基本的にはそのまま利用するというよりは、これをベースにして加工する、という感じになるかなぁと思います。

総括

AutoEncoderとかの生成系ネットワークって面白いです。パラメータを与えて線画の出来方を変える、ってのにもチャレンジしたいところ。

調べたり実装したりは大変ですが、素人がDeepLearningを出来るようになっていますので、ぜひ少し投資してやってみることをオススメします(自前orクラウドで)。