OpenCVのカスケード分類器を自作して自動車検出を行う

#1. はじめに

OpenCVでは、人の顔や目等を検出するカスケード分類器があらかじめ用意されており、
それを用いると簡単に画像上の人の顔や目等の検出が可能です。

またOpenCVで用意されている実行ファイルとサンプル画像を用意することで、
任意の対象のカスケード分類器が作成できるとのことなので、
今回はCIFAR10の自動車画像を用いて、自動車検出を試みました。

*カスケード型識別器（Cascade detector）は、複数の強識別器を連結した識別器です。
カスケード型識別器では、各強識別器により順番に判別を行います。
(cascade:（連続した）小さな滝)

https://algorithm.joho.info/machine-learning/cascade-object-detector/


#2. 前準備
##2-1. 実行ファイルの用意

まずOpenCVの公式ページから、必要なファイルのダウンロードを行います。


OpenCV公式サイト

https://opencv.org/releases/

カスケード分類器の作成に必要なツールは、3系にのみ含まれており、
4系には含まれていないため、今回は以下のバージョンをダウンロードしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2048607/8ae2dcc8-a582-ab81-f79f-ea0c72d85321.png)

ダウンロードしたファイルをダウンロードフォルダに展開すると、
以下のフォルダに必要なファイルが格納されています。

C:\Users\USERNAME\Downloads\opencv\build\x64\vc15\bin

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2048607/53bc5263-a52f-22cb-62ca-ebecf74708c9.png)


必要なファイルは以下の3つです。

・opencv_createsamples.exe
・opencv_traincascade.exe
・opencv_world3416.dll

今回はデスクトップ下にopencv-cascadeのフォルダを作成し、
そこに必要なファイルをコピーしました。

C:\Users\USERNAME\Desktop\opencv\cascade
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2048607/2f6de58e-cff2-c97b-0582-4d9e48aebb25.png)



また同じ階層に以下の3つのフォルダを作成しています。
cascade - 作成されたカスケード分類器が保存される
ng - NG画像を保存する。
ok - OK画像を保存する。

##2-2. 画像の用意

OK画像としては、CIFAR10の画像より、自動車画像を5000枚用意しました。
NG画像としては、CIFAR10の画像より、自動車画像以外を3000枚用意しました。

まず以下のリンクより、「CIFAR-10 python version」をダウンロードします。

CIFAR10

https://www.cs.toronto.edu/~kriz/cifar.html

ダウンロードしたファイルを解凍すると、data_batch_1からdata_batch_5までが格納されています。
そのファイルからOK画像とNG画像の生成は以下のコードによって実行しました。


```python
import numpy as np
import cv2

def unpickle(file):
    import pickle
    with open(file, 'rb') as fo:
        dict = pickle.load(fo, encoding='bytes')
    return dict

def preprocess(dict):
    image = dict[b'data']
    image = image.reshape(-1,32,32,3,order ='F') #order指定で高い次元から埋める
    image = image.transpose(0,2,1,3) #列番号0123　→　0213に変更
    label = dict[b'labels']
    return image,label

X_train, Y_train = preprocess(unpickle("cifar10/data_batch_1"))
for i in range(4):
    X, Y =preprocess(unpickle("cifar10/data_batch_"+ str(i+2)))
    X_train = np.concatenate([X_train,X])
    Y_train = np.concatenate([Y_train,Y])

X_train = np.array(X_train)
Y_train = np.array(Y_train)

X_car = X_train[Y_train == 1] #carのlabelは1
X_notcar = X_train[Y_train != 1]

#ok画像
for i in range(5000):
    filename = 'C:/Users/USERNAME/Desktop/opencv/cascade/ok/{:0>4d}.jpg'.format(i)    
    cv2.imwrite(filename, X_car[i])

#ng画像
for i in range(3000):
    filename = 'C:/Users/USERNAME/Desktop/opencv/cascade/ng/{:0>4d}.jpg'.format(i)    
    cv2.imwrite(filename, X_notcar[i])
```

