先日目をでかくして誰でもアイドルみたいになれるアプリを作ったという記事を書きました。
この記事では、具体的にどのようにしてこの機能を実現したのかを書きます。
実装内容
実装内容は以下です。
- Amazon Rekognitionで目を認識
- OpenCVで切り抜いて大きくして合成する
- 合成後自然になるように境界線をぼかす
Amazon Rekognitionで目を認識
目の場所を検知するためのコードは以下。
import cv2
import boto3
from botocore.config import Config
config = Config(
retries = {
'max_attempts': 10,
'mode': 'standard'
}
)
def rekog_eye(im):
#amazon_rekognition に投げる
client = boto3.client('rekognition','ap-northeast-1',config=config)
result, buf = cv2.imencode('.jpg', im)
#rekognition_imageによる顔情報
faces = client.detect_faces(Image={'Bytes':buf.tobytes()}, Attributes=['ALL'])
leftEyeLeft = faces['FaceDetails'][0]['Landmarks'][11]
leftEyeRight = faces['FaceDetails'][0]['Landmarks'][12]
leftEyeUp = faces['FaceDetails'][0]['Landmarks'][13]
leftEyeDown = faces['FaceDetails'][0]['Landmarks'][14]
rightEyeLeft = faces['FaceDetails'][0]['Landmarks'][15]
rightEyeRight = faces['FaceDetails'][0]['Landmarks'][16]
rightEyeUp = faces['FaceDetails'][0]['Landmarks'][17]
rightEyeDown = faces['FaceDetails'][0]['Landmarks'][18]
EyeList = [leftEyeLeft, leftEyeRight, leftEyeUp, leftEyeDown, rightEyeLeft, rightEyeRight, rightEyeUp, rightEyeDown]
EyePoints = {}
h, w, ch = im.shape
for eyepoint in EyeList:
EyePoints[eyepoint['Type']] = {'X': int(eyepoint['X']*w), 'Y': int(eyepoint['Y']*h)}
return EyePoints
AWSの各種サービスを簡単に利用できるライブラリboto3を使って実装していきます。
boto3のclientを作成し、使うサービス(今回はrekognition)とリージョンを指定します。
configで何回tryするかを指定しています。今回だと10なので、仮に1回目でサーバーエラーやネットワークエラーなどでうまくRekognitionが動かなくても2回目再度tryし、そして最大10回は試すというようになっています。
client = boto3.client('rekognition','ap-northeast-1',config=config)
そしてrekognitionのdetect_facesに画像バイト列を渡して検出します。
faces = client.detect_faces(Image={'Bytes':buf.tobytes()}, Attributes=['ALL'])
あとは必要なランドマークをそれぞれ使いやすいように名称をふります。
例えば左目の左端だとleftEyeLeft、左目の右端だとleftEyeRightです。
よりギリギリで切り取って広げてということをしたいので、片目に対して4点を検出しています。
検出できた場所はより使いやすいように配列に入れます。
EyeList = [leftEyeLeft, leftEyeRight, leftEyeUp, leftEyeDown, rightEyeLeft, rightEyeRight, rightEyeUp, rightEyeDown]
更に整形してからreturnします。
EyePoints = {}
h, w, ch = im.shape
for eyepoint in EyeList:
EyePoints[eyepoint['Type']] = {'X': int(eyepoint['X']*w), 'Y': int(eyepoint['Y']*h)}
OpenCVで切り抜いて大きくして合成する
まずは画像をそれぞれbase64からcv2に、また逆にcv2からbase64に変換する関数を用意します。
import numpy as np
import base64
def base64_to_cv2(image_base64):
"""base64 image to cv2"""
image_bytes = base64.b64decode(image_base64)
np_array = np.fromstring(image_bytes, np.uint8)
image_cv2 = cv2.imdecode(np_array, cv2.IMREAD_COLOR)
return image_cv2
def cv2_to_base64(image_cv2):
"""cv2 image to base64"""
image_bytes = cv2.imencode('.jpg', image_cv2)[1].tostring()
image_base64 = base64.b64encode(image_bytes).decode()
return image_base64
切り取って合成するためのコードは以下。
def handler(event, context):
##############リクエストペイロードからリクエストボディを抜き出す############
# event['body']は文字列になっているのでdictに変換
try:
base_64ed_image = event.get('myimg', 'none')
magnification = float(event.get('magni', 1.4)
blur_num = int(event.get('blur', 3)) ##ぼかし機能のため(後述)
im = base64_to_cv2(base_64ed_image)
EyePoints = rekog_eye(im)
bityouseix = 20
bityouseiy = 5
leftTop = min([EyePoints['leftEyeUp']['Y'], EyePoints['leftEyeDown']['Y'], EyePoints['leftEyeRight']['Y'], EyePoints['leftEyeLeft']['Y']])
leftBottom = max([EyePoints['leftEyeUp']['Y'], EyePoints['leftEyeDown']['Y'], EyePoints['leftEyeRight']['Y'], EyePoints['leftEyeLeft']['Y']])
leftRight = max([EyePoints['leftEyeUp']['X'], EyePoints['leftEyeDown']['X'], EyePoints['leftEyeRight']['X'], EyePoints['leftEyeLeft']['X']])
leftLeft = min([EyePoints['leftEyeUp']['X'], EyePoints['leftEyeDown']['X'], EyePoints['leftEyeRight']['X'], EyePoints['leftEyeLeft']['X']])
rightTop = min([EyePoints['rightEyeUp']['Y'], EyePoints['rightEyeDown']['Y'], EyePoints['rightEyeRight']['Y'], EyePoints['rightEyeLeft']['Y']])
rightBottom = max([EyePoints['rightEyeUp']['Y'], EyePoints['rightEyeDown']['Y'], EyePoints['rightEyeRight']['Y'], EyePoints['rightEyeLeft']['Y']])
rightRight = max([EyePoints['rightEyeUp']['X'], EyePoints['rightEyeDown']['X'], EyePoints['rightEyeRight']['X'], EyePoints['rightEyeLeft']['X']])
rightLeft = min([EyePoints['rightEyeUp']['X'], EyePoints['rightEyeDown']['X'], EyePoints['rightEyeRight']['X'], EyePoints['rightEyeLeft']['X']])
leftEye = im[leftTop:leftBottom+bityouseiy, leftLeft-bityouseix:leftRight+bityouseix]
leftEye = cv2.resize(leftEye, (leftEye.shape[1], int(leftEye.shape[0]*magnification)))
rightEye = im[rightTop:rightBottom+bityouseiy, rightLeft-bityouseix:rightRight+bityouseix]
rightEye = cv2.resize(rightEye, (rightEye.shape[1], int(rightEye.shape[0]*magnification)))
im[leftTop:leftTop+leftEye.shape[0], leftLeft-bityouseix:leftLeft+leftEye.shape[1]-bityouseix] = leftEye
im[rightTop:rightTop+rightEye.shape[0], rightLeft-bityouseix:rightLeft+rightEye.shape[1]-bityouseix] = rightEye
return {'status':200, 'message':'OK', 'img': cv2_to_base64(im)}
except Exception as e:
return {'status':500, 'message':str(e)}
以下部分で切り取るための座標を再度計算しています。
EyeUpのY座標をそのままTopとしないのは、目が斜めになっている場合を考慮してです。
(ただし、切り取ったあとにそのまま縦に伸ばしているだけなので、歪みはするのですが。)
なので必要であれば、例えば右が最大値になったときは斜めに伸ばす、などをすればもっと自然だと思いますが、今回はクソアプリなのでここまではしてません。
leftTop = min([EyePoints['leftEyeUp']['Y'], EyePoints['leftEyeDown']['Y'], EyePoints['leftEyeRight']['Y'], EyePoints['leftEyeLeft']['Y']])
leftBottom = max([EyePoints['leftEyeUp']['Y'], EyePoints['leftEyeDown']['Y'], EyePoints['leftEyeRight']['Y'], EyePoints['leftEyeLeft']['Y']])
leftRight = max([EyePoints['leftEyeUp']['X'], EyePoints['leftEyeDown']['X'], EyePoints['leftEyeRight']['X'], EyePoints['leftEyeLeft']['X']])
leftLeft = min([EyePoints['leftEyeUp']['X'], EyePoints['leftEyeDown']['X'], EyePoints['leftEyeRight']['X'], EyePoints['leftEyeLeft']['X']])
rightTop = min([EyePoints['rightEyeUp']['Y'], EyePoints['rightEyeDown']['Y'], EyePoints['rightEyeRight']['Y'], EyePoints['rightEyeLeft']['Y']])
rightBottom = max([EyePoints['rightEyeUp']['Y'], EyePoints['rightEyeDown']['Y'], EyePoints['rightEyeRight']['Y'], EyePoints['rightEyeLeft']['Y']])
rightRight = max([EyePoints['rightEyeUp']['X'], EyePoints['rightEyeDown']['X'], EyePoints['rightEyeRight']['X'], EyePoints['rightEyeLeft']['X']])
rightLeft = min([EyePoints['rightEyeUp']['X'], EyePoints['rightEyeDown']['X'], EyePoints['rightEyeRight']['X'], EyePoints['rightEyeLeft']['X']])
そしてその座標に応じて切り取ります。
leftEye = im[leftTop:leftBottom+bityouseiy, leftLeft-bityouseix:leftRight+bityouseix]
rightEye = im[rightTop:rightBottom+bityouseiy, rightLeft-bityouseix:rightRight+bityouseix]
切り取った左目と右目をリサイズ。
leftEye = cv2.resize(leftEye, (leftEye.shape[1], int(leftEye.shape[0]*magnification)))
rightEye = cv2.resize(rightEye, (rightEye.shape[1], int(rightEye.shape[0]*magnification)))
そして合成します。
im[leftTop:leftTop+leftEye.shape[0], leftLeft-bityouseix:leftLeft+leftEye.shape[1]-bityouseix] = leftEye
im[rightTop:rightTop+rightEye.shape[0], rightLeft-bityouseix:rightLeft+rightEye.shape[1]-bityouseix] = rightEye
境界線をぼかす
境界線をぼかすために、OpenCVのガウシアンフィルタを使いました。
参考
使い方はcv2.GaussianBlur()
の引数にぼかす部分(image)とカーネルサイズと標準偏差を指定するだけです。
def mosaic_area(src, x, y, width, height, blur_num):
dst = src.copy()
for i in range(blur_num):
dst[y:y + height, x:x + width] = cv2.GaussianBlur(dst[y:y + height, x:x + width], (3,3),3)
return dst
ちなみにどれぐらいぼかすかをフロント側から制御したかったので、blur_num
で何回このぼかしを実行するかを指定しています。
あとは上記のhandler関数の中で上記関数を組み込みます。
im = mosaic_area(im, leftLeft-bityouseix-int(bityouseix/2), leftTop, bityouseix, leftEye.shape[0]+bityouseiy, blur_num)
im = mosaic_area(im, leftRight+int(bityouseix/2), leftTop, bityouseix, leftEye.shape[0]+bityouseiy, blur_num)
im = mosaic_area(im, leftLeft-bityouseix, leftTop+leftEye.shape[0]-int(bityouseiy/2), leftEye.shape[1], bityouseiy, blur_num)
im = mosaic_area(im, rightLeft-bityouseix-int(bityouseix/2), rightTop, bityouseix, rightEye.shape[0]+bityouseiy, blur_num)
im = mosaic_area(im, rightRight+int(bityouseix/2), rightTop, bityouseix, rightEye.shape[0]+bityouseiy, blur_num)
im = mosaic_area(im, rightLeft-bityouseix, rightTop+rightEye.shape[0]-int(bityouseiy/2), rightEye.shape[1], bityouseiy, blur_num)
今回は目の上の部分は何も変更がないのでぼかす必要はありませんが、目の左、右、下部分は広げて合成しているため境界線が見えてしまいます。それぞれの境界線でぼかしを実行しているため、(左、右、下)×両目部分のぼかしのために上記の6回ぼかしフィルターを使っています。
あとはこれをLambdaにデプロイしてからAPIGatewayでAPI化するなりしたら完成です。
OpenCVをLambdaで動かすためには少しコツがいるので、必要であれば【LambdaでOpenCVを利用】AWSとOpenCVを利用してポケモン画像でアスキーアート風に変換するAPIを作ったを読んでください。
あとがき
今回LambdaでAmazon Rekognitionを動かすためにLambdaに対して権限を渡す必要があるのですが、それを忘れていて最初はうまく動かすことができなくて少し苦労しました。笑
AWSでのAPI作成はようやく慣れてきたので今後も色々作っていきたいなと思っています。
最後まで読んでいただきありがとうございました!