Amazon Rekognitionを使うと画像や動画をAIで解析し、そこにどういった物体が存在しているか「ラベル」を付けてくれます。例えばジャイアントパンダが写っていれば「Giant Panda」というテキストと、画像内のどこに存在しているか座標情報が送られて来ます。
今回は画像をNode.jsからRekognitionを利用するコードを書いてみます。AWSの流儀やNodeについての基礎知識があれば非常に手軽に実行できます。
最終的なコード
最終的なコードをGitHub上にアップしました。サンプル画像付きです。 github.com
Rekognitionを試してみる
解析結果
とりあえず何ができるのか見てみましょう。画像を送信するだけで、冒頭でお話したようにどこにどういった物体が写っているかテキストと座標情報をJSONで送ってきてくれます。それをNodeで可視化したのが右側の画像です。 ※画像をクリックすると大きいサイズを表示します
元画像 | 解析結果 |
---|---|
サンプルからも分かる通り現段階では得手不得手があるようです。一番下の例のように人間の識別はびっくりするほど細かくやってくれますが、最初のハンバーグは色合いや形状がパンに近いため誤判定されていますね、ゴハンなだけに(ドヤ顔)。またパンダの場合はパンダ(Giant Panda)とクマ(Bear)の2つの候補が送られてきました。
このように現時点では100%正確にはいきませんが、対象が人間であったり大まかな内容を調べるくらいであればそれなりに使えそうですね。また日々アップデートされているようなので徐々に精度は上がっていくことも期待できそうです。
料金
「画像1枚を処理するとn円」といった従量制になっています。執筆時点では次の表の通り。
処理数 | 料金 |
---|---|
100万枚まで | 0.0013 USD |
101〜1,000万枚 | 0.001 USD |
1,001万枚〜1憶枚 | 0.0008 USD |
1億枚を超える | 0.0005 USD |
- 処理数は1か月あたり
- 料金は画像1枚あたり
1ドル105円とすると1枚約0.13円といったところでしょうか。1000枚で130円、1万枚で1300円と大量に分析する場合は微妙にかかりますが、自分でGPUをぶん回して機械学習のモデルを作る手間やコストを考えたら格安だと思います。
最新の料金は以下を参照ください。 aws.amazon.com
準備
IAM
AWSのマネジメントコンソールなどでRekognitionが利用できるポリシーをIAMに付与します。S3上の画像を利用する場合はS3の権限もお忘れなく。
IAMのアクセスキーIDとシークレット、利用するリージョンの指定を今回は.env
というファイル名で保存しておきます。すでに環境変数から参照できる状態であれば以下の設定は不要です。なお.env
はGitなどのリポジトリには絶対に登録しないでください。
AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXXX AWS_SECRET_ACCESS_KEY=YYYYYYYYYYYYYYYYYYYYYYYYYYYY AWS_REGION=ap-northeast-1
Nodeのプロジェクトを作成
Node.jsをインストール後、適当なディレクトリを作成し、package.jsonを生成するためにnpm init
を実行します。
$ mkdir rekog1; cd rekog1 $ npm init
必要なモジュールを入れます。
$ npm install aws-sdk dotenv canvas
- aws-sdk
- Node.jsからAWSを利用するためのライブラリ集
- dotenv
- 先ほど作成した .env を環境変数化してくれるモジュール
- canvas
- Node.jsでWebブラウザと同様にcanvasが利用できるnode-canvasモジュール。Rekognitionから返された座標を元に画像上に線を引くのに使います。線を引かない場合は不要です。
node-canvasは外部のライブラリに依存していますので環境に合わせて追加でインストールします。以下はmacOSの例です。Windowsやその他のOSはドキュメントを参照してください。
$ brew install pkg-config cairo pango libpng jpeg giflib librsvg
基本的な原理
ローカルのファイルを解析する
クライアント
最小限のコードは以下の通りです。AWSにSDK経由で画像データを投げつけるとJSONが返ってくるという非常にシンプルな作りです。
const AWS = require('aws-sdk') const fs = require('fs') // .envの内容を環境変数化 require('dotenv').config() // IAM設定 AWS.config.update({ accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, region: process.env.AWS_REGION }) // Rekognitionに渡す値を準備 const client = new AWS.Rekognition(); const params = { Image: { Bytes: fs.readFileSync('sample.jpg') // 解析対象の画像データ }, MaxLabels: 10 } // Rekognitionで解析 client.detectLabels(params, (err, response) =>{ // エラー時 if (err) { console.log(err, err.stack) } //解析結果を表示 else { const str = JSON.stringify(response, null, 2) cosole.log(str) } })
レスポンス
すると以下のようなJSONが返されます。
Name
が発見された物体の名称Confidence
は信頼性。100に近いほど的中率が上がります。Instances
の中に実際に発見された座標情報が含まれます。Parents
は親にあたるラベルを指し示しています。ラベルは階層構造になってるんですね。以下のJSONではFoodの下にSteakやBreadがいることがわかります。
{ "Labels": [ { "Name": "Food", "Confidence": 94.963134765625, "Instances": [], "Parents": [] }, { "Name": "Steak", "Confidence": 94.30536651611328, "Instances": [], "Parents": [ { "Name": "Food" } ] }, { "Name": "Bread", "Confidence": 83.53762817382812, "Instances": [ { "BoundingBox": { "Width": 0.27659285068511963, "Height": 0.580934464931488, "Left": 0.45731204748153687, "Top": 0.3908308446407318 }, "Confidence": 83.53762817382812 } ], "Parents": [ { "Name": "Food" } ] }, ], "LabelModelVersion": "2.0" }
座標情報を変換する
BoundingBox内にラベルの座標情報が含まれるのですがこれがちょっと特殊で、画像全体の大きさに対する比率になっています。
例えばBoundingBox.Widthはラベルの横幅を指しますが、以下では約0.27という値になっています。要するに画像全体の横幅の27%のサイズという意味になります。
"Instances": [ { "BoundingBox": { "Width": 0.27659285068511963, "Height": 0.580934464931488, "Left": 0.45731204748153687, "Top": 0.3908308446407318 }, "Confidence": 83.53762817382812 } ],
そこで実際に画像上にプロットする際には以下のように、単純に画像サイズを掛け算する必要があります。
const WIDTH = 1024 const HEIGHT = 768 const box = instance.BoundingBox const boxLeft = Math.floor(box.Left * WIDTH) const boxTop = Math.floor(box.Top * HEIGHT) const boxWidth = Math.floor(box.Width * WIDTH) const boxHeight = Math.floor(box.Height * HEIGHT)
S3上のファイルを解析する
すでにS3上に存在するファイルを解析する場合は、Rekognitionにバケット名と対象のファイルを渡すだけです。
// Rekognitionに渡す値を準備 const client = new AWS.Rekognition(); const params = { Image: { // 解析対象の画像データ S3Object: { Bucket: 'image.example.com', Name: 'sample1.jpg' }, }, MaxLabels: 10 }
画像を投げラベルを描画するソースコード
ここまでの内容を踏まえ、冒頭のサンプル画像を生成するソースコードは以下の通りです。
/** * Amazon Rekognition Sample * 画像の内容を解析しラベルを付ける */ //----------------------------------------------- // 定数 //----------------------------------------------- // ファイルリスト const FILES = [ {origin:'image/sample1.jpg', out:'image/sample1o.jpg', json:'image/sample1.json', width:1024, height:576}, // 0: いきなりステーキ {origin:'image/sample2.jpg', out:'image/sample2o.jpg', json:'image/sample2.json', width:1024, height:768}, // 1: パンダ {origin:'image/sample3.jpg', out:'image/sample3o.jpg', json:'image/sample3.json', width:1024, height:768} // 2: 金閣寺 ] // 解析するファイル const FILE = FILES[2] //----------------------------------------------- // モジュール //----------------------------------------------- const AWS = require('aws-sdk') const fs = require('fs') const { createCanvas, loadImage } = require('canvas') // .envを環境変数に require('dotenv').config() //----------------------------------------------- // 画像解析 //----------------------------------------------- // IAM設定 AWS.config.update({ accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, region: process.env.AWS_REGION }) // Rekognitionに渡す値を準備 const client = new AWS.Rekognition(); const params = { Image: { Bytes: getFile(FILE.origin) }, MaxLabels: 10 } // Rekognitionで解析 client.detectLabels(params, (err, response) =>{ if (err) { console.log(err, err.stack) } else { // 画像に線を引いて保存 drawLine(response.Labels) // レスポンスを保存 saveFile(FILE.json, JSON.stringify(response, null, 2)) } }) /** * 発見されたラベルの箇所に線を引く * * @param {object} label Rekognitionからのレスポンス */ async function drawLine(labels){ const canvas = createCanvas(FILE.width, FILE.height) const ctx = canvas.getContext('2d') // 画像を貼り付け const image = await loadImage(FILE.origin) ctx.drawImage(image, 0, 0, canvas.width, canvas.height); // 線のスタイル設定 ctx.strokeStyle = 'blue' ctx.lineWidth = 5 // 線を描く for( let label of labels ){ const name = label.Name for( let instance of label.Instances ){ // 画像上の座標に変換 const box = instance.BoundingBox const boxLeft = Math.floor(box.Left * FILE.width) const boxTop = Math.floor(box.Top * FILE.height) const boxWidth = Math.floor(box.Width * FILE.width) const boxHeight = Math.floor(box.Height * FILE.height) // ラベル ctx.font = '20px serif'; ctx.fillText(name, boxLeft+10, boxTop+20); // 線 ctx.beginPath(); ctx.moveTo(boxLeft, boxTop); // 左上からスタート ctx.lineTo(boxLeft+boxWidth, boxTop); // 右上 ctx.lineTo(boxLeft+boxWidth, boxTop+boxHeight); // 右下 ctx.lineTo(boxLeft,boxTop+boxHeight); // 左下 ctx.lineTo(boxLeft, boxTop); // 左上に戻る ctx.stroke(); } } // JPEGに変換して保存 canvas.toBuffer((err, buff) => { if (err) throw err saveFile(FILE.out, buff) }, 'image/jpeg', { quality: 0.95 }) } /** * 指定ファイルのデータを返却する * * @param {string} path * @return {string} */ function getFile(path){ try{ const image = fs.readFileSync(path) return(image) } catch(e){ console.error(e.message) process.exit() } } /** * 指定ファイルにデータを保存する * * @param {string} path * @param {any} data */ function saveFile(path, data){ try{ fs.writeFileSync(path, data) } catch(e){ console.error(e.message) process.exit() } }
続き
これがやりたかったんですよねw blog.katsubemakito.net