光流估计
概念
1、光流描述了第一帧到第二帧所有的像素点移动情况。假设一个像素点,在第一帧的坐标是(x1, y1),在第二帧的坐标是(x2, y2),则该点的光流设为(△x, △y),满足如下公式:
x2 = x1 + △x
y2 = y1 + △y
2、光流法也分为dense(右)和sparse(左)两个方向,简单来说sparse方法预测关键点的光流,dense方法预测全图像素的光流。
原理
光流的约束公式
参考:https://aistudio.baidu.com/projectdetail/4597614
说实话这个公式粗略了解下来真的很像雅可比矩阵。
f
(
x
+
h
)
=
h
∗
J
f
(
x
)
+
f
(
x
)
f(x+h) = h * J_{f}(x) + f(x)
f(x+h)=h∗Jf(x)+f(x)
雅可比矩阵的物理意义表示多元向量的改变量(坐标warp的方向和大小,是不是可以这么理解),也就是DaGan以及Fomm中使用雅可比矩阵warp关键点得到目标图像的原理。而光流估计是来估计像素值的warp方向以及大小
算法
FlowNet
开山之作
RAFT
data部分
dataloader抛出:两张邻帧输入,光流真值flow gt,有效像素valid mask
spatial transform:针对dense方法的变换,主要是resize flow gt的方法
def resize_sparse_flow_map(self, flow, valid, fx=1.0, fy=1.0):
ht, wd = flow.shape[:2]
### 生成输入图片的每个像素的坐标点
coords = np.meshgrid(np.arange(wd), np.arange(ht))
coords = np.stack(coords, axis=-1)
### 把(H, W, 2) ->(H * W, 2)
coords = coords.reshape(-1, 2).astype(np.float32)
flow = flow.reshape(-1, 2).astype(np.float32)
valid = valid.reshape(-1).astype(np.float32)
### 筛选掉无效坐标
coords0 = coords[valid>=1]
flow0 = flow[valid>=1]
ht1 = int(round(ht * fy))
wd1 = int(round(wd * fx))
### 坐标点直接 * scale_factor
coords1 = coords0 * [fx, fy]
flow1 = flow0 * [fx, fy]
xx = np.round(coords1[:,0]).astype(np.int32)
yy = np.round(coords1[:,1]).astype(np.int32)
### 过滤越界点
v = (xx > 0) & (xx < wd1) & (yy > 0) & (yy < ht1)
xx = xx[v]
yy = yy[v]
flow1 = flow1[v]
### 把变换后的flow gt根据坐标点填到全0 flow图中
flow_img = np.zeros([ht1, wd1, 2], dtype=np.float32)
valid_img = np.zeros([ht1, wd1], dtype=np.int32)
flow_img[yy, xx] = flow1
valid_img[yy, xx] = 1
return flow_img, valid_img
涉及到坐标变换的:flow gt其实可以理解为向量,上一帧该像素坐标指向下一帧该像素坐标的向量,所以坐标变换可以直接作用于flow gt。同理的翻转和裁剪都可以直接操作,只要保持输入图片对和flow gt一一对应。
仅光照颜色变换等:源码中是把两张图纵向拼接2 * (H,W,3)->(2H,W,3),统一做完变换再拆开即可。也就是需要保持邻帧图像对应的像素保持变化一致。
def spatial_transform(self, img1, img2, flow, valid):
# randomly sample scale
ht, wd = img1.shape[:2]
min_scale = np.maximum(
(self.crop_size[0] + 1) / float(ht),
(self.crop_size[1] + 1) / float(wd))
scale = 2 ** np.random.uniform(self.min_scale, self.max_scale)
scale_x = np.clip(scale, min_scale, None)
scale_y = np.clip(scale, min_scale, None)
if np.random.rand() < self.spatial_aug_prob:
# rescale the images
img1 = cv2.resize(img1, None, fx=scale_x, fy=scale_y, interpolation=cv2.INTER_LINEAR)
img2 = cv2.resize(img2, None, fx=scale_x, fy=scale_y, interpolation=cv2.INTER_LINEAR)
flow, valid = self.resize_sparse_flow_map(flow, valid, fx=scale_x, fy=scale_y)
if self.do_flip:
if np.random.rand() < 0.5: # h-flip
img1 = img1[:, ::-1]
img2 = img2[:, ::-1]
flow = flow[:, ::-1] * [-1.0, 1.0]
valid = valid[:, ::-1]
margin_y = 20
margin_x = 50
y0 = np.random.randint(0, img1.shape[0] - self.crop_size[0] + margin_y)
x0 = np.random.randint(-margin_x, img1.shape[1] - self.crop_size[1] + margin_x)
y0 = np.clip(y0, 0, img1.shape[0] - self.crop_size[0])
x0 = np.clip(x0, 0, img1.shape[1] - self.crop_size[1])
img1 = img1[y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
img2 = img2[y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
flow = flow[y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
valid = valid[y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
return img1, img2, flow, valid
消除变化(eraser_transform):不太理解,源码里会单独对另一帧的图像做消除,但是并不会修改valid mask的内容,也就是需要模型自己做补全。
def eraser_transform(self, img1, img2):
ht, wd = img1.shape[:2]
if np.random.rand() < self.eraser_aug_prob:
mean_color = np.mean(img2.reshape(-1, 3), axis=0)
for _ in range(np.random.randint(1, 3)):
x0 = np.random.randint(0, wd)
y0 = np.random.randint(0, ht)
dx = np.random.randint(50, 100)
dy = np.random.randint(50, 100)
img2[y0:y0+dy, x0:x0+dx, :] = mean_color
return img1, img2
model
1、特征提取部分
先CNN网络提特征,然后做矩阵相乘(相似度计算)
PS:做完矩阵相乘一样除以 sqrt(dim),和transformer一样防止尺度变大。
2、后续部分用的GRU门控单元,挺像LSTM的结构的,只是对内部的门控做了一些改动。
PS:
tanh和sigmoid的运用,tanh和sigmoid分别可以把输入非线性映射到[-1, 1], [0, 1],sigmoid映射到[0, 1]的特性作为门控开关,tanh映射到[-1, 1]的特性作为内部状态位。
为什么要这么映射?
(1)非线性激活函数强化学习能力
(2)因为是类似RNN、LSTM的循环结构,需要参数映射到x<|1|保证叠乘不会爆炸,解决梯度爆炸梯度消失问题
https://zhuanlan.zhihu.com/p/350613342
3、上采样部分
上采样中主要解释下shape代表的意义
def upsample_flow(self, flow, mask):
""" Upsample flow field [H/8, W/8, 2] -> [H, W, 2] using convex combination """
'''
flow->(b, 2, H/8, W/8)
mask->(b, 576, H/8, W/8) 576 = 8*8*9 (1/8下采样,每个坐标xy各9个偏移量[-4, 4])
'''
N, _, H, W = flow.shape
mask = mask.view(N, 1, 9, 8, 8, H, W)
''' 对dim 2做softmax,即对当前坐标的9个偏移量加权(下面mask会乘进flow里)'''
mask = torch.softmax(mask, dim=2)
up_flow = F.unfold(8 * flow, [3,3], padding=1)
up_flow = up_flow.view(N, 2, 9, 1, 1, H, W)
up_flow = torch.sum(mask * up_flow, dim=2)
up_flow = up_flow.permute(0, 1, 4, 2, 5, 3)
return up_flow.reshape(N, 2, 8*H, 8*W)
评价以及可视化
光流可视化的图片或者视频中出现的颜色代表对应的变化方向以及幅度