競走馬の骨格を動画から推定したい その1
2024年12月22日はGIレースの有馬記念でしたね。ふと競馬レースを見ていると、「競馬って過去データを出しているからモデルを構築すれば予想できるんじゃない?」と誰もが思いつくようなことが頭をよぎり気になって調べてしまいました。
まぁ、案の定ちょっと検索しただけでも競走馬予測チャレンジャーはごろごろいるわけで、そりゃそうだよなと思ったんですが、多くの方々はnetkeiba.comにでているような情報を使ってモデルを組んでいるんですよね。でも、競走馬の勝ち負けってやっぱり当日の体調に大きく左右されるんじゃないかって個人的に思うわけです。
つまり、いくら過去の戦績がよくても出走日にあまりいい走りをしないのでは、やっぱり勝てないと思うんですよ。
だからむしろ当日のパドックでの動きや毛並みから判断したほうが勝利予測しやすいんじゃないか?と思ったんですよ。
それでそこも気になってちょっと調べてみたんですが、こっちはあんまりやっている人が少ないんですよね。なので、あまり個人的にも挑戦してこなかった分野でもあるので競走馬の歩きからそういったレースの勝ち負けを判定するようなモデルづくりを勉強がてらやってみました。
競走馬の走りを骨格や姿勢から判断する
ただ、当日の歩き方や毛並みから当日走れそうかを判断するといっても、個人的にそこまで競馬に詳しくないのでよくわかりません。そこで知り合いに聞いてみると「やっぱり歩き方にでる」という反応だったので、とりあえず歩き方をデータで表現することから始めようと思います。
このあたりも知らなったのですが、姿勢推定とか骨格推定とかがあるようですね。主には人間の姿勢や骨格の推定をスポーツのパフォーマンス向上につなげることが目的のようです。ただ別に人間にしか適用できないわけではなく、他の動物にも適用できそうなので、いくつかめぼしいOSSのライブラリを調査してみました。
1. OpenPose
概要: 人間の姿勢を推定できるライブラリ。
特徴: マルチスケルトン検出をサポートし、カスタムモデルの訓練も可能。
方法: 基本的に人間用なので馬の骨格を表すために、カスタムトレーニングデータを用意する必要があります。
リポジトリ: https://github.com/CMU-Perceptual-Computing-Lab/openpose
論文:Zhe Cao , Tomas Simon,Shih-En Wei, Yaser Sheikh, The Robotics Institute, Carnegie Mellon University (2017), ”Realtime Multi-Person 2D pose estimation using Part Affinity Fields”
2. DeepLabCut
概要: 動物の姿勢推定に特化したライブラリ。Tensorflowがベースになっているがアルファ版としてPytorchベースもリリースされている
特徴: ラベル付きデータを使用して動物(特に馬を含む)向けにカスタムモデルをトレーニング可能。
方法:
馬の骨格に対応するポイントをラベル付けしたデータセットを準備。
モデルをトレーニング。
推論で馬の骨格や姿勢を推定。
論文:Alexander Mathis, Pranav Mamidanna, Taiga Abe, Kevin M. Cury, Venkatesh N. Murthy, Mackenzie W. Mathis, Matthias Bethge, Cornell University(2018), Markerless tracking of user-defined features with deep learning
3. MediaPipe
概要: リアルタイム姿勢推定を可能にするGoogleのライブラリ。
特徴: 人間の姿勢に特化しているが、カスタマイズ次第で動物にも対応可能。
方法: カスタムモデルを訓練することで馬の骨格推定を可能に。
論文:Camillo Lugaresi, Jiuqiang Tang, …, Matthias Grundmann, google (2019), MediaPipe: A Framework for Building Perception Pipelines
4.MMpose
概要: 骨格推定ライブラリで、特にpytorchをベースにした姿勢推定が特徴。
特徴: 人間以外にも多様な姿勢推定モデルをサポートしており、2Dおよび3Dの姿勢推定モデルを簡単に扱える。
方法: カスタムデータセットを使えば動物(馬など)にも適用可。
論文: Jingdong Wang, Ke Sun,… Bin Xiao,(2019),Deep High-Resolution Representation Learning for Visual Recognition
まずはチュートリアルで試してみる
いくつかライブラリがあるですが、試してみないことにはどれがいいのか選べないのでチュートリアルを行っていきます。ただし4つもいきなりできないので、なんとなく動物での利用が簡単そうなDeepLabCutとMMposeから試していこうと思います。
DeepLabCut
公式ドキュメントによると実行環境として以下が求められます(Pytorch版もアルファリリースされいますが、今回はTensorflow版を利用します)。
Python3.10以上
Tensorflow2.10以下
いくつかインストール方法が用意されています。
pipインストール
condaインストール
dockerインストール
pipでサクッとインストールしてもいいのですが、動かないのも嫌なので、今回はDockerインストールを行います。Dockerインストールの場合以下のイメージが配布されています。
今回はGPUも利用していきたいので次のコマンドから実行します。
まずは公式Dockerイメージをローカルにダウンロードします。
docker pull deeplabcut/deeplabcut:2.3.5-base-cuda11.7.1-cudnn8-runtime-ubuntu20.04-latest
次にダウンロードしたDockerイメージを使ってコンテナ環境を立ち上げます。
docker run --gpus all -it --rm -p 8000:8000 deeplabcut/deeplabcut:2.3.5-base-cuda11.7.1-cudnn8-runtime-ubuntu20.04-latest bash
さて公式ドキュメントによるとdeeplabcut内のDemoコードを動かすのにGithubのレポジトリをとってくる必要があるようです。Docker内にGitを入れgitコマンドが使えるようにします。
apt-get update && apt-get install -y git && apt-get clean
適当なパスに移動してGit cloneでレポジトリをDownloadしてきます。
cd usr/src
git clone -s https://github.com/DeepLabCut/DeepLabCut.git cloned-DLC-repo
ダウンロードしたDockerイメージにはJupyter notebookが入ってなかったのでとりあえず入れます。このあたりDockerコンテナでJupyter notebookを立ち上げる方法は「AWSを駆使したクラウドデータサイエンティストになるための教科書:Dockerコンテナ上でJupyter notebookを使ったデータ分析環境を構築する」で解説していますので、慣れていない人はこちらを参照してください。
pip install jupyter notebook --no-cache-dir
Jupyter notebookを立ち上げ、ホストマシン側のWebブラウザからアクセスします。
jupyter notebook --ip=0.0.0.0 --port=8000 --no-browser --allow-root
表示された127.0.0.1:8000のループバックアドレスからアクセスします。
無事アクセスできました。
次にDemoコードを見てみましょう。
Demoコードをは「cloned-DLS-repo > examples > JUPYTER」の中に5つほどあります。説明を読む限り、openfieldのものから試すのが良さそうなので「Demo_labeledexample_MouseReaching.ipynb」から試すことにします。
DeepLabCut Toolbox - Open-Field DEMO
このDemoでは以下の内容が含まれるようです。
Demoプロジェクトの読み込み
ネットワークの学習
ネットワークの評価
ビデオの分析
自動的にラベル付けされたビデオの作成
軌跡のプロット
異常値の特定
手動での異常値のアノテーション
データセットの結合と学習データの更新
ネットワークの学習
Demoプロジェクトの読み込み
Demoプロジェクトはconfig.yamlに仕様が記載されており、その仕様を指定してload_demo_dataすると読み込めるようになっています。
順にコードを実行すると、初っ端で「FileNotFoundError」でエラーがでますが、これは「openfield-Pranav-2018-10-30/config.yaml」のパスが現パスの配下にあるような指定をしているためなので、実際のパスを確認しながら、「os.path.join(os.getcwd(),'openfield-Pranav-2018-10-30/config.yaml')」を「'..','openfield-Pranav-2018-10-30/config.yaml')」に書き換えます。
無事Demoプロジェクトを読み込めたようです。
なお、このload_demo_dataを実行すると、openfield-Pranav-2018-10-30配下に以下2つのサブディレクトリが作成されます。
dlc-models
training-datasets
ちょっと、load_demo_data関数の中身から読み込まれているDemoデータが何なのかを確認しておきましょう。自分ではどのDemoデータを読み込むかについてパスは指定していません。おそらくconfig.yamlにDemoデータのパスが記載されていて、それを読み込んでいるんだろうと察しはつくのでconfig.yamlをみてみると以下の記述が発見できます。
# Project path (change when moving around)
project_path: /usr/src/cloned-DLC-repo/examples/openfield-Pranav-2018-10-30
# Annotation data set configuration (and individual video cropping parameters)
video_sets:
/usr/src/cloned-DLC-repo/examples/openfield-Pranav-2018-10-30/videos/m4s1.mp4:
crop: 0, 640, 0, 480
おそらくこのm4s1.mp4ファイルがDemoデータ何だろうと思います。こちらもJupyter notebookからダウンロードして、ホストマシンの動画再生アプリから再生して確認してみます(ちなみにWindowsのMedia playerで再生しようとするとエンコーディングの問題で再生できなかったので、別の動画再生ソフトを使用しました)。すると、次のようなマウスが歩く動画でした。公式のDocsにもこの画像はあったので、このマウスの動きの姿勢推定・骨格推定を行うのだろうと察しがつきます。
肝心のload_demo_data関数ですが、deeplabcut.load_demo_data()という記述なので、Deeplabcutのレポジトリのdeeplabcutディレクトリ直下にある.pyファイルに関数の定義があるはずです。すると__init__.pyの中に次のような記載があります。
from deeplabcut.create_project import (
create_new_project,
create_new_project_3d,
add_new_videos,
load_demo_data,
create_pretrained_project,
create_pretrained_human_project,
)
なるほど。deeplabcut.create_project.pyファイルの中で関数の一つとして定義されているようです。ちなみに、__init__.pyはこうしたライブラリをimport XXしたときに自動的に読み込まれるファイルです。そのため「import deeplabcut」したときに上の内容が自動的に読み込まれます。from XX import AAなので、これによってload_demo_data関数がcreate_project.load_demo_dataという形で記述しなくても読み込めるようになっているということになります。
実際の中身はレポジトリ内のcreate_projectディレクトリのdemo_data.pyに定義されていました。関数の中の全体は説明しませんが、次のような内容が含まれています。
引数として渡しているconfig.yamlのパス解決
transform_data関数の実行(Configファイルの設定値を更新)
create_training_dataset関数の実行(動画フレームとラベルを配列に変換し、TrainとTestデータセットを作成の上、Config.yamlの設定値を更新)
transform_data関数ではさらに別途auxiliaryfunctions.read_config(config)関数が呼び込まれています。名前から察するにauxiliaryfunctionsはヘルパー関数なのでしょう。レポジトリ内のutilsディレクトリに実際の関数定義は見つけられます。read_configはその名の通り引数であるyaml形式のconfigファイルのパス解決をしながら"project_path"を指定しています。"project_path"は上記ですでに確認していますが「project_path: /usr/src/cloned-DLC-repo/examples/openfield-Pranav-2018-10-30」というものでした。
さらにtransform_data関数はyamlの"video_sets"の値を読み込んでauxiliaryfunctions.write_config(config, cfg)でconfigファイルに何かしらの設定値の書き込みをしています。"video_sets"も上記で確認しましたが、「/usr/src/cloned-DLC-repo/examples/openfield-Pranav-2018-10-30/videos/m4s1.mp4:」なのでやはり先ほどのマウスの動画がDemoデータであるという考え方であっているようですね。
それを受けてcreate_training_dataset関数でtrainデータとtestデータを作成するというもののようです。
さらに次はcheck_labels関数が実行されていますが、こちらはラベルが正しく設定されているかをチェックしているもののようです。
Check_labels関数を実行することでopenfield-Pranav-2018-10-30/labeled-data配下に以下のサブディレクトリが作成されます。
m4s1_labeled
ネットワークの学習
次にネットワークの学習はtrain_network()関数でやはりyamlに記載された仕様を読み込むことで行うようです。しかし実際に実行してみると「FileNotFoundError」がでてしまいます。
FileNotFoundError: [Errno 2] No such file or directory: '../openfield-Pranav-2018-10-30/dlc-models/iteration-0/openfieldOct30-trainset95shuffle1/train/pose_cfg.yaml'
ただ、os.path.exists()関数でパスがないと怒られている上のパスを確認すると、実在はしているようです。
os.path.exists('../openfield-Pranav-2018-10-30/dlc-models/iteration-0/openfieldOct30-trainset95shuffle1/train/pose_cfg.yaml')
>>> True
おそらく、内部的にパスの解決でこけているのでパスの指定方法を修正してある必要がありそうです。確かに上記で「os.path.join(os.getcwd(),'openfield-Pranav-2018-10-30/config.yaml')」を「'..','openfield-Pranav-2018-10-30/config.yaml')」に書き換えてます。それでパスを認識はしていたのですが、おそらく相対パスが認められないのでしょう。
そこで'..'じゃなく、絶対パスで改めてload_demo_data()関数のパスを指定しなおします。
path_config_file = os.path.join('/usr/src/cloned-DLC-repo/examples','openfield-Pranav-2018-10-30/config.yaml')
deeplabcut.load_demo_data(path_config_file)
すると、deeplabcut.train_network(path_config_file, shuffle=1, displayiters=10, saveiters=100)もうまく動作しました。
しかし、これ、普通にネットワークの学習をやってるとメモリエラーになって学習できないです。色々調べていて、別途用意されているGoogle Colaboratoryのほうを実行してみて、maxiterationの引数をつけないと学習が終わらなくてメモリエラーになってしまうようです。
Google Colaboratoryのほうではmaxiters = 10000がついています。
その実行セルの下には次のような注意書きもされています。
ということで、iterationの上限を付けたら、実行できました。
deeplabcut.train_network(path_config_file, shuffle=1, displayiters=10, saveiters=100,maxiters = 1000)
openfield-Pranav-2018-10-30/dlc-modesl/openfieldOct30-trainset95shuffle/trainにはもともとpose_cfg.yamlしかありませんでしたが、この関数を実行することで配下にいくつかのファイルとlogサブディレクトリが生成されます。
ネットワークの評価
続いてネットワークの評価です。Trainデータに対しての評価が実行され、評価結果はevaluation-resultsのフォルダにcsvファイルで格納されるということなので、とりあえず実行して評価結果を確認してみます。
次のような結果が表示されています。評価結果に影響するパラメータをconfig.yamlで調整することができるようです。
以下がconfg.yamlの内容です。TrainingGractionやpcutoff、colormap, dotsizeなどが重要なパラメータのようですが、どんな項目なのかがわからないのでとりあえずほっておきます。
このステップが終わるとopenfield-Pranav-2018-10-30配下にevaluation-resultsというサブディレクトリが生成されます。
ビデオの分析
続いてビデオの分析の項目に移ります。説明を読む限り、この項目を実行することで学習済モデルを利用して推定した姿勢のモデルデータがh5ファイルとして手に入るようです。
さっそく実行していきます。しかし、またパスの指定がおかしいので直してから実行します。
# Creating video path:
import os
videofile_path = os.path.join('/usr/src/cloned-DLC-repo/examples','openfield-Pranav-2018-10-30/videos/m3v1mp4.mp4')
実行すると、以下のようにpickleファイルとh5ファイルが出来上がっているのでモデルデータが生成されたことが確認できます。
自動的にラベル付けされたビデオの作成
これを実行すると実際に推定したモデルのラベルデータをくっつけたビデオデータが生成されるようです。つまり上の項目では姿勢の推定をし、その推定結果をビデオに載せて可視化したものをここで生成できるというわけですね。結果はもともとのビデオが保存してあるフォルダに生成されるようです。実行すると、次のように新しいビデオファイルが生成されました。
ローカル環境にビデオをダウンロードして確認してみます。確かにラベル付けされたビデオになっています。
軌跡のプロット
続いて軌跡のプロットに移ります。この機能は身体全体の軌跡をプロットしてくれるもので、それぞれの身体パーツを個別の色で表現してくれると説明が書かれています。軌跡が何を意味するのかわかりませんが、一旦実行してみます。
実行すると結果がplot-posesディレクトリに生成されたというメッセージが表示されました。中身を確認すると以下の画像ファイルが確認できます。
それぞれのファイルは以下のような内容です。
これを見てだいたい何をしているのかわかってきました。先ほどのラベル付けされたビデオではマウスの右目が黄色、左目が水色、そしてしっぽの付け根が茶色で示されていました。ここの各プロットの色はそれに呼応していると考えられます。例えばtrajectory.pngはX position in pixelsを横軸に、Y position in pixelsを縦軸に各色の点が描かれていることから、これは2次平面で各マウスのパーツがどのように動いているのかを示していると察しがつきます。それをtrajectory、軌跡と呼んでいたのですね。
そしてplot.pngはY軸が2次元平面の座標を一次元で示しているもので、X軸はFrame Indexとあるので、おそらくこれはビデオの時間軸なのでしょう。つまりplot.pngは時間経過ごとにマウスの各パーツがどのように動いているのかを示していると考えられます。
plotlikehood.pngのY軸はおそらくその姿勢推定の尤もらしさ、つまり推定の確信度的な値なんだろうと考えらます。
hist.pngのDeltaXとDeltaYはちょっとわかりませんが、hist.pngの名前からヒストグラムで何かしらの出現頻度のカウントなのはわかります。
異常値の特定
これはTrainデータが不足している際に手動でデータを追加するステップのようです。ここでは誤ってついているラベルの動画のフレームを取り出すことができると説明があります。
実行してみると252の推定上の誤った(異常値)ラベルがあると表示されました。とりあえずサンプリングで20フレームを抽出するかと聞かれているので「はい」と回答して実行を続けます。
実行が完了すると、異常値ラベルがふられた20のフレーム(画像データ)がlabeled-dataディレクトリの保存されたと表示されました。このディレクトリを確認すると確かに20個の画像ファイルが生成されています。
手動での異常値のアノテーション
このステップでは先ほど抽出した20の異常値フレームに対して正しいラベルを手動でつける作業を行うようです。
とりあえず最初のセルを実行してみると、なんとrefine_labels関数なんてそんな関数がないと怒られます。説明を見る限り、この関数で異常値フレームに正しいラベルを手動でつけるはずですが、deeplabcutのレポジトリで中身を確認してみます。
__init__.pyを確認すると、refine_labels関数はgui.tabs.label_framesに定義されているようです。
しかし、実際にgui/tabs/label_frames.pyを確認するとrefine_labels関数が定義されていないようです。エラーの原因はこれなので、実際にrefine_labels関数を定義しているモジュールを特定して__init__.pyを書き換える必要がありそうです。が、どういうわけかレポジトリを探してもそれっぽい関数が見当たらないので、このステップはスキップします。。。
ネットワークの学習
上記でラベルを再定義したものを使って再度学習するステップですね。
おさらい
ここまでチュートリアルに倣ってdeeplabcutで姿勢推定を行ってみました。目次に沿って考えると次のような形でdeeplabcutは学習と予測ができる感じでした。
Demoプロジェクトの読み込み
学習データや学習パラメータの定義を行うネットワークの学習
モデルの学習を行うネットワークの評価
学習したモデルの評価するビデオの分析
モデルの生成を行う自動的にラベル付けされたビデオの作成
モデルを使って実際に姿勢推定を行う。軌跡のプロット
姿勢推定の結果を可視化する異常値の特定
誤っている予測結果を抽出する手動での異常値のアノテーション
誤っている予測結果を修正するデータセットの結合と学習データの更新
修正結果を踏まえた学習データを生成するネットワークの学習
新たな学習データで再学習を行う。
補足
いくつか気になったポイントを追加で調査してみました。
そもそも学習するネットワークとはどういうものなのか?
deeplabcut.train_network(path_config_file, shuffle=1, displayiters=10, saveiters=100,maxiters = 1000)の部分でネットワークの学習を行っていますが、ここのセルの説明によれば、/openfield-Pranav-2018-10-30/dlc-models/.../pose_cfg.yamlを定義ファイルとしているみたいで、その中身を確認すると、dataset: training-datasets/iteration-0/UnaugmentedDataSet_openfieldOct30/openfield_Pranav95shuffle1.matという記載があるので、このMATLAB形式のデータが実際の学習データであることがわかります。MATLAB形式のデータはOSSのライブラリを使えば、MATLABがなくても中身を表示することができます。参考程度ですが、以下のような感じです。
MATLAB v7.2(HDF5形式ではない)以前のファイル
from scipy.io import loadmat
# .matファイルを読み込む
data = loadmat('data.mat')
# データを確認
print(data.keys()) # 保存されている変数の一覧
print(data['variable_name']) # 特定の変数を取得
HDF5形式(MATLAB v7.3以降)の.matファイル
import h5py
# HDF5形式の.matファイルを開く
with h5py.File('data.mat', 'r') as file:
print(list(file.keys())) # データセットの一覧を取得
data = file['variable_name'][:] # データを取得
print(data)
この.matファイル自体は/usr/src/cloned-DLC-repo/examples/openfield-Pranav-2018-10-30/videos/m4s1.mp4のビデオファイルからtrain_network関数を実行した段階でおそらく自動的に複数のFrame(画像)とこのmatファイルなどに分割されて作成されます。
つまり学習データは分割されたこのビデオファイルということになります。
結局、ユーザーは何を用意して、どういう順序でどんなことをしなければいけないのか?
上記のチュートリアルに従ってやっていて、なんとなくの流れはつかめましたが、実際にでは自分が解析したい動画があったとして何をどう用意すればいいのかがいまいちつかめません。そこで、公式Docsを探していたら、standardDeepLabCut_UserGuideを見つけました。これに沿って考えれば、何をどう用意すればいいかつかめそうです。
今回はここまで