def render_3dphoto(
src_imgs, # 输入的源图像,维度为 [batch_size, 3, height, width]
mpi_all_src, # 输入的所有源图像的MPI,维度为 [batch_size, num_planes, 4, height, width]
disparity_all_src, # 所有源图像的视差信息,维度为 [batch_size, num_planes]
k_src, # 源相机的内参矩阵,维度为 [batch_size, 3, 3]
k_tgt, # 目标相机的内参矩阵,维度为 [batch_size, 3, 3]
save_path, # 渲染视频的保存路径
):
disparity 也就是深度。
使用HomographySample变换。utils.mpi.homography_sampler.HomographySample
class HomographySample:
"""
处理与单应性变换相关的操作,它提供了生成二维网格坐标的功能,计算平面法线,以及将欧拉角转换为旋转矩阵
"""
def __init__(self, H_tgt, W_tgt, device=None):
if device is None:
self.device = torch.device("cpu")
else:
self.device = device
self.Height_tgt = H_tgt
self.Width_tgt = W_tgt
self.meshgrid = self.grid_generation(self.Height_tgt, self.Width_tgt, self.device) # 生成目标图像的二维网格坐标
self.meshgrid = self.meshgrid.permute(2, 0, 1).contiguous() # 3xHxW 对生成的网格进行维度重排,以匹配 3xHxW 的形状
self.n = self.plane_normal_generation(self.device) # 生成平面法线
其中方法:
def euler_to_rotation_matrix(x_angle, y_angle, z_angle, seq='xyz', degrees=False):
"""将欧拉角转换为旋转矩阵
Note that here we want to return a rotation matrix rot_mtx, which transform the tgt points into src frame,
i.e, rot_mtx * p_tgt = p_src
Therefore we need to add negative to x/y/z_angle
由于在欧拉角中,旋转是绕坐标轴的正方向进行的,而我们希望进行的是相反方向的旋转,
因此需要对 x/y/z 分量的欧拉角添加负号,以实现从目标点到源坐标系的变换。
注意 **这里的欧拉角需要取反,因为我们希望得到的旋转矩阵是将目标点变换到源坐标系的旋转**
https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.transform.Rotation.html
:param roll:
:param pitch:
:param yaw:
:return:
"""
r = Rotation.from_euler(seq,
[-x_angle, -y_angle, -z_angle],
degrees=degrees)
rot_mtx = r.as_matrix().astype(np.float32)
# 用于图像的单应性变换,其中 meshgrid 用于定义原始图像中每个像素的坐标,plane_normal_generation 用于定义平面法线(对于单应性变换而言通常是不必要的,但可能用于其他3D变换),
# euler_to_rotation_matrix 则用于根据给定的欧拉角生成对应的旋转矩阵。这些操作都是在准备实施单应性变换或其他相关图像几何变换的数据和参数。
return rot_mtx
映射前定义网格
@staticmethod
def grid_generation(H, W, device):
"""
:生成网格坐标,用于后续的映射计算。
"""
# 使用 numpy 生成x和y方向的线性等分点,并通过 np.meshgrid 生成一个二维网格
x = np.linspace(0, W - 1, W)
y = np.linspace(0, H - 1, H)
xv, yv = np.meshgrid(x, y) # HxW
xv = torch.from_numpy(xv.astype(np.float32)).to(dtype=torch.float32, device=device)
yv = torch.from_numpy(yv.astype(np.float32)).to(dtype=torch.float32, device=device)
# 生成一个全为1的张量,与x和y坐标一起堆叠(通道维度)成3通道的网格坐标张量
ones = torch.ones_like(xv)
meshgrid = torch.stack((xv, yv, ones), dim=2) # HxWx3
return meshgrid
@staticmethod
def plane_normal_generation(device):
"""
创建一个表示垂直于xy平面(即z轴方向)的法线向量的张量
"""
n = torch.tensor([0, 0, 1], dtype=torch.float32, device=device)
return n
在utils.utils.render_novel_view
中:
def render_novel_view(
mpi_all_rgb_src,
mpi_all_sigma_src,
disparity_all_src,
G_tgt_src,
K_src_inv,
K_tgt,
homography_sampler,
):
"""
mpi_all_rgb_src, # 来源视角的RGB图像集合
mpi_all_sigma_src, # 来源视角的不透明度(sigma)集合
disparity_all_src, # 来源视角的视差图集合
G_tgt_src, # 目标视角到来源视角的变换矩阵
K_src_inv, # 来源视角的相机内参矩阵的逆矩阵
K_tgt, # 目标视角的相机内参矩阵
homography_sampler, # 用于生成和采样单应性(Homographies)的工具
"""
# 从源视角的视差图计算出每个平面上点的3D坐标
xyz_src_BS3HW = mpi_rendering.get_src_xyz_from_plane_disparity(
homography_sampler.meshgrid,
disparity_all_src,
K_src_inv
)
# 使用源视角的3D坐标和从目标视角到源视角的变换矩阵 G_tgt_src 来计算目标视角的3D坐标
xyz_tgt_BS3HW = mpi_rendering.get_tgt_xyz_from_plane_disparity(
xyz_src_BS3HW,
G_tgt_src
)
# 调用 render_tgt_rgb_depth 函数,将源视角的RGB图像、不透明度、视差图和3D坐标映射到目标视角,生成合成的目标视角图像 tgt_imgs_syn
tgt_imgs_syn, _, _ = mpi_rendering.render_tgt_rgb_depth(
homography_sampler,
mpi_all_rgb_src,
mpi_all_sigma_src,
disparity_all_src,
xyz_tgt_BS3HW,
G_tgt_src,
K_src_inv,
K_tgt,
use_alpha=False,
is_bg_depth_inf=False,
)
return tgt_imgs_syn
def render_3dphoto(
src_imgs, # [b,3,h,w]
mpi_all_src, # [b,s,4,h,w]
disparity_all_src, # [b,s]
k_src, # [b,3,3]
k_tgt, # [b,3,3]
save_path,
):
"""
src_imgs, # 输入的源图像,维度为 [batch_size, 3, height, width]
mpi_all_src, # 输入的所有源图像的MPI,维度为 [batch_size, num_planes, 4, height, width]
disparity_all_src, # 所有源图像的视差信息,维度为 [batch_size, num_planes]
k_src, # 源相机的内参矩阵,维度为 [batch_size, 3, 3]
k_tgt, # 目标相机的内参矩阵,维度为 [batch_size, 3, 3]
save_path, # 渲染视频的保存路径
"""
h, w = mpi_all_src.shape[-2:]
device = mpi_all_src.device
homography_sampler = HomographySample(h, w, device) # 生成单应性变换
k_src_inv = torch.inverse(k_src) # 计算源相机内参矩阵的逆矩阵,用于从像素坐标到相机坐标系的转换。
# preprocess the predict MPI
# 将视差信息转换为3D空间坐标
xyz_src_BS3HW = mpi_rendering.get_src_xyz_from_plane_disparity(
homography_sampler.meshgrid,
disparity_all_src,
k_src_inv,
)
# 分离通道
mpi_all_rgb_src = mpi_all_src[:, :, 0:3, :, :] # BxSx3xHxW
mpi_all_sigma_src = mpi_all_src[:, :, 3:, :, :] # BxSx1xHxW
# 调用一个渲染函数,用于计算从MPI合成图像时使用的混合权重
_, _, blend_weights, _ = mpi_rendering.render(
mpi_all_rgb_src,
mpi_all_sigma_src,
xyz_src_BS3HW,
use_alpha=False,
is_bg_depth_inf=False,
)
# 混合权重来更新源图像的MPI RGB部分,这里使用的是alpha blending算法
mpi_all_rgb_src = blend_weights * src_imgs.unsqueeze(1) + (1 - blend_weights) * mpi_all_rgb_src
# render novel views
# 定义通过改变相机外参来模拟相机移动的参数。同时初始化一个空列表用于存储渲染出的帧。
swing_path_list = gen_swing_path()
frames = []
# for cam_ext in tqdm(swing_path_list):
# 利用render_novel_view函数来渲染从新视角看到的图像。这个函数需要当前的MPI RGB数据、透明度数据、视差数据、目标相机的外参(这里假设cam_ext是外参
# ,cam_ext.cuda()将外参移动到GPU上)、源相机的内参矩阵的逆以及目标相机的内参矩阵。homography_sampler可能用于计算在新视角下图像的单应性变换。
for cam_ext in swing_path_list:
frame = render_novel_view(
mpi_all_rgb_src,
mpi_all_sigma_src,
disparity_all_src,
cam_ext.cuda(), # 目标视角到来源视角的变换矩阵
k_src_inv,
k_tgt, # 目标视角的相机内参矩阵
homography_sampler,
)
# 张量转换为NumPy数组,并进行必要的排列以匹配图像的形状要求。然后它将像素值缩放到0-255范围,并转换为uint8类型
frame_np = frame[0].permute(1, 2, 0).contiguous().cpu().numpy() # [b,h,w,3]
frame_np = np.clip(np.round(frame_np * 255), a_min=0, a_max=255).astype(np.uint8)
frames.append(frame_np)
rgb_clip = ImageSequenceClip(frames, fps=30)
# moviepy库的ImageSequenceClip类将帧列表转换成视频剪辑,设置每秒帧数为30。然后调用write_videofile方法将视频保存到指定的save_path路径
# 使用mpeg4编解码器,不显示详细信息,设置比特率为3000kbps。
rgb_clip.write_videofile(save_path, verbose=False, codec='mpeg4', logger=None, bitrate='3000k')
获取移动路径
def gen_swing_path(num_frames=90, r_x=0.14, r_y=0.0, r_z=0.10):
"""
生成一个摆动路径.
生成一系列的4x4变换矩阵,每个矩阵代表了不同时间点的位置
num_frames 表示要生成的帧数,默认为90帧;
r_x、r_y 在x、y方向上的摆动半径
r_z 表示z轴方向上的摆动半径
"""
# 创建了一个从0到1等间距的一维张量 t。这个张量表示从摆动开始到结束的时间线索引
t = torch.arange(num_frames) / (num_frames - 1)
# 创建了一个4x4的单位矩阵(identity matrix),然后将其重复 num_frames 次,用来存储最终生成的每一帧的变换矩阵。
poses = torch.eye(4).repeat(num_frames, 1, 1)
# 计算了在x轴上的摆动,使用正弦函数来生成x轴的平移部分,摆动的幅度由 r_x 确定
# 正余弦: 需要一个物体的运动从最大幅度开始并且回到最大幅度
poses[:, 0, 3] = r_x * torch.sin(2. * math.pi * t)
poses[:, 1, 3] = r_y * (torch.cos(2. * math.pi * t) )
poses[:, 2, 3] = r_z * (torch.cos(2. * math.pi * t) - 1.0) # 减去了1,这样摆动的起始点会更低
return poses.unbind() # 张量 poses 拆分成一个元组,元组中的每个元素是一个4x4矩阵,代表了每一帧的变换矩阵