はじめに
こんにちは、バックエンドチームの河井です。 スタメンでは TUNAG という社内 SNS を開発・運用しています。SNS としての基本的な機能はそろっていますので、各ユーザーはプロフィール画像を登録できるし、投稿には画像を添付することができます。 ですので、例えばプロフィール画像を元に、そのユーザーの写っている画像を振り返れたら楽しそうだなーと思っていました。(※今のところ TUNAG 本体への実装予定はありません)
そこで今回は、 Amazon Rekognition を使って 「入力された画像内に登録済みのユーザーが写っていれば、そのユーザーの顔を囲う枠とユーザー名を描画した画像を返す。」 というものを作ってみます。
Amazon Rekognition の概要
Amazon Rekognitionとは、深層学習をベースとした汎用的な画像認識サービスです。 画像のクラス分類から、物体検出、感情分析、テキスト抽出など、よくある画像処理タスク全般に対応しています。
その中から、顔の検索機能を使っていきます。 顔の検索機能とは、画像から検出した顔に関する情報をコレクションと呼ばれるコンテナに保存しておき、そのコレクション内の顔情報に対して検索できるものです。
使用する機能
今回の目的のために必要になる機能は、
- コレクションを作る
- 顔情報を登録する
- 顔情報を検索する
の3つになります。それぞれ解説していきます。なお、これから紹介していくのは Ruby SDK のコードになります。 ドキュメントはこちらを参照ください。
コレクションを作る
コレクションの作成には CreateCollection を使います。
リクエスト
resp = client.create_collection({ collection_id: "myphotos", })
レスポンス
# resp.to_h { collection_arn: "aws:rekognition:us-west-2:123456789012:collection/myphotos", status_code: 200, }
顔情報を登録する
続いて、顔情報の登録には IndexFaces を使います。
リクエスト
resp = client.index_faces({ collection_id: "myphotos", image: { bytes: image_bytes, s3_object: { bucket: "mybucket", name: "myphoto", }, }, })
作成済みコレクションの collection_id に対して顔情報を登録します。 image キーに対しては、画像ファイルを直接指定することもできるし、S3のバケットとファイル名を指定することもできます。 現状だと入力画像に写っている大きい顔から100人分まで処理してくれるようです。 当然ですが、写真がボケていたり暗かったりすると精度が出ないので、それぞれの顔がしっかり写っている必要があります。
レスポンス
# resp.to_h { face_records: [ { face: { bounding_box: { height: 0.33481481671333313, left: 0.31888890266418457, top: 0.4933333396911621, width: 0.25, }, confidence: 99.9991226196289, face_id: "ff43d742-0c13-5d16-a3e8-03d3f58e980b", image_id: "465f4e93-763e-51d0-b030-b9667a2d94b1", }, face_detail: { # 省略 }, } ], orientation_correction: "ROTATE_0", }
顔の向きやパーツの位置など色々な情報が返ってきますが、今回は face_id と bounding_box を使用します。 face_id は画像から検出された顔の特徴ベクトルに対応するもので、同じ人物と判断されるからといって同じ face_id をもつわけではないことに注意してください。
顔情報を検索する
登録済みの顔情報の検索には SearchFaces を使います。
リクエスト
resp = client.search_faces({ collection_id: "myphotos", face_id: "70008e50-75e4-55d0-8e80-363fb73b3a14", face_match_threshold: 90, max_faces: 10, })
IndexFaces で返ってきた face_id で登録済みの顔情報を検索します。 face_match_threshold を指定しておくことで、confidence の閾値を設けることができます。
レスポンス
resp.to_h outputs the following: { face_matches: [ { face: { bounding_box: { height: 0.3259260058403015, left: 0.5144439935684204, top: 0.15111100673675537, width: 0.24444399774074554, }, confidence: 99.99949645996094, face_id: "8be04dba-4e58-520d-850e-9eae4af70eb2", image_id: "465f4e93-763e-51d0-b030-b9667a2d94b1", }, similarity: 99.97222137451172, } ], searched_face_id: "70008e50-75e4-55d0-8e80-363fb73b3a14", }
リクエストした face_id をもつ顔と同じものだと判定された顔情報が、一致度が高い順に並んだ配列として返ってきます。
補足
顔の検索には他にも SearchFacesByImage というオペレーションがあり、SearchFaces は face_id での検索だったのに対し、こちらは画像による検索ができます。 しかしながら、このオペレーションでは画像に一番大きく写っている顔で検索されるため、画像に写っている全員で検索することはできないので今回は使用しません。
実装
まず、User と RekognitionFace という2つのテーブルを定義します。 ※将来的に Web 上で動かしたいと思っているので、(今回の実装上あまり意味がありませんが)Rails 上でテーブルやそれを操作するクラスの定義をしています。
User は自身の名前 name を持ちます。 RekognitionFace には、user_id と face_id を結びつける役割を持たせます。 前述の通り face_id は User 固有ではないため、User と RekognitionFace を一対多の関係として保持します。 これによって、
- face_id を使うことで RekognitionFace を絞り込み、入力画像に写っているユーザー名を取得できる
- image_id を使うことでそのユーザーが写っている画像の一覧を取得できる
という機能が実現できます。前者の機能を実装してみます。
AmazonRekognition というクラスを定義しておき、入力と出力を必要なものだけにしておきます。
class AmazonRekognition class << self def index_faces(image_path) res = client.index_faces({ collection_id: collection_id, image: { bytes: File.open(image_path, 'r+b') } }) res.to_h[:face_records].map { |face| [face[:face][:face_id], face[:face][:bounding_box]] }.to_h end def search_faces(face_id) res = client.search_faces({ collection_id: collection_id, face_id: face_id, face_match_threshold: 95 }) searched_face_id = res.to_h[:searched_face_id] matched_faces = res.to_h[:face_matches] return if matched_faces.blank? matched_face_id = matched_faces[0][:face][:face_id] { searched_face_id: searched_face_id, matched_face_id: matched_face_id } end def search_all_faces_by_image(image_path) faces = index_faces(image_path) matched_faces = faces.map do |face_id, bounding_box| result = search_faces(face_id) next if result.blank? result[:searched_face_bounding_box] = bounding_box result end matched_faces.compact end private def client @client = Aws::Rekognition::Client.new( region: 'ap-northeast-1', access_key_id: '*****', secret_access_key: '*****' ) end def collection_id 'myphotos' end end end
RekognitionFace では、face_id と user の橋渡しをします。
class RekognitionFace < ApplicationRecord belongs_to :user class < 1 user.rekognition_faces.create(face_id: res.keys[0]) end def search(image_path) faces = AmazonRekognition.search_all_faces_by_image(image_path) matched_face_ids = faces.map { |face| face[:matched_face_id] } where(face_id: matched_face_ids) end end end
User クラスでは、クラスメソッドとしてその画像に写っているユーザーを取得するもの、インスタンスメソッドとしてそのユーザー顔画像を登録するためのものを定義しておきます。
class User < ApplicationRecord has_many :rekognition_faces class << self def in_the_image(image_path) faces = RekognitionFace.search(image_path) where(id: faces.pluck(:user_id)) end end def register_face(image_path) rekognition_faces.factory!(self, image_path) end end
これらを使って進めていきます。
動かしてみる
下準備として、 画像の用意、ユーザーの作成、画像の登録をします。
画像はぱくたそさんからもらってきた以下の3枚で、最初の2枚を登録に使い、最後の1枚を検索に使います。それぞれ画像の下に書かれているファイル名で保存しておきます。
Okawa-san.jpg
Yusei-san.jpg
test.jpg
次にユーザーを作成します。
user1 = User.create(name: 'Okawa-san') user2 = User.create(name: 'Yusei-san')
最後に、各ユーザーの画像を登録します。
user1.register_image('Okawa-san.jpg') user2.register_image('Yusei-san.jpg')
結果を表示するのに、RMagick を使って直接画像に書き込みます。 画像に写っている人物のうち、登録されているユーザーについて、顔の位置とそのユーザー名を表示するものです。
image_path = 'test.jpg' matched_faces = AmazonRekognition.search_all_faces_by_image(image_path) return if matched_faces.size == 0 image = Magick::ImageList.new(image_path) gc = Magick::Draw.new matched_faces.each do |face| user = RekognitionFace.find_by(face_id: face[:matched_face_id])&.user next if user.blank? bbox = face[:searched_face_bounding_box] x1 = rimg.columns * bbox[:left] y1 = rimg.rows * bbox[:top] x2 = rimg.columns * (bbox[:left] + bbox[:width]) y2 = rimg.rows * (bbox[:top] + bbox[:height]) gc.fill_opacity(0) gc.stroke('red') gc.stroke_width(3) gc.rectangle x1, y1, x2, y2 gc.pointsize(50) gc.text(x1, y1 - 25, user.name) end gc.draw(image) image.write "tmp/result.jpg"
一つ注意点として、bounding_box の値は入力画像の幅と高さの割合を示しているので、実際の座標を得るためには計算する必要があります。
実行結果↓
うまく認識してくれましたね。
まとめ
Amazon Rekognition の顔の検索機能を使って、写真から個人を検出する機能を実装してみました。 非常に高精度で、基本的にユーザーあたり1枚登録してあれば検出できます。 機械学習、特に深層学習は精度を出すのにコツがいるところもあるので、まずはこういったクラウドサービスで挙動を見てみるのは、イメージを掴むのに良いのではないかと思いました。
最後に、スタメンではこれからの TUNAG の機能を一緒に作っていくエンジニアを募集しています。ご興味のある方はお話だけでもできると嬉しいです。よければこちら(や DM でも)からお願いします!