Edited at

OpenVINO の 顔検出・分析デモを Pythonでやってみる


はじめに

Intel が 提供する OpenVINO Tool Kit をインストールすると様々な 画像認識系のディープラーニング(CNN)のデモが利用できます。ここではリアルタイムでの顔検出デモ ( interactive_face_detecion )を真似て、Pythonで実装した内容をまとめます。

お手本となる顔検出・分析 の内容は以下のIntel のサイトで紹介されています。

このチュートリアルでは、顔検出と顔分析(年齢/性別、感情認識と頭部の向き推定)について書かれています。


This tutorial explores the use of deep learning models for face detection, age, gender, and emotion recognition, and head pose estimation included in versions of the Intel® Distribution of OpenVINO™ toolkit.

https://software.intel.com/en-us/articles/use-the-deep-learning-recognition-models-in-the-intel-distribution-of-openvino-toolkit



使用モデル

使用モデルの一覧です。

Input/Outputは実際に確認した値です。説明は Githubの公式リポジトリ からの転記です。


  • Models

model
input name: shape
output name: shape

face-detection-adas-0001
'data': [1, 3, 384, 672]
An input image in the format [BxCxHxW]
Expected color order is BGR.
'detection_out':[1, 1, 200, 7]
format: [image_id, label, conf, x_min, y_min, x_max, y_max]

age-gender-recognition-retail-0013
"data": [1, 3, 62, 62]
"age_conv3": [1, 1, 1, 1]
Estimated age divided by 100.
"prob": [1, 2, 1, 1]
Softmax output across 2 type classes [female, male]

emotion-recognition-retail-0003
"data": [1, 3, 64, 64]
"prob_emotion": [1, 5, 1, 1]
Softmax output across five emotions ('neutral', 'happy', 'sad', 'surprise', 'anger').

head-pose-estimation-adas-0001
"data": [1, 3, 60, 60]
(Inference Engine format)
angle_y_fc:[1, 1]
angle_p_fc:[1, 1]
angle_r_fc: [1, 1]
Supported ranges YAW [-90,90], PITCH [-70,70], ROLL [-70,70]

facial-landmarks-35-adas-0001
"data" shape:[1, 3, 60, 60]
"align_fc3": [1, 70]
70 floating point values for 35 landmarks' normed coordinates in the form (x0, y0, x1, y1, ..., x34, y34).


顔検出・分析モデルの紹介

デモで使用されている顔検出・分析の実行結果を紹介します。画像は、 Microsoftの How-Old.net のものでテストしています。

使用したJupyter notebook は、Gistに上げました。"image_url" 部分を変えれば他の画像でも試すことができます。

https://gist.github.com/kodamap/aa747ae2058bbb919e0308cf8b5f1718


Face Detection

まずはベースとなる顔検出です。

以下は、Pythonで OpenVINOの 推論エンジン(Inference Engine) を使う時の処理の流れです。これは他のモデルも共通です。


1. Pluginを初期化

Plugin の初期化は デバイス毎に一度だけやります。各モデル毎にデバイス(Plugin) を指定することもできます。(例えば Face Detection は CPU, 他の分析は MYRIADというように。)

# 1. Plugin initialization for specified device and load extensions library if specified

device = "CPU"
fp_path = "../extension/IR/FP32/" if device == "CPU" else "../extension/IR/FP16/"
plugin = IEPlugin(device=device, plugin_dirs=None)
if device == "CPU":
plugin.add_cpu_extension("../extension/cpu_extension.dll")


2. IRの読み込み

# 2.Read IR

model_xml = fp_path + "face-detection-adas-0001.xml"
model_bin = os.path.splitext(model_xml)[0] + ".bin"
net = IENetwork(model=model_xml, weights=model_bin)

IR(intermediate representation)とは Caffe*, TensorFlow* などの一般的なフレームワークで作成した学習済みモデルを OpenVINO の推論エンジン用にコンバートした「中間表現フォーマット」(xml,bin)です。


3. Input/Outputを設定

モデルの Input / Output の情報を取得します。 Input の n, c, h ,w は、 それぞれ number of batch size, number of channels, image height, image widthです。

# 3. Configure input & output

input_blob = next(iter(net.inputs))
out_blob = next(iter(net.outputs))
n, c, h, w = net.inputs[input_blob].shape


4. Plugin にモデルをロード

# 4.Load Model

exec_net = plugin.load(network=net, num_requests=2)


5. 非同期リクエスト

フレームをInputの形に整えて 非同期リクエストします。

# 5. Create Async Request

in_frame = cv2.resize(frame, (w, h))
in_frame = in_frame.transpose((2, 0, 1))
in_frame = in_frame.reshape((n, c, h, w))
exec_net.start_async(request_id=0, inputs={input_blob: in_frame}) # res's shape: [1, 1, 200, 7]


6. 結果を受け取る

非同期の場合、exec_net.requests[0].wait(-1) が必須です。ここでは「顔」として検出する閾値を 0.5 としています。

# 6. Receive Async Request

