本质矩阵:透视变换的黄金钥匙
魔法钥匙
在上一章,我们认识了基本矩阵这位"图像红娘",它能帮我们找到两张照片中对应点的神秘联系。但基本矩阵还带着相机的"有色眼镜"(内参),有时候不够纯粹。
这时候,本质矩阵就登场了!它是基本矩阵的"升级版",去掉了相机内参的影响,直接揭示了空间中两个视角的本质关系。
想知道这把"黄金钥匙"如何打开3D视觉的大门吗?让我们一起探索吧!
核心知识点讲解
1 什么是本质矩阵?
本质矩阵(E矩阵)是一个3×3的矩阵,它描述了归一化图像坐标下两个相机视角之间的几何关系。
与基本矩阵不同,本质矩阵消除了相机内参的影响,直接反映了两个相机之间的相对运动(旋转和平移)。
🔑 钥匙类比:如果把基本矩阵比作一把带花纹的钥匙(包含相机内参),那么本质矩阵就是一把纯金打造的钥匙,它去掉了华丽的装饰,直接对应锁芯的结构(相机的相对运动)。
本质矩阵的趣闻与故事
🧙♂️ 本质与表象:本质矩阵的英文是"Essential Matrix",这个名字非常贴切,因为它揭示了多视图几何的"本质"(essence)。
🔢 神奇的自由度:本质矩阵虽然是一个3×3的矩阵,但它只有5个自由度!这是因为它满足两个约束条件:行列式为0,且有两个相等的奇异值。
🔄 与基本矩阵的关系:本质矩阵(E)和基本矩阵(F)是"表兄弟",它们的关系是:F = K'^{-T} E K^{-1}
,其中K和K'是两个相机的内参矩阵。
2 它能干什么?
本质矩阵的核心魔法是:直接揭示两个相机之间的相对运动(旋转和平移)。
从本质矩阵中,我们可以恢复出相机的旋转矩阵(R)和平移向量(t)(虽然有一定的歧义性)。这是3D重建和SLAM(同步定位与地图构建)中的关键步骤!
对于归一化图像点,本质矩阵同样可以定义对极约束:如果点 \( \hat{x} \) 和 \( \hat{x}' \) 是对应点,那么它们满足 \( \\hat{x}'^T E \\hat{x} = 0 \)。
🚀 实战应用:在自动驾驶和机器人导航中,本质矩阵帮助我们从连续图像中恢复相机的运动轨迹,让机器知道自己"在哪里"和"往哪里去"!
3 怎么来的?(数学推导)
本质矩阵的推导比基本矩阵更简洁,因为它直接针对归一化图像坐标。让我们一步步来看:
第一步:归一化图像坐标
对于像素坐标 \( x \),我们可以通过相机内参矩阵 \( K \) 将其转换为归一化坐标 \( \hat{x} \):
第二步:对极约束的简化
对于归一化图像点 \( \hat{x} \) 和 \( \hat{x}' \),对极约束可以简化为:
第三步:本质矩阵的定义
本质矩阵 \( E \) 定义为:
其中 \( [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} \),那么可能的旋转矩阵有两种:
这里 \( 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 \)。
- 从本质矩阵可以恢复相机的运动(旋转和平移),但需要解决歧义性。
练习题
- 在互动工具中,试着调整相机的旋转和平移,观察本质矩阵如何变化。
- 修改代码中的相机参数,观察计算结果有什么不同。
- 思考:本质矩阵和基本矩阵的主要区别是什么?它们各自适用于什么场景?