MediaPipe Gesture Recognizerを使って動画のハンドサインを判定してみます。
忍者の印を画像解析する研究などもあったかと思うのですが、人間の手で作れる形状というのには限界があり、その範囲の中でAIによって形状を判定するというのはよくあるニーズなのかなと考えています。
指差しに定評のある ゴー☆ジャス さまの映像を分析してみます。

Google Colabインストール
公式ガイドはこちら
https://ai.google.dev/edge/mediapipe/solutions/vision/gesture_recognizer?hl=ja
ビックリするほどドツボにハマったので、基本的なところを実施していきます。
他のソリューションと違って明示的にGPUを使うフラグはない?ようですね。
!pip install mediapipe
!wget -q https://storage.googleapis.com/mediapipe-models/gesture_recognizer/gesture_recognizer/float16/1/gesture_recognizer.task
忘れ去られたジェスチャー?
MediaPipeは2023年にそれまでの従来ソリューション形式から、API利用などを考慮した「新しいソリューション」に刷新されました。
考慮漏れなのか、ジェスチャーがどこにもありません。

古い公式の資料から流用します。
from matplotlib import pyplot as plt
import mediapipe as mp
from mediapipe.framework.formats import landmark_pb2
plt.rcParams.update({
'axes.spines.top': False,
'axes.spines.right': False,
'axes.spines.left': False,
'axes.spines.bottom': False,
'xtick.labelbottom': False,
'xtick.bottom': False,
'ytick.labelleft': False,
'ytick.left': False,
'xtick.labeltop': False,
'xtick.top': False,
'ytick.labelright': False,
'ytick.right': False
})
mp_hands = mp.solutions.hands
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
def display_one_image(image, title, subplot, titlesize=16):
"""Displays one image along with the predicted category name and score."""
plt.subplot(*subplot)
plt.imshow(image)
if len(title) > 0:
plt.title(title, fontsize=int(titlesize), color='black', fontdict={'verticalalignment':'center'}, pad=int(titlesize/1.5))
return (subplot[0], subplot[1], subplot[2]+1)
def display_batch_of_images_with_gestures_and_hand_landmarks(images, results):
"""Displays a batch of images with the gesture category and its score along with the hand landmarks."""
# Images and labels.
images = [image.numpy_view() for image in images]
gestures = [top_gesture for (top_gesture, _) in results]
multi_hand_landmarks_list = [multi_hand_landmarks for (_, multi_hand_landmarks) in results]
# Auto-squaring: this will drop data that does not fit into square or square-ish rectangle.
rows = int(math.sqrt(len(images)))
cols = len(images) // rows
# Size and spacing.
FIGSIZE = 13.0
SPACING = 0.1
subplot=(rows,cols, 1)
if rows < cols:
plt.figure(figsize=(FIGSIZE,FIGSIZE/cols*rows))
else:
plt.figure(figsize=(FIGSIZE/rows*cols,FIGSIZE))
# Display gestures and hand landmarks.
for i, (image, gestures) in enumerate(zip(images[:rows*cols], gestures[:rows*cols])):
title = f"{gestures.category_name} ({gestures.score:.2f})"
dynamic_titlesize = FIGSIZE*SPACING/max(rows,cols) * 40 + 3
annotated_image = image.copy()
for hand_landmarks in multi_hand_landmarks_list[i]:
hand_landmarks_proto = landmark_pb2.NormalizedLandmarkList()
hand_landmarks_proto.landmark.extend([
landmark_pb2.NormalizedLandmark(x=landmark.x, y=landmark.y, z=landmark.z) for landmark in hand_landmarks
])
mp_drawing.draw_landmarks(
annotated_image,
hand_landmarks_proto,
mp_hands.HAND_CONNECTIONS,
mp_drawing_styles.get_default_hand_landmarks_style(),
mp_drawing_styles.get_default_hand_connections_style())
subplot = display_one_image(annotated_image, title, subplot, titlesize=dynamic_titlesize)
# Layout.
plt.tight_layout()
plt.subplots_adjust(wspace=SPACING, hspace=SPACING)
plt.show()
import mediapipe as mp
import cv2
import math
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
base_options = python.BaseOptions(model_asset_path='gesture_recognizer.task')
options = vision.GestureRecognizerOptions(base_options=base_options)
recognizer = vision.GestureRecognizer.create_from_options(options)
# 画像の読み込み
image = mp.Image.create_from_file("/content/input.png")
img_np = image.numpy_view()
# リサイズ処理
DESIRED_HEIGHT = 480
DESIRED_WIDTH = 480
h, w = img_np.shape[:2]
if h < w:
new_width = DESIRED_WIDTH
new_height = math.floor(h / (w / DESIRED_WIDTH))
else:
new_height = DESIRED_HEIGHT
new_width = math.floor(w / (h / DESIRED_HEIGHT))
resized_np = cv2.resize(img_np, (new_width, new_height))
resized_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=resized_np)
# ジェスチャー
recognition_result = recognizer.recognize(resized_image)
print("Recognition result:", recognition_result)
if recognition_result.gestures and len(recognition_result.gestures[0]) > 0:
top_gesture = recognition_result.gestures[0][0]
else:
top_gesture = None
# 手のランドマーク確認
hand_landmarks = recognition_result.hand_landmarks
if hand_landmarks:
if not isinstance(hand_landmarks[0], (list, tuple)):
hand_landmarks = [hand_landmarks]
else:
hand_landmarks = []
result_tuple = (top_gesture, hand_landmarks)
images = [resized_image]
results = [result_tuple]
display_batch_of_images_with_gestures_and_hand_landmarks(images, results)
結果
None

