はじめに
「モーション テンプレート」は、MIT Media Labが開発した動き抽出の効率的な方法です(リンク1、リンク2)。計算量が少なくリアルタイムな計算に向いています。応用範囲も広く、ジェスチャ認識、スポーツ中継で選手の動き、ボール、バット、クラブ、ラケットの軌跡を視覚的に表示したりすることができます。
今回は、OpenCV 3を使って、モーションテンプレート解析をしてみます。
OpenCV
OpenCV(Open Source Computer Vision Library)はBSDライセンスの映像/画像処理ライブラリ集です。画像のフィルタ処理、テンプレートマッチング、物体認識、映像解析、機械学習などのアルゴリズムが多数用意されています。
■ OpenCVを使った動体追跡の例 (OpenCV Google Summer of Code 2015)
https://www.youtube.com/watch?v=OUbUFn71S4s
■ インストールと簡単な使い方はこちら
OpenCV 3(core + contrib)をPython 3の環境にインストール&OpenCV 2とOpenCV 3の違い&簡単な動作チェック
★ モーションテンプレートを実行するために、core + opencv_contrib をインストールしてください。
■ 静止画像のフィルター処理についてはこちら
OpenCVでエッジ検出してみる
OpenCVで各種フィルター処理をする(グラディエント、ハイパス、ラプラシアン、ガウシアン)
OpenCVで特徴点を抽出する(AgastFeature, FAST, GFTT, MSER, AKAZE, BRISK, KAZE, ORB, SimpleBlob)
■ 動画ファイルの処理についてはこちら
OpenCVで動画をリアルタイムに変換してみる
OpenCVでWebカメラ/ビデオカメラの動画をリアルタイムに変換してみる
OpenCVでオプティカルフローをリアルタイムに描画する(Shi-Tomasi法、Lucas-Kanade法)
OpenCVを使った物体追跡(マウスで指定した特徴点をLucas-Kanade法で追跡する
簡単な説明
「モーション テンプレート」もOpenCVを使えば簡単に実現することができます。
今回利用するOpenCVのメソッドを簡単にまとめておきます。
ID | メソッド | 概要 |
---|---|---|
(a) | cv2.motempl.updateMotionHistory() | モーション画像の更新 |
(b) | cv2.motempl.calcMotionGradient() | モーション画像から各座標に対して方向を計算 |
(c) | cv2.motempl.calcGlobalOrientation() | モーション画像の全体の方向を計算 |
今回は、(a)(b)(c)全てを使ったプログラムを作成しますが、必要なもののみ取捨選択することも可能です。例えば、モーション画像のみ欲しければ(b)(c)は不要です。
今回のプログラムの主な機能は以下になります。
- モーションを検出しグレースケールで表示します。
- 新しいモーションを強調し、古いモーションは徐々に薄く表示します。
- 各座標のモーションの方向を緑色の線で表示します。
- 画面全体のモーションの方向を黄色い線で表示します。
プログラム
-
動作環境
- python: 3.5.1
- OpenCV(core + contrib): 3.1.0
-
動画データ
プログラムではOpenCVに付属しているサンプル動画を利用します。
OpenCV\opencv\sources\samples\data\768x576.avi
import time
import math
import cv2
import numpy as np
# ビデオデータ
VIDEO_DATA = "768x576.avi"
# Esc キー
ESC_KEY = 0x1b
# モーションの残存期間(sec)
DURATION = 1.0
# 全体の方向を表示するラインの長さ
LINE_LENGTH_ALL = 60
# 座標毎の方向を表示するラインの長さ
LINE_LENGTH_GRID = 20
# 座標毎の方向を計算する間隔
GRID_WIDTH = 40
# 方向を表示するラインの丸の半径
CIRCLE_RADIUS = 2
# 表示ウィンドウの初期化
cv2.namedWindow("motion")
# ビデオデータの読み込み
video = cv2.VideoCapture(VIDEO_DATA)
# 最初のフレームの読み込み
end_flag, frame_next = video.read()
height, width, channels = frame_next.shape
motion_history = np.zeros((height, width), np.float32)
frame_pre = frame_next.copy()
while(end_flag):
# フレーム間の差分計算
color_diff = cv2.absdiff(frame_next, frame_pre)
# グレースケール変換
gray_diff = cv2.cvtColor(color_diff, cv2.COLOR_BGR2GRAY)
# 2値化
retval, black_diff = cv2.threshold(gray_diff, 30, 1, cv2.THRESH_BINARY)
# プロセッサ処理時間(sec)を取得
proc_time = time.clock()
# モーション履歴画像の更新
cv2.motempl.updateMotionHistory(black_diff, motion_history, proc_time, DURATION)
# 古いモーションの表示を経過時間に応じて薄くする
hist_color = np.array(np.clip((motion_history - (proc_time - DURATION)) / DURATION, 0, 1) * 255, np.uint8)
# グレースケール変換
hist_gray = cv2.cvtColor(hist_color, cv2.COLOR_GRAY2BGR)
# モーション履歴画像の変化方向の計算
# ※ orientationには各座標に対して変化方向の値(deg)が格納されます
mask, orientation = cv2.motempl.calcMotionGradient(motion_history, 0.25, 0.05, apertureSize = 5)
# 各座標の動きを緑色の線で描画
width_i = GRID_WIDTH
while width_i < width:
height_i = GRID_WIDTH
while height_i < height:
cv2.circle(hist_gray, \
(width_i, height_i), \
CIRCLE_RADIUS, \
(0, 255, 0), \
2, \
16, \
0)
angle_deg = orientation[height_i - 1][width_i - 1]
if angle_deg > 0:
angle_rad = math.radians(angle_deg)
cv2.line(hist_gray, \
(width_i, height_i), \
(int(width_i + math.cos(angle_rad) * LINE_LENGTH_GRID), int(height_i + math.sin(angle_rad) * LINE_LENGTH_GRID)), \
(0, 255, 0), \
2, \
16, \
0)
height_i += GRID_WIDTH
width_i += GRID_WIDTH
# 全体的なモーション方向を計算
angle_deg = cv2.motempl.calcGlobalOrientation(orientation, mask, motion_history, proc_time, DURATION)
# 全体の動きを黄色い線で描画
cv2.circle(hist_gray, \
(int(width / 2), int(height / 2)), \
CIRCLE_RADIUS, \
(0, 215, 255), \
2, \
16, \
0)
angle_rad = math.radians(angle_deg)
cv2.line(hist_gray, \
(int(width / 2), int(height / 2)), \
(int(width / 2 + math.cos(angle_rad) * LINE_LENGTH_ALL), int(height / 2 + math.sin(angle_rad) * LINE_LENGTH_ALL)), \
(0, 215, 255), \
2, \
16, \
0)
# モーション画像を表示
cv2.imshow("motion", hist_gray)
# Escキー押下で終了
if cv2.waitKey(20) == ESC_KEY:
break
# 次のフレームの読み込み
frame_pre = frame_next.copy()
end_flag, frame_next = video.read()
# 終了処理
cv2.destroyAllWindows()
video.release()
実行結果
- 歩いている人影がモーションテンプレートで表示されます。
- 緑色の線で各グリッドがどちらの方向に移動しているのか表示しています。
- 黄色い線は画面全体としてどちらの方向に移動しているのかを表示しています。
全体の方向というのが少し分かりづらいですが、固定フレームであれば、画面内の物体が全体としてどちらの方向に移動しているのかを表しています。フレーム全体が移動しているようなケースでは、フレームの移動方向を表すことになります。
方向情報を使った動きの解析
モーションテンプレートを使い、どのように動きを解析するかといった実例として、錦織圭のショットの瞬間を解析してみます。
モーションテンプレート情報から計算した方向情報を使った解析を行います。
オリジナル画像に対して、モーションテンプレートのプログラムを実行してみます。
モーション画像では、フレーム毎のボールの軌跡を見てとることができます(モーション画像1、モーション画像2)。
ボールの数を数えてみると、モーション2画像はモーション1画像に対して3フレーム分進んでいます。
モーション画像1、モーション画像2に対し、各グリッドの方向を分析してみると、以下のような動きを読み取ることができます。
- モーション1画像では停止していた右肩がモーション2画像では右上方向に動いている。
- モーション1画像では停止していた右ふくらはぎがモーション2画像では下方向に動いている。
- モーション1画像では停止していた左膝がモーション2画像では上方向に動いている。
- モーション1画像では後ろ方向に移動していた腰がモーション2画像では右上方向に動いている。
- 左足の脛から先、左右肩甲骨の中心、頭部はモーション1画像、モーション2画像ともに停止していている。
- 全体的には、左半身を軸として固定し、右半身を伸ばしてボールを打ち返しています。
テレビで見ると、全身でジャンプし、ボールを打っているように見えますが、この動画で実際にボールを打つ瞬間は、左半身でしっかりと軸を作った上で、右半身のバネを使った打ち方をしていることが分かります。
スポーツをされる方は、プロの動きを解析してみたり、自分が成功したときと失敗したときの動きの違いを解析してみたりすると面白いのではないでしょうか。