@Satoh_D no blog

大分にUターンしたので記念に。調べたこととか作ったこととか食べたこととか

Amazon RekognitionをLaravelから使う

案件でWebカメラから取得した画像を元に表情を分析してどうのこうのする必要があり調べてみたのでメモ。
Amazon Rekognitionを選定した理由は料金がGCPのサービスに比べて安かったから…!
Nuxt.jsから直接APIに対してPOSTすることもできるんだけど、CORSを回避する手段も用意しておきたいと思ってLaravelも利用している

環境

  • MacOS: 10.15.6
  • node.js: v14.12.0
  • yarn: v1.22.5
  • Nuxt.js: v.2.14.6
  • Laravel: 5.3.8

手順

1-1. IAMでユーザを作る

1-2. AWS SDKを利用して実装

まずはSDKをインストール

$ composer require aws/aws-sdk-php

1-3. 表情分析用のコントローラを作成する

$ php artisan make:controller FaceController

Amazon Rekognition自体はAws\Rekognition\RekognitionClientを通して行う

<?php

namespace App\Http\Controllers;

use Aws\Rekognition\RekognitionClient;
use Illuminate\Http\Request;

class FaceController extends Controller
{

    /* フロントからのPOSTデータを受け取る */
    public function post(Request $request)
    {
        $photo = $request->photo;
        $detectedData = $this->detectFaces($photo);

        $ret = [
            'detectedData' => $detectedData,
        ];

        return $ret;
    }

    /* Amazon Rekognitionで表情分析を行う */
    private static function detectFaces($file)
    {
        $options = [
            'region' => 'ap-northeast-1',
            'version' => 'latest',
            'credentials' => [
                'key' => 'XXXX',    // ここを編集してね
                'secret' => 'XXXX'  // ここを編集してね
            ]
        ];
        $result = null;
        try {
            $client = new RekognitionClient($options);
            // 顔検出
            $result = $client->detectFaces([
                'Image' => [
                    'Bytes' => file_get_contents($file),
                ],
                // 'ALL': 全ての結果が返ってくる
                // 'DEFAULT': 一部結果(顔の角度, 明るさetc)のみ返る
                'Attributes' => ['ALL']
            ]);
        } catch (\Exception $e) {
            echo $e->getMessage() . PHP_EOL;
        }

        return isset($result['FaceDetails']) ? $result['FaceDetails'] : null;
    }
}

api.phpにルーティングを追加する

...
Route::group(['prefix' => 'face'], function ($route) {
    $route->post('/post', 'FaceController@post');
});

1-4. フロント部分を作成する

フロントは先日ブログに書いたNuxt.jsでWebカメラで映像を撮影 & 画像の取得をしてみる - @Satoh_D no blogのコードに以下を追記する

<template>
  <section class="section">
    <div class="columns">
      <div class="column"><video id="localvideo" ref="localVideo" autoplay :srcObject.prop="localstream" width="640" height="350" ></video></div>
    </div>
    <div class="columns">
      <div class="column">
        <canvas id="localPic" ref="localPicCanvas" width="640" height="350" ></canvas>
      </div>
    </div>
    <div class="buttons">
      <b-button @click="getUserMedia">映像を取得する</b-button>
      <b-button @click="getUserPic">写真を撮る</b-button>
      <b-button @click="getEmotion">表情判定</b-button>
    </div>
  </section>
</template>

export default {
  data() {
    return {
      localstream: undefined,
      canvas: undefined,
      video: undefined,  // movie -> videoに変更
      photo: undefined,  // 追加
      res: undefined,  // 追加
      streamConst: {
        video: {
          width: 640,
          height: 350,
        },
      },
    }
  },
  mounted() {
    this.canvas = this.$refs.localPicCanvas
    this.video = this.$refs.localVideo
  },
  computed: {},
  methods: {
    getLocalMediaStream(mediaStream: any) {
      this.localstream = mediaStream
    },
    handleLocalMediaStreamError(error: string) {
      console.error(`navigator.getUserMedia error: ${error}`)
    },
    getUserMedia() {
      navigator.mediaDevices
        .getUserMedia(this.streamConst)
        .then(this.getLocalMediaStream)
        .catch(this.handleLocalMediaStreamError)
    },
    getUserPic() {
      const ctx = this.canvas.getContext('2d')

      ctx.drawImage(this.video, 0, 0, 640, 350)
      this.photo = this.canvas.toDataURL()
    },
    getEmotion() {
      // base64 -> blobに変換
      const dataurl = this.photo
      const bin = atob(dataurl.split(',')[1])
      const buffer = new Uint8Array(bin.length)
      for (let i = 0; i < bin.length; i++) {
        buffer[i] = bin.charCodeAt(i)
      }
      const blob = new Blob([buffer.buffer], { type: 'image/png' })

      const formdata = new FormData()
      formdata.append('photo', blob, 'image.png')
      formdata.append('hogehoge', 'fugafuga')

      this.res = this.$axios
        .$post('/face/post', formdata, {
          headers: { 'content-type': 'multipart/form-data' },
        })
        .then((res) => {
          console.log('resnpose data', res)
        })
        .catch((error) => {
          console.log('response error', error)
        })
    },
  },
}

必要なものはgetEmotion()の部分 canvas要素に書き出した画像を画像ファイルとして実体化し、axiosでLaravel側にPOSTする

canvasの画像をblobに変換する方法としてHTMLCanvasElement.toBlob()という関数が用意されているようだが、現時点でSafariがサポートされていないのでこのような実装になっている

const dataurl = this.photo
const bin = atob(dataurl.split(',')[1])
const buffer = new Uint8Array(bin.length)
for (let i = 0; i < bin.length; i++) {
  buffer[i] = bin.charCodeAt(i)
}
const blob = new Blob([buffer.buffer], { type: 'image/png' })

POST部分で気をつけることは、画像などファイルを含む場合はheaders: { ‘content-type’: ‘multipart/form-data’ }でheaderをつけてあげる

今は戻り値をconsole.log()で出力するだけ(必要に応じて処理を追加する)

Vue.js&Nuxt.js超入門

Vue.js&Nuxt.js超入門

PHPフレームワーク Laravel入門 第2版

PHPフレームワーク Laravel入門 第2版