我一直想知道 OpenCV 的 calcOpticalFlowFarneback 函数返回的光流矩阵说明了什么。如果我计算这个 Python 行:
flow = cv2.calcOpticalFlowFarneback(cv2.UMat(prvs),cv2.UMat(next), None, 0.5, 3, 15, 3, 5, 1.2, 0)
我将得到一个与
prvs
和 next
帧大小相同的矩阵,对于每个位置,包含两个元素 (x,y) 的向量。我的问题是...那个向量是从 prvs
到 next
或从 next
到 prvs
的向量?谢谢。
最佳答案
光流方法的一般目的是找到两个图像(或视频帧,通常)之间每个像素(如果密集)或每个特征点(如果稀疏)的速度分量。这个想法是第 N-1 帧中的像素移动到第 N 帧中的新位置,这些像素的位置差异就像一个速度向量。这意味着前一帧中位置 (x, y) 的像素将位于下一帧中的位置 (x+v_x, y+v_y)。
对于像素值,这意味着对于给定位置 (x, y), prev_frame(x, y)
处的像素值与 curr_frame(x+v_x, y+v_y)
处的像素值相同。或者更具体地说,就实际数组索引而言:
prev_frame[y, x] == curr_frame[y + flow[y, x, 1], x + flow[y, x, 0]]
请注意此处 (x, y) 的相反顺序。数组以 (row, col) 排序进行索引,这意味着 y 组件首先出现,然后是 x 组件。请特别注意
flow[y, x]
是一个向量,其中第一个元素是 x 坐标,第二个元素是 y 坐标——因此我添加了 y + flow[y, x, 1]
和 x + flow[y, x, 0]
。你会看到用 the docs for calcOpticalFlowFarneback()
写的同样的东西:密集光流算法期望像素离它们开始的位置不是很远,因此它们通常用于视频——每帧都没有大量变化。如果每一帧都有很大的差异,你可能不会得到正确的估计。当然,金字塔分辨率模型的目的是帮助实现更大的跳跃,但您需要注意选择合适的分辨率比例。
这是一个完整的示例。我将从今年早些时候在温哥华拍摄的 this short timelapse 开始。我将创建一个函数,该函数将每个像素的流动方向归因于一种颜色,并将流动的幅度归因于该颜色的亮度。这意味着更亮的像素将对应更高的流量,颜色对应于方向。这也是他们在 OpenCV optical flow tutorial 上的最后一个示例中所做的。
import cv2
import numpy as np
def flow_to_color(flow, hsv):
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)
return cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
cap = cv2.VideoCapture('vancouver.mp4')
fps = cap.get(cv2.CAP_PROP_FPS)
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter('optflow.mp4', fourcc, fps, (w, h))
optflow_params = [0.5, 3, 15, 3, 5, 1.2, 0]
frame_exists, prev_frame = cap.read()
prev = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)
hsv = np.zeros_like(prev_frame)
hsv[..., 1] = 255
while(cap.isOpened()):
frame_exists, curr_frame = cap.read()
if frame_exists:
curr = cv2.cvtColor(curr_frame, cv2.COLOR_BGR2GRAY)
flow = cv2.calcOpticalFlowFarneback(prev, curr, None, *optflow_params)
rgb = flow_to_color(flow, hsv)
out.write(rgb)
prev = curr
else:
break
cap.release()
out.release()
print('done')
和 here's the resulting video 。
但是,您要做的是在帧之间进行插值。这有点令人困惑,因为最好的方法是使用
cv2.remap()
但这个函数在我们想要的相反方向上工作。光流告诉我们像素去了哪里,但 remap()
想知道像素来自哪里。所以实际上,我们需要将光流计算的顺序交换为 remap
。有关 remap()
函数的详细说明,请参阅我的回答 here。所以在这里我创建了一个函数
interpolate_frames()
,它可以插入你想要的流中的多少帧。这与我们在评论中讨论的完全一样,但请注意 curr
中 prev
和 calcOpticalFlowFarneback()
的翻转顺序。上面的延时视频是一个糟糕的候选者,因为帧间移动非常高。相反,我将在与输入相同的位置使用 short clip from another video 镜头。
import cv2
import numpy as np
def interpolate_frames(frame, coords, flow, n_frames):
frames = [frame]
for f in range(1, n_frames):
pixel_map = coords + (f/n_frames) * flow
inter_frame = cv2.remap(frame, pixel_map, None, cv2.INTER_LINEAR)
frames.append(inter_frame)
return frames
cap = cv2.VideoCapture('vancouver.mp4')
fps = cap.get(cv2.CAP_PROP_FPS)
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter('optflow-inter1a.mp4', fourcc, fps, (w, h))
optflow_params = [0.5, 3, 15, 3, 5, 1.2, 0]
frame_exists, prev_frame = cap.read()
prev = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)
y_coords, x_coords = np.mgrid[0:h, 0:w]
coords = np.float32(np.dstack([x_coords, y_coords]))
while(cap.isOpened()):
frame_exists, curr_frame = cap.read()
if frame_exists:
curr = cv2.cvtColor(curr_frame, cv2.COLOR_BGR2GRAY)
flow = cv2.calcOpticalFlowFarneback(curr, prev, None, *optflow_params)
inter_frames = interpolate_frames(prev_frame, coords, flow, 4)
for frame in inter_frames:
out.write(frame)
prev_frame = curr_frame
prev = curr
else:
break
cap.release()
out.release()
和 here's the output 。原始文件中的每一帧都有 4 帧,因此速度减慢了 4 倍。当然,会有黑色边缘像素进入,因此在执行此操作时,您可能想要对帧进行某种边框插值(您可以使用
cv2.copyMakeBorder()
)来重复类似的边缘像素,和/或裁剪最终输出有点摆脱它。请注意,大多数视频稳定算法确实出于类似原因裁剪图像。这就是为什么当您将手机摄像头切换为视频时,您会注意到焦距更大(看起来放大了一点)的部分原因。关于python - OpenCV 的 calcOpticalFlowFarneback 的未知输出,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/47732102/