検出できるジェスチャー
デフォルトでは、以下の6つのジェスチャーを判定可能とされています。
1 - 握りこぶし (Closed_Fist)
手を握って指を曲げた状態。たとえば、拳を作っている状態
2 - 開いた手のひら (Open_Palm)
手を広げ、全ての指が伸びた状態。手全体が見える状態
3 - 指を上に向けたジェスチャー (Pointing_Up)
人差し指を伸ばして上方向を指している状態
4 - 親指を下に向けたジェスチャー (Thumb_Down)
親指だけが下向きになっている状態
5 - 親指を上に向けたジェスチャー (Thumb_Up)
親指だけが上向きになっている状態
6 - ピースサイン (Victory)
人差し指と中指を立てたジェスチャー
7 - ラブ / I Love You (ILoveYou)
手の形で愛情を表現するジェスチャー。一般的には「I Love You」の意味を示します。
Pointingだけだとありがたいのですが、かなり上方向を向いている必要がありそうです。

90度ずつ回転させながら判定
多少強引ですが、90度ずつインプットを回転させながら判定させてみました。

最も高いスコアの Pointing_Up を採用するようにすれば良い?
コードはこちら
import cv2
import mediapipe as mp
import math
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
import matplotlib.pyplot as plt
mp_drawing = mp.solutions.drawing_utils
import mediapipe.framework.formats.landmark_pb2 as landmark_pb2
base_options = python.BaseOptions(model_asset_path='gesture_recognizer.task')
options = vision.GestureRecognizerOptions(base_options=base_options)
recognizer = vision.GestureRecognizer.create_from_options(options)
original_image = mp.Image.create_from_file("/content/宇宙海賊.jpg")
img_np = original_image.numpy_view()
rotation_angles = [0, 90, 180, 270]
images = []
results = []
for angle in rotation_angles:
print(f"\n--- {angle}度回転 ---")
# 角度に応じた回転処理
if angle == 0:
rotated_np = img_np
elif angle == 90:
rotated_np = cv2.rotate(img_np, cv2.ROTATE_90_CLOCKWISE)
elif angle == 180:
rotated_np = cv2.rotate(img_np, cv2.ROTATE_180)
elif angle == 270:
rotated_np = cv2.rotate(img_np, cv2.ROTATE_90_COUNTERCLOCKWISE)
rotated_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rotated_np)
# ジェスチャー認識
recognition_result = recognizer.recognize(rotated_image)
# トップジェスチャーの抽出(存在しなければ None)
if recognition_result.gestures and len(recognition_result.gestures) > 0 and len(recognition_result.gestures[0]) > 0:
top_gesture = recognition_result.gestures[0][0]
else:
top_gesture = None
print("トップジェスチャー:", top_gesture)
hand_landmarks = recognition_result.hand_landmarks
if hand_landmarks:
if not isinstance(hand_landmarks[0], (list, tuple)):
hand_landmarks = [hand_landmarks]
else:
hand_landmarks = []
# 結果がある(ジェスチャーまたはランドマークが検出される)場合のみリストに追加
if top_gesture is not None or len(hand_landmarks) > 0:
images.append(rotated_image)
results.append((top_gesture, hand_landmarks))
else:
print(f"{angle}度回転: 結果なし。")
if images:
display_batch_of_images_with_gestures_and_hand_landmarks(images, results)
else:
print("どの回転角度でも結果が検出されませんでした。")
両手検出は至難の業?
「検出できる手の最大数は GestureRecognizer によって決まります」とあるので、オプションパラメータを2に設定して両手の検出にトライしたのですがかなりキビシめです。
options = vision.GestureRecognizerOptions(base_options=base_options, num_hands=2)
画像をぱくたそさまからお借りしました。
これで両手ブイサインではないという判定のようです。

今度は両手がブイサインであると検出できました。
角度とかが関係しているのでしょうか。

ここまで厳しいなら、YOLO系で判定するとかしたほうが良いかも?とすら思ってしまいました。
動画モードもあるのですが、回転をさせる必要があるとすると、結局は画像ベースの判定になってしまいますし。
I Love You のジェスチャー

サンシャイン池崎氏のポーズが I Love You だそうで…