if exec_net.requests[0].wait(-1) == 0:
res = exec_net.requests[0].outputs[out_blob]
faces = res[0][:, np.where(res[0][0][:, 2] > 0.5)] # prob threshold : 0.5

出力フォーマットは [image_id, label, conf, x_min, y_min, x_max, y_max] です。3列目の数値が「顔」の確率です。上で指定した 0.5 より大きい行が取得されます。(4つの顔が検出されたので4行)


Output(faces)

array([[[[0.        , 1.        , 0.99999905, 0.8040092 , 0.5131588 ,

0.9414059 , 0.7515552 ],
[0. , 1. , 0.9999846 , 0.34716758, 0.14653066,
0.47502246, 0.41063544],
[0. , 1. , 0.9999583 , 0.67403555, 0.20618674,
0.80854833, 0.41743663],
[0. , 1. , 0.9983182 , 0.07791171, 0.22670019,
0.22216976, 0.47467715]]]], dtype=float32)


7. 顔に枠を描画

x_min, y_min, x_max, y_max に元フレームの width , height を掛けて座標を取得します。

検出した顔(faces)はさらに、Age/Gender, Emotions, Head pose , Facial landmark で使用します。アプリケーションで使う場合には、Face Detection で検出した顔ごとに各種分析を処理します。

# 7. draw faces

frame = init_frame.copy()
for face in faces[0][0]:
box = face[3:7] * np.array([frame_w, frame_h, frame_w, frame_h])
(xmin, ymin, xmax, ymax) = box.astype("int")
...
# 他の分析を face 毎に処理
...
# 最後に描画
cv2.rectangle(frame, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2)


Age/Gender Recognition

このモデルの制限として、認識できる年齢の幅は 18 - 75 で、training set に子供は含んでいないとのこと。なので下の画像のお子様の年齢/性別はうまく判定されていません。また、大人でも年齢は6-7才若く予想されるように思います。


The network is able to recognize age of people in [18, 75] years old range, it is not applicable for children since their faces were not in the training set.


アウトプットが複数あるのでリクエストで Outputs の key に 'age_conv3'、'prob' を指定します。

...

# 5. Get reponse
exec_net.start_async(request_id=0 ,inputs={input_blob: in_frame})
if exec_net.requests[0].wait(-1) == 0:
age = exec_net.requests[0].outputs['age_conv3']
prob = exec_net.requests[0].outputs['prob']

1つの顔につき、prob(ここでは性別)のアウトプットの形は [1, 2, 1, 1] です。左から2つ目の配列に Female, Male のパーセンテージが入っています。


Outout(prob)

array([[[[0.07477977]],

[[0.9252202 ]]]], dtype=float32)


出力ラベルから確率が最大値であるインデックスの値を取得すれば性別が判定できます。(上の例では、0.9252202が大きいので2番目のラベル "Male" を得ます。)

label = ('Female', 'Male')

...
# 5. Get reponse
if exec_net.requests[0].wait(-1) == 0:
age = exec_net.requests[0].outputs['age_conv3']
prob = exec_net.requests[0].outputs['prob']
age = age[0][0][0][0] * 100
gender = label[np.argmax(prob[0])]


Emotion recognition

みんな happy ですが、他の表情も割と正確に判断してくれます。

ラベルは、順に'neutral', 'happy', 'sad', 'surprise', 'anger' です。


Output(prob_emotion)

array([[[[4.6693120e-04]],

[[9.9327552e-01]],

[[5.3842259e-03]],

[[6.9855538e-04]],

[[1.7480589e-04]]]], dtype=float32)


Genderと同じように、最大値のラベル "happy" を得ます。

# Emotion Recognition

label = ('neutral', 'happy', 'sad', 'surprise', 'anger')
...
# 5. Get reponse
if exec_net.requests[0].wait(-1) == 0:
res = exec_net.requests[0].outputs[out_blob]
emotion = label[np.argmax(res[0])]


Facial Landmarks Recognission

目を閉じたのが分かれば、居眠り判定に使えそうですが検出点が足りず変化なしでした。

(x0, y0, x1, y1, ..., x34, y34)というように 顔の特徴を表す35コの座標 (x, y) が返ってきます。値は 0 - 1の範囲で正規化されています。


Output(align_fc3)

array([0.5010737 , 0.4165041 , 0.35779357, 0.35278273, 0.716592  ,

0.48957786, 0.85324055, 0.52083653, 0.5638323 , 0.6584594 ,
0.5294477 , 0.7084239 , 0.42417508, 0.6257404 , 0.6465968 ,
0.6950229 , 0.33009773, 0.7238579 , 0.6208136 , 0.812707 ,
0.49984622, 0.7688369 , 0.4660769 , 0.83963037, 0.31695354,
0.23903084, 0.46712804, 0.24487072, 0.57751596, 0.34223077,
0.73529005, 0.3982857 , 0.86269015, 0.3879333 , 0.9438973 ,
0.45161498, 0.15577579, 0.26196492, 0.12226337, 0.3641823 ,
0.10097629, 0.4659006 , 0.09227693, 0.56769276, 0.10503221,
0.6665518 , 0.14787704, 0.7613008 , 0.21194094, 0.8451543 ,
0.29245877, 0.9190828 , 0.40447468, 0.9693033 , 0.50801575,
0.97025037, 0.5961952 , 0.9372798 , 0.67800665, 0.8949146 ,
0.75121903, 0.8414749 , 0.8133403 , 0.77663666, 0.8666756 ,
0.70392895, 0.91145146, 0.6258948 , 0.9513542 , 0.5379647 ],
dtype=float32)

