ディープラーニングでスクワットのフォームをチェックする

**この記事で使用した動画はだいぶ前に録画したものであり、現在はジムでの筋トレを自粛しております。**

# はじめに
　「筋トレxディープーラーニング」第二弾です。第一弾はこちら（[ディープラーニングで肉体変化のタイムラプスを劇的に見やすくした](https://qiita.com/shuheilocale/items/f0e60416da3bdde38cfc)）。
　トレーニー（筋トレを愛している人）の多くが習慣化している「フォームチェック」。トレーニング中にスマホを固定して自撮りし、後で見返すことで自らのフォームの癖を知ることができ、以降のトレーニングの質を高めることができますよね！
　この記事は、筋トレの中でも「スクワット（筋トレの王様）」に種目を絞り、ディープラーニングを使ってのフォームをチェックした話を書いています。

## まずは結果から

![ezgif.com-optimize.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22648/631e2623-998a-bfda-6f01-d183540e1655.gif)
※掲載用に圧縮しています。

ソースコードはGoogle Colabのノートブックに公開しています。
https://colab.research.google.com/drive/18bFHGZ415T6emBoPHacrMyEckPGW9VCv
ご自身の動画を使って実行することもできますので是非試してみてください。

# 目次

* [1.姿勢推定](#1姿勢推定)
  * [1-1.動かしてみる](#1-1動かしてみる)
  * [1-2.ヒートマップ](#1-2ヒートマップ)
  * [1-3.姿勢推定](#1-3姿勢推定)
* [2.スクワットフォームチェックへの応用](#2スクワットフォームチェックへの応用)
  * [2-1.スクワットにおいて重要な要因](#2-1スクワットにおいて重要な要因)
  * [2-2.フォームチェックらしくする](#2-2フォームチェックらしくする)
* [まとめ](#まとめ)

# 1.姿勢推定
　姿勢推定とは、人体や動物などの姿勢を推定する技術です。例えば人体であれば、首やお尻や膝などを検出し、それぞれをつなげることで姿勢を表現できます。有名なものでは、[OpenPose](https://github.com/CMU-Perceptual-Computing-Lab/openpose)があります。

![https://github.com/CMU-Perceptual-Computing-Lab/openpose/blob/master/doc/media/dance_foot.gif?raw=true](https://github.com/CMU-Perceptual-Computing-Lab/openpose/blob/master/doc/media/dance_foot.gif?raw=true)
*openposeのgithubより引用*

## 1-1.動かしてみる
　今回参考にしたのは、「[つくりながら学ぶ！PyTorchによる発展ディープラーニング](https://www.amazon.co.jp/%E3%81%A4%E3%81%8F%E3%82%8A%E3%81%AA%E3%81%8C%E3%82%89%E5%AD%A6%E3%81%B6%EF%BC%81PyTorch%E3%81%AB%E3%82%88%E3%82%8B%E7%99%BA%E5%B1%95%E3%83%87%E3%82%A3%E3%83%BC%E3%83%97%E3%83%A9%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0-%E5%B0%8F%E5%B7%9D-%E9%9B%84%E5%A4%AA%E9%83%8E-ebook/dp/B07VPDVNKW/)」の[GitHubページ](https://github.com/YutaroOgawa/pytorch_advanced)になります。PyTorch x Google Colabで実装したかったので、上記の実装がとても役に立ちました。

　とりあえず、フリー素材を使って、画像一枚から姿勢推定をしてみます。ソースは概略版です。詳細は、Google Colabのノートブックを参照してください。

```python

def create_model(weight_path):
  """
  モデルを作成する。
  学習済みモデルと、OpenPoseNetでネットワークの層の名前が違う
  (例えば、module.model0.0.weight　と　model0.model.0.weight)ので、
  対応させてロードする
  """

  # モデルの定義
  model = OpenPoseNet()

  # 学習済みパラメータをロードする
  net_weights = torch.load(
      weights_path, map_location={'cuda:0': 'cpu'})
  keys = list(net_weights.keys())

  weights_load = {}

  # ロードした内容を、OpenPoseNetの
  # パラメータ名model.state_dict().keys()にコピーする
  for i in range(len(keys)):
    weights_load[list(model.state_dict().keys())[i]
                 ] = net_weights[list(keys)[i]]

  # コピーした内容をモデルに与える
  state = model.state_dict()
  state.update(weights_load)
  model.load_state_dict(state)
  return model

model = create_model(weights_path)
model.to(device)

model.eval()
with torch.no_grad():
  predicted_outputs, _ = model(img.to(device))
  pafs = predicted_outputs[0][0].cpu().detach().numpy().transpose(1, 2, 0)
  heatmaps = predicted_outputs[1][0].cpu().detach().numpy().transpose(1, 2, 0)

pafs = cv2.resize(
    pafs, (test_img.shape[1], test_img.shape[0]), interpolation=cv2.INTER_CUBIC)
heatmaps = cv2.resize(
    heatmaps, (test_img.shape[1], test_img.shape[0]), interpolation=cv2.INTER_CUBIC)

_, result_img, _, _ = decode_pose(test_img, heatmaps, pafs)

cv2_imshow(result_img)
```

　結果は以下の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22648/0cbec73b-4420-db81-5168-0d4c09d5b8b0.png)
*[Freepic.diller - jp.freepik.com によって作成された写真](https://jp.freepik.com/free-photo/full-length-of-young-concentrated-sportsman-doing-squats-during-workout-outdoors_2593727.htm#page=1&query=squat&position=17)を利用*

　なかなか良い感じです。これを動画の各フレームに実施すれば、フォームのチェックができそうです。ただし、このジョイント部全て欲しいわけでなく、必要最低限の部位だけで大丈夫です。今回は、

* 首
* お尻（右）
* 膝（右）
* くるぶし（右）

だけに絞ることにします。

## 1-2.ヒートマップ
　利用するモデルでは、ヒートマップを出力し、そこから各部位を抽出することで姿勢推定を行います。まずは、対象部位のヒートマップを描画してみます。

```python
# 1:首、8:お尻（右）、9:膝（右）、10:くるぶし（右）
necessary_parts=[1,8,9,10] 
fig, ax = plt.subplots(2, 2, figsize=(16, 10))

for i, part in enumerate(necessary_parts):
    heat_map = heatmaps[:, :, part]
    heat_map = np.uint8(cm.jet(heat_map)*255)
    heat_map = cv2.cvtColor(heat_map, cv2.COLOR_RGBA2RGB)
    blend_img = cv2.addWeighted(test_img, 0.5, heat_map, 0.5, 0)
    ax[int(i/2), i%2].imshow(blend_img)

plt.show()

```
　結果は以下の通りです。ちゃんと特定部位を抜き出すことができています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22648/c8b13052-6b71-0a94-2b00-52bfee9c6a0a.png)

## 1-3.姿勢推定

　出力した特定部位のヒートマップから関節の位置を特定します。今回は一枚に１人しか写らない前提で、各部位１点のみ残す処理を施し、特定した部位間を繋ぎます。

```python
def find_joint_coords(heatmaps, necessary_parts, param = {'thre1': 0.1, 'thre2': 0.05, 'thre3': 0.5}):
  """
  ジョイントの座標を検出する
  """
  
  joints = []
  for part in necessary_parts:
      heat_map = heatmaps[:, :, part]
      peaks = find_peaks(param, heat_map)
      if len(peaks) == 0:
        joints.append(img, [np.nan, np.nan], [np.nan, np.nan])
      # ピークが2つ以上の場合は、最も強い場所だけ残す
      if peaks.shape[0]>1:
        max_peak = None
        for peak in peaks: 
          val = heat_map[peak[1], peak[0]]
          if max_peak is None or heat_map[max_peak[1], max_peak[0]] < val:
            max_peak = peak
      else:
        max_peak = peaks[0]
      joints.append(max_peak)

  return joints

img = test_img.copy()
joints = find_joint_coords(heatmaps, necessary_parts)
for i in range(len(joints)-1):
  img = cv2.line(img,tuple(joints[i].astype(int)),tuple(joints[i+1].astype(int)),(255,0,0),3)

cv2_imshow(img)
```

　結果は以下の通りです。いい感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22648/309b57c5-a240-273e-7445-1607566c7a3b.png)


# 2.スクワットフォームチェックへの応用

　使えそうなことがわかったので、次に自分のスクワット動画に適用してみます。

## 2-1.スクワットにおいて重要な要因

　脛部と大腿部と体幹を形成する骨格は人によって異なり、そのため人によって最適なフォームも自然と異なります。下図でイメージしやすくなると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22648/eab56609-f6c0-1b06-57ba-62c51616cb29.png)

つまり、その人にあったフォームというのは、柔軟性や筋力バランスよりも骨格プロポーションによる要因が大きいというとです。よって、この骨格プロポーションを自身で把握することができれば、怪我の防止にもつながり、安全にスクワットの重量を伸ばすことができると思います。詳しくは、下記のYouTubeやその他にいくつもある素晴らしいウェブページをご参照ください。

[![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/Av3LO2GwpAk/0.jpg)](http://www.youtube.com/watch?v=Av3LO2GwpAk)
[Squats Part 1: Fold-Ability and Proportions](https://www.youtube.com/watch?v=Av3LO2GwpAk)

## 2-2.フォームチェックらしくする

　関節の座標が取れるので、関節の角度とモーメントアームを求めて、時系列に並べてみます。関節の角度は、関節を挟む２つのベクトルの角度とします。モーメントアームは、理想的な重心（ミッドフット）を擬似的に求め、その垂直方向の直線に対する関節を通る垂線の距離とします。これを各フレームで算出し、つなげば完成です。
　ついにで、各フレームで産出した角度とモーメントアームを、matplotlibでプロットして、画面上部に貼り付け、なんかよさげに解析している風なグラフも追加しておきます。

**※ソースコードは長くなるので割愛します。ノートブックをご参照ください。**

完成例は下記の通りです（適当にフレームを選んでいます）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22648/9747f06a-933d-3eb8-27d3-02d427085ffc.png)

# まとめ

　ディープラーニングを用いた姿勢推定を応用し、スクワットのフォームチェックを行いました。なかなか精度良く関節が検出できたと思います。この結果から、自分と近い骨格の選手を見つけたり、その選手とフォームを比較することで、さらにスクワットの質を向上できるかもしれません。

　課題としては、下記があります。

* ミッドフット（理想的な重心）がハードコーディング
* 処理が重い
    * 無駄に部位を検出している
    * Pointwise convolutionのような工夫をいれる
    * 学習もやり直す必要がある
* プレートをつけたときに検出できない
    * これは致命的な課題なのですが、投稿できないのが嫌で見て見ぬ振りをしていました、ごめんなさい
    * 対応としては、プレートを担いだ状態のデータを学習させる。や、バーの先端や顔を検出してそこから首を推定する、などが考えられます



　以上、自宅でできる筋トレは、体を動かすだけじゃない！楽しい筋トレハックライフを！