LoginSignup
8
7

More than 1 year has passed since last update.

【OpenCV】Dense Optical Flowを矢印や色空間で表示する方法

Last updated at Posted at 2022-08-27

概要

OpenCVで提供されているオプティカルフローは2種類あり、本記事ではDense Optical Flowについて実装方法と矢印や色空間で表示する方法を紹介する。Optical Flowの説明については、

を参照されたい。

実装

Google Colabで作成した本記事のコードは、こちらにあります。

各種インポート

各種インポート
import cv2
import matplotlib.pyplot as plt
import numpy as np

実行時の各種バージョン

Name Version
opencv-python 4.6.0.66
matplotlib 3.2.2
numpy 1.21.6

使用する関数

使用する関数
def flow_vector(flow, spacing, margin, minlength):
    """Parameters:
    input
    flow: motion vectors 3D-array
    spacing: pixel spacing of the flow
    margin: pixel margins of the flow
    minlength: minimum pixels to leave as flow
    output
    x: x coord 1D-array
    y: y coord 1D-array
    u: x direction flow vector 2D-array
    v: y direction flow vector 2D-array
    """
    h, w, _ = flow.shape

    x = np.arange(margin, w - margin, spacing, dtype=np.int64)
    y = np.arange(margin, h - margin, spacing, dtype=np.int64)

    mesh_flow = flow[np.ix_(y, x)]
    mag, _ = cv2.cartToPolar(mesh_flow[..., 0], mesh_flow[..., 1])
    mesh_flow[mag < minlength] = np.nan  # minlength以下をnanに置換

    u = mesh_flow[..., 0]
    v = mesh_flow[..., 1]

    return x, y, u, v

def adjust_ang(ang_min, ang_max):
    """Parameters
    input
    ang_min: start angle of degree
    ang_max: end angle of degree
    output
    unique_ang_min: angle after conversion to unique `ang_min`
    unique_ang_max: angle after conversion to unique `ang_max`
    """
    unique_ang_min = ang_min
    unique_ang_max = ang_max
    unique_ang_min %= 360
    unique_ang_max %= 360
    if unique_ang_min >= unique_ang_max:
        unique_ang_max += 360
    return unique_ang_min, unique_ang_max

def any_angle_only(mag, ang, ang_min, ang_max):
    """
    input
    mag: `cv2.cartToPolar` method `mag` reuslts
    ang: `cv2.cartToPolar` method `ang` reuslts
    ang_min: start angle of degree after `adjust_ang` function
    ang_max: end angle of degree after `adjust_ang` function
    output
    any_mag: array of replace any out of range `ang` with nan
    any_ang: array of replace any out of range `mag` with nan
    description
    Replace any out of range `mag` and `ang` with nan.
    """
    any_mag = np.copy(mag)
    any_ang = np.copy(ang)
    ang_min %= 360
    ang_max %= 360
    if ang_min < ang_max:
        any_mag[(ang < ang_min) | (ang_max < ang)] = np.nan
        any_ang[(ang < ang_min) | (ang_max < ang)] = np.nan
    else:
        any_mag[(ang_max < ang) & (ang < ang_min)] = np.nan
        any_ang[(ang_max < ang) & (ang < ang_min)] = np.nan
        any_ang[ang <= ang_max] += 360
    return any_mag, any_ang

def hsv_cmap(ang_min, ang_max, size):
    """
    input
    ang_min: start angle of degree after `adjust_ang` function
    ang_max: end angle of degree after `adjust_ang` function
    size: map px size
    output
    hsv_cmap_rgb: HSV color map in radial vector flow
    x, y, u, v: radial vector flow value
    x: x coord 1D-array
    y: y coord 1D-array
    u: x direction flow vector 2D-array
    v: y direction flow vector 2D-array
    description
    Create a normalized hsv colormap between `ang_min` and `ang_max`.
    """
    # 放射状に広がるベクトル場の生成
    half = size // 2
    x = np.arange(-half, half+1, 1, dtype=np.float64)
    y = np.arange(-half, half+1, 1, dtype=np.float64)
    u, v = np.meshgrid(x, y)

    # HSV色空間の配列に入れる
    hsv = np.zeros((len(y), len(x), 3), dtype='uint8')
    mag, ang = cv2.cartToPolar(u, v, angleInDegrees=True)
    any_mag, any_ang = any_angle_only(mag, ang, ang_min, ang_max)
    hsv[..., 0] = 180*(any_ang - ang_min) / (ang_max - ang_min)
    hsv[..., 1] = 255
    hsv[..., 2] = cv2.normalize(any_mag, None, 0, 255, cv2.NORM_MINMAX)
    hsv_cmap_rgb = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)

    return hsv_cmap_rgb, x, y, u, v

