概要
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)]
でy
とx
の格子点の座標の配列を作っていて、スライスでの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
)から、任意の角度範囲外のmag
とang
をnan
に置換する。nan
にすることで、任意の角度範囲外の情報を使わないで処理することができる。 -
hsv_cmap
: 放射状のベクトル場をHSV色空間を表示する関数で、この後のoptical-flowをHSV色空間で表示するときに角度と色の対応を見やすくするために使用する。ベクトル場をHSV色空間で表示する方法は私の記事で恐縮ですが、【OpenCV+matplotlib】ベクトル場をカラーマップで良い感じに表示する方法を参照されたい。
vtest.aviから連番画像の作成
本記事ではOPtical-flowのvtest.aviからダウンロードし、以下のコードより連番画像作成し、16, 17フレーム('00015.png'
, '00016.png'
)の画像を使用する。
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()
dence optical-flowの計算
cv2.calcOpticalFlowFarneback
は、8-bit single-channel画像のみ対応しているのでグレースケール化した2枚の画像を入力画像に使用する。また、各パラーメータはOpenCV公式optical flow Dense Optical Flow in OpenCVをそのまま使っていて、各パラメータの意味についてはcalcOpticalFlowFarneback()を参照されたい。
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()
ベクトル場を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()
実際の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)に配置して確認するためのものなので入力画像の大きさに合わせて適宜画像サイズを調整してください。
# 角度範囲のパラメータ
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()
ベクトル場を任意の角度のHSV色空間で表示
左上が原点(0, 0)の場合、角度は画像で時計回りになっていて、時計の3時の針が0°で6時が90°とうい対応関係であるので注意してください。
# 角度範囲のパラメータ
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()
実際のflowを任意の角度のHSV色空間で表示
ang_min
, ang_max
に任意の開始角度、終了角度の範囲でHSV色空間を作成する。画像の原点に矢印の角度に対応したHSV色空間を埋め込み、上の画像が矢印をHSV色空間で表示したもの、下の画像が上の画像に矢印を追加したものである。
このように、任意の角度の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()
補足(矢印を画像として保存)
矢印を画像として保存したい場合は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)
矢印を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)
矢印と矢印の色空間の両方を表示して画像で保存
# 角度範囲のパラメータ
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)
まとめ
Dense Optical Flowを矢印や色空間で表示する方法を紹介しました。任意の角度の色空間で矢印を表示することで矢印の分布を空間で詳細に把握できるのでおすすめです。
参考資料