1 | initial version |
Here's the code I used to generate the image that I added to the original question. I modified it just a little bit to map radius to value instead of saturation to be in closer correspondence with the optical flow output. Admittedly, HSV wheels generally assign radius to saturation. My goal here is to provide the clearest possible data visualization of optical flow. Of course, most of the responses to my question seem to indicate that no one else considers this to be very important, so whatever. Just trying to help.
from PIL import Image, ImageDraw
import numpy as np
import cv2
wh = 200
wh_2 = wh / 2
# Generate a standard HSV color wheel
hsv_color_wheel = np.zeros((wh, wh, 3))
for y in range(wh):
yy = y - wh_2
for x in range(wh):
xx = x - wh_2
radius = math.sqrt(xx**2 + yy**2)
if radius < wh_2:
angle = math.atan2(xx, yy)
angle = 360 - (angle * 180 / np.pi)
# hsv_color_wheel[x, y] = (angle, radius / wh_2, 255) # Radius as saturation, which is a more conventional HSV wheel
hsv_color_wheel[x, y] = (angle, 1, radius / wh_2 * 255) # Radius as value, which maps optical flow's magnitude
hsv_color_wheel = np.asarray(hsv_color_wheel, dtype=np.float32)
hsv_color_wheel = cv2.cvtColor(hsv_color_wheel, cv2.COLOR_HSV2RGB)
hsv_color_wheel_flat = hsv_color_wheel.reshape(wh * wh, 3)
hsv_color_wheel_flat_tuple = [tuple(v) for v in hsv_color_wheel_flat]
hsv_color_wheel_img = Image.new("RGB", (wh, wh))
hsv_color_wheel_img.putdata(hsv_color_wheel_flat_tuple)
# Generate some shifted boxes to demonstrate optical flow
box_sz = wh / 5
box_sz_2 = 2 * wh / 5
box_sz_3 = 3 * wh / 5
dim = (wh, wh)
img1 = Image.new("L", dim)
img2 = Image.new("L", dim)
shift = 5
draw = ImageDraw.Draw(img1)
draw.ellipse((wh/5+10, wh/5+10, 4*wh/5-10, 4*wh/5-10), 255)
draw.ellipse((wh/5+20, wh/5+20, 4*wh/5-20, 4*wh/5-20), 0)
draw = ImageDraw.Draw(img2)
draw.ellipse((wh/5+10 - shift, wh/5+10 - shift, 4*wh/5-10 + shift, 4*wh/5-10 + shift), 255)
draw.ellipse((wh/5+20 - shift, wh/5+20 - shift, 4*wh/5-20 + shift, 4*wh/5-20 + shift), 0)
img1_arr = np.array(img1)
img2_arr = np.array(img2)
# Run optical flow and visualize two possible ways
for method in [1, 2]:
flow = cv2.calcOpticalFlowFarneback(img1_arr, img2_arr, None, 0.5, 3, 40, 3, 5, 1.2, 0)
# Convert from cartesian to polar
mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1])
# Create an HSV image
hsv = np.zeros((img1.size[0], img1.size[1], 3))
hsv[:,:,1] = 1 # Full saturation
# Set hue from the flow direction
if method == 1:
hsv[:,:,0] = ang * (180 / np.pi / 2)
else:
hsv[:,:,0] = 360 - (ang * (180 / np.pi))
# Set value from the flow magnitude
hsv[:,:,2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)
# Convert HSV to int32's
hsv = np.asarray(hsv, dtype=np.float32)
rgb_flow = cv2.cvtColor(hsv,cv2.COLOR_HSV2RGB)
# Convert to an image
rgb_flow_flat = rgb_flow.reshape(rgb_flow.shape[0] * rgb_flow.shape[1], 3)
rgb_flow_flat_tuple = [tuple(v) for v in rgb_flow_flat]
flow_img = Image.new("RGB", img1.size)
flow_img.putdata(rgb_flow_flat_tuple)
analysis_img = Image.new("RGB", (img1.size[0] * 4, img2.size[1]))
analysis_img.paste(img1, (0, 0))
analysis_img.paste(img2, (img1.size[0], 0))
analysis_img.paste(flow_img, (img1.size[0] * 2, 0))
analysis_img.paste(hsv_color_wheel_img, (img1.size[0] * 3, 0))
print("Original visualization method:" if method == 1 else "Proposed visualization method:")
display(analysis_img)