AmazonRekognitionを使って画像検索機能を作る

はじめに

はじめて外部APIを使ってみたのでその内容を書きます。

順序

1:画像を送信する。

2:送信された画像をawss3を使って、bucketを指定して保存する。

3:rubyからAmazon RekognitonAPIを叩いて、先程保存した画像を画像認識して、ラベルを取得する。

4:取得したラベルをAmazon Translateを使って日本語翻訳して、そのワードを元にフリーワード検索をしてデータを取得する。

awsのIAMユーザーの作成

まず、awsIAMにアクセスしてユーザーを作成します。

その際にAccess key IDSecret access keyが作成されるので、保管して置いてください。

また、Amazon s3Amazon RekognitonAmazon Translateを使用するので、それらのアクセス許可のポリシーをアタッチしてください。

詳しい方法についてはドキュメントを参照してください。

docs.aws.amazon.com

docs.aws.amazon.com

docs.aws.amazon.com

aws s3のバケットを作成

続いて、s3にアクセスしてバケットを作成します。

やることとしては、任意のバケット名をつけることと、リージョンを設定するだけです。

詳しくはドキュメントを参照してください。

docs.aws.amazon.com

環境変数を設定

先程保管しておいたしておいた、Access key IDSecret access key環境変数に設定します。

export AWS_ACCESS_KEY_ID=<取得したAccess key ID>
export AWS_SECRET_ACCESS_KEY=<取得したSecret access key>

ここまでの設定が終わったら、本題の画像検索機能を実装していきます。

Gemのインストール

まずは下記のgemをインストールします。

gem 'aws-sdk-s3'
gem 'aws-sdk-rekognition'
gem 'aws-sdk-translate'

画像のアップロード画面を作成

続いて画像入力のビューを作ります。

※active storage使ってます

      <%= form_with url: search_images_path, scope: :search, method: :post, local: true do |f| %>
        <div class="form-group mb-3">
          <%= f.label :search_image %>
          <%= f.file_field :image, class: 'form-control' %>
        </div>
        <div class="actions mb-3">
          <%= f.submit '検索する', class: 'btn btn-primary' %>
        </div>
      <% end %>

画像をs3に保存する

続いて、取得した画像をs3に保存します。

  def upload_image(data)
    credentials = Aws::Credentials.new(
      ENV['AWS_ACCESS_KEY_ID'],
      ENV['AWS_SECRET_ACCESS_KEY']
    )
    region = 'us-east-1'

    # アップロードされた画像をs3に保存
    s3_client = Aws::S3::Client.new(region: region, credentials: credentials)

    body = data
    bucket = 'article-image-search'
    key = "article_image"

    s3_client.put_object({
      body: body,
      bucket: bucket,
      key: key
    })

    put_labels(key, s3_client)
  end

upload_imageというメソッドを作って、画像のデータを引数として受け取ります。

    credentials = Aws::Credentials.new(
      ENV['AWS_ACCESS_KEY_ID'],
      ENV['AWS_SECRET_ACCESS_KEY']
    )
    region = 'us-east-1'

credentialsregionを定義します。

regions3バケットを作成したときに設定したものを指定します。

    s3_client = Aws::S3::Client.new(region: region, credentials: credentials)

    body = data
    bucket = 'article-image-search'
    key = "article_image"

    s3_client.put_object({
      body: body,
      bucket: bucket,
      key: key
    })

    put_labels(key, s3_client, credentials, bucket)

bodyに引数で受け取った画像データを代入します。

bucketには先程作成したバケット名を定義します。

keyには任意の値をしていします。

put_objectメソッドを使用して、s3に画像を保存します。

最後にこの後作成するput_labelsというメソッドを呼び出します。

画像のラベルを検出する

  def put_labels(key, s3_client, credentials, bucket)
    rekogniton_client   = Aws::Rekognition::Client.new(credentials: credentials)
    attrs = {
      image: {
        s3_object: {
          bucket: bucket,
          name: key
        },
      },
      max_labels: 10
    }

    response = rekogniton_client.detect_labels attrs
    label_list = []
    response.labels.each do |label|
      label_list << label.name.downcase
    end

    s3_client.delete_objects({
      bucket: bucket,
      delete: {
        objects: [
          {
            key: key
          },
        ],
        quiet: false
      }
    })

    translate_label(label_list)
    rekogniton_client   = Aws::Rekognition::Client.new(credentials: credentials)
    attrs = {
      image: {
        s3_object: {
          bucket: bucket,
          name: key
        },
      },
      max_labels: 10
    }

