[Python]簡単なニューラルネットワークで物体検出をやってみた
はじめに
先輩方もすなるブログといふものを新入社員もしてみむとてするなり。
始めまして。最近勉強したことをブログという形でまとめてみました。
今回のテーマは「画像認識の入門」です。
やることは「画像処理100本ノック」様(url,https://github.com/minido/Gasyori100knock-1) の提供する問題集より抜粋して、イモリの顔の画像を機械学習で学習させてから別のイモリの画像から顔を物体検出させます。本記事で使用している画像とコードはMITライセンスの下で提供されています。著作権 (c) 2019 Yoshito Nagaoka
今回はかなり基礎から始めるという事でディープラーニングが流行る前からの手法である、画像の端から一定のサイズの矩形を動かしていくスライディングウィンドウによる手法を使いました。
物体検出の流れとしては
- 画像の左上からスライディングウィンドウを行います。
- 各座標において矩形のサイズは数種類設定します。
- 矩形ごとに画像を切り抜いて、特徴抽出を行います。(今回はHOG特徴量を利用する)
- 識別器としてニューラルネットワークを用いています。
データ読み込み
まずは必要なライブラリをインポートします。
"""
Python 3.10.12
numpy==1.25.2
opencv-python==4.8.0.76
matplotlib==3.7.1
requests==2.31.0
"""
import numpy as np
import requests
import shutil
import matplotlib.pyplot as plt
import cv2
次に画像をダウンロードします。手動で直接Githubからダウンロードしても良いのですが今回はなるべくコーディングで自動化してみたくウェブスクレイピングを行いました。ウェブスクレイピングとはインターネット上にある情報から特定の情報を抽出する技術であり、このコードでは100本ノックのgithubに自動でアクセスしてニューラルネットワークの学習用画像とテスト用画像をダウンロードしています。なおウェブスクレイピングは場合によっては著作権に抵触する恐れがある為、ライセンスフリーのモノのみを利用する様にしましょう。
import requests
import shutil
import cv2
import matplotlib.pyplot as plt
for filename in ["imori_1.jpg","imori_many.jpg"]:# 保存先ファイル名
# 画像のURL
url = "https://raw.githubusercontent.com/minido/Gasyori100knock-1/master/Question_91_100/"+filename+"?raw=true"
# URLから画像をダウンロード
response = requests.get(url, stream=True)
if response.status_code == 200:
with open(filename, 'wb') as f:
response.raw.decode_content = True
shutil.copyfileobj(response.raw, f)
print("画像をダウンロードしました:", filename)
plt.figure(figsize=(3,2))
img=plt.imread(filename)
plt.imshow(img)
plt.show()
else:
print("画像をダウンロードできませんでした。")
学習用画像
テスト用画像
ニューラルネットワークの学習には学習用画像のイモリの顔から教師あり学習をしていきます。下の画像みたいに正解の場所を緑枠で示します。このような物体を囲む矩形のことをBounding-box(バウンディングボックス)と呼ぶ。
img = plt.imread("imori_1.jpg")
cv2.rectangle(img, (47, 41), (129, 103), (0,255,0), 1)
plt.figure(figsize=(3,2))
plt.imshow(img)```python
img = plt.imread("imori_1.jpg")
cv2.rectangle(img, (47, 41), (129, 103), (0,255,0), 1)
plt.figure(figsize=(3,2))
plt.imshow(img)
畳み込みニューラルネットワーク(CNN)
ニューラルネットワークとは機械学習と人工知能の分野で広く使われているモデルです。脳の神経細胞(ニューロン)を模しており、データから学習を行い未知のデータに対して予測や分類を行うことができます。
ニューラルネットワークの基本構成
- ニューロン (Neuron)
ニューラルネットワークの基本単位です。各ニューロンは入力を受けとり、重み付き和を計算し、活性化関数により出力を生成します。 - 層 (Layer)
ニューラルネットワークは複数の層によって構成されています。各層は複数のニューロンを持ち、前の層の出力を入力として受け取ります。- 入力層 (Input Layer)
最初の層に当たります。データをネットワークに入力する層です。今回は入力画像のHOG特徴量を入力としています。 - 隠れ層 (Hidden Layer)
中間層(intermediate layer)とも。入力層と出力層の中間にある層で、今回は2層の中間層で構成されています。 - 出力層 (Output Layer)
最終的な予測や分類の結果を出力する層です。
- 入力層 (Input Layer)
- 重み (Weight)
各ニューロンの接合の強さを示すパラメータです。重みは学習過程で調整されます。 - バイアス (Bias)
ニューロンの出力を調整するためのパラメータです。 - 活性化関数 (Activation Function)
ニューロンの出力を非線形に変換する関数です。非線形性を導入することで、複雑なパターンの学習が可能になります。代表的な活性化関数にはシグモイド関数、ReLU(Rectified Linear Unit)、ソフトマックス関数などがあります。今回はシグモイド関数を使います。
動作原理
- フォワードプロパゲーション (Forward Propagation)
入力層から出力層に向かって順次伝搬します。各層のニューロンは前の層のニューロンの出力を入力として受け取り、重み付き和を計算し、活性化関数(シグモイド関数)を適用して次の層に出力を渡します。 - 損失関数 (Loss Function)
モデルの予測と実際の値の差を予測するための関数です。損失関数の値を最小化することが学習の目的です。代表的な損失関数には平均二乗誤差(MSE)、クロスエントロピー損失などがあります。今回は二乗誤差を損失関数として計算しています。 - バックプロパゲーション (Back Propagation)
フォワードプロパゲーションの結果を基に損失関数の勾配を計算し、ネットワークの重みとバイアスを調整します。具体的には誤差を出力層から入力層に向かって逆伝搬させ、各層の重みとバイアスを更新します。これにより、ネットワークが学習し、より良い予測ができるようになります。 - 最適化アルゴリズム (Optimization Algorithm)
重みとバイアスを更新する方法です。代表的な最適化アルゴリズムには、確率的勾配降下法(SGD)、Adam、PMSpropなどがあります。今回はシンプルな勾配降下法を利用しています。
class NN:
def __init__(self, ind=2, w=64, w2=64, outd=1, lr=0.1):#ニューラルネットワークの構成
self.w1 = np.random.normal(0, 1, [ind, w]) #中間層1層目重み
self.b1 = np.random.normal(0, 1, [w]) #中間層1層目バイアス
self.w2 = np.random.normal(0, 1, [w, w2]) #中間層2層目重み
self.b2 = np.random.normal(0, 1, [w2]) #中間層2層目バイアス
self.wout = np.random.normal(0, 1, [w2, outd]) #出力層重み
self.bout = np.random.normal(0, 1, [outd]) #出力層バイアス
self.lr = lr
def forward(self, x):#フォワードプロパゲーション
self.z1 = x
self.z2 = sigmoid(np.dot(self.z1, self.w1) + self.b1)
self.z3 = sigmoid(np.dot(self.z2, self.w2) + self.b2)
self.out = sigmoid(np.dot(self.z3, self.wout) + self.bout)
return self.out
def train(self, x, t):
# バックプロパゲーション 出力層
En = (self.out - t) * self.out * (1 - self.out)
grad_wout = np.dot(self.z3.T, En)
grad_bout = np.dot(np.ones([En.shape[0]]), En)
self.wout -= self.lr * grad_wout
self.bout -= self.lr * grad_bout
# バックプロパゲーション 中間層2層目
grad_u2 = np.dot(En, self.wout.T) * self.z3 * (1 - self.z3)
grad_w2 = np.dot(self.z2.T, grad_u2)
grad_b2 = np.dot(np.ones([grad_u2.shape[0]]), grad_u2)
self.w2 -= self.lr * grad_w2
self.b2 -= self.lr * grad_b2
# バックプロパゲーション 中間層1層目
grad_u1 = np.dot(grad_u2, self.w2.T) * self.z2 * (1 - self.z2)
grad_w1 = np.dot(self.z1.T, grad_u1)
grad_b1 = np.dot(np.ones([grad_u1.shape[0]]), grad_u1)
self.w1 -= self.lr * grad_w1
self.b1 -= self.lr * grad_b1
def sigmoid(x): #シグモイド関数
return 1. / (1. + np.exp(-x))
HOG特徴量 (Histogram of Oriented Gradients)
入力となるHOG特徴量とは画像処理とコンピュータビジョンにおける形状や物体認識の為のデイスクリプタです。HOGは、画像内の局所的な物体の形状や輪郭を捉えるのに優れた方法であり、主に物体検出に広く利用されています。
特徴
画像をグレースケールに変換し(つまり色の情報を捨てる)、輝度情報のみに基づいています。画像の各ピクセルにおける輝度の勾配の強度と方向を計算し、最終的に特徴ベクトルを生成します。
手順
-
グレースケール画像の準備
HOG特徴量を計算する前に、入力画像をグレースケールに変換します。 -
勾配の計算
画像の各ピクセルにおける勾配の強度と方向を計算します。これには、一般的にSobelフィルタなどを用いて、x方向およびy方向の勾配を求めます。 -
セルの分割
画像を小さなセル(今回は8x8ピクセル)に分割します。各セルごとに、勾配のヒストグラムを計算します。 -
勾配のヒストグラムの計算
各セル内で、勾配方向のヒストグラムを作成します。通常、0度から180度(もしくは0度から360度)をいくつかのビン(今回は9つ)に分割し、各ビンに勾配の強度を集計します。 -
ブロックの正規化
隣接するセルを組み合わせてブロックを形成し、各ブロック内のヒストグラムを正規化します。これは、局所的な輝度変動やコントラストの変化に対して特徴量をより頑健にするためです。通常、L2正規化が用いられます。 -
特徴ベクトルの形成
各ブロックの正規化されたヒストグラムを結合し、最終的な特徴ベクトルを形成します。この特徴ベクトルが、HOG特徴量として画像の形状や輪郭を表現します。
def hog(gray):#HOG特徴量の計算 (入力:グレースケール画像)
h, w = gray.shape
# 勾配の強度と方向
gray = np.pad(gray, (1, 1), 'edge')
gx = gray[1:h+1, 2:] - gray[1:h+1, :w]#x方向の勾配の計算
gy = gray[2:, 1:w+1] - gray[:h, 1:w+1]#y方向の勾配の計算
gx[gx == 0] = 0.000001
mag = np.sqrt(gx ** 2 + gy ** 2) #勾配の強度
gra = np.arctan(gy / gx) #勾配の方向
gra[gra<0] = np.pi / 2 + gra[gra < 0] + np.pi / 2
# 勾配ヒストグラム
gra_n = np.zeros_like(gra, dtype='i')
d = np.pi / 9
for i in range(9):#角度を9個のビンに分ける。
gra_n[np.where((gra >= d * i) & (gra <= d * (i+1)))] = i
N = 8
HH = h // N
HW = w // N
Hist = np.zeros((HH, HW, 9), dtype=np.float32) #ヒストグラムの初期化
for y in range(HH):
for x in range(HW):
for j in range(N):
for i in range(N):
Hist[y, x, gra_n[y*4+j, x*4+i]] += mag[y*4+j, x*4+i] #ヒストグラムの割り当て
## 正規化
eps = 1 #平滑化パラメータ
for y in range(HH):
for x in range(HW):
Hist[y, x] /= np.sqrt(np.sum(Hist[max(y-1,0):min(y+2, HH), max(x-1,0):min(x+2, HW)] ** 2) + eps)
return Hist
IoU (Intersection over Union)の計算
IoUは物体検出やセグメンテーションなどのタスクにおいて、予測領域と実際の領域の重なり具合を評価するための指標です。IoUは2つの領域A,Bの交差部分の面積A∩BをAとBの和集合A∪Bで割ることによって求められます。
つまり
IoU = |A∩B| / |A∪B|
となります。
今回はイモリの顔を囲んだ矩形をグランドトゥルースとして、スライディングウィンドウとともに生成された矩形とのIoUを計算しています。
def iou(a, b):#IoU (Intersection over Union)の計算
area_a = (a[2] - a[0]) * (a[3] - a[1])
area_b = (b[2] - b[0]) * (b[3] - b[1])
iou_x1 = np.maximum(a[0], b[0])
iou_y1 = np.maximum(a[1], b[1])
iou_x2 = np.minimum(a[2], b[2])
iou_y2 = np.minimum(a[3], b[3])
iou_w = max(iou_x2 - iou_x1, 0)
iou_h = max(iou_y2 - iou_y1, 0)
area_iou = iou_w * iou_h # |A∩B|
iou = area_iou / (area_a + area_b - area_iou)
return iou
非極大抑制 (Non-Maximum Suppression, NMS)
非極大抑制(NMS)は物体検出で多くの重複した結果を除去し、信頼性の高い検出結果を選択する為に使用されます。今回ではバウンディングボックス(bbox)とスコアを考慮して処理を行う。
# Non-maximum suppression
def nms(_bboxes, iou_th=0.5, select_num=None, prob_th=None):
#
# Non Maximum Suppression
#
# Argument
# bboxes(Nx5) ... [bbox-num, 5(leftTopX,leftTopY,w,h, score)]
# iou_th([float]) ... threshold for iou between bboxes.
# select_num([int]) ... max number for choice bboxes. If None, this is unvalid.
# prob_th([float]) ... probability threshold to choice. If None, this is unvalid.
# Return
# inds ... choced indices for bboxes
#
#バウンディングボックスの幅と高さを計算
bboxes = _bboxes.copy()
bboxes[:, 2] = bboxes[:, 2] - bboxes[:, 0]
bboxes[:, 3] = bboxes[:, 3] - bboxes[:, 1]
# スコアが高い順にバウンディングボックスのインデックスをソート
sort_inds = np.argsort(bboxes[:, -1])[::-1]
#初期化
processed_bbox_ind = []
return_inds = []
unselected_inds = sort_inds.copy()
#メインループ
while len(unselected_inds) > 0:
process_bboxes = bboxes[unselected_inds]
argmax_score_ind = np.argmax(process_bboxes[::, -1])
max_score_ind = unselected_inds[argmax_score_ind]
return_inds += [max_score_ind]
unselected_inds = np.delete(unselected_inds, argmax_score_ind)
base_bbox = bboxes[max_score_ind]
compare_bboxes = bboxes[unselected_inds]
base_x1 = base_bbox[0]
base_y1 = base_bbox[1]
base_x2 = base_bbox[2] + base_x1
base_y2 = base_bbox[3] + base_y1
base_w = np.maximum(base_bbox[2], 0)
base_h = np.maximum(base_bbox[3], 0)
base_area = base_w * base_h
#IoUの計算
iou_x1 = np.maximum(base_x1, compare_bboxes[:, 0])
iou_y1 = np.maximum(base_y1, compare_bboxes[:, 1])
iou_x2 = np.minimum(base_x2, compare_bboxes[:, 2] + compare_bboxes[:, 0])
iou_y2 = np.minimum(base_y2, compare_bboxes[:, 3] + compare_bboxes[:, 1])
iou_w = np.maximum(iou_x2 - iou_x1, 0)
iou_h = np.maximum(iou_y2 - iou_y1, 0)
iou_area = iou_w * iou_h
compare_w = np.maximum(compare_bboxes[:, 2], 0)
compare_h = np.maximum(compare_bboxes[:, 3], 0)
compare_area = compare_w * compare_h
all_area = compare_area + base_area - iou_area
iou_ratio = np.zeros((len(unselected_inds)))
iou_ratio[all_area < 0.9] = 0.
_ind = all_area >= 0.9
iou_ratio[_ind] = iou_area[_ind] / all_area[_ind]
#閾値以上のバウンディングボックスを削除
unselected_inds = np.delete(unselected_inds, np.where(iou_ratio >= iou_th)[0])
#スコアの閾値でフィルタリング(必要な場合)
if prob_th is not None:
preds = bboxes[return_inds][:, -1]
return_inds = np.array(return_inds)[np.where(preds >= prob_th)[0]].tolist()
#選択数でフィルタリング(必要な場合)
if select_num is not None:
return_inds = return_inds[:select_num]
return return_inds #選択されたバウンディングボックスのインデックス
NMS前
NMS後
画像のリサイズ
入力画像をリサイズするアルゴリズムはいくつか存在しますが、今回はバイリニア補間を使っています。バイリニア補間は元の画像の周囲4つのピクセルを使用して加重平均を計算することで新しいピクセル値を計算することができます。これにより縮尺でも拡大でも滑らかで自然な画像を生成することが可能です。
def resize(img, h, w):
_h, _w = img.shape
ah = 1. * h / _h
aw = 1. * w / _w
y = np.arange(h).repeat(w).reshape(w, -1)
x = np.tile(np.arange(w), (h, 1))
y = (y / ah)
x = (x / aw)
ix = np.floor(x).astype(np.int32)
iy = np.floor(y).astype(np.int32)
ix = np.minimum(ix, _w-2)
iy = np.minimum(iy, _h-2)
dx = x - ix
dy = y - iy
out = (1-dx) * (1-dy) * img[iy, ix] + dx * (1 - dy) * img[iy, ix+1] + (1 - dx) * dy * img[iy+1, ix] + dx * dy * img[iy+1, ix+1]
out[out>255] = 255
return out
予測の評価方法
最後に今回の検出の評価を忘れない様にしましょう。今回の評価指標はRecall、Precision、F-score、mAPです。今回の例ではグランドトゥルースは矩形(27, 48, 95, 110)と矩形 (101, 75, 171, 138)としています。
Recall
Recallは正解の矩形がどれだけ検出できたか、正解をどれだけ検出できたかを示す指標です。0~1の範囲の値をとり、1に近づくほど正解に近くなります。
G' … グランドトゥルースのどれかと検出矩形とのIoUが閾値t(今回は0.5)以上となったグランドトゥルースの数。
G ... グランドトゥルースの矩形の数。
Recall = G' / G
Precision
Precisionは検出がどれだけ正確に行われたかを示します。0~1の範囲の値をとり、1に近づくほど正解に近くなります。
D' ... 検出した矩形の中で、グランドトゥルースのどれかとIoUが閾値t以上となった検出の数。
D ... 検出した矩形の数。
Precision = D' / D
F-score
RecallとPrecisonの調和平均です。2つの値のバランスを示すもので、0~1の範囲の値をとり、1に近づくほど良いとされます。
F-score = 2 × Recall × Precision / (Recall + Precision)
文字を検出する場合などはRecall、Precision、F-scoreで精度を図ることが多いです。
mAP
mAP(mean Average Precision)は、物体検出のパフォーマンスを評価するのによく使われる指標です。各クラスごとの平均精度(AP)を計算し、それを全クラスに対して平均したものになります。mAPの計算手順を以下に示します。
-
検出した各矩形に関してグランドトゥルースとのIoUが閾値t以上かどうかを判断して、表を作成します。
Detect | judge
detect1 | 1 (グランドトゥルースとのIoU>=t)
detect2 | 0 (グランドトゥルースとのIoU<t)
detect3 | 1
- mAP = 0として、上から順に見て、judgeが1の時は、見ているものの上すべてに関して、Precisionを計算し、mAPに加算する。
- 上から順に2を行い、全て行ったら、加算回数でmAPを割ります。
出力結果
以上を踏まえて出力結果を見ていくと下図のようになりました。GTを緑の枠線で示し、それに対してIoUが0.5以上の検出された矩形を赤、それ以外を青で示します。いくつか誤検出はあるものの、キチンと顔は検出できていますし、青の枠線を取り除いてしまえば顔の自動検出としては十分機能するといえるでしょう。
予測の精度
Recall >> 1.00 (2.0 / 2)
Precision >> 0.25 (2.0 / 8)
F-score >> 0.4
mAP >> 0.0625
import cv2
import numpy as np
np.random.seed(0)
# read image
img = cv2.imread("imori_1.jpg")
H, W, C = img.shape
# Grayscale
gray = 0.2126 * img[..., 2] + 0.7152 * img[..., 1] + 0.0722 * img[..., 0]
gt = np.array((47, 41, 129, 103), dtype=np.float32)
# crop and create database
Crop_num = 200
L = 60
H_size = 32
F_n = ((H_size // 8) ** 2) * 9
db = np.zeros((Crop_num, F_n+1))
for i in range(Crop_num):
x1 = np.random.randint(W-L)
y1 = np.random.randint(H-L)
x2 = x1 + L
y2 = y1 + L
crop = np.array((x1, y1, x2, y2))
_iou = iou(gt, crop)
if _iou >= 0.5:
cv2.rectangle(img, (x1, y1), (x2, y2), (0,0,255), 1)
label = 1
else:
cv2.rectangle(img, (x1, y1), (x2, y2), (255,0,0), 1)
label = 0
crop_area = gray[y1:y2, x1:x2]
crop_area = resize(crop_area, H_size, H_size)
_hog = hog(crop_area)
db[i, :F_n] = _hog.ravel()
db[i, -1] = label
## training neural network
nn = NN(ind=F_n, lr=0.01)
for i in range(10000):
nn.forward(db[:, :F_n])
nn.train(db[:, :F_n], db[:, -1][..., None])
# read detect target image
img2 = cv2.imread("imori_many.jpg")
H2, W2, C2 = img2.shape
# Grayscale
gray2 = 0.2126 * img2[..., 2] + 0.7152 * img2[..., 1] + 0.0722 * img2[..., 0]
# [h, w]
recs = np.array(((42, 42), (56, 56), (70, 70)), dtype=np.float32)
detects = np.ndarray((0, 5), dtype=np.float32)
# sliding window
for y in range(0, H2, 4):
for x in range(0, W2, 4):
for rec in recs:
dh = int(rec[0] // 2)
dw = int(rec[1] // 2)
x1 = max(x-dw, 0)
x2 = min(x+dw, W2)
y1 = max(y-dh, 0)
y2 = min(y+dh, H2)
region = gray2[max(y-dh,0):min(y+dh,H2), max(x-dw,0):min(x+dw,W2)]
region = resize(region, H_size, H_size)
region_hog = hog(region).ravel()
score = nn.forward(region_hog)
if score >= 0.7:
#cv2.rectangle(img2, (x1, y1), (x2, y2), (0,0,255), 1)
detects = np.vstack([detects, np.array([x1, y1, x2, y2, score[0]])])
detects = detects[nms(detects, iou_th=0.25)]
# Evaluation
# [x1, y1, x2, y2]
GT = np.array(((27, 48, 95, 110), (101, 75, 171, 138)), dtype=np.float32)
## Recall, Precision, F-score
iou_th = 0.5
Rs = np.zeros((len(GT)))
Ps = np.zeros((len(detects)))
for i, g in enumerate(GT):
iou_x1 = np.maximum(g[0], detects[:, 0])
iou_y1 = np.maximum(g[1], detects[:, 1])
iou_x2 = np.minimum(g[2], detects[:, 2])
iou_y2 = np.minimum(g[3], detects[:, 3])
iou_w = np.maximum(0, iou_x2 - iou_x1)
iou_h = np.maximum(0, iou_y2 - iou_y1)
iou_area = iou_w * iou_h
g_area = (g[2] - g[0]) * (g[3] - g[1])
d_area = (detects[:, 2] - detects[:, 0]) * (detects[:, 3] - detects[:, 1])
ious = iou_area / (g_area + d_area - iou_area)
Rs[i] = 1 if len(np.where(ious >= iou_th)[0]) > 0 else 0
Ps[ious >= iou_th] = 1
R = np.sum(Rs) / len(Rs)#Recall
P = np.sum(Ps) / len(Ps)#Precison
F = (2 * P * R) / (P + R)#F-score
print("Recall >> {:.2f} ({} / {})".format(R, np.sum(Rs), len(Rs)))
print("Precision >> {:.2f} ({} / {})".format(P, np.sum(Ps), len(Ps)))
print("F-score >> ", F)
## mAP
mAP = 0.
for i in range(len(detects)):
mAP += np.sum(Ps[:i]) / (i + 1) * Ps[i]
mAP /= np.sum(Ps)
print("mAP >>", mAP)
# Display
for i in range(len(detects)):
v = list(map(int, detects[i, :4]))
if Ps[i] > 0:
cv2.rectangle(img2, (v[0], v[1]), (v[2], v[3]), (255,0,0), 1)
else:
cv2.rectangle(img2, (v[0], v[1]), (v[2], v[3]), (0,0,255), 1)
cv2.putText(img2, "{:.2f}".format(detects[i, -1]), (v[0], v[1]+9),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255,0,255), 1)
for g in GT:
cv2.rectangle(img2, (int(g[0]), int(g[1])), (int(g[2]), int(g[3])), (0,255,0), 1)
plt.imshow(img2)
改善手法
今回はそれぞれのイモリの顔を検出できました。しかし一方で全く違うところも候補として検出されてしまいましたし、スライディングウィンドウという方法も計算コストの点で効率がいいわけではありません。検出精度の改善には例えば以下の例が挙げられます。
- リージョンプロポーザル法の導入
画像中の潜在的な物体領域(プロポーザル)を候補として生成する手法です。Selective SearchやRegion Proposal Network (RPN)を行う事でスライディングウィンドウで全検索を行う必要がなくなり計算コストを大幅に削減でき、より効率的で精度の高い物体検出が可能になります。 - 機械学習モデルの変更
畳み込みニューラルネットワークConvolutional Neural Network (CNN)は特に画像データの処理に優れた機械学習モデルの一つです。通常のニューラルネットワークが全結合層によって構成されており各層のニューロンが次の層の全ニューロンと接続しているのに対して、CNNは畳み込み層、プーリング層、全結合層などから構成されており畳み込み層とプーリング層は局所的な接続を持つのが特徴です。これによりCNNは局所的な特徴をより効果的に抽出できます。またフィルタの重みを共有するのでパラメータ数も抑えられ、計算効率も向上します。
まとめ
今回は比較的単純なニューラルネットワークを学習させて、イモリの顔の検出を実装できました。この手法だとまだ精度が高いとは言えませんね。ですが画像データから特徴量を抽出して、物体検出した後も、より精度の高い矩形を残す方法や検出の評価指標などの一連の基礎的な流れを学ぶことができました。これからも機械学習を勉強していこうと思います!
参考文献・引用元
1) Gasyori100knock, yoyoyo-yo, 2019, https://github.com/yoyoyo-yo/Gasyori100knock