openCVで物体認識 by traincascade

## 何をするか？
こういうのやりたい。

<img width="498" alt="スクリーンショット 2016-10-24 23.32.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/40159/0efb83e9-1583-6be4-a9d5-ab0c274f4805.png">
すなわち、写真の中に検出したいものが写っていたら、その座標を特定する。

## どうやってやるか？
openCVで、HOG特徴量(輝度の勾配で作る特徴量)やHaar-Like特徴量(画像の明度で作る特徴量)と正解ラベルを用いて、複数の弱学習器を作って、boostingして判別。
openCVは、pyenvでanacondaインストールからの`conda install -c https://conda.binstar.org/jjhelmus opencv`でサクッと入る。
pyenvとかanacondaはググれば沢山方法が出てきます。

## レッツトライ
- 0 ディレクトリ構成
- 1 学習用の正解データ情報を作成
- 2 学習用の不正解データ情報を作成
- 3 positiveのベクトル作成
- 4 学習器作成


### 0 ディレクトリ構成
```
 任意のディレクトリ/
　├data/
　│　├pos/               [手順1で収集]
　│　│　├xxx001.jpg
　│　│　├xxx002.jpg
　│　│　├ ...
　│　│　└xxx100.jpg
　│　└neg/               [手順2で収集]
　│　│　├yyy001.jpg
　│　│　├yyy002.jpg
　│　│　├ ...
　│　│　└yyy100.jpg
　│　└model/
　│　　 ├param.xml       [手順4で作成]
　│　　 ├stage0.xml      [手順4で作成]
　│　　 ├stage1.xml      [手順4で作成]
　│　　 ├stage2.xml      [手順4で作成]
　│　　 └stageX.xml      [手順4で作成]
　└src/
　 　├positive.dat       [手順1で作成]
　 　├negative.dat       [手順2で作成]
　 　├positive.vec       [手順3で作成]
　 　└create_cascade.sh  [手順3-4で利用]

```

### 1 学習用の正解データ情報を作成
正解データには、以下の情報が必要。

- 元の画像の場所
- 判別したい物が写っている数
- x座標
- y座標
- 横幅
- 高さ

これを、半角スペース区切りで一つのファイル[positive.dat]に保存。

```{positive.dat}
/Path/To/positive/xxx001.jpg 1 200 50 50 50
/Path/To/positive/xxx002.jpg 2 150 30 40 36 230 300 55 60
・・・
/Path/To/positive/xxx100.jpg 1 150 30 40 36
```

特定したい物が二つ写っている場合、以下のような形式になる。
画像パス 2 1個目のx座標 1個目のy座標 1個目の横幅 1個目の高さ 2個目のx座標 2個目のy座標 2個目の横幅 2個目の高さ