okフォルダ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2048607/1ca658c6-f226-5a6e-6515-3f566dc959e0.png)

ngフォルダ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2048607/113551d8-3fed-5b5f-6807-b79cecd56ea8.png)

##2-3. 画像リストファイルの作成

次に、画像リストファイルを作成します。

まずコマンドプロンプトで、okフォルダに移動し、以下のコマンドを実行します。

```
dir *.jpg /b > poslist.txt
```

poslistはファイル名と正解の対象物の数と位置を記入する必要があります。

poslist.txt
0000.jpg 1 0 0 32 32 ←（ファイル名/対象物の数/x軸開始位置/Y軸開始位置/幅/高さ）
0001.jpg 1 0 0 32 32
0002.jpg 1 0 0 32 32

次にコマンドプロンプトで、ngフォルダに移動し、以下のコマンドを実行します。

```
dir *.jpg /b > neglist.txt
```

neglistはフルパスで記載します。

neglist.txt
C:\Users\USERNAME\Desktop\opencv\cascade\ng\0000.jpg
C:\Users\USERNAME\Desktop\opencv\cascade\ng\0001.jpg
C:\Users\USERNAME\Desktop\opencv\cascade\ng\0002.jpg

##2-4. ベクトルファイルの作成

最後に、poslistよりベクトルファイルを作成します。
コマンドラインで、desktop-opencv-cascadeファルダに移動し、
以下のコマンドを実行します。

```
opencv_createsamples -info ./ok/poslist.txt -vec ./ok/positive.vec -num 5000 -w 32 -h 32 -show
```

| オプション名 | 用途 | パラメータ |
|:-----------|:-----------|:------------|
| -info       | 正解画像リストファイル|ok/poslist.txt|
| -vec     |ベクトルファイル保存場所|ok/positive.vec|
| -num     |画像の数|5000|
| -w      | ベクトルの幅| 32|
| -h |ベクトルの高さ| 32 |
| -show |画像ファイルを見れる| - |


今回は画像ファイルを5000枚分用意しましたが、
1枚の画像ファイルを複製して学習ファイルを作成する方法も用意されています。

```
opencv_createsamples.exe -img ./ok/0000.jpg -num 1000 -vec ./ok/positive.vec -show
```

| オプション名 | 用途 | パラメータ |
|:-----------|:-----------|:------------|
| -img       | 正解画像|ok/0000.jpg|
| -vec     |ベクトルファイル保存場所|ok/positive.vec|
| -num     |画像の数|1000|
| -show |画像ファイルを見れる| - |

用意するファイルは以上です。

#3　カスケード分類器の作成

カスケード分類器の作成を行います。
コマンドラインで、desktop-opencv-cascadeファルダに移動し、
以下のコマンドを実行します。

```
opencv_traincascade.exe -data cascade -vec ok/positive.vec -bg ng/neglist.txt -numPos 4000 -numNeg 3000 -w 32 -h 32
```

| オプション名 | 用途 | パラメータ |
|:-----------|:-----------|:------------|
| -data       | 作成されたカスケード分類器が保存されるファルダ|cascade|
| -vec     |ベクトルファイル|ok/positive.vec|
| -numPos     |正解画像数|4000|
| -numNeg |不正解画像数| 3000 |
| -w      | ベクトルの幅| 32|
| -h |ベクトルの高さ| 32 |

正解画像数は用意した画像するより少なくすると良いとのことで、
今回は4000枚を指定しました。

正常に実行されると以下の表示が出ます。

