シヨツ鬼のブログ

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

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

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

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


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

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

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

4日目:顔の追従検索で検出速度向上!

3日目のアイデアを実現します。

やりたいこと

・(今度こそ)顔を一度検知した場合、次のフレームでは、直前の顔検知位置より一回り大きい範囲を検出範囲とする
f:id:shiyotsuki:20200501165336p:plain

f:id:shiyotsuki:20200502160550p: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座標
	

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

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

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

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

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

			//検出範囲として、直前のフレームの顔検出の範囲より一回り(上下左右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)); 

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

			//顔座標の左上の座標を求める
			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;
}

解説

ポイントは「切り出したフレーム内で検知した顔の座標」と「カメラキャプチャ全体の座標」が異なるということです。
切り出したフレーム内で顔の検出を行うと、そのフレーム内での座標が取れるため、そのままその座標を使ってカメラキャプチャのフレームに四角を描くと全部左上に表示されるようなことになります。

まとめ

検出速度はかなり向上しました!
3日目では座標の扱いで混乱しましたが、落ち着いてやればそんなに難しいものではなかったですね(笑)

また、実は今のソースコードだと次のような問題が発生します。
・顔が画面の隅にある場合、検出範囲が枠外となり、エラーで異常停止する
・顔が少しでも斜めになると検出不可となる。

5日目ではここを解消していきます。

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

最後まで読んでくれてありがとう。
参考になったら「☆」を押してね。そして僕のYouTubeTwitterもよろしくね。