概要
木の幹の画像からH,S(色相、彩度)成分を取得、さらにその度数を計算しそれらの値を特徴量として機械学習を行い木の種類を分類する。
使用する木の種類
4種類の木を対象とした。
用意した画像データ
・クスノキ
3本の木から異なる角度で撮影した写真計6枚
・モッコク
3本の木から異なる角度で撮影した写真計5枚
・マツ
3本の木から異なる角度で撮影した写真計8枚
・サクラ
3本の木から異なる角度で撮影した写真計6枚
H,Sヒストグラム比較
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()
さくらだけは他の3種と異なりH=100~130のあたりに第2のピークがみられる。
4種類ともピークの現れ方がそれぞれ異なる。
H成分の0~30について拡大してみる。
マツは0~10の範囲が相対的に見て多く、モッコクは13~17あたりが多い。
こうした特徴を利用して分類できるか試してみた。
前処理
- 画像の分割
木の画像はそれぞれ数枚しか用意できていないので、データ数を増やすため1枚の画像を400x400の大きさに切り取る。
切り取る大きさは、樹皮の模様(主に溝)が切り取った1枚の画像にはっきりと映るくらいの大きさで決めた。
- HS成分の取得
OpenCVでHSVに変換した値は極座標系の値なので、直交座標系に変換する。
以上の処理を1つのプログラムに記述し、直交座標系に変換したHSVのデータを木の種類ごとに作成、numpyのバイナリ形式で保存しておく。
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)
- ヒストグラムの計算
- 正規化
計算したヒストグラムは元データの数に依存した値になっているため、データ数で割ることで正規化する。
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とランダムフォレストでそれぞれモデルを作成し、正解率を比較する。
ハイパーパラメータはグリッドサーチで決定する。
# 学習データとテストデータに分割
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_)
グリッドサーチの結果は以下の通り。
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
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
# モデル作成
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
# モデル作成
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
# モデル作成
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枚の画像を機械学習で分類したのち、それらの分類結果から元の木の種類を多数決で決めるといった方法がある。
さらに、実用においては学習に使用していない木の画像データを分類することから、その場合正解率がどの程度低下するのかも評価してみたい。
ただ前述した通り多数決による最終結果が合っていれば良いので、正解率があまり高くなかったとしても実用レベルに持っていくことはできるんじゃないかと思った。