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

  • 22
    いいね
  • 0
    コメント

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

線画が欲しい

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

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

とまぁ、いくつか問題がありますが、手軽(OpenCVが動けばいい)かつ高速(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, batch_size):
        self.conv1 = Encoder(3, 32, 5, 5, strides=[1,2,2,1], name='encoder1')
        self.conv2 = Encoder(32, 64, 5, 5, strides=[1,2,2,1], name='encoder2')
        self.conv3 = Encoder(64, 128, 5, 5, strides=[1,2,2,1], name='encoder3')
        self.conv4 = Encoder(128, 256, 5, 5, strides=[1,2,2,1], name='encoder4')
        self.flat_conv1 = Encoder(128, 256, 3, 3, name='flat_encoder1')
        self.flat_conv2 = Encoder(256, 512, 3, 3, name='flat_encoder2')
        self.flat_conv3 = Encoder(512, 1024, 3, 3, name='flat_encoder3')
        self.flat_conv4 = Encoder(1024, 512, 3, 3, name='flat_encoder4')
        self.flat_conv5 = Encoder(512, 256, 3, 3, name='flat_encoder5')
        self.flat_conv6 = Encoder(256, 128, 3, 3, name='flat_encoder6')

        self.bnc1 = op.BatchNormalization(name='bnc1')
        self.bnc2 = op.BatchNormalization(name='bnc2')
        self.bnc3 = op.BatchNormalization(name='bnc3')
        self.bnc4 = op.BatchNormalization(name='bnc4')
        self.fbnc1 = op.BatchNormalization(name='fbnc1')
        self.fbnc2 = op.BatchNormalization(name='fbnc2')
        self.fbnc3 = op.BatchNormalization(name='fbnc3')
        self.fbnc4 = op.BatchNormalization(name='fbnc4')
        self.fbnc5 = op.BatchNormalization(name='fbnc5')
        self.fbnc6 = op.BatchNormalization(name='fbnc6')

        self.deconv1 = Decoder(256, 128, 4, 4, batch_size, strides=[1,2,2,1],name='decoder1')
        self.deconv2 = Decoder(128, 64, 4, 4, batch_size, strides=[1,2,2,1],name='decoder2')
        self.deconv3 = Decoder(64, 32, 4, 4, batch_size, strides=[1,2,2,1],name='decoder3')
        self.deconv4 = Decoder(32, 1, 4, 4, batch_size, strides=[1,2,2,1],name='decoder4')

        self.bnd1 = op.BatchNormalization(name='bnd1')
        self.bnd2 = op.BatchNormalization(name='bnd2')
        self.bnd3 = op.BatchNormalization(name='bnd3')

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

    AE = AutoEncoder(batch_size)

    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.bnc2(AE.conv2(net, [div(height, 2), div(width, 2)])))
    net = relu(AE.bnc3(AE.conv3(net, [div(height, 4), div(width, 4)])))
    net = relu(AE.fbnc1(AE.flat_conv1(net, [div(height, 8), div(width, 8)])))
    net = relu(AE.fbnc2(AE.flat_conv2(net, [div(height, 8), div(width, 8)])))
    net = relu(AE.fbnc3(AE.flat_conv3(net, [div(height, 8), div(width, 8)])))
    net = relu(AE.fbnc4(AE.flat_conv4(net, [div(height, 8), div(width, 8)])))
    net = relu(AE.fbnc5(AE.flat_conv5(net, [div(height, 8), div(width, 8)])))
    net = relu(AE.fbnc6(AE.flat_conv6(net, [div(height, 8), div(width, 8)])))
    net = relu(AE.bnd2(AE.deconv2(net, [div(height, 4), div(width, 4)])))
    net = relu(AE.bnd3(AE.deconv3(net, [div(height, 2), div(width, 2)])))
    net = tf.nn.sigmoid(AE.deconv4(net, [height, width]))

    return net

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

やってみた

元画像はいずれもPixivの塗ってみたから拝借しています。この画像は元線画がないものです。

巨大サイズの画像

めっちゃでかいサイズです。CPUでやったら数分たっても終わらない&2GiB以上のメモリ食う、というなんともな感じです。

元絵→塗らせていただきました。でんぱおんな | らなびーむ さん
オリジナルの線画→線画 | くっしー君 さん

抽出してみた線画。巨大な絵になると、やっぱり色々とアラが出ますね。。。
output2.png

普通サイズの画像

大体一般的なサイズです。こっちはCPUでも30秒くらいで終わりました。

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

そこそこサイズですが、それなりな感じです。元絵でぼやけてる部分についてはノーコメントで。。。
output1.png

サムネイルサイズ

128*128のサイズです。このサイズは、線画着色のネットワークで利用しているので、OpenCVでの前処理版も貼ります。

オリジナル
small.jpg
OpenCV版。綺麗なんですが、特に髪の辺りが塗りの特徴がそのまま出てしまって、無用に詳細すぎる感があります。
small_opencv.jpg
ネットワークで抽出してみた線画。色が薄いのは学習不足っぽいですが、髪の辺りはかなり線画っぽいんじゃないでしょうか。
small_out.jpg

弱点

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

  • 黒塗り部分がそのまま出る
    • 学習に利用したデータの中に、元線画自体に黒塗りがあったことと相まって、黒い部分はかなりの割合でそのままになります
  • ぼやけている部分は基本諦め
    • 無理やりガウシアンフィルタとかでほやかした画像で学習させたりもしてみましたが、かなりきついです
  • 線の色とほぼ同じ色で塗られている部分はかなり曖昧になる
    • 濃い影とか塗り方によります
    • オリジナルを再現することは、基本的に無理です(線の上になんか追加されてたりするし)
  • 細か過ぎる部分は苦手
    • これは学習不足なんだとは思いますが、細かい部分はかなり怪しいです。
    • 書き込みの多い画像を変換したりするとひどいことになるケースが往々にして・・・

基本的にはそのまま利用するというよりは、これをベースにして加工する、という感じになるかなぁと思います。ただ、参考にした論文ではvectorizationとかをやっているところ、これではやっていないので、それを取り入れるとどうなるか?というのはやってみようかと思います。

総括

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

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