骨格のランドマーク推定でダンスを比較する

## 概要

学生時代，研究の過程で骨格座標を使った．
社会人になってから画像処理をしなくなったので，忘れないうちに骨格座標の取得方法をメモしておこうと思う．
ただのMediaPipeに関するメモだと見返したときに飽き飽きするので，
ダンスを比較するようなプログラムも添えておくようにする．

言語はpythonで，Jupyter Notebookを利用する．
ライブラリはMediaPipeを主に利用する．

## 骨格座標を取得する方法について

### MediaPipe

https://developers.google.com/mediapipe/solutions

MediaPipeがgoogleが提供しているオープンソースライブラリ．
画像処理の基礎知識を要することなく，画像を入力すると手の座標を取得したりできる．
CPU/GPUで処理を選べるが，スマホで駆動するくらい軽量なのでスマホアプリにしても動作する．

今回は，`Pose landmark detection`を利用する．

https://developers.google.com/mediapipe/solutions/vision/pose_landmarker

### 座標の取得

画像を入力した際に得られるのは以下の座標群である．
なお各座標は奥行きを含んだ3次元の座標で取得できる．

以下：[公式ドキュメント](https://developers.google.com/mediapipe/solutions/vision/pose_landmarker)を参照

![index.png](https://developers.google.com/static/mediapipe/images/solutions/pose_landmarks_index.png)

>0 - nose
1 - left eye (inner)
2 - left eye
3 - left eye (outer)
4 - right eye (inner)
5 - right eye
6 - right eye (outer)
7 - left ear
8 - right ear
9 - mouth (left)
10 - mouth (right)
11 - left shoulder
12 - right shoulder
13 - left elbow
14 - right elbow
15 - left wrist
16 - right wrist
17 - left pinky
18 - right pinky
19 - left index
20 - right index
21 - left thumb
22 - right thumb
23 - left hip
24 - right hip
25 - left knee
26 - right knee
27 - left ankle
28 - right ankle
29 - left heel
30 - right heel
31 - left foot index
32 - right foot index

### 相対座標

今回座標を比較するために，「写真のうちどこに映っているか」よりも「頭の位置・体の位置に対して手がどこにあるか」が重要なので，上記で求めたランドマークのうちいずれかからの座標に変換する．

相対座標に変換するために以下の関数を用意した．
（ついでに扱いやすいようにランドマークの構造体をnumpy配列になるようにした）
```python:相対座標に変換

#入力：姿勢推定から得られたランドマークのデータ
#出力：12の点(右肩)からの相対座標(numpy配列)

def landmark2np(pose_landmarks):
    detected_point = 12
    li = []
    for j in (pose_landmarks.landmark):
            li.append([j.x, j.y, j.z])
    for k in li:
        if k[0] == 0 and k[1] == 0  and k[2]==0:
            print("No detected")
            li[k] = li[detected_point]

    return np.array(li) - li[detected_point]
```

# ダンスの比較

ダンスの類似度を比較する際，通常のユークリッド空間座標の距離(肩から肘までの距離など)で比較するとなると．
**体格差があるだけで，ダンスが異なる**という扱いになってしまう．

そのため，距離ではなく角度を類似度にすることが重要になる．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3600683/e473ed58-6954-7e87-b947-5b509352a129.png)

そこで利用するのが**コサイン類似度**である．

### コサイン類似度

コサイン類似度は，ベクトル同士の向きの類似度であり角度が近いほど1になり遠いほど-1になる．
角度が分からない場合は，各座標とベクトルの内積から求めることができる．

ベクトルAと比較する際，角度が近い(θ=0に近い)時に類似度は1になる．(角度は同じ)
逆にベクトルAとDを比較すると，角度が180°であるため，類似度は-1となる．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3600683/b17a06d7-e786-d4de-c4c9-008d365fe002.png)

コサイン距離は以下のように実装した．

```python:コサイン距離
#入力：比較する2つの座標ランドマーク群
#出力：各ベクトルごとに比較したコサイン平均類似度

def manual_cos(A, B):
    dot = np.sum(A*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-10)

    # 検出できない場合の処理
    for i in cos:
        count = 0
        if i == 0:
            print("cos deleted")
            np.delete(cos,count)
        count = count +1
    return cos.mean()
```

## 実際に比較する

以下のプログラムを作成した．

mp4を入力することで，各フレームごとで相似度を取る仕組み．
そのため，音で同期をとった動画を入力とする必要があることに注意．
score_pathに各フレームの類似度を出力することで，どのあたりのダンスにずれがあるかを確認できるようにする．

```python:ダンスの比較
import cv2
import mediapipe as mp
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
import statistics

mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
mp_pose = mp.solutions.pose
poses = mp_pose.Pose(
    static_image_mode=False,  #静止画かどうか
    # UPPER_BODY_ONLY=False,   #上半身のみかどうか
    # SMOOTH_LANDMARKS=True,   #ジッターを減らすかどうか
    min_detection_confidence=0.7, #検出成功の最低信頼値
    min_tracking_confidence=0.7 #追跡成功の最小信頼値
     ) 

target_vid =  cv2.VideoCapture("mv/target.mp4")
player_vid =  cv2.VideoCapture("mv/player.mp4") 
score_path = "score_fp.csv"
# 59.94fps 1920p1080
if target_vid.get(cv2.CAP_PROP_FPS) != player_vid.get(cv2.CAP_PROP_FPS):
    print("It is not same FPS.")
    print("target video's FPS : "+ str(target_vid.get(cv2.CAP_PROP_FPS)))
    print("player video's FPS : "+ str(player_vid.get(cv2.CAP_PROP_FPS)))

frames =  0
totalScore =  []

# try:
while True:
  target_success, target_img = target_vid.read()
  player_success, player_img = player_vid.read()
  if target_success == False:
    break
  if player_success == False:
    break
  targer_imgRGB = cv2.cvtColor(target_img, cv2.COLOR_BGR2RGB)
  player_imgRGB = cv2.cvtColor(player_img, cv2.COLOR_BGR2RGB)
  target_results = poses.process(targer_imgRGB)
  player_results = poses.process(player_imgRGB)

  if player_results.pose_landmarks:
      if target_results.pose_landmarks:
        target_mortion = landmark2np(target_results.pose_landmarks)
        player_mortion = landmark2np(player_results.pose_landmarks)
      
      score = manual_cos(target_mortion, player_mortion)
  totalScore.append(score)
  frames = frames + 1
  print("SCORE : " + str(score))  
endScore = statistics.mean(totalScore)
print("AVE SCORE : " + str(endScore))

print(score)
np.savetxt(score_path,totalScore,delimiter=",")
```

## ちょっとした課題：スコアを100点満点にしたい

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3600683/d0cc967b-63a1-76a2-5f47-08cb364dd9de.png)


実はこのコサイン距離の計算だと寸分違わぬ動きをしないと満点にはならないし，
適当に踊ったとしても40点くらいになる．

ある程度実用的にするためには，スコアの計算にひと工夫必要だと実感した．

## まとめ

今回の記事では,骨格座標についての取得し，ダンスを比較する方法を検討した．
ツールはJupyter NotebookとMediaPipeライブラリを利用した．
ライブラリを使用するだけじゃなく，相対座標・コサイン類似度の計算を行うため，
python初学者にとってはちょうどいい難易度だと思う．