AnyMotion APIを用いて姿勢推定を行う方法

# はじめに
人間の動きを可視化、定量化する手法として、近年姿勢推定技術が注目されています。
姿勢推定とは、カメラで撮影した画像や動画から人体の関節や目鼻の座標データを検出する技術であり、画像認識系のAI分野において研究、開発が進められています。
しかし、この技術を用いたシステムを１から作成する場合、モデルの作成や、環境の構築、運用などのハードルがあります。
AnyMotionというサービスを利用することで、API経由で簡単に姿勢推定を行うことができます。

# この記事を通してできること
下記動画（0:15〜）のお辞儀選手権を例に、姿勢推定を用いた動作の可視化/定量化と、お手本動画との比較の方法を説明致します。同様の流れで、他の動作についても可視化、定量化ができるようになります。

<iframe width="560" height="315" src="https://www.youtube.com/embed/E_CCm2YOFNc" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>


# 対象読者
- スポーツやヘルスケアなどの領域で動作の見える化を実施したい方
- 画像認識や姿勢推定に興味のある方
- APIを利用したサービス作成に興味のある方


# 利用方法
次の順番で説明します。

1. 利用登録
2. APIキーの取得
3. 動作解析
4. 結果を用いてフィードバック

# 利用登録
AnyMotionは最大2ヶ月間無料トライアルにて利用可能です。ここではトライアルプランでのご利用方法を記載します。
トライアルプランは自動更新がなく、お支払い情報の登録も不要のためお気軽にお試し頂けます。
<img width="1049" alt="スクリーンショット 2021-11-15 15.35.56.png (153.7 kB)" src="https://nttpc-datascience-esa.s3.ap-northeast-1.amazonaws.com/uploads/production/attachments/9658/2021/11/15/55803/6c0879a8-8ac9-4e0a-b543-411fb5f2e6fe.png">
参考：[AnyMotion公式ページ](https://dm.nttpc.co.jp/lp/anymotion)

## 申し込み方法
[お申し込みフォーム](https://dm.nttpc.co.jp/form/anymotion_trial.html)からお申し込み下さい。3営業日以内に担当者からアカウント発行メールが送信されます。
![スクリーンショット 2021-12-09 13.02.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2340820/4281a2e1-d0da-81aa-e0fc-388d380b43ab.png)

# APIキーの取得
AnyMotion APIを利用するには、[ポータルサイト](https://portal.anymotion.jp/#/)にてAPIキーを取得します。
申し込み時に設定のメールアドレスとパスワードを入力し、SIGN INボタンをクリックして下さい。
<img width="1159" alt="スクリーンショット 2021-11-15 17.31.21.png (103.8 kB)" src="https://nttpc-datascience-esa.s3.ap-northeast-1.amazonaws.com/uploads/production/attachments/9658/2021/11/15/55803/c1051a37-c6fd-4040-aa99-9f32b10ee5e9.png">


次のような画面が開きます。こちらでは、画像や動画の利用状況を確認できます。右上のメールアドレスから、API認証情報をクリックして下さい。
<img width="1155" alt="スクリーンショット 2021-11-15 16.29.54.png (213.8 kB)" src="https://nttpc-datascience-esa.s3.ap-northeast-1.amazonaws.com/uploads/production/attachments/9658/2021/11/15/55803/57e86602-557b-48eb-b15e-7ecd9392bcb2.png">

こちらに記載されているClient IDとClient Secretを控えて下さい。APIを利用する際に用います。
<img width="1160" alt="スクリーンショット 2021-11-15 17.21.01.png (103.9 kB)" src="https://nttpc-datascience-esa.s3.ap-northeast-1.amazonaws.com/uploads/production/attachments/9658/2021/11/15/55803/f8d18610-cfea-4004-b91b-4d47f271ffea.png">

# 動作解析
今回の解析は次の流れで行います。各項目の詳細は[APIドキュメント](https://docs.anymotion.jp/)をご確認下さい。

1. アクセストークンの発行
2. 動画/画像アップロード
3. キーポイント抽出
4. 描画
5. 比較
6. 比較描画
7. （参考） 解析

## 環境
AnyMotionはRest APIとして提供しており、APIリクエストの機能を持つ様々な環境から利用可能です。
また、[CLI](https://github.com/nttpc/anymotion-cli)や[python SDK](https://github.com/nttpc/anymotion-python-sdk)での利用も可能です。ここではpython SDKを用いた実装を行います。

次のコマンドにてSDKをインストールします。

```sh
pip install anymotion-sdk
```

使用したバージョンは次の通りです。

```sh
Python 3.9.7
anymotion-sdk 1.2.6
```

## 解析の実施
### アクセストークンの発行からキーポイント抽出まで
次のコードにて実施します。client_idとclient_secretには、ポータルサイトから取得したClient IDとClient Secretの値を記載して下さい。また、file_pathには利用したい動画のパスを記載して下さい。
動画の処理結果はmovie_id、キーポイントの処理結果はkeypoint_idのように処理結果はidで管理されます。

```python
import json
import os
import pandas as pd
import plotly.express as px

from anymotion_sdk import Client

client = Client(client_id="CLIENT_ID", client_secret="CLIENT_SECRET")
file_path = 'target.mp4'
movie_id = client.upload(file_path).movie_id
keypoint_id = client.extract_keypoint(movie_id=movie_id)
extraction_result = client.wait_for_extraction(keypoint_id)
print(extraction_result.json)
```

実行すると、フレーム毎に体の各部位の座標が出力されます。

```sh
{'id': 3483, 'image': None, 'movie': 1584, 'keypoint': [{'nose': [1016, 154], 'leftEye': [1029, 141], 'leftEar': [1080, 141], 'leftShoulder': [1118, 230], 'rightShoulder': [1092, 230], ...
```

### 描画
キーポイント取得後は、キーポイントの座標値を元に、元の動画中に描画を行うことが出来ます。上記のコードに続いて以下のコードを記載します。
描画では一部の点や線のみ描画することや、線の色を変えるなどルールを指定することができますが、ここではルールを指定しないデフォルトの描画（全ての点と線をグレーで表示）を行なっています。

```python
drawing_id = client.draw_keypoint(keypoint_id)
drawing_result = client.wait_for_drawing(drawing_id)
file_name = os.path.splitext(os.path.basename(file_path))[0]
output_file_path = f'{file_name}_drawing.mp4'
client.download(drawing_id=drawing_id, path=output_file_path, exist_ok=True)
```

取得した結果はこちらです。（Qiitaへのアップロードのため取得したmp4をgifに変換しています）
![target_drawing.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2340820/cf25943e-afda-b902-15b0-e41f5bd48ec8.gif)


### 比較
生徒動画でも同様にキーポイント抽出を行います。生徒動画とお手本動画のキーポイントを用いて比較を行います。

```python
source_file_path = 'source1.mp4'
source_file_name = os.path.splitext(os.path.basename(source_file_path))[0]
source_movie_id = client.upload(source_file_path).movie_id
source_keypoint_id = client.extract_keypoint(movie_id=source_movie_id)
extraction_result = client.wait_for_extraction(source_keypoint_id)

comparison_id = client.compare_keypoint(source_id=source_keypoint_id, target_id=keypoint_id)
comparison_result = client.wait_for_comparison(comparison_id)
print(comparison_result.json)
```

比較結果は次のように取得できます。フレーム毎に部位毎の差異の程度が距離と方向で表されます。

```sh
{"id": 617, "target": 3478, "source": 3481, "difference": [{"nose": {"distance": 0.07003811893479578, "direction": 270.10328999075534}, "leftEye": {"distance": 0.0681293388883316, "direction": 270.16504414555806}, "leftEar": {"distance": 0.11 ...
```


### 比較描画
比較結果は比較idを用いて描画することも可能です。以下では、距離の値0.1と0.2を閾値として描画しています。ルールを指定しない場合は、デフォルト値として閾値0.2を超えた場合に赤色で描画されます。

```python
drawing_rule = [
{
  "drawingType": "keypoint",
  "pattern": "all",
  "color": "grey",
  "comparisons": [
    {
      "threshold": 0.1,
      "color": "yellow"
    },
    {
      "threshold": 0.2,
      "color": "red"
    }
  ]
}
]

drawing_id = client.draw_keypoint(comparison_id=comparison_id, rule=drawing_rule)
client.wait_for_drawing(drawing_id)

output_file_path = f'comparison_{source_file_name}_{file_name}_drawing.mp4'
client.download(drawing_id=drawing_id, path=output_file_path, exist_ok=True)
```

例えば、お手本に近い動作を行なった場合、全ての点がグレーで表示され、お手本と異なる動作を行なった場合、黄色や赤で点が表示されます。
![comparison1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2340820/c9740e15-1b33-f657-e484-7143c557ef72.gif) ![comparison.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2340820/e89a9eec-754b-d7c9-b50c-8d32bcbc8e97.gif)

### （参考）解析
今回はフィードバックに用いませんが、キーポイント取得後は、キーポイントの座標値を元に、様々な解析を行うことも出来ます。例えば、お辞儀の角度（左肩-左腰-左腰から上方向への垂線）を算出する場合次のようにルールを指定して解析を実施できます。これにより、より詳細な動作の数値化を行えます。

```python
analyze_angles_rule = [
  {
    'analysisType': 'vectorAngle',
    'points': ['leftShoulder', 'leftHip', {
      "pointType": 'verticalTop',
      'pointName': 'leftHip'
    }]
  }
]
analysis_id = client.analyze_keypoint(keypoint_id, rule=analyze_angles_rule)
analysis_result = client.wait_for_analysis(analysis_id)
print(analysis_result.json)
```

次のように、フレーム毎に指定した角度が得られます。

```sh
{'id': 603, 'keypoint': 3486, 'result': [{'analysisType': 'vectorAngle', 'description': 'leftShoulder, leftHip and verticalTop of leftHip angles', 'values': [6.53403849618031, 3.012787504183286, 3.100491449807808, 3.100491449807926, 2.862405226111906, ...
```

お手本動画のお辞儀の角度をフレーム毎にグラフ化すると次のようになります。

```python
angles = analysis_result.json['result'][0]['values']
frames = list(range(1,len(angles)+1))
df = pd.DataFrame(dict(frames=frames,angles=angles))
fig = px.line(df, x='frames', y='angles')
fig.write_image(f'{file_name}_graph.png')
```

<img width="700" alt="image.png (29.9 kB)" src="https://nttpc-datascience-esa.s3.ap-northeast-1.amazonaws.com/uploads/production/attachments/9658/2021/12/01/55803/006eda9e-1080-40e8-a923-a2b343a48ffd.png">


## 結果を用いてフィードバック
APIから得られた結果を元に、動作の類似度を点数化します。
点数化のルールは閾値によって全フレームにおいて座標が取得される全部位に点数を付けて平均値を算出しました。

```text
difference < 0.1 の場合、100点
0.1 <= difference < 0.2 の場合、50点
difference >= 0.2 の場合、0点
```

ソースコードは次のようになります。

```python
parts = ['nose', 'leftEye', 'rightEye', 'leftEar', 'rightEar', 'leftShoulder', 'rightShoulder', 'leftElbow', 'rightElbow', 'leftWrist', 'rightWrist', 'leftHip', 'rightHip', 'leftKnee', 'rightKnee', 'leftAnkle', 'rightAnkle']
differences = comparison_result.json['difference']
results = []
for difference in differences:
  for part in parts:
    try:
      results.append(difference[part]['distance'])
    except:
      pass

print(f'全データの個数：{len(results)}')
gray_results_num = len([result for result in results if result < 0.1])
yellow_results_num = len([result for result in results if (result >= 0.1) and (result < 0.2)])
red_results_num = len([result for result in results if result >= 0.2])
print(f'グレーで描画されるデータの個数：{gray_results_num}')
print(f'黄色で描画されるデータの個数：{yellow_results_num}')
print(f'赤色で描画されるデータの個数：{red_results_num}')
print(f'点数：{(gray_results_num*100+yellow_results_num*50+red_results_num*0)/len(results)}')
```

結果は次の通りです。それぞれの動作は比較描画と同じものを利用しています。

- お手本と近い動作を行った場合

```sh
全データの個数：2145
グレーで描画されるデータの個数：2101
黄色で描画されるデータの個数：44
赤色で描画されるデータの個数：0
点数：98.97435897435898
```

- お手本と少し異なる動作を行なった場合

```sh
全データの個数：1991
グレーで描画されるデータの個数：1032
黄色で描画されるデータの個数：870
赤色で描画されるデータの個数：89
点数：73.6815670517328
```

以上のように点数を算出できました。ここでは簡易的に点数を算出していますが、部位毎に結果を整理したり、どのフレームが閾値を超えているかなどより細かなつくり込みも可能です。

## ソースコード全体
こちらにソースコード全体を記載します。
`client_id`、`client_secret`、`target_file_path`、`source_file_path`の値を適宜変えて動作比較にご利用頂ければと思います。

```python
import json
import numpy as np
import os
import pandas as pd
import plotly.express as px

from anymotion_sdk import Client

# 解析
client = Client(client_id="CLIENT_ID", client_secret="CLIENT_SECRET")

target_file_path = 'movies/target.mp4'
target_movie_id = client.upload(target_file_path).movie_id
target_keypoint_id = client.extract_keypoint(movie_id=target_movie_id)
target_extraction_result = client.wait_for_extraction(target_keypoint_id)
print(target_extraction_result.json)

target_drawing_id = client.draw_keypoint(target_keypoint_id)
target_drawing_result = client.wait_for_drawing(target_drawing_id)
target_file_name = os.path.splitext(os.path.basename(target_file_path))[0]
target_output_file_path = f'output_movies/{target_file_name}_drawing.mp4'
client.download(drawing_id=target_drawing_id, path=target_output_file_path, exist_ok=True)

source_file_path = 'movies/source.mp4'
source_file_name = os.path.splitext(os.path.basename(source_file_path))[0]
source_movie_id = client.upload(source_file_path).movie_id
source_keypoint_id = client.extract_keypoint(movie_id=source_movie_id)
extraction_result = client.wait_for_extraction(source_keypoint_id)

comparison_id = client.compare_keypoint(source_id=source_keypoint_id, target_id=target_keypoint_id)
comparison_result = client.wait_for_comparison(comparison_id)
print(comparison_result.json)

drawing_rule = [
  {
    "drawingType": "keypoint",
    "pattern": "all",
    "color": "grey",
    "comparisons": [
      {
        "threshold": 0.1,
        "color": "yellow"
      },
      {
        "threshold": 0.2,
        "color": "red"
      }
    ]
  },
  {
    "drawingType": "stickPicture",
    "pattern": "all",
    "color": "grey",
  }
]

drawing_id = client.draw_keypoint(comparison_id=comparison_id, rule=drawing_rule)
client.wait_for_drawing(drawing_id)

output_file_path = f'output_movies/comparison_{source_file_name}_{target_file_name}_drawing_with_rule.mp4'
client.download(drawing_id=drawing_id, path=output_file_path, exist_ok=True)


# フィードバック
parts = ['nose', 'leftEye', 'rightEye', 'leftEar', 'rightEar', 'leftShoulder', 'rightShoulder', 'leftElbow', 'rightElbow', 'leftWrist', 'rightWrist', 'leftHip', 'rightHip', 'leftKnee', 'rightKnee', 'leftAnkle', 'rightAnkle']
differences = comparison_result.json['difference']
results = []
for difference in differences:
  for part in parts:
    try:
      results.append(difference[part]['distance'])
    except:
      pass

print(f'全データの個数：{len(results)}')
gray_results_num = len([result for result in results if result < 0.1])
yellow_results_num = len([result for result in results if (result >= 0.1) and (result < 0.2)])
red_results_num = len([result for result in results if result >= 0.2])
print(f'グレーで描画されるデータの個数：{gray_results_num}')
print(f'黄色で描画されるデータの個数：{yellow_results_num}')
print(f'赤色で描画されるデータの個数：{red_results_num}')
print(f'点数：{(gray_results_num*100+yellow_results_num*50+red_results_num*0)/len(results)}')
```


# おわりに
この記事ではAnyMotion APIを用いて、動作の可視化、定量化と、2つの動画間での比較、点数化を実施しました。
他の動作についても簡単に解析できますので、是非一度お試し下さい。