学習画像はどうやって集めるかというと、気合です。。。
[補助ツール](http://shkh.hatenablog.com/entry/2012/11/03/052251)があるみたいです・・・。

### 2 学習用の不正解データ情報を作成
不正解データは、正解データのような複雑な形は不要です。
認識したい物が写っていない画像のパスを一つのファイル[negative.dat]へ列挙すれば終わり。

```{negative.dat}
/Path/To/negative/yyy001.jpg
/Path/To/negative/yyy002.jpg
・・・
/Path/To/negative/yyy100.jpg
```

positiveとnegativeの枚数は、差があっても問題なし。
できたら、positiveで検出したい枠の大きさと同じくらいのサイズの画像だけでなく、色々なサイズの画像を入れてあげると良い。なぜなら、cascadeで検出する際、四角形を様々な大きさにしながら、写っているものを判別するため。

### 3 positiveのベクトル作成
```{create_cascade.sh}
# positive.vec作成コマンド
opencv_createsamples -info positive.dat -vec positive.vec -num 100 -w 40 -h 40
```

- -info:手順1で作ったdatファイルを指定
- -vec:出力ベクトルファイル名を指定
- -num:positive.datの行数(positiveの画像枚数)
- -w:横幅
- -h:高さ

※　この時エラーが出てはまったのが、-wと-hのサイズ以下の画像があった場合。もしエラーで詰まったら、postiveの画像のサイズを確認してみると良いかも。

### 4 学習器作成
```{create_cascade.sh}
# model作成コマンド
opencv_traincascade -data /Path/To/model -vec /Path/To/src/positive.vec -bg /Path/To/src/negative.dat -numPos 100 -numNeg 100 -featureType HOG -maxFalseAlarmRate 0.1 -w 50 -h 50 -minHitRate 0.97 -numStages 17
```

- -data:modelファイルの格納場所を指定
- -vec:positive.vecの場所を指定
- -bg:negative.datの場所を指定
- -numPos:positiveの枚数を指定
- -numNeg:negativeの枚数を指定
- -featureType:HOGならHOG特徴量をLBPならLBP特徴量を、指定なしならHaar-Like特徴量を利用。
- -maxFalseAlarmRate:各学習ステージでの許容する誤検出率
- -w:横幅
- -h:高さ
- -minHitRate:各学習ステージでの許容する最小検出率
- -numStages:作成するステージ数

※ numPosは、実際にvecに変換した枚数の8〜9割くらいを指定するのが吉。これをしないと、以下のようなエラーがたまに見られる。
よくわかってないけど、学習中にpositiveでも正しくないって判断したら、その画像を次の学習器作成に利用しない仕組みが内部的にあるらしい。これで弾いたため、学習に使おうとしてデータがたりないよーってエラーになるらしい。あと、wとhは、positive.vecを作成したときに指定した値でGO。

```{opencv_traincascade_error.log}
OpenCV Error: Bad argument (Can not get new positive sample. The most possible reason is insufficient count of samples in given vec-file.
```

これで、以下のような出力を吐き出しながら、 /Path/To/modelにstageX.xmlが何個も作られていく。

```{opencv_traincascade.log}
PARAMETERS:
cascadeDirName: ../../../model
vecFileName: positive.vec
bgFileName: negative.dat
numPos: 150
numNeg: 200
numStages: 17
precalcValBufSize[Mb] : 256
precalcIdxBufSize[Mb] : 256
stageType: BOOST
featureType: HOG
sampleWidth: 50
sampleHeight: 50
boostType: GAB
minHitRate: 0.97
maxFalseAlarmRate: 0.1
weightTrimRate: 0.95
maxDepth: 1
maxWeakCount: 100

===== TRAINING 0-stage =====
<BEGIN
POS count : consumed   150 : 150
NEG count : acceptanceRatio    200 : 1
Precalculation time: 0
+----+---------+---------+
|  N |    HR   |    FA   |
+----+---------+---------+
|   1|        1|        1|
+----+---------+---------+
|   2|        1|    0.285|
+----+---------+---------+
|   3|        1|    0.285|
+----+---------+---------+
|   4| 0.993333|    0.135|
+----+---------+---------+
|   5|        1|    0.145|
+----+---------+---------+
|   6|        1|    0.095|
+----+---------+---------+
```

表は、NがHRがステージの閾値に基づくヒット率 (v_hitrate / numpos)、FAがステージの閾値に基づく誤警報 (v_falsealarm / numneg)を表す。

最終的に、cascade.xmlというファイルが出来上がり、これを用いて予測する。

### 5 学習器を用いた判別
判別するコードをpythonで書くとこんな感じ。

```py
import cv2
import numpy as np

# 学習器(cascade.xml)の指定
Cascade = cv2.CascadeClassifier('../model/2/cascade.xml')
# 予測対象の画像の指定
img = cv2.imread('image.jpg', cv2.IMREAD_COLOR)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
point = Cascade.detectMultiScale(gray, 1.1, 3)

if len(point) > 0:
	for rect in point:
		cv2.rectangle(img, tuple(rect[0:2]), tuple(rect[0:2]+rect[2:4]), (0, 0,255), thickness=2)
else:
	print "no detect"

cv2.imwrite('detected.jpg', img)
```

これで保存されたdetected.jpgには、枠で囲われた画像が出来上がる。
枠内の画像だけ保存したければ、以下でできるはず。

```py
img = img[point[0][1]:point[0][1]+point[0][3], point[0][0]:point[0][0]+point[0][2]]
cv2.imwrite('out.jpg', img)
```

Cascade.detectMultiScaleのリターンであるpointは、x,y,w,hが返ってくる。

- x:四角の左上の座標
- y:四角の左上の座標
- w:横幅
- h:縦幅

そのため、img[y:y+h, x:x+w]で指定すれば切り抜くことができます。