【機械学習】画像のパス化っぽいことをしてみた


こんにちは。安藤 勇輝([@holiholiday1173](https://twitter.com/holiholiday1173))です。

今回の記事では、前回の記事で紹介したK-means法とよばれるクラスタリング手法の一種を用いて、Illustratorなどで使うことの多い、画像の[パス化(ベクタ化)](https://school.dhw.co.jp/word/design/vector.html)っぽいことをやってみました。

## 概要
　前回の記事でやったことは以下のとおりです。

1. 画像の1ピクセルごとの画素値を、K-meansを使ってクラスタリング

2. クラスタごとにクラスタ内平均画素値を割り当て、色の数を減らして描画


前回記事→[【機械学習】色の量子化を解説](https://qiita.com/BigTheAndo/items/4e8046998be9627ca85d)


　実際に処理を行った結果がこちら。
（左：処理前、右：処理後）
![Parrots_with_k9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/448983/e8630bfb-a67b-c768-b1fe-0d23d4c6b51c.png)

　クラスタ数を９としてK-means処理を行った結果が右の画像になります。クラスタ数＝色の数なので、処理後の画像は９色のみで描画されています。
　少しのっぺりした塗り絵のような画像になっていますね。


　前回の記事では画素値のみをK-meansの入力として使用していたため、色が似ていれば画像内のすこし離れた領域にあるピクセルでも同じクラスタに分けられてしまいました。

　なので、画素値にくわえてピクセル座標の情報も合わせてクラスタリングしてあげれば、パス化したベクタっぽい画像が得られるのではないかと思いいたり、やってみました。

## 結果
　使用したコードはこちらです。

```python
import os
import argparse
import numpy as np
import cv2


if __name__ == "__main__":
    # 引数でパスを指定
    parser = argparse.ArgumentParser()
    parser.add_argument('--img', '-i', help='処理したい画像のパス')
    parser.add_argument('--output', '-o', help='結果画像を出力するディレクトリ')
    args = parser.parse_args()

    # 画像の読み込み
    img = cv2.imread(args.img)

    # 画像をそのままk-meansにかけることはできないので、shapeを(ピクセル数, 3(BGR))に変換
    Z1 = img.reshape((-1, 3))
    
    """
    ピクセルごとのxy座標を取得
    """
    # 画像の縦横幅
    h, w = img.shape[:-1]
    # 画像のxy座標を2次元リスト化
    Z2 = [[i, j] for i in range(h) for j in range(w)]
    
    # RGB + xy座標を連結
    Z = np.hstack((Z1, Z2))
    # np.float32型に変換
    Z = np.float32(Z)

    # k-meansの終了条件
    # デフォルト値を使用
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
    # 分割後のグループの数
    for K in range(5, 21, 3):
        # K = 10
        # k-means処理
        _, label, center = cv2.kmeans(
            Z, K, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)

        # np.uint8型に変換
        center = np.uint8(center)
        # グループごとにグループ内平均値を割り当て
        res = center[label.flatten()]
        res = res[:, :-2]
        # 元の画像サイズにもどす
        res2 = res.reshape((img.shape))

        output_path = os.path.join(args.output, 'Parrots_K{:02}.png'.format(K))
        # 画像の保存
        cv2.imwrite(output_path, res2)
```


　前回のコードに追加した主な処理はこちらです。

```python
"""
ピクセルごとのxy座標を取得
"""
# 画像の縦横幅
h, w = img.shape[:-1]
# 画像のxy座標を2次元リスト化
Z2 = [[i, j] for i in range(h) for j in range(w)]
```
　この部分の処理で、ピクセルごとのxy座標を取得します。ここで取得できたxy座標を、画素値のリストと結合することで、RGB＋xy座標を連結したリストを生成します。
　まずimgの縦幅(h)、横幅(w)を取得します。
　Z2 = ~ の行で[[0, 0], [0, 1], [0, 2] ... ]　となるxy座標リストを作っています。

```python
# RGB + xy座標を連結
Z = np.hstack((Z1, Z2))
```
　この行で画素値とxy座標のリストを結合し、K-meansに入力するデータの完成です。

　
　以下がその結果です。
元画像
![Parrots.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/448983/15ece354-349d-59b9-a1f3-ec8af946264b.png)


K = 5
![Parrots_K05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/448983/5ecb84be-2aff-b3c0-502b-0699df2bfbe5.png)

K = 8
![Parrots_K08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/448983/ba817f11-c59c-0d85-6186-8b854850545f.png)

K = 11
![Parrots_K11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/448983/ba5b6bc8-bbde-7d24-dfb5-6e074c039ecb.png)

K = 14
![Parrots_K14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/448983/ad132224-ba79-028e-6710-5ee7403c1acb.png)

K = 17
![Parrots_K17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/448983/a646cdd9-d147-f45e-774e-3d38061947fb.png)

K = 20
![Parrots_K20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/448983/46901f24-454d-fc93-eaf7-5330ef899585.png)

　画素値だけでなく、位置も考慮してクラスタ分けしているので、パス化っぽいことができていますね！


### 参考
- <b>画像</b>
<br>今回使用させてもらっている画像データは以下のリンクからダウンロードできます。
<br>http://www.ess.ic.kanagawa-it.ac.jp/app_images_j.html