概要
ベクトル場の角度と大きさをカラーマップで表示したい場合がある。そんなときに便利なのがmatplotlibのquiverであるが、実はOpenCVのHSV空間を組み合わせるとベクトル場の角度と大きさを一目で見やすく表示できる。
本記事は筆者が個人的に見やすいと思うベクトル場の表示方法について紹介する。
実装
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 |
準備
hsv, jetのカラーマップを見やすくするために背景を黒塗りにする
plt.style.use('dark_background')
座標変換用の関数
matplotlibのSecondary Axisを使って、ピクセル座標とベクトル場の座標の軸のスケールを調整する関数である。例えば、ベクトル場の角度を背景にカラーマップ表示のように背景にベクトル場の角度情報をax.imshow()
で追加する場合、通常軸はピクセル座標で表示されてしまうが、この関数を適用すると軸のスケールを元のベクトル場のスケールに変換して表示できる。ただし、以降のax.set_aspect('equal')
は、ピクセル座標でのアスペクト比が統一されるので、厳密にベクトル場でのアスペクト比が統一されない場合があるので注意してください。この関数を適用する上でアスペクト比が調節されないケースは、ベクトルの取得座標間隔がX
とY
で異なるときである。
def px2coord_axis(ax, X, Y):
"""
input
ax: matplotlib axis object
X: 1D or 2D equally spaced vector field x array
Y: array of vector fields y of the same dimension as `X`
output
ax: matplotlib ax object
secax_x: vector field x coord object
secax_y: vector field y coord object
description
Function to convert axis labels from pixel coordinates to vector field coordinates.
"""
if X.ndim == 2:
X_start = X[0, 0]
X_step = X[0, 1] - X[0, 0]
Y_start = Y[0, 0]
Y_step = Y[1, 0] - Y[0, 0]
else:
X_start = X[0]
X_step = X[1] - X[0]
Y_start = Y[0]
Y_step = Y[1] - Y[0]
ax.tick_params(labelbottom=False, labelleft=False)
secax_x = ax.secondary_xaxis('bottom',
functions=(lambda x: x*X_step + X_start, lambda x: (x - X_start) / X_step))
secax_y = ax.secondary_yaxis('left',
functions=(lambda x: x*Y_step + Y_start, lambda x: (x - Y_start) / Y_step))
return ax, secax_x, secax_y
サンプルのベクトル場の準備
注意点として、cv2.cartToPolar
の入力のU
, V
は、float型の同じ型である必要がある。cv2.cartToPolar(U, V, angleInDegrees=True)
は、mag
とang
にそれぞれ各座標に対応したベクトルの大きさと角度が入った配列が入る。angleInDegrees=True
とするとこで、ang
には反時計回りを正にした0°~360°の度数法に基づく角度が入る。サンプルでは、放射状のベクトルを用意した。
X = np.arange(-10, 11, 1, dtype=np.float64)
Y = np.arange(-10, 11, 1, dtype=np.float64)
U, V = np.meshgrid(X, Y)
mag, ang = cv2.cartToPolar(U, V, angleInDegrees=True)
X_shape, Y_shape = X.shape[0], Y.shape[0] # X, Yが1次元配列の場合
# X_shape, Y_shape = X.shape[1], Y.shape[0] # X, Yが2次元配列の場合
サンプルのベクトル場を表示して確認する。
fig, ax = plt.subplots(figsize=(10, 8))
ax.quiver(X, Y, U, V, color='w')
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_aspect('equal')
plt.show()
方法
ベクトル場の角度をカラーマップ表示
fig, ax = plt.subplots(figsize=(10, 8))
Q = ax.quiver(X, Y, U, V, ang, cmap='hsv')
ax.set_xlabel('X')
ax.set_ylabel('Y')
fig.colorbar(Q, label='vector angle (degree)')
ax.set_aspect('equal')
plt.show()
ベクトル場の大きさをカラーマップ表示
fig, ax = plt.subplots(figsize=(10, 8))
Q = ax.quiver(X, Y, U, V, mag, cmap='jet')
ax.set_xlabel('X')
ax.set_ylabel('Y')
fig.colorbar(Q, label='vectorrr size')
ax.set_aspect('equal')
plt.show()
ベクトル場の角度を背景にカラーマップ表示
fig, ax = plt.subplots(figsize=(10, 8))
cmap = plt.imshow(ang, cmap='hsv', origin='lower')
ax.quiver(np.arange(X_shape), np.arange(Y_shape), U, V, color='w')
ax, secax_x, secax_y = px2coord_axis(ax, X, Y)
secax_x.set_xlabel('X')
secax_y.set_ylabel('Y')
fig.colorbar(cmap, label='vector angle (degree)')
ax.set_aspect('equal')
plt.show()
ベクトル場の大きさを背景にカラーマップ表示
fig, ax = plt.subplots(figsize=(10, 8))
cmap = plt.imshow(mag, cmap='jet', origin='lower')
ax.quiver(np.arange(X_shape), np.arange(Y_shape), U, V, color='w')
ax, secax_x, secax_y = px2coord_axis(ax, X, Y)
secax_x.set_xlabel('X')
secax_y.set_ylabel('Y')
fig.colorbar(cmap, label='vector size')
ax.set_aspect('equal')
plt.show()
HSV色空間にベクトル場の角度に色相を、大きさに角度を対応させて表示
hsv
はHSV色空間の配列であり、hsv
の配列の定義は以下の通りである。
-
hsv[..., 0]
は色相を表し、0~180で定義される。 -
hsv[..., 1]
は彩度を表し、0~255で定義される。ベクトル場を扱うときは、基本的に255で問題ないと思われる。 -
hsv[..., 2]
は明度を表し、0~255で定義される。
cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)
でRGB画像に変換している。
hsv = np.zeros((Y_shape, X_shape, 3), dtype='uint8')
hsv[..., 0] = ang / 2
hsv[..., 1] = 255
hsv[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)
flow_rgb = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)
fig, ax = plt.subplots(figsize=(8, 8))
ax.imshow(flow_rgb, origin='lower')
ax.quiver(np.arange(X_shape), np.arange(Y_shape), U, V, color='w')
ax, secax_x, secax_y = px2coord_axis(ax, X, Y)
secax_x.set_xlabel('X')
secax_y.set_ylabel('Y')
ax.set_aspect('equal')
plt.show()
HSV色空間にベクトル場の大きさに色相と明度を対応させて表示
hsv = np.zeros((Y_shape, X_shape, 3), dtype='uint8')
hsv[..., 0] = cv2.normalize(mag, None, 0, 180, cv2.NORM_MINMAX)
hsv[..., 1] = 255
hsv[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)
flow_rgb = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)
fig, ax = plt.subplots(figsize=(8, 8))
ax.imshow(flow_rgb, origin='lower')
ax.quiver(np.arange(X_shape), np.arange(Y_shape), U, V, color='w')
ax, secax_x, secax_y = px2coord_axis(ax, X, Y)
secax_x.set_xlabel('X')
secax_y.set_ylabel('Y')
ax.set_aspect('equal')
plt.show()
少し構造のあるベクトル場での例
X, Y = np.meshgrid(np.arange(0, 3 * np.pi, .2), np.arange(0, 2 * np.pi, .2))
U = np.cos(X)
V = np.sin(Y)
mag, ang = cv2.cartToPolar(U, V, angleInDegrees=True)
X_shape, Y_shape = X.shape[1], Y.shape[0] # 2次元配列の場合
ベクトル場を表示([ベクトル場を表示](figsize=(15, 8)
としてベクトル場を表示)
ベクトル場の角度をカラーマップ表示(ベクトル場の角度をカラーマップ表示のfigsize=(15, 8)
としたコード)
ベクトル場の大きさをカラーマップ表示(ベクトル場の大きさをカラーマップ表示のfigsize=(15, 8)
としたコード)
ベクトル場の角度を背景にカラーマップ表示(ベクトル場の角度を背景にカラーマップ表示のfigsize=(15, 8)
としたコード)
ベクトル場の大きさを背景にカラーマップ表示(ベクトル場の大きさを背景にカラーマップ表示のfigsize=(15, 8)
としたコード)
HSV色空間にベクトル場の角度に色相を、大きさに角度を対応させて表示(HSV色空間にベクトル場の角度に色相を、大きさに角度を対応させて表示のfigsize=(15, 8)
としたコード)
HSV色空間にベクトル場の大きさに色相と明度を対応させて表示(HSV色空間にベクトル場の大きさに色相と明度を対応させて表示のfigsize=(15, 8)
としたコード)
補足(カラーマップを任意の範囲で設定)
matplotlibのquiverは、カラーマップの色の表示範囲(matplotlibに良くあるvmin, vmax等)の引数は2022/08/29時点ではなく、また角度の表示範囲は360°を超えると不連続なので、少し複雑で以下のコードを参考にしていただきたい。
カラーマップ任意の範囲で表示するためのコード
関数の準備
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
ang_max: end angle of degree
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 any_size_only(mag, ang, mag_min, mag_max):
"""
input
mag: `cv2.cartToPolar` method `mag` reuslts
ang: `cv2.cartToPolar` method `ang` reuslts
mag_min: range min mag
mag_max: range max mag
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)
any_mag[(mag < mag_min) | (mag_max < mag)] = np.nan
any_ang[(mag < mag_min) | (mag_max < mag)] = np.nan
return any_mag, any_ang
ベクトル場を任意の範囲の角度のみでカラーマップ
ベクトル場を任意の範囲の角度のみでカラーマップで表すことで、ベクトル場の角度に対する色の感度を上げて詳細に見ることができる。ただ、360°を超える範囲を指定すると不連続なので少し複雑で、自作のany_angle_only
関数で、角度を反時計回りに指定する。ang_min
は反時計回りの角度の始まり、ang_max
反時計回りの角度の終わりを度数法で指定する。例のany_angle_only(mag, ang, 270, 60)
は、any_angle_only(mag, ang, 270, 420)
としても同様に機能する。
"""サンプルのベクトル場"""
X = np.arange(-10, 11, 1, dtype=np.float64)
Y = np.arange(-10, 11, 1, dtype=np.float64)
U, V = np.meshgrid(X, Y)
mag, ang = cv2.cartToPolar(U, V, angleInDegrees=True)
any_mag, any_ang = any_angle_only(mag, ang, 270, 60) # 270°から60°のみでカラーマップ表示
# any_mag, any_ang = any_angle_only(mag, ang, 270, 420) # こちらでも可能
X_shape, Y_shape = X.shape[0], Y.shape[0] # 1次元配列の場合
"""ベクトル場の角度をカラーマップ表示"""
fig, ax = plt.subplots(figsize=(10, 8))
ax.quiver(X, Y, U, V, color='w')
Q = ax.quiver(X, Y, U, V, any_ang, cmap='hsv')
fig.colorbar(Q, label='vector angle (degree)')
ax.set_aspect('equal')
plt.show()
ベクトル場を任意の範囲の大きさのみでカラーマップ
"""サンプルのベクトル場"""
X = np.arange(-10, 11, 1, dtype=np.float64)
Y = np.arange(-10, 11, 1, dtype=np.float64)
U, V = np.meshgrid(X, Y)
mag, ang = cv2.cartToPolar(U, V, angleInDegrees=True)
any_mag, any_ang = any_size_only(mag, ang, 5, 10)
X_shape, Y_shape = X.shape[0], Y.shape[0] # 1次元配列の場合
"""ベクトル場の大きさをカラーマップ表示"""
fig, ax = plt.subplots(figsize=(10, 8))
ax.quiver(X, Y, U, V, color='w')
Q = ax.quiver(X, Y, U, V, any_mag, cmap='jet')
fig.colorbar(Q, label='vectorrr size')
ax.set_aspect('equal')
plt.show()
ベクトル場を任意の範囲の角度のみで背景にカラーマップ
"""サンプルのベクトル場"""
X = np.arange(-10, 11, 1, dtype=np.float64)
Y = np.arange(-10, 11, 1, dtype=np.float64)
U, V = np.meshgrid(X, Y)
mag, ang = cv2.cartToPolar(U, V, angleInDegrees=True)
any_mag, any_ang = any_angle_only(mag, ang, 270, 60) # 270°から60°のみでカラーマップ表示
# any_mag, any_ang = any_angle_only(mag, ang, 270, 420) # こちらでも可能
X_shape, Y_shape = X.shape[0], Y.shape[0] # 1次元配列の場合
"""ベクトル場の角度を背景にカラーマップ表示"""
fig, ax = plt.subplots(figsize=(10, 8))
cmap = plt.imshow(any_ang, cmap='hsv', origin='lower')
ax.quiver(np.arange(X_shape), np.arange(Y_shape), U, V, color='w')
ax, secax_x, secax_y = px2coord_axis(ax, X, Y)
secax_x.set_xlabel('X')
secax_y.set_ylabel('Y')
fig.colorbar(cmap, label='vector angle (degree)')
ax.set_aspect('equal')
plt.show()
ベクトル場を任意の範囲の大きさのみで背景にカラーマップ
"""サンプルのベクトル場"""
X = np.arange(-10, 11, 1, dtype=np.float64)
Y = np.arange(-10, 11, 1, dtype=np.float64)
U, V = np.meshgrid(X, Y)
mag, ang = cv2.cartToPolar(U, V, angleInDegrees=True)
any_mag, any_ang = any_size_only(mag, ang, 5, 10)
X_shape, Y_shape = X.shape[0], Y.shape[0] # 1次元配列の場合
"""ベクトル場の大きさを背景にカラーマップ表示"""
fig, ax = plt.subplots(figsize=(10, 8))
cmap = plt.imshow(any_mag, cmap='jet', origin='lower')
ax.quiver(np.arange(X_shape), np.arange(Y_shape), U, V, color='w')
ax, secax_x, secax_y = px2coord_axis(ax, X, Y)
secax_x.set_xlabel('X')
secax_y.set_ylabel('Y')
fig.colorbar(cmap, label='vector size')
ax.set_aspect('equal')
plt.show()
HSV空間にベクトル場を任意の範囲の角度に色相を、大きさに明度を対応させて表示
cv2.normalize(any_ang, None, 0, 180, cv2.NORM_MINMAX)
とすることで、0~180に正規化するのがポイントである。
"""サンプルのベクトル場"""
X = np.arange(-10, 11, 1, dtype=np.float64)
Y = np.arange(-10, 11, 1, dtype=np.float64)
U, V = np.meshgrid(X, Y)
mag, ang = cv2.cartToPolar(U, V, angleInDegrees=True)
any_mag, any_ang = any_angle_only(mag, ang, 270, 60) # 270°から60°のみでカラーマップ表示
# any_mag, any_ang = any_angle_only(mag, ang, 270, 420) # こちらでも可能
X_shape, Y_shape = X.shape[0], Y.shape[0] # 1次元配列の場合
"""HSV色空間にベクトル場の角度に色を、大きさに明度を対応させて表示"""
hsv = np.zeros((Y_shape, X_shape, 3), dtype='uint8')
hsv[..., 0] = cv2.normalize(any_ang, None, 0, 180, cv2.NORM_MINMAX)
hsv[..., 1] = 255
hsv[..., 2] = cv2.normalize(any_mag, None, 0, 255, cv2.NORM_MINMAX)
flow_rgb = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)
"""ベクトル場の角度をカラーマップ表示"""
fig, ax = plt.subplots(figsize=(8, 8))
ax.imshow(flow_rgb, origin='lower')
ax.quiver(np.arange(X_shape), np.arange(Y_shape), U, V, color='w')
ax, secax_x, secax_y = px2coord_axis(ax, X, Y)
secax_x.set_xlabel('X')
secax_y.set_ylabel('Y')
ax.set_aspect('equal')
plt.show()
HSV空間にベクトル場を任意の範囲の大きさに色相と明度を対応させて表示
"""サンプルのベクトル場"""
X = np.arange(-10, 11, 1, dtype=np.float64)
Y = np.arange(-10, 11, 1, dtype=np.float64)
U, V = np.meshgrid(X, Y)
mag, ang = cv2.cartToPolar(U, V, angleInDegrees=True)
any_mag, any_ang = any_size_only(mag, ang, 5, 10)
X_shape, Y_shape = X.shape[0], Y.shape[0] # 1次元配列の場合
"""HSV色空間にベクトル場の大きさに色相と明度を対応させて表示"""
hsv = np.zeros((Y_shape, X_shape, 3), dtype='uint8')
hsv[..., 0] = cv2.normalize(any_mag, None, 0, 180, cv2.NORM_MINMAX)
hsv[..., 1] = 255
hsv[..., 2] = cv2.normalize(any_mag, None, 0, 255, cv2.NORM_MINMAX)
flow_rgb = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)
"""ベクトル場の大きさをカラーマップ表示"""
fig, ax = plt.subplots(figsize=(10, 8))
ax.imshow(flow_rgb, origin='lower')
ax.quiver(np.arange(X_shape), np.arange(Y_shape), U, V, color='w')
ax, secax_x, secax_y = px2coord_axis(ax, X, Y)
secax_x.set_xlabel('X')
secax_y.set_ylabel('Y')
ax.set_aspect('equal')
plt.show()
まとめ
ベクトル場の角度と大きさをカラー角度で表示する方法を紹介しました。ベクトル場は角度と大きさの2つの情報を一目で見たい場合が多いので、本記事が誰かのお役になれば幸いです。
参考資料