はじめに
自分自身の姿のかわりに、キャラクターの姿で仮想空間に登場させることができる、アバター。
面白そうでやってみたいなーと思っていたので、作ることにしました☺️
やりたいこと:
・フェイストラッキングを実装する
・ハンドトラッキングを実装する
・アバターを作る
・組み合わせて仮想空間で動かせるようにする
これらを達成するのに、無料/有料ツールがいくつも公開されていますが、せっかくなので自分で作ってみます。
今回は表情と両手の検出を試していきます。
環境構築
画像処理の基本にはOpenCVを使用します。
OpenCVは以下の言語がサポートされています。今回はPythonを使用します。
C++
Python
Java
顔や手の検出には、Googleより提供されているMediapipeという仕組みを使用します。
フェイストラッキングやハンドトラッキングには他の仕組みもありますが、
最終的には顔と手を同時にトラッキングしたいので、Mediapipeが最適かなと思いました。
※
eightはMacを使っているので、ツールのインストール方法やFPSはみなさんの環境とは異なるかもしれません
VSCode
こちらを参考に、Python用の拡張機能をインストールします。
Code Runner
Python
Python Debugger
Python
homebrewでインストールします。
homebrewそのもののインストール方法は公式HPより参照ください。
https://brew.sh/ja/
ターミナルで以下コマンドを実行します。
$ brew install python
インストール後、ターミナルで
python -V
コマンド実行すると、インストールしたバージョンが表示されます。
$ python -V
Python 3.12.5
Mediapipe
ターミナルで、以下コマンドを実行します。
$ python3 -m pip install mediapipe
こんな感じになればインストール成功です。
VSCodeでお試し
試しに以下のコードのように、cv2をimportしてみると・・・
import cv2
print(“Hello World”)
print(cv2.__version__)
1行目に黄色い波線が出て、エラーになってしまいました😭
インポート “cv2” を解決できませんでした
解決策は以下の通り。
ターミナルで以下コマンドを実行します。
$ python
>>>import sys
>>>print(sys.executable)
実行するとpythonバージョンが表示されます。これを覚えておいてください。
VSCode画面左下の歯車マーク→コマンドパレットで、「インタープリター」と検索すると、以下のような選択肢が出てきます。
先ほどターミナルで表示されたバージョンを選択しましょう。
すると、黄色波線が消えて、問題なく実行できるようになりました。
フェイストラッキングとハンドトラッキングの実装
ひとまず、ノートPC内蔵カメラでのフェイストラッキングとハンドトラッキングを実装しました。
ソースコードを貼ります。
カメラで顔と手をトラッキングするサンプルコード
※何やらインデントがおかしいので、各自で調節お願いします
import cv2
import mediapipe as mp
import numpy as np
# フェイストラッキング関数
def face_mesh_exec(img, face_mesh):
img.flags.writeable = False
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# 顔ランドマーク検出
results = face_mesh.process(img)
img.flags.writeable = True
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
# 背景を黒にして、ランドマーク画像だけ残す
# 元画像に重ねたい場合はコメントアウトする
img_h, img_w, _ = img.shape
blank = np.zeros((img_h, img_w, 3))
img = blank
if results.multi_face_landmarks:
# 検出したランドマークを画像内に描画
for face_landmarks in results.multi_face_landmarks:
# 顔の中をメッシュ表示する場合、このコメントアウトを解除する
# mp_drawing.draw_landmarks(
# image=img,
# landmark_list=face_landmarks,
# connections=mp_face_mesh.FACEMESH_TESSELATION,
# landmark_drawing_spec=None,
# connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_tesselation_style())
mp_drawing.draw_landmarks(
image=img,
landmark_list=face_landmarks,
connections=mp_face_mesh.FACEMESH_CONTOURS, # 目、口、輪郭の境界線
landmark_drawing_spec=None,
connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_contours_style())
mp_drawing.draw_landmarks(
image=img,
landmark_list=face_landmarks,
connections=mp_face_mesh.FACEMESH_IRISES, # 虹彩
landmark_drawing_spec=None,
connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_iris_connections_style())
return img
# ハンドトラッキング関数
def hands_exec(img, hands):
img.flags.writeable = False
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# 手ランドマーク検出
results = hands.process(img)
img.flags.writeable = True
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
# 背景を黒にして、ランドマーク画像だけ残す
# 元画像に重ねたい場合はコメントアウトする
img_h, img_w, _ = img.shape
blank = np.zeros((img_h, img_w, 3))
img = blank
if results.multi_hand_landmarks:
# 検出したランドマークを画像内に描画
for hand_landmarks in results.multi_hand_landmarks:
mp_drawing.draw_landmarks(
image=img,
landmark_list=hand_landmarks,
connections=mp.solutions.hands.HAND_CONNECTIONS,
landmark_drawing_spec=mp_drawing_styles.get_default_hand_landmarks_style(),
connection_drawing_spec=mp_drawing_styles.get_default_hand_connections_style())
return img
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
mp_face_mesh = mp.solutions.face_mesh
mp_hands = mp.solutions.hands
cap = cv2.VideoCapture(0) # カメラID 内蔵カメラはおそらく0
# max_num_faces = 画像内で検出する顔の数
# refine_landmarks = 目の周りを細かくするか
# min_detection_confidence = ランドマーク検出成功判定の閾値
# min_tracking_confidence = ランドマークトラッキング成功判定の閾値
face_mesh = mp_face_mesh.FaceMesh(
max_num_faces=1,
refine_landmarks=True,
min_detection_confidence=0.5,
min_tracking_confidence=0.5)
# max_num_hands = 画像内で検出する手の数
# min_detection_confidence = ランドマーク検出成功判定の閾値
# min_tracking_confidence = ランドマークトラッキング成功判定の閾値
# static_image_mode = 静止画かどうか
hands = mp_hands.Hands(
max_num_hands=2,
min_detection_confidence=0.5,
min_tracking_confidence=0.5,
static_image_mode=False)
# カメラが有効の場合のみ処理する
while cap.isOpened():
# カメラから画像1枚取得
success, image_org = cap.read()
if not success:
print(“Ignoring empty camera frame.”)
continue
tick = cv2.getTickCount() # fps計算用
# フェイストラッキング
# 元画像から顔の特徴点のみ抽出
image_face = face_mesh_exec(image_org, face_mesh)
# ハンドトラッキング
# 元画像から手の特徴点のみ抽出
image_hand = hands_exec(image_org, hands)
# フェイストラッキング結果とハンドトラッキング結果の画像を合成
image = cv2.addWeighted(src1=image_face, alpha=0.5, src2=image_hand, beta=0.5, gamma=0)
# image = cv2.flip(image, 1) # ミラー反転する
# fps計算し、画像内に埋め込む
fps = cv2.getTickFrequency() / (cv2.getTickCount() – tick)
cv2.putText(
image,
“FPS: ” + str(int(fps)),
(image.shape[1] – 150, 40), # 画面右上あたり
cv2.FONT_HERSHEY_PLAIN,
2, # 文字の大きさ
(0, 255, 0), # 文字色(緑)
)
# PC画面に画像表示
cv2.imshow(‘MediaPipe Face Mesh’, image)
#終了判定 ESCで終了する
if cv2.waitKey(5) & 0xFF == 27:
break
cap.release()
実行結果
怖い😂
スクショのマウス操作のため片手は写ってませんが、両手ともリアルタイムに追従できています。
顔や手が完全に隠れてしまうとトラッキングできなくなりますが、一部が隠れる程度なら推論で追従できるようです。ウインクも可能。
すごい😳
顔1つ+手2つをトラッキングするときのFPSは30程度でした。
さほど高くはありませんが、30あればひとまず十分でしょう。60以上あるともっと良いですが、ここは別途検証します。
鼻と耳は写ってませんね。積極的に動かせる部分ではない(基本的に輪郭に連動する)からランドマークとして検出不要、ということなのでしょう。
トラッキング用の関数として、face_mesh_exec()とhands_exec()を作りました。
元画像を入力することで、ランドマーク情報を返すようにしています。
返すときは背景を黒にした上で、ランドマーク情報だけの画像として返しますが、関数内の以下処理をコメントアウトすれば、元画像に重ねてランドマークを描画することも可能です。
# 背景を黒にして、ランドマーク画像だけ残す
img_h, img_w, _ = img.shape
blank = np.zeros((img_h, img_w, 3))
img = blank
face_mesh_exec関数内の以下部分は、コメントアウトを解除することで顔の輪郭内側をメッシュ表示します。顔の造形がよりはっきりします。鼻も映るようになります。
# 顔の中をメッシュ表示する場合、このコメントアウトを解除する
# mp_drawing.draw_landmarks(
# image=img,
# landmark_list=face_landmarks,
# connections=mp_face_mesh.FACEMESH_TESSELATION,
# landmark_drawing_spec=None,
# connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_tesselation_style())
サンプルコードを実行したとき、左右の動く向きに違和感があるかもしれません。
これは以下部分のコメントアウトを解除すれば解決します。
# image = cv2.flip(image, 1) # ミラー反転する
ミラー反転すると、自分が鏡を見ているような見た目になります。
この場合、自分の体の左右方向は気になりませんが、文字を写すと反転して読めなくなります。
一方、ミラー反転しない場合、右手を動かすと画面左側の手が動くため、やりづらさを感じるかもしれません。自分と相手が対面で話しているときの、相手からの目線です。
ただし写した文字は正しく読めます。
カメラ映像は、cv2.VideoCapture(0)から取得しています。
引数に与えている0はカメラIDで、PCに接続した順に割り当てられます。
再起動のたびに変わる可能性があるので、使えなければ1や2に変更してみましょう。
FPSが上がらない
フェイストラッキングとハンドトラッキングを動かしていると、FPSは30前後になっていました。
フェイストラッキングだけのときは100FPS超えていたので、原因を探ってみます。
大きな要因として、トラッキング対象が増えるほど処理が重くなる、というのはあると思うので、パターンごとにFPSを計測しました。
結果はこの通り。
検出対象 | 最大FPS |
顔1つ | 130 |
手1つ | 60 |
手2つ | 42 |
顔1つ+手1つ | 38 |
顔1つ+手2つ | 30 |
フェイストラッキングに比べて、ハンドトラッキングの負荷が非常に高いことがわかりました。
Python自体がさほど速くない、というのもあるかもしれませんが、サンプルコードはあまり最適化していない状態なので、高速化する方法が何かあるかもしれません。
さいごに
Mediapipeを試してみましたが、思ったよりしっかり追従してくれて、技術の進歩はすごいなと思いました。動かしながら作っていて、とても楽しかったです😄
今後、ここからさらにどんなことができるか、アバターにどう適用すれば良いかなど、検証を進めていきます。
アバターこれで良くない?
ダメですか😂
それでは、今回はここまで。
ありがとうございました😊
コメント