OpenCV的calcOpticalFlowFarneback函数输出未知

9

我一直在想OpenCV的calcOpticalFlowFarneback函数返回的光流矩阵有什么作用。如果我运行以下Python代码:

flow = cv2.calcOpticalFlowFarneback(cv2.UMat(prvs),cv2.UMat(next), None, 0.5, 3, 15, 3, 5, 1.2, 0)

我将得到一个与prvsnext帧大小相同的矩阵,每个位置都包含一个由两个元素(x,y)组成的向量。我的问题是...该向量是从prvsnext还是从nextprvs?

谢谢。


1
你可以通过查看数值来验证,从prvsnext。像素在prvs[y, x]处移动flow[y, x]个像素以到达next[y', x']。换句话说,next[flow[y, x]] = prvs[y, x]。(这只是一个例子,在此需要特别注意索引顺序)。 - alkasm
然后,我不明白为什么它给我错误的结果。我试图使用该流来执行运动插值并插入中间帧。如果我想将一个点从“prvs”帧移动到“prvs”和“next”之间的确切中间点,我应该考虑什么? prvs[x,y] + flow[x,y] 还是 prvs[x,y] - flow[x,y]?@AlexanderReynolds - user2229358
为什么你使用[y,x]而不是[x,y]?流向量元素改变了吗?首先是y分量,其次是x分量吗?@AlexanderReynolds - user2229358
因为它们是数组,所以图像可以使用(row, col)(y, x)进行索引。prvs[y, x]是一个像素flow[y, x]是一个向量,你不应该将它们相加。从技术上讲,应该是next[ [y, x] + flow[y, x][::-1] ] = prvs[y, x],这里的[::-1]是因为我认为flow[y, x]会给你(x, y)顺序的坐标,所以[::-1]将它们反转为[y, x]进行索引。当我回家后,我可以尝试一下并给你更好的答复。 - alkasm
1个回答

14

光流方法的通常目的是在两幅图像(或视频帧)中找到每个像素(如果密集)或每个特征点(如果稀疏)的速度分量。这个想法是,帧 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)的逆序排列。数组使用(行,列)索引排序,这意味着y组件首先出现,然后是x组件。请特别注意flow[y,x]是一个向量,其第一个元素是x坐标,第二个元素是y坐标—因此我添加了y + flow[y,x,1]x + flow[y,x,0].在calcOpticalFlowFarneback()的文档中也有同样的写法:

  

该函数使用Farneback算法为每个prev像素找到光流。

prev(y,x) ~ next(y + flow(y,x)[1], x + flow(y,x)[0])
稠密光流算法期望像素点不要离其初始位置太远,因此通常用于视频中——每帧之间没有太大的变化。如果每帧之间有巨大的差异,你可能无法得到正确的估计。当然,金字塔分辨率模型的目的是帮助处理更大的跳跃,但您需要注意选择合适的分辨率比例。这里有一个完整的例子。我将从我今年早些时候在温哥华拍摄的这个短时间序列开始。我将创建一个函数,为每个像素指定方向并用颜色表示,将光流的大小用该颜色的亮度表示。这意味着亮度更高的像素将对应于更高的光流,并且颜色对应于方向。这也是OpenCV光流教程上最后一个示例所做的。
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')

并且 这是结果视频

然而,你想要做的是在帧之间插值。这有点令人困惑,因为最好的方法是使用cv2.remap(),但是这个函数的工作方向与我们想要的相反。 光流告诉我们像素哪里,但是remap()想知道像素来自哪里。所以实际上,我们需要交换remap的光流计算顺序。在这里查看我关于remap()函数的详细解释。

所以在这里,我创建了一个名为interpolate_frames()的函数,它可以从光流中插值出你想要的任意数量的帧。 这完全按照我们在评论中讨论的方式工作,但是请注意在calcOpticalFlowFarneback()内部翻转了currprev的顺序。

由于帧间运动非常高,上面的延时视频不是很合适。 相反,我将使用另一个视频的短片段,在与输入相同的位置拍摄。

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()

这里是输出的结果。每个原始帧都有4个帧,所以它放慢了4倍。当然,这样做会产生黑色边缘像素,因此您可能希望对您的帧进行某种边框插值(可以使用cv2.copyMakeBorder()),以重复类似的边缘像素,或者对最终输出进行裁剪以消除这些像素。请注意,大多数视频稳定算法确实为类似的原因而裁剪图像。这也是为什么当你将手机相机切换到视频模式时,你会注意到更大的焦距(看起来有点变焦)的原因之一。


然后,有了流向量、前一帧和下一帧,要找到插值帧中像素的值,我应该使用这个。例如,如果我想在前一帧中找到x坐标: x_prev = x - 0.5 * flow[x,y][0] 以及下一帧中的x坐标: x_next = x + 0.5 * flow[x,y][0] 我是对的吗? - user2229358
没错!这就是如何生成帧间插值的方法。再次注意顺序,应该是flow[y, x]。但是!这是一个重要的问题:流向量将给出像素间的测量值(即flow[y, x][0]可能是3.105)。当然,您需要四舍五入或截断为整数索引。但是如果两个值在四舍五入后映射到同一点会发生什么?同样地,如果有一些像素没有被映射怎么办?这在很多地方都会发生。您应该使用cv2.remap来进行插值,并且它会为您处理这些问题。 - alkasm
@kelirkenan,此外,请查看我的答案这里,展示如何使用cv2.remap()。文档可能有点令人困惑,但这应该可以让您迅速上手。此外,这个答案展示了如何立即从光流结果中应用cv2.remap() - alkasm
完美!谢谢。 - user2229358
@kelirkenan 我已经进行了更新,展示如何使用remap()进行插值。我之前还没有尝试过这个功能,所以只是好奇它是如何工作的。来看看吧,结果非常酷炫! - alkasm
这是一个很好的答案,但我想知道它是否可以用于来自numpy数组的零散随机数据。我有一个大的2D数组中的值在0-60之间变化。我已经将数组除以255进行归一化,然后传递到cv2.calcOpticalFlowFarneback(curr, prev, None, *optflow_params)函数中。最大流量值似乎非常小1.492787e-11,并且我在重新映射的数组中看不到任何值。我也尝试调整了optflow_params。有什么想法吗? - wuffwuff

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接