【MMAction2】inference_recognizerでリアルタイム推論がうまくいかない人のための実践ガイド
はじめに
SREホールディングス株式会社 のデータサイエンティストとして画像認識案件の開発に携わっている池内です。
さて、MMAction2は非常に強力な行動認識ライブラリですが、学習済みのモデルをWebカメラなどでリアルタイム推論しようとした際に、こんな経験はないでしょうか?
-
inference_recognizerにカメラフレームを渡しているのに、全く意味のない結果が返ってくる - 動画ファイルならうまくいくのに、生のフレームだとなぜか動かない
- エラーは出ないが、とにかく精度が全く出ない
この記事は、まさにそうした問題で悩んでいる方のために、その原因と、安定したリアルタイム推論を実現するための確実な方法を、筆者が試行錯誤した経験を元に解説するものです。
結論:生のフレームを渡す際はinference_recognizerの挙動の完全な理解が必須
inference_recognizerは高機能ですが、生のフレームを入力する場合、内部パイプラインが期待するデータ形式(キー名、サンプリング方法など)と完全に一致したデータを渡す必要があります。
この前提が一つでも崩れると、エラーが出ないまま正規化などの重要な処理がスキップされ、デバッグが困難な問題を引き起こします。
そのため、特にリアルタイム推論のような応用的なタスクでは、inference_recognizerの挙動を完全に理解して使うよりも、本記事で解説する手動実装の方が、処理が透明で問題解決も容易なため、遥かに堅牢なアプローチと言えます。
inference_recognizerの挙動と「罠」の正体
inference_recognizerの挙動は、渡す引数によって大きく異なります。
- 簡単モード(動画ファイルパス):
result = inference_recognizer(model, 'path/to/video.mp4')
test_pipelineの全工程(読み込み、サンプリング、正規化など)を自動で完璧に実行してくれます。
- 応用モード(生のフレーム):
frames = get_frames_from_webcam()
result = inference_recognizer(model, frames)
動画読み込みなど一部の工程をスキップしますが、後続の処理が期待するデータ形式と完全に一致していないと、後述する「罠」にはまり、処理が静かに失敗します。
リアルタイム推論における3つの落とし穴
inference_recognizerは、test_pipelineでのデータ整形後、モデル定義内のdata_preprocessorという機能を使って最終的な正規化などを自動で行うように設計されています。
しかし、生のフレームを入力すると、この自動正規化のステップに到達する前に、下記のような罠によって処理が失敗してしまいます。これが不具合の核心です。
罠①:色空間の不一致 (BGR vs RGB)
- 問題: OpenCVで取得したフレームはBGR形式ですが、モデルはRGB形式を期待しています。これは単純な問題ですが、見落とすと致命的な不一致となります。
-
解決策: 必ず
cv2.cvtColorでRGBに変換します。
罠②:フレームサンプリング方法の不一致
- コメント: 筆者自身はここでかなりハマりました。当初、スライディングウィンドウを使いつつ、一定間隔でフレームをキューに溜めることでサンプリングを擬似的に実現しようとしましたが、これではモデルの性能を引き出せませんでした。
- 問題: その理由は、リアルタイム処理で使いがちな連続したフレームと、モデルが学習時に見た等間隔に間引かれたフレーム群との間に、サンプリング戦略の根本的な不一致があるためです。
-
解決策: 学習時の
SampleFrames設定を尊重し、推論時もnp.linspaceなどを用いて手動で等間隔サンプリングを正確に再現することです。
罠③:データキー名の不一致 (imgs vs array)
-
コメント: これも非常に見つけにくい問題でした。MMAction2のサンプルには動画チャンクを処理する
long_video_demo.pyがあり、そこではarrayキーが使われています。動画チャンク処理がarrayキーでうまく動作したことから、カメラからの映像も同様に扱おうとして、この罠にはまりました。 -
問題: しかし、
arrayキーはArrayDecodeという特殊なパイプラインコンポーネント専用です。汎用的なパイプライン内の処理は、imgsというキーを持つデータを期待するため、キーが違うと処理対象を見つけられず、data_preprocessorに渡すための正しいデータを作成できません。 -
解決策 (鉄則):
-
imgs: 最も標準的なキー名。基本的にはimgsを使いましょう。 -
array:ArrayDecodeという特殊なコンポーネントを使う場合のみ使用します。
-
【推奨】パイプライン手動実装の実践
これらの罠を確実に回避し、正規化を含む全ての処理を完全に制御するため、以下の手動実装が最も推奨されるアプローチです。
Step 1: setup_modelでのパイプライン構築
1.1 元のパイプライン定義の確認 (Before)
まず、config.pyで定義されている、動画ファイル処理用のtest_pipelineを確認します。
# config.py より
test_pipeline = [
dict(type="DecordInit"), # 動画ファイルの初期化
dict(type='SampleFrames', ...), # 動画からフレームをサンプリング
dict(type="DecordDecode"), # フレームを画像にデコード
dict(type="Resize", scale=(-1, 256)),
dict(type="CenterCrop", crop_size=224),
dict(type="FormatShape", input_format="NCHW"),
dict(type="PackActionInputs"),
]
1.2 パイプラインの改造 (After)
生のフレームを入力するため、動画処理に関連するDecordInit, DecordDecode, SampleFramesは不要です。これらを動的に除外します。
original_pipeline_cfg = cfg.test_pipeline.copy()
excluded_types = ['DecordInit', 'DecordDecode'] # 除外リスト
post_sampler_pipeline_cfg = []
for step in original_pipeline_cfg:
step_type_str = get_str_type(step['type'])
if step_type_str not in excluded_types and 'SampleFrames' not in step_type_str:
post_sampler_pipeline_cfg.append(step)
# 改造したパイプライン設定から、Composeオブジェクトを生成
self.post_sampler_pipeline = Compose(post_sampler_pipeline_cfg)
この結果、self.post_sampler_pipelineはリサイズから始まる画像処理のみを行うパイプラインになります。
Step 2: _inference_loopでのデータ準備と推論
推論ループ内では、SampleFramesやdata_preprocessorの役割を一つずつ手動で実行します。
2.1 手動サンプリング (SampleFramesの代替)
configのclip_len=16に基づき、np.linspaceでフレームチャンクから16フレームを等間隔に抽出します。
indices = np.linspace(0, len(frame_chunk) - 1, self.sample_length, dtype=int)
sampled_frames = [frame_chunk[i] for i in indices]
2.2 データ辞書の作成
パイプラインが認識できるよう、標準キーimgsを使って辞書を作成します。
data = dict(
imgs=sampled_frames,
num_clips=1,
clip_len=self.sample_length,
...
)
2.3 改造パイプラインの適用
作成したデータを、改造したパイプラインに通し、リサイズやクロップ、次元整形を行います。
processed_data = self.post_sampler_pipeline(data)
inputs = processed_data['inputs'][0]
2.4 手動での正規化 (data_preprocessorの代替)
data_preprocessorが担うはずだった正規化を、configからmeanとstdを取得して明示的に実行します。
# セットアップ時にconfigから値を取得しておく
mean = torch.tensor(cfg.model.data_preprocessor.mean, device=self.device).view(1, 3, 1, 1)
std = torch.tensor(cfg.model.data_preprocessor.std, device=self.device).view(1, 3, 1, 1)
# 推論ループ内で正規化を適用
n, c, t, h, w = inputs.shape
inputs_reshaped = inputs.permute(0, 2, 1, 3, 4).contiguous().view(-1, c, h, w)
inputs_reshaped = (inputs_reshaped - mean) / std
inputs = inputs_reshaped.view(n, t, c, h, w)
2.5 model.forward()による直接推論
全ての準備が整ったテンソルを、model.forward()に直接渡して推論を実行します。
with torch.no_grad():
result_list = self.model.forward(inputs, data_samples, mode='predict')
おわりに
inference_recognizerは、その前提条件を完全に理解し、入力データを完璧に準備できるのであれば、生のフレームに対しても使用可能です。
しかし、リアルタイム推論のような応用的なタスクでは、隠れた前提条件がデバッグを困難にする要因となりがちです。今回解説したデータ処理フローの完全な手動化は、処理の透明性を確保し、問題発生時の原因特定を容易にするため、結果として最も堅牢なシステム構築に繋がります。
この記事が、同じ問題で悩む誰かの助けになれば幸いです。
Discussion