シヨツ鬼のブログ

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

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

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

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


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

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

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

6日目:ついに、バーチャルユーチューバー化!?

リアルタイムでの顔検出が行えるようになったので、検出した顔の位置に任意の画像を貼り付けることでバーチャルユーチューバー化していきます。

やりたいこと

・検出した顔の位置に任意の画像を貼り付ける
今回は、自らの存在をフリー素材宣言をしているゆゆうたさんの画像を使わせていただきます。

f:id:shiyotsuki:20200502114534p:plain
今回用いるゆゆうたさんの画像

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

ソースコード

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

using namespace cv;
using namespace std;

/**------------------------------------------------------------*
 * @fn          OpenCVのピクチャーインピクチャ
 * @brief       画像内に画像を貼り付ける(位置を座標で指定)
 * @par[in ]    srcImg  背景画像
 * @par[in ]    smallImg    前景画像
 * @par[in ]    p0  前景画像の左上座標
 * @par[in ]    p1  前景画像の右下座標
 *------------------------------------------------------------*/
Mat PinP_point(const cv::Mat &srcImg, const cv::Mat &smallImg, const cv::Point2f p0, const cv::Point2f p1)
{
	//背景画像の作成
	cv::Mat dstImg;
	srcImg.copyTo(dstImg);

	//3組の対応点を作成
	vector<cv::Point2f> src, dst;
	src.push_back(cv::Point2f(0, 0));
	src.push_back(cv::Point2f(smallImg.cols, 0));
	src.push_back(cv::Point2f(smallImg.cols, smallImg.rows));

	dst.push_back(p0);
	dst.push_back(cv::Point2f(p1.x, p0.y));
	dst.push_back(p1);

	//前景画像の変形行列
	cv::Mat mat = cv::getAffineTransform(src, dst);

	//アフィン変換の実行
	cv::warpAffine(smallImg, dstImg, mat, dstImg.size(), INTER_LINEAR, cv::BORDER_TRANSPARENT);
	//imshow("winName", dstImg);

	return dstImg;
}

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:はい(見つからなかった))
	
	//ゆゆうたさんの顔画像読み込み
	//画像をフルパスで指定する。区切り文字は「/」または「\\」にする。「\」はダメ
	Mat faceImg = imread("C:/face_base.png");

	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);

			//顔の検出位置にゆゆうたさんの顔画像を貼り付ける
			frame = PinP_point(frame, faceImg, Point(x, y), Point(x_end, y_end));
			//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;
}

解説

OpenCVでは意外にも画像内に画像を貼り付ける関数が用意されていません。
そのため、関数を自作する必要があります。
下記サイトを参考にしました。
OpenCVで画像上に別の画像を描画する - Qiita

まとめ

これで、今回の連載は終了です。
webカメラを用いて、リアルタイムで顔認識をして、その部分に任意の画像を貼り付ける簡易バーチャルユーチューバーが完成しました。
ここまで連載を読んでいただきありがとうございました!

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