木の画像からHSの度数を取得し機械学習で分類する

# 概要
木の幹の画像からH,S(色相、彩度)成分を取得、さらにその度数を計算しそれらの値を特徴量として機械学習を行い木の種類を分類する。
# 使用する木の種類
4種類の木を対象とした。

- クスノキ<br>
![IMG_4642のコピー.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483780/14df9b39-e9b5-bd8f-3699-d4d32ddf1a91.jpeg)
- モッコク<br>
![IMG_4611のコピー.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483780/bedf4854-7dd6-0b1a-0a79-c568ed3fd099.jpeg)
- マツ<br>
![IMG_4784のコピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483780/1ab52c73-2cd6-6b2c-2520-21dc23e6adaa.png)
- サクラ<br>
![IMG_4560のコピー.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483780/689e931f-e86b-a4ad-fc8e-b799ceba20ff.jpeg)

# 用意した画像データ
・クスノキ
3本の木から異なる角度で撮影した写真計6枚
・モッコク
3本の木から異なる角度で撮影した写真計5枚
・マツ
3本の木から異なる角度で撮影した写真計8枚
・サクラ
3本の木から異なる角度で撮影した写真計6枚
# H,Sヒストグラム比較

```python:Plot_HS_histogram.ipynb
import cv2
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
import sys,os
import re
from tqdm.notebook import tqdm

# 画像保存先
dir_path = '/Users/user/work/2020_ai/tree_classifying/original_images/'
sub_dir_path = ['Kusu/','Mok/','Matsu/','Sakura/']

imgs = []

for sub_dir in tqdm(sub_dir_path):
    img_perclass = []
    for f in os.listdir(dir_path+sub_dir):
        if os.path.isfile(os.path.join(dir_path+sub_dir, f)):
            if re.search(r'(\.bmp|\.BMP|\.jpg|\.JPG|\.png|\.PNG|\.jpeg|\.JPEG)$', f):
                img_bgr = cv2.imread(dir_path+sub_dir+f)
                img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
                if img_perclass == []:
                    img_perclass = np.array(img_hsv.reshape(1,-1,3)[0])
                else:
                    img_perclass = np.concatenate([img_perclass, img_hsv.reshape(1,-1,3)[0]])
    imgs.append(img_perclass)

i = 0
for img_perclass in imgs_1d:
    # ヒストグラムを計算し、画素数で割る
    hist_h = np.histogram(img_perclass[:,0], bins=180, range=(0,180))[0] / img_perclass.shape[0]
    plt.plot(range(0,180), hist_h, label=sub_dir_path[i])
    i+=1
plt.title('Histogram of H (scaled)')
plt.legend()
plt.show()


i = 0
for img_perclass in imgs_1d:
    hist_s = np.histogram(img_perclass[:,1], bins=256, range=(0,256))[0] / img_perclass.shape[0]
    plt.plot(range(0,256), hist_s, label=sub_dir_path[i])    
    i+=1
plt.title('Histogram of S (scaled)')
plt.legend()
plt.show()
```
![Hist_H.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483780/15739a9d-0bff-0ae3-be0c-2eafa79da42e.png)
さくらだけは他の３種と異なりH=100~130のあたりに第２のピークがみられる。
![Hist_S.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483780/ccc28d90-49e0-f993-0f0f-38b6a5f6b7f6.png)
4種類ともピークの現れ方がそれぞれ異なる。

H成分の0~30について拡大してみる。
![Hist_H_0_30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483780/971c95b7-ea9f-46ef-e6dc-3d07f0d12586.png)
マツは0~10の範囲が相対的に見て多く、モッコクは13~17あたりが多い。

こうした特徴を利用して分類できるか試してみた。


# 前処理

- 画像の分割

木の画像はそれぞれ数枚しか用意できていないので、データ数を増やすため1枚の画像を400x400の大きさに切り取る。
切り取る大きさは、樹皮の模様（主に溝）が切り取った1枚の画像にはっきりと映るくらいの大きさで決めた。

(例)クスノキ1本の写真から切り取った画像
![trimmed_IMG_4642_0000のコピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483780/ce1113f3-f100-cbc4-e5a3-48a7e257ce69.png)

- HS成分の取得

