旅の途中。

ここは歩き始めた旅の途中

Catch the Wink! (Part 1)

はじめに

水瀬いのりさんの3rdアルバム「Catch the Rainbow!」が発売されました。おめでとうございます。
自分も暇さえあれば聴いていますが、ようやく「Step Up!」の世界観(?)が掴めてきた感があります。先は長いです。

アルバムの発売に伴って、彼女が初めて作詞に挑戦した曲であり、表題曲でもある「Catch the Rainbow!」のMVもYouTubeで公開されています。
www.youtube.com

この動画の魅力は実際に観てもらえば分かると思いますが、ひとつ挙げるならば4:06くらいのウィンクは皆好きではないでしょうか。私は好きです。*1

TwitterというSNSのTimeLineをぼーっと眺めていたところ、YuiOguraアイコンの方がこのウィンクのスクショを流しているのを見て保存しようとした瞬間に(どうせならもっと良い画質で保存するか〜)と思ったところまでは良かったのですが、色々試行錯誤したもののYouTubeの動画をスクショするのには限界がありました。

最終的に「折角なのでこの機会にopenCV触りながら水瀬いのりさんのご尊顔を検出してCatch the Winkしていくか〜」となりました。

この記事では簡単にC++を用いたopenCVで画像の表示、顔の検出、動画読み込み、保存までを解説していきます。

はじめに断っておきますが、この記事では仕様や原理にはあまり深入りせず、「動いたのでよし」という方針を採用しています。突っ込んで知りたい方は適宜ググっていただくか、リンクを貼っている場合もあるのでそこから飛んでいただけると良いかと思います。

もし間違った発言などありましたら指摘など歓迎です。

Step 1. 動画の入手

まず、水瀬いのりさんのウィンクが収められた動画を入手する必要があります。

自分はここで買いました。400円するだけあって画質は十分です。

Step 2. 画像の読み込み、表示

言い忘れてましたがopenCVのインストールは各自でお願いします(えぇ...
ちなみに自分の環境はMacOSgccコンパイルしてます。openCVのバージョンは3だった気がする。
まずは普通の画像を表示します。ソースと同じディレクトリに inori00.png を置いています。

#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/highgui.hpp>

using namespace std;
using namespace cv;

int main()
{
	//入力画像読み込み
	Mat img = imread("inori00.png", IMREAD_UNCHANGED);

	if (img.empty())
	{
		cout << "READ FAILED..." << endl;
		return -1;
	}

	//windowを作る(必須)
	namedWindow("inori", WINDOW_AUTOSIZE);
	//指定したwindowに画像を表示
	imshow("inori", img);

	waitKey(0);
	return 0;
}

これを普通にコンパイルするとエラーが出ると思います。大抵の場合は以下のようにコンパイルオプションを指定すると直ります。

g++ test.cpp  `pkg-config --cflags --libs opencv`

毎回これを入力するのは骨なので、自分は以下のようにmakefileに書き込んでmakeするようにしています。

main: test.cpp
	g++ test.cpp -std=gnu++14 -O3 `pkg-config --cflags --libs opencv`

cv::Matというのは画像のためのclassで、この中に画像についてのいろいろな情報が保存されています。詳しくは(ここ)に載っています。

あとはコメントから察してください。(えぇ...

画像が表示されたら成功です。

Step 3. 顔の検出

続いて人間の顔の検出に写っていきたいと思います。
この検出器を一から書くとなるとそれこそ水瀬いのりさんが次のアルバムを出すまでかかってしまいそうですが、なんとopenCVにはあらかじめ検出器が用意されています。数ある検出器の中で、今回はCascadeClassifierというものを使ってみたいと思います。

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

using namespace std;
using namespace cv;

int main()
{
	//入力画像読み込み
	Mat img = imread("inori001.png", IMREAD_UNCHANGED), gray;
	if (img.empty())
	{
		cout << "READ FAILED..." << endl;
		return -1;
	}

	//分類機に顔識別用パラメータを読み込ませる
	CascadeClassifier cascade;
	cascade.load("haarcascade_frontalface_alt.xml");

	vector<Rect> faces;

	// 検出しやすいようにgray画像を用いる
	cvtColor(img, gray, CV_RGB2GRAY);
	cascade.detectMultiScale(gray, faces, 1.1, 3);

	int faceNum = faces.size();

	cout << faceNum << " faces detected." << endl;

	for (int i = 0; i < faceNum; i++)
	{
		//検出した顔の周りに赤い長方形を描画
		rectangle(img, Point(faces[i].x, faces[i].y), Point(faces[i].x + faces[i].width, faces[i].y + faces[i].height), Scalar(0, 0, 255), 3, CV_AA);
	}

	//windowを作り、表示する
	namedWindow("inori", WINDOW_AUTOSIZE);
	imshow("inori", img);

	waitKey(0);

	imwrite("res.jpg", img);
	return 0;
}

順を追って説明しますと、

  1. 画像を読み込みます
  2. 検出器のインスタンスを宣言し、パラメータを読み込ませます。
    • このパラメータは(ここ)からダウンロードしてきて同じディレクトリに置いています。猫の顔とかも検出できるようです。
  3. 顔を検出しやすいように画像をグレースケール化して、検出器にかけます。結果は長方形(cv::Rect)のvectorに格納されます。cv::Rectは角の座標と各辺の長さなどを持っているクラスです。詳しくは(ここ)に。
  4. 見つけた顔の数を標準出力に表示し、実際に長方形を元の画像(のデータが格納されたcv::Mat)の上に描画します。あとは先ほどと同じように画像を表示します。
    • cv::Scalarは図形の枠線のRGBを指定しています。正確にはBGRの順です。
    • cv::rectangleについては(ここ)に。
  5. 最後に、cv::imwriteで画像を保存しています。同名のファイルがなければ新しく作られますが、もし先にあったら上書きされてしまうのでご注意を。

こんな感じになったら成功です。
f:id:sifi_border:20190415021136p:plain



終わりに

想像以上に長くなってしまったので一旦ここで区切りたいと思います。
動画についても一応できてはいるのですが、


こんな感じで精度がガバガバなのでもうちょっと調べてから書きたいと思います。

(改善しなかったら妥協するかもしれません...)

ここまでお付き合いくださりありがとうございました。
それではまた。

*1:nico生で言及されていましたがあのウィンクは台本だそうです。でもそんなの関係ねぇ。