使用Python将3D模型放置在视频中的深度感知方法说明

在计算机视觉领域,视频中深度和相机姿态的一致估计为更高级的操作奠定了基础,例如在视频中插入深度感知对象。本文基于我之前探讨的这些基本技术,将焦点转向深度感知对象的插入。利用基于Python的计算方法,我将概述一种根据深度和相机方向数据将对象添加到现有视频帧中的策略。这种方法不仅提高了编辑视频内容的逼真度,而且在视频后期制作中具有广泛的应用。
总的来说,该方法包括两个主要步骤:首先,在视频中估计一致的深度和相机位置,然后在视频帧上叠加网格对象。为了使对象在视频的3D空间中看起来静止不动,它会沿着相机运动的相反方向移动。这种反向运动确保对象在整个视频中看起来固定在原地。
您可以在我的GitHub页面上查看我的代码,本文将在整个过程中参考这些代码。
第一步:生成相机位姿矩阵和视频的一致深度估计
在我之前的文章中,我详细解释了如何估计视频的一致深度帧和相应的相机位姿矩阵。
对于本文,我选择了一个特定的视频,其中一个人在街上行走,特意选择了其明显的沿轴的相机运动。这将有助于清楚评估插入的对象是否在视频的3D空间内保持固定位置。
我按照我在之前的文章中解释的所有步骤来获取深度帧和估计的相机位姿矩阵。我们尤其需要由COLMAP生成的“custom.matrices.txt”文件。
原始素材及其估计的深度视频如下所示。

以下是对应于第一帧的点云可视化。

第二步:选择要插入的网格文件
现在,我们选择要插入视频序列的网格文件。各种平台,如Sketchfab.com和GrabCAD.com,提供了各种可供选择的3D模型。
对于我的演示视频,我选择了两个3D模型,其链接如下图标题所示:

