【ネコと和解せよ】AIの力で神をネコにしたかった...

この記事は[OthloTech Advent Calendar 2017](https://qiita.com/advent-calendar/2017/othlotech)の17日目の記事です。
このAdvent Calendarには基礎から実践的なことまで参考になる記事ばかりですね。
タイトルで御察しの通りネタ的な記事になりますのでご了承ください...

この記事では、過去にネット上でネタとなった「神と和解せよ」の看板を自動で「ネコと和解せよ」のコラ画像に変換します。[前日の記事](https://qiita.com/pokohide/items/43203f109fd95df9a7cc)とネタ被り感が...

タイトルを「したかった」にした理由は後述ですが、雑ながら実現できていますのでご注意を。

# 「ネコと和解せよ」とは？

田舎でよく見かけるキリスト教の看板が元ネタです。「神と和解せよ」などちょっと不気味な内容ばかりですが、何年か前からインターネット上で「神」を「ネコ」に書き換えるコラが話題になりました。

そのコラ画像はこんな感じです
![BhdB8TiCIAAOjKo.jpg](https://qiita-image-store.s3.amazonaws.com/0/62541/8e911371-9ba2-ab3f-670e-ec9707bdd0f1.jpeg)
出典：https://twitter.com/daidoooooo/status/438891993150652416/photo/1

前置きの通り、今回このコラ画像を自動で生成します。

# 使用技術

ふとコラ画像のことを思い出してAI技術で自動生成できないか？と思いました。

<img src="https://qiita-image-store.s3.amazonaws.com/0/62541/d831bacd-7683-b2c8-dd9c-9354fdfc144e.jpeg" width="280px" />

出典：https://twitter.com/thetak11/status/841924218208018432

そこで、今もなお話題のなっているディープラーニングの力で自動生成しよう！

・・・と思いましたが断念しました...そもそもコラ画像が膨大にあるわけでもありませんし、コラ画像と元の画像の対応を簡単に結びつけられそうにありません・・・データセットの量がない以上、ディープラーニングを行うのは悪手ですし、画像同士の対応がなければ教師あり学習もできません。

一応、教師なし学習のクラスタリングが役立ちましたし、Microsoft AzureのクラウドAIは使いましたが、pix2pixみたいなものを使いたかった・・・

前置きはともかく使用技術としては、

- OCR（文字認識）技術
  - 今回はMicrosoft AzureのComputer Vision APIを使いました。Tesseractというローカルでも動くものがありますが、Azureの方が認識してくれました。
- OpenCV
  - 画像処理の鉄板ですね。今回はpythonから使いました。
- 機械学習
  - 今回はクラスタリング手法となるK-means法を使いました。使い方は後ほど。

# アルゴリズム

今回は機械学習モデルが全部なんとかしてくれる、ってわけではないので試行錯誤に基づいてアルゴリズムを練りました。試行錯誤含めて、３,4時間ほどで編み出しました。

1. 画像から「神」の字を探す（MS AzureのAPIを使用）
2. 「神」の字を切り抜き、コーナー検出をする（ここでは元画像を消さないように）
3. 取得したコーナーのx座標のみをK-means法にかけてクラスタリング
4. クラスタリングしたコーナーは「ネ」と「申」に別れるので、座標とクラスを元に元画像の「申」を「コ」に上書きする

## 画像から「神」の字を探す
前述の通りMicrosoft Azureの[Computer Vison API](https://azure.microsoft.com/ja-jp/services/cognitive-services/computer-vision/)を使います。
１ヶ月使える無料試用版があるので、その期間なら誰でも無料で使えます。

このAPIは画像から情報を解析するものですが、その中にOCR（文字認識）のAPIがあるのでこれを使います。

例として画像をAPIに通しましょう。

![img_1.jpeg](https://qiita-image-store.s3.amazonaws.com/0/62541/68a053d3-9814-aa0f-c524-d12d78fa6d76.jpeg)

以下のような出力が得られるはずです。

```json
{
  "textAngle": -4.1000000000000343,
  "orientation": "NotDetected",
  "language": "ja",
  "regions": [
    {
      "boundingBox": "54,89,175,233",
      "lines": [
        {
          "boundingBox": "54,89,74,233",
          "words": [
            {
              "boundingBox": "59,89,69,58",
              "text": "和"
            },
            {
              "boundingBox": "56,155,66,66",
              "text": "解"
            },
            {
              "boundingBox": "61,229,48,44",
              "text": "せ"
            },
            {
              "boundingBox": "54,278,51,44",
              "text": "よ"
            }
          ]
        },
        {
          "boundingBox": "130,115,99,166",
          "words": [
            {
              "boundingBox": "130,115,99,106",
              "text": "神"
            },
            {
              "boundingBox": "158,230,44,51",
              "text": "と"
            }
          ]
        }
      ]
    }
  ]
}
```

下の方にありますね。`text`と`boundingBox`が組になっていますが、`boundingBox`が対応する文字の座標となります。`（左端のx座標）,（上端のy座標）,（幅）,（高さ）`というフォーマットとなっており、左上の座標(0,0)としたピクセル表現の座標となっているので、opencvでこの数値をほぼそのままで使えます。ただしAPI出力が文字列であることに注意しましょう。

また、画像によっては「神」の字を認識しないものもありました。

## 「神」の字を切り抜き、コーナー検出をする

「神」の字の座標を取得できました。字を切り抜きは簡単にできそうです。しかし「ネコ」へと変換する方法に悩みました。文字の特徴を抽出すればできると思いその方法を探しました。今回はコーナー検出を使います。

[公式のサンプル](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_feature2d/py_features_harris/py_features_harris.html)を元に組みました。

```python
import cv2
import numpy as np

# 取得した座標をそれぞれ x, y, w, h に代入した前提
# x, y, w, h はそれぞれ左端のx座標、上端のy座標、幅、高さに対応
img = cv2.imread('/path/to/image.jpg')
cropped = img[y:y+h, x:x+w] # 「神」の字を切り抜く
gray = cv2.cvtColor(cropped, cv2.COLOR_BGR2GRAY) # グレースケール化
dst = cv2.cornerHarris(gray,2,3,0.04) # コーナー検出
dst = cv2.dilate(dst,None)
corner_points = np.argwhere(dst>0.01*dst.max()) # 不必要かもしれないが、閾値によるフィルタリング
```

先ほどの画像の「神」から検出したコーナーを描画したらこんな感じになりました。

![ダウンロード.png](https://qiita-image-store.s3.amazonaws.com/0/62541/a99edb52-6a6d-8b9d-1246-55852e950a01.png)

描画コードはこんな感じです。

```python
drawn = cropped.copy()
for (y, x) in  corner_points:
    drawn[y, x] = [0, 255, 0]

import matplotlib.pyplot as plt
%matplotlib inline # jupyterの場合
plt.imshow(drawn)
```

## 取得したコーナーのx座標のみをK-means法にかけてクラスタリング

コーナーを検出したところで、どう加工すればいいのか。最初は、「申」から「コ」を取り出そうしましたが、それを行うには情報が足りませんでした。そこで代替案として、「申」の上に「コ」を上書きすることにしました。そのためには、「ネ」と「申」を取り出す必要があります。これをクラスタリングでできないかと思いましたが、K-meansでできました。

```python
from sklearn.cluster import KMeans

# 学習データはベクトルではなく行列であることに注意
kmeans = KMeans(n_clusters=2, random_state=0).fit(corner_points[:, 1].reshape(-1, 1))
x_ne_right = int(kmeans.cluster_centers_.mean()) # クラスごとの重心座標の平均から分け目のx座標を取得
```

今度はクラスタごとに分けて描画します。

![ダウンロード (2).png](https://qiita-image-store.s3.amazonaws.com/0/62541/6b46e2dd-0f89-c8c8-3f77-570779f09f7f.png)

見事に「ネ」のポイントと「申」のポイントに分けられましたね。成功です。
描画コードは以下のようになりました。

```python
drawn = cropped.copy()
for (x, y), label in  zip(corner_points, kmeans.labels_):
    drawn[x, y] = [0, 255, 0] if label == 0 else [0, 0, 255]
plt.imshow(drawn)
```

他の看板画像で試してもうまくいきましたが、なぜ都合よくコーナーをクラスタリングできたのか。使った時はあまり気にしませんでしたが、k-means法の各クラスの座標の重心をとる特性からでしょうか。検出したコーナーを使ったからこそ成功したのだと思います。

ちなみに、二つの部分の分け目ですが、重心のx座標の平均値から求めました。果たしてその値でうまく分けれるでしょうか。

```python
left = cropped[:, :x_ne_right]
right = cropped[:, x_ne_right:]
```

`left`と`right`を描画した結果が以下です。

![ダウンロード (3).png](https://qiita-image-store.s3.amazonaws.com/0/62541/28110989-a8c1-8340-6665-efac8a45d702.png)

![ダウンロード (4).png](https://qiita-image-store.s3.amazonaws.com/0/62541/6a639753-cdad-f044-9399-d5abe873ee5c.png)

完璧には分けられませんでしたね。他にも左側のクラスでもっとも右端のx座標を取得し、それを元に切っても同様の結果となりました。後にコーナーをフィルタリングする閾値を0にしたら綺麗に分けられることがわかりましたが、コーナーを描画する分にはフィルタリングした方が見やすいです。

## 座標とクラスを元に元画像の「申」を「コ」に上書きする

「申」の座標を取得できたので、これを上書きします。このステップは以下の二つのステップに細分化されています。

1.上書きに使う色を抽出する
2.「申」を消して「コ」を書き込む

### 上書きに使う色を抽出する

これもK-meansを使います。切り抜いた画像の色をクラスタリングして重心の色を取得、それらの色を使って上書きします。

```python
colors = cropped.reshape((-1, 3)) # ピクセルごとの色の配列を作る
kmeans = KMeans(n_clusters=2, random_state=0).fit(colors)
# K-meansモデルから背景色を抽出（看板だけでいえば背景の方が暗い）
bg_color = kmeans.cluster_centers_[np.argmin(kmeans.cluster_centers_.sum(axis=1))]
# K-meansモデルから文字色を抽出
ch_color = kmeans.cluster_centers_[np.argmax(kmeans.cluster_centers_.sum(axis=1))]

# 後にtupleとして渡すのと、中身がfloatになっていることがあるので変換
bg_color, ch_color = tuple(map(int, bg_color)), tuple(map(int, ch_color))
```

### 「申」を消して「コ」を書き込む
```python
new_img = img.copy()
# 「申」の座標を作る。ここでクラスタリングから計算したx座標がクロップしたx座標であることに注意。
x_min, y_min, x_max, y_max = x + x_ne_right, y, x + w, y+ h

# 「申」を背景色で塗りつぶし
cv2.rectangle(new_img, (x_min, y_min), (x_max, y_max), bg_color, -1)

# 「コ」を四角形で描く
thick = 10 # thick 「コ」の字は太さ。画像に合わせてお好みで。
cv2.rectangle(new_img, (x_min, y_min), (x_max, y_min + thick), ch_color, -1)
cv2.rectangle(new_img, (x_max - thick, y_min), (x_max, y_max), ch_color, -1)
cv2.rectangle(new_img, (x_min, y_max - thick), (x_max, y_max), ch_color, -1)
```

実際に上書きした画像はこちらになります。（コーナーのフィルタリングの閾値を0にしたものです）

![ダウンロード (5).png](https://qiita-image-store.s3.amazonaws.com/0/62541/d80435f5-dd45-4478-8f4e-5bfa36d0845a.png)

めっちゃ雑コラですね（笑）
座標取得ができていなかったのか若干棒がはみ出しているのがわかります。
しかしやや違和感はあるものの色の取得なんかはそれっぽくできていると思います。

他の画像でも自動生成にトライしました。結果はこんな感じです。

<img src="https://qiita-image-store.s3.amazonaws.com/0/62541/27e2826b-7127-3a09-8d32-bded81b07851.png" width="280px" />

ネコと和解せよ（別画像）
こちらははみ出さずにうまくいっていますね

<img src="https://qiita-image-store.s3.amazonaws.com/0/62541/0606bef9-89fd-7495-25ea-3f5057012ece.png" width="280px" />

ネコへの態度を悔い改めよ
ルビの「かみ」が見えますね。ネコ（かみ）

<img src="https://qiita-image-store.s3.amazonaws.com/0/62541/5d4fb678-b080-9ee0-0986-49d6365673ff.png" width="280px" />

心の罪もネコはさばく
こっちも上にはみ出して失敗した感じになっています。

# アルゴリズムの改善案

考察項です。より雑コラ感を薄めるには

1. 「神」の字をもっと綺麗に取り出す（OCRの座標取得部分の改善）
2. 上書き箇所の改善（使用する色の取り方、明度に合わせてグラデーションなどをかける）
3. 「申」を消すのではなく「申」の不要部分を消して「コ」を作る
4. 「コ」の描画方法を再検討する
5. 無敵のディープラーニングでなんとかする（データセットを自作する）

でしょうか。

4は苦行になると思います。特にデータセットの数が揃わない限りは当然のように過学習が起きまくるので挑戦したい方は頑張ってください。

3もopencvの特徴量抽出では限界があると思います。

1をする場合は頑張ってMicrosoftさんや他のOCR APIに負けないモデルを作ってください。

この中でもできそうなのが2でしょうかね。使用する色の取り方に関しては、エッジ抽出などで頑張って看板をとり出せるかもしれません。それができればより自然な背景色の選定ができるのではないでしょうか。上書きする画像にグラデーションをかける場合はかなり複雑な要素が絡みそうですね。泥臭いチューニングやPDCAは回避できないでしょう。

また、紹介したアルゴリズムは看板の傾きには対応していないものの、OCR APIに画像内の文字の傾き方の情報を抽出できます。上書きの際にその傾きを使えば自然な「ネコ」が描けるかもしれません。

# 終わりに
[前日のMr.ビーンの記事](https://qiita.com/pokohide/items/43203f109fd95df9a7cc)とは、使用技術が顔検出とOCR、対象がMr.ビーンかネコであることが差異ぐらいで、かなり被りましたね...しかも向こうの方が綺麗にコラ作れて羨ましい...

向こうではたくさんMr.ビーンがでたにもかかわらず、猫が中心のこの記事では一度も猫が出ませんでしたね...

末筆ながら猫を貼らせていただきます

![kijinekoguttari171027_TP_V.jpg](https://qiita-image-store.s3.amazonaws.com/0/62541/2d0c460c-4847-5db5-804a-b10f26c54f06.jpeg)

出典：https://www.pakutaso.com/20171046300post-13838.html

かわいい