[OpenCV]手の認識の手法

## 目標
- 人間の手を認識し、その位置を検出する
- さらにその輪郭を抽出する
- さらに手の状態を知覚する(今回の記事では実現されない)

## 色相による肌色領域の抽出
### 手順
1. 入力をRGBからHSVに変換
2. Hue(色相)の値をもとに画像の二値化を行う
3. フィルタ処理
4. 入力にマスクする

### 実装
```C++
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main() {
	VideoCapture cap(0);
	Mat frame;

	while (1) {
		cap >> frame;

		Mat hsv;
		cvtColor(frame, hsv, COLOR_BGR2HSV);
		Mat hue;
		extractChannel(hsv, hue, 0);

		inRange(hue, 2, 10, hue);

		medianBlur(hue, hue, 9);

		Mat output;
		copyTo(frame, output, hue);

		imshow("output", output);

		int key = waitKey(1);
		if (key == 27) { //escが押されたら終了
			break;
		}
	}

	return 0;
}
```

### 解説
`cvtColor` によって入力されたRGB画像をHSV画像に変換し、 `extractChannel` によって色相(hue)のシングルチャンネルを取り出す。
`inRange` は、上限/下限を指定して二値化を行う関数であり、今回は肌色(4≦H≦20とした)である画素の値を255に、それ以外を0にしている。OpenCV上では色相を0~180であらわすため、H/2の値で設定することに注意。
二値化された画像は光の当たり方などによりノイズが多いため、何かしらのフィルタをかけてましなものにする(今回は `medianBlur` を用いた)。
最後に、肌色と認識した箇所のみを表示するよう入力にマスクをかける。 `copyTo` の第3引数にマスクを設定できる。

### 結果
出力
![output.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256627/53a6a588-db14-aa62-b401-13c88628a932.jpeg)
マスクしたHue画像
![hue.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256627/1f1c601e-f9b0-fb9f-6ae7-f9f33ac3897b.jpeg)
画像右上に映った首も対象領域と認識されてしまっていることがわかる。

### 長所
- 実装が容易
- 処理が軽い
- 意外と精度がいい

### 短所
- 顔などの肌色領域と区別が付かない
- 照明などの影響を受けやすい

## ヒストグラムの逆投影法
### 手順
1. 検出対象のヒストグラムを計算する
2. ヒストグラムを正規化する
3. 逆投影法による抽出を行う
4. 入力にマスクする

### 実装
```C++
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

Mat getHistogram(Mat hue, Point s, Point e) {
	Rect roi(s, e);
	Mat target_rect = hue(roi);

	int histSize = 256;
	float range[] = { 0, 256 };
	const float* histRange = range;

	Mat histogram;
	calcHist(&target_rect, 1, 0, Mat(), histogram, 1, &histSize, &histRange);
	normalize(histogram, histogram, 0, 255, NORM_MINMAX);

	return histogram;
}

int main() {
	VideoCapture cap(0);
	int width = cap.get(CAP_PROP_FRAME_WIDTH);
	int height = cap.get(CAP_PROP_FRAME_HEIGHT);
	Point rect_s = Point(width / 2 - 20, height / 2 - 20);
	Point rect_e = Point(width / 2 + 20, height / 2 + 20);
	Mat frame;
	Mat hand_hist;
	while (1) {
		cap >> frame;

		rectangle(frame, rect_s, rect_e, Scalar(0, 200, 0), 1, 0);

		imshow("frame", frame);

		Mat hsv;
		cvtColor(frame, hsv, COLOR_BGR2HSV);
		Mat hue;
		extractChannel(hsv, hue, 0);

		Mat dst;
		Mat output;
		if (countNonZero(hand_hist) >= 1) {
			float range[] = { 0, 256 };
			const float* histRange = range;
			calcBackProject(&hue, 1, 0, hand_hist, dst, &histRange, 1, true);
			medianBlur(dst, dst, 9);
			threshold(dst, dst, 80, 255, THRESH_BINARY);

			copyTo(frame, output, dst);

			imshow("output", output);
		}

		int key = waitKey(1);
		if (key == 27) { //escが押されたら終了
			break;
		} else if (key == 'c') {
			hand_hist = getHistogram(hue, rect_s, rect_e);
		}
	}
	return 0;
}
```

### 解説
コアとなる関数は `calcHist` と `calcBackProject` の二つである。
Cのキーを入力したときに、ユーザー定義の `getHistogram` を呼んでいる。
`calcHist` で第１引数に与えられたHue画像のヒストグラムを計算し、 `calcBackProject` でそのヒストグラムに類似する領域を抽出している。
`threshold` は前項における `inRange` と同じ役割の関数(使い分けた意味は特にない)。

