pythonでハンドサインのジェスチャー認識

はじめに

以前、mediapipeを利用した手のハンドサインを登録、検出するという記事を書きました。今回はそれをもう少し拡張して、一連の動作(ジェスチャー)で検出しようという試みです!

こういったモーションキャプチャはさまざまな技術で行われていて、特に加速度センサ等を用いて測定するものなんかが多いイメージがあります。

また、認識のラグがネックになるはずなので、認識にかかる時間も計測していこうと思っています。

ではまず、イメージを掴んでもらうためにも今回作成したデモ動画を紹介します!

かなり高精度でジェスチャー認識をすることができました!!

実装

今回の実装ではジェスチャーを保存して、同様のジェスチャーをした際に、それを検出してお知らせするという形にしています。

検出について

動作というものは1フレームではなく、複数フレームから構成されているため、単純な比較では動作同士を比べることは難しいです。

そこで、DTWという手法を紹介します。

DTWとは時系列データ同士の距離・類似度を測る際に用いる手法です。波形の距離を求める手法としてはユークリッド距離(Euclidean Distance)や マンハッタン距離等(Manhattan distance)があるかと思います。

DTWは2つの時系列の各点の距離(誤差の絶対値)を総当たりで求め、全て求めた上で2つの時系列が最短となるパスを見つけます。時系列同士の長さや周期が違っても類似度を求めることができます。

S-Analysisより引用

要するにDTWを使うと、同じジェスチャーを同じ速度でおこなえなくてもその類似度を推定することができる、ということです。伝わりますかね・・・?

ここまでDTWについて説明しましたが、今回の実装では使用していません。。。

cos類似度でデモを作成してみたところ、満足のゆく精度だったのでそのまま紹介しちゃいます。また気が向いたら性能向上を試してみるかもしれません。

コード

mediapipeで取得した座標データを操作しやすいようにnumpy形式に変換します。

def landmark2np(hand_landmarks):
    li = []
    for j in (hand_landmarks.landmark):
        li.append([j.x, j.y, j.z])

    return np.array(li) - li[0]

X,Y,Zの3種類の座標、21点のランドマーク、時間軸の3次元配列同士における類似度を算出していきます。基本的にはcos類似度を取得するのですが、次元情報が一致していないといけないので、保存したジェスチャーの時間方向の数、フレーム数を同数にして類似度を計算しています。

if len(now_array) > len(saved_array):
    now_array.pop(0)
    score = manual_cos(saved_array, now_array)

cos類似度の計算

def manual_cos(A, B):
    dot = np.sum(np.array(A)*np.array(B), axis=-1)
    A_norm = np.linalg.norm(A, axis=-1)
    B_norm = np.linalg.norm(B, axis=-1)
    cos = dot / (A_norm*B_norm+1e-7)

    return cos[1:].mean()

単純にこの時点で結構な精度でジェスチャー認識をすることができています。

ただ、一点だけ欠点があります。。

ジェスチャーと言いながら正規化した座標系でしか比較していないため、動きを評価しているとは言い難いです。

要するに、指の動きの違いは検出できても、手全体の移動は識別できていません。

そこで、座標成分だけでなく、速度加速度成分を求めて、それぞれの類似度も計測していこうと思います。速度と加速度はそれぞれの偏差をとることで取得できます。

def calc_score(A,B):
    x_score = manual_cos(A, B)

    A_v = np.diff(np.array(A), axis=0)
    B_v = np.diff(np.array(B), axis=0)
    v_score = manual_cos(A_v, B_v)

    A_a = np.diff(A_v, axis=0)
    B_a = np.diff(B_v, axis=0)
    a_score = manual_cos(A_a, B_a)

    return [x_score, v_score, a_score]

実行結果がこちら!

保存したジェスチャーと同様に、手を下に下げた時だけを検出できるようになりました!

閾値などは動作見ながら感覚で設定してしまいましたが、同一の環境下で使う分には十分な性能な気がしています。

コード全文は以下です!!

import cv2
import mediapipe as mp
import time
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
import time


def landmark2np(hand_landmarks):
    li = []
    for j in (hand_landmarks.landmark):
        li.append([j.x, j.y, j.z])

    return np.array(li) - li[0]