```
cascadeDirName: cascade
vecFileName: ok/positive.vec
bgFileName: ng/neglist.txt
numPos: 4000
numNeg: 3000
numStages: 20
precalcValBufSize[Mb] : 1024
precalcIdxBufSize[Mb] : 1024
acceptanceRatioBreakValue : -1
stageType: BOOST
featureType: HAAR
sampleWidth: 32
sampleHeight: 32
boostType: GAB
minHitRate: 0.995
maxFalseAlarmRate: 0.5
weightTrimRate: 0.95
maxDepth: 1
maxWeakCount: 100
mode: BASIC
Number of unique features given windowSize [32,32] : 510112

===== TRAINING 0-stage =====
<BEGIN
POS count : consumed   4000 : 4000
NEG count : acceptanceRatio    3000 : 1
Precalculation time: 66.801
+----+---------+---------+
|  N |    HR   |    FA   |
+----+---------+---------+
|   1|        1|        1|
+----+---------+---------+
|   2|        1|        1|
+----+---------+---------+
|   3|        1|        1|
+----+---------+---------+
|   4|  0.99625| 0.879667|
+----+---------+---------+
|   5|   0.9955| 0.848333|

・・・
・・・
===== TRAINING 10-stage =====
<BEGIN
POS count : consumed   4000 : 4206
NEG count : acceptanceRatio    0 : 0
Required leaf false alarm rate achieved. Branch training terminated.

```


#4. 結果の確認

上段に自動車、下段にそれ以外を並べた画像を用意しました。


![test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2048607/66f8f770-037f-cad0-afda-16ae666fb095.png)

この画像に対し、カスケード分類器を使って自動車の検出を行います。

```python
import cv2

try:
    img = cv2.imread('C:/temp/test.png')

    if img is None:
        print('ファイルを読み込めません')
        import sys
        sys.exit()
    
    cascade = cv2.CascadeClassifier(
        r'C:/Users/USERNAME/Desktop/opencv4/cascade/cascade/cascade.xml')
    carrect = cascade.detectMultiScale(img)

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

    cv2.imwrite('c:/temp/dobj.png', img)
    cv2.imshow('img', img)

    cv2.waitKey(0)
    cv2.destroyAllWindows()

except:
    import sys
    print("Error:", sys.exc_info()[0])
    print(sys.exc_info()[1])
    import traceback
    print(traceback.format_tb(sys.exc_info()[2]))
```


結果は以下の通りです。

![dobj.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2048607/c375d328-1e8a-b784-c11f-dd8a320b10ae.jpeg)

うまくいっていないことが分かります。。。

#5. エラーコード集

自分が出会ったエラーコードを列挙します。

前に作成されたカスケード分類器が、保存先のフォルダに入っていると以下の警告が出ます。
前回と違う設定となる場合は、一度保存先のフォルダ内のファイルを削除してください。

```
Training parameters are pre-loaded from the parameter file in data folder!
Please empty this folder if you want to use a NEW set of training parameters.
```

ベクトルファイルの幅/高さとカスケード分類器作成ファイル実行時に指定する幅/高さが異なると
以下のエラーがでます。

```
OpenCV: terminate handler is called! The last OpenCV error is:
OpenCV(3.4.16) Error: Assertion failed (_img.rows * _img.cols == vecSize) in CvCascadeImageReader::PosReader::get, file C:\build\3_4_winpack-build-win64-vc15\opencv\apps\traincascade\imagestorage.cpp, line 153
```

カスケードファイル実行時に、ベクトルファイルの画像数より多い画像数を指定すると
以下のエラーが出ます。

```
OpenCV: terminate handler is called! The last OpenCV error is:
OpenCV(3.4.16) Error: Bad argument (> Can not get new positive sample. The most possible reason is insufficient count of samples in given vec-file.
> ) in CvCascadeImageReader::PosReader::get, file C:\build\3_4_winpack-build-win64-vc15\opencv\apps\traincascade\imagestorage.cpp, line 158
```

#6. まとめ

今回は結果がうまくいきません出来したが、やり方の備忘録として記録しました。
もう少し結果改善出来たらまた更新しようと思います。