针孔相机模型和点投影
在深入探讨高斯泼溅(Gaussian Splatting)等前沿 3D 渲染技术之前,我们必须先掌握一个核心的基础概念:相机模型。在计算机图形学中,最经典、最常用的便是针孔相机模型(Pinhole Camera Model)。
什么是针孔相机模型?
针孔相机模型描述的是一种最基础的透视投影(Perspective Projection)。
我们可以把它想象成一个简单的“鞋盒实验”:拿一个密封的鞋盒,在前面戳一个小孔(针孔),在鞋盒内部的底面放上一块感光传感器。外界物体反射的光子(Photons)穿过这个小孔,打在传感器上。经过一段时间的曝光(Exposure Time),传感器上就会留下外界真实的影像。
现实中的针孔相机虽然简单,但有一个明显的物理特性:成像在传感器上是上下颠倒的。因为光线是沿直线传播并交叉穿过小孔的。
焦距(Focal Length)
在这个简单的模型中,有一个至关重要的参数:焦距(
焦距指的是针孔(相机中心)到成像平面(传感器)之间的距离。改变焦距会直接影响画面的视野大小:
- 增大焦距:相当于增加了相机中心和传感器之间的距离,物体在传感器上看起来会更大,产生放大(Zoom in)的效果(例如从 50mm 切换到 100mm 镜头)。
- 减小焦距:视野变宽,物体看起来更小,产生缩小(Zoom out)的效果。
真实的相机远比针孔模型复杂。为了缩短几十分钟的曝光时间,现代相机引入了复杂的透镜组,这也随之带来了诸如色差(Chromatic Aberration)、景深(Depth of Field)和畸变等物理现象。但在高斯泼溅的计算中,为了简化数学模型,我们通常将其理想化,直接假设为完美的针孔相机模型。
解决“颠倒”问题
前面提到,物理上的针孔成像是上下颠倒的。在编写代码和构建数学模型时,为了避免每次都要将图像“翻转”回来,图形学界采用了一个非常聪明的做法:将成像平面从相机中心“后方”移到“前方”。
通过将成像平面放置在相机中心正前方距离为
投影推导
接下来是整个模型的核心:如何将 3D 世界中的一个点映射到 2D 的像素屏幕上?
假设在相机坐标系下,我们有一个3D点
第一步:消除深度(z轴),统一映射到成像平面
我们希望所有的点都被“拍扁”到距离相机中心深度为
此时,点已经被投影到了二维的物理像平面上。
第二步:从物理坐标转换为像素索引 (u, v)
图像是以像素矩阵表示的,且原点
- 偏移中心点:我们需要加上图像中心的偏移量。设图像宽度的一半为
c_x ,高度的一半为c_y 。 - 转换为像素单位:物理尺寸通常是毫米(
mm ),而图像坐标是像素(Pixel )。我们需要除以每个像素的物理尺寸s_x 和s_y 。
为了简化公式,我们通常将焦距与像素尺寸的转换合并为一个参数:
这也是为什么我们在使用 COLMAP 等相机标定软件时,会看到
最终的投影公式诞生了:
公式中的加减符号高度依赖于所使用的坐标系约定(右手系还是左手系,x/y 轴的正方向朝向哪里)。例如 COLMAP 有自己固定的一套坐标系,而 PyTorch3D 或 OpenCV 可能有所不同。在跨软件处理数据时,必须第一时间对齐坐标系,否则会在投影时出现莫名其妙的翻转或偏移。
代码实现
理解了公式,我们就可以开始用 PyTorch 构建原先留空的 project_points 函数了。
- def project_points(PC, height, width, fx, fy, cx, cy):
- """
- 将三维点投影到二维屏幕坐标系。
- 根据针孔相机模型公式:
- u = fx * x_cam / z_cam + cx
- v = fy * y_cam / z_cam + cy
- """
- x_cam = PC[:, 0]
- y_cam = PC[:, 1]
- z_cam = PC[:, 2]
- u = fx * x_cam / z_cam + cx
- v = fy * y_cam / z_cam + cy
- uv = torch.stack([u, v], dim=-1)
- return uv, x_cam, y_cam, z_cam
torch.stack 将独立的
有了投影逻辑,我们需要捏造一些数据来验证它。我们沿着球面制作一些随机点。完整的可视化代码可查看 Commit d206c66:visualize.py。