max_labelsで検出するラベルの上限を指定できます。

    response = rekogniton_client.detect_labels attrs
    label_list = []
    response.labels.each do |label|
      label_list << label.name.downcase

detect_labelsを使って画像を解析したデータを取得して、

ラベル名だけを配列に入れます。

このとき、ラベル名の頭文字が大文字になっていることがあるので、

downcaseメソッドを使ってすべて小文字に変換します。

これをしないと、次で説明する日本語翻訳がうまくいきません。

    s3_client.delete_objects({
      bucket: bucket,
      delete: {
        objects: [
          {
            key: key
          },
        ],
        quiet: false
      }
    })

    translate_label(label_list)

delete_objectsメソッドを使って、画像認識に使用した画像を削除します。

最後に次で作成するtranslate_labelという日本語翻訳するためのメソッドを呼び出します。

ラベル名を翻訳する

  def translate_label(label_list)
    region = 'us-east-1'

    # ラベルを翻訳
    translate_client = Aws::Translate::Client.new(
      region: region,
      credentials: credentials,
    )

    label_list_ja = []

    label_list.each do |label|
      res = translate_client.translate_text({
              text: label,
              source_language_code: 'auto',
              target_language_code: 'ja'
            })
      label_list_ja << res.translated_text
    end

    # ラベルのリストを返す
    label_list_ja
  end
    label_list.each do |label|
      res = translate_client.translate_text({
              text: label,
              source_language_code: 'auto',
              target_language_code: 'ja'
            })
      label_list_ja << res.translated_text
    end

translate_textメソッドを使用して先程のラベル名を翻訳します。

textには翻訳したいテキストを指定します。

source_language_codeには翻訳前の言語を指定します。

autoを指定するとapi側がよしなに指定してくれます。

target_language_codeには、翻訳後の言語を指定します。

翻訳したラベルのリストをlabel_list_jaに代入して値を返します。

あいまい検索のメソッドを作成

続いて、先程翻訳したラベルの配列を使って、あいまい検索をします。

  def label_search(label_list)
    relation = Bug.distinct

    result = []

    label_list.each do |label|
      relation.where('name LIKE ?', "%#{label}%")
              .or(relation.where('feature LIKE ?', "%#{label}%"))
              .or(relation.where('approach LIKE ?', "%#{label}%"))
              .or(relation.where('prevention LIKE ?', "%#{label}%"))
              .or(relation.where('harm LIKE ?', "%#{label}%"))
              .each { |bug| result << bug }
    end
    result.uniq!
  end

orメソッドを使ってor検索をしています。

取得したレコードを配列にいれ、uniq!メソッドを使って重複したレコードを削除します。

コントローラを作成

続いて、コントローラに下記のコードを追加します。

class SearchImagesController < ApplicationController
  include SearchImagesHelper

  def new; end

  def create
    data = params[:search][:image]
    label_list = upload_image(data)
    bug_array = label_search(label_list)
    if bug_array.present?
      @bugs = Bug.where(name: bug_array.map(&:name)).page(params[:page])
    else
      @bugs = Bug.none.page(params[:page])
    end
    render 'bugs/index'
  end
end

先程のヘルパーをインクルードします。

    data = params[:search][:image]
    label_list = upload_image(data)

paramsで画像データを取得してdata変数に代入し、それを引数にして先程作成したupload_imageメソッドを呼び出します。

帰ってきたラベルの配列データをlabel_listに代入します。

    bug_array = label_search(label_list)

先程作成したlabel_searchを使って、レコードの配列を取得します。

    if bug_array.present?
      @bugs = Bug.where(name: bug_array.map(&:name)).page(params[:page])
    else
      @bugs = Bug.none.page(params[:page])
    end
    render 'bugs/index'

kaminariというページネーションのgemで使用できるようになるpageメソッドを使う際に、

Arrayクラスでは使用できないため、whereメソッドとnoneメソッドを使って無理やりActiveRecord::Relationクラスに変更させます。

最後に

もっといい方法があればコメント等ください!

参考

qiita.com

docs.aws.amazon.com

docs.aws.amazon.com

docs.aws.amazon.com