OpenCVでHSVに変換した値は極座標系の値なので、直交座標系に変換する。

以上の処理を1つのプログラムに記述し、直交座標系に変換したHSVのデータを木の種類ごとに作成、numpyのバイナリ形式で保存しておく。

```python:Make_HSV_data.ipynb
import cv2
import numpy as np
import sys,os
import pandas as pd
import re

from tqdm.notebook import tqdm

# 画像ファイルパス
dir_path = '/Users/user/work/2020_ai/tree_classifying/Sakura/aggr/'
# 保存先
save_path = '/Users/user/work/2020_ai/tree_classifying/'
# 保存ファイル名
save_filename = 'Sakura_hsv'

file_list = [] #読み込む画像ファイルのリスト
data_list = [] #list型の画像データ

# ディレクトリ内の画像を順次読み込み
for f in tqdm(os.listdir(dir_path)):
    if os.path.isfile(os.path.join(dir_path, f)):
        if re.search(r'(\.bmp|\.BMP|\.jpg|\.JPG|\.png|\.PNG|\.jpeg|\.JPEG)$', f):
            file_list.append(f)
            img_bgr = cv2.imread(dir_path + f)
            img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
            data_list.append(img_hsv)

data = np.array(data_list)
print(data.shape)
# (780, 400, 400, 3)

data_xyz = np.zeros(data.shape) # x,yに変換したデータを格納

data_xyz[:, :, :, 0] = data[:, :, :, 1] * np.cos(2*np.pi * data[:, :, :, 0] / 180)
data_xyz[:, :, :, 1] = data[:, :, :, 1] * np.sin(2*np.pi * data[:, :, :, 0] / 180)
data_xyz[:, :, :, 2] = data[:, :, :, 2]

np.savez_compressed(save_path + save_filename, data_xyz)
```

- ヒストグラムの計算
- 正規化

計算したヒストグラムは元データの数に依存した値になっているため、データ数で割ることで正規化する。

```python
import cv2
import numpy as np
import sys,os
from matplotlib import pyplot as plt
import pandas as pd
import re
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier as RFC
from sklearn.preprocessing import StandardScaler
from tqdm.notebook import tqdm

npz_path = '/Users/user/work/2020_ai/tree_classifying/npz_datas/'

data = []
label = []

# クラスごとに分けられたファイル読み込み
i = 0
for f in tqdm(os.listdir(npz_path)):
    if os.path.isfile(os.path.join(npz_path, f)):
        if re.search(r'\.npz$', f):
            img_xyz = np.load(npz_path + f)['arr_0']
            # ヒストグラムを計算して配列に格納
            for j in range(img_xyz.shape[0]):
                tmp = np.histogram(img_xyz[j,:,:,0], bins=511, range=(-255,256))[0]
                tmp = np.append(tmp, np.histogram(img_xyz[j,:,:,1], bins=511, range=(-255,256))[0])
                tmp = tmp / img_xyz.shape[0]  # 計算したヒストグラムを正規化
                data.append(tmp)
                label.append(i)
            i+=1

data = np.array(data)
print(f'shape of data: {data.shape}')
# shape of data: (2664, 1022)
```

# 作成したデータセット
400x400の画像データのHSヒストグラム
行数：2664 (クスノキ900枚+モッコク585枚+マツ399枚+サクラ780枚)
列数：1022 (X511諧調+Y511諧調)

# モデルの作成と評価
SVMとランダムフォレストでそれぞれモデルを作成し、正解率を比較する。
ハイパーパラメータはグリッドサーチで決定する。

```Python
# 学習データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(data, label, test_size=0.3, stratify=label, random_state=10)

# グリッドサーチ
params_svc = {'C': [1, 10, 100, 1000, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9], 'gamma': [1e-12, 1e-11, 1e-10, 1e-9, 1e-8, 1e-7, 1e-6, 1e-5, 1e-4, 1e-3]}
params_rfc = {'n_estimators': [10, 50, 100, 200]}

cv_svc = GridSearchCV(SVC(), params_svc, cv=5)
cv_rfc = GridSearchCV(RFC(), params_rfc, cv=5)
cv_svc.fit(X_train, y_train)
cv_rfc.fit(X_train, y_train)
res_svc = pd.DataFrame(cv_svc.cv_results_)
res_rfc = pd.DataFrame(cv_rfc.cv_results_)
```

