MVSNet网络pytorch版的单应性函数实现:
def homo_warping(src_fea, src_proj, ref_proj, depth_values):
# src_fea: [B, C, H, W]
# src_proj: [B, 4, 4]
# ref_proj: [B, 4, 4]
# depth_values: [B, Ndepth]
# out: [B, C, Ndepth, H, W]
batch, channels = src_fea.shape[0], src_fea.shape[1]
num_depth = depth_values.shape[1]
height, width = src_fea.shape[2], src_fea.shape[3]
with torch.no_grad():
proj = torch.matmul(src_proj, torch.inverse(ref_proj)) # 把像素坐标从ref到src 但是目的是要把源到参考? 答:特征是要src到ref,但是投影坐标是从ref到src
rot = proj[:, :3, :3] # [B,3,3]
trans = proj[:, :3, 3:4] # [B,3,1]
# 生成网格坐标 meshgrid是ref的,因为矩阵从ref->src
y, x = torch.meshgrid([torch.arange(0, height, dtype=torch.float32, device=src_fea.device),
torch.arange(0, width, dtype=torch.float32, device=src_fea.device)])
y, x = y.contiguous(), x.contiguous()
y, x = y.view(height * width), x.view(height * width) # 展平为height * width个元素
xyz = torch.stack((x, y, torch.ones_like(x))) # [3, H*W] xyz是要得到像素坐标系(x,y,1) [[x1...xn],[y1...yn],[1...1]],1 是齐次坐标中的常数分量
xyz = torch.unsqueeze(xyz, 0).repeat(batch, 1, 1) # [B, 3, H*W] # 在指定维度 dim 插入一个新的维度
rot_xyz = torch.matmul(rot, xyz) # [B, 3, H*W]
rot_depth_xyz = rot_xyz.unsqueeze(2).repeat(1, 1, num_depth, 1) * depth_values.view(batch, 1, num_depth, 1) # [B, 3, Ndepth, H*W] 对于每个深度值的三维坐标。这个操作通过将深度值应用于旋转后的坐标,得到每个深度层的三维坐标
proj_xyz = rot_depth_xyz + trans.view(batch, 3, 1, 1) # [B, 3, Ndepth, H*W] 将旋转和位移应用到三维坐标上的结果,得到的是每个深度值的三维坐标
proj_xy = proj_xyz[:, :2, :, :] / proj_xyz[:, 2:3, :, :] # [B, 2, Ndepth, H*W] 将 proj_xyz 的前三维(x 和 y)除以第三维(z)来将三维坐标投影到二维平面上。
proj_x_normalized = proj_xy[:, 0, :, :] / ((width - 1) / 2) - 1
proj_y_normalized = proj_xy[:, 1, :, :] / ((height - 1) / 2) - 1
proj_xy = torch.stack((proj_x_normalized, proj_y_normalized), dim=3) # [B, Ndepth, H*W, 2]
grid = proj_xy
# grid 是一个采样网络,例如grid(0,0)表示输出图像的第一个像素是输入图像【1.5,1.5】位置的像素,实际上这个位置的像素值由插值算出来
# grid = [[[1.5, 1.5], [2.5, 2.5]], // 第一行输出的两个像素位置
# [[1.5, 1.5], [2.5, 2.5]]] // 第二行输出的两个像素位置
# 前面的几步都是为了生成采样网络,为了对原图像的特征图进行采样,采样的过程已经包含了将源图像投射到参考图像坐标系,并且建立了深度层(这里相当于是初始深度,后面还要计算实际的)
warped_src_fea = F.grid_sample(src_fea, grid.view(batch, num_depth * height, width, 2), mode='bilinear',
padding_mode='zeros')#
warped_src_fea = warped_src_fea.view(batch, channels, num_depth, height, width)
return warped_src_fea
0. 坐标转换
proj = torch.matmul(src_proj, torch.inverse(ref_proj)) # 把像素坐标从ref到src 但是目的是要把源到参考? 答:特征是要src到ref,但是投影坐标是从ref到src
rot = proj[:, :3, :3] # [B,3,3]
trans = proj[:, :3, 3:4] # [B,3,1]
# 生成网格坐标 meshgrid是ref的,因为矩阵从ref->src
这里非常关键的一点是,proj矩阵是把ref坐标转换到src坐标(src_proj乘ref-proj的逆),
就是采样的时候要按scr坐标系下采样他的特征值,比如ref的点x0,y0 要转到scr下的对应的x0’,y0’,来得到scr的特征值,作为ref 坐标下x0,y0的特征值,对应F.grid_sample函数
1. 生成采样网格 grid
y, x = torch.meshgrid([torch.arange(0, height, dtype=torch.float32, device=src_fea.device),
torch.arange(0, width, dtype=torch.float32, device=src_fea.device)])
y, x = y.contiguous(), x.contiguous()
y, x = y.view(height * width), x.view(height * width)
xyz = torch.stack((x, y, torch.ones_like(x))) # [3, H*W]
这里我们生成了图像的二维像素网格坐标 (x, y)
,这里的坐标是参考图像坐标系下的坐标:
torch.meshgrid
用于生成图像的网格坐标。x
和y
表示图像的每个像素在二维平面上的位置。torch.stack((x, y, torch.ones_like(x)))
将这些坐标堆叠成[3, H*W]
形状的张量,其中第三个维度是1,表示齐次坐标。
2. 应用旋转和平移
rot_xyz = torch.matmul(rot, xyz) # [B, 3, H*W]
rot_depth_xyz = rot_xyz.unsqueeze(2).repeat(1, 1, num_depth, 1) * depth_values.view(batch, 1, num_depth, 1) # [B, 3, Ndepth, H*W]
proj_xyz = rot_depth_xyz + trans.view(batch, 3, 1, 1) # [B, 3, Ndepth, H*W]
torch.matmul(rot, xyz)
计算旋转后的坐标。rot_depth_xyz
将旋转后的坐标扩展到不同深度值上。proj_xyz
将旋转和平移应用到这些三维坐标上。
2. 1参考影像视锥的建立
rot_depth_xyz = rot_xyz.unsqueeze(2).repeat(1, 1, num_depth, 1) * depth_values.view(batch, 1, num_depth, 1) # [B, 3, Ndepth, H*W] 这行代码就是在建立参考影像的视锥
建立过程分为三步:1,建立depth维度 ->unsqueeze(2)
, 2,在depth维度重复num-depth层->repeat(1, 1, num_depth, 1)
,此时每层深度的xy坐标是相同的,建立的是一个方块,3.在方块上乘depth-value的值,不同深度的(x,y)坐标乘对应深度的深度值,也就是说在不同深度对(x,y)坐标缩放,深度越深,乘的depth值越大,也就是x,y值越大,看起来就是一个视锥。
第三步涉及到python的相乘操作:
在代码 rot_depth_xyz = rot_xyz.unsqueeze(2).repeat(1, 1, num_depth, 1) * depth_values.view(batch, 1, num_depth, 1)
中,乘法操作执行的方式如下:
代码分析
-
rot_xyz.unsqueeze(2).repeat(1, 1, num_depth, 1)
:rot_xyz
形状为[B, 3, H*W]
,unsqueeze(2)
在第三个维度插入一个新的维度,将形状变为[B, 3, 1, H*W]
。repeat(1, 1, num_depth, 1)
将这个坐标重复num_depth
次,使得它的形状变为[B, 3, num_depth, H*W]
,即每个像素点在不同深度平面上都有一个 相同的3D 坐标。
-
depth_values.view(batch, 1, num_depth, 1)
:depth_values
是深度值,形状为[B, num_depth]
,表示每张图像在批量中的不同深度平面上的深度值。view(batch, 1, num_depth, 1)
将其重新调整为形状[B, 1, num_depth, 1]
,以便与rot_xyz
的形状相匹配。
乘法操作
-
广播机制:
rot_depth_xyz
和depth_values
之间的乘法操作依赖于 PyTorch 的广播机制。- 两者形状分别是
[B, 3, num_depth, H*W]
和[B, 1, num_depth, 1]
,根据广播机制,depth_values
会在第二和第四个维度上自动扩展,使得它的形状与rot_depth_xyz
匹配。
-
元素级别的乘法:
- 扩展后的
depth_values
和rot_depth_xyz
逐元素相乘:- 对于每个
B
(批次中的一张图像)和每个num_depth
(深度平面),rot_depth_xyz
中的每个 3D 坐标都乘以相应的深度值,结果是每个像素点在这个特定深度平面上的 3D 坐标。 - 举例来说,如果
rot_xyz
中的一个元素是[x, y, z]
,对应的深度值是d
,那么乘积结果就是[dx, dy, dz]
。
- 对于每个
- 扩展后的
通过这个乘法操作,每个像素点的 3D 坐标被调整为不同深度平面上的坐标,形成一个 3D 空间中的“视锥”,以便后续进行投影和变换。
3. 投影到二维平面
proj_xy = proj_xyz[:, :2, :, :] / proj_xyz[:, 2:3, :, :] # [B, 2, Ndepth, H*W]
- 这一步通过除以深度值,将三维坐标投影到二维平面,得到新的
(x, y)
坐标。
4. 归一化坐标并生成采样网格
proj_x_normalized = proj_xy[:, 0, :, :] / ((width - 1) / 2) - 1
proj_y_normalized = proj_xy[:, 1, :, :] / ((height - 1) / 2) - 1
proj_xy = torch.stack((proj_x_normalized, proj_y_normalized), dim=3) # [B, Ndepth, H*W, 2]
grid = proj_xy
- 归一化到
[-1, 1]
范围,这是F.grid_sample
函数要求的格式。 proj_xy
包含了每个像素在不同深度值下的归一化二维坐标。
5. 重采样特征图
warped_src_fea = F.grid_sample(src_fea, grid.view(batch, num_depth * height, width, 2), mode='bilinear', padding_mode='zeros')
warped_src_fea = warped_src_fea.view(batch, channels, num_depth, height, width)
F.grid_sample
根据采样网格grid
从源特征图中采样特征值,并进行插值(双线性插值)。- 将重采样后的特征图重新 reshape 为
[batch, channels, num_depth, height, width]
。
详细解析
-
源图像像素的三维坐标:在实际的深度估计中,每个像素都可以看作是空间中的一个点。源图像像素的三维坐标(x, y, depth)表示该点在空间中的位置,其中 depth 是深度值。
-
旋转和平移的目的:旋转和平移矩阵是用来将源图像的坐标转换到参考图像的坐标系中。通过这个转换,我们可以将源图像的特征映射到参考图像的视角中,从而对齐不同视角下的特征。
-
投影到二维平面:在多视图立体视觉中,我们需要将三维坐标投影回到二维平面上,以便在图像平面上进行处理。这一步是通过将三维坐标除以深度值实现的。