勉強しないとな~blog

ちゃんと勉強せねば…な電気設計エンジニアです。

OpenCVやってみる - 57. Optical Flow

OpenCVのOptical Flowを試してみる

画像からの動き検出をやってみたいと思っています。

OpenCVの公式チュートリアルの中で、Optical Flowが取り上げられていて、これでできそうなので、やってみます。

OpenCV: Optical Flow

対象データ

同じデータを使っても面白くないので、別のデータで。
MOTChallengeの中のMOT16のデータを使ってみたいと思います。

https://motchallenge.net/data/MOT16/

色々と動画データがありますが、特に密なOptical Flowを計算するときは背景が動いていない(カメラが固定されている)ほうが効果が分かりやすそうなので、そういうデータを選びます。

  • MOT16-09
  • MOT16-04
  • MOT16-02
  • MOT16-08
  • MOT16-03
  • MOT16-01

今回は、この中のMOT16-09を使ってみます。

動画データは上記リンクからダウンロード。

Optical Flow計算実施

チュートリアルのコード通りでできると思いますが、途中の状態も見ながらやってみようと思います。

まずはLucas-Kanadeの手法から。

import numpy as np
import cv2
from matplotlib import pyplot as plt

img_path = 'C:/work/MOT16/train/MOT16-09/img1/%06d.jpg'
cap = cv2.VideoCapture(img_path)
print(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
print(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
1920.0
1080.0
def plot_image(img):
    img0 = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    plt.imshow(img0), plt.xticks([]), plt.yticks([])
    plt.show()
    print('MOT16 - https://motchallenge.net/data/MOT16/')
ret, old_frame = cap.read()
plot_image(old_frame)

MOT16 - https://motchallenge.net/data/MOT16/

↑ MOTChallengeの動画はCreative Commons Attribution-NonCommercial-ShareAlike 3.0 Licenseなので、とりあえずいちいちリンクを貼っておきます。
これでいいのかな…

Creative Commons — Attribution-NonCommercial-ShareAlike 3.0 Unported — CC BY-NC-SA 3.0

ブログに画像を入れたい方必見!「クリエイティブ・コモンズ」の意味と画像の探し方を徹底解説! | インターネットビジネスラボ

クリエイティブ・コモンズ・ライセンスとは | クリエイティブ・コモンズ・ジャパン

cv2.VideoCaptureでは、上記の通り連番画像ファイルの読み込みが可能。

OpenCV: cv::VideoCapture Class Reference

以下は下準備。

# params for ShiTomasi corner detection
feature_params = dict( maxCorners = 100,
                       qualityLevel = 0.3,
                       minDistance = 7,
                       blockSize = 7 )
# Parameters for lucas kanade optical flow
lk_params = dict( winSize  = (15, 15),
                  maxLevel = 2,
                  criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))

# Create some random colors
color = np.random.randint(0, 255, (100, 3))

以下は、cv2.goodFeaturesToTrack()関数で特徴点を検出しています。

old_gray = cv2.cvtColor(old_frame, cv2.COLOR_RGB2GRAY)
p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params)
print(type(p0))
print(p0.shape)
print(p0[:5,:,:])
print(p0.dtype)
<class 'numpy.ndarray'>
(100, 1, 2)
[[[1437.  179.]]

 [[1421.  174.]]

 [[1305.  144.]]

 [[1390.  137.]]

 [[ 401.   39.]]]
float32

maxCorners引数を100にしているので、100点出てきています。

img_with_corners = old_frame.copy()
for p in p0[:,0,:]:
    cv2.circle(img_with_corners, center=(int(p[0]), int(p[1])), radius=10, color=(0,255,0), thickness=3)

plot_image(img_with_corners)

MOT16 - https://motchallenge.net/data/MOT16/

人じゃないところに特徴点検出してしまっている…
背景がガチャガチャしているとあまり良くないか。

チュートリアルのコード続きです。
ただし、結果を動画データに保存できるようにコード追加しています。