グリッドサーチの結果は以下の通り。

```python
res_svc[res_svc['rank_test_score'] == 1]

	mean_fit_time	std_fit_time	mean_score_time	std_score_time	param_C	param_gamma	params	split0_test_score	split1_test_score	split2_test_score	split3_test_score	split4_test_score	mean_test_score	std_test_score	rank_test_score
27	0.780955	0.087561	0.165345	0.006702	100	1e-05	{'C': 100, 'gamma': 1e-05}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
36	0.756443	0.008853	0.161597	0.005326	1000	1e-06	{'C': 1000, 'gamma': 1e-06}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
37	0.510188	0.008355	0.088438	0.003934	1000	1e-05	{'C': 1000, 'gamma': 1e-05}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
45	0.805055	0.096169	0.163324	0.005390	10000	1e-07	{'C': 10000.0, 'gamma': 1e-07}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
46	0.544133	0.013255	0.090187	0.004100	10000	1e-06	{'C': 10000.0, 'gamma': 1e-06}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
47	0.505472	0.010267	0.088975	0.001913	10000	1e-05	{'C': 10000.0, 'gamma': 1e-05}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
54	0.771960	0.020971	0.158751	0.005042	100000	1e-08	{'C': 100000.0, 'gamma': 1e-08}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
55	0.553612	0.007341	0.087472	0.002180	100000	1e-07	{'C': 100000.0, 'gamma': 1e-07}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
56	0.574675	0.035277	0.089660	0.003910	100000	1e-06	{'C': 100000.0, 'gamma': 1e-06}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
57	0.530672	0.029622	0.089334	0.002266	100000	1e-05	{'C': 100000.0, 'gamma': 1e-05}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
64	0.540808	0.044080	0.075787	0.003550	1e+06	1e-08	{'C': 1000000.0, 'gamma': 1e-08}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
65	0.526612	0.011834	0.081723	0.004195	1e+06	1e-07	{'C': 1000000.0, 'gamma': 1e-07}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
66	0.518916	0.016057	0.083340	0.003087	1e+06	1e-06	{'C': 1000000.0, 'gamma': 1e-06}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
67	0.493541	0.010324	0.085467	0.002011	1e+06	1e-05	{'C': 1000000.0, 'gamma': 1e-05}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
73	0.529144	0.018930	0.071493	0.002236	1e+07	1e-09	{'C': 10000000.0, 'gamma': 1e-09}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
74	0.595466	0.032245	0.077605	0.015454	1e+07	1e-08	{'C': 10000000.0, 'gamma': 1e-08}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
75	0.620033	0.027268	0.086070	0.004056	1e+07	1e-07	{'C': 10000000.0, 'gamma': 1e-07}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
76	0.621780	0.040632	0.091296	0.004463	1e+07	1e-06	{'C': 10000000.0, 'gamma': 1e-06}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
77	0.546178	0.034436	0.088642	0.003792	1e+07	1e-05	{'C': 10000000.0, 'gamma': 1e-05}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
84	0.593776	0.022242	0.066398	0.003423	1e+08	1e-08	{'C': 100000000.0, 'gamma': 1e-08}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
85	0.531727	0.008548	0.084426	0.003201	1e+08	1e-07	{'C': 100000000.0, 'gamma': 1e-07}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
86	0.601080	0.068931	0.090125	0.003586	1e+08	1e-06	{'C': 100000000.0, 'gamma': 1e-06}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
87	0.522254	0.050433	0.085424	0.002590	1e+08	1e-05	{'C': 100000000.0, 'gamma': 1e-05}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
93	0.491134	0.026989	0.062112	0.003813	1e+09	1e-09	{'C': 1000000000.0, 'gamma': 1e-09}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
94	0.566583	0.026231	0.070406	0.001855	1e+09	1e-08	{'C': 1000000000.0, 'gamma': 1e-08}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
95	0.652987	0.036169	0.088767	0.008282	1e+09	1e-07	{'C': 1000000000.0, 'gamma': 1e-07}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
96	0.633960	0.029051	0.088672	0.001232	1e+09	1e-06	{'C': 1000000000.0, 'gamma': 1e-06}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
97	0.577285	0.035824	0.090383	0.004738	1e+09	1e-05	{'C': 1000000000.0, 'gamma': 1e-05}	1.0	1.0	0.997319	1.0	1.0	0.999464	0.001072	1
```
```python
res_rfc.sort_values('rank_test_score').head()

	mean_fit_time	std_fit_time	mean_score_time	std_score_time	param_n_estimators	params	split0_test_score	split1_test_score	split2_test_score	split3_test_score	split4_test_score	mean_test_score	std_test_score	rank_test_score
3	1.596434	0.008670	0.023556	0.000759	200	{'n_estimators': 200}	0.959786	0.975871	0.962466	0.994638	0.959677	0.970488	0.013476	1
1	0.404198	0.008753	0.007282	0.000462	50	{'n_estimators': 50}	0.959786	0.975871	0.959786	0.989276	0.959677	0.968879	0.011958	2
2	0.786645	0.009857	0.012463	0.000741	100	{'n_estimators': 100}	0.959786	0.978552	0.957105	0.994638	0.946237	0.967263	0.017193	3
0	0.089134	0.007619	0.003566	0.001785	10	{'n_estimators': 10}	0.970509	0.932976	0.943700	0.962466	0.913978	0.944726	0.020318	4
```
・SVM
ランク1のスコアが28個、標準偏差も一緒だった。
以下3種類の値を抜粋して評価してみる。
(1) {C: 1e5, gamma: 1e-8}
(2) {C: 1e4, gamma: 1e-5}
(3) {C: 1e9, gamma: 1e-9}

