動画内の物体を自動的に追跡(トラッキング)する事ができれば、高価な計測システムを使わなくても変位や速度を知る事が出来ます。ここではPythonのOpenCVを使って動画内の物体検知を行う例を紹介します。
こんにちは。wat(@watlablog)です。ここではパターンマッチングを使って物体トラッキングをする例を紹介します!
物体検出・トラッキングの例
当ブログでは過去にいくつか物体検知、トラッキングのプログラムを試してきました。
本記事ではまた違った方法で物体トラッキングをしようとしていますが、まずは過去のおさらいをしてみましょう。
差分で動体検知した例
まずは動画の各フレーム差分を使って動体検知をしてみた例です。詳細は「Python/OpenCVで動体検知!動画の動いている部分を検出」という記事に記載しましたが、この記事に記載のコードを使うと以下の動画に示す処理を行う事ができます。
動いていない物体は黒く、動いている物体は白くなります。何かのトリガとする処理をしたい時等には便利ですが、この動体検知では物体がどちらの方向に動いているかといった情報はわかりません。
輪郭情報でトラッキングした例
「Python/OpenCVで動画内の物体トラッキングをする方法」という記事では輪郭抽出を使って物体を検出し、トラッキング(追跡)するコードを紹介しています。
結果の例は以下の動画です。緑の丸い輪郭線が検出結果ですが、当ブログのロゴマークを検出し続けています。
この輪郭線は幾何学的な情報を持っているので、座標を割り出す事が出来ます。以下のグラフは座標情報から\(xy\)情報を抽出してプロットした例です。
この方法であれば移動量の数値化ができ、信号処理をする事も可能なため、工学的な用途に使用する事もできるでしょう。
しかし、背景がいつも黒く(もしくは容易に二値化できる等)、輪郭がいつも綺麗に抽出できるとは限りません。なかなか理想的な状況は少ないのではと思っていました。
パターンマッチングでPIVをした例
「Python/OpenCVでPIV計測!粒子移動をベクトル化する」という記事では、PIV(粒子画像速度計測法)プログラムを作ってみました。
本来PIVは流体工学の分野で流れを可視化する事に使われますが、以下のように物体の各部の挙動をベクトル化する事が可能です。
このベクトルは大きさや角度といった数値情報を持っていますので、その数値を適切に処理する事で物体の動きを分析する事も出来ます。
しかし、このPIVは画像内全域でパターンマッチングを行っているため、単一の物体をトラッキングする場合はかなり処理に無駄が出てしまうのがネックです。また、複数のベクトルから着目する物体の動きのみを計算するには、さらに複雑なコードが必要になりそうです。
以下のように動く物体が一つでも、検出されるベクトルはサーチウィンドウのサイズによってしまいます。テンプレート画像を用意する手間が無いというのはメリットの一つですが、時系列分析したい時等はやっかい。
本記事の目標
題材:楕円軌道の動画
今回は以下の単一点の動きをトラッキングして数値化する事を目標とします。
ちなみにこの動画は「Pythonのmatplotlibアニメーションで楕円の軌跡!」という記事で作り方を紹介していますので、是非ご覧下さい。
コードの方針
基本は「Python/OpenCVでPIV計測!粒子移動をベクトル化する」で書いたPIVコードを使います。しかし単一の物体トラッキングにとっては画像の全域サーチは無駄が多いので、ベクトルは1本しか計算しないように作る方針とします。
ベクトルを1本しか計算しなければ、素直にそのベクトルの情報が物体の動きを表現している事になるからです。
本記事は画像処理やプログラミングの専門教育を受けた事が無い素人がライブラリの力を借りてコードを書いています。至らぬ点はあると思いますので、ご使用は自己責任でお願いします。
それでは早速作ってみたコードを紹介します。
Pythonパターンマッチングで物体トラッキングするコード
事前準備:テンプレート画像を用意するために
今回はテンプレート画像を使って物体トラッキングを行います。テンプレート画像は解析対象の画像から抽出するのが手っ取り早いので、元画像を抽出して使用します。
movieフォルダを.py実行フォルダの下に作成し、その中に動画を連番画像にした解析対象の画像を入れてください。
そして、以下のコードを実行すると最初の画像が読み込まれ、matplotlibを使って表示されます。はい、単に画像表示をしているだけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import cv2 import os import glob from matplotlib import pyplot as plt # 画像表示する関数 def preview_img(dir): path_list = sorted(glob.glob(os.path.join(*[dir, '*']))) # ファイルパスをソートしてリストする img = cv2.imread(path_list[0], 1) # 画像をカラーで読み込み img = cv2.cvtColor(img, code=cv2.COLOR_BGR2RGB) # OpenCVのカラーGBRをRGBに変換 # ここからグラフ描画------------------------------------- # フォントの種類とサイズを設定する。 plt.rcParams['font.size'] = 14 plt.rcParams['font.family'] = 'Times New Roman' # 目盛を内側にする。 plt.rcParams['xtick.direction'] = 'in' plt.rcParams['ytick.direction'] = 'in' # グラフの上下左右に目盛線を付ける。 fig = plt.figure() ax1 = fig.add_subplot(111) ax1.yaxis.set_ticks_position('both') ax1.xaxis.set_ticks_position('both') # 画像の表示 ax1.imshow(img) ax1.axis('off') # レイアウトタイト設定 fig.tight_layout() # 表示 plt.show() plt.close() return # PIV解析の関数を実行 preview_img(dir='movie') |
マウスカーソルをプロット画面に持ってくると、プロット画面の右下にx座標とy座標が表示されます。トラッキングしたい物体の付近を四角く囲うように、左上と右下の座標をメモります(ここはまだアナログ…)。
本当は以下の外部サイトにあるようなGUI上で範囲指定して選択する方法をとりたかった…。ちょっと自分にはハードルが高いのでまた今度。
Qiita:【python】マウスドラッグで画像から範囲指定する
全コード
以下に全コードを示します。詳細はブログ用に細かくコード内コメントを残しましたのでご確認下さい。
「Python/OpenCVでPIV計測!粒子移動をベクトル化する」と同じように、予めmovieフォルダに入れておいた解析用画像を使って物体トラッキング解析を行い、結果の画像をtracking_outフォルダに入れるというものです。
ini_startとini_endに先ほどのテンプレート画像の座標を入力して使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
import cv2 import os import glob import numpy as np from PIL import Image from matplotlib import pyplot as plt # トラッキング解析をする関数 def tracking(dir, out_dir, ini_start, ini_end): count = 0 coordinates = [] # 座標情報を格納する空配列 correlation_coef = [] # 相関係数情報を格納する空配列 vector_amps = [] # ベクトル振幅を格納する空配列 path_list = sorted(glob.glob(os.path.join(*[dir, '*']))) # ファイルパスをソートしてリストする img0 = cv2.imread(path_list[0], 0) # 初期画像を読み込み template = img0[ini_start[1]:ini_end[1], ini_start[0]:ini_end[0]] # 初期画像からテンプレート画像を抽出 w = (ini_end[0] - ini_start[0]) # テンプレート画像の幅 h = (ini_end[1] - ini_start[1]) # テンプレート画像の高さ x0 = int(ini_start[0] + w / 2) # 初期x y0 = int(ini_start[1] + h / 2) # 初期y # 全画像ファイルをリストしてトラッキング計算を実施 for i in range(len(path_list)-1): # len(path_list)-1 count += 1 img = cv2.imread(path_list[i], 0) # グレースケール画像でi+1番目の画像を読み込み # パターンマッチングから移動先座標を計算 method = cv2.TM_CCOEFF_NORMED # NCC(正規化相互相関係数)を選択 res = cv2.matchTemplate(img, template, method) # img[i+1]に対するテンプレートマッチングの結果 min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) # 最小値と最大値、その位置を取得 # i+1番目とi番目の差(移動量)を計算 dx = (max_loc[0] + w/2) - x0 dy = (max_loc[1] + h/2) - y0 # ベクトル振幅 vector_amp = np.sqrt(dx ** 2 + dy ** 2) # 情報を格納 coordinates.append(max_loc) # 座標 correlation_coef.append(max_val) # 相関係数 vector_amps.append(vector_amp) # ベクトル振幅 # ここからグラフ描画--------------------------------------------------------------- # フォントの種類とサイズを設定する。 plt.rcParams['font.size'] = 14 plt.rcParams['font.family'] = 'Times New Roman' # 目盛を内側にする。 plt.rcParams['xtick.direction'] = 'in' plt.rcParams['ytick.direction'] = 'in' # グラフの上下左右に目盛線を付ける。 fig = plt.figure() ax1 = fig.add_subplot(111) ax1.yaxis.set_ticks_position('both') ax1.xaxis.set_ticks_position('both') # 背景画像の用意と表示設定 img = cv2.cvtColor(img, code=cv2.COLOR_BGR2RGB) ax1.imshow(img) # 背景画像 cm = plt.get_cmap('jet') # カラーマップ vsf = 2 # ベクトル表示のスケールファクタ # ベクトルをプロットする ax1.arrow(x=x0, y=y0, dx=vsf * dx, dy=vsf * dy, width=0.01, head_width=10) ax1.axis('off') # レイアウトタイト設定 #fig.tight_layout() # out_dirで指定したフォルダが無い時に新規作成 if os.path.exists(out_dir): pass else: os.mkdir(out_dir) # 画像保存パスを連番で準備 path = os.path.join(*[out_dir, str("{:05}".format(count)) + '.png']) # 画像を保存する plt.savefig(path) #plt.show() plt.close() # 画像作成の進捗を表示 print(count, '/', len(path_list) - 1) # x0,y0を更新 x0 = int(max_loc[0] + w / 2) y0 = int(max_loc[1] + w / 2) return vector_amps # GIFアニメーションを作成 def create_gif(in_dir, out_filename): path_list = sorted(glob.glob(os.path.join(*[in_dir, '*']))) # ファイルパスをソートしてリストする imgs = [] # 画像をappendするための空配列を定義 # ファイルのフルパスからファイル名と拡張子を抽出 for i in range(len(path_list)): img = Image.open(path_list[i]) # 画像ファイルを1つずつ開く imgs.append(img) # 画像をappendで配列に格納していく # appendした画像配列をGIFにする。durationで持続時間、loopでループ数を指定可能。 imgs[0].save(out_filename, save_all=True, append_images=imgs[1:], optimize=False, duration=100, loop=0) def velocity_analysis(vectors): x = np.arange(0, len(vectors), 1) # ここからグラフ描画--------------------------------------------------------------- # フォントの種類とサイズを設定する。 plt.rcParams['font.size'] = 14 plt.rcParams['font.family'] = 'Times New Roman' # 目盛を内側にする。 plt.rcParams['xtick.direction'] = 'in' plt.rcParams['ytick.direction'] = 'in' # グラフの上下左右に目盛線を付ける。 fig = plt.figure() ax1 = fig.add_subplot(111) ax1.yaxis.set_ticks_position('both') ax1.xaxis.set_ticks_position('both') # 軸のラベルを設定する。 ax1.set_xlabel('Frame [-]') ax1.set_ylabel('displacement [pix] ') # プロット ax1.plot(x, vectors, color='red') # レイアウトタイト設定 fig.tight_layout() plt.show() plt.close() # トラッキング解析の関数を実行 vector_amps = tracking(dir='movie', out_dir='tracking_out', ini_start=[240, 130], ini_end=[270, 160]) # GIFアニメーションを作成する create_gif(in_dir='tracking_out', out_filename='tracking.gif') velocity_analysis(vector_amps) |
コードを実行するとtracking.gifが作成されます。
今回の動画の場合の結果を以下に示します。単一の動点の上にベクトルが重なっている事がわかります。
以下はベクトルの大きさを時系列で並べた結果を示しています。ちょっと粗い?
まとめ
本記事は物体検知や輪郭検出による物体トラッキング、PIVによる動画内動体のベクトル化を紹介しました。
PIVでは画像内全域を走査するため、テンプレート画像が必要無いメリットがありますが、ある一点の動体の数値化には苦労します。
一方、今回作った単一のテンプレート画像のみを使って物体トラッキングをすると、テンプレート画像を用意する手間はありますが、単一動点の数値化は大変楽になりました。
どちらも一長一短ですが、武器は多いに越したことはありません。
テンプレートマッチングは色々使えますね!Twitterでも関連情報をつぶやいているので、wat(@watlablog)のフォローお待ちしています!