読者です 読者をやめる 読者になる 読者になる

僕もレムにキスしてもらいたくて頑張った話

この記事は、 Re:ゼロから始める Advent Calendar 2016 - Adventar 25日目の記事です。


Re:ゼロから始める異世界生活」は、今年一番影響を受けた作品でした。
特にヒロイン(?)のレムが健気で可愛いのです。


レムが無償の愛で主人公のスバルを信じる。
スバルはレムが信じて支えてくれるから絶望的な状況でも諦めずに頑張れる。
そんな2人の関係がとても素晴らしく、理想のパートナー関係だなと感じました。


そして18話のこのシーン
f:id:bucchi49:20161224165334p:plain


妬ましい。
じゃなかった、羨ましい。


私もレムにキスして欲しい人生でした。
あまりにも羨ましく思ったので、Google Cloud Vision APIを利用して顔認識を行い、レムにキスしてもらう処理を作成しました。



Google Cloud Vision APIとは

Google Cloud Vision APIGoogleが提供する画像解析APIです。
APIに画像と幾つかのパラメータを渡すと、解析結果が受け取れます。


リクエストを投げるエンドポイントはこちら
https://vision.googleapis.com/v1/images:annotate?key=APIキー
APIキーが必要ですので、各自Google Cloud PlatformからAPIを作成してAPIキーを取得してください。


今回は人の顔を認識するのに使用しましたが、観光地やロゴを検出することも可能です。


phpで処理を作成する場合、以下の記事がとても参考になります。
syncer.jp
qiita.com




↓試しにこの画像をAPIに渡してみます。
f:id:bucchi49:20161225011414j:plain


すると次のような解析結果が得られます。

{
    "responses": [{
        "faceAnnotations": [ {
            "boundingPoly": {
                "vertices": [
                    {"x": 284, "y": 32 }, 
                    { "x": 381, "y": 32 }, 
                    { "x": 381, "y": 145 }, 
                    { "x": 284, "y": 145 }
                ]
            },
            "fdBoundingPoly": {
                "vertices": [
                    { "x": 299, "y": 70 },
                    { "x": 366, "y": 70 }, 
                    { "x": 366, "y": 138 }, 
                    { "x": 299, "y": 138 }
                ]
            },
            "landmarks": [
                { "type": "LEFT_EYE", "position": { "x": 320.05154, "y": 86.781647, "z": 0.00085658336 } }, 
                { "type": "RIGHT_EYE", "position": { "x": 347.45663, "y": 91.585304, "z": -1.0987207 } }, 
                { "type": "LEFT_OF_LEFT_EYEBROW", "position": { "x": 310.69406, "y": 77.5779, "z": 1.7462301 } }, 
                { "type": "RIGHT_OF_LEFT_EYEBROW", "position": { "x": 327.39276, "y": 81.8262, "z": -7.3354692 } }, 
                { "type": "LEFT_OF_RIGHT_EYEBROW", "position": { "x": 341.57535, "y": 84.248291, "z": -7.9102631 } }, 
                { "type": "RIGHT_OF_RIGHT_EYEBROW", "position": { "x": 359.19348, "y": 86.599365, "z": -0.10983154 } }, 
                { "type": "MIDPOINT_BETWEEN_EYES", "position": { "x": 333.25031, "y": 89.615265, "z": -6.7586174 } }, 
                { "type": "NOSE_TIP", "position": { "x": 329.57935, "y": 107.70503, "z": -11.99788 } }, 
                { "type": "UPPER_LIP", "position": { "x": 328.4032, "y": 116.22711, "z": -3.2120161 } }, 
                { "type": "LOWER_LIP", "position": { "x": 326.9353, "y": 126.67342, "z": 1.0525653 } }, 
                { "type": "MOUTH_LEFT", "position": { "x": 314.65683, "y": 114.9194, "z": 6.5293856 } }, 
                { "type": "MOUTH_RIGHT", "position": { "x": 340.93924, "y": 120.02564, "z": 5.2773614 } }, 
                { "type": "MOUTH_CENTER", "position": { "x": 327.88455, "y": 120.83481, "z": -0.051624309 } }, 
                { "type": "NOSE_BOTTOM_RIGHT", "position": { "x": 337.96179, "y": 110.03934, "z": -0.60749954 } }, 
                { "type": "NOSE_BOTTOM_LEFT", "position": { "x": 322.09082, "y": 106.36255, "z": -0.067581393 } }, 
                { "type": "NOSE_BOTTOM_CENTER", "position": { "x": 329.0321, "y": 111.29434, "z": -4.3742375 } }, 
                { "type": "LEFT_EYE_TOP_BOUNDARY", "position": { "x": 319.22333, "y": 85.072235, "z": -2.2954779 } }, 
                { "type": "LEFT_EYE_RIGHT_CORNER", "position": { "x": 324.73248, "y": 89.239838, "z": 0.08430361 } }, 
                { "type": "LEFT_EYE_BOTTOM_BOUNDARY", "position": { "x": 319.17694, "y": 88.668655, "z": 0.15983413 } }, 
                { "type": "LEFT_EYE_LEFT_CORNER", "position": { "x": 313.7431, "y": 85.492928, "z": 2.8691576 } }, 
                { "type": "LEFT_EYE_PUPIL", "position": { "x": 318.46722, "y": 86.77636, "z": -0.65383321 } }, 
                { "type": "RIGHT_EYE_TOP_BOUNDARY", "position": { "x": 348.23621, "y": 90.471146, "z": -3.4103446 } }, 
                { "type": "RIGHT_EYE_RIGHT_CORNER", "position": { "x": 353.85083, "y": 92.800095, "z": 1.3618857 } }, 
                { "type": "RIGHT_EYE_BOTTOM_BOUNDARY", "position": { "x": 347.39783, "y": 93.831024, "z": -0.95386577 } }, 
                { "type": "RIGHT_EYE_LEFT_CORNER", "position": { "x": 341.71686, "y": 91.814034, "z": -0.63955253 } }, 
                { "type": "RIGHT_EYE_PUPIL", "position": { "x": 348.27011, "y": 92.328812, "z": -1.8369875 } }, 
                { "type": "LEFT_EYEBROW_UPPER_MIDPOINT", "position": { "x": 319.34015, "y": 76.726822, "z": -5.3895044 } }, 
                { "type": "RIGHT_EYEBROW_UPPER_MIDPOINT", "position": { "x": 350.85443, "y": 82.603157, "z": -6.6037073 } }, 
                { "type": "LEFT_EAR_TRAGION", "position": { "x": 300.65076, "y": 90.483795, "z": 39.347996 } }, 
                { "type": "RIGHT_EAR_TRAGION", "position": { "x": 366.61346, "y": 102.76015, "z": 36.823357 } }, 
                { "type": "FOREHEAD_GLABELLA", "position": { "x": 334.30682, "y": 83.3632, "z": -8.7228413 } }, 
                { "type": "CHIN_GNATHION", "position": { "x": 324.75366, "y": 138.24594, "z": 7.5888171 } }, 
                { "type": "CHIN_LEFT_GONION", "position": { "x": 299.68307, "y": 111.66374, "z": 31.079149 } }, 
                { "type": "CHIN_RIGHT_GONION", "position": { "x": 359.29422, "y": 122.75174, "z": 28.786921 } } 
            ],
            "rollAngle": 10.12269, 
            "panAngle": -2.2039406, 
            "tiltAngle": -11.073109, 
            "detectionConfidence": 0.89102042, 
            "landmarkingConfidence": 0.75468844, 
            "joyLikelihood": "VERY_LIKELY", 
            "sorrowLikelihood": "VERY_UNLIKELY", 
            "angerLikelihood": "VERY_UNLIKELY, 
            "surpriseLikelihood": "VERY_UNLIKELY", 
            "underExposedLikelihood": "VERY_UNLIKELY", 
            "blurredLikelihood": "VERY_UNLIKELY", 
            "headwearLikelihood": "VERY_UNLIKELY"
        } 
    ]} 
]}