・ランダムフォレスト
以下2つのパラメータで評価する。
(1) n=200　平均スコア: 0.970488　標準偏差: 0.013476
(2) n=50　平均スコア: 0.968879　標準偏差: 0.011958

```python
# モデル作成
models = {
    'SVM': SVC(C=1e5, gamma=1e-8),
    'RFC': RFC(n_estimators=200),
}
scores = {}
for model_name, model in models.items():
    model.fit(X_train, y_train)
    scores[(model_name, 'train_score')] = model.score(X_train, y_train)
    scores[(model_name, 'test_score')] = model.score(X_test, y_test)

# 結果表示
pd.Series(scores).unstack()

	test_score	train_score
RFC	0.9725	1.0
SVM	1.0000	1.0
```
```python
# モデル作成
models = {
    'SVM': SVC(C=1e4, gamma=1e-5),
    'RFC': RFC(n_estimators=50),
}
scores = {}
for model_name, model in models.items():
    model.fit(X_train, y_train)
    scores[(model_name, 'train_score')] = model.score(X_train, y_train)
    scores[(model_name, 'test_score')] = model.score(X_test, y_test)

# 結果表示
pd.Series(scores).unstack()

	test_score	train_score
RFC	0.96875	1.0
SVM	1.00000	1.0
```
```python
# モデル作成
models = {
    'SVM': SVC(C=1e9, gamma=1e-9),
    'RFC': RFC(n_estimators=50),
}
scores = {}
for model_name, model in models.items():
    model.fit(X_train, y_train)
    scores[(model_name, 'train_score')] = model.score(X_train, y_train)
    scores[(model_name, 'test_score')] = model.score(X_test, y_test)

# 結果表示
pd.Series(scores).unstack()

	test_score	train_score
RFC	0.96125	1.0
SVM	1.00000	1.0
```
## 正解率
- SVM: 100%
- ランダムフォレスト: 96~97%

ヒストグラムをそのまま特徴量に使っただけなのだが、ほぼ100%の正解率となった。
実用化を考えた場合、カメラで撮影した写真から今回のように小さい画像に切り取り、切り取った1枚1枚の画像を機械学習で分類したのち、それらの分類結果から元の木の種類を多数決で決めるといった方法がある。
さらに、実用においては学習に使用していない木の画像データを分類することから、その場合正解率がどの程度低下するのかも評価してみたい。
ただ前述した通り多数決による最終結果が合っていれば良いので、正解率があまり高くなかったとしても実用レベルに持っていくことはできるんじゃないかと思った。