これに検出した顔フレームの width, height を掛けて、さらに顔フレームの左上(ymin, ymax)の位置をそれぞれ足して 点の座標(x,y)を取得します。

    # 6. draw  Response        

for i in range(int(normed_landmarks.size / 2)):
normed_x = normed_landmarks[2 * i]
normed_y = normed_landmarks[2 * i + 1]
x_lm = xmin + face_frame.shape[1] * normed_x
y_lm = ymin + face_frame.shape[0] * normed_y
cv2.circle(frame, (int(x_lm), int(y_lm)), 1 + int(0.03 * face_frame.shape[1]), (255, 255, 0), -1)

MYRIAD プラグインでは、結果(黄色い点)がうまく描画されませんでした。


Head Pose Estimation

頭の向きを3Dの軸で描画します。Raspberry Pi を利用すれば、わき見運転検知に使えるでしょうか。

Head Post Estimation は苦労しました。Outputから、最終的な描画までの過程が難解だったからです。結局は理解できていないので詳しい説明はできないのですが、ポイントをいくつか記載します。(お手本のC++ の処理をそのままpythonで書いたつもりですが間違いがあるかもしれません。)


  • Outputs は、yaw, pitch, roll(軸に対する角度)で、yaw(首の左右の動き), pitch(上下の動き), roll(首をかしげる動き)に相当します。この角度に対応する軸(X, Y ,Z)にどれが対応するのか分からず混乱しましたが、お手本のプログラムと描画結果を合わせると以下の図のイメージになると思います。



  • 回転行列(Rotation matrix) の積を求める際は順番が重要 (Rz, Ry, Rxの順)です。


  • 回転行列積(R)の計算では numpy と opencv で行列積の求め方の違いに嵌まりました。C++ と Python の違いについては、以下を参考にしました。

    https://www.learnopencv.com/rotation-matrix-to-euler-angles/

    最終的には、Python3.5 以降で使える @ 演算子で行列積を計算しています。(c++ と同じように書けるのでシンプルです。)



draw_axes

...

def draw_axes(frame, center_of_face, yaw, pitch, roll, scale, focal_length):
yaw *= np.pi / 180.0
pitch *= np.pi / 180.0
roll *= np.pi / 180.0

cx = int(center_of_face[0])
cy = int(center_of_face[1])

Rx = np.array([[1, 0, 0],
[0, math.cos(pitch), -math.sin(pitch)],
[0, math.sin(pitch), math.cos(pitch)]])
Ry = np.array([[math.cos(yaw), 0, -math.sin(yaw)],
[0, 1, 0],
[math.sin(yaw), 0, math.cos(yaw)]])
Rz = np.array([[math.cos(roll), -math.sin(roll), 0],
[math.sin(roll), math.cos(roll), 0],
[0, 0, 1]])

#R = np.dot(Rz, Ry, Rz) # demo code (c++) : auto r = cv::Mat(Rz*Ry*Rx);
#ref: https://www.learnopencv.com/rotation-matrix-to-euler-angles/
##R = np.dot(Rz, np.dot( Ry, Rx ))
R = Rz @ Ry @ Rx # Python 3.5+
...



Pythonでの実装

前置きが長くなりました。以上を踏まえ Jupyter notebook で各モデルを確認しつつ最終的にできたのがこれです。WebUIでボタンで各モデルを切り替えるようにしています。デバイスは、Raspberry Pi 3B です。

demo

Github: https://github.com/kodamap/object_detection_demo

実装ができなかったのは、「顔分析」の非同期処理です。

「顔分析」の非同期処理では、「非同期リクエスト/結果」と「どの顔の結果なのか」を紐づける仕組みが必要となります。「顔検出」の非同期モード(フレーム先読みと現在フレームの交互処理の仕組み)をそのまま使うと「顔」の分析結果がずれます。例えば、男女二人の顔があったすると非同期ゆえに性別/年齢が入れ替わってしまいます。(C++のお手本ではキューを使っている?)

なんとか実装しました。(2/6)


まとめ・感想

顔検出・分析デモについて一応理解できたと思います。汎用的なモデルですが、アイデア次第で安全運転支援や表情によるイベント操作など何かに利用できるような気がします。

また、Pythonでの実装にあたり推論モデル毎にデバイス指定ができること、推論結果を用いてさらに推論させる方法が分かりました。

次はこの仕組みを利用してドローン(Tello) のストリーミングキャプチャで顔検出・分析をやってみます。