関数の説明

  • flow_vector: この関数はQuiver plot with optical flow? - Stack Overflowの回答からヒントを得て作成している。optical-flowの結果を矢印で表示するための関数で、1 px単位で表示すると見にくいため、表示間隔を任意に調整して表示することができる仕様である。flow[np.ix_(y, x)]yxの格子点の座標の配列を作っていて、スライスでのflow[margin:w - margin:spacing, margin:h - margin:spacing]と同義であるが、出力としてy, xを使うので可読性向上のためnp.ix_を使っている。詳しい説明は私の記事で恐縮ですが、【numpy】np.ix_を使った任意の格子点の要素を抜き出して配列を作成する方法を参照されたい。
  • adjust_ang: 任意の度数法による角度範囲の開始角と終了角を入力に対して一意的な角度で出力する関数で、360°をまたぐ角度を指定したときに連続的に角度を表示できるように終了角に360°加えた値を返す。また、360で割った余りを内部で処理することで入力に負の値等も対応している。例えば、ang_min=300, ang_max=60として、adjust_ang(ang_min, ang_max)に入力すると、unique_ang_min=300, unique_ang_max=420として出力される。
  • any_angle_only: OpenCVのcv2.cartToPolarによるベクトルの大きさ(mag)と角度(ang)から、任意の角度範囲外のmagangnanに置換する。nanにすることで、任意の角度範囲外の情報を使わないで処理することができる。
  • hsv_cmap: 放射状のベクトル場をHSV色空間を表示する関数で、この後のoptical-flowをHSV色空間で表示するときに角度と色の対応を見やすくするために使用する。ベクトル場をHSV色空間で表示する方法は私の記事で恐縮ですが、【OpenCV+matplotlib】ベクトル場をカラーマップで良い感じに表示する方法を参照されたい。

vtest.aviから連番画像の作成

本記事ではOPtical-flowのvtest.aviからダウンロードし、以下のコードより連番画像作成し、16, 17フレーム('00015.png', '00016.png')の画像を使用する。

vtest.aviから連番画像の作成
cap = cv2.VideoCapture('vtest.avi')
frame_num = 0
while True:
    ret, frame = cap.read()
    if ret:
        cv2.imwrite(f'{frame_num:05}.png', frame)
        frame_num += 1
    else:
        break

フレームの読み込みと確認

OpenCVではRGB画像はcv2.imread()で、(B, G, R)の順で読み込まれるので注意してください。

フレームの読み込みと確認
# フレームの読み込み
prev_frame = cv2.imread('00015.png')
next_frame = cv2.imread('00016.png')

# グレースケール化
prev_gray = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)
next_gray = cv2.cvtColor(next_frame, cv2.COLOR_BGR2GRAY)

# 表示
fig, axs = plt.subplots(2, 2, figsize=(20, 16))
prev_rgb = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2RGB)
axs[0, 0].set_title('prev_frame')
axs[0, 0].imshow(prev_rgb)
next_rgb = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2RGB)
axs[0, 1].set_title('next_frame')
axs[0, 1].imshow(next_rgb)
axs[1, 0].set_title('prev_gray')
axs[1, 0].imshow(prev_gray, cmap='gray')
axs[1, 1].set_title('next_gray')
axs[1, 1].imshow(next_gray, cmap='gray')

plt.show()

出力結果
image.png

dence optical-flowの計算

cv2.calcOpticalFlowFarnebackは、8-bit single-channel画像のみ対応しているのでグレースケール化した2枚の画像を入力画像に使用する。また、各パラーメータはOpenCV公式optical flow Dense Optical Flow in OpenCVをそのまま使っていて、各パラメータの意味についてはcalcOpticalFlowFarneback()を参照されたい。

dence optical-flowの計算
flow = cv2.calcOpticalFlowFarneback(prev_gray, next_gray, None, 0.5, 3, 15, 3, 5, 1.2, 0)

矢印で表示