我使用CloudCompare对3D模型进行了预处理,CloudCompare是一个开源的用于3D点云处理的工具。具体而言,我从对象中删除了地面部分,以增强它们在视频中的整合效果。虽然这一步骤是可选的,但如果您希望修改您的3D模型的某些方面,强烈推荐使用CloudCompare。
在预处理网格文件之后,将其保存为.ply或.obj文件。(请注意,并非所有3D模型文件扩展名都支持彩色网格,例如.stl)。
步骤3:使用深度感知对象插入重新渲染帧
现在我们来到了项目的核心部分:视频处理。在我的存储库中,提供了两个关键脚本 – video_processing_utils.py和depth_aware_object_insertion.py。正如它们的名称所示,video_processing_utils.py包含了所有用于对象插入的重要函数,而depth_aware_object_insertion.py则作为主要脚本,在循环中执行这些函数以处理每个视频帧。
下面是depth_aware_object_insertion.py主要部分的一段代码片段。在一个for循环中,循环次数与输入视频的帧数相同,我们加载深度计算管道的批处理信息,其中包括原始RGB帧及其深度估计。然后,我们计算相机位姿矩阵的逆矩阵。然后,我们将网格、深度和相机的内参输入到名为render_mesh_with_depth()的函数中。
for i in tqdm(range(batch_count)): batch = np.load(os.path.join(BATCH_DIRECTORY, file_names[i])) # ... (为了简洁起见省略了一部分代码) # 对网格进行与相机外参的逆变换 frame_transformation = np.vstack(np.split(extrinsics_data[i],4)) inverse_frame_transformation = np.empty((4, 4)) inverse_frame_transformation[:3, :] = np.concatenate((np.linalg.inv(frame_transformation[:3,:3]), np.expand_dims(-1 * frame_transformation[:3,3],0).T), axi inverse_frame_transformation[3, :] = [0.00, 0.00, 0.00, 1.00] mesh.transform(inverse_frame_transformation) # ... (为了简洁起见省略了一部分代码) image = np.transpose(batch['img_1'], (2, 3, 1, 0))[:,:,:,0] depth = np.transpose(batch['depth'], (2, 3, 1, 0))[:,:,0,0] # ... (为了简洁起见省略了一部分代码) # 在图像域中渲染转换后的网格的颜色和深度缓冲区 mesh_color_buffer, mesh_depth_buffer = render_mesh_with_depth(np.array(mesh.vertices), np.array(mesh.vertex_colors), np.array(mesh.triangles), depth, intrinsics) # 将网格和原始图像进行深度感知的叠加 combined_frame, combined_depth = combine_frames(image, mesh_color_buffer, depth, mesh_depth_buffer) # ... (为了简洁起见省略了一部分代码)
render_mesh_with_depth函数接受一个3D网格,由其顶点、顶点颜色和三角形表示,并将其渲染到2D深度帧上。该函数首先初始化深度和颜色缓冲区以保存渲染的输出。然后,使用相机内参数将3D网格顶点投影到2D帧上。该函数使用扫描线渲染来遍历网格中的每个三角形,并将其光栅化为2D帧上的像素。在此过程中,函数计算每个像素的重心坐标以插值深度和颜色值。然后使用这些插值值来更新深度和颜色缓冲区,但仅当像素的插值深度比深度缓冲区中的现有值更接近相机时。最后,该函数返回颜色和深度缓冲区作为渲染的输出,其中颜色缓冲区转换为适合图像显示的uint8格式。
def render_mesh_with_depth(mesh_vertices, vertex_colors, triangles, depth_frame, intrinsic): vertex_colors = np.asarray(vertex_colors) # 初始化深度和颜色缓冲区 buffer_width, buffer_height = depth_frame.shape[1], depth_frame.shape[0] mesh_depth_buffer = np.ones((buffer_height, buffer_width)) * np.inf # 将3D顶点投影到2D图像坐标上 vertices_homogeneous = np.hstack((mesh_vertices, np.ones((mesh_vertices.shape[0], 1)))) camera_coords = vertices_homogeneous.T[:-1,:] projected_vertices = intrinsic @ camera_coords projected_vertices /= projected_vertices[2, :] projected_vertices = projected_vertices[:2, :].T.astype(int) depths = camera_coords[2, :] mesh_color_buffer = np.zeros((buffer_height, buffer_width, 3), dtype=np.float32) # 遍历每个三角形进行渲染 for triangle in triangles: # 获取三角形顶点的2D坐标和深度 points_2d = np.array([projected_vertices[v] for v in triangle]) triangle_depths = [depths[v] for v in triangle] colors = np.array([vertex_colors[v] for v in triangle]) # 按y坐标对顶点进行排序,以进行扫描线渲染 order = np.argsort(points_2d[:, 1]) points_2d = points_2d[order] triangle_depths = np.array(triangle_depths)[order] colors = colors[order] y_mid = points_2d[1, 1] for y in range(points_2d[0, 1], points_2d[2, 1] + 1): if y < 0 or y >= buffer_height: continue # 确定当前扫描线的起始和结束x坐标 if y < y_mid: x_start = interpolate_values(y, points_2d[0, 1], points_2d[1, 1], points_2d[0, 0], points_2d[1, 0]) x_end = interpolate_values(y, points_2d[0, 1], points_2d[2, 1], points_2d[0, 0], points_2d[2, 0]) else: x_start = interpolate_values(y, points_2d[1, 1], points_2d[2, 1], points_2d[1, 0], points_2d[2, 0]) x_end = interpolate_values(y, points_2d[0, 1], points_2d[2, 1], points_2d[0, 0], points_2d[2, 0]) x_start, x_end = int(x_start), int(x_end) # 遍历扫描线上的每个像素 for x in range(x_start, x_end + 1): if x < 0 or x >= buffer_width: continue # 计算像素的重心坐标 s, t, u = compute_barycentric_coords(points_2d, x, y) # 检查像素是否位于三角形内部 if s >= 0 and t >= 0 and u >= 0: # 插值像素的深度和颜色 depth_interp = s * triangle_depths[0] + t * triangle_depths[1] + u * triangle_depths[2] color_interp = s * colors[0] + t * colors[1] + u * colors[2] # 如果像素的插值深度比深度缓冲区中的现有值更接近相机,则更新像素 if depth_interp < mesh_depth_buffer[y, x]: mesh_depth_buffer[y, x] = depth_interp mesh_color_buffer[y, x] = color_interp # 将浮点颜色转换为uint8 mesh_color_buffer = (mesh_color_buffer * 255).astype(np.uint8) return mesh_color_buffer, mesh_depth_buffer
然后,变换后的网格的颜色和深度缓冲区与原始的RGB图像和其估计的深度图一起输入到combine_frames()函数中。该函数旨在合并两组图像和深度帧。它使用深度信息来决定原始帧中的哪些像素应该由渲染网格帧中的相应像素替换。具体而言,对于每个像素,函数检查渲染网格的深度值是否小于原始场景的深度值。如果是,则认为该像素在渲染网格帧中更“靠近”相机,并相应地替换颜色和深度帧中的像素值。该函数返回合并的颜色和深度帧,根据深度信息将渲染的网格叠加到原始场景上。
# 根据深度信息合并原始帧和渲染的网格帧def combine_frames(original_frame, rendered_mesh_img, original_depth_frame, mesh_depth_buffer): # 创建一个掩码,其中网格比原始深度更接近 mesh_mask = mesh_depth_buffer < original_depth_frame # 初始化合并的帧 combined_frame = original_frame.copy() combined_depth = original_depth_frame.copy() # 在掩码为True的情况下使用网格信息更新合并的帧 combined_frame[mesh_mask] = rendered_mesh_img[mesh_mask] combined_depth[mesh_mask] = mesh_depth_buffer[mesh_mask] return combined_frame, combined_depth
下面是mesh_color_buffer、mesh_depth_buffer和combined_frame的第一个对象——大象的外观。由于大象对象在帧内没有被任何其他元素遮挡,因此它仍然完全可见。在不同的摆放位置,会发生遮挡。

