OpenCVでカスケード分類器を作って物体検出をやってみた

OpenCVを使って簡単な物体検出をやってみました。


##環境
Python3 (3.5.2)
Anaconda 4.7.11
macOS Catalina 10.15.7


##OpenCVとは
OpenCVとはオープンソースの画像処理のライブラリです。画像変形や領域分割、物体検出など様々な機能を利用することができます。
ここで紹介する物体検出の機能では、自分でカスケード分類器を作成することで好きな物体を検出できるようになります。


##カスケード分類器とは
物体検出を行う際には、検出したい物体がどのような特徴を有しているのか把握しておく必要があります。そこで、あらかじめ該当する物体を含む画像と含まない画像を用意し、検出したい物体の特徴を抽出しておきます。この抽出された特徴をまとめたものをカスケード分類器と言います。


##何を物体検出するか
下の画像内にある"▲"を検出してみます。
![IMG_6862-min.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151361/5196dec3-5255-dfdf-48a3-ae5701746990.jpeg)




##準備

Anacondaで仮想環境を構築

```
$conda create --name opencvproject python=3.5.2
$conda activate opencvproject
```

OpenCV３をインストール

```
$conda install -c https://conda.anaconda.org/menpo opencv3
```


###フォルダ構成
　triangle/
　　├ data/
　　│　└ model/ ← 生成する分類器の格納フォルダ
　　│　　　
　　├ src/
　　│　├ posi/ ← 正解データ格納フォルダ
　　│　├ nega/ ← 不正解データ格納フォルダ
　　│　├ positive.dat
　　│　└ negative.dat
　　└ cascade.py

##実装


###1.検出したい物体が写っている画像を用意

まず"▲"を均等に複数記載した紙を印刷して、カメラで斜めから撮ったり、少し拡大したりと様々なパターンの画像を4枚ほど用意しました。

![s_IMG_5286.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151361/9d366390-a019-fe3d-b438-972a7ea85d08.jpeg)


###2.正解データを作成する
１で用意した画像データを使って正解データを記載した`positive.dat`というファイルを作成します。

ファイルには
　・検出したい物体が写っている画像パス
　・その画像内の検出対象の物体数
　・各物体の座標&サイズ
を記載していきます。

記入例）

```
hoge/fuga_img.jpg 2 110 274 22 23   110 408 24 22
```

ここで面倒なのが「各物体の座標&サイズ」を記載するところです。
検出対象の物体の画像内での
　・x座標
　・y座標
　・横幅
　・縦幅
を物体の数だけ記載しなければなりません。

