シヨツ鬼のブログ

初心者向けに分かりやすくIT関連の情報を発信しています。

【OpenCV C++】顔認識をリアルタイムで行いたい ~5日目~

f:id:shiyotsuki:20200426151531p:plain
どうも、プログラミングの鬼シヨツ鬼です。
OpenCV(C++)で顔認識をリアルタイムに行いたいぜ」って人に向けて、僕が試行錯誤した開発記を連載6回で書いています。

過去の記事は↓からどうぞ!
【OpenCV C++】顔認識をリアルタイムで行いたい ~1日目~ - シヨツ鬼のブログ


ちなみに、この記事は下記の動画と関連しています。
こちらの動画を先に見ていただくと、実際に動く際のイメージが掴めます。

YouTube:【自作】ゆゆうたさんをバーチャルユーチューバー化してみた

また、OpenCVのインストール方法はこちらの動画をご覧ください。
YouTube:OpenCVのインストール方法【Visual Studio】【C++】

5日目:端っこ対策と斜め対策

顔が端に行ったときにエラーにならないようにします。
また、OpenCVの顔検出は顔が斜めに傾くと途端に検出できなくなってしまいますので、そこを改善していきます。

やりたいこと

・顔が端に行ったときにエラーにならないようにする
・検出範囲を斜めにすることで、顔の傾きに対応できるようにする

f:id:shiyotsuki:20200502162826p:plain
完成イメージ

ソースコード

#include "opencv2/opencv.hpp"
#include "opencv2/highgui.hpp"
#include <vector>

using namespace cv;
using namespace std;