flow_vector(flow=flow, spacing=10, margin=0, minlength=1)の引数の意味について、spacingは表示間隔(px)、marginは画像の縁の余白(px)、minlengthは表示する矢印の大きさの最小値(px)である。
minlength=1を使う理由はminlength=0にすると矢印が格子状に残ってしまって見にくいためであるのと、1 px未満の変化ではノイズの影響が多いためである。
本記事はmargin=0を使っているが、一般的に画像の縁では物体が画像外にはみ出てしまったりしてoptical-flowの精度が低いことが多いので、状況によってmarginを設定すると良い。

矢印で表示
fig, ax = plt.subplots(figsize=(10, 8))
ax.set_title('prev_frame and prev2nextflow vector')
ax.imshow(prev_rgb)
x, y, u, v = flow_vector(flow=flow, spacing=10, margin=0, minlength=1)
ax.quiver(x, y, u, v, angles='xy', scale_units='xy', scale=1, color=[0.0, 1.0, 0.0])

plt.show()

出力結果
image.png

ベクトル場をHSV色空間で表示

HSV色空間にベクトル場の角度に色相を、大きさに明度を対応させて表示
# 角度範囲のパラメータ
ang_min = 0
ang_max = 360
_ang_min, _ang_max = adjust_ang(ang_min, ang_max)  # 角度の表現を統一する

hsv_cmap_rgb, x, y, u, v = hsv_cmap(_ang_min, _ang_max, 31)
fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(10, 8))
ax1.set_title('radial_flow_rgb')
ax1.imshow(hsv_cmap_rgb)

ax2.imshow(hsv_cmap_rgb)
ax2.set_title('radial_flow_rgb and vector')
ax2.quiver(np.arange(len(x)), np.arange(len(y)), u, v, angles='xy', scale_units='xy', color='w')

plt.show()

出力結果
image.png

実際のflowをHSV色空間で表示

画像の原点(0, 0)に矢印の角度に対応したHSV色空間を埋め込み、上の画像が矢印をHSV色空間で表示したもの、下の画像が上の画像に矢印を追加したものである。

hsvはHSV色空間の配列であり、hsvの配列の定義は以下の通りである。

  • hsv[..., 0]は色相を表し、0~180で定義される。
  • hsv[..., 1]は彩度を表し、0~255で定義される。ベクトル場を扱うときは、基本的に255で問題ないと思われる。
  • hsv[..., 2]は明度を表し、0~255で定義される。

hsv[..., 0] = 180*(any_ang - _ang_min) / (_ang_max - _ang_min)は、_ang_minを最小値、_ang_maxを最大値として0~180に規格化している。ここで、hsv[..., 0] = cv2.normalize(any_ang, None, 0, 180, cv2.NORM_MINMAX)を使えば良いと思うかもしれないが、any_angには_ang_min_ang_max以内の配列が入っているが、any_angの最小値と最大値で規格化すると左上のカラーマップの角度と色の対応が実際のoptical flowの画像とでずれてしまうので、このようなコードを使っている。
hsv_cmap_bgr, *_ = hsv_cmap(_ang_min, _ang_max, 51)は、_ang_min, _ang_maxの間の角度を51 pxの角度に対応した色空間を作る処理であり、原点(0, 0)に配置して確認するためのものなので入力画像の大きさに合わせて適宜画像サイズを調整してください。

実際のflowでHSV色空間に矢印の角度に色相を、大きさに明度を対応させて表示
# 角度範囲のパラメータ
ang_min = 0
ang_max = 360
_ang_min, _ang_max = adjust_ang(ang_min, ang_max)  # 角度の表現を統一する

# HSV色空間の配列に入れる
hsv = np.zeros_like(prev_frame, dtype='uint8')
mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1], angleInDegrees=True)
any_mag, any_ang = any_angle_only(mag, ang, ang_min, ang_max)
hsv[..., 0] = 180*(any_ang - _ang_min) / (_ang_max - _ang_min)
hsv[..., 1] = 255
hsv[..., 2] = cv2.normalize(any_mag, None, 0, 255, cv2.NORM_MINMAX)
flow_rgb = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)

# 画像の原点にHSV色空間を埋め込み
flow_rgb_display = np.copy(flow_rgb)
hsv_cmap_rgb, *_ = hsv_cmap(_ang_min, _ang_max, 51)
flow_rgb_display[0:hsv_cmap_rgb.shape[0], 0:hsv_cmap_rgb.shape[1]] = hsv_cmap_rgb