この結果のうち、

"boundingPoly": {
    "vertices": [
        {"x": 284, "y": 32 }, 
        { "x": 381, "y": 32 }, 
        { "x": 381, "y": 145 }, 
        { "x": 284, "y": 145 }
    ]
}

この部分が顔のエリアの座標になります。
顔の座標が分かれば後はレムちゃんの画像をうまいこと合わせてあげるだけです。


ちなみにGoogle Cloud Vision APIでは顔の各パーツの座標も取得できます。
どうせならスバル君と同じようにおでこにチューをして欲しいです。


FOREHEAD_GLABELLAという名称で眉間の座標が取得できるので、この座標を使って処理を作っていきます。

{
    "type": "FOREHEAD_GLABELLA", 
    "position": {
        "x": 334.30682, 
        "y": 83.3632,
        "z": -8.7228413
    }
}


顔の位置、大きさ、眉間の場所が分かればあとは簡単です。
用意したレムの画像をリサイズして、眉間の位置に合わせてあげるだけです。



できたもの

残念ながら私は「ふぉとしょっぷ」というツールを使いこなせないので、Windowsのペイントを使って画像を用意しました。
用意したものがこちら↓
f:id:bucchi49:20161224180932p:plain


勤勉さが足りていませんがご容赦ください。


この画像とPHPのGDライブラリを使って画像の合成をおこないます。
そして最終的に完成したコードがこちら↓

<?php
$api_key = "APIキー";
$image_path = '画像のパス';

// リクエスト用のパラメータを作成
$request_params = array(
	"requests" => array(
		array(
			"image" => array(
				"content" => base64_encode(file_get_contents($image_path))
			),
			"features" => array(
				array(
					"type" => "FACE_DETECTION" ,
					"maxResults" => 3
				)
			)
		)
	)
);
$request_json = json_encode( $request_params ) ;