def calc_score(A,B):
    x_score = manual_cos(A, B)

    A_v = np.diff(np.array(A), axis=0)
    B_v = np.diff(np.array(B), axis=0)
    v_score = manual_cos(A_v, B_v)

    A_a = np.diff(A_v, axis=0)
    B_a = np.diff(B_v, axis=0)
    a_score = manual_cos(A_a, B_a)

    print(round(x_score, 2), round(v_score, 2), round(a_score, 2))

    return [x_score, v_score, a_score]


def manual_cos(A, B):
    dot = np.sum(np.array(A)*np.array(B), axis=-1)
    A_norm = np.linalg.norm(A, axis=-1)
    B_norm = np.linalg.norm(B, axis=-1)
    cos = dot / (A_norm*B_norm+1e-7)

    return cos[1:].mean()

def main():
    cap = cv2.VideoCapture(0)
    mp_hands = mp.solutions.hands
    hands = mp_hands.Hands()
    mp_draw = mp.solutions.drawing_utils

    saved_array = None
    saved_landmark_array = None
    start = -100
    score = [0, 0, 0]
    now_array = []
    pose_time = 2
    counter = 0

    while True:
        _, img = cap.read()
        imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        results = hands.process(imgRGB)

        if results.multi_hand_landmarks:
            for hand_landmarks in results.multi_hand_landmarks:
                for i, lm in enumerate(hand_landmarks.landmark):
                    height, width, channel = img.shape
                    cx, cy = int(lm.x * width), int(lm.y * height)
                    cv2.putText(img, str(i+1), (cx+10, cy+10), cv2.FONT_HERSHEY_PLAIN, 4, (255, 255, 255), 5, cv2.LINE_AA)
                    cv2.circle(img, (cx, cy), 10, (255, 0, 255), cv2.FILLED)
                mp_draw.draw_landmarks(img, hand_landmarks, mp_hands.HAND_CONNECTIONS)

                if cv2.waitKey(1) & 0xFF == ord('s'):
                    saved_array = [landmark2np(hand_landmarks)]
                    saved_landmark_array = [hand_landmarks]
                    start = time.time()
                    score = [0, 0, 0]
                
                elif time.time()-start < pose_time:
                    saved_array.append(landmark2np(hand_landmarks))
                    saved_landmark_array.append(hand_landmarks)

                # cos類似度でチェック
                if saved_array is not None and time.time()-start > pose_time:
                    now_array.append(landmark2np(hand_landmarks))
                    
                    if len(now_array) > len(saved_array):
                        now_array.pop(0)
                        score = calc_score(saved_array, now_array)


        # 3s 表示
        if time.time() - start < pose_time:
            cv2.putText(img, 'now saving...', (50, 100), cv2.FONT_HERSHEY_SIMPLEX, 3.0, (255, 255, 255), thickness=2)

        elif score[0]>0.91 and score[1]>0.1 and score[2]>0.1:
            saved_array = None
            now_array = []
            cv2.putText(img, 'pose!', (50, 100), cv2.FONT_HERSHEY_SIMPLEX, 3.0, (255, 0, 255), thickness=2)
        
        # 左上で保存したポーズを再生する
        if saved_array is not None and time.time()-start > pose_time:
            # Canvas
            p = 440
            size = (440, 440, 3)
            plot = np.zeros(size, dtype=np.uint8)

            # 再生フレームを決定
            i = counter % len(saved_landmark_array)
            plot_hand_landmark = saved_landmark_array[i]

            for i, lm in enumerate(plot_hand_landmark.landmark):
                height, width, channel = plot.shape
                cx, cy = int(lm.x * width), int(lm.y * height)
                cv2.circle(img, (cx, cy), 10, (255, 0, 255), cv2.FILLED)
            mp_draw.draw_landmarks(plot, plot_hand_landmark, mp_hands.HAND_CONNECTIONS)

            img[0:height, 0:width] = plot


        cv2.imshow("Image", img)
        counter += 1

        if cv2.waitKey(1) & 0xFF == ord('q'):
            break


if __name__ == "__main__":
    main()

おわりに

前回やったハンドサイン検出から、ジェスチャー検出に拡張してみました。コード書いてみる前段階では、DTWの利用やなんかを考えていましたが、思ったより精度が高かったので良しとしました!

おすすめの記事