从 Python 工程师的角度理解和编写 Gaussian Splatting
欢迎来到雲闪世界。2023 年初,来自法国蔚蓝海岸大学和马克斯普朗克信息研究所的作者发表了一篇题为“用于实时场渲染的 3D 高斯溅射”的论文。¹ 该论文展示了实时神经渲染的重大进步,超越了 NeRF 等先前方法的实用性。² 高斯溅射不仅降低了延迟,而且达到或超过了 NeRF 的渲染质量,席卷了神经渲染领域。
高斯溅射虽然有效,但对于不熟悉相机矩阵和图形渲染的人来说,理解起来可能具有挑战性。此外,我发现在 Python 中实现高斯溅射的资源很少,因为作者的源代码甚至都是用 CUDA 编写的!本教程旨在弥补这一差距,为精通 Python 和机器学习但缺乏图形渲染经验的工程师提供基于 Python 的高斯溅射简介。GitHub 上的随附代码演示了如何初始化和渲染 COLMAP 扫描中的点,使其成为类似于溅射应用程序中的前向传递的最终图像(以及一些额外的 CUDA 代码,供有兴趣的人使用)。本教程还有一个配套的 jupyter 笔记本( GitHub 中的part_1.ipynb),其中包含学习所需的所有代码。虽然我们不会构建完整的高斯溅射场景,但如果按照本教程进行操作,读者应该具备基础知识,可以更深入地研究溅射技术。
首先,我们使用 COLMAP,这是一款使用“运动结构”(SfM) 提取在多幅图像中一致可见的点的软件。³ SfM 本质上是识别在多幅图像中发现的点(例如,门口的右上边缘)。通过匹配不同图像中的这些点,我们可以估算 3D 空间中每个点的深度。这非常接近模拟人类立体视觉的工作原理,通过比较每只眼睛略有不同的视图来感知深度。因此,SfM 从多幅图像中找到的公共点生成一组 3D 点,每个点都有 x、y 和 z 坐标,从而为我们提供场景的“结构”。
在本教程中资料和完整代码,可联系博主。预构建 COLMAP 扫描(Apache 2.0 许可证)。具体来说,我们将使用下载的数据集中的 Treehill 文件夹。
该图像以及从输入到 COLMAP 的所有图像中提取的所有点。请参阅下面的示例代码或 part_1.ipynb 以了解该过程
该文件夹包含三个文件,分别对应相机参数、图像参数和实际 3D 点。我们将从 3D 点开始。
点文件由数千个 3D 点以及相关颜色组成。这些点以所谓的世界原点为中心,本质上它们的 x、y 或 z 坐标基于相对于这个世界原点的观察位置。世界原点的确切位置对于我们的目的来说并不重要,因此我们不会关注它,因为它可以是空间中的任意点。相反,唯一重要的是知道您在这个世界中相对于这个原点的位置。这就是图像文件变得有用的地方!
广义上讲,图像文件告诉我们图像的拍摄位置和相机的方向,这两者都与世界原点有关。因此,我们关心的关键参数是四元数向量和平移向量。四元数向量使用 4 个不同的浮点值描述相机在空间中的旋转,这些浮点值可用于形成旋转矩阵(3Blue1Brown 有一个很棒的视频,在这里准确解释了四元数是什么)。然后,平移向量告诉我们相机相对于原点的位置。这些参数一起形成外部矩阵,四元值用于计算 3x3 旋转矩阵(公式),并将平移向量附加到此矩阵。
典型的“外部”矩阵。通过组合 3x3 旋转矩阵和 3x1 平移向量,我们能够将坐标从世界坐标系平移到我们的相机坐标系
外部矩阵将点从世界空间(点文件中的坐标)转换到相机空间,使相机成为世界的新中心。例如,如果相机在 y 方向上向上移动 2 个单位而不进行任何旋转,我们只需从所有点的 y 坐标中减去 2 个单位即可获得新坐标系中的点。
当我们将坐标从世界空间转换为相机空间时,我们仍然有一个 3D 矢量,其中 z 坐标表示相机视图中的深度。此深度信息对于确定 splats 的顺序至关重要,我们稍后需要使用它来进行渲染。
我们通过解释相机参数文件来结束我们的 COLMAP 检查。相机文件提供高度、宽度、焦距(x 和 y)和偏移量(x 和 y)等参数。使用这些参数,我们可以组成固有矩阵,它表示 x 和 y 方向的焦距和主点坐标。
如果你对相机矩阵完全不熟悉,我会向你推荐 Shree Nayar 讲授的《计算机视觉第一原理》讲座。特别是针孔和前瞻性投影讲座,以及随后的内在和外在矩阵讲座。
典型的固有矩阵。表示 x 和 y 方向的焦距以及主点坐标。
内在矩阵用于将点从相机坐标(使用外在矩阵获得)转换为 2D 图像平面,即您所看到的“图像”。相机坐标中的点本身并不能表明它们在图像中的外观,因为必须反映深度才能准确评估相机将看到的内容。
要将 COLMAP 点转换为 2D 图像,我们首先使用外部矩阵将它们投影到相机坐标系,然后使用内部矩阵将它们投影到 2D 图像。但是,一个重要的细节是,我们在此过程中使用齐次坐标。外部矩阵为 4x4,而我们的输入点为 3x1,因此我们将 1 堆叠到输入点上,使它们变成 4x1。
以下是该过程的分步说明:
-
将点转换为相机坐标:将 4x4 外部矩阵乘以 4x1 点向量。
-
转换为图像坐标:将 3x4 内在矩阵乘以得到的 4x1 向量。
这会产生一个 3x1 矩阵。为了获得最终的 2D 坐标,我们用这个 3x1 矩阵的第三个坐标除以它,然后得到图像中的 x 和 y 坐标!您可以准确地看到图像编号 100 的显示效果,下面显示了复制结果的代码。
def get_intrinsic_matrix( f_x: float, f_y: float, c_x: float, c_y: float ) -> torch.Tensor: """ Get the homogenous intrinsic matrix for the camera """ return torch.Tensor( [ [f_x, 0, c_x, 0], [0, f_y, c_y, 0], [0, 0, 1, 0], ] ) def get_extrinsic_matrix(R: torch.Tensor, t: torch.Tensor) -> torch.Tensor: """ Get the homogenous extrinsic matrix for the camera """ Rt = torch.zeros((4, 4)) Rt[:3, :3] = R Rt[:3, 3] = t Rt[3, 3] = 1.0 return Rt def project_points( points: torch.Tensor, intrinsic_matrix: torch.Tensor, extrinsic_matrix: torch.Tensor ) -> torch.Tensor: """ Project the points to the image plane Args: points: Nx3 tensor intrinsic_matrix: 3x4 tensor extrinsic_matrix: 4x4 tensor """ homogeneous = torch.ones((4, points.shape[0]), device=points.device) homogeneous[:3, :] = points.T projected_to_camera_perspective = extrinsic_matrix @ homogeneous projected_to_image_plane = (intrinsic_matrix @ projected_to_camera_perspective).T # Nx4 x = projected_to_image_plane[:, 0] / projected_to_image_plane[:, 2] y = projected_to_image_plane[:, 1] / projected_to_image_plane[:, 2] return x, y colmap_path = "treehill/sparse/0" reconstruction = pycolmap.Reconstruction(colmap_path) points3d = reconstruction.points3D images = read_images_binary(f"{colmap_path}/images.bin") cameras = reconstruction.cameras all_points3d = [] all_point_colors = [] for idx, point in enumerate(points3d.values()): if point.track.length() >= 2: all_points3d.append(point.xyz) all_point_colors.append(point.color) gaussians = Gaussians( torch.Tensor(all_points3d), torch.Tensor(all_point_colors), model_path="point_clouds" ) # we will examine the 100th image image_num = 100 image_dict = read_image_file(colmap_path) camera_dict = read_camera_file(colmap_path) # convert quaternion to rotation matrix rotation_matrix = build_rotation(torch.Tensor(image_dict[image_num].qvec).unsqueeze(0)) translation = torch.Tensor(image_dict[image_num].tvec).unsqueeze(0) extrinsic_matrix = get_extrinsic_matrix( rotation_matrix, translation ) focal_x, focal_y = camera_dict[image_dict[image_num].camera_id].params[:2] c_x, c_y = camera_dict[image_dict[image_num].camera_id].params[2:4] intrinsic_matrix = get_intrinsic_matrix(focal_x, focal_y, c_x, c_y) points = project_points(gaussians.points, intrinsic_matrix, extrinsic_matrix)
我们有所需的各种位置和相机参数,我们现在就可以取任意一组 3D 点,并将它们投影到 2D 图像平面上!掌握了这些知识后,我们可以继续理解第2 部分中高斯溅射的“高斯”部分
感谢关注雲闪世界。(亚马逊aws和谷歌GCP服务协助解决云计算及产业相关解决方案)
订阅频道(https://t.me/awsgoogvps_Host) TG交流群(t.me/awsgoogvpsHost)