【Python】OpenCVで物体の追跡 – Lucas-Kanade法を使ったOptical Flow

Pythonの応用
スポンサーリンク

OpenCVを使ったPythonでの画像処理について、物体の追跡(Object Tracking)について扱っていきます。

ここではオプティカルフロー(Optical Flow)の概念とWebカメラを使ってのLucas-Kanade法による物体の追跡を行ってみようと思います。

ここではテキストエディタを使ってコードを書き、ターミナルの対話型シェルから実行して行こうと思います。

スポンサーリンク

オプティカルフローとは

オプティカルフローとは、視覚表現(物体やカメラの移動によって生じる隣接フレーム)の中で物体の動きをベクトルで表したものです。

各ベクトルの1フレーム目から2フレーム目への移る変位ベクトルを、2次元ベクトル場で表現します。

参考までに、英文のウィキペディアでの解説がこちらです。

Optical flow - Wikipedia

数学的な説明には触れませんが、オプティカルフローは次のような仮定に基づいて計算されます。

  • 連続フレーム間で物体の画像上の明るさは変わらない
  • 隣接する画素は似たような動きをする

画像中の画素を考え、わずかな時間t後に撮影された次の画像の画素がそれだけ距離を移動したとして、この二つの画素は同じものとし、明るさは変わらないと仮定して計算したものがオプティカルフローです。この計算には未知のものが残るのですが、この一つの解決法がLucas-Kanade法となります。

スポンサーリンク

OpenCVのLucas-Kanade法で物体追跡

Lucas-Kanade法は、ある点に対してその点を含む周囲の3×3に含まれる9画素が同じ動きをしていると仮定し、この9画素の情報を基に注目点の位置の移動を計算します。

ただし、小さい運動を扱うのと、大きな動きがあった場合は観測に違いがあるので、画像のピラミッドの概念を使います。

参考までに、英文のウィキペディアでの画像のピラミッドの解説がこちらです。

Pyramid (image processing) - Wikipedia

画像のピラミッドとは、オリジナルの画像からレベルをピラミッド頂点へズラすようにスケールをアップすると、小さな動きが消され大きな動きが小さな動きとして観測することができます。Lukas-Kanade法をこの解像度で適用することで、そのスケールでのオプティカルフローを得ることができます。

というイメージですが、細かい数学的な素養が無いと深い理解はできないので、そのあたりは関心があるかたは別で勉強してください。検索すると色々な解説が出てきます。

ここではコードを書いて実際にwebカメラで物体追跡をやってみましょう。

import cv2
import numpy as np

まずはOpenCVとNumpyのインポート。お約束の作業です。

次にパラメータを設定して行きます。

corner_track_params = dict(maxCorners = 10,
                       qualityLevel = 0.3,
                       minDistance = 7,
                       blockSize = 7 )

物体のコーナーを検出して動きを追跡する為にShi-Tomasiコーナー検出を使う為のパラメータの設定です。

Shi-Tomasiコーナー検出はこちらで扱いました。

【Python】OpenCVのHarrisコーナー検出とShi-Tomasiコーナー検出
OpenCVを使ったPythonでの画像処理について、ここではコーナー検出を学びます。Harrisコーナー検出でcornerHarris()を、Shi-Tomasiコーナー検出でgoodFeaturesToTrack()を扱います。

あとで出てきますが、goodFeaturesToTrack()でコーナを検出しますが、これに画像を渡し、ここでは辞書型のデータとして、検出したいコーナーの数、0から1の間の値の検出するコーナーの最低限の質、検出される2つのコーナー間の最低限のユークリッド距離を設定します。

次に、Lucas-Kanade法を使ったオプティカルフローのパラメータを設定していきます。あとで出てきますが、OpenCVはLucas-Kanade法の全ての処理を行うcalcOpticalFlowPyrLK()関数が用意されています。このパラメータを設定しておきます。

lk_params = dict(winSize = (200,200),
                 maxLevel = 2,
                 criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10,0.03))

winSize引数を変更して、特定の点の動きや集約された領域の動きを検出します。小さくするとノイズに敏感になり、大きな動きを見逃す可能性があります。

