OpenCVの物体検出を回転不変にさせる

## haarcascadeの特徴量は回転不変じゃない！
OpenCVのhaarcascade_frontalfaceを使って、顔認識をしているのですが、どうもSIFTやSURFと違って、回転に不変な特徴ではないようで、顔をちょっと傾けただけで顔が認識されなくなります。

カメラ画像でリアルタイムに検出する際は、カメラをちゃんとまっすぐに置いておけば、普通の人は首をまっすぐに向けているのでそこそこ認識されるのですが
こんな感じで顔を傾けている写真を単体で検出すると、ちゃんと認識されない。

![Show FACES Image_screenshot_18.09.2015.jpg](https://qiita-image-store.s3.amazonaws.com/0/50474/ee2a0788-a9a6-fe18-ad1f-42a2a8f52716.jpeg)
この写真では、まっすぐ正面を向いてる高城れにさんしか顔の検出がされていない

## 解決方法
一定の角度しか認識しないのなら、画像自体を傾けてしまえばいいのではないか、との着想で、小刻みに傾きを変えた画像全てを判定させれば、全ての傾きの顔が検出されるだろう。

ステップは以下の通り
1. 画像を読み込む
2. 高さ幅がそれぞれ2倍の空画像を用意する
3. 読みこんだ画像を5度だけ傾ける
4. 空の画像の中心に、傾けた画像を貼り付ける
5. 顔を判定する
6. 2~5を繰り返す

図で表すとこんな感じ
![回転不変.001.jpg](https://qiita-image-store.s3.amazonaws.com/0/50474/fe7b1db4-68a1-9750-267f-5bf76e7cca4b.jpeg)

## ソースコード
顔写真を切り抜いて保存するソースコードです。

```py
import cv2
import numpy as np
import os
from math import ceil

temp_face_img_path = 'work/temp/face/'

# 顔判定で使うxmlファイルを指定する。
cascade_path =  os.path.dirname(os.path.abspath(__file__)) + "/haarcascade_frontalface_alt2.xml"

class classifyPhoto:
    def __init__(self):
        print("init")

    # 画像から顔を切り取り、保存、パスを返す
    def crop_face(self, img_path):
        # ファイル名解析
        base_name = os.path.basename(img_path)
        name,ext = os.path.splitext(base_name)
        if (ext != '.jpg') and (ext != '.jpeg') :
            print('not a jpg image')
            return

        img_src = cv2.imread(img_path, 1)
        # グレースケールに変換
        img_gray = cv2.cvtColor(img_src, cv2.COLOR_BGR2GRAY)
        cascade = cv2.CascadeClassifier(cascade_path)

        org_width = img_src.shape[1]
        org_height = img_src.shape[0]
        i = 0

        for j in range(0,71):
            # 拡大画像の作成
            big_img = np.zeros((org_height * 2, org_width * 2 ,3), np.uint8)
            big_img[ceil(org_height/2.0):ceil(org_height/2.0*3.0), ceil(org_width/2.0):ceil(org_width/2.0*3.0)] = img_src

            # 画像の中心位置
            center = tuple(np.array([big_img.shape[1] * 0.5, big_img.shape[0] * 0.5]))

            # 画像サイズの取得(横, 縦)
            size = tuple(np.array([big_img.shape[1], big_img.shape[0]]))

            # 回転させたい角度
            angle = 5.0 * float(j)
            # 拡大比率
            scale = 1.0

            # 回転変換行列の算出
            rotation_matrix = cv2.getRotationMatrix2D(center, angle, scale)

            # アフィン変換
            img_rot = cv2.warpAffine(big_img, rotation_matrix, size, flags=cv2.INTER_CUBIC)
            rot_gray = cv2.cvtColor(img_rot, cv2.COLOR_BGR2GRAY)

            #顔判定
            faces  =  cascade.detectMultiScale(img_rot, scaleFactor=1.2, minNeighbors=2, minSize=(50, 50))
            # 顔があった場合
            if len(faces) > 0:
                for (x,y,w,h) in faces:
                    face = img_rot[y:y+h, x:x+w]
                    file_name =  name + "_face_" + str(i) + ext
                    cv2.imwrite(temp_face_img_path + file_name, face )
                    i += 1

            else :
                print('does not have any faces')

        return 

if __name__ == '__main__':
    classifier = classifyPhoto()
    classifier.crop_faceo('image.jpg')
```

## 結果

![回転不変.002.jpg](https://qiita-image-store.s3.amazonaws.com/0/50474/c1835052-eefb-d938-b747-97c20877e7c1.jpeg)

* 全部で40枚ほど検出
* れにちゃんが8枚、あーりんが9枚、ももかとしおりんが１枚、かなこは0枚、その他顔パーツや服などの誤検出


## 考察
* 判定枚数が増えた分、誤検出が増えた
* メンバーによって偏りがある。単に傾きだけでなく、顔認識の精度が悪いだけの可能性も。
* 複数枚検出されたメンバーは、座標変換＋クラスタリングすればまとめられそう
* 5度刻みにそれぞれ計算してるので、**計算量が72倍**になる

というわけで、チューニングすればもっとマシになるかもしれませんが、多分使えません。