競走馬の骨格を動画から推定したい その2
前回までの記事で競走馬の骨格・姿勢推定を行う目的で4つほどOSSのライブラリをご紹介した中で、まずはDeeplabcutのチュートリアルをしていました。
その中で何となく何をしているのかは分かったのですが、じゃあ、自分が一から自分の分析したいデータで分析するにはどうすればいいのかはいまいちわかりませんでした。
そこで今回はまっさらなipynbからdeeplabcutを使って予測できるまでの流れをおっていきたいと思います。この流れは公式ドキュメントを参考にしています。
真っ新なipynbから新しいプロジェクトを作成
前回と同じDockerイメージを使ってやっていきます。まずは空のipynbファイルをexamples/JUPYTER配下に適当に作成します。
作成したファイルでとりあえず以下でライブラリを読み込みます。
import deeplabcut
公式ドキュメントに従い、新しいプロジェクトを次の関数で作成します。
deeplabcut.create_new_project('Name of the project', 'Name of the experimenter', ['Full path of video 1', 'Full path of video2', 'Full path of video3'], working_directory='Full path of the working directory', copy_videos=True/False, multianimal=True/False)
引数の説明をレポジトリの方で確認してみると以下のように説明されています。
これを見る限り、必須で渡さなければいけないのがproject, experimenter, videosです。あとの引数はデフォルト値が入っているので指定しなくても新規プロジェクトは立ち上がります。いくつか引数の設定を見る限り注意すべき点があります。
projectとexperimenter
この二つはプロジェクト名とプロジェクト主で任意の名前を付ければいいのですが、実際にプロジェクトを新規作成すると「プロジェクト名-プロジェクト主名-(プロジェクトを作成した日付)」という名前でプロジェクトのルートディレクトリが作成されます。注意しておきたいのがこのプロジェクトのルートフォルダはよくパスとして指定するので、日本語や空欄などは使わない方がいいです。videos
使うビデオの絶対パスを指定します。この関数の実行によって自動的にこのビデオはプロジェクトのvideoサブディレクトリにコピーされるので、もとのビデオを置く場所はどこでも構いません。multianimal: True/False
もしビデオで解析する動物が2種類以上いる場合はおそらくこれをTrueにする必要があります。今回は一旦シンプルに理解することが目的なので1種類です。
実際に関数を実行してみます。以下は私の環境で実行しているものです。
deeplabcut.create_new_project('TestProject', 'ShinaJones', ['/usr/src/cloned-DLC-repo/examples/test.mp4'], working_directory='/usr/src/cloned-DLC-repo/examples', copy_videos=False, multianimal=False)
実行した結果、無事想定通りプロジェクトが作成されました。以下のようなサブディレクトリも併せて作成され、videosの中にはビデオがコピーされてあります。
各サブディレクトリの説明は以下のように公式ドキュメントに記載されている内容を踏まえて私が要約した内容です。
dlc-models: このディレクトリは特徴量を抽出する検出器のパラメータに関するメタ情報を保有します。これらのパラメータはpose_cfg.yamlとして保存されています。このディレクトリにはTrainとTestの2つのサブディレクトリがあります。Trainはモデルの学習中のCheckpoint(Snapshot)を保存しています。ユーザーはこれらのSnapshotを用いることでモデルの再学習をすることなくモデルの読み込みを行うことができます。
labeled-data: このディレクトリはTrainデータを作成するときに用いられるフレームデータを保持します。フレームデータとは動画中の1コマを画像として切り出したものをさします。動画は時間的に連続した画像の集合体で構成されており、その各画像が「フレーム」と呼ばれています。複数のビデオがある場合は、ビデオごとにサブディレクトリが生成され、その中で各ビデオのフレームが格納されます。各フレームのファイル名は動画の時系列に沿ったIndexとなっているので、簡単にビデオの追跡を行うことができます。
training-datasets: このディレクトリはネットワークを学習するために使うTrainデータを保持します。加えてどのようにTrainデータを作成するかを定義するようなメタ情報もここに保存します。
videos: ビデオを保存するディレクトリです。プロジェクト作成時にcopy_videosの引数でFalseを選択した場合、元のビデオはこのディレクトリにコピーされず、元のビデオへのリンクが生成され、ここのディレクトリに置かれることになります。ユーザーはadd_new_videos関数を実行することで、プロジェクトを作成した後でもビデオを追加することができます。この関数を実行することで設定ファイル内のビデオのリストの記載も更新されます。
deeplabcut.add_new_videos('Full path of the project configuration file*', ['full path of video 4', 'full path of video 5'], copy_videos=True/False)
プロジェクトのパラメータを設定する
このプロジェクトでモデル学習などさまざまなことをしていくにあたり、多くのユーザーが指定すべきパラメータはconfig.yamlで指定することになります。そのためまずはconfig.yamlを指定します。yamlファイルに慣れていない方もいるかもしれませんが、特段そんなにJsonファイルと違いはないので、慣れの問題です。以下がconfig.yamのパラメータの説明です。
config.yaml
このプロジェクトは上記のサブディレクトリ以外に各種パラメータの値を定義するconfig.yamlも持ちます。このconfig.yamlが最も大事で、以下の図に示すようなパラメータを持ちます。
create_new_project関数を実行した場合、 これらのパラメータのうち以下のtask, scorer, date, project_path, video_setsは値が自動でセットされます。さらに最初の3つのパラメータはセットされた値から変えることは推奨されていません。video_setsはadd_new_videos関数による実行やあるいは手動での追加でパスの追加や削除を行うこともできます。
Task:プロジェクト名(変更非推奨)
scorer:プロジェクト主(変更非推奨)
date:プロジェクト作成日(変更非推奨)
project_path:プロジェクトのルートディレクトリの絶対パス。プロジェクトの場所を移動したい場合は変更できます
video_sets:ビデオの絶対パス。Dictになっていて、キーはビデオの絶対パス、値はcropを設定します。cropはビデオからフレームを抽出するときに使用されます。この説明では書かれていませんが、実際にconfig.yamlでサンプルを確認すると「0, 640, 0, 480」のように4つの値があります。察するにこれらはビデオの解析範囲を縦横でピクセル単位で指定していると考えられます。この例の場合ですと、動画の左上から右下までの640x480ピクセルの範囲を切り出すよう指定されています。
横方向(x方向): 0ピクセル(最初の値)から640ピクセル(2番目の値)まで。
縦方向(y方向): 0ピクセル(3番目の値)から480ピクセル(4番目の値)まで。
次のパラメータはプロジェクトを作成後に編集する重要なパラメータです。
bodyparts : 推定を行いたい体の部位名をリスト形式で指定します。デフォルトでは、手、指1、指2、ジョイスティック(?)が設定されています。指定する値の部位名は任意の名前ですが名前にスペースは使えませんのでご注意ください。フレームのラベルを付けた後は変更できません。
numframes2pick: ビデオから抽出するフレーム数をInt型で指定します。デフォルトでは20に設定されています。
colormap: 推定した部位をビデオで表現したり、あるいはグラフとして可視化する際にそれぞれの部位に割り当てられる色をカラーマップで指定します(例えばjetのように指定)。カラーマップはMatplotlibのカラーマップが使用されています。
dotsize: 推定した部位はビデオや画像では点で表現されますが、その点の大きさを指定します。デフォルトは12で設定されています。
alphavalue: ラベルをプロットするときの透明度を指定します。デフォルトは0.5です。
iteration: Trainデータを生成する際のイテレーション(学習ステップ)のスタートインデックスを決めます。ここは手動で変えてはいけません。イテレーションは普通0から始まるので、デフォルト値も0になっています。
もし、長めのビデオを解析する場合、以下も編集する重要なパラメータのようです。
start: フレームをサンプリングする間隔を指定します。スタートはデフォルトだと0です。
end: startと同様です。デフォルトでは1です。
以下は、ニューラルネットワークに関連するパラメータです。
TrainingFraction : TrainデータとTestデータの振り分けの割合を決める値です。0~1の範囲でTrainデータ側の割合を小数で指定します。デフォルトでは0.95です。小数は小数点第2位まで指定できます。
resnet: どのPre-trainedモデルを使用するかを決めます。50か101が使えるようで、デフォルトだと50になります。このあたりのモデルの意味はMathis et al, 2018のペーパーに詳細が記載されているようです。
以下はビデオの解析中に使われるパラメータです。
batch_size : 学習時に一バッチに何フレーム渡すかを指定します。このパラメータのチューニングについてはMathis & Warren 2018を参考にできるようです。
snapshotindex: networkを評価するときにどのCheckpointを使うかを指定します。デフォルトでは-1が設定されています。-1はすべてのCheckpointを使用するという意味になります。Snapshotsは特徴量の検出器の重み(Weighs)を保持するTensorflowの設定を参照しています。
p-cutoff: 尤もらしさを決める閾値の値です。身体から推定したい部位かどうかを判定するときに、実際にはこの値以上であれば、その部位であると判定するのだろうと思われます。デフォルトでは0.1です。
cropping: 解析するビデオがトリミングが必要であれば指定します。デフォルトではFalseとなります。
x1,x2,y1,y2: cropする(トリミングする)際に指定します。意味合いはvideo_setsのcropと同じだと思われます。
以下はrefinementステップの時に利用されるパラメータです。モデルの中身を見ていないので確実ではないですが、普通に考えてモデルをfine-tuningするステップだと思われます。
move2corner: DeepLabCutでは、予測結果が指定したCrop外になってしまうケースがあります。それらのケースがあった場合に自動的にユーザーが指定しているCropサイズ内に予測位置を修正するパラーメータです。この自動修正機能を使うかどうかのTrue/Falseを指定します。デフォルトではTrueに設定されています。
corner2move2: move2cornerがTrueである場合のターゲット位置を示します。デフォルトでは50,50です。
その他のパラメータ
skeleton: ペアの形で各ボディパートの接続を記述します。
bodypart1 と bodypart2 の間に線(エッジ)を引くような関係を定義します。この情報は、ラベル付けした座標データを基にして骨格を描画する際に利用されます。
config.yamlのパラメータ設定が終わったら、後の処理でよく使うので以下でconfig.yamlのパスを保存しておきます。なお、基本的にはconfig.yamlはプロジェクトルートディレクトリ直下にプロジェクト作成時に作成されるので、次のようにしておくといいかもしれません。
from pathlib import Path
def find_directory(target_dir_name, search_path="."):
"""指定した名前のディレクトリを検索し、その一つ上のディレクトリの絶対パスを返す。
まず search_path を基準に下方向に探し、見つからない場合は上方向に探す。
Args:
target_dir_name (str): 検索するディレクトリ名。
search_path (str): 検索を開始する親ディレクトリ(デフォルトは現在のディレクトリ)。
Returns:
str or None: 見つかった場合はその親ディレクトリの絶対パス、見つからなければ None。
"""
def find_directory_downwards(target_dir_name, search_path):
"""下方向に指定した名前のディレクトリを検索"""
search_path = Path(search_path).resolve()
for dir_path in search_path.rglob(target_dir_name):
if dir_path.is_dir() and dir_path.name == target_dir_name:
return dir_path
return None
def find_directory_upwards(target_dir_name, start_path="."):
"""上方向に指定した名前のディレクトリを検索"""
current_path = Path(start_path).resolve()
while current_path != current_path.parent: # ルートディレクトリに到達するまでループ
target_path = current_path / target_dir_name
if target_path.is_dir():
return target_path
current_path = current_path.parent # 上位のディレクトリに移動
return None
# 下方向の検索
found_dir = find_directory_downwards(target_dir_name, search_path)
if not found_dir:
# 上方向の検索
found_dir = find_directory_upwards(target_dir_name, search_path)
if found_dir:
parent_path = found_dir.parent
print(f"見つかったディレクトリの親パス: {parent_path}")
return str(parent_path)
else:
print(f"ディレクトリ '{target_dir_name}' は見つかりませんでした。")
return None
# 使用例
target = "TestProject-ShinaJones-2025-01-18" # 探したいディレクトリ名
project_full_path = find_directory(target)
if project_full_path:
print(f"ターゲットの一つ上のディレクトリ: {project_full_path}")
else:
print("ターゲットの親ディレクトリは見つかりませんでした。")
config_path = os.path.join(project_full_path,f'{target}/config.yaml')
ビデオからのフレーム抽出
Trainデータを作成します。Trainデータはフレームですが、良いTrainデータを作成するためには推定に役立つような情報を持つ十分なフレーム数が必要になります。推定に役立つような情報を持つ十分なフレーム数にするためには、例えば異なる明るさを持つビデオや異なる動物が映っているビデオなどさまざまなビデオをTrainデータソースとして持つ必要があります。例えば開けた空間でのマウスやハエの行動は100-200フレームほどのフレーム数でも十分な学習が可能ですが、推定したい行動や予測精度の期待値などによってより多くのフレーム数が必要になってきます。
さて、実際にはフレーム抽出は以下の関数によって行うことができます。
deeplabcut.extract_frames(config_path, mode='automatic/manual', algo='uniform/kmeans', userfeedback=False, crop=True/False)
この関数の実行によってconfig.yamlのvideo_setsで指定したすべてのビデオのフレームがlabeled-dataディレクトリ配下に生成されます。前述の通り、ビデオが複数ある場合はビデオ事にビデオの名前をディレクトリ名にもつサブディレクトリが生成され、そこにそれぞれのフレームが保存されます。
この関数にはいくつか引数を設定します。
mode: フレームを抽出する際の操作モードを指定します。以下のオプションがあります:
automatic: フレーム抽出が自動で行われます。動画の全体から選択されたアルゴリズム(algo)に基づいて、均等またはクラスタリングされた方法でフレームが抽出されます。ユーザーの介入はありません。
manual : フレーム抽出時にGUIが開き、ユーザーが動画を確認しながらフレームを手動で選択できます。このモードでは、ユーザーが具体的に重要と思う場面をフレームとして選ぶことが可能です。
algo: mode='automatic' の場合にフレームを選ぶアルゴリズムを指定します。以下のオプションがあります:
uniform: 動画の全体を通じて均等間隔でフレームを抽出します。この方法は、動画内でフレーム間の変化が少ない場合や、シンプルな解析に適しています。
kmeans: クラスタリングアルゴリズム(K-Means)を使用して、動画内で動きの特徴が異なるフレームを選びます。この方法は、動きや場面変化が多い動画で、代表的なフレームを抽出したい場合に便利です。つまり似ている画像(フレーム)はk-Meansによって同じクラスターに分類されるので、異なるクラスターからそれぞれフレームを抽出することで違った情報をもつフレームを選ぶことができるということです。フレームをラベル付けする枚数を減らさないと大変なので基本的にはkmeansで冗長なフレームにならないようにするのがいいみたいです。
userfeedback: mode='automatic' の場合に、抽出したフレームについてユーザーの確認を求めるかどうかを制御します。Trueの場合は抽出したフレームをGUIで表示し、ユーザーが確認・調整します。
crop: フレームサイズをトリミングするかどうかをTrue/Falseで選択します。フレームサイズは大きすぎると学習と推論に時間がかかるので、小さいフレームサイズにすることがポイントです。Trueにするとconfig.yamlで指定した値に基づきトリミングを実行します。
フレームにラベルをつける
続いて、次のコマンドで抽出したフレームにラベルを付けます。そしてここで気が付いたんですが、この関数はQtベースのGUIを立ち上げて画面上からラベル付けを行うことできるようにする機能です。つまりLinuxのようなヘッドレス環境では通常動作しないのです。とはいえ、WindowsOSのローカル環境だとTensorflowでNVIDIA製GPUを使えないので、全部ローカル環境でやることもできないです。ということで、基本的にはコンテナ環境で行い、この部分は素直にGUIを持つローカル環境で実行するのがよさそうです(どうしてもすべてコンテナ環境で行いたいという方はX11ディスプレイサーバーを利用する方法などもあるようですが、さすがにめんどくさいのでここではやりません)。
ということで、ローカル環境にPython仮想環境をCondaで立ち上げて以下を実施します。Condaがない人はAnacondaをインストールするかMinicondaをインストールしてCondaを手に入れてください。Anaconda経由の場合、こちらにインストールガイドがあります。
Anacondaをインストール
CondaでDeeplabcutの仮想環境を構築するため、環境を定義するyamlファイルをローカル環境にDLし、仮想環境を構築したい場所に保存。
Terminalを開き、保存した場所までTerminal上で移動。
「conda env create -f DEEPLABCUT.yam」を実行。もしcondaが認識されていないようなら、conda環境のScriptを環境変数のPathに追加してから実行する。加えてgitも必要なので、もしインストールしていない場合はgitもインストールしてから実行する。
VS code等で適当なipynbファイルを新規で作成し、Kernelは構築した仮想環境のPythonを選択し、上記で実行してきた内容を一通り実行する。
ここまで実行したら、ようやくラベル付けのためにlabel_frames関数を実行します。なお、コンテナ環境とWindows環境をバインドマウントしておけば後々楽です。
deeplabcut.label_frames(config_path)
これにより、次のようなGUIのポップアップが立ち上がります。
このツールの使い方については画面が表示されるとポップアップでツアーガイドがでるので、それに従ってもらえばOKですが、ざっと以下のような感じです。
ラベル付けするフレームを選択する。(フレームはlabeled-dataの直下にあるサブディレクトリにあるのでツールの「file>select folder」からそのディレクトリを選択することで読み込まれます。)
動画のフレームが読み込まれるので、config.yamlで定義したBody partsごとにこのツール上でラベル付けをする。例えば馬の前足の蹄を一つのラベル付けしたいBody partsだとしたら、フレーム事にそのラベルとなる点をツール上で一つ一つ打っていくような感じです(例えば画像のような形)。こういった作業をアノテーションといいます。
・アノテーションが終わったらラベル情報を保存します(「Ctrl+S」)。
ラベル付けが正しくできているかを確認
前項で行ったラベル付けデータがきちんと保存され、正しいフォーマットになっているかを確認します。これで問題がなければlabeled-dataのフォルダの中にもともとあるサブディレクトリとは別に「XX_labeled」というサブディレクトリが生成され、そこにアノテーションしたフレーム画像が格納されるようになります。
deeplabcut.check_labels(config_path, visualizeindividuals=True/False)
学習データ生成とネットワークの選択
ラベル付けしたデータを用意したところで次は学習データを構築していきます。学習データは先ほどアノテーションしたフレームと元のフレームをTrainとTestにわけるような作業です。加えて学習時に利用するネットワークを選択します。
さてここまで来てDeeplabcutで実際に動いているネットワークについてあまり見えてきませんでしたが、このDocsを読む限り、いくつかのモデルを用途別に選択できるようになっています。例えばSingle animalの場合はResnet50やHrNet32を選択できるようです。つまりここでいう学習とはPre-trainedなこうしたモデルを自分の用意したデータで転移学習したりFine-tuningしたりすることを指しているのですね。モデル自体の解説はまた別の機会に行うとして、以下でこの工程は実行できます。まずコンテナ環境に戻ります。これまで作成していた環境ですと上記のアノテーションデータがないので、バインドマウントを利用してアクセスできるようにします。「競走馬の骨格を動画から推定したい1」の「jupyter notebook --ip=0.0.0.0 --port=8000 --no-browser --allow-root」まで実施しますが、「docker run --gpus all -it --rm -p 8000:8000 deeplabcut/deeplabcut:2.3.5-base-cuda11.7.1-cudnn8-runtime-ubuntu20.04-latest bash」の部分を以下のようにします。
docker run --gpus all -it --rm -v "C:(ローカル環境のDeelplabcutのパス):usr/src/(好きなパス)"-p 8000:8000 deeplabcut/deeplabcut:2.3.5-base-cuda11.7.1-cudnn8-runtime-ubuntu20.04-latest bash
これでJupyter notebookを立ち上げたら、config.yamlのproject_pathとvideo_setsのパスをコンテナ環境のものに書き換えてください。
どうしてもバインドマウントがうまくいかない場合は、普通にコンテナ環境を立ち上げてからローカル環境のファイルをJupyter notebookからアップロードしてもいいですし、Github等に一旦アップロードして、それをコンテナ環境でCloneする方法もありますのでやりやすい方法でやればいいと思います。
ここまで来たら下記を実行してまずはTrainとTestデータを生成します。
deeplabcut.create_training_dataset(config_path)
ネットワークの学習
ネットワークの学習を実行する前にネットワークの設定を任意で変更することができます。この設定ファイルはdlc-modelディレクトリの配下にあるtrainサブディレクトリのpose_cfg.yamlを編集することで行うことができます。以下が主なパラメータです。
1. net_type: 使用するネットワークの種類を指定します。
例:
resnet_50: ResNet-50 アーキテクチャ(標準的に使用される)。
resnet_101: より深い ResNet-101(精度向上が期待されるが計算コストが高い)。
役割: モデルのアーキテクチャを選択することで、精度や計算効率に影響を与えます。
2. display_iters
説明: トレーニング中に進捗を表示する間隔(イテレーション数)。
例: 100 の場合、100イテレーションごとにトレーニング進捗を表示。
役割: 学習プロセスの監視を簡単にします。
3. save_iters
説明: トレーニング中のモデルを保存する間隔(イテレーション数)。
例: 1000 の場合、1000イテレーションごとにモデルを保存。
役割: トレーニング中のモデルのバックアップを確保します。
4. init_weights
説明: 初期重み(事前学習済みモデル)のパスを指定。resnet_v1_50.ckptがデフォルトで設定されています。
役割: 他のデータセットで学習済みの重みを使用してトレーニングを加速し、性能を向上させます。
5. multi_step
説明: 学習率のスケジュールを定義。
例: [0.001, 0.0001, 0.00001] の場合、特定のイテレーションで学習率を段階的に減少。
役割: トレーニングの進行に応じて学習率を調整し、より効率的な収束を促進。
6. max_input_size
説明: ネットワークに入力する画像の最大サイズをピクセルで指定。
例: 1500 など。
役割: メモリ使用量を制御し、大きすぎる画像による計算コストの増加を防ぎます。
7. global_scale
説明: 入力画像全体のスケールを調整する係数。
例: 0.5 で画像サイズを半分に縮小。
役割: 特徴抽出や処理効率に影響を与えるため、適切なスケール調整が重要です。
8. pos_dist_thresh
説明: トラッキング時にポイント間の距離のしきい値(ピクセル単位)。
役割: 距離がしきい値を超えると、異なる個体または間違いとして認識される。
9. dataset_type
説明: トレーニングに使用するデータセットの種類を指定。
例:
default: 単一の個体に焦点を当てるデフォルトモード。
multianimal: 複数個体のトラッキング用モード。
役割: データセットに応じたモデル動作を選択します。
10. scale_jitter_lo & scale_jitter_up
説明: データ拡張としてスケールをランダムに調整する範囲(下限と上限)。
例: 0.8(下限)、1.2(上限)で、80%~120%のスケールに変化。
役割: モデルがスケールの異なるデータに対応できるようにします。
11. mirror
説明: トレーニング中にデータ拡張としてミラーリング(左右反転)を行うかどうか。
役割: データの多様性を増し、汎化性能を向上。
12. cropping
説明: 入力画像をトリミングするかどうか。
役割: 必要に応じて画像の不要な部分を削除し、処理効率を向上させます。
13. cropratio
説明: トリミングの際に使用する比率を指定。
役割: トリミング範囲を調整し、重要な領域にフォーカスを当てます。
14. minisize
説明: トリミング後の最小画像サイズを指定。
役割: トリミングが極端になりすぎないようにするための制約。
15. leftwidth, rightwidth, bottomheight, topheight
説明: トリミングの際に、それぞれ左、右、下、上方向のピクセル量を指定。
例:
leftwidth: 50 は左側50ピクセルをトリミング。
役割: トリミング範囲をカスタマイズし、不要な領域を削除して処理効率を向上。
必要に応じてこれらのパラメータを適切に設定し、最適なネットワークとネットワーク学習を行います。ネットワークの学習は以下の関数を実行するだけです。マシンスペックにもよりますがmaxitersを設定しておかないと学習が終わらなくなることもあるので注意しましょう。
deeplabcut.train_network(config_path,shuffle=1, displayiters=10, saveiters=100,maxiters = 1000)
ネットワークの評価
ここは前回の記事と同じです。Trainデータに対しての評価が実行され、評価結果はevaluation-resultsのフォルダにcsvファイルで格納されます。
deeplabcut.evaluate_network(config_path, Shuffles=[1], plotting=True)
私の馬の動画で評価してみた結果が以下です。このようにTrainデータに対するerrorが111ピクセルほど、Testデータに対するerrorが116ピクセルほどとなっています。公式のDocsによれば損失関数はMean Absolute error(MAE)なので予測値と観測値の差の絶対値の平均ということになります。なので、例えばTestデータでいえば、正解(私がアノテーションしたラベルの位置)と予測した位置が平均して116ピクセルほどずれていたということになります。この差が看過できるものなのかそうでないのかはプロジェクトによります。今回ですと例えば馬の左前脚の蹄などをラベル付けしていますが、蹄の位置が116ピクセルずれていたとて、それでも左前脚の蹄にしっかりラベルがのっているなら問題ないとみるか、やはり蹄の位置を正確にみることで速い競走馬と遅い競走馬を見分けることができるんだ、ということだったらもうちょっとラベルを増やすなりして精度を上げる必要があります。
ちなみにevaluate_network関数も引数があります。以下はそれぞれの意味合いです。
1. config
説明: config.yaml ファイルのフルパスを指定します。
役割: プロジェクトの設定ファイルを読み込み、ネットワークの評価に必要な情報を取得します。
2. Shuffles
型: list, オプション、デフォルトは [1]
説明: トレーニングデータセットのシャッフルインデックスを指定するリスト。
役割: 指定されたシャッフルでトレーニングされたモデルを評価します。つまりシャッフルのパターンを選んで、そのパターンしたがって選ばれたトレーニングデータのフレームが選択されるということですね。
3. trainingsetindex
型: int または str, オプション、デフォルトは 0
説明: config.yaml 内の TrainingsetFraction リストからどのトレーニングデータの割合を使用するかを指定します。デフォルトでTrainingsetFractionは0.95で設定されています。
例:
0: 最初の割合(例えば 0.5)。
"all": すべての割合で評価。
役割: トレーニングデータセットの異なる割合での評価を可能にします。
4. plotting
型: bool または str, オプション、デフォルトは False
説明: 評価時にトレーニング画像とテスト画像の予測をプロットするかどうかを指定します。
選択肢:
False: プロットを無効化。
True: 単純なプロットを実行。
"bodypart": ボディパーツごとのプロット(マルチアニマルプロジェクトのデフォルト)。
"individual": 個体ごとのプロット(マルチアニマルプロジェクト用)。
役割: 結果の可視化を有効にし、モデルの予測精度を視覚的に確認できます。
5. show_errors
型: bool, オプション、デフォルトは True
説明: トレーニングとテストのエラーを表示するかどうかを指定します。
役割: エラーを確認することで、モデルのパフォーマンスを評価しやすくなります。
6. comparisonbodyparts
型: str または list, オプション、デフォルトは "all"
説明: エラーを計算する対象のボディパーツを指定します。
役割: 特定のボディパーツに焦点を当てた評価が可能です。
7. gputouse
型: int または None, オプション、デフォルトは None
説明: 使用するGPUを指定します(nvidia-smi で確認できる番号)。
例: 0 は最初のGPU。
None: GPU を使用しない場合。
役割: GPU を指定することで、評価を高速化できます。
8. rescale
型: bool, オプション、デフォルトは False
説明: pose_config.yaml の global_scale に基づいて画像をリサイズして評価を行います。
動作:
評価時に画像をスケール変更し、変更後のピクセル単位で誤差を計算。
元のスケールに戻してエラーを報告。
例:
元の画像サイズが [200,200] の場合、global_scale=0.5 なら [100,100] に縮小して評価。
エラーは元のスケールで報告されます。
役割: スケール変更の影響を考慮した精度評価が可能。
9. modelprefix
型: str, オプション、デフォルトは ""
説明: ネットワークを評価する際に使うDeeplabcutのモデルが保存されているディレクトリを指定。
空文字列の場合、プロジェクトフォルダ内を使用。
役割: カスタムのモデルパスを指定することで、柔軟な評価が可能。
10. per_keypoint_evaluation
型: bool, オプション、デフォルトは False
説明: 各キーポイントごとのトレーニングおよびテストの RMSE(平方根平均二乗誤差)を計算して保存。
役割: より詳細なエラー分析を可能にします。
11. snapshots_to_evaluate
型: list, オプション、デフォルトは None
説明: 評価するスナップショット(モデルの保存状態)のリストを指定。
例: ["snapshot-50000", "snapshot-75000"]
役割: 特定のスナップショットで評価を絞り込むことで、効率的な結果分析が可能。
予測
作成したモデルが良さそうであれば、学習時に利用したビデオ以外を用いて実際に推定を行ってみましょう。以下の関数の実行で新たなビデオを学習したモデルを使って予測します。引数に予測したいビデオのパスをいれます。このビデオのパスはconfig.yamlにリストアップしていなくても問題ありません。また利用するモデルもどのSnapshotのモデルを利用するのかを選択できます。各Checkpointの評価結果を見て最もいい評価であるものを選びます。選択するときはconfig.yamlのsnapshotindexでsnapshotのインデックス番号を渡します。デフォルトでは-1になっており、これは最新のSnapshotを利用すること意味します。他にも引数がありますので、以下を参考にしてみてください。必須なのは第二引数までです。
deeplabcut.analyze_videos(config_path, ['fullpath/analysis/project/videos/reachingvideo1.avi'], save_as_csv=True)
config
型: str
説明:
設定ファイル config.yaml のフルパスを指定します。
このファイルにはプロジェクトの設定やモデルの情報が含まれています。
videos
型: list[str]
説明:
動画のフルパスを含むリストを指定します。
または、特定の拡張子の動画が保存されているディレクトリのパスも指定可能です。パスにルートフォルダを含める必要はありません。ルートフォルダまで含めるとビデオが見つからないというエラーがでます。例えばMyProjectがプロジェクトディレクトリならMyProjectはパスに含める必要はありません。
videotype
型: str, デフォルト値: ""
説明:
動画の拡張子を指定します。例えば、.avi や .mp4。
ディレクトリ内の動画を指定する場合のみ使用されます。
未指定の場合、一般的な拡張子(avi, mp4, mov, mpeg, mkv)が対象となります。
shuffle
型: int, デフォルト値: 1
説明:
学習データセットのシャッフルインデックスを指定します。
モデル学習時に異なるシャッフルで生成されたデータに対応します。
trainingsetindex
型: int, デフォルト値: 0
説明:
使用するトレーニングデータのインデックスを指定します。
設定ファイル config.yaml の TrainingFraction リスト内の対応する値が使用されます。
gputouse
型: int または None, デフォルト値: None
説明:
使用するGPUデバイス番号を指定します(例: nvidia-smi の出力番号)。
GPUがない場合は None を指定します。
save_as_csv
型: bool, デフォルト値: False
説明:
分析結果をCSV形式(.csv ファイル)で保存します。
in_random_order
型: bool, デフォルト値: True
説明:
動画をランダムな順序で処理するかどうかを指定します。
動画ディレクトリを指定した場合にのみ有効です。
destfolder
型: str または None, デフォルト値: None
説明:
分析結果を保存するフォルダを指定します。
未指定の場合、動画が保存されているフォルダに結果が保存されます。
batchsize
型: int または None, デフォルト値: None
説明:
推論時のバッチサイズを変更します。
未指定の場合、pose_cfg.yaml ファイルに設定された値が使用されます。
cropping
型: list または None, デフォルト値: None
説明:
[x1, x2, y1, y2] の形式で動画のクロップ範囲を指定します。
全ての動画に同じクロップ設定が適用されます。
TFGPUinference
型: bool, デフォルト値: True
説明:
TensorFlowを使用してGPUで推論を行うかどうかを指定します。
dynamic
型: tuple(bool, float, int), デフォルト値: (False, 0.5, 10)
説明:
動的クロッピングを有効化するかどうかを指定します。
動物が検出された位置に基づいてクロッピング範囲を更新します。
各値の意味:
state: 動的クロッピングを有効化するかどうか。
detectionthreshold: 検出の閾値(例: 部位が検出された確率の最低値)。
margin: 検出領域の周囲に追加する余白。
modelprefix
型: str, デフォルト値: ""
説明:
使用するモデルが保存されているディレクトリを指定します。
デフォルトではプロジェクトフォルダ内のモデルが使用されます。
robust_nframes
型: bool, デフォルト値: False
説明:
動画のフレーム数をファイルメタデータに頼らず厳密に計算するかどうかを指定します。
メタデータが壊れている場合に有効です。
allow_growth
型: bool, デフォルト値: False
説明:
メモリの動的確保を許可します。
小規模なGPU環境でのメモリ不足対策に使用します。
use_shelve
型: bool, デフォルト値: False
説明:
分析データをメモリ上に保持せず、逐次ディスクに書き込むかどうかを指定します。
メモリ使用量を抑えたい場合に有効です。
推定結果を補正する(任意)
上記で予測はできていますが、推定では、フレーム間のばらつきやノイズ(例えば、一時的に予測が大きく外れること)が発生する場合があります。DeepLabCutで推定されたデータをフィルタリングすることで、ノイズ除去やデータスムージングが行われ、より精度の高い予測データを生成することが可能になります。結果は新しい.h5ファイルとして保存されます。このファイル名には「_filtered」という接尾辞が追加されます。
DeepLabCutでは、以下の方法でポーズデータをフィルタリングできます。
(a) メディアンフィルタ(Median Filter)
デフォルトの方法でフレームごとの位置データを処理し、周囲の値の中央値を使ってスムージングします。主に外れ値を効果的に除去します。シンプルで計算負荷が低いのが特徴です。
(b) SARIMAXモデル(Seasonal AutoRegressive Integrated Moving Average with eXogenous inputs)
時系列データ分析に基づいたモデルでより高度なフィルタリングが可能です。特に長期的な傾向や季節性のあるデータに適しています。一方でメディアンフィルタに比べて計算コストが高く、設定が必要です。
(c) スプライン補間 (spline interpolation)
以下の関数でこれを実行できます。
deeplabcut.filterpredictions(config_path, ['fullpath/analysis/project/videos/reachingvideo1.avi'], shuffle=1, trainingsetindex=0, comparisonbodyparts='all', filtertype='arima', p_bound=0.01, ARdegree=3, MAdegree=1, alpha=0.01)
関数の引数の説明は以下です。必須なのは第二引数までです。
config
プロジェクトの config.yaml ファイルへの完全パス。
モデルやトレーニングデータセットの情報が含まれています。
video
フィルタリング対象となる動画ファイルへの完全パス。
動画は事前に分析されている必要があります(推定データが存在すること)。
filtertype
フィルタの種類。デフォルトは "median"。
"median": メディアンフィルタでノイズを除去。
"arima": 時系列モデル(ARIMA/SARIMAX)を使用した高度なフィルタリング。
"spline": 欠損値の補間やスムージングにスプラインを使用。
windowlength(デフォルト: 5)
メディアンフィルタの場合の窓幅(奇数である必要あり)。
スプラインの場合、ギャップ補間の最大幅。
p_bound(デフォルト: 0.001)
arima フィルタのみに適用。
指定された確率以下のデータポイントを欠損値として扱う。
ARdegree(デフォルト: 3)
arima フィルタの自回帰次数。
MAdegree(デフォルト: 1)
arima フィルタの移動平均次数。
alpha(デフォルト: 0.01)
ARIMAモデルで外れ値を検出するための信頼区間。
save_as_csv(デフォルト: True)
フィルタリング結果をCSVファイルとして保存するかどうか。
destfolder
フィルタリング結果を保存するディレクトリ。
指定しない場合、動画のパスと同じ場所に保存。
track_method
複数動物プロジェクトの場合にトラッキング方法を指定(例: 'box' や 'skeleton')。
return_data(デフォルト: False)
Trueの場合、フィルタリング結果を辞書形式で返す。
軌跡のプロット
このあたりも前回の記事と同じですね。予測結果に関するグラフをいくつか描いてくれます。詳細は前回の記事を参照してください。一つ前回の記事でよくわかっていなかったDeltaXとDeltaYについてですが、これはX軸方向とY軸方向での予測値と観測値の差(ピクセル)ですね。
deeplabcut.plot_trajectories(config_path, [‘fullpath/analysis/project/videos/reachingvideo1.avi’])
予測結果をマッピングしたビデオの作成
予測結果をビデオに実際マッピングしてみます。これで人間でも視覚的にわかりやすい予測結果を確認することができます。以下に引数の説明も掲載しておきます。注意点として今度はビデオのパスをフルパスで指定しないといけません。これはプロジェクトルートディレクトリからじゃなく、実行環境のルートディレクトリから指定しなければいけないので、os.getcwd()コマンド使いながら指定することをおすすめします。
deeplabcut.create_labeled_video(config_path, ['fullpath/analysis/project/videos/reachingvideo1.avi','fullpath/analysis/project/videos/reachingvideo2.avi'], save_frames = True/False)
# もし上記でfilterした方を使いたい場合
deeplabcut.create_labeled_video(config_path, ['fullpath/afolderofvideos'], videotype='.mp4', filtered=True)
# 予測ラベルを点(ポイント)だけじゃなく線でつないだもの(スケルトン)も表示したい場合
deeplabcut.create_labeled_video(config_path, ['fullpath/afolderofvideos'], videotype='.mp4', trailpoints=10)
config:
str型
プロジェクトのconfig.yamlファイルへのフルパス。
videos:
list[str]型
分析対象のビデオファイルのフルパスを含むリスト、またはビデオが保存されているディレクトリのパス。os.getcwd()でフルパスを作成するようにしましょう。
videotype:
str型 (省略可能、デフォルト値: "")
指定された拡張子のビデオのみを分析。省略時は一般的な拡張子(avi、mp4など)が対象。
shuffle:
int型 (省略可能、デフォルト値: 1)
トレーニングデータセットのシャッフル回数。
trainingsetindex:
int型 (省略可能、デフォルト値: 0)
使用するトレーニングセットのインデックス。
filtered:
bool型 (省略可能、デフォルト値: False)
フィルタリング済みの出力をプロットするかどうか。
fastmode:
bool型 (省略可能、デフォルト値: True)
OpenCVを使用して高速処理を行うか(カスタマイズ性は低下)。
save_frames:
bool型 (省略可能、デフォルト値: False)
各フレームを個別に保存してからビデオを作成するかどうか。
keypoints_only:
bool型 (省略可能、デフォルト値: False)
フレームを非表示にしてキーポイントのみを表示するかどうか。
Frames2plot:
list[int]型 または None (省略可能、デフォルト値: None)
プロットするフレームのインデックスを指定(save_frames=True時のみ有効)。
displayedbodyparts:
list[str]型 または str (省略可能、デフォルト値: "all")
表示する身体部位の指定("all"の場合は全て)。
displayedindividuals:
list[str]型 または str (省略可能、デフォルト値: "all")
表示する個体を指定("all"の場合は全て)。
codec:
str型 (省略可能、デフォルト値: "mp4v")
使用するビデオコーデック。
outputframerate:
int型 または None (省略可能、デフォルト値: None)
ラベル付きビデオのフレームレート。
destfolder:
str型 または None (省略可能、デフォルト値: None)
分析データの保存先フォルダ。
draw_skeleton:
bool型 (省略可能、デフォルト値: False)
フレームに骨格を描画するかどうか。
trailpoints:
int型 (省略可能、デフォルト値: 0)
表示する過去フレーム数。
displaycropped:
bool型 (省略可能、デフォルト値: False)
クロップされたフレームのみを表示するかどうか。
color_by:
str型 (省略可能、デフォルト値: "bodypart")
色分けルール(部位または個体単位)。
modelprefix:
str型 (省略可能、デフォルト値: "")
使用するモデルが格納されているディレクトリ。
init_weights:
str型
スーパーモデルの初期重み。
track_method:
str型 (省略可能、デフォルト値: "")
トラッキング方法の指定(マルチ個体時に使用)。
overwrite:
bool型 (省略可能、デフォルト値: False)
既存のラベル付きビデオを上書きするかどうか。
confidence_to_alpha:
bool型 または Callable[float, float] (省略可能、デフォルト値: False)
信頼値をα値に変換する関数を指定可能。
モデルの強化
実際にモデルの結果を評価したり、動画にしてみて期待通りの予測になっていない場合、モデルの改善・強化を行うことが次に求められます。
基本的にモデルの評価後、Trainデータに対して行うべきことは4つに大別されます。
正しい予測 (A): 正確な予測で特に修正は不要です。
誤った予測 (B): 予測対象が画像内でしっかり映っているにもかかわらず予測が間違っている場合は、ラベルを正しい位置に修正してあげます。
不可視または遮蔽された部位 (C): そもそもそ画像内で予測対象が他の物体によって遮蔽されて見えない場合は、その予測対象のラベルは削除します。
無効な画像 (D): 特に何か画像に問題がある場合は画像を削除します。
以下は特にBやCに対応するための具体的な方法となります。
外れ値的な予測の除去
ラベルの予測がかなり外れ値的なものを抽出して除去します。
DeepLabCutでは、特定の条件を満たす外れ値的なフレームを抽出する以下の方法が提供されています。
信頼度が閾値以下のフレームを選択:outlieralgorithm='uncertain'
予測した点(例えば馬の左足の蹄を予測点とした場合)ごとの信頼度が pbound 以下の場合、フレームを抽出します。これはエラーだけでなく、遮蔽(予測対象が他の物体などによって隠れてしまったりすること)による可能性もあります。フレーム間で大きな位置変化がある場合を選択:outlieralgorithm='jump'
予測点が前のフレームからあるピクセル以上移動した場合にフレームを選択します。時系列モデルに基づく逸脱を選択
各予測点の時系列データにARIMAモデルを適用し、予測位置から平均で あるピクセル以上離れたフレームを選択します。時系列データにおいて信頼度が pbound 以下の検出は欠損値として扱われ、予測は時系列ポイントごとに算出されます。その結果として各予測点が時系列の平均で見た時あるピクセル以上離れたフレームかどうかで判定されます。手動選択:outlieralgorithm='manual'
ユーザーが視覚的に確認して手動でアウトライアーフレームを選択します
上記手段によって外れ値的なフレームを除外するとき、抽出するフレームの数がconfig.yamlのnumframes2pickを超えてしまうケースもあります。そのため、ユーザーが期待する以上の外れ値的なフレーム数を抽出しないように、それらのフレームをさらにサンプリングする方法を指定することができます。
ランダムサンプリング(extractionalgorithm='uniform'):一様分布に基づいたサンプリングを行います。
クラスタリング(extractionalgorithm='k-means'):k-meansに基づいてフレームをサンプリングをします。
自動設定では、条件を満たすフレーム数が多い場合、抽出前に確認のプロンプトが表示され、パラメータを調整できます。
deeplabcut.extract_outlier_frames(config_path, ['videofile_path'])
Trainデータのラベルの修正
GUIでラベルの修正を行います。GUIを利用するということはヘッドレスなコンテナ環境では実行できないので、こちらはローカル環境に戻って実行する必要があります。手順としては以下のような形です。
ラベルをロードし、閾値を設定し信頼度が低いラベルを抽出。
ラベルをドラッグして位置を修正する。必要に応じてラベルを削除する。
deeplabcut.refine_labels(config_path)
修正後、データセットを統合するために以下のコマンドを実行します。統合時に config.yaml の関連パラメータが更新され、新しいデータセットが作成されます。
deeplabcut.merge_datasets(config_path)
上記の方法でラベル付けを修正したら
以下の手順でモデルを更新しましょう。
モデルの再トレーニング
統合したデータセットを用いて、トレーニングデータセットを生成し、ネットワークを再トレーニングします。調整が加えられた場合は、初期状態の重みから開始することが推奨されています。モデルの再評価
再トレーニング後、モデルが十分に一般化できるかモデルの評価結果(特にTestに対するMAE)を確認します。不十分ならさらにラベル付けを追加し、再度学習を行います。
このプロセスを繰り返し、モデルの精度を向上させた後に、新しい動画の解析を進めましょう!以上でdeeplabcutでSingleAnimalの予測手順がわかりました。やり方はわかったので次回以降はチューニングしながらモデルの構築を行いたいと思います。