はじめに
こんにちは。はんぺんです。 テニスのスイングの検出・分類モデルを作ってみました。
モチベーションとしては、インテリジェントデバイス的な何かを作ってみたいというものです。
ちょうど魔法少女リリカルなのはのレイジングハートみたいなイメージです。
出展:レイジングハートとは (レイジングハートとは) [単語記事] - ニコニコ大百科
簡単にいうと、戦いの中で戦闘者をサポートしてくれるものです。
自分は魔法で戦うことはありませんが、趣味でテニスをする上で戦うことはあるので、テニスでこのようなインテリジェントデバイスを活用できたら面白いなーと思っています。
今回はそのための要素技術としてのテニスショット検出・分類という位置付けになります。

テニスにおけるインテリジェントデバイス活用のイメージ図
手法の決定
動作検出や分類の手法を簡単にリサーチしました。やはりDeep learningでネットワーク内でEnd to endで予測しているものが多いですね。
こちらは単一の画像または連続の画像からの2D / 3D姿勢推定を行い、ポーズと視覚情報を使用して、行動を認識するといった論文です。
学習が重そう(小並感)。
そんな中、個人的にこれが気になりました。
人間の行動をAutoEncoderとLightGBMで分類するという論文です。 具体的には、スマートフォンから取得できる加速度センサーやジャイロセンサーの値を取得して、AutoEncoderによって潜在変数を取得して、潜在変数を使ってLightGBMで人間の活動を分類するというものです。
まとめると以下になります。
前者:複数画像を入力に視覚情報と関節位置情報の特徴を用いて動作を推定するモデル
後者:センサー情報を入力にAutoEncoderで抽出した特徴を用いてLightGBMで動作を推定するモデル
実装がそこまで大変じゃなさそうなので、本記事では以下の手法を用いることにしました.
本記事:複数画像を入力に関節位置情報の特徴を用いてLightGBMで動作を推定するモデル
Pose Estimationの推論結果を入力にLightGBMで動作を推論するので、これをPoseGBMと名付けようと思います(適当)
姿勢推定とLightGBMの解説は以下を参照ください。
開発環境
主な開発環境と主な使用パッケージは以下になります。
Macbook Pro (2017)
Core i7 (2.9GHz)
MacOS Mojava(10.14.5)
Python (3.6.5)
OpenVINO (2019 R2)
pyvino(latest)
LIghtGBM(2.2.3)
個別要素のモデルで学習を行なったのは動作検出のLightGBMのみで、人の検出と関節位置推定といった画像周りのモデルはOpenVINOの学習済みのものを使用しました。
OpenVINOはIntel社が出している、機械学習向けのツールキットです。
サポートはIntelのCPUでのみですが、その分パフォーマンスを上げるために徹底的にチューニングされており、推論に特化しております。
OpenVINOに搭載されるモデルをPythonで呼び出しやすくしたpyvinoというパッケージを以前に開発したので、今回はそれに含まれる姿勢推定モデルを使用しています。
データ取得
動画撮影
オープンなデータセットでテニスの適当なものがなかったので、分析対象となる動画の撮影から取り掛かりました。 撮影した動画はフォアハンド、バックハンド、フォアスライス、バックスライスに分けた4つで、壁打ちでの動画になります。 ちなみに後のアノテーション作業を考えると、動作ごとに動画を分けておいたほうが圧倒的に効率が良いです。 動画はFullHD画質で60fpsの動画で、手持ちのiPhone 8plusで撮影しましたが、分析で利用した動画は、HD画質で30fpsに変換したものになります。
アノテーション
次にアノテーション作業です。 今回は動画の中の人にではなく、動画内に一人しか映らないと仮定して、フレームに対して動作ラベルをつけました。 動作ラベルはidle(スイングしていない状態), forehand, backhand, foreslice, backsliceの5種類です。 動画の読み込みとフレーム確認のためにaviutlを用いて、動作ラベルをつけるためにエクセルを使いました。

動作ラベルの付け方としては、動作を定義してその動作に当たるフレームに対してつけています。 今回はテニスのスイングが動作対象なので、ボールを打った瞬間を動作と定義して、ラベルを付けております。

