【機械学習】色の量子化を解説

<br>&emsp;こんにちは。安藤 勇輝([@holiholiday](https://twitter.com/holiholiday1173))です。

&emsp;今日はOpenCVをつかって画像の量子化を行う手法を解説します。この記事では量子化のアルゴリズムとしてK-means法とよばれる、機械学習手法を用いています。

## OpenCVとは？
&emsp;OpenCVとはOSSの画像処理ライブラリです。
&emsp;C/C++,Java,Python,MATLABで使用することができます。今回はPythonを使って解説します。

## 量子化とは？
&emsp;さっそく本題に入りたいのですが、量子化という言葉になじみがない方も多いと思うので、量子化の説明からさせてください。

&emsp;量子化とは少数などの数値データを、整数などの離散値で近似することです。  
&emsp;今回は０〜２５５の８ビットで表現される整数の画素値を量子化するので少数ではありませんが、整数でも量子化することは可能です。

&emsp;先に結果から見せてしまうと、OpenCVの量子化をつかうと以下のようなことができます。

- 左：入力画像　右：量子化画像<br>
<div align="center">
![Parrots_with_k5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/448983/24dda841-3b34-efd2-9eba-1f7137514775.png)
</div>
　今回の処理では、５色だけの画像に変換しています(処理後の色の数も決めることができます)。<br>
　画像に対して量子化を行うと、画像内で使われている色の種類の数が減り、のっぺりした塗り絵のような画像をつくることができます。<br>
<br>
　実際に行っていることのイメージを持ってもらうために、K-means法の説明をします。
<br>

## K-means法とは？
&emsp;K-means法とは、データのグループ分けを行うアルゴリズムです。このグループ分けのことを、機械学習分野ではクラスタリングと呼びます。

&emsp;今回グループ分けを行ったのは、画像の画素値です。カラー画像は１ピクセルごとに色を表す３つの数値(画素値)を持ち、この数値１つずつが青、緑、赤の画素値をあらわしています。これら３つの値を合わせて、ピクセル１つ１つの色を表現しています。


&emsp;処理の手順としては以下のとおりです。

1. ピクセルあたりの画素値を１つのデータとみなし、画像１枚分のデータをグループ分けします。
2. このグループ分けによってできたグループごとに、グループ内の平均値をとります。
3. グループに存在しているデータに、グループ内の平均値をわりあて、画像内の色の種類を減らします。<br>

&emsp;このような処理を行うことで、画像内にたくさんあった色の種類が、グループ数と同じだけになります。
<br><br>

## コードの解説
&emsp;処理の手順は説明し終えたので、ここからはコードの解説をしていきます。
&emsp;使用したコードとしては、以下のとおりです。

```python
import numpy as np
import cv2


if __name__ == "__main__":
    # 画像の読み込み
    img = cv2.imread('処理したい画像のパス')
    # 画像をそのままk-meansにかけることはできないので、shapeを(ピクセル数, 3(BGR))に変換
    Z = img.reshape((-1, 3))
    # np.float32型に変換
    Z = np.float32(Z)

    # k-meansの終了条件
    # デフォルト値を使用
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
    # 分割後のグループの数
    K = 5
    # 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()]
    # 元の画像サイズにもどす
    res2 = res.reshape((img.shape))

    # 画像の保存
    cv2.imwrite("保存先のパス", res2)
```

### 画像の読み込みからデータの変換
```python
# 画像の読み込み
img = cv2.imread('処理したい画像のパス')
# 画像をそのままk-meansにかけることはできないので、shapeを(ピクセル数, 3(BGR))に変換
Z = img.reshape((-1, 3))
# np.float32型に変換
Z = np.float32(Z)
```
&emsp;OpenCVをつかって画像の読み込みを行い、データの変換を行います。<br>
&emsp;読み込んだ画像データをそのままK-meansに入力することはできないので、画像の変換が必要になります。<br>
&emsp;入力するデータはnp.floatの２次元配列で、(データ数, データの次元数)と変換する必要があります。データの次元数は、今回の場合で言うとBGRの３次元になります。
### K-meansの設定と実行
```python
# k-meansの終了条件
# デフォルト値を使用
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
# 分割後のグループの数
K = 5
# k-means処理
_, label, center = cv2.kmeans(
    Z, K, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
```
&emsp;K-meansを実行するためには、設定が必要なパラメータがいくつかあります。

&emsp;まずはじめにcriteriaという変数で、K-means処理の終了条件を決定します。詳しくは説明しませんが、K-means法にはイテレーション回数としきい値が存在しており、それらをcriteriaで決めることができます。

&emsp;次にKという変数で、分割後のグループ数を決めます。グループ数は処理後の色の数と一致するので、色の数を少なくしたければ、Kを小さくしてください。

&emsp;さいごに、cv2.kmeansでK-means処理を実行しています。うしろから１、２番めのパラメータは初期値の設定です。
&emsp;今回はすべて参考ページのデフォルト値をつかっています。<br>
&emsp;cv2.kmeansが返す変数は３つあります。２、３番めの変数はNumpy配列です。

&emsp;labelという変数には、データが何番目のグループに属しているのかを示すインデックスが格納されており、このshapeは(データ数, 1(インデックスのみ))となっています。

&emsp;centerという変数には、グループごとのデータの平均値が入っています。そのため、この変数のshapeは(グループ数,  データの次元数(今回は３))となります。
### 処理したデータの変換から保存まで
```python
# np.uint8型に変換
center = np.uint8(center)
# グループごとにグループ内平均値を割り当て
res = center[label.flatten()]
# 元の画像サイズにもどす
res2 = res.reshape((img.shape))

# 画像の保存
cv2.imwrite("保存先のパス", res2)
```
&emsp;ここから処理したデータを元の画像のShapeに変換していきます。<br>
&emsp;まずfloat型のcenterをuin8型に変換します。
&emsp;つぎの処理で、１つのグループに属するピクセルデータすべてに、そのグループ内平均値をわりあてます。つまり、グループ１に属するピクセルにはグループ１の平均値を、グループ２に属するピクセルにはグループ２の平均値を割り当てていくことになります。<br>
&emsp;さいごに元の画像サイズに変換し、画像の保存をして終了になります。<br>
<br><br>

## グループ数ごとの処理結果
&emsp;分割後のグループ数によって処理結果は大きく変わってきます。<br>
&emsp;グループ数ごとの処理結果を載せておくので、パラメータ設定の参考にしてください。<br>

- K＝3
<div align="center">
![Parrots_k3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/448983/9f37262d-040c-d3b5-1bd6-af5440213ab7.png)
</div>
- K＝5
<div align="center">
![Parrots_k5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/448983/7b4e652a-5818-4c1c-06e8-9fcd1bf29139.png)
</div>
- K＝7
<div align="center">
![Parrots_k7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/448983/e8d4f924-54bf-20c6-d133-a09bc93e6e0a.png)
</div>
- K＝9
<div align="center">
![Parrots_k9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/448983/3e5fea5e-b80e-61f8-5f13-7ad55ba8cace.png)
</div><br><br>

&emsp;いかがでしたか?
&emsp;こんなふうに解説記事を上げているので、興味があれば他の記事も見ていってください。<br>
&emsp;ご閲覧どうもありがとうございました。<br>

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

- OpenCVのK-meansチュートリアル
http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_ml/py_kmeans/py_kmeans_opencv/py_kmeans_opencv.htm