AI技術を使ってキャラの画像をラフ画に戻してみた。

# 初めに
タイトルにもある通りです。
数回に分けてこういった画像を
![2450112.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3163792/40db3c99-9f4b-f4b1-a742-b988dd8edebd.jpeg)
![sampling_img.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3163792/fccf1cb9-ef60-ff73-4236-3992cfafec02.png)
こんな感じに変換する方法のざっくりとした解説を行っていきます。
Pythonの基本構文のお話やライブラリに関する突っ込んだ内容はここでは割愛して、別の機会に気が向けば記事にします。

# もくじ
>[・初めに](#初めに)
[・ソースコード全文](#ソースコード全文)
[・解説](#解説)
[・二値化処理](#解説二値化処理)
[・0パディング](#解説0パディング)
[・線画抽出](#解説線画抽出)
[・あとがき](#あとがき)

# ソースコード全文
少しずつ解説していって最後に完成！って形にしてもいいのですが、面倒なので先に完成系を出してしまいます。
``` Python3 edge_samling.py
import cv2
import numpy as np

# 0パディングを行っている関数
def zero_padding(image):
    # 0パディングをするために元の画像より一回り大きい２次元配列を作成
    # 入力された画像が3次元だった場合
    if 3 == len(image.shape):
        padding_img = np.zeros([image.shape[0] + 2, image.shape[1] + 2, image.shape[2]], dtype=np.uint8)
    # それ以外だった場合
    else:
        padding_img = np.zeros([image.shape[0] + 2, image.shape[1] + 2], dtype=np.uint8)

    # 元画像の縦を展開
    for i, y in enumerate(image):
        i += 1
        # 元画像の横を展開
        for j, x in enumerate(y):
            j += 1
            # 0配列の中に画像を入力
            padding_img[i, j] = x

    return padding_img


# 二値化処理を行う関数
def binarization_image(image, threshold=127):
    # 画像をグレースケール化
    gray_img = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

    # 1pxごとの値が閾値よりも小さい・大きい場合それぞれ0・255に変更する
    gray_img[gray_img < threshold] = 0
    gray_img[gray_img >= threshold] = 255

    return gray_img


# 線画抽出を行う関数
def line_sampling(image, image_shape, kernel):
    # 線画抽出を行った後の画像の大きさを定義
    sampling_img = np.zeros([image_shape[0], image_shape[1]], dtype=np.uint8)

    # 縦のpx分回す
    for y in range(sampling_img.shape[0]):
        # 横のpx分回す
        for x in range(sampling_img.shape[1]):
            # カーネルと同じ大きさで画像を取得し、値を掛け合わせ、それらの値が入っている配列の各値を合算した結果を配列に格納
            sampling_img[y, x] = np.sum(image[y:y + 3, x:x + 3] * kernel)

    return sampling_img


# 二値化する際の閾値を定義
THRESHOLD = 128
# ラプラシアンフィルタを定義
LAPLACIAN_KERNEL = np.array([
    [1, 1, 1],
    [1, -8, 1],
    [1, 1, 1]
], dtype=np.uint8)

# 画像データを読み込み、ndarray化
img = cv2.imread(r"画像のパスに書き換えてください。")
# shapeは(縦, 横, チャンネル)の順番で格納されている
print(img.shape)

# 二値化処理
binarized_img = binarization_image(img, THRESHOLD)

# 0パディング
padding_img = zero_padding(binarized_img)

# 線画抽出
result_img = line_sampling(padding_img, img.shape, LAPLACIAN_KERNEL)

# 画像の色を反転
result_img[:, :] = 255 - result_img[:, :]

# 画像を保存
cv2.imwrite(r"保存先のパスに書き換えてください。", result_img)

# 線画の画像を出力
cv2.imshow('img', result_img)
cv2.waitKey(0)
```
これを動かすことでお好きな画像をラフ画にすることができます。
(パスの指定はお忘れなく)


## 解説
さて、変換の流れですが大まかに分けるとこんな感じ。

>1.画像データの読み込み
[2.**二値化処理**](#解説二値化処理)
[3.**0パディング**](#解説0パディング)
[4.**線画抽出**](#解説線画抽出)
5.色調反転
6.画像出力

本記事では**二値化処理**、**0パディング**、**線画抽出**の三項目に焦点を当てて解説します。
~~(というよりほかに解説する項目がないだけ)~~


### 解説[二値化処理]
線画の抽出は二値化処理した画像データに対して畳み込みを行うことで、色の境目を浮き上がらせることで行っています。
そのため、まずは読みこんだ画像を二値化、つまりは白と黒の二色のみで構成された画像に変換して画像の輪郭をはっきりさせなくてはいけません。

``` Python3 binarization_image.py
    # 画像をグレースケール化
    gray_img = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

    # 1pxごとの値が閾値よりも小さい・大きい場合それぞれ0・255に変更する
    gray_img[gray_img < threshold] = 0
    gray_img[gray_img >= threshold] = 255

    return gray_img
```
この部分ですね。やってることは至極簡単です。
まずはOpenCVからcvtColorを呼ぶことでモノクロの画像にしてしまいます。
~~諸君、我々は失敗した。~~
![gray.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3163792/70ed3502-a04a-68fa-ee95-57118d73433a.png)

次にしきい値をもとに灰色の部分を白か黒かに分けていきます。
```Python3
    # 1pxごとの値が閾値よりも小さい・大きい場合それぞれ0・255に変更する
    gray_img[gray_img < threshold] = 0
    gray_img[gray_img >= threshold] = 255
```
これも特に難しいことはせず、if分岐して白(255)か黒(0)に置き換えていくだけです。
![binary_img.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3163792/c1753261-bbc8-e35d-f896-07a7a18444dc.png)
この時点ですでにそれっぽくなっていますね。
これで画像の輪郭をはっきりさせることができました。


### 解説[0パディング]
```Python3
# 0パディングを行っている関数
def zero_padding(image):
    # 0パディングをするために元の画像より一回り大きい２次元配列を作成
    # 入力された画像が3次元だった場合
    if 3 == len(image.shape):
        padding_img = np.zeros([image.shape[0] + 2, image.shape[1] + 2, image.shape[2]], dtype=np.uint8)
    # それ以外だった場合
    else:
        padding_img = np.zeros([image.shape[0] + 2, image.shape[1] + 2], dtype=np.uint8)

    # 元画像の縦を展開
    for i, y in enumerate(image):
        i += 1
        # 元画像の横を展開
        for j, x in enumerate(y):
            j += 1
            # 0配列の中に画像を入力
            padding_img[i, j] = x

    return padding_img
```
次に0パディングです。ざっくり言ってしまうと画像の外周部分に余白を追加することでこの後の畳み込みを行う際に、座標[0,0]のピクセルから畳み込むことができるようにしています。
というのも、畳み込み処理では1pxごとに情報を処理していくのですがその際の計算に**最低でも処理するpxの周囲8pxの情報が必要**なのです。
そのため0パディングを行わないと画像外周に近い部分が正常に処理されないという悲しい事態に・・・。
元の画像に0パディング処理を行うとこんな感じ。
![padding.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3163792/169c9d3d-49b4-4894-6093-120768b3b56c.png)


### 解説[線画抽出]
さて、いよいよ本題です。
こちらが実際に畳み込みを行っている部分。
``` Python3
# 線画抽出を行う関数
def line_sampling(image, image_shape, kernel):
    # 線画抽出を行った後の画像の大きさを定義
    sampling_img = np.zeros([image_shape[0], image_shape[1]], dtype=np.uint8)

    # 縦のpx分回す
    for y in range(sampling_img.shape[0]):
        # 横のpx分回す
        for x in range(sampling_img.shape[1]):
            # カーネルと同じ大きさで画像を取得し、値を掛け合わせ、それらの値が入っている配列の各値を合算した結果を配列に格納
            sampling_img[y, x] = np.sum(image[y:y + 3, x:x + 3] * kernel)

    return sampling_img
```
それに加えて重要な個所がこちら。
``` Python3
# ラプラシアンフィルタを定義
LAPLACIAN_KERNEL = np.array([
    [1, 1, 1],
    [1, -8, 1],
    [1, 1, 1]
], dtype=np.uint8)
```
先ほど触れた周囲8pxを指定している箇所になります。
今回は画像の輪郭を浮き上がらせるだけなのでこの形になっています。
画像処理の目的に応じて別の形で作成したり違う形のものを複数使用したり、といったこともあるのですがそれはまたの機会に・・・。
>詳しく知りたい方はラプラシアンフィルタで検索してみてください。
https://www.google.com/search?q=%E3%83%A9%E3%83%97%E3%83%A9%E3%82%B7%E3%82%A2%E3%83%B3%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF&oq=&aqs=chrome.1.35i39i362l8.515625991j0j15&sourceid=chrome&ie=UTF-8

この配列を画像データの1pxごとに重ね合わせ、それぞれのpxで掛けた値を合計し元の配列に格納していくことで**近くに異なる色があるpx**の配列にはより大きな値が、**周囲に同じ色しか存在しないpx**には小さな値が格納されていくはずです。
この工程が**畳み込み**や**Convolution**と呼称されているのです。
最終的に全pxに対して畳み込みを行うとこんな感じになります。
![black.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3163792/a31010b0-aa11-a75e-8405-cd53242477aa.png)
これであとは画像の色調を反転させてから出力すれば変換完了です。

# あとがき
おつかれさまでした。
今回はさらっと触れただけでしたが、この技術はAIを使った画像処理の基礎中の基礎だったりします。
気になる方はコンボリューションニューラルネットワーク(CNN)等の単語で検索してみると楽しいかもしれません。
あ、初投稿なので読みにくい箇所等あると思いますがご了承ください。
次の記事も遠くないうちに書きたい・・・。

記事作成：Y&Q