皆様こんにちは。
キャスレーコンサルティング・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に蓄積するプログラムになります。

  1. S3に置かれたタイミングで、Lambdaが呼ばれる
  2. 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)
    });
};

クライアント

画像選択→結果画像の表示を行います。

  1. サーバーへの画像のアップロード
  2. サーバーサイドの返却値から、画像を表示

といった動きのものを、作成します。

以下は、画面イメージになります。

各パーツについての用途としては
の部分は、類似画像を検索するための元画像をアップロードするパーツ
にて選択した画像から類似画像を検索し、表示するボタン
検索結果が表示される箇所
になります。

実際のコードは、以下になります。

<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の画像とのマッチングを行います。

  1. DynamoDBからのレコード取得
  2. クライアントから渡された画像を、S3へのアップロード
    (S3の画像同士をマッチングする、APIを利用するため)
  3. 2の画像と1で取得したレコードの画像を、Rekognitionへ渡す
  4. マッチング結果をクライアントへ返却

といった動きのものを、作成します。

以下は、作成したソースコードになります。

// 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を利用してバージョンアップも検討していきます。

最後までお読みいただき、ありがとうございました。

田中(雅)
CSVIT事業部 ID(インテグレーション&デザイン)部 田中(雅)
最近はAWS,node.jsを勉強中です!