OpenCV 3で犬と猫を分類できるように学習してみる（BOW: Bag Of Visual Words, KNN: k-Nearest Neighbour, k-meansクラスタリング, KAZE）

# はじめに
今回は、画像の中の物体が何なのかをOpenCV3とPython3で推測してみます。

　　![10385.jpg](https://qiita-image-store.s3.amazonaws.com/0/107056/0912d318-3f8c-97e6-c38a-6f4da5abab08.jpeg)　・　![12462.jpg](https://qiita-image-store.s3.amazonaws.com/0/107056/028f06b9-786d-a8ec-727c-6072dfa78196.jpeg)　＝　Dog or Cat ?

物体認識は、訓練フェーズとテストフェーズに分かれ、それぞれ、次のようになります。

+ 訓練フェーズ
    1. 入力画像
    2. 特徴量抽出
    3. 特徴量をクラスタリング
    4. クラスタリングされた特徴量を集計（ヒストグラム）
    5. ヒストグラムと物体カテゴリの相関を学習

+ テストフェーズ
    1. 入力画像
    2. 特徴量抽出
    3. 特徴量をクラスタリング
    4. クラスタリングされた特徴量を集計（ヒストグラム）
    5. ヒストグラムがどの物体カテゴリに近いか距離を計算

今回、それぞれのステップでOpenCV3でサポートされている以下の手法を用いました。

+ ステップ２  
    　KAZE
+ ステップ３～５  
    　BOW: Bag Of Visual Words  
    　・訓練フェーズ：k-meansクラスタリング  
    　・テストフェーズ：KNN: k-Nearest Neighbour  

# OpenCVとは
OpenCV(Open Source Computer Vision Library)はBSDライセンスの映像/画像処理ライブラリ集です。画像のフィルタ処理、テンプレートマッチング、物体認識、映像解析、機械学習などのアルゴリズムが多数用意されています。

■ OpenCVを使った動体追跡の例 (OpenCV Google Summer of Code 2015)
[https://www.youtube.com/watch?v=OUbUFn71S4s](https://www.youtube.com/watch?v=OUbUFn71S4s)

■ インストールと簡単な使い方はこちら
[OpenCV 3（core + contrib）をPython 3の環境にインストール＆OpenCV 2とOpenCV 3の違い＆簡単な動作チェック](http://qiita.com/olympic2020/items/d5d475a446ec9c73261e)  

■ 静止画像の処理についてはこちら
[OpenCVでエッジ検出してみる](http://qiita.com/olympic2020/items/2c3a2bfefe73ab5c86a4)
[OpenCVで各種フィルター処理をする（グラディエント、ハイパス、ラプラシアン、ガウシアン）](http://qiita.com/olympic2020/items/93e01ef22e46b14a60a9)
[OpenCVで特徴点を抽出する（AgastFeature, FAST, GFTT, MSER, AKAZE, BRISK, KAZE, ORB, SimpleBlob）](http://qiita.com/olympic2020/items/62989573a30ec1d8180b)
[OpenCVを使った顔認識（Haar-like特徴分類器）](http://qiita.com/olympic2020/items/04b1b26c1bc2e8081427)
[OpenCVを使って誰の顔なのかを推定する（Eigenface, Fisherface, LBPH）](http://qiita.com/olympic2020/items/8f2f37646179aca68649)
[OpenCVで形状のある物体の輪郭と方向を認識する（主成分分析：PCA、固有ベクトル）](http://qiita.com/olympic2020/items/92fc43226ca37c0f90f6)
[OpenCV 3とPython 3で特徴量マッチング（A-KAZE, KNN）](http://qiita.com/olympic2020/items/caac014b7ab246faf6b1)

■ 動画の処理についてはこちら
[OpenCVで動画をリアルタイムに変換してみる](http://qiita.com/olympic2020/items/ce00fab38d829965db3b)
[OpenCVでWebカメラ/ビデオカメラの動画をリアルタイムに変換してみる](http://qiita.com/olympic2020/items/12a2eceaf65f142ec3df)
[OpenCVでオプティカルフローをリアルタイムに描画する（Shi-Tomasi法、Lucas-Kanade法）](http://qiita.com/olympic2020/items/772549d0fc3c89fb3cc4)
[OpenCVを使った物体追跡（マウスで指定した特徴点をLucas-Kanade法で追跡する](http://qiita.com/olympic2020/items/3d8973f855e963c9d999)
[OpenCVを使ったモーション テンプレート解析（リアルタイムに物体とその動く方向を認識する）](http://qiita.com/olympic2020/items/a4ecf7babdbe710208ae)


# BOW: Bag Of Visual Words
　「Bag Of Visual Words」は、画像の特徴量を１次元のベクトルに次元圧縮する手法です。
　Bag of Wordsとか、Bag of Featuresとか、いろいろな名前で呼ばれています。
　k-meansでk個のカテゴリに分類し、各カテゴリ毎にそのカテゴリに入る特徴量の個数を集計してヒストグラムを作成します。
　テストフェーズでは、KNNを使ってどのクラスに分類されるのかを判定します。
![bagofwords.jpg](https://qiita-image-store.s3.amazonaws.com/0/107056/23687485-a7dc-7e5b-464c-47a7a8250f98.jpeg)
出典：イリノイ大学　http://www.ifp.illinois.edu/~yuhuang/sceneclassification.html

# データを準備する
kaggleのDogs vs. Cats
https://www.kaggle.com/c/dogs-vs-cats/data
のデータを使いました。データを利用するには、ユーザ登録が必要です。

+ train
    - Dog: 12500枚
    - Cat: 12500枚
+ test1
    - DogとCat合わせて25000枚

trainデータは、ファイル名が、cat0.jpg～cat12499.jpg, dog0.jpg～dog12499.jpgのようになっていて、ファイル名からカテゴリが分かるようになっています。test1のデータは、1.jpg～12500.jpgのようになっていて、DogとCatが混在しています。

ディレクトリ構造が次のようになるように画像を配置しました。
トレーニングデータとテストデータは重ならないようにします。

```
train_img
　　├ 0
　　│　└ cat0.jpg～
　　└ 1
　　 　└ dog0.jpg～

test_img
　　├ 0
　　│　└ catxxx.jpg～
　　└ 1
　　 　└ dogyyy.jpg～
```

![cat.png](https://qiita-image-store.s3.amazonaws.com/0/107056/551c0a42-4ede-2760-bea7-87e77bc0d5d0.png)
　　　　　　　　　　　　　　**Catのテストデータ**

![dog.png](https://qiita-image-store.s3.amazonaws.com/0/107056/cc0d90e7-10bf-54fb-08e5-dad23404e11f.png)
　　　　　　　　　　　　　　**Dogのテストデータ**

# プログラム
+ OpenCV 3.1.0
+ Python 3.5.2
+ Windows 10
で実行しました。

```bow.py
# -*- coding: utf-8 -*-
import os
import sys
import cv2
import numpy as np

## 画像データのクラスIDとパスを取得
#
# @param dir_path 検索ディレクトリ
# @return data_sets [クラスID, 画像データのパス]のリスト
def getDataSet(dir_path):
    data_sets = []    
    
    sub_dirs = os.listdir(dir_path)
    for classId in sub_dirs:
        sub_dir_path = dir_path + '/' + classId
        img_files = os.listdir(sub_dir_path)
        for f in img_files:
            data_sets.append([classId, sub_dir_path + '/' + f])
            
    return data_sets

"""
main
"""
# 定数定義
GRAYSCALE = 0
# KAZE特徴量抽出器
detector = cv2.KAZE_create()

"""
train
"""
print("train start")
# 訓練データのパスを取得
train_set = getDataSet('train_img')
# 辞書サイズ
dictionarySize = 2
# Bag Of Visual Words分類器
bowTrainer = cv2.BOWKMeansTrainer(dictionarySize)

# 各画像を分析
for i, (classId, data_path) in enumerate(train_set):
    # 進捗表示
    sys.stdout.write(".")
    # グレースケールで画像読み込み
    gray = cv2.imread(data_path, GRAYSCALE)
    # 特徴点とその特徴を計算
    keypoints, descriptors= detector.detectAndCompute(gray, None)
    # intからfloat32に変換
    descriptors = descriptors.astype(np.float32)
    # 特徴ベクトルをBag Of Visual Words分類器にセット
    bowTrainer.add(descriptors)

# Bag Of Visual Words分類器で特徴ベクトルを分類
codebook = bowTrainer.cluster()
# 訓練完了
print("train finish")

"""
test
"""
print("test start")
# テストデータのパス取得
test_set = getDataSet("test_img")

# KNNを使って総当たりでマッチング
matcher = cv2.BFMatcher()

# Bag Of Visual Words抽出器
bowExtractor = cv2.BOWImgDescriptorExtractor(detector, matcher)
# トレーニング結果をセット
bowExtractor.setVocabulary(codebook)

# 正しく学習できたか検証する
for i, (classId, data_path) in enumerate(test_set):
    # グレースケールで読み込み
    gray = cv2.imread(data_path, GRAYSCALE)
    # 特徴点と特徴ベクトルを計算
    keypoints, descriptors= detector.detectAndCompute(gray, None)
    # intからfloat32に変換
    descriptors = descriptors.astype(np.float32)
    # Bag Of Visual Wordsの計算
    bowDescriptors = bowExtractor.compute(gray, keypoints)

    # 結果表示
    className = {"0": "cat",
                 "1": "dog"}

    actual = "???"    
    if bowDescriptors[0][0] > bowDescriptors[0][1]:
        actual = className["0"]
    elif bowDescriptors[0][0] < bowDescriptors[0][1]:
        actual = className["1"]

    result = ""
    if actual == "???":
        result = " => unknown."
    elif className[classId] == actual:
        result = " => success!!"
    else:
        result = " => fail"

    print("expected: ", className[classId], ", actual: ", actual, result)
```

# 実行結果

```
expected:  cat , actual:  cat  => success!!
expected:  cat , actual:  cat  => success!!
expected:  cat , actual:  cat  => success!!
expected:  cat , actual:  cat  => success!!
expected:  cat , actual:  cat  => success!!
expected:  cat , actual:  cat  => success!!
expected:  cat , actual:  cat  => success!!
expected:  cat , actual:  cat  => success!!
expected:  cat , actual:  cat  => success!!
expected:  cat , actual:  cat  => success!!
expected:  dog , actual:  dog  => success!!
expected:  dog , actual:  cat  => fail
expected:  dog , actual:  dog  => success!!
expected:  dog , actual:  dog  => success!!
expected:  dog , actual:  dog  => success!!
expected:  dog , actual:  dog  => success!!
expected:  dog , actual:  dog  => success!!
expected:  dog , actual:  cat  => fail
expected:  dog , actual:  dog  => success!!
expected:  dog , actual:  dog  => success!!
```

テストデータが少ないですが、正解：18、不正解：2 でした。
カテゴリが２つなので何も処理しなくても50%正解しますが、50%を有意に上回った結果になっているので、訓練の効果があることを確認できます。

# プログラムの注意点
+ 特徴量抽出器
    cv2.BOWImgDescriptorExtractorクラスのcompute()メソッドは、バイナリ系の特徴量を指定すると、下記のエラーがでます。
    
    ```
    error: ..\..\..\modules\features2d\src\matchers.cpp:726: error: (-215) _queryDescriptors.type() == trainDescType in function cv::BFMatcher::knnMatchImpl
    ```
    
    AKAZE, BRISK, ORBはバイナリ特徴量です。
    今回は、実数特徴量のKAZEを利用しました。
    SHIFT, SURFを用いたサンプルプログラムを見かけますが、これらも実数特徴量のため、動作しているものと思われます。ただし、SHIFT, SURFは、OpenCV3では、contribに移動しcoreには含まれていません。
    バイナリ特徴量でBag of Visual Wordsをしたい場合は、自分でヒストグラム作成のロジックを実装してあげる必要があります。単にカウントしてあげるだけなので、実装はそんなに難しくはありません。
    　
+ KNNの探索方法
    cv2.BOWImgDescriptorExtractorクラスのcompute()メソッドは、高速近似近傍探索法（FLANN: Fast Library for Approximate Nearest Neighbors）を使うと、次のエラーがでます。
    
    ```
    error: (-215) The data should normally be NULL! in function NumpyAllocator::allocate
    ```
    
    今回は、FLANNではなく、総当たり法（Brute-Force)を利用しました。
    　
+ クラス数
    今回は、犬と猫の２つのクラスに分類し、高い精度で分類することができました。クラス数を増やした場合、徐々に精度が低下していきます。

# Bag Of Visual Wordsの課題
特徴量をベースに学習を行うBag Of Visual Wordsの課題を考えてみます。

+ 特徴量を検出するアルゴリズムに依存する  
    　エッジを特徴量としてとらえるアルゴリズムを採用すると、当然、平らであることを特徴とする物体に関する学習はうまくいきません。
+ 特徴量同士の位置関係を考慮していない  
    　たとえば、人であれば、人物 → 顔 → 目 のような包含関係があります。ところが、Bag Of Visual Wordsの場合は、画像中に特徴量が出現回数をカウントするだけなので、位置関係が考慮されていません。
+ データの前処理  
    　今回、背景も含めて画像をそのまま処理しました。また、顔だけ、全身など、どの部位なのかも、特に考慮していません。これらを考慮して、学習することで、学習精度が向上することが予想されます。

これら問題を解決したいのであれば、（ディープ）ニューラルネットワークの出番になります。

+ 全画素を入力にし、ニューラルネットワーク自体に特徴量抽出器をになってもらう。
+ 推定部分を非線形に強いニューラルネットワークにまかせる。

あたりの改善を見込むことができます。

つづく...。