maxLevelは画像のピラミッドのことで、ここが0の場合、ピラミッドを使用しないことを意味します。ピラミッドを使用すると、画像のさまざまな解像度でオプティカルフローを見つけることができます。

criteriaで繰り返し処理の終了条件を与えています。この条件が満たされた時にアルゴリズムの繰り返し計算が終了します。cv2.TERM_CRITERIA_EPSは指定された精度(epsilon)に到達したら繰り返し計算を終了します。 cv2.TERM_CRITERIA_COUNTは指定された繰り返しの最大回数(count)に到達したら繰り返し計算を終了します。ここではこれらのどちらかの条件が満たされた時に繰り返し計算の終了を指定しています。ここでは回数を10、精度を0.03にしています。多くの反復はより徹底的な検索を意味し、小さな精度は処理を早く終わらせることを意味します。

cap = cv2.VideoCapture(0)

ret, prev_frame = cap.read()

prev_gray = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)

prevPts = cv2.goodFeaturesToTrack(prev_gray, mask = None, **corner_track_params)

mask = np.zeros_like(prev_frame)

cv2.VideoCapture(0)でwebカメラに接続します。ここではMac内臓のデフォルトのカメラに接続しているので0を指定します。

cap.read()で映像を読み込んで、cv2.cvtColor()を使ってグレースケールの変換しています。これを直前のイメージとして理解することにします。

cv2.goodFeaturesToTrack()で物体のコーナーを検出(Shi-Tomasiコーナー検出)しています。ここで先ほどの設定したパラメータを使っています。

あとで検出を描画するために、直前のイメージのフレームにマッチするmaskをnp.zeros_like()を使って作成します。このnp.zeros_like()は、np.zeros()のように第一引数にshapeで型を渡すのではなく、shapeを真似たい配列を指定することで、np.zerosと同じように0の配列を作ってくれます。

次は、wile文でwebカメラでのキャプチャーを実行して行きます。

while True:
    
    ret,frame = cap.read()
    
    frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    
    nextPts, status, err = cv2.calcOpticalFlowPyrLK(prev_gray, frame_gray, prevPts, None, **lk_params)
    
    good_new = nextPts[status==1]
    good_prev = prevPts[status==1]
    
    for i,(new,prev) in enumerate(zip(good_new,good_prev)):
        
        x_new,y_new = new.ravel()
        x_prev,y_prev = prev.ravel()
        
        mask = cv2.line(mask, (x_new,y_new),(x_prev,y_prev), (0,255,0), 3)
        
        frame = cv2.circle(frame,(x_new,y_new),8,(0,0,255),-1)
    
    img = cv2.add(frame,mask)
    cv2.imshow('frame',img)
    
    if cv2.waitKey(30) & 0xff == 27:
        break
   
    prev_gray = frame_gray.copy()
    prevPts = good_new.reshape(-1,1,2)
    
    
cap.release()
cv2.destroyAllWindows()

while文を使って、キャプチャーを実行し、read()で読み込みます。retはデータの読み込みの可否のture/false、frameで映像のパラメータを取得しています。

cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)で映像をグレースケールに変換します。ここではframe_grayとしておきます。

ここでcv2.calcOpticalFlowPyrLK()を使ってグレースケールのフレーム上のオプティカルフローを計算します。この関数の引数に、直前のイメージ、直後のイメージ、直前に検出したポイント、直後のポイント(ここではNone)、最初の方で準備した辞書型のパラメータを順に渡して計算します。

返された新しいポイントと直前のポイントの配列を使います。対応する特徴のオプティカルフローが見つかった場合はベクトルの各要素は1に設定され、それ以外の場合は0に設定されます。これをそれぞれgood_new、good_prevにします。

zip関数を使うと複数のシーケンスを並列に反復処理することができるので、good_new、good_prevを同時に処理します。これをfor-inループを使ってenumrate()でインデックスと新旧の検出ポイントを取り出します。

これをravel()で1次元化してx、yの座標に代入する処理を、新しいポイントと直前のポイントで行っています。

