2020/06/09
「AI CORE XスターターキットとOpenVINO™ですぐに始めるディープラーニング推論」シリーズの10回目記事です。
このシリーズは、「ディープラーニングとは何か」から始まり、「各種ツールの使い方」「プログラミング基礎」「プログラミング応用・実践」までをステップバイステップでじっくり学び、自分で理解してオリジナルのAIアプリケーションが作れるようになることを目指しています。
第10回目はこれまでの内容を使ってリアルタイムなバーチャル試着アプリを作成します!
バーチャル試着で、カメラ画像に対してpng画像を描画させる際に必要となる情報は以下の3つだけです。
スケールは元の画像に対して何倍の大きさにするかの設定、角度は画像の中心座標を基準としてどれだけ回転させるかの設定、座標は画像の中心座標を基準としてどの位置に表示させるかの設定です。この3つの情報が分かれば、バーチャル試着アプリが完成したも同然です。
この3つの情報を得るためには、ちょっとだけ算数や数学の式が必要になりますが、1つずつ丁寧に解説しますので安心してください。みなさんはコードで式を書くだけで、計算自体はPythonが行います。
前回のツールで赤い眼鏡(6629_trim_small.png)のEyePointを出しました。今回はそれぞれの座標の値を EPL_x
, EPL_y
, EPR_x
, EPR_y
という変数に代入して使います。数字は人によって多少違うと思いますので、皆さんがツールで出力した値を使っていただいて構いません。
# EyePoint情報(6629_trim_small.png)EPL_x = 137EPL_y = 237EPR_x = 381EPR_y = 237
では、これから「スケール」「座標」「角度」の順に1つずつ対応してゆきます。
まず「スケール」のみの対応です。
アイテム画像の大きさは常に一定であるのに対し、カメラに映る顔の大きさはカメラとの位置関係によって変わってきます。ここでは、顔の大きさに合わせてアイテム画像の大きさを縮小もしくは拡大するために、掛け算する値のことをスケールと呼ぶことにします。
「アイテム画像」と「カメラに映る顔」で、共通なものに対し比較すればスケールが求まりそうです。今回は左右のEyePointの距離EP_distance
とカメラに映った左右の目の距離eye_distance
を比較し、スケールを決めることにしましょう
2点の座標が分かっているとき、座標2点間の距離は以下の式から求まります
これは三平方の定理(ピタゴラスの定理)から求まりますので、興味のある方は調べてみてください
pythonでルート(平方根)を計算するには masth.sqrt
を使います。例えば√2 であれば math.sqrt(2)
です。また、pythonで2乗を計算する場合は **
を使います。例えば10の2乗であれば10 ** 2
です。
まずEyePoint側の距離EP_distance
は以下のコードで求まります
import math# EyePoint距離EP_distance = math
次にカメラに映った目の距離eye_distance
ですが、ディープラーニングでは以下の変数に目の座標を代入していますので、この変数を利用します。
eye_left_x
eye_left_y
eye_right_x
eye_right_y
先程と同様に以下のコードで求まります。
import math# 目の距離eye_distance = math
これで、EyePoint、実際の目、それぞれの左右間の距離を得ることが出来ました。
スケール、つまり比率は以下の計算式で求まります。
比率 = 対象 / 基準
今回は対象が「カメラに映った目の距離」、基準が「EyePoint距離」なのでスケールitem_scale
は以下のコードで求まります
# アイテムのスケールitem_scale = eye_distance / EP_distance
アイテムを表示する位置はとりあえず仮固定で(300, 100)
としましょう。frame
に対して、item_scale
を適用したアイテム描画は以下で可能です
from pngoverlay import PNGOverlayitem =# アイテム描画item # スケールitem # 仮の座標
これでスケール対応の準備が整いました。8回目記事の最後のコードから「目の位置に表示」部を削除して、これまでの内容を盛り込んでみて下さい。
全て盛り込んだソースコードは以下のリンク先にありますが、まずは自分で作成できるか挑戦してみてください。
実行結果はこんな感じになります
どうでしょうか。位置は(300, 100)
で固定ですが、カメラと顔の距離関係に合わせてアイテムの大きさが適切に変化していると思います。なお、顔や目がカメラからはみ出るとディープラーニングは正しく結果を出せないため、アイテム表示も正しくない表示になりますのでご注意ください
コードの解説です。
基本的には「スケール対応」と書かれた箇所に今回の内容を盛り込んでいます。全てwhile True:
の中に書いても良いのですが、EyePoint座標やEyePoint距離などの数値はカメラ映像によって変わるものではないため、while Ture:
の直前に書いています。またimport
関連は最初にまとめています。
次はアイテムを表示する位置を実際の目の位置に合わせたいと思います。
図のように、左目位置と左EyePoint位置を一致させる前提で進めます。
前回のツールでもお伝えしましたが、PNGOverlayは指定座標に対し、画像の中心が来るように描画します
item
例えば上記コードに記述した内容は下記の図の左側ではなく、右側になります
アイテム画像の左上を(300, 100)
に描くのではなく、アイテム画像の中心を(300, 100)
に描いています。
最終的に知りたい情報は、アイテム画像(中心)をカメラ画像frameのどこに表示するかです。
しかし、「アイテム画像(中心)」と「カメラ画像frame」には直接の関係性はないため、間接的な関連をたどって計算したいと思います。
アイテム画像(中心)について以下の順に求めてゆきます
「アイテム画像(中心)」と「EyePoint」の関連が分かれば求まります。
EyePointの各座標は、アイテム画像(左上)からの数値を表しています。
つまり、アイテム画像(左上)を基準とした場合の EyePoint_leftの座標は(EPL_x, EPL_y)
です。
逆にEyePoint_leftを基準とした場合のアイテム画像(左上)の座標は(-EPL_x, -EPL_y)
です
アイテム画像の幅と高さはitem.width
, item.height
で取得できます。
よって、EyePoint_leftを基準とした場合のアイテム画像(中心)の座標は以下で求めることができます
# アイテム座標(EyePoint_left基準)item_x_EPL = item.width/2 - EPL_xitem_y_EPL = item.height/2 - EPL_y
先程の座標(item_x_EPL, item_y_EPL)
に「EyePoint」と「eye」の関連を加えれば求まります。
先程述べたように、EyePoint_left と eye_left の座標は同じです。
そのとき、 左目以外の座標はどうでしょうか?
スケールを考慮する必要があることが分かると思います
これを踏まえると、eye_leftを基準とした場合のアイテム画像(中心)の座標は以下で求めることとなります
# アイテム座標(左目基準)item_x_eyeleft = item_x_EPL * item_scaleitem_y_eyeleft = item_y_EPL * item_scale
先程の座標(item_x_eyeleft, item_y_eyeleft)
に「frame」と「eye」の関連を加えれば求まります
eyeはframeをディープラーニングした結果の座標です。
つまり、eye_leftの位置はframe原点から座標です。
これを加えることにより、「frame」基準のアイテム座標位置が決まります
# アイテム座標item_x = item_x_eyeleft + eye_left_xitem_y = item_y_eyeleft + eye_left_y
これで座標対応の準備が整いました。先程の「バーチャル試着アプリ完成前コード(スケールのみ対応)」に今までの内容を盛り込んでみて下さい
全て盛り込んだソースコードは以下のリンク先にあります。
実行結果はこんな感じになります
かなり完成に近づきました。ただし、首を傾けるなど「角度」があるとずれます。
コードの解説です。
EyePoint_left基準のアイテム座標計算は、カメラ画像によって影響を受けないため、While True:
直前に追加、その他はWhile True:
のアイテム描画前に加えています。またitem.show
で座標item_x, item_y
を指定する際に、整数である必要があるためint()
で整数化しています。
最後に「角度」対応したいと思います。
PNGOverlayは角度を与えると画像の中心で簡単に回転できます。
つまり、回転角度さえ分かれば良いということです
そして、目の傾きは、左目に対して右目がどれだけの角度があるかを調べれば良さそうです。角度を調べる際に三角関数が必要となりますので、さらっと説明します。
三角関数 sinθ、cosθ、tanθは、角度θさえ分かれば直角三角形の2辺の比を求めることが出来る便利な計算式です。
これとは逆に直角三角形の2辺の比が分かることで角度θの値を求めることができます。
そのときに使われるのは「逆三角関数」で、arcsin, arccos, arctanと呼ばれています。数式で書く場合は-1
を使って以下のように表記します
この式が示す通り、θを求める方法が3通りあるということです。今回はarctanを活用したいと思います。
三角関数や逆三角関数は直角三角形だけではなく、座標においても適用可能です。
例えば原点(0, 0)に対し、点の座標(a, b)が分かっている場合は、arctanを使って図の角度θを求めることが可能です。
Pythonプログラムでarctanを使う場合はmath.atan()
もしくはmath.atan2()
という関数を使います。今回のように-90~+90度の範囲を超える可能性がある場合はmath.atan2()
の方を使います。
math.atan2は引数が2つあり、b, a の順に記述しますのでご注意下さい
math.atan2(b, a)
戻り値の角度ですが、単位は度(degree)ではなく、radianという単位です。
radianからdegreeへの変換はPythonの場合、math.degrees()
という関数があるので、こちらを活用します
以下の画像のときの角度をPythonで新規コードを作成して確かめてみましょう
# 新規作成でatan2確認コードimport mathangle = mathprint(math)
ほぼ30度で値が出たと思います。先程の図形はよく数学で出てくる辺の長さの比が1:2:√3という直角三角形です。√3 ≒ 1.73 です
これを実際の目eyeの座標に当てはめます。eye_leftを基準として考えた相対座標で求めます。
# 目の角度eye_angle = math
これでeyeの角度が分かりました。
一方でEyePointは今までたまたま0度でしたが、もしかしたら角度を持ったEyePointも出てくる可能性があります。そのようなアイテム画像にも対応できるようにしたいと思います。
EyePointの角度も同様にコード化します
# EyePointの角度EP_angle = math
先程の図のように、最初からEyePointが傾いている場合は、eyeに対して足りない角度だけ対応すればよさそうです。コードで書くと次のように単純な引き算となります
# アイテムの回転角度item_angle = eye_angle - EP_angle
これで角度が求まりました!
OpenCVの座標系の回転は時計回りが正なのに対し、PNGoverlayの回転方向は反時計回りが正です。
実際に以下のコードを新規作成して実行してみて下さい
赤いメガネが画像中心を基準として反時計回りに60度回転しています
import cv2import numpy as npfrom pngoverlay import PNGOverlay# 白画の生成img = np + 255# PNGOverlayインスタンス生成item =# 透過PNGを描画itemitemcv2cv2cv2
これまでの座標系では、時計回りかつ角度の単位はradianですので、rotate()
で引数を指定する際に変換が必要となります。では、今までの内容を盛り込んでみて下さい。
盛り込んだコードの回答例はこちらです
バーチャル試着アプリ完成前コード(スケール・座標・角度対応)
実行結果はこんな感じになります
メガネは確かに同じように回転しています。しかし、位置が少しずれていますね?
この後、この原因と対策を行いたいと思います。
コードの解説です。
EyePointの角度のみwhile True:
の直前、その他はwhile True:
の中に記述しています。item.rotate(-math.degrees(item_angle))
にて、-1
を掛けることにより逆回転、math.degrees
によりradianからdegreeに変換しているのがポイントです。
なお、角度関連のコードは「スケール」と「座標」の間に挿入しています。
先程のコードで位置がずれた原因です。
下の図を見てください。左側は「目」も「アイテム画像」もeye_leftを基準として回転していますが、右側は「アイテム画像」はアイテム画像の中心を基準として回転しています。つまり、角度としては合っていますが、どこを中心として回転するかでずれが生じるということです。
スライドアプリや描画ツールのように2つの画像をグループ化して回転することが出来れば簡単ですが、カメラ画像とアイテム画像をグループ化するようなことは出来ません。
では、どうすれば良いでしょうか?
頭に帽子を載せていて首を曲げたとき、左目を中心とすると帽子は単にその場で回転だけではなく、移動も伴うはずです。この左目を中心とする回転に対する移動量が分かれば良さそうです
緑の点がアイテム画像の中心を表しています。
回転移動は数学で有名な式があります。
原点を中心に点(x1, y2)から角度θで回転したときの点(x2, y2)の座標は以下の式で表せます
sinやcosは少し前に出てきた三角関数です。なぜこの式になるのか興味のある方は回転行列などで調べてみて下さい
この式をPythonのコードで書くと以下のようになります。
# (x1, y1)をitem_angle回転させた座標x2 = x1 * math - y1 * mathy2 = x1 * math + y1 * math
(x1, y1) は(item_x_eyeleft, item_y_eyeleft)
に相当しますので、先程のコードの中の「アイテム座標(左目基準)」と「アイテム座標」の間で計算してあげる必要があります。
これを盛り込んだコードがこちらの最終コードです
#==================================================# 準備#==================================================# importimport cv2import numpy as npfrom openvino.inference_engine import IENetwork, IEPluginimport mathfrom pngoverlay import PNGOverlay# ターゲットデバイスの指定plugin =# モデルの読み込みと入出力データのキー取得(顔検出)net_face =exec_net_face = plugininput_blob_face =out_blob_face =# モデルの読み込みと入出力データのキー取得(landmarks)net_landmarks =exec_net_landmarks = plugininput_blob_landmarks =out_blob_landmarks =# カメラ準備cap = cv2# PNGOverlayインスタンス生成item =# EyePoint情報(6629_trim_small.png)EPL_x = 137EPL_y = 237EPR_x = 381EPR_y = 237# EyePoint距離EP_distance = math# EyePointの角度EP_angle = math# アイテム座標(EyePoint_left基準)item_x_EPL = item.width/2 - EPL_xitem_y_EPL = item.height/2 - EPL_y#==================================================# メインループ#==================================================while True:# キー押下で終了key = cv2if key != -1:break# カメラ画像読み込みret, frame = cap# 入力データフォーマットへ変換img = cv2 # HeightとWidth変更img = img # HWC > CHWimg = np # CHW > BCHW# 推論実行out = exec_net_face# 出力から必要なデータのみ取り出しout =# 不要な次元を削減out = np# 検出されたすべての顔領域に対して1つずつ処理for detection in out:# conf値の取得confidence =# バウンディングボックス座標を入力画像のスケールに変換xmin =ymin =xmax =ymax =# conf値が0.5より大きい場合のみLandmarks推論とバウンディングボックス表示if confidence > 0.5:# 顔検出領域はカメラ範囲内に補正する。特にminは補正しないとエラーになるif xmin < 0:xmin = 0if ymin < 0:ymin = 0if xmax > frame.shape:xmax = frame.shapeif ymax > frame.shape:ymax = frame.shape#--------------------------------------------------# ディープラーニングLandmarks推定#--------------------------------------------------# 顔領域のみ切り出しimg_face =# 入力データフォーマットへ変換img = cv2 # HeightとWidth変更img = img # HWC > CHWimg = np # CHW > BCHW# 推論実行out = exec_net_landmarks# 出力から必要なデータのみ取り出しout =# 不要な次元を削減out = np# 目の座標を顔画像のスケールに変換し、オフセット考慮eye_left_x = + xmineye_left_y = + ymineye_right_x = + xmineye_right_y = + ymin#--------------------------------------------------# アイテムのスケール・座標・角度・回転移動対応#--------------------------------------------------# 目の距離eye_distance = math# アイテムのスケールitem_scale = eye_distance / EP_distance# 目の角度eye_angle = math# アイテムの回転角度item_angle = eye_angle - EP_angle# アイテム座標(左目基準)item_x_eyeleft = item_x_EPL * item_scaleitem_y_eyeleft = item_y_EPL * item_scale# アイテム座標(左目基準)をitem_angle回転させた座標x2 = item_x_eyeleft * math - item_y_eyeleft * mathy2 = item_x_eyeleft * math + item_y_eyeleft * math# アイテム座標item_x = x2 + eye_left_xitem_y = y2 + eye_left_y# アイテム描画item # スケールitem # 角度item # 座標# 画像表示cv2#==================================================# 終了処理#==================================================capcv2
変更点はwhile True:
ループ内の「アイテム座標(左目基準)をitem_angle
回転させた座標」追加と「アイテム座標」の変更のみです。
実行結果はこんな感じになります
いかがでしょうか?
これで、首を傾けても合うようになりました!
ただし、90度近くまで回転させるとディープラーニングのランドマーク回帰自体が正しく検出できないため、表示も正しくなくなりますのでご注意ください。
お疲れさまでした!!
ぜひ、いろんなアイテム画像で試してみて下さい。その際、事前にツールを使ってEyePoint情報を取得しておくことをお忘れなく。
次回は、少し話題を変えて、OpenVINO™ツールキットの特徴の1つ「Model Optimizer」を使ったディープラーニングに挑戦します。
以上、「バーチャル試着アプリ」でした。