OpenCVのcalcOpticalFlowPyrLK関数を使った光流法による動き検出

概要

OpenCVのcalcOpticalFlowPyrLKという機能は、Lucas-Kanade法という方法を使って、ビデオの中で物体がどう動いているかを検出することができます。これは、ビデオの一つの画面から次の画面へと移るときに、物体がどう動いたかを推測する方法です。これは、ビデオの中で何が起こっているかを理解するのにとても役立ちます。特に、物体がどこに移動したかを追跡したり、ビデオを小さくする(圧縮する)ために使われます。

「calcOpticalFlowPyrLK」の機能は、ビデオの一つの画面で特に注目すべき点(特徴点)を見つけ、それが次の画面でどう動いたかを計算します。これにより、物体がどのように動いているかのパターンを理解し、それをもとに物体がこれからどう動くかを予測したり、ビデオの中で何が起こっているかを分析したりすることができます。

使用する画像

ファイル:windmill_-_112957 (540p).mp4

出力結果

プログラム全体

#解説1 import cv2 import numpy as np #解説2 # ShiTomasiのコーナー検出のためのパラメータ fparams = dict( maxCorners = 100, qualityLevel = 0.3, minDistance = 7, blockSize = 7 ) # lucas kanadeのオプティカルフローのためのパラメータ lparams = dict( winSize = (15,15), maxLevel = 2, criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)) #解説3 # ランダムな色を生成 color = np.random.randint(0,255,(100,3)) # ビデオを読み込む cap = cv2.VideoCapture('windmill_-_112957 (540p).mp4') # 最初のフレームを取得し、それに含まれるコーナーを見つける ret, old_frame = cap.read() old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY) p0 = cv2.goodFeaturesToTrack(old_gray, mask = None, **fparams) # 描画用のマスク画像を作成 mask = np.zeros_like(old_frame) #解説4 while(1): ret,frame = cap.read() if not ret or frame is None: break frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # オプティカルフローを計算 p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lparams) # 良い点を選択 good_new = p1[st==1] good_old = p0[st==1] # トラックを描画 for i,(new,old) in enumerate(zip(good_new,good_old)): a,b = new.ravel() c,d = old.ravel() a,b,c,d = int(a), int(b), int(c), int(d) # 座標を整数に変換 mask = cv2.line(mask, (a,b),(c,d), color[i].tolist(), 2) frame = cv2.circle(frame,(a,b),5,color[i].tolist(),-1) img = cv2.add(frame,mask) # 画像を表示 cv2.imshow('Optical Flow', img) if cv2.waitKey(1) & 0xFF == ord('q'): break # 前のフレームと前の点を更新 old_gray = frame_gray.copy() p0 = good_new.reshape(-1,1,2) cap.release() cv2.destroyAllWindows()

解説

解説1: ライブラリのインポート

import cv2 import numpy as np

この部分では、プログラムで使用するライブラリをインポートしています。cv2はOpenCVという画像処理ライブラリで、画像や動画の読み込み、加工、表示などを行うために使用します。numpyは数値計算ライブラリで、配列や行列の操作、数学的な計算などを行うために使用します。これらのライブラリをインポートすることで、その後のコードでこれらのライブラリの機能を利用できます。

解説2: パラメータの設定

# ShiTomasiのコーナー検出のためのパラメータ fparams = dict( maxCorners = 100, qualityLevel = 0.3, minDistance = 7, blockSize = 7 ) # lucas kanadeのオプティカルフローのためのパラメータ lparams = dict( winSize = (15,15), maxLevel = 2, criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))

このプログラムでは、2つの主要な部分、ShiTomasiのコーナー検出とLucas-Kanadeのオプティカルフロー計算のためのパラメータを設定しています。

ShiTomasi このアルゴリズムは写真の中で色や形が大きく変わるコーナーを検出しています。ShiTomasiの方法は、写真の各ピクセルを見て、その周囲がどれだけ特徴があるのかを数値で評価します。そして、最も特徴のあるピクセル(つまり、最も高い評価を得たピクセル)をコーナーとして選びます。下記がそれぞれの引数に関する説明です。
maxCorners 画像から検出するコーナー(特徴点)の最大数を指定します。ここでは、最大100点のコーナーを検出するように設定しています。
qualityLevel コーナーの品質レベルを指定します。この値は0から1の間で、値が大きいほど良いコーナー(より鮮明な特徴を持つ点)だけを検出します。ここでは、0.3と設定しています。
minDistance 検出するコーナー間の最小距離を指定します。ここでは、7ピクセルと設定しており、7ピクセルよりも近いコーナーは無視されます。
blockSize 使用する近傍領域のサイズを指定します。ここでは、7x7のサイズを使用しています。
Lucas-Kanade このアルゴリズムは動画の連続する2つのフレーム間で物体がどのように動いたかを検出しています。例えば、ゲームでキャラクターが画面上を動くとき、その動きを追跡するのに役立ちます。Lucas-Kanadeの方法は、最初のフレームで見つけたコーナー(ShiTomasiの方法で見つけたもの)が次のフレームでどこに移動したかを計算します。これにより、物体の動きのパターンを理解し、それに基づいて予測や分析を行うことができます。下記がそれぞれの引数に関する説明です。
winSize 検出するオプティカルフローを計算するための窓のサイズを指定します。ここでは、15x15のサイズを使用しています。
maxLevel 使用する画像ピラミッドのレベルを指定します。画像ピラミッドとは、元の画像から縮小版を作成することで、さまざまなスケールで特徴を検出できるようにするものです。ここでは、2レベルと設定しています。例えば、"maxLevel"が0だと、元の画像だけを使って動きを追跡します。"maxLevel"が1だと、元の画像とその半分のサイズの画像の2つを使って動きを追跡します。言い換えると、"maxLevel"が大きいほど、より多くのスケールの画像を使って動きを追跡することができます。これにより、大きな動きも小さな動きも捉えることができるようになります。ただし、"maxLevel"が大きすぎると計算量が増えて処理が遅くなるので、適切な値を設定する必要があります。
criteria アルゴリズムの終了条件を指定します。ここでは、10回の反復計算または誤差が0.03以下になったら終了するように設定しています。

