LoginSignup
2
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-22

概要

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

使用する木の種類

4種類の木を対象とした。

  • クスノキ
    IMG_4642のコピー.JPG
  • モッコク
    IMG_4611のコピー.JPG
  • マツ
    IMG_4784のコピー.png
  • サクラ
    IMG_4560のコピー.JPG

用意した画像データ

・クスノキ
3本の木から異なる角度で撮影した写真計6枚
・モッコク
3本の木から異なる角度で撮影した写真計5枚
・マツ
3本の木から異なる角度で撮影した写真計8枚
・サクラ
3本の木から異なる角度で撮影した写真計6枚

H,Sヒストグラム比較

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
さくらだけは他の3種と異なりH=100~130のあたりに第2のピークがみられる。
Hist_S.png
4種類ともピークの現れ方がそれぞれ異なる。

H成分の0~30について拡大してみる。
Hist_H_0_30.png
マツは0~10の範囲が相対的に見て多く、モッコクは13~17あたりが多い。

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

前処理

  • 画像の分割

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

(例)クスノキ1本の写真から切り取った画像
trimmed_IMG_4642_0000のコピー.png

  • HS成分の取得

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

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

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)
  • ヒストグラムの計算
  • 正規化

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

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枚の画像を機械学習で分類したのち、それらの分類結果から元の木の種類を多数決で決めるといった方法がある。
さらに、実用においては学習に使用していない木の画像データを分類することから、その場合正解率がどの程度低下するのかも評価してみたい。
ただ前述した通り多数決による最終結果が合っていれば良いので、正解率があまり高くなかったとしても実用レベルに持っていくことはできるんじゃないかと思った。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2