// リクエストを実行
$curl = curl_init();
curl_setopt( $curl, CURLOPT_URL, "https://vision.googleapis.com/v1/images:annotate?key=" . $api_key ) ;
curl_setopt( $curl, CURLOPT_HEADER, true ) ;
curl_setopt( $curl, CURLOPT_CUSTOMREQUEST, "POST" ) ;
curl_setopt( $curl, CURLOPT_HTTPHEADER, array( "Content-Type: application/json" ) ) ;
curl_setopt( $curl, CURLOPT_SSL_VERIFYPEER, false ) ;
curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true ) ;
if( isset($referer) && !empty($referer) ) curl_setopt( $curl, CURLOPT_REFERER, $referer ) ;
curl_setopt( $curl, CURLOPT_TIMEOUT, 15 ) ;
curl_setopt( $curl, CURLOPT_POSTFIELDS, $request_json ) ;
$res1 = curl_exec( $curl ) ;
$res2 = curl_getinfo( $curl ) ;
curl_close( $curl ) ;


// 取得したデータ
$response_json = substr( $res1, $res2["header_size"] ) ;
$header = substr( $res1, 0, $res2["header_size"] ) ;
$array = json_decode($response_json, true);


//結果から必要な情報を取得
$x1 = $array['responses'][0]['faceAnnotations'][0]['boundingPoly']['vertices'][0]['x'];
$y1 = $array['responses'][0]['faceAnnotations'][0]['boundingPoly']['vertices'][0]['y'];
$x2 = $array['responses'][0]['faceAnnotations'][0]['boundingPoly']['vertices'][1]['x'];
$y2 = $array['responses'][0]['faceAnnotations'][0]['boundingPoly']['vertices'][1]['y'];
$x3 = $array['responses'][0]['faceAnnotations'][0]['boundingPoly']['vertices'][2]['x'];
$y3 = $array['responses'][0]['faceAnnotations'][0]['boundingPoly']['vertices'][2]['y'];
$x4 = $array['responses'][0]['faceAnnotations'][0]['boundingPoly']['vertices'][3]['x'];
$y4 = $array['responses'][0]['faceAnnotations'][0]['boundingPoly']['vertices'][3]['y'];
$head_x = $array['responses'][0]['faceAnnotations'][0]['landmarks'][29]['position']['x'];
$head_y = $array['responses'][0]['faceAnnotations'][0]['landmarks'][29]['position']['y'];


// 画像の中の顔の大きさに合わせて、レムの画像を修正
$org_file = "rem.png";
list($org_w, $org_h) = getimagesize($org_file);
$copy_w = ($x2 - $x1);
$copy_h = ($y3 - $y1);
$png_name = "output" . $copy_w . "x" . $copy_h . ".png";
$org_img = imagecreatefrompng($org_file);
$copy_img = imagecreatetruecolor($copy_w, $copy_h);
imagealphablending($copy_img, false);
imagesavealpha($copy_img, true);
imagecopyresized($copy_img, $org_img, 0, 0, 0, 0, $copy_w, $copy_h, $org_w, $org_h);
imagepng($copy_img, $png_name);
imagedestroy($org_img);
imagedestroy($copy_img);


// レムの画像をおでこあわせて合成する
$ext = substr($image_path, -3);
$create_file_name = 'result.' . $ext;

switch($ext) {
	case 'png':
		$im = imagecreatefrompng($image_path);
		break;
	case 'jpg':
		$im = imagecreatefromjpeg($image_path);
		break;
	default: return 'error';
}

$rem = imagecreatefrompng($png_name);

$im_width = imagesx($im);
$im_height = imagesy($im);
$rem_width = imagesx($rem);
$rem_height = imagesy($rem);

imagecopy(
	$im,
	$rem,
	$head_x - $copy_w * (248 / $org_w),
	$head_y - $copy_h * (190 / $org_h) * 1.2,
	0,
	0,
	$rem_width,
	$rem_height);

imagepng( $im, $create_file_name);

imagedestroy($im);


// 生成した画像を表示
echo '<img src="' . $create_file_name . '">';

2行目の$api_keyにはAPIキーを、
3行目の$image_pathには画像のパスを入力します。


この処理を実行すると先ほどの男性の画像はこのようになります↓
f:id:bucchi49:20161225015718j:plain


用意した画像があまりよろしくないので若干不自然な感じですが、
だいたいおでこの位置にレムがキスしているのが分かるかと思います。


あとは自分の顔写真で実行して、ニヤニヤするだけです!

まとめ

日頃ストレスにさらされているエンジニアの皆さんにとっては、
レムの献身的な姿は心の癒やしになったのではないでしょうか?
少なくとも私はそうでした。

レムはいいぞ!