今回はこちらの[サイト](http://wisteriahill.sakura.ne.jp/OpenCV/getArea4Haartraining/)を使って物体１つ１つの座標&サイズを取得していきました。


これを画像枚数分だけ記載していきます。

```dat:positive.dat
posi/posi_IMG_0001.JPG 48	181 136 34 40	331 137 36 34	482 133 33 35	634 133 32 29 ・・・・・
posi/posi_IMG_0002.JPG 63	206 50 24 31	344 50 29 30	491 52 26 28	631 51 31 30 ・・・・・
・
・
・
・
```

###3.不正解データを作成する

今度は不正解データを記載した`negative.dat`というファイルを作成します。不正解データの場合は座標の入力などは必要ありません。検出対象の物体が写っていない画像のパスを記載していくだけでOKです。今回は220枚ほど用意しました。

```dat:negative.dat
nega/nega_IMG_0001.JPG
nega/nega_IMG_0002.JPG
・
・
・
・
```


###4.ベクトルファイルを作成する

分類器を作成するためには、学習用データをベクトルファイルにまとめる必要があります。そこで`createsamples`コマンドを使用して、先ほど作成した`positive.dat`ファイルから`positive.vec`というベクトルファイルを生成します。

```
$opencv_createsamples -info <２で作成したpositive.dat> -vec <出力するベクトルファイルの名前> -num <positive.datに記載した画像数> -w <生成画像の横幅> -h <生成画像の縦幅>
```

```
$opencv_createsamples -info ~/triangle/src/positive.dat -vec ~/triangle/src/positive.vec -num 400 -w 35 -h 35

Info file name: ~/triangle/src/positive.dat
Img file name: (NULL)
Vec file name: ~/triangle/src/positive.vec
BG  file name: (NULL)
Num: 400
BG color: 0
BG threshold: 80
Invert: FALSE
Max intensity deviation: 40
Max x angle: 1.1
Max y angle: 1.1
Max z angle: 0.5
Show samples: FALSE
Width: 35
Height: 35
Create training samples from images collection...
Done. Created 400 samples

```

これで400枚の正解データの情報が`positive.vec`というファイルにまとめられました。


###5.分類器を作成する

4で作成した学習用のベクトルファイルを使って分類器を作成します。

```
$opencv_traincascade -data <分類器を保存するフォルダのパス> -vec <ベクトルファイルのパス> -bg <negative.datのパス> -numPos <正解データ数(実際の正解データ数の9割が目安)> -numNeg <不正解データ数> -featureType <特徴量の見つけ方> -maxFalseAlarmRate 0.1 -w <生成画像の横幅> -h <生成画像の縦幅> -minHitRate 0.99 -numStages <ステージ数(数が大きければ処理時間は長くなるが精度は上がる)>
```

※`featureType`には、「HOG特徴量(HOG)」「LBP特徴量(LBP)」「Haar-Like特徴量」を指定できます。ざっくりと説明するとHaar-Like特徴量は物体の局所的な明暗差によって画像を判別し、HOG&LBPは物体の局所的な輝度の分布によって画像を判別します。今回は処理速度の観点からLBPを採用しました。


```
$opencv_traincascade -data ~/data/model -vec ~/src/positive.vec -bg ~/src/negative.dat -numPos 570 -numNeg 220 -featureType LBP -maxFalseAlarmRate 0.1 -w 35 -h 35 -minHitRate 0.99 -numStages 19
```

```
PARAMETERS:
cascadeDirName: ~/triangle/data/model
vecFileName: ~/triangle/src/positive.vec
bgFileName: ~/triangle/src/negative.dat
numPos: 380
numNeg: 220
numStages: 19
precalcValBufSize[Mb] : 1024
precalcIdxBufSize[Mb] : 1024
acceptanceRatioBreakValue : -1
stageType: BOOST
featureType: LBP
sampleWidth: 35
sampleHeight: 35
boostType: GAB
minHitRate: 0.99
maxFalseAlarmRate: 0.1
weightTrimRate: 0.95
maxDepth: 1
maxWeakCount: 100
mode: BASIC
Number of unique features given windowSize [24,24] : 162336

===== TRAINING 0-stage =====
<BEGIN
POS count : consumed   380 : 380
NEG count : acceptanceRatio    218 : 1
Precalculation time: 0
+----+---------+---------+
|  N |    HR   |    FA   |
+----+---------+---------+
|   1| 0.992105|0.00917431|
+----+---------+---------+
END>
Training until now has taken 0 days 0 hours 0 minutes 1 seconds.

===== TRAINING 1-stage =====
<BEGIN
POS count : consumed   380 : 383
NEG count : acceptanceRatio    218 : 0.28534
Precalculation time: 1
+----+---------+---------+
|  N |    HR   |    FA   |
+----+---------+---------+
|   1|        1|        1|
+----+---------+---------+
|   2| 0.992105|0.0321101|
+----+---------+---------+
END>
Training until now has taken 0 days 0 hours 0 minutes 3 seconds.

===== TRAINING 2-stage =====
<BEGIN
POS count : consumed   380 : 386
NEG count : acceptanceRatio    218 : 0.0803242
Precalculation time: 1
+----+---------+---------+
|  N |    HR   |    FA   |
+----+---------+---------+
|   1|        1|0.0458716|
+----+---------+---------+

・
・
・
・

===== TRAINING 18-stage =====
<BEGIN
POS count : consumed   340 : 372
NEG count : acceptanceRatio    218 : 1.2646e-06
Precalculation time: 0
+----+---------+---------+
|  N |    HR   |    FA   |
+----+---------+---------+
|   1|        1|        1|
+----+---------+---------+
|   2| 0.997059| 0.720183|
+----+---------+---------+
|   3| 0.991176| 0.798165|
+----+---------+---------+
|   4| 0.991176| 0.344037|
+----+---------+---------+
|   5| 0.991176| 0.307339|
+----+---------+---------+
|   6| 0.997059| 0.233945|
+----+---------+---------+
|   7| 0.991176|0.0458716|
+----+---------+---------+
END>
Training until now has taken 0 days 0 hours 43 minutes 44 seconds.
```


stageが上がるごとに処理時間は長くなり、最終的に1時間以上掛かりました。

途中下記のようなエラーに遭遇しました。

エラー①

```
Parameters can not be written, because file 〜/model/params.xml can not be opened.
```

生成した分類器を保存するフォルダのパス指定に誤りがあったので発生したエラーでした。

エラー②

```
error: (-5) Can not get new positive sample. The most possible reason is insufficient count of samples in given vec-file.
```

指定した正解画像数が作った数と同数であると発生するエラーのようで、指定する正解画像数は9割程度に設定する必要があるようです。


各ステージでの処理が終わると`cascade.xml`というファイルが生成されます。このファイルが分類器となりますので、これを使って物体検出をしていきます。
（同時にStage0,Stage1,・・・というファイルも生成されていますが、こちらのファイルは無視してください）




###5.分類器を使って物体検出をしてみる
4で生成した分類器を使って実際に物体検出をしてみます。


```python:cascade.py
import cv2

# カスケード分類器を読み込む
cascade = cv2.CascadeClassifier("data/model/cascade.xml")

# 入力画像の読み込み&グレースケール変換
img = cv2.imread("triangle_test.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# "▲"を物体検出する
triangle = cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=3, minSize=(30, 30))

# 検出した領域を赤色の矩形で囲む
for (x, y, w, h) in triangle:
    cv2.rectangle(img, (x, y), (x + w, y+h), (0,0,200), 3)

# 結果画像を保存
cv2.imwrite("result_triangle.jpg",img)

#結果画像を表示
cv2.imshow('image', img)

# 何かのキーを押したら処理を終了させる
cv2.waitKey(0)
cv2.destroyAllWindows()
```

結果以下のように"▲"のみを検出することができました！

![result_triangle-min.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151361/6daee276-2bcb-4058-7cfb-dcd9116bf17d.jpeg)


##まとめ
今回は簡易的な分類器を作ることで、OpenCVの物体検出をやってみました。作成した分類器は検出対象の向きが少し斜めになると、検出できなくなるなどまだ精度的には十分ではありません。十分な精度を出すには正解データは7000枚、不正解データは3000枚は必要とOpenCVの公式ドキュメントに記載されていました。地味に大変ですが、十分なサンプルデータを揃えた上でOpenCVを使いこなせれば高精度の物体検出が簡単にできるようです。