こちらの画像の真ん中の画像がインパクトの瞬間で、インパクトの瞬間のみの1フレームにラベルを付与しました。
4本の動画で1時間半くらいの作業時間くらいだった気がします。 炭酸飲んで音楽聴きながら一気にやることを強くお勧めします。
アノテーションでつけた動作ラベルは1フレームのみですが、実際のスイングは一回あたり約50フレーム(約0.8秒)であるので、後に機械的に前後50フレームにも動作ラベルをつけます。
関節位置のデータ
動画から人の関節位置を取得します。対象の関節位置は両耳、両目、鼻、首、両肩、両肘、両手首、両腰?、両膝、両足首と18種類になり、x軸とy軸の両方あるので合計36種類の値を取得します。

実際にスイングしている範囲が、関節位置から読み取れそうかを確認するために、取得した関節位置を時系列データとして可視化しました。
今回はスイングの検出・分類に使えそうな両肩、両肘、両手首、のX, Y軸の座標の合計12種類の値を用いてモデルを作成します。 姿勢推定で検出した値はデフォルトだとフレーム内のピクセルの位置として結果が返ってきますが、それだと検出対象者の位置の変化への対応ができないので、Bbox内で座標の値を0~1に正規化した値を用いています。
X軸が動画のフレーム数で、Y軸が正規化された座標の値になります。
フォアハンド
赤くなっている範囲がフォアハンドのスイングをしているところです。 右肩のX軸(水色)Y軸(黄色)、右肘のX軸(緑色)Y軸(赤色)、右手首のX軸(紫色)Y軸(茶色)、
右肩のY軸(黄色)以外はフォアハンドのスイングをしている時にピークが見られており、検出のための特徴量として機能しそうな予感がします。
バックハンド、フォアスライス、バックスライスも同様に可視化しました。 バックスライスはカメラから若干遠いところでの撮影となったため、他と比べて関節位置の取得精度が落ちています。
バックハンド
フォアスライス
バックスライス
モデリング
学習用データ
学習に用いたフレーム数とスイング回数は以下になります。
| action | frame count | swing count |
|---|---|---|
| idle | 11384 | - |
| forehand | 1482 | 57 |
| backhand | 1430 | 55 |
| foreslice | 1118 | 43 |
| backslice | 988 | 38 |
データ加工
前述した通り、動作検出のモデルはLightGBMを用いています。 モデリングのための数値的なデータ加工は行なっておらず、予測対象のフレームから15フレーム(0.5秒)前までの関節位置情報を横持ちにするという加工のみを施しています。
データ分割
4種類の動画を、前半8割を学習、後半2割をテストデータに割り当てました。 今回扱うデータは時系列性があるため、リークしないようにするためにシャッフルはしていません。

学習
15フレーム分の関節位置情報を横持ちしたデータでLightGBMを用いて学習します。 学習データ内でクロスバリデーションを行い、得られたbest epochを以って学習データ全量で学習し直しています。
評価
評価用データ
評価に用いたテストデータのフレーム数とスイング回数は以下になります。
| action | frame count | swing count |
|---|---|---|
| idle | 2850 | - |
| forehand | 260 | 10 |
| backhand | 364 | 14 |
| foreslice | 234 | 9 |
| backslice | 348 | 14 |
Feature Importance