fig, (ax1, ax2) = plt.subplots(nrows=2, figsize=(10, 16))
ax1.set_title('prev2nextflow rgb')
ax1.imshow(flow_rgb_display)

ax2.imshow(flow_rgb_display)
x, y, u, v = flow_vector(flow=flow, spacing=10, margin=0, minlength=1)
ax2.set_title('prev2nextflow rgb and vector')
ax2.quiver(x, y, u, v, angles='xy', scale_units='xy', scale=1, color='w')

plt.show()

出力結果
image.png

ベクトル場を任意の角度のHSV色空間で表示

左上が原点(0, 0)の場合、角度は画像で時計回りになっていて、時計の3時の針が0°で6時が90°とうい対応関係であるので注意してください。

任意の角度範囲のHSV色空間にベクトル場の角度に色相を、大きさに明度を対応させて表示
# 角度範囲のパラメータ
ang_min = 300
ang_max = 60
_ang_min, _ang_max = adjust_ang(ang_min, ang_max)  # 角度の表現を統一する

hsv_cmap_rgb, x, y, u, v = hsv_cmap(_ang_min, _ang_max, 31)
fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(10, 8))
ax1.set_title('radial_flow_rgb')
ax1.imshow(hsv_cmap_rgb)

ax2.imshow(hsv_cmap_rgb)
ax2.set_title('radial_flow_rgb and vector')
ax2.quiver(np.arange(len(x)), np.arange(len(y)), u, v, angles='xy', scale_units='xy', color='w')

plt.show()

image.png

実際のflowを任意の角度のHSV色空間で表示

ang_min, ang_maxに任意の開始角度、終了角度の範囲でHSV色空間を作成する。画像の原点に矢印の角度に対応したHSV色空間を埋め込み、上の画像が矢印をHSV色空間で表示したもの、下の画像が上の画像に矢印を追加したものである。
このように、任意の角度のHSV空間を使うことで、画像の部分的な角度に注目して詳細に見ることができる。

実際のflowで任意の角度範囲のHSV色空間に矢印の角度に色相を、大きさに明度を対応させて表示
# 角度範囲のパラメータ
ang_min = 300
ang_max = 60
_ang_min, _ang_max = adjust_ang(ang_min, ang_max)  # 角度の表現を統一する

# HSV色空間の配列に入れる
hsv = np.zeros_like(prev_frame, dtype='uint8')
mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1], angleInDegrees=True)
any_mag, any_ang = any_angle_only(mag, ang, ang_min, ang_max)
hsv[..., 0] = 180*(any_ang - _ang_min) / (_ang_max - _ang_min)
hsv[..., 1] = 255
hsv[..., 2] = cv2.normalize(any_mag, None, 0, 255, cv2.NORM_MINMAX)
flow_rgb = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)

# 画像の原点にHSV色空間を埋め込み
flow_rgb_display = np.copy(flow_rgb)
hsv_cmap_rgb, *_ = hsv_cmap(_ang_min, _ang_max, 51)
flow_rgb_display[0:hsv_cmap_rgb.shape[0], 0:hsv_cmap_rgb.shape[1]] = hsv_cmap_rgb

fig, (ax1, ax2) = plt.subplots(nrows=2, figsize=(10, 16))
ax1.set_title('prev2nextflow rgb')
ax1.imshow(flow_rgb_display)

ax2.imshow(flow_rgb_display)
x, y, u, v = flow_vector(flow=flow, spacing=10, margin=0, minlength=1)
ax2.set_title('prev2nextflow rgb and vector')
ax2.quiver(x, y, u, v, angles='xy', scale_units='xy', scale=1, color='w')

plt.show()

image.png

補足(矢印を画像として保存)

矢印を画像として保存したい場合はOpenCVのcv2.arrowedLineを使うとのが便利で、以下のコードを参考にしてください。

画像として保存するためのコード

使用する関数

使用する関数
def cv2_vector_display(img, color, x, y, u, v):
    """Parameters:
    input
    img: the first image used to calculate the flow
    color: 3 channels 0 to 255 color
    x, y, u, v: `flow_vector` function reuslts
    x: x coord 1D-array
    y: y coord 1D-array
    u: x direction flow vector 2D-array
    v: y direction flow vector 2D-array
    output
    flow_img: image with vector added to input `img`
    description
    Create an image with a vector added to the input `img` using cv2.
    e.g. u[j, i] is the vector of flow in the x direction at (x[i], y[j]) coord.
    """
    flow_img = np.copy(img)
    for i in range(len(x)):
        for j in range(len(y)):
            if np.isnan(u[j, i]) or np.isnan(v[j, i]):
                continue
            pts = np.array([[x[i], y[j]], [x[i]+u[j, i], y[j]+v[j, i]]], np.int64)
            cv2.arrowedLine(flow_img, pts[0], pts[1], color, thickness=1, tipLength=0.5)
    return flow_img