### 結果
出力
![output.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256627/8fc37a97-6500-631c-252d-bfc5d44a4e80.jpeg)
マスクしたHue画像
![hue.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256627/99083894-4268-7b68-3e4c-00f4b9d136c6.jpeg)
対象ヒストグラムを取る部分をよく気を付けると、首の領域を認識外とできたが、実際には手なのに認識されない部分が多かったり、逆に関係ない部分が認識されすぎたりした。
単一の矩形領域でなく、抽出したい物体(手)全体についてヒストグラムの計算を行うと精度が向上することが予想される。

### 長所
- 単に色相による抽出をするよりも照明の変化にある程度対応ができる

### 短所
- 前項と同様に同色領域の区別が付きにくい
- 事前に対象物のヒストグラムを知る必要がある
- 処理が少し重い?

## Haar-like特徴量によるカスケードマッチング
### 手順
1. 対象物のカスケードファイルを入手/作成する
2. カスケードマッチングによる抽出を行う
3. 入力にマスクする

### 実装
```C++
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>

using namespace std;
using namespace cv;

int main() {
	VideoCapture cap(0);

	CascadeClassifier cascade;
	cascade.load("C:\\opencv\\sources\\data\\haarcascades\\aGest.xml");
	vector<Rect> hands;
	
	Mat frame;
	while (1) {
		cap >> frame; 

		cascade.detectMultiScale(frame, hands, 1.1, 3, 0, Size(80, 80));
		for (int i = 0; i < hands.size(); i++) {
			rectangle(frame, Point(hands[i].x, hands[i].y), Point(hands[i].x + hands[i].width, hands[i].y + hands[i].height), Scalar(0, 200, 0), 3, 0);
		}
		flip(frame, frame, 1);
		imshow("Detect hands", frame);

		int key = waitKey(1);
		if (key == 27) {//escが押されたら終了
			break;
		}
	}
	return 0;
}
```

### 解説
カスケード分類器のクラス `CascadeClassifier` を宣言する。クラス関数 `load` でカスケードファイルを読み込み、 `detectMultiScale` で抽出を実行する。
今回用いたカスケードファイルは [こちら](https://github.com/Aravindlivewire/Opencv/blob/master/haarcascade/aGest.xml) で公開されているものを使用させていただいた。
forで検出した数だけループを回して検出した手の位置に矩形を描画している。

Haar-like特徴量は画像の局所的な明暗差をもとにした特徴量で、複数の明暗差のパターンの組み合わせで評価してマッチングしている。詳しくは参考サイトを参照のこと。

### 結果
出力
![output.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256627/ab904bb1-5e65-ebf1-957a-6f68cd8d638b.jpeg)

画像のように、曲げた指を正面にとらえる形のみ反応した(恐らく明暗差がはっきり表れるため)。疑似的に指を曲げた状態を検知したとも言い換えられるかもしれない。

### 長所
- 色の情報への依存度がより低くなる
- 同色領域の区別が可能

### 短所
- 事前に対象物の特徴量を学習したファイルが必要になる
- 精度向上のための学習にコストがかかる
- 対象により向き不向きがある(手は不得意)
- 回転に弱い

## まとめ
手の認識を試みようとしても、"手自体"を認識することは難しい(機械の処理であるから当然なのだが)。
色相による領域抽出はそのよい例で、実際には肌色画素の領域を求めているに過ぎず、顔なども同様に抽出してしまう。しかし簡易な実装のわりに精度はそこそこ良く、限定的な用途では十分実用ができると感じた。
"手自体"の認識をしたいならば機械学習的なアプローチによって、より人間の認識に近い(?)多角的な評価を行う必要があると感じた。

## 参考
[OpenCV: Back Projection](https://docs.opencv.org/3.4/da/d7f/tutorial_back_projection.html)
[OpenCV3.1.0とVisual C++ 2015による画像処理と認識（５）](http://ishidate.my.coocan.jp/opencv310_5/opencv310_5.htm)
[Finger Detection and Tracking using OpenCV and Python](https://dev.to/amarlearning/finger-detection-and-tracking-using-opencv-and-python-586m)
[OpenCVで顔検出](http://ytskmt14.hateblo.jp/entry/2016/09/29/054215)
[Haar Cascadesを使った顔検出](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_objdetect/py_face_detection/py_face_detection.html#face-detection)