int main()
{
	VideoCapture cap(0); // USBカメラのオープン
	if (!cap.isOpened()) //カメラが起動できなかった時のエラー処理
	{
		return -1;
	}

	Mat frame; //USBカメラから得た1フレームを格納する場所
	CascadeClassifier cascade; //カスケード分類器格納場所
	cascade.load("C:/opencv/sources/data/haarcascades/haarcascade_frontalface_alt.xml"); //正面顔情報が入っているカスケード
	vector<Rect> faces; //輪郭情報を格納場所

	Mat detection_frame;//顔の検出範囲
	Rect roi;
	int detection_flag = 0;//直前に顔を検出したか(0:してない 1:した)
	
	int x = 0;//顔座標の左上のx座標
	int y = 0;//顔座標の左上のy座標
	int x_end = 0;//顔座標の右下のx座標
	int y_end = 0;//顔座標の右下のy座標

	int basic_flag = 0;//連続で顔を検知しているかフラグ(0:いいえ(初めての検知) 1:はい(2連続以上の検知))
	int x_basic = 0;//基準点のX座標
	int y_basic = 0;//基準点のY座標

	int not_found_flag = 1;//連続顔を見つけられなかったフラグ(0:いいえ(見つかった) 1:はい(見つからなかった))
	

	while (1)//無限ループ
	{
		cap >> frame; //USBカメラが得た動画の1フレームを格納

		//直前のフレームで顔が検出されていない場合
		if (detection_flag == 0) {

			//検出範囲はカメラ映像全体とする
			detection_frame = frame;

			//基準点をリセット
			basic_flag = 0;
			x_basic = 0;
			y_basic = 0;

		}
		else {//直前のフレームで顔が検出された場合

			//検出範囲がキャプチャフレーム内に収まるように変換する
			if (x - 50 < 1) {
				x = 51;
			}
			if (y - 50 < 1) {
				y = 51;
			}
			if (x_end + 50 > frame.cols - 1) {
				x_end = frame.cols - 51;
			}
			if (y_end + 50 > frame.rows - 1) {
				y_end = frame.rows - 51;
			}

			//検出範囲として、直前のフレームの顔検出の範囲より一回り(上下左右50pixel)大きい範囲とする
			Rect roi(Point(x - 50, y - 50), Point(x_end + 50, y_end + 50));
			detection_frame = frame(roi);

			//検出範囲をピンク枠で囲う
			rectangle(frame, Point(x - 50, y - 50), Point(x_end + 50, y_end + 50), Scalar(200, 0, 255), 3);

			//連続検索フラグを1(2連続以上の)
			basic_flag = 1;
		}

		detection_flag = 0;

		//格納されたフレームに対してカスケードファイルに基づいて顔を検知
		cascade.detectMultiScale(detection_frame, faces, 1.2, 5, 0, Size(20, 20)); 

		//連続顔検出フラグが0のとき顔を斜めにする
		//(直前に顔を検出していた時だけ斜めの検出を行う)
		if (not_found_flag == 0) {
			not_found_flag=1;
			if (faces.size() == 0) {
				//右に15度傾けるアフィン行列を求める
				Mat trans = getRotationMatrix2D(Point(detection_frame.cols / 2, detection_frame.rows / 2), 15, 1);
				//求めたアフィン行列を使って、ピンク枠内画像を回転する
				warpAffine(detection_frame, detection_frame, trans, detection_frame.size());
				//傾けた画像で顔を検出
				cascade.detectMultiScale(detection_frame, faces, 1.2, 5, 0, Size(20, 20));
			}
			if (faces.size() == 0) {
				//左に15度傾けるアフィン行列を求める(右に15度傾けていたので-30度右に傾けることで実質左に15度傾く)
				Mat trans = getRotationMatrix2D(Point(detection_frame.cols / 2, detection_frame.rows / 2), -30, 1);
				//求めたアフィン行列を使って、ピンク枠内画像を回転する
				warpAffine(detection_frame, detection_frame, trans, detection_frame.size());
				//傾けた画像で顔を検出
				cascade.detectMultiScale(detection_frame, faces, 1.2, 5, 0, Size(20, 20));
			}
		}



		//顔を検出した場合
		if (faces.size() > 0) {
			//顔の検出フラグを1(発見)にする
			detection_flag = 1;

			//連続顔を見つけられなかったフラグを0
			not_found_flag = 0;

			//顔座標の左上の座標を求める
			if (basic_flag == 0) {//初検知の場合

				//初検知の場合は検出された値をそのまま使う
				x = faces[0].x;
				y = faces[0].y;

			}
			else if (basic_flag == 1) {//連続検知の場合

				//連続検知の場合は、検出座標と直前の基準点を使って顔座標を検出する
				//(x_basic - 50):カメラキャプチャ全体の座標から見た検出範囲の左上の座標(ピンク枠の左上)
				//faces[0].x:切り出したフレーム(ピンク枠内)から見た顔の左上の座標(赤枠の左上)

				x = (x_basic - 50) + faces[0].x ;
				y = (y_basic - 50) + faces[0].y ;

			}

			//顔座標の右下の座標を求める
			x_end = x + faces[0].width;
			y_end = y + faces[0].height;

			//基準点を今算出した顔座標に更新する
			x_basic = x;
			y_basic = y;

			rectangle(frame, Point(x, y), Point(x_end, y_end), Scalar(0, 0, 255), 3);
			//printf("(%d,%d) (%d,%d)\n", x, y, x_end, y_end);
		}

		imshow("window", frame);//画像を表示.

		int key = waitKey(1);
		if (key == 113)//qボタンが押されたとき
		{
			break;//whileループから抜ける(終了)
		}
	}
	destroyAllWindows();
	return 0;
}

解説

ポイントは「GetRotationMatrix2D」と「warpAffine」関数です。
GetRotationMatrix2Dを使うと2次元回転のアフィン変換行列(つまり画像の回転に必要な行列)を得ることができます。
warpAffineでは、この行列を使って実際にピンク枠内を傾けています。

まとめ

思ったよりもうまく斜めの顔を検出できるようになりました!
こうやって自分の工夫が目に見えて分かるところが、画像認識の面白さの一つですね!

6日目では、検出した顔の位置に任意の画像を貼り付けることでバーチャルユーチューバーとして仕上げていきます!


続きはこちらの記事へ
https://shiyotsuki.hatenablog.com/entry/virtual_youtuber_6