解説3: 初期設定と最初のフレームの処理

# ランダムな色を生成 color = np.random.randint(0,255,(100,3)) # ビデオを読み込む cap = cv2.VideoCapture('video.avi') # 最初のフレームを取得し、それに含まれるコーナーを見つける ret, old_frame = cap.read() old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY) p0 = cv2.goodFeaturesToTrack(old_gray, mask = None, **fparams) # 描画用のマスク画像を作成 mask = np.zeros_like(old_frame)

ここでは、プログラムの初期設定と最初のフレームの処理を行っています。まず、color = np.random.randint(0,255,(100,3))でランダムな色を生成し、cap = cv2.VideoCapture('video.avi')でビデオを読み込んでいます。次に、ret, old_frame = cap.read()で最初のフレームを取得し、そのフレームからコーナー(特徴的な点)を検出し、cv2.cvtColorでモノクロ変換します。

次に、p0 = cv2.goodFeaturesToTrack(old_gray, mask = None, **fparams)では、グレースケールに変換したフレームから「特徴的な点」(コーナー)を見つけています。これらの点は、物体の形状を表すのに役立ち、また動きを追跡するのにも使われます。最後にmask = np.zeros_like(old_frame)で、old_frameと同じ形状とデータ型を持つ、すべての要素が0の配列(つまり黒い画像)を作成しています。これらの処理は、この後のフレーム間のオプティカルフローの計算や描画のためで使用されます。

解説4: フレーム間のオプティカルフローの計算と描画

while(1): ret,frame = cap.read() if not ret or frame is None: break frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # オプティカルフローを計算 p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lparams) # 良い点を選択 good_new = p1[st==1] good_old = p0[st==1] # トラックを描画 for i,(new,old) in enumerate(zip(good_new,good_old)): a,b = new.ravel() c,d = old.ravel() a,b,c,d = int(a), int(b), int(c), int(d) # 座標を整数に変換 mask = cv2.line(mask, (a,b),(c,d), color[i].tolist(), 2) frame = cv2.circle(frame,(a,b),5,color[i].tolist(),-1) img = cv2.add(frame,mask) # 画像を表示 cv2.imshow('Optical Flow', img) if cv2.waitKey(1) & 0xFF == ord('q'): break # 前のフレームと前の点を更新 old_gray = frame_gray.copy() p0 = good_new.reshape(-1,1,2)

この部分では、ビデオの各フレーム間でのオプティカルフロー(物体の動き)を計算し、その結果を描画しています。まず、新しいフレームを読み込み、そのフレームでのオプティカルフロー(ピクセルの動き)を計算しています。次に、計算結果から良い点(追跡すべき点)を選択し、それらの点の移動を描画しています。最後に、描画結果を表示し、前のフレームと前の点を更新しています。これらの処理を繰り返すことで、ビデオ全体でのオプティカルフローを計算し、その結果をリアルタイムで描画しています。

解説5: リソースの解放

cap.release() cv2.destroyAllWindows()

この部分では、プログラムの最後にリソースを解放しています。cap.release()はビデオキャプチャのリソースを解放し、cv2.destroyAllWindows()は作成したウィンドウを全て閉じるための関数です。これらの処理を行うことで、プログラムが終了したときにリソースが適切に解放され、メモリリーク等の問題を防ぎます。

おまけ

産業用カメラは高フレームレートをサポートしており、グローバルシャッターという撮影方式を採用したモデルもあります。これにより、高速に動く対象でも歪みなくクリアに捉えることが可能です。一方、Webカメラのフレームレートは大体60fps程度で、高速に動く対象を撮影すると、ブラー(ぼやけ)や歪みが発生することがあります。 なお、TheImagingSource社の下記のUSBカメラでは、解像度を256×24に設定することで、フレームレートを最大6310fpsまで上げて撮影することが可能です。これにより、非常に高速な動きを捉えることができます。
https://www.argocorp.com/cam/usb3/tis/DxK33UP1300.html