相应地,我将第二个网格——汽车放在了路边。我还调整了它的初始方向,使其看起来像是停在那里。以下是该网格的相应mesh_color_buffer、mesh_depth_buffer和combined_frame的外观。

插入了两个对象的点云可视化如下图所示。由于新的对象带来了新的遮挡区域,所以出现了更多的白色间隙。

在计算了每个视频帧的叠加图像之后,我们现在准备渲染视频。
第4步:从处理过的帧中渲染视频
在depth_aware_object_insertion.py的最后一节中,我们只需使用render_video_from_frames函数从插入对象的帧中渲染视频。您还可以在此步骤中调整输出视频的帧速率。以下是代码:
video_name = 'depth_aware_object_insertion_demo.mp4'save_directory = "depth_aware_object_insertion_demo/"frame_directory = "depth_aware_object_insertion_demo/"image_extension = ".png"fps = 15 # 渲染叠加帧的视频render_video_from_frames(frame_directory, image_extension, save_directory, video_name, fps)
这是我的演示视频:

这个动画的更高分辨率版本已经上传到YouTube。
总的来说,物体的完整性似乎得到了很好的保持;例如,在场景中,汽车物体被路灯杆巧妙地遮挡。虽然在整个视频中,汽车的位置存在轻微可察觉的抖动 —— 最可能是由于相机姿态估计的不完美 —— 但基本上,世界锁定机制在演示视频中的表现如预期。
虽然视频中插入物体的概念并不新颖,已经有像After Effects这样的工具提供基于特征跟踪的方法,但对于不熟悉视频编辑工具的人来说,这些传统方法通常非常具有挑战性和昂贵。这就是基于Python算法的承诺发挥作用的地方。借助机器学习和基本的编程结构,这些算法有潜力将高级视频编辑任务民主化,使其即使对于在该领域经验有限的个人也可以轻松使用。因此,随着技术的不断发展,我预计软件为基础的方法将成为强大的推动者,平衡竞争环境,并为视频编辑中的创造性表达打开新的途径。
祝您有一个美好的一天!