[AWS] Rekognitionで画像内に存在する物体を検出する

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

基本的な原理

ローカルのファイルを解析する

クライアント

最小限のコードは以下の通りです。AWSSDK経由で画像データを投げつけると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

参考ページ