【问题标题】:Unknown output of OpenCV's calcOpticalFlowFarnebackOpenCV 的 calcOpticalFlowFarneback 的未知输出
【发布时间】:2018-05-23 17:55:28
【问题描述】:

我一直想知道 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 的向量?

谢谢。

【问题讨论】:

  • 当然是从prvsnext!您也可以通过查看这些值来验证这一点。 prvs[y, x] 处的像素将移动flow[y, x] 像素以到达next[y', x']。换句话说,next[flow[y, x]] = prvs[y, x]。 (这只是一个示例,您需要特别注意此处的索引顺序)。
  • 那么,我不明白为什么它会给我带来不好的结果。我正在尝试使用该流程来执行运动插值和插值中间帧。如果我想将一个点从prvs 帧移动到prvsnext 之间的确切中间点,我应该考虑什么? prvs[x,y] + flow[x,y] 还是 prvs[x,y] - flow[x,y]? @亚历山大雷诺兹
  • 你为什么用[y,x]而不是[x,y]?流向量元素是否改变了?首先是 y 分量,然后是 x 分量? @亚历山大雷诺兹
  • [y, x] 因为它们是图像,它们是数组,因此用(row, col) 索引,即(y, x)prvs[y, x]pixelflow[y, x]vector,你不应该添加它们。从技术上讲,它应该是next[ [y, x] + flow[y, x][::-1] ] = prvs[y, x],那里的[::-1],因为我认为flow[y, x] 会以(x,y)顺序为您提供坐标,因此[::-1] 将它们反转为[y, x] 以进行索引。我可以试试看,等我回家后再给你更好的答复。

标签: python python-2.7 opencv computer-vision opticalflow


【解决方案1】:

光流方法的一般目的是找到两个图像(通常是视频帧)之间每个像素(如果密集)或每个特征点(如果稀疏)的速度分量。这个想法是第 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()中写的同样的东西:

该函数使用 Farneback 算法为每个 prev 像素找到一个光流,以便

prev(y,x) ~ next(y + flow(y,x)[1], x + flow(y,x)[0])

密集光流算法期望像素离它们开始的地方不是很远,因此它们通常用于视频——每帧都没有大量的变化。如果每一帧都存在巨大差异,那么您可能无法得到正确的估计。当然,金字塔分辨率模型的目的是帮助实现更大的跳跃,但您需要注意选择合适的分辨率比例。

这是一个完整的示例。我将从今年早些时候在温哥华拍摄的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(),它将插入到流中您想要的任意多帧。这与我们在 cmets 中讨论的完全一样,但请注意 currprevcalcOpticalFlowFarneback() 中的翻转顺序。

上面的延时视频是一个糟糕的候选,因为帧间移动非常高。相反,我将在与输入相同的位置使用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())来重复相似的边缘像素,和/或裁剪最终输出有点摆脱它。请注意,大多数视频稳定算法出于类似原因裁剪图像。这就是为什么当您将手机摄像头切换到视频时,您会发现焦距变大(看起来放大了一点)的部分原因。

【讨论】:

  • 然后,有了流向量,前一帧和下一帧,要在插值帧中找到一个像素的值,我应该使用它。例如,如果我想找到上一帧的 x 坐标:x_prev = x - 0.5 * flow[x,y][0] 和下一帧的 x 坐标:x_next = x + 0.5 * flow[x,y][0] 对吗?
  • 是的!这就是您可以生成帧间插值的方式。再次注意这里的订购,应该是flow[y, x]。然而!这一点很重要:流向量将给出像素间测量值(即flow[y, x][0] 可能是3.105)。您当然需要四舍五入或截断为整数索引。但是如果两个值在四舍五入后映射到同一个点会发生什么?同样,如果有一些像素没有被映射到会发生什么?这将在很多地方发生。您应该使用cv2.remap 为您进行插值,它会处理好。
  • @kelirkenan 也可以查看我的答案here,展示如何使用cv2.remap()。文档可能有点令人困惑,这应该可以让您立即上手。此外,this 答案显示了如何从光流结果中立即应用cv2.remap()
  • 完美!谢谢。
  • @kelirkenan 我实际上继续并更新了如何使用remap() 进行插值。我自己没有这样做,所以我只是好奇它是如何工作的。看看,结果很酷!
猜你喜欢
  • 1970-01-01
  • 2013-07-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-06-05
相关资源
最近更新 更多