四元数
在这篇文章中,我们处理上篇文章遗留的四元数解析。
从“四元数”到“3x3 矩阵”
在图形学渲染中,我们最习惯使用的是
四元数由四个值组成:
将四元数转换为矩阵时,我们应用以下数学公式:
- def quat_to_rotmat(quat):
- """
- Convert quaternion [x, y, z, w] to a 3x3 rotation matrix.
- Supports batch dimensions (..., 4) -> (..., 3, 3).
- """
- # Normalize quaternion to ensure valid rotation matrix
- quat = quat / torch.norm(quat, dim=-1, keepdim=True)
- x, y, z, w = torch.unbind(quat, dim=-1)
- # Precompute products
- x2 = x * x
- y2 = y * y
- z2 = z * z
- xy = x * y
- xz = x * z
- yz = y * z
- xw = x * w
- yw = y * w
- zw = z * w
- # Calculate R components
- r00 = 1.0 - 2.0 * (y2 + z2)
- r01 = 2.0 * (xy - zw)
- r02 = 2.0 * (xz + yw)
- r10 = 2.0 * (xy + zw)
- r11 = 1.0 - 2.0 * (x2 + z2)
- r12 = 2.0 * (yz - xw)
- r20 = 2.0 * (xz - yw)
- r21 = 2.0 * (yz + xw)
- r22 = 1.0 - 2.0 * (x2 + y2)
- # Stack along last dimension and reshape
- rot_matrix = torch.stack([r00, r01, r02, r10, r11, r12, r20, r21, r22], dim=-1)
- rot_matrix = rot_matrix.reshape(*quat.shape[:-1], 3, 3)
- return rot_matrix
输入 quat 的形状可能是 (N, 4)。所以我们不能简单地用索引提取标量,因为每一列会包含
Colmap:从“世界到相机”逆转为“相机到世界”
Colmap 软件输出的矩阵是“世界到相机 (World-to-Camera, W2C)”的矩阵,而我们的渲染管线需要的是“相机到世界 (Camera-to-World, C2W)”的矩阵。
我们需要进行一次逆变换。变换的原理我们在之前的文章中已经了解过了。
假设原始的 W2C 旋转矩阵为
1. 旋转矩阵的逆等于它的转置:
2. 平移向量结合新的旋转矩阵进行反向推导:
- def w2c_to_c2w(q_w2c, t_w2c):
- """
- Convert world-to-camera parameters to camera-to-world matrix.
- q_w2c: quaternion of shape (..., 4)
- t_w2c: translation of shape (..., 3)
- Returns: c2w matrix of shape (..., 4, 4)
- """
- R_w2c = quat_to_rotmat(q_w2c)
- # R_c2w = R_w2c.T (transpose the rotation matrix)
- R_c2w = R_w2c.transpose(-1, -2)
- # t_c2w = -R_c2w @ t_w2c
- t_c2w = -torch.matmul(R_c2w, t_w2c.unsqueeze(-1)).squeeze(-1)
- # Construct 4x4 matrix
- shape = q_w2c.shape[:-1]
- if len(shape) == 0:
- c2w = torch.eye(4, dtype=q_w2c.dtype, device=q_w2c.device)
- c2w[:3, :3] = R_c2w
- c2w[:3, 3] = t_c2w
- else:
- c2w = torch.eye(4, dtype=q_w2c.dtype, device=q_w2c.device).repeat(*shape, 1, 1)
- c2w[..., :3, :3] = R_c2w
- c2w[..., :3, 3] = t_c2w
- return c2w
内参缩放
搞定了四元数,我们解析得到相机外参。
- # 3. Load camera poses and create camera pose
- cameras_path = "datasets/out_colmap/bonsai/cameras.npy"
- print(f"Loading camera parameters from {cameras_path}...")
- cameras = np.load(cameras_path, allow_pickle=True)
- # Select the first camera pose (cam_id = 0)
- cam_id = 0
- cam = cameras[cam_id]
- print(f"Using camera {cam_id} ({cam['name']}): q={cam['q']}, t={cam['t']}")
- q_w2c = torch.tensor(cam['q']).float()
- t_w2c = torch.tensor(cam['t']).float()
- c2w = w2c_to_c2w(q_w2c, t_w2c)
但是最终渲染的结果不是很清楚,只能隐约看到一盆“盆景”的点云轮廓。
原因是此处使用的本就是稀疏点云,加上原图又非常大,每个点落在单一像素上,广袤的屏幕中只有零星的像素被点亮,肉眼根本看不清。
为了可视化更清晰,我们可以在较小的分辨率上(例如缩小 4 倍或 8 倍)进行渲染。
这就引出了一个非常关键的操作:如果改变了图像的渲染分辨率,必须等比例地修改相机的内参(Intrinsics)。
相机的内参包括焦距(
- def scale_intrinsics(W_target, H_target, W_source, H_source, fx, fy, cx, cy):
- # 1. 计算宽高的缩放比例 (Scale Factors)
- scale_x = W_target / W_source
- scale_y = H_target / H_source
- # 2. 对焦距进行等比例缩放
- scaled_fx = fx * scale_x
- scaled_fy = fy * scale_y
- # 3. 对光心(主点)位置进行等比例缩放
- scaled_cx = cx * scale_x
- scaled_cy = cy * scale_y
- return scaled_fx, scaled_fy, scaled_cx, scaled_cy