cv2_vector_display: optical-flowに使った前のフレームの画像とflow_vector関数を使った後のx, y, u, vを引数に入れることで、矢印を前のフレームの画像に埋め込んで出力する関数である。

矢印付きの画像で保存

cv2_vector_display(prev_frame, (0, 255, 0), x, y, u, v)について、prev_frameが前のRGB画像、(0, 255, 0)が表示する矢印の色で0~255で指定、flow_vector関数を使った後のx, y, u, vで、prev_frameに矢印を埋め込んだ画像が出力される。

矢印付きの画像で保存
x, y, u, v = flow_vector(flow=flow, spacing=10, margin=0, minlength=1)
flow_img_vector = cv2_vector_display(prev_frame, (0, 255, 0), x, y, u, v)
cv2.imwrite('flow_img_vector.png', flow_img_vector)

保存した画像
image.png

矢印をHSV色空間で表示して画像で保存

矢印をHSV色空間で表示して画像で保存
# 角度範囲のパラメータ
ang_min = 0
ang_max = 360
_ang_min, _ang_max = adjust_ang(ang_min, ang_max)  # 角度の表現を統一する

hsv = np.zeros_like(prev_frame, dtype='uint8')
mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1], angleInDegrees=True)
any_mag, any_ang = any_angle_only(mag, ang, ang_min, ang_max)
hsv[..., 0] = 180*(any_ang - _ang_min) / (_ang_max - _ang_min)
hsv[..., 1] = 255
hsv[..., 2] = cv2.normalize(any_mag, None, 0, 255, cv2.NORM_MINMAX)
flow_bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)

# 画像の原点にHSV色空間を埋め込み
hsv_cmap_rgb, *_ = hsv_cmap(_ang_min, _ang_max, 51)
hsv_cmap_bgr = cv2.cvtColor(hsv_cmap_rgb, cv2.COLOR_RGB2BGR)
flow_bgr[0:hsv_cmap_bgr.shape[0], 0:hsv_cmap_bgr.shape[1]] = hsv_cmap_bgr
cv2.imwrite('flow_rgb.png', flow_bgr)

保存した画像
image.png

矢印と矢印の色空間の両方を表示して画像で保存

矢印と矢印の色空間の両方を表示して画像で保存
# 角度範囲のパラメータ
ang_min = 0
ang_max = 360
_ang_min, _ang_max = adjust_ang(ang_min, ang_max)  # 角度の表現を統一する

hsv = np.zeros_like(prev_frame, dtype='uint8')
mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1], angleInDegrees=True)
any_mag, any_ang = any_angle_only(mag, ang, ang_min, ang_max)
hsv[..., 0] = 180*(any_ang - _ang_min) / (_ang_max - _ang_min)
hsv[..., 1] = 255
hsv[..., 2] = cv2.normalize(any_mag, None, 0, 255, cv2.NORM_MINMAX)
flow_bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
x, y, u, v = flow_vector(flow=flow, spacing=10, margin=0, minlength=1)
flow_bgr_vector = cv2_vector_display(flow_bgr, (255, 255, 255), x, y, u, v)

# 画像の原点にHSV色空間を埋め込み
hsv_cmap_rgb, *_ = hsv_cmap(_ang_min, _ang_max, 51)
hsv_cmap_bgr = cv2.cvtColor(hsv_cmap_rgb, cv2.COLOR_RGB2BGR)
flow_bgr_vector[0:hsv_cmap_bgr.shape[0], 0:hsv_cmap_bgr.shape[1]] = hsv_cmap_bgr
cv2.imwrite('flow_rgb_vector.png', flow_bgr_vector)

保存した画像
image.png

まとめ

Dense Optical Flowを矢印や色空間で表示する方法を紹介しました。任意の角度の色空間で矢印を表示することで矢印の分布を空間で詳細に把握できるのでおすすめです。

参考資料

8
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7