Python3とOpenCVを使ってSURF特徴量でBag Of Visual Words

なかなかSURF,SIFT特徴量を用いたBOWの記事が見つけられなかったので、個人的に試してみた結果を記録として。
ベースは[OpenCV 3で犬と猫を分類できるように学習してみる（BOW: Bag Of Visual Words, KNN: k-Nearest Neighbour, k-meansクラスタリング, KAZE）](https://qiita.com/hitomatagi/items/883770046de5746a5deb)を参考に作成。

## きっかけ
機械学習で画像の分類をしよう！となった時にAlexNetとかVGG16とかInceptionV3とか色々試してみたけど、元のデータの質が悪いせいか90％前後で頭打ちになったので、特徴量抽出からのその結果とアンサンブルでどうにかこうにかできないかする為に、今回BagOfVisualWordsを使ってみました。

<br>
物体検知の流れや使用したライブラリ・手法については先述したリンク先に記載されている為割愛。

## データを準備する
今回のデータセットは独自で用意したデータセットを用いています。

train,testをそれぞれディレクトリ分けしており、画像ファイルと別にそれに対応するラベルリストのデータを用意しました。

```.csv
0,./images/train/0.jpg
8,./images/train/1.jpg
3,./images/train/2.jpg
...
```

## プログラム
先述したリンク先のプログラムを少し改変し作成。
進捗表示をtqdm、特徴量をSURFで計算。
この先も計算した特徴量を再利用する予定なのでコードブックのpickle化も実行

```.py
detector = cv2.xfeatures2d.SURF_create()
```
の部分を

```.py
detector = cv2.xfeatures2d.SIFT_create() #SIFT特徴量
detector = cv2.KAZE_create() #KAZE特徴量
```
とすることも可能。

```bag_of_visual_words.py
# coding:utf-8

import os
import sys
import cv2
import numpy as np
import csv
from tqdm import tqdm
import pickle


def getDataSet(csv_name):
    data_sets = []
    
    csv_file = open(csv_name, 'r')
    f = csv.reader(csv_file, delimiter=',', lineterminator='\n')
    for row in f:
        data_sets.append([row[0], row[1]])
    
    return data_sets

if __name__ == '__main__':
    GRAYSCALE = 0
    
    # 特徴量を選択
    # SURF feature
    detector = cv2.xfeatures2d.SURF_create()


    print('train_start')

    # 訓練用データのパス、ラベルを取得
    train_set = getDataSet('labels_train.csv')
    # クラス数
    dictionarySize = 22

    # 分類器
    bowTrainer = cv2.BOWKMeansTrainer(dictionarySize)
    
    # 画像の分析
    for i, (classId, dataPath) in enumerate(tqdm(train_set)):
        # グレースケール画像の読み込み
        gray = cv2.imread(dataPath, GRAYSCALE)
        # 特徴点とその特徴を計算
        keypoints, descriptors = detector.detectAndCompute(gray, None)
        # 型変換
        descriptors = descriptors.astype(np.float32)
        # 特徴ベクトルを分類器にセット
        bowTrainer.add(descriptors)
    
    # Bag Of Visual Words分類器で特徴ベクトルを作成
    codebook = bowTrainer.cluster()

    print("train finish")
    
    # 作成したコードブックをpickle化
    with open('codebook.pickle', 'wb') as f:
        pickle.dump(codebook, f)
    
    # 作成したコードブックを再利用する場合はコメントアウト
    '''
    with open('codebook.pickle', 'rb') as f:
        codebook = pickle.load(f)
    '''

    print("test start")

    # テストデータのパス、ラベルを取得
    test_set = getDataSet('labels_test.csv')
    # KNNを使って総当りでマッチング
    matcher = cv2.BFMatcher()

    # 分類器を用意、学習結果のコードブックをセット
    bowExtractor = cv2.BOWImgDescriptorExtractor(detector, matcher)
    bowExtractor.setVocabulary(codebook)

    res = 0
    total = 0
    for i, (classId, dataPath) in enumerate(test_set):
        total += 1

        # グレースケール画像の読み込み
        gray = cv2.imread(dataPath, GRAYSCALE)
        # 特徴点とその特徴を計算
        keypoints, descriptors = detector.detectAndCompute(gray, None)
        # 型変換
        descriptors = descriptors.astype('float32')
        # Bag Of Visual Wordsの計算
        bowDescriptors = bowExtractor.compute(gray, keypoints)

        # 計算結果から最も近いクラスを選択
        pred = bowDescriptors[0].argmax()
        
        if str(classId) == str(pred):
            res += 1
        print('expected : {}, predict_result : {}'.format(str(classId), str(pred)))
        #print(classId, bowDescriptors)

    # 結果
    print('accuracy: {} (correct/total = {}/{})'.format(str(res/total), str(res), str(total)))
```

## AttributeError: module 'cv2' has no attribute 'xfeatures2d'が出る場合
[こちら](http://danglingfarpointer.hatenablog.com/entry/2016/01/21/235921)のサイトをみるとOpenCV3からSIFT,SURFが別モジュールに別れているそう。
リンク先のサイトではopnecvのインストール方法を変えて紹介しているが、今回は

```
pip install opencv-contrib-python
```
を実行して解決。
OpenCVインストール時に--with-contribにしたかどうかが曖昧なので、上記のpipで解決できない場合はそちらを試すといいかも？