Date: 2023-09-07
省流:光流法计算prev 到next 的flow,之后flow (加上当前位置坐标)生成flow_map,利用flow_map 和OpenCV remap 函数,可以将next remap 得到 prev,即remap 后一帧得到前一帧图像。
疑问
一直以来我都有个疑问,光流(optical flow)中存储的是 next 相对于 prev 的平移量(dx,dy)。但是为什么使用光流生成的 map(以下简称 flow_map)表进行 remap 操作的时候,却是用 next+flow_map 得到 prev?平移量不是 prev 到 next 的平移量吗?不应该是 prev+flow_map 得到next 吗?
光流值的含义
坐标原点在图像左上角,假设在 prev 图像上有一个像素点为
(
x
1
,
y
1
)
(x_1,y_1)
(x1,y1),在 next 图像上对应的像素点为
(
x
2
,
y
2
)
(x_2,y_2)
(x2,y2) ,那么位移表示为
(
d
x
,
d
y
)
=
(
x
2
,
y
2
)
−
(
x
1
,
y
1
)
(dx,dy)=(x_2,y_2)-(x_1,y_1)
(dx,dy)=(x2,y2)−(x1,y1)
prev 图像
next 图像
图上整体是往左上平移的,也就是如果原点在左上角,那么dx 和dy 都为负数。
inst = cv2.optflow.createOptFlow_DeepFlow()
flow = inst.calc(input_image_gray, output_image_gray, None)
下面是 flow 里面的值,确实也都是负数
接下来我们用 flow_map 来进行重映射。
remap 重映射
在利用 flow 进行映射之前我们需要了解两个概念,前向映射和后向映射。
前向映射(Forward Mapping)和后向映射(Inverse Mapping)是在计算机图形学和计算机视觉领域中用于处理图像和几何变换的两种不同方法。
前向映射(Forward Mapping):
- 前向映射是一种直接的方法,它将输入图像中的每个像素通过变换映射到输出图像中的相应位置。
- 在前向映射中,我们遍历输入图像中的每个像素,将其通过变换计算出在输出图像中的位置,然后将像素值复制到该位置。
- 前向映射的优点是简单直观,但在某些情况下可能会导致输出图像中的某些位置没有被填充或者有重叠。
后向映射(Inverse Mapping):
- 后向映射是一种反向的方法,它将输出图像中的每个像素通过逆变换映射到输入图像中的相应位置。
- 在后向映射中,我们遍历输出图像中的每个像素,通过逆变换计算出它在输入图像中的位置,并从输入图像中取得对应位置的像素值。
- 后向映射的优点是可以确保输出图像中的每个位置都有对应的像素值,避免了前向映射中的缺失或重叠问题。
选择使用前向映射还是后向映射取决于具体的应用需求和变换操作。前向映射通常用于简单的几何变换,而后向映射通常用于复杂的非线性变换或需要确保像素一对一映射的情况。每种方法都有其优点和限制,根据具体情况选择合适的映射方法是重要的。
以上引用内容由 ChatGPT 回答得出。那么 remap 是属于哪种映射方式?知道了它属于哪种映射方式有助于我们理解映射过程。虽然ChatGPT 给出的答案是 remap 函数是前向映射,但按照我的理解它似乎是后向映射。
remap 是前向映射还是后向映射?
我做了这样一个实验:
生成一个(x,y) 都是(100,100)的map 表,如果remap 是前向映射,那么遍历原图中所有的坐标点,并把它的像素映射到目标图上(100,100) 的位置,那么目标图像将会是一个只在(100,100)处有值,其他部分为黑色(没有值)的图像。
而实际测试上生成的目标图是个纯色的图像。它更符合后向映射的描述,即遍历目标图每个像素点,从原图上取值填充过去,对于这个map 表中所有元素都是(100,100)的映射来说,目标图上所有像素取值来源相同,就应该是个纯色图像。
鉴于 remap 函数可能是后向映射,我们不可能用 prev+flow_map 生成的map 表得到 next。反而是 next+flow_map 生成的map 表有可得到prev。
下面是一段将图像按照 flow 中表示的位移进行 remap 的代码,内含用 flow 生成 flow_map 的操作。
def cv_warp(src, flow):
# 生成网格点坐标矩阵
h, w = flow.shape[:2]
warp_grid_x, warp_grid_y = np.meshgrid(np.linspace(0, w - 1, w), np.linspace(0, h - 1, h))
# 因为flow 里面存储的是dx dy, 而remap 需要的是点到点的映射,所以需要加上x,y 坐标,也就是上面生成的网格点坐标矩阵
# axis=-1 的作用是将分开的grid_x(x0,x1,...) 和grid_y(y0,y2,...) 合并成(x0,y0),(x1,y1),... 的形式
flow_inv = flow + np.stack((warp_grid_x, warp_grid_y), axis=-1)
flow_inv = flow_inv.astype(np.float32)
warped = cv2.remap(src, flow_inv, None, cv2.INTER_CUBIC)
return warped
我们将 next 和 flow 输入到上面的函数,得到如下结果:
它基本上可以和 prev 的内容对齐。
如果我们想要从 prev remap 得到 next,则需要将 flow 的值取反再生成map 表。
remap 函数真正的映射关系
OpenCV 官方文档对于 remap 函数的解释中有这么一个公式:
d
s
t
(
x
,
y
)
=
s
r
c
(
m
a
p
x
(
x
,
y
)
,
m
a
p
y
(
x
,
y
)
)
dst(x,y)=src(mapx(x,y),mapy(x,y))
dst(x,y)=src(mapx(x,y),mapy(x,y))
我们实际带入数据来理解一下这公式。先来看dst 图像中的第(0,0) 个像素,假设map_x(0,0) 中存储的是10,map_y(0,0) 存储的是10,那么它表示的是目标图像上第(0,0) 位置的像素值,需要在源图像上第(10,10) 中取得(不考虑插值)。你会发现这个像素从原图像的(10,10) 移动到了目标图像的(0,0),它是往左上角移动了,但实际你计算光流的话这个点的(dx,dy)=(0,0)-(10,10) 是(-10,-10),map 表中存储的是(10,10),但是flow 中存的是(-10,-10),有一点反直觉对不对?
现在我们回头看看我最初的疑问是不是合理的 “平移量不是 prev 到 next 的平移量吗?不应该是 prev+flow_map 得到next 吗?” flow 中村的是 prev 到 next 的平移量这个理解没问题,问题在于(dx,dy) 的坐标。当我们把光流的意义用下面的表达式表示
(
d
x
,
d
y
)
=
n
e
x
t
(
x
,
y
)
−
p
r
e
v
(
x
′
,
y
′
)
(dx,dy) = next(x,y)-prev(x',y')
(dx,dy)=next(x,y)−prev(x′,y′)你会发现flow 中每一点的偏移量,是表示的next 图像中对应坐标中的平移量,所以当你试图用flow_map 对prev 做remap 时,其实是将当前的平移量应用到了另一个点上,这显然是错误的。 而next+flow_map 进行remap 才是合理的,同样带入数据,目标图上(0,0)的像素,要在源图像(此时为 next)的(-10,-10)取得,这表示像素向右下角移动了,和前面src 变换成dst 移动的方向相反,于是dst 还原成了src。
光流的可视化
光流的可视化方法不唯一,可视化方法不同,则相同的颜色/亮度会表示的含义不同。
有些方法用亮度表示位移大小,有些方法用饱和度表示位移大小,也有可能相同的位移方向在不同的转换方法中表现为不同颜色。
个人认为不必过分追求统一的表示方法,与人交流时提前明确表示方法即可。
(下面光流图和上面计算光流的输入图不是同一组)
OpenCV 官方文档中的可视化方法:OpenCV: Optical Flow
csdn 上某博主的可视化方法:光流介绍以及FlowNet学习笔记_光流可视化
参考链接
- 一文搞懂光流 光流的生成,可视化以及映射(warp)_光流可视化_深山里的小白羊的博客-CSDN博客
- opencv光流预测和remap重映射函数使用-腾讯云开发者社区-腾讯云 (tencent.com)
- c++ - OpenCV warping image based on calcOpticalFlowFarneback - Stack Overflow
- python - How do I use OpenCV’s remap function? - Stack Overflow
这个链接里有个答案比较详细的解释了他/她对 remap 映射的理解,以及remap选择这种映射的原因。 - OpenCV: Geometric Image Transformations(remap)