たかのめ

Twitterでは文字数が足りないことを書きます。語録はあります。Twitter:@Originfall

OpenCVでSVMとHOG特徴量を使って人を検出するのを試みてみる。【part3】

 前回Part2までで、データセットの準備が終わりましたので、いよいよSVMに学習させます。

 part2はこちら。
originfall.hatenablog.com



 Part1でも書きましたが、この段階でのプログラムの流れは以下の通りです。

  • 読み込んだ画像のHOG特徴量を計算します。
  • SVMをセットして、学習させます。
  • 学習してできたSVMのモデルを保存します。
  • 完成したSVMをテストします。

 では、早速コードをのっけます。100行ぐらいあるので、部分部分毎に分けます。

import cv2
import sys
import random
import numpy as np

#parameter for hogdescriptor
winSize = (44, 124)
blockSize = (16, 16)
blockStride = (4, 4)
cellSize = (4, 4)
nbins = 9
derivAperture = 1
winSigma = -1
histogramNormType = 0
L2HysThreshold = 0.2
gammaCorrection = 1
nlevels = 64
signedGradients = True

pos_datas = 80 #max:167
neg_datas = 80 #max:99
#parameter for SVM
gamma = 0.5
C = 0.5

cnt = 0
hog_train = []
hog_test = []

# set labels
labels = []
labels_test = []

