首页 > 3DGS教程 > 基本矩阵 > 本质矩阵

本质矩阵:透视变换的黄金钥匙

魔法钥匙

在上一章,我们认识了基本矩阵这位"图像红娘",它能帮我们找到两张照片中对应点的神秘联系。但基本矩阵还带着相机的"有色眼镜"(内参),有时候不够纯粹。

这时候,本质矩阵就登场了!它是基本矩阵的"升级版",去掉了相机内参的影响,直接揭示了空间中两个视角的本质关系。

想知道这把"黄金钥匙"如何打开3D视觉的大门吗?让我们一起探索吧!

核心知识点讲解

1 什么是本质矩阵?

本质矩阵(E矩阵)是一个3×3的矩阵,它描述了归一化图像坐标下两个相机视角之间的几何关系。

与基本矩阵不同,本质矩阵消除了相机内参的影响,直接反映了两个相机之间的相对运动(旋转和平移)。

🔑 钥匙类比:如果把基本矩阵比作一把带花纹的钥匙(包含相机内参),那么本质矩阵就是一把纯金打造的钥匙,它去掉了华丽的装饰,直接对应锁芯的结构(相机的相对运动)。

本质矩阵的趣闻与故事

🧙‍♂️ 本质与表象:本质矩阵的英文是"Essential Matrix",这个名字非常贴切,因为它揭示了多视图几何的"本质"(essence)。

🔢 神奇的自由度:本质矩阵虽然是一个3×3的矩阵,但它只有5个自由度!这是因为它满足两个约束条件:行列式为0,且有两个相等的奇异值。

🔄 与基本矩阵的关系:本质矩阵(E)和基本矩阵(F)是"表兄弟",它们的关系是:F = K'^{-T} E K^{-1},其中K和K'是两个相机的内参矩阵。

E
本质矩阵 E = [t]× R 归一化坐标 归一化坐标

2 它能干什么?

本质矩阵的核心魔法是:直接揭示两个相机之间的相对运动(旋转和平移)

从本质矩阵中,我们可以恢复出相机的旋转矩阵(R)平移向量(t)(虽然有一定的歧义性)。这是3D重建和SLAM(同步定位与地图构建)中的关键步骤!

对于归一化图像点,本质矩阵同样可以定义对极约束:如果点 \( \hat{x} \) 和 \( \hat{x}' \) 是对应点,那么它们满足 \( \\hat{x}'^T E \\hat{x} = 0 \)。

🚀 实战应用:在自动驾驶和机器人导航中,本质矩阵帮助我们从连续图像中恢复相机的运动轨迹,让机器知道自己"在哪里"和"往哪里去"!

3 怎么来的?(数学推导)

本质矩阵的推导比基本矩阵更简洁,因为它直接针对归一化图像坐标。让我们一步步来看:

第一步:归一化图像坐标

对于像素坐标 \( x \),我们可以通过相机内参矩阵 \( K \) 将其转换为归一化坐标 \( \hat{x} \):

\( \hat{x} = K^{-1} x \)

第二步:对极约束的简化

