ディープラーニングでアスキーアートを作る

はじめまして。
本業はアスキーアート (以下AA) 職人のOsciiArtといいます (本業ではない)。
AlphaGo対イ・セドルの対局を見て、「**僕もディープラーニングで神AA職人を倒したい！**」と思い、pythonをインストールしてちょうど一年の成果を書いていきます。

コードはこちらにアップしてあります。
https://github.com/OsciiArt/DeepAA

# ここで扱うアスキーアートとは
ここで扱うAAとは、
こういうの……↓
![notaa1.png](https://qiita-image-store.s3.amazonaws.com/0/168784/09f9294d-de38-2b8e-de42-41693da82ca4.png)
ではなく、こういうの……↓
![notaa2.png](https://qiita-image-store.s3.amazonaws.com/0/168784/d371f4e5-79bc-0483-79b9-b0c3d1e67df4.png)
でもなく、こういうの……↓
![notaa3.png](https://qiita-image-store.s3.amazonaws.com/0/168784/b699a322-a33e-bcec-0253-e0a57d53113a.png)
ともちょっと違って、こういうの……↓
![notaa4.png](https://qiita-image-store.s3.amazonaws.com/0/168784/1d81eeb2-8288-b140-52f3-a43b0b9d8486.png)
ではもちろんなく、こういうのです。↓
![この1.png](https://qiita-image-store.s3.amazonaws.com/0/168784/4c06f91d-b8a1-4af4-6da2-f95d82caf24c.png)

このような、線画を文字を作って再現した「**トレースAA**」と呼ばれるタイプのAAをここでは扱います。
詳細はwikipediaの「アスキーアート」のページの「プロポーショナルフォント」の項を参照してください。

[wikipedia:アスキーアート - プロポーショナルフォント](https://ja.wikipedia.org/wiki/%E3%82%A2%E3%82%B9%E3%82%AD%E3%83%BC%E3%82%A2%E3%83%BC%E3%83%88#.E3.83.97.E3.83.AD.E3.83.9D.E3.83.BC.E3.82.B7.E3.83.A7.E3.83.8A.E3.83.AB.E3.83.95.E3.82.A9.E3.83.B3.E3.83.88.E3.81.AE.E3.82.82.E3.81.AE)

フォント等の条件は2ちゃんねるの仕様である、

- MS Pゴシック
- サイズ16ピクセル
- 行間2ピクセル

が広く採用されています。

# アスキーアートの良し悪し
誤解が多いのですが前提としてAAは基本的に**手書き**です。
(誤解の一例:[Yahoo知恵袋:AAアスキーアートを2ちゃんねるで見るんですが、どうやって作るんですか？](http://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q1314829798    ))

自動でAAを作るソフトはいくつか存在していますが人間の手書きには遠く及ばないのが現状です。
AAの良し悪しを判断する時に注意しなければならないのは**サイズ**です。際限なく大きく作れば1文字で1ピクセルを表して完全に元画像を再現できます (ソフトでも)。そうではなく1文字でより多くの線を表現しサイズを小さく収めているAAが良いAAです。

つまり、AAの良し悪しは

**元画像の再現度 ÷ サイズ**

と定義できます。

# 学習データ
ディープラーニングの学習には元画像とAAのペアが大量に必要となります。しかし、AAは基本的に元画像とペアで発表されることはありません。そのため、データ収集は困難です。また、AAは元画像の線を大きくデフォルメするケースが多く、仮にペアが得られても学習は困難であると予想されます。
そこで今回は、AAから元画像らしきものを生成して学習データとしました。この辺のアプローチはシモセラ・エドガーらの研究 ([ラフスケッチの自動線画化](http://hi.cs.waseda.ac.jp/~esimo/ja/research/sketch/))を参考にしました。

### 手順
1. AAを画像化します。

![k0.png](https://qiita-image-store.s3.amazonaws.com/0/168784/50718989-97fc-8532-aaf3-fb6f91cd4516.png)
2. これでは実際の線画とはかけ離れているので、シモセラ・エドガーらのウェブサービス[ラフスケッチの自動線画化](http://hi.cs.waseda.ac.jp:8081/)で画像を線画っぽくします。
![k0_3.png](https://qiita-image-store.s3.amazonaws.com/0/168784/7dc904cd-afe3-5872-88fa-74a55aa34913.png)
3. 画像を64 x 64 ピクセルで切り出し、中央の16 x 16の領域に対応する文字を正解ラベルとします。
![データ例.png](https://qiita-image-store.s3.amazonaws.com/0/168784/ea8ed923-014e-d8a5-7def-d9f50ccec428.png)
4. このような処理を約200個のAAに対して行い、学習データとしました。

# 学習
フレームワークはKeras (バックエンド: TensorFlow) を用いました。
ネットワークにはクラス分類を行う標準的な畳み込みニューラルネットワークを用いました。以下にコードを示します。

```py
def DeepAA(num_label=615, drop_out=0.5, weight_decay=0.001, input_shape = [64, 64]):
    """
    Build Deep Neural Network.
    :param num_label: int, number of classes, equal to candidates of characters
    :param drop_out:  float
    :param weight_decay: float
    :return: 
    """
    reg = l2(weight_decay)
    imageInput = Input(shape=input_shape)
    x = Reshape([input_shape[0], input_shape[1], 1])(imageInput)
    x = GaussianNoise(0.1)(x)
    x = Convolution2D(16, 3, 3, border_mode='same', W_regularizer=reg, b_regularizer=reg, init=normal)(x)
    x = BatchNormalization(axis=-3)(x)
    x = Activation('relu')(x)
    x = MaxPooling2D(pool_size=(2, 2), border_mode='same')(x)
    x = Dropout(drop_out)(x)
    x = Convolution2D(32, 3, 3, border_mode='same', W_regularizer=reg, b_regularizer=reg, init=normal)(x)
    x = BatchNormalization(axis=-3)(x)
    x = Activation('relu')(x)
    x = MaxPooling2D(pool_size=(2, 2), border_mode='same')(x)
    x = Dropout(drop_out)(x)
    x = Convolution2D(64, 3, 3, border_mode='same', W_regularizer=reg, b_regularizer=reg, init=normal)(x)
    x = BatchNormalization(axis=-3)(x)
    x = Activation('relu')(x)
    x = MaxPooling2D(pool_size=(2, 2), border_mode='same')(x)
    x = Dropout(drop_out)(x)
    x = Convolution2D(128, 3, 3, border_mode='same', W_regularizer=reg, b_regularizer=reg, init=normal)(x)
    x = BatchNormalization(axis=-3)(x)
    x = Activation('relu')(x)
    x = MaxPooling2D(pool_size=(2, 2), border_mode='same')(x)
    x = Flatten()(x)
    x = Dropout(drop_out)(x)
    y = Dense(num_label, activation='softmax')(x)

    model = Model(input=imageInput, output=y)
    
    return model
```

学習条件

- データ数: 484654
- バッチサイズ: 128
- 学習回数: 20,000 バッチ
- 損失関数: cross entropy
- 最適化関数: Adam

上記設定でGPUなしのマシンで2日程度学習させました。

# 結果
いくつかの他のAA自動作成ソフトとの比較を示します↓
![比較1.png](https://qiita-image-store.s3.amazonaws.com/0/168784/ba370e98-f574-a179-3d21-b5beeeea50db.png)

- Input Image: 元画像
- （´д｀）Auto: 作成者 - kuronowish氏, 入手先 - [（´д｀）Editさいたま営業所の避難所](http://sky.geocities.jp/edit_hinanzyo/)
- AsciiArtConverter: 作成者 - うりゅP氏, 入手先  - [test](https://onedrive.live.com/?authkey=%21APuDfORKnMld3YE&id=2A550F2DD4D15CFC%21116&cid=2A550F2DD4D15CFC)
- DeepAA: 本記事の提案手法 (以下DeepAA)
- Human: 手書きAA

サイズ設定は手書き時に選択されたもので統一しています。変換はいずれのソフトも初期設定です。
どのソフトも設定をいろいろ試行錯誤してよくしていくものなので初期設定ではやや横並びです。線が込み入った部分にピタリとハマる文字を選択する能力は提案手法がやや優れているように思います。

比較をもう一点↓
![比較2.png](https://qiita-image-store.s3.amazonaws.com/0/168784/053524f1-b2b8-4eb2-1cfa-ab0c5257a599.png)
こちらは提案手法で上手く出力されたサイズで統一しています。よってかなりひいき目の比較になります。
しかしながら、DeepAAは特に目の作り方がかなり上手く、人間に勝るとも劣らない文字選択が出来ていると思います。

以下何点か作例を貼っていきます。
![作例2.png](https://qiita-image-store.s3.amazonaws.com/0/168784/780c11ae-a18e-7d40-1823-13eb6adc046e.png)
経験則的には線画は細線化した方が良い結果が得られやすいです。
![作例3.png](https://qiita-image-store.s3.amazonaws.com/0/168784/fe1d6a53-3c6a-2af1-2d0b-e3c7ed5878c5.png)
あえて細線化せずベタもそのまま出力しても面白い結果が出ます。
![作例1.png](https://qiita-image-store.s3.amazonaws.com/0/168784/0eb2a750-e975-13c5-adf8-76ad3947d68a.png)
いかがでしょうか。

# 課題

既存の自動AA作成ソフト以上の精度は得られたのではないかと思いますが、まだまだ手書きAAの精度には程遠い結果となりました。以下改善のための課題を記していきます。

### ズレ
現状、手書きに比べてDeepAAの最大の弱点はズレだと思います。文字の幅が一定である等幅フォントと異なり、ここでは文字の幅が文字ごとに異なるプロポーショナルフォントを扱っています。等幅フォントの場合、画像のどこに文字を当てはめるかは一意的に決まりますが、プロポーショナルフォントでは文字の組み合わせによって位置を調整することができます。
例えば↓の例では、DeepAAの線は青く囲んだ部分ではガタガタですが、人が手直しすれば右のようにキレイにそろえることができます。(全角スペース (幅 11 ピクセル), 半角スペース (幅 5 ピクセル), ピリオド (幅 3 ピクセル) の組み合わせによって調整する。)
![修正例.png](https://qiita-image-store.s3.amazonaws.com/0/168784/0f7a7de7-3979-1bae-d6be-ee413998ff0b.png)
また、赤く囲んだ部分では「/」が 2 回当てはめられていますが、明らかに正解は 2 つの「/」の間の位置に 1 つ「/」を当てはめることです。
この問題は、学習データの段階で「**どこ**」に文字を当てはめるかは決まった上で、「**なに**」の文字を当てはめるかだけを学習させていることにあります。しかし、「どこ」を学習させるにはどうすればいいのか正直思いつきません。

### 線画化
AA画像の線画化にウェブサービスを使っているためデータ数を稼ぐときのネックになっているので代替案を考えたいところです。

### 学習回数
これまでCPUだけで学習させていたのですが、最近GPU環境を整えたのでより多くの回数を、より複雑なモデルで学習させるのも試したいです。