皆様こんにちは。
キャスレーコンサルティング・ID(インテグレーション&デザイン)部の田中(雅)です。
今回は、AWSの「Amazon Rekognition」を利用して
S3上の画像からマッチした画像を探すプログラムを、作成しましたので紹介いたします。
Amazon Rekognitionとは
顔分析、顔認証を行ってくれるサービスです。
通常、機械学習を利用する場合、サンプルとなるデータを取り込む必要がありますが、
こちらのサービスを使えばその手間がなくなるので、
機械学習を利用したアプリケーション開発の敷居を、グッと下げてくれます!
全体の構成
構成としては、以下のイメージで作成しました。
開発環境
クライアント:javascript
使用ライブラリ:
JQuery
サーバーサイド:node.js(8.11.4)
使用ライブラリ:
aws-sdk(2.293.0)
express(4.16.3)
multer(1.3.1)
AWS Lambda:node.js(6.10)
使用ライブラリ:
moment(2.22.2)
となります。
事前準備
- 検索対象となる、画像を置く用のS3バケット(バケットA)
- フォームからの登録画像を置く用のS3バケット(バケットB)
- ダイナモDBのテーブル(テーブルの構成は path(PK):string)
をご用意ください。
それでは、プログラムの作成に入ります!
AWS Lambda
1つ目は、S3に置かれた画像をDBに蓄積するプログラムになります。
- S3に置かれたタイミングで、Lambdaが呼ばれる
- 1.がDynamoDBへ画像パスを登録
といった動きのものを、作成します。
以下は、Lambdaの設定になります。
実際のコードは、以下になります。
const moment = require('node_module/moment'), AWS = require('aws-sdk'), s3 = new AWS.S3({ apiVersion: '2006-03-01' }), dynamoDB = new AWS.DynamoDB.DocumentClient({ region: "[リージョン]" // DynamoDBのリージョン }); exports.handler = (event, context, callback) => { var putBucket = event.Records[0].s3.bucket.name; var putKey = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' ')); const putParams = { TableName: "[ダイナモDBのテーブル名]", Item: { "path": putKey, "regist_date": moment().format('YYYY-MM-DD HH:mm:ss:SSS') } }; console.log(putParams); dynamoDB.put(putParams).promise().then((data) => { console.log( "Put success" ); callback(null); }).catch((err) => { console.log( err ); callback(err) }); };
クライアント
画像選択→結果画像の表示を行います。
- サーバーへの画像のアップロード
- サーバーサイドの返却値から、画像を表示
といった動きのものを、作成します。
以下は、画面イメージになります。
各パーツについての用途としては
①の部分は、類似画像を検索するための元画像をアップロードするパーツ
②は①にて選択した画像から類似画像を検索し、表示するボタン
③検索結果が表示される箇所
になります。
実際のコードは、以下になります。
<html> <head> <title>S3の画像検索</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <link rel="stylesheet" type="text/css" media="screen,tv" href="css/upload.css"/> <script src="//ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script> <script type="text/javascript"> $(function() { const S3Host = '[S3のホスティングURL]'; $('#searchImage').on('click', function(evt) { const form = $('#myForm').get()[0]; // FormData オブジェクトを作成 const formData = new FormData( form ); // Ajaxで送信 $.ajax({ url: '/', method: 'post', dataType: 'json', // dataに FormDataを指定 data: formData, // Ajaxがdataを整形しない指定 processData: false, // contentTypeもfalseに指定 contentType: false }).done(function( res ) { // 照合結果取得 showMatchedImages(res); console.log( 'SUCCESS', res ); }).fail(function( jqXHR, textStatus, errorThrown ) { // 送信失敗 console.log( 'ERROR', jqXHR, textStatus, errorThrown ); }); return false; }); // マッチした画像を表示 const showMatchedImages = (imageList) => { // 結果をリセット $('#results').hide(); $("#search-result-list").empty(); // マッチした画像を結果に追加 for (var i = 0; i < imageList.length; i++) { const imageSrc = S3Host + '/' + imageList[i].path; $('#search-result-list').append('<div class="result-image"><img src="' + imageSrc + '" width="180"></div>'); } // マッチ画像がないとき if ( imageList.length == 0 ) { $('#search-result-list').append('<div>マッチする画像はありませんでした。</div>'); } $('#results').show(); } }); </script> </head> <body> <form id="myForm" class="form"> <input type="file" name="file" id="file-chooser" /> <button id="searchImage" type="button">画像を検索する</button> </form> <div id="results"> <span class='search-result'>検索結果</span> <div id="search-result-list"></div> </div> </body> </html>
サーバーサイド
ここでは、Rekognitionを利用してのバケットAの画像とのマッチングを行います。
- DynamoDBからのレコード取得
- クライアントから渡された画像を、S3へのアップロード
(S3の画像同士をマッチングする、APIを利用するため) - 2の画像と1で取得したレコードの画像を、Rekognitionへ渡す
- マッチング結果をクライアントへ返却
といった動きのものを、作成します。
以下は、作成したソースコードになります。
// node のコアモジュールのhttpを使う const fs = require('fs'), express = require('express'), app = express(), multer = require('multer'), config = require('./config/config.js'), AWS = require('aws-sdk'); app.use(express.static(__dirname + '/assets')); // AWS関連の設定値 const POOL_ID = config.POOL_ID, REGION = '[S3リージョン]', SEARCHBUCKET = '[バケット1]', PUTBUCKET = '[バケット2]'; var localFilePath; // AWS認証(POOLID) AWS.config.region = REGION; AWS.config.credentials = new AWS.CognitoIdentityCredentials({ IdentityPoolId: POOL_ID, }); // S3オブジェクト const s3 = new AWS.S3({apiVersion: '2006-03-01'}); // DynamoDBオブジェクト const dynamo = new AWS.DynamoDB.DocumentClient(); const SEARCHTABLE = '[ダイナモDBのテーブル名]'; const scanParams = { TableName: SEARCHTABLE } // Rekognitionオブジェクト作成 const rekognition = new AWS.Rekognition(); // ファイル保存方法 const storage = multer.diskStorage({ destination: (req, file, cb) => { // 保存パス cb(null, './tmp') }, filename: (req, file, cb) => { // 元のファイル名で保存 cb(null, file.originalname) localFilePath = './tmp' + '/' + file.originalname; } }); const upload = multer({ storage: storage}); // localhostでの初期表示用 app.get('/', (req, res) => { res.sendFile(__dirname + "/" + "template/upload.html"); }); app.post('/', upload.single('file'), async(req, res) => { const postFileInfo = JSON.stringify(req.file); // 照合結果を返却 res.send(await receiveFiles(req)); }); /** * 1-A.DBからファイルリストを取得 * 1-B.S3へのファイルプット * 2.AのリストとBのファイルをrekognitionへ(ループ) * 3.結果返却 * * @param {object} リクエストされた画像情報 * @return {object} マッチした画像 * path: 画像パス */ const receiveFiles = async(req) => { // DBからの画像リスト取得 const dbAllImages = await scanDB(); // リクエストの画像をS3へPUT await s3Put(req); // 画像照合 const searchResults = await imageRekognition(req.file.originalname, dbAllImages); return searchResults; } /** * DBから全データ取得① * ※ ここで取得したデータを後続で利用するためpromise * @return {object} DB から取得できたObject */ const scanDB = () => { return new Promise((resolve, reject) => { dynamo.scan(scanParams, (err, res) => { resolve(res.Items); }); }); } /** * 検索したい画像をS3へPUT② * ※ ここでPUTしたファイルを後続で利用するためpromise * @param {object} リクエストされた画像情報 * @return {object} DB から取得できたObject */ const s3Put = (req) => { return new Promise((resolve, reject) => { // S3へのパラメータ const s3PutParams = { Body: fs.readFileSync(localFilePath), Bucket: PUTBUCKET, Key: req.file.originalname, ContentType: req.file.mimetype }; s3.putObject(s3PutParams, (err, data) => { if (err) { console.log(err, err.stack); // an error occurred } else { console.log('success s3 uploded' + req.file.originalname); // successful response // ローカルのファイル削除 fs.unlink(localFilePath, (err) => { if (err) { console.log(err, err.stack); // an error occurred } else { console.log('delete localfile ' + localFilePath); // successful response } }); } }); resolve(); }); } /** * 画像パスを渡しRekognitionSDKを実行④ * @param {string} 探したい人の画像パス * @param {object} 検索対象の画像パス * path: 画像パス * regist_date: 日付 * @return {object} rekognitionの実行結果 * path: 検索をかけた画像パス * Similarity: 探したい人の画像との類似性 */ const imageRekognition = async(uploadImage, dbAllImages) => { let results = []; for (var i = 0; i < dbAllImages.length; i++) { // Rekognition実行(promise) const result = await execRekognition(uploadImage, dbAllImages[i].path); // マッチしない場合はネスト if (!Object.keys(result).length) { continue; } results.push(result); }; return results; }; /** * Rekognitionへのパラメーターをセット * @param {string} 探したい人の画像パス * @param {string} 検索対象の画像パス * @return {object} rekognitionのへのパラメータ */ const setRekognitionParam = (uploadImage, searchImage) => { // Rekognition用パラメータ return RekognitionParams = { SimilarityThreshold: 90, SourceImage: { S3Object: { Bucket: PUTBUCKET, Name: uploadImage } }, TargetImage: { S3Object: { Bucket: SEARCHBUCKET, Name: searchImage } } }; } /** * RekognitionSDKを実行③ * @param {string} 探したい人の画像パス * @param {string} 検索対象の画像パス * @return {object} rekognitionのへのパラメータ */ const execRekognition = (uploadImage, targetPath) => { let compareResult = {}; return new Promise((resolve, reject) => { const sendRekognitionParam = setRekognitionParam(uploadImage, targetPath); // 画像比較開始 rekognition.compareFaces(sendRekognitionParam, (err, data) => { if (err) { console.log('Face recognition was not done ' + targetPath); // console.log(err); } else { // 画像として認識されたがunmatch かどうかの判別用 if (typeof(data.FaceMatches[0]) != 'undefined') { // successful response console.log('match:' + targetPath); compareResult = { path: targetPath, Similarity: data.FaceMatches[0].Similarity }; } else { console.log('unmatch:' + targetPath); } } resolve(compareResult); }); }); } // サーバを待ち受け状態にする const server = app.listen(3000, function() { const host = server.address().address; const port = server.address().port; console.log("listening at http://localhost%s:%s", host, port); });
注意点として画像内に顔を検出できない場合、error として返ってきます。
この場合のハンドリングが必要になります。
201行目が、その条件分岐に当たります。
Rekognition の返却値として、「Similarity」というのを取得していますが、
こちらは類似性となり、両画像で検出できた画像の類似性になります。
こちらを利用して閾値を設けることも可能です。
私が行ったときには、本人の画像がなければ「FaceMatches」は返ってきませんでした。
終わりに
AIについては、設定が複雑であったり多量のイメージがありますが、
すでにサービスとして提供されているものを利用することで、簡単に使えることができます。
今回は、非ストレージAPIのcompareFacesを利用しておりましたが、
ストレージAPIを利用してバージョンアップも検討していきます。
最後までお読みいただき、ありがとうございました。