codec = cv2.VideoWriter_fourcc(*'mp4v')
writer = cv2.VideoWriter('MOT16-09-LK.mp4', codec, 30.0, (1920,1080))
# Create a mask image for drawing purposes
mask = np.zeros_like(old_frame)
while(1):
    ret, frame = cap.read()
    if not ret:
        print('No frames grabbed!')
        break
    frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    # calculate optical flow
    p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params)
    # Select good points
    if p1 is not None:
        good_new = p1[st==1]
        good_old = p0[st==1]
    # draw the tracks
    for i, (new, old) in enumerate(zip(good_new, good_old)):
        a, b = new.ravel()
        c, d = old.ravel()
        mask = cv2.line(mask, (int(a), int(b)), (int(c), int(d)), color[i].tolist(), 2)
        frame = cv2.circle(frame, (int(a), int(b)), 5, color[i].tolist(), -1)
    img = cv2.add(frame, mask)
    writer.write(img)
    img = cv2.resize(img, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
    cv2.imshow('frame', img)
    k = cv2.waitKey(30) & 0xff
    if k == 27:
        break
    # Now update the previous frame and previous points
    old_gray = frame_gray.copy()
    p0 = good_new.reshape(-1, 1, 2)

cv2.destroyAllWindows()
writer.release()
No frames grabbed!

それらしい動きが取得できているよう。
動画データも残せました。(一部のみ切り出して下記に示しています。)

MOT16 - https://motchallenge.net/data/MOT16/

処理は結構重たく、上記のコードの実行時間は46.6(s)でした。(動画自体は18秒。PC性能は低い。)

全部の軌跡を残しているので、後半のほうはかなりぐちゃぐちゃ。
とりあえずオプティカルフローの雰囲気はこんな感じということで。

ある程度のフレーム数、同じ点を追尾してくれていますが、軌跡が途中で飛んだりすることもあります。

チュートリアルのコードでは、cv2.calcOpticalFlowPyrLK()で推定した次の特徴点位置を繰り返し同じ関数に与えて、軌跡の計算をしているので、次々と新しい人物が現れてくるような今回のデータは不適切だったかも。

ときどき特徴点検出をやり直したほうが良かったかも。

今回はそこまで深堀しませんが。

密なオプティカルフロー

チュートリアルのもう一つの項目「密なオプティカルフロー」(Dense Optical Flow)をやってみます。
データは同じMOT16のものを使用。

実施

まずは動画データを先頭に巻き戻し。

cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
True

前と同様に、チュートリアルのコードに動画保存を追加。

ret, frame1 = cap.read()
prvs = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY)
hsv = np.zeros_like(frame1)
hsv[..., 1] = 255

codec = cv2.VideoWriter_fourcc(*'mp4v')
writer = cv2.VideoWriter('MOT16-09-FB.mp4', codec, 30.0, (1920,1080*2))

while(1):
    ret, frame2 = cap.read()
    if not ret:
        print('No frames grabbed!')
        break
    next = cv2.cvtColor(frame2, cv2.COLOR_BGR2GRAY)
    flow = cv2.calcOpticalFlowFarneback(prvs, next, None, 0.5, 3, 15, 3, 5, 1.2, 0)
    mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1])
    hsv[..., 0] = ang*180/np.pi/2
    hsv[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)
    bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
    frame2 = np.vstack((frame2, bgr))
    writer.write(frame2)
    frame2 = cv2.resize(frame2, None, fx=0.25, fy=0.25, interpolation=cv2.INTER_AREA)
    cv2.imshow('frame2', frame2)
    k = cv2.waitKey(30) & 0xff
    if k == 27:
        break
    prvs = next
cv2.destroyAllWindows()
writer.release()
No frames grabbed!

これもなんとなく動きが得られているような。

  • エッジ部分で特に動きが見られる。(黒っぽい服、ズボンだと顕著)
  • 動きの方向も区別できている(左→右: 赤い色、右→左: 青っぽい色、すれ違いが発生しているところは特に見どころ)

MOT16 - https://motchallenge.net/data/MOT16/

処理はより重たく、上記のコードの実行時間は6m23.9(s)でした。(動画自体は18秒。PC性能は低い。)

以上

今回はここまで。
きちんと評価はできていませんが、Optical Flowを体感しました。

オブジェクトトラッキングアルゴリズムに興味を持っているので、今後関係しそうなものを見ていきたいと思います。