# make hog descriptor
hog = cv2.HOGDescriptor(winSize, blockSize, blockStride, cellSize, nbins, derivAperture, winSigma, histogramNormType, L2HysThreshold, gammaCorrection, nlevels, signedGradients)

 初期値の設定。parameter hor hogdescriptorでHOG特徴量の計算時に使うパラメータを設定しています。winSizeがウィンドウサイズ、blockSizeがブロックサイズ、blockStrideがブロックを動かす幅、cellSizeがセルサイズ、nbinsが勾配をヒストグラムにまとめる時のビンの数、つまり何個に分けられたヒストグラムを作成するかを決定する値です。ここから先のパラメータはよくわかっていません。derivAperture及びhistogramNormTypeは現在使用されていないようです(参考:derivAperture, histogramNormType not used in HOGDescriptor · Issue #9224 · opencv/opencv · GitHub)。winSigmaはガウシアン平滑化のウィンドウサイズみたいです。HOG特徴量を計算するのにガウシアン平滑化をするんでしょうか。L2HysThresholdはL2-Hys正規化縮小処理の閾値だそうです。gammaCorrectionは前処理としてガンマ値の補正をするかどうか、nlevelsは画像中の探索時の検出窓の拡大回数の最大値です。signedGradientsは、単語から憶測すると、ベクトルの勾配を符号付きにするかどうかを決定するのでしょうか?
(パラメータに関しては、古いバージョンのドキュメントですが、以下を参考にすると良いと思います。
物体検出 — opencv 2.2 documentation)

 pos_datasとneg_datasがそれぞれ、学習に用いる、人が写っているポジティブデータ、人が写っていないネガティブデータの数です。
 parameter for SVMSVMのパラメータの値です。今回は両方とも0.5になってますが、多分変えたほうがいいと思います。
 cntはカウント用の変数、hog_trainとhog_testのリストを作成しておきます。それぞれ学習データとテストデータのHOG特徴量を格納するリストになります。また、ラベルという、画像に人が入っていたら1、入ってなければ0とする値を考えて、学習データのラベルとテストデータのラベルのリストを作成してきます。
 最後の行で、HOGDescriptorを作成します。こうしておくと、あとで出てきますが、hog.compute(画像)とすると、画像のHOG特徴量を計算してくれます。
 それでは次に行きます。

# calculate hog for training svm
while cnt <= pos_datas:
	temp = random.randint(1, 3)
	for i in range(cnt, cnt+temp):
		path = ('image/front/resized/front%04d.bmp' %i)
		img_train = cv2.imread(path, 0)
		img_train = cv2.resize(img_train, (winSize))
		hog_train.append(hog.compute(img_train))
		labels.append(1)

	for i in range(cnt, cnt+temp):
		path = ('image/negative/cut/neg%04d.png' %i)
		img_train = cv2.imread(path, 0)
		img_train = cv2.resize(img_train,(winSize))
		hog_train.append(hog.compute(img_train))
		labels.append(0)
	cnt += temp

hog_train = np.array(hog_train)
labels = np.array(labels, dtype=int)

# make SVM
svm = cv2.ml.SVM_create()
svm.setKernel(cv2.ml.SVM_LINEAR)
svm.setType(cv2.ml.SVM_C_SVC)
svm.setC(C)
# Train SVM on training data
svm.train(hog_train, cv2.ml.ROW_SAMPLE, labels)

# Save trained model
svm.save('man_svm.xml')

この部分からSVMの学習が始まります。最初に、変数tempにrandom.randint(1, 3)で1から3の乱数整数を入れてますが、これは学習時にポジティブデータとかネガティブデータばかりを連続して学習させないようにするためです。ですが、このコードを書いた当時は気にしていませんでしたが、この部分はもうちょっといいやり方がありそうです。例えば、画像ファイル番号のリストを作って、そこからランダムに抽出する、とかできそうなので、うまくやってみてほしいです。

参考:
adragoona.hatenablog.com

 まあとにかく、まずポジティブデータの画像を読み込んで、指定されたウィンドウサイズ(44, 124)にリサイズし、HOG特徴量を計算します。算出された特徴量をhog_trainリストに格納していきます。また、ポジティブデータなので、labelsリストに1を追加します。その後、ネガティブデータの画像についても同様のことをします。

 全ての画像についてHOG特徴量の算出及び格納が終わったら、hog_trainリストとlabelsリストをnp.arrayに変換します。SVMはnumpy.array(ndarray)型じゃないと扱えないので。

 その後、SVMを作成します。この場合、svm.setKernel()で、SVMで用いるカーネル関数のタイプを設定します。今回は単純な、線形カーネルです。svm.setType()でSVMのタイプを設定します。これはSVMに何をやらせたいかで変わるみたいですが、よくわからないので今回は、クラス分けソフトマージンSVMを意味するC_SVCにしておきます。svm.setC()でCの値をセットします。このようにしてSVMの各種パラメータを設定したあと、svm.train()で作成したSVMに学習させます。第2引数のcv2.ml.ROW_SAMPLEはたぶん、学習データが行で並んでるよっていうのを指定するのかなあと思ったんですが、合ってるかはわかりません。(一応、cv2.ml.COL_SAMPLEっていうのもあるみたいです)。svm.save()で、作成したSVMを保存します。こうすることで、後で実際にSVMを使って人検出するときに保存したSVMを読み込んで使うことができ、一々SVMの学習からやらなくて済みます。
 次がコードの最後の部分です。

resp = svm.predict(hog_train)[1].ravel()

ok = 0
for i in range(0, pos_datas + neg_datas):
	if resp[i] == labels[i]:
		ok += 1

print(ok / (pos_datas + neg_datas))

for i in range(pos_datas, pos_datas+10):
	path = ('image/front/resized/front%04d.bmp' %i)
	img_test = cv2.imread(path, 0)
	img_test = cv2.resize(img_test, (winSize))
	hog_test.append(hog.compute(img_test))
	labels_test.append(1)

for i in range(neg_datas, neg_datas+10):
	path = ('image/negative/cut/neg%04d.png' %i)
	img_test = cv2.imread(path, 0)
	img_test = cv2.resize(img_test,(winSize))
	hog_test.append(hog.compute(img_test))
	labels_test.append(0)

hog_test = np.array(hog_test)
labels_test = np.array(labels_test, dtype=int)

resp_test = svm.predict(hog_test)[1].ravel()

ok = 0
for i in range(0, 20):
	if resp_test[i] == labels_test[i]:
		ok += 1

print(ok / 20)

print("SVM has been made!")

 まず、念のため、学習に使ったデータに対して、学習させたSVMを適用します。最初のresp = svm,predict().ravel()の部分で、変数respに、学習用データにSVMを適用した結果を配列として格納していきます(全ての値が1か0の配列になります)。その後、その結果とラベルを比較して、正答率を出します。

 同様にして、後半部分では、SVMを学習で使わなかった画像について適用して正答率を出しています。

 このコードを書くにあたり参考にしたサイトが以下です。(英語)
http://www.learnopencv.com/handwritten-digits-classification-an-opencv-c-python-tutorial/

 次回は、学習させたSVMを画像に適用させます。

 part4はこちら。
originfall.hatenablog.com