個人的に評価してしてて面白かったポイントとして、Feature importanceの上位に現在のフレーム(末尾0)と予測に使った一番古いフレーム(末尾14)が上位に来ていたことです。 確かにスイングが何であるかの判断を人間がするときも、スイングの最初と最後の画像さえあれば判断はできそうという直感とあっているため、個人的に納得感のある結果になっています。
Confusion matrix
| idle_pred | forehand_pred | backhand_pred | foreslice_pred | backslice_pred | |
|---|---|---|---|---|---|
| idle_gt | 2684 | 14 | 40 | 13 | 99 |
| forehand_gt | 55 | 188 | 0 | 17 | 0 |
| backhand_gt | 43 | 0 | 660 | 0 | 0 |
| foreslice_gt | 87 | 12 | 0 | 135 | 0 |
| backslice_gt | 52 | 0 | 1 | 0 | 295 |
テストデータの推論によるAccuracyは 89.32 %です。
Confusion matrixの結果は2点気になるところがあります。
1点目はフォアハンドとフォアスライスの誤検出です。 自分の場合はフォアハンドとフォアスライスは片手であるため、類似性が高く誤検出が増えたと考えています。
フォアスライスの方が右手首が高い位置にあり、フォアハンドの方が低い位置にあると思いますが、全体のスイングとしては4種類の中ではもっとも近いです。
2点目は見逃し率の多さ(idleとしての誤検出)です。 一番左の2~5行目はidleと予測したが実際はスイングしていたというフレームの数を表しています。アノテーションの簡略化のためスイングの長さは51フレームに固定していますが、スイングによってはそのアノテーション結果が妥当ではないこともあるので、ある程度はアノテーションの質の低さによるものと考えています。
動作の誤検出
記事冒頭にも載せましたが、テストデータに対して推論を行い、動画を生成しました。
動画を見て確認した限りでは、フォームが崩れた時に誤検出をしている印象です。 特にフォアスライスをフォアハンドに誤検出しているケースはそれが顕著に見られます。
また、最後のバックスライスはテイクバックの時に1フレームだけバックハンドとして検出してしまっています。
高度化検討
トラッキングの追加
今回の分析では問題の簡略化のために、動画内に人が一人しか写っていない前提で分析を行ないました。 しかし、実際の動画ではそういったケースの方が稀であり、動画のフレーム間で同一人物や同一物体の紐付けを行う必要があります。 紐付けには一般的に、対象物体のBboxの位置や大きさ、速度、特徴量などが用いられており、次はトラッキングのロジックも組み込んだ上での分析・モデリングを行おうと思います。
姿勢の検出精度の高いモデルの使用
今回用いた姿勢推定のモデルはOpenVINOに搭載されている、バックボーンにMobileNet v2を用いたもので、推論時間は早いものの、関節位置の取得精度は決して高いとは言えません。 特に動画をみても分かるように検出の解像度があまり高くないです。 Publicで精度を担保しつつ推論時間の早いモデルを探して、試してみたいと考えています。
特徴量の再検討
今回は両肩、両肘、両手首、のX, Y軸の座標の合計12種類の値を直前15フレーム分用いていますが、特徴量を追加することでスイングの分類精度を向上や、見逃しの低減ができると考えています。 例えば、腰や足の位置情報や、各位置の速度情報、肩、膝、手首が成す角度情報などもモデルにおいて重要な情報ではないかと考えています。
予測モデルへの転換
今回は現在のフレームのスイングを検出・分類するというモデリングでしたが、数フレーム先の予測への試みも考えています。
例えば、ショットを打つまでのフォームで、右か左のどちらに打つかの予測などです。
ちょうど半年ほど前にバレーボールの軌道予測についての記事が出ていましたが、イメージこんな感じです。
関節位置データのAugmentation
取得した36つの関節位置データのAugmentationを行なって学習データにすることで、動作検出・分類における表現力が向上するのではないかと考えています。 球にやっと追いついてスイングをする際などは、フォームは崩れてしまいますし、そもそも関節位置が検出できないこともあります。 そういったケースが多いと考えられるため、姿勢位置の回転(rotate)や一部姿勢位置をnp.nanに置き換える(cutout)といった、画像に用いられるAugmentationを、姿勢位置にも適用できるのではないかといった考えです。 画像のAugmentationすると時間も容量も食うので、こちらの方が省力化できそうです。
さいごに
ずっとやりたかったテニスのスイング検出・分類のモデリングを、なんとか形にできました。 やはり対象が自分の興味のあるものだと分析もモデリングも結構テンション上がりますね。
ただ、もっと手法の検討のためのリサーチしてから実装すればよかったなーと思ってます。
テニス×機械学習は今後も続けて行こうと思います。
最後まで読んでいただきありがとうございました。
参考
非常に雑ですが本記事で使用したコード