对于归一化图像点 \( \hat{x} \) 和 \( \hat{x}' \),对极约束可以简化为:

\( \hat{x}'^T E \hat{x} = 0 \)

第三步:本质矩阵的定义

本质矩阵 \( E \) 定义为:

\( E = [t]_{\times} R \)

其中 \( [t]_{\times} \) 是平移向量 \( t \) 的反对称矩阵,\( R \) 是旋转矩阵。

第四步:本质矩阵的性质

本质矩阵具有以下重要性质:

  • 秩为2(行列式为0)
  • 有两个相等的非零奇异值和一个零奇异值
  • 可以分解为 \( U \begin{bmatrix} 0 & -1 & 0 \\ 1 & 0 & 0 \\ 0 & 0 & 0 \end{bmatrix} V^T \),其中 \( U \) 和 \( V \) 是正交矩阵

第五步:从本质矩阵恢复运动

通过奇异值分解(SVD),我们可以从本质矩阵中恢复出旋转矩阵 \( R \) 和平移向量 \( t \)。不过,这个过程会产生四种可能的解,需要通过cheirality check(可视性检查)来确定正确的解。

具体来说,如果 \( E = U S V^T \),其中 \( S = \begin{bmatrix} \sigma & 0 & 0 \\ 0 & \sigma & 0 \\ 0 & 0 & 0 \end{bmatrix} \),那么可能的旋转矩阵有两种:

\( R_1 = U W V^T \) 或 \( R_2 = U W^T V^T \)

这里 \( W = \begin{bmatrix} 0 & -1 & 0 \\ 1 & 0 & 0 \\ 0 & 0 & 1 \end{bmatrix} \)。

💡 推导小提示:本质矩阵的推导比基本矩阵更简洁,因为它消除了相机内参的影响。记住 \( E = [t]_{\times} R \) 和 \( \hat{x}'^T E \hat{x} = 0 \) 这两个公式,它们是理解本质矩阵的关键!

动手实践:用Python计算本质矩阵

下面我们用OpenCV来计算本质矩阵,并从本质矩阵中恢复相机的运动。这段代码模拟了两个相机拍摄同一物体的场景,并展示了本质矩阵的计算和分解过程。

import numpy as np
import cv2
import matplotlib.pyplot as plt

# 设置中文字体
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]

class EssentialMatrixDemo:
    """
    本质矩阵演示类:计算本质矩阵并恢复相机运动
    """
    def __init__(self):
        # 设置随机种子,确保结果可复现
        np.random.seed(42)

    def generate_correspondence_points(self, num_points=8, noise_level=0.01):
        """
        生成对应点对(模拟从3D场景投影到两个相机)
        """
        # 1. 生成随机3D点(在空间前方1-5米处)
        points_3d = np.random.rand(num_points, 3) * 4 + 1  # 1-5米
        points_3d = np.hstack([points_3d, np.ones((num_points, 1))])  # 齐次坐标

        # 2. 设置相机内参
        f = 500  # 焦距
        c = (320, 240)  # 主点
        K = np.array([[f, 0, c[0]],
                      [0, f, c[1]],
                      [0, 0, 1]])

        # 3. 设置相机外参(相机1在原点,相机2有旋转和平移)
        R1 = np.eye(3)
        t1 = np.zeros(3)
        # 相机2绕y轴旋转30度,向右平移0.5米
        angle = np.radians(30)
        R2 = np.array([[np.cos(angle), 0, np.sin(angle)],
                       [0, 1, 0],
                       [-np.sin(angle), 0, np.cos(angle)]])
        t2 = np.array([0.5, 0, 0])

        # 4. 投影到两个相机
        P1 = K @ np.hstack([R1, t1.reshape(3, 1)])
        P2 = K @ np.hstack([R2, t2.reshape(3, 1)])

        # 投影计算
        points1_hom = P1 @ points_3d.T
        points1 = (points1_hom[:2, :] / points1_hom[2, :]).T
        points1 += np.random.randn(num_points, 2) * noise_level  # 添加噪声

        points2_hom = P2 @ points_3d.T
        points2 = (points2_hom[:2, :] / points2_hom[2, :]).T
        points2 += np.random.randn(num_points, 2) * noise_level  # 添加噪声

        return points1.astype(np.float32), points2.astype(np.float32), K

    def compute_essential_matrix(self, points1, points2, K):
        """
        计算本质矩阵
        """
        # 归一化图像坐标
        points1_norm = cv2.undistortPoints(points1, K, None)
        points2_norm = cv2.undistortPoints(points2, K, None)

        # 计算本质矩阵
        E, mask = cv2.findEssentialMat(points1_norm, points2_norm, np.eye(3), method=cv2.FM_8POINT)

        # 仅保留有效的点对
        points1 = points1[mask.ravel() == 1]
        points2 = points2[mask.ravel() == 1]
        points1_norm = points1_norm[mask.ravel() == 1]
        points2_norm = points2_norm[mask.ravel() == 1]

        return E, points1, points2, points1_norm, points2_norm

    def recover_pose(self, E, points1_norm, points2_norm, K):
        """
        从本质矩阵恢复相机位姿
        """
        # 从本质矩阵恢复旋转和平移
        _, R, t, _ = cv2.recoverPose(E, points1_norm, points2_norm)

        return R, t

    def visualize_results(self, points1, points2, R, t, K):
        """
        可视化结果
        """
        # 绘制匹配点对
        plt.figure(figsize=(12, 6))

        # 第一张图像
        plt.subplot(121)
        plt.title('第一张图像的点')
        plt.scatter(points1[:, 0], points1[:, 1], c='r', marker='o')
        plt.xlim(0, 640)
        plt.ylim(480, 0)  # 翻转y轴,符合图像坐标系
        plt.grid(True)

        # 第二张图像
        plt.subplot(122)
        plt.title('第二张图像的点')
        plt.scatter(points2[:, 0], points2[:, 1], c='b', marker='o')
        plt.xlim(0, 640)
        plt.ylim(480, 0)  # 翻转y轴,符合图像坐标系
        plt.grid(True)

        plt.tight_layout()
        plt.show()

        # 打印恢复的旋转和平移
        print("🔄 恢复的旋转矩阵 R:")
        print(R)
        print("📏 恢复的平移向量 t:")
        print(t)

    def run_demo(self):
        """
        运行演示
        """
        print("🌟 本质矩阵演示开始 🌟")

        # 生成对应点对
        points1, points2, K = self.generate_correspondence_points()

        # 计算本质矩阵
        E, points1, points2, points1_norm, points2_norm = self.compute_essential_matrix(points1, points2, K)

        print("💎 本质矩阵 E 已计算完成:")
        print(E)

        # 验证本质矩阵的正确性
        errors = []
        for i in range(len(points1_norm)):
            p1 = np.append(points1_norm[i][0], 1)  # 转换为齐次坐标
            p2 = np.append(points2_norm[i][0], 1)

            error = np.dot(p2.T, np.dot(E, p1))
            errors.append(abs(error))

        avg_error = np.mean(errors)
        print(f"📏 平均误差: {avg_error:.6f}")

        # 从本质矩阵恢复相机运动
        R, t = self.recover_pose(E, points1_norm, points2_norm, K)

        # 可视化结果
        self.visualize_results(points1, points2, R, t, K)

        print("🎉 本质矩阵演示结束!希望你对本质矩阵有了更深的理解。")

if __name__ == "__main__":
    demo = EssentialMatrixDemo()
    demo.run_demo()

💡 代码小提示:这段代码使用了OpenCV的findEssentialMat函数来计算本质矩阵,以及recoverPose函数来从本质矩阵中恢复相机的旋转和平移。注意在计算本质矩阵前,我们需要先对图像坐标进行归一化处理!

互动探索:相机运动与本质矩阵

下面的互动工具让你可以直观感受相机运动如何影响本质矩阵:调整相机的旋转角度和平移距离,看看本质矩阵如何变化!

相机运动参数

当前值: 30度

当前值: 0.5米

本质矩阵 E:

[ [ 0.000,  0.000,  0.000 ]
  [ 0.000,  0.000,  0.000 ]
  [ 0.000,  0.000,  0.000 ] ]

相机运动可视化

调整左侧的滑块,观察相机运动如何改变本质矩阵!注意本质矩阵的元素如何随着旋转和平移变化。

总结与练习

知识点总结

  • 本质矩阵是归一化图像坐标下两个相机视角之间的几何关系描述。
  • 它的定义是 \( E = [t]_{\times} R \),其中 \( R \) 是旋转矩阵,\( t \) 是平移向量。
  • 对极约束在归一化坐标下简化为 \( \hat{x}'^T E \hat{x} = 0 \)。
  • 从本质矩阵可以恢复相机的运动(旋转和平移),但需要解决歧义性。

练习题

  • 在互动工具中,试着调整相机的旋转和平移,观察本质矩阵如何变化。
  • 修改代码中的相机参数,观察计算结果有什么不同。
  • 思考:本质矩阵和基本矩阵的主要区别是什么?它们各自适用于什么场景?