最初のフレームから作成したマスクを使用してcv2.line()で移動の線を描画していきます。新しい点から直前の点に向けて線を描くことで軌跡のように描けるので、そういう渡し方をしています。色は緑色、太さを3にしました。こちらはmask側に描いていることに注意です。

cv2.circle()で新規のポイントに円を描きますが、こちらはframe側に描きます。半径8、色は赤、塗りつぶしなので-1を指定しています。

cv2.add()を使って画像の足し算します。frameをmask与えて点と線の描画を合成しています。これをimshow()で表示します。

if cv2.waitKey(30) & 0xff == 27: で[esc]キーを押された時にbreak処理をします。

直前のフレームと直前のポイントを更新します。frame_gray.copy()でフレームの更新、reshape()でポイントの更新をしています。good_new.reshape(-1,1,2)は、-1で元の形に戻し1行2列の型にしています。

ここまでがwhile文の処理です。

cap.release()でデバイスを解放し、cv2.destroyAllWindows()で全て終了させます。

コードはここまでです。

スポンサーリンク

全体のコード(再掲)

全体のコードを再度表すと、次のようになります。

import cv2
import numpy as np

corner_track_params = dict(maxCorners = 10,
                       qualityLevel = 0.3,
                       minDistance = 7,
                       blockSize = 7)

lk_params = dict(winSize = (200,200),
                 maxLevel = 2,
                 criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10,0.03))

cap = cv2.VideoCapture(0)

ret, prev_frame = cap.read()

prev_gray = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)

prevPts = cv2.goodFeaturesToTrack(prev_gray, mask = None, **corner_track_params)

mask = np.zeros_like(prev_frame)


while True:

    ret,frame = cap.read()

    frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    nextPts, status, err = cv2.calcOpticalFlowPyrLK(prev_gray, frame_gray, prevPts, None, **lk_params)

    good_new = nextPts[status==1]
    good_prev = prevPts[status==1]

    for i,(new,prev) in enumerate(zip(good_new,good_prev)):

        x_new,y_new = new.ravel()
        x_prev,y_prev = prev.ravel()

        mask = cv2.line(mask, (x_new,y_new),(x_prev,y_prev), (0,255,0), 3)

        frame = cv2.circle(frame,(x_new,y_new),8,(0,0,255),-1)

    img = cv2.add(frame,mask)
    cv2.imshow('frame',img)

    if cv2.waitKey(30) & 0xff == 27:
        break

    prev_gray = frame_gray.copy()
    prevPts = good_new.reshape(-1,1,2)


cap.release()
cv2.destroyAllWindows()

このPythonスクリプトをターミナルから実行するとwebカメラが起動して、ビューワーに赤色のポイントが入った映像が映るはずです。自分が写っていたら顔を移動したり、カメラを移動したりすると、検出されたポイントが移動して緑色の線で物体追跡の軌跡が描かれるはずです。

スポンサーリンク

最後に

OpenCVを使ったPythonでの画像処理について、物体の追跡(Object Tracking)について扱いました。

ここではオプティカルフロー(Optical Flow)の概念とWebカメラを使ってのLucas-Kanade法による物体の追跡をやってみました。

オプティカルフローとは、物体やカメラの移動によって生じる隣接フレームの中で物体の動きをベクトルで表したものです。

Lucas-Kanade法は、ある点に対してその点を含む周囲の3×3に含まれる9画素が同じ動きをしていると仮定し、この9画素の情報を基に注目点の位置の移動を計算する方法です。

ここではwebカメラで映した画像の注目点の移動を線で描くコードを扱いましたが、次では画像画像中の全画素に対してオプティカルフローを計算する方法を扱います。

【Python】OpenCVの密なオプティカルフロー calcOpticalFlowFarneback()
OpenCVを使ったPythonでの画像処理について、Lucas-Kanade法は「疎」の物体追跡の検出でしたが、ここでは逆に密なオプティカルフローの検出アルゴリズムであるcalcOpticalFlowFarneback()を扱います。
タイトルとURLをコピーしました