3. Epipolar Geometry in Practice: From Theory to Implementation with Essential and Fundamental Matri

1. 对极几何:从“两张照片”到“三维世界”的桥梁

想象一下,你站在一个广场上,用手机拍了一张照片。然后你往旁边走了几步,又拍了一张。这两张照片里的同一个建筑物,在画面中的位置肯定不一样了。你的大脑能瞬间理解,这两张照片拍的是同一个东西,只是视角变了。对极几何(Epipolar Geometry)要解决的,就是让计算机也能拥有这种“理解”能力。

在计算机视觉,尤其是三维重建和立体视觉领域,我们常常面临一个核心问题:给你两张从不同位置拍摄的同一场景的照片,如何恢复出相机的运动(你走了多远、转了多少角度)以及场景的粗略三维结构?对极几何就是解决这个“两张图”问题的数学模型。它不依赖于任何已知的三维模型,纯粹从两幅二维图像中的匹配点对出发,建立起一个强大的几何约束。

这个约束的核心,就是极线。在左图(第一张照片)中任意选取一个像素点,比如建筑物的一个角点,那么它在右图(第二张照片)中的对应点会落在一条特定的直线上,这条线就是极线。反过来,右图的点也会在左图中确定一条极线。这个关系不是随机的,而是由两个相机之间的相对位置(旋转和平移)严格决定的。理解并利用这个约束,我们就能从海量的像素匹配可能性中,极大地缩小搜索范围,甚至直接计算出相机的运动。

我刚开始接触这个概念时,总觉得很抽象。直到我亲手写代码实现了一遍,看着算法从一堆杂乱的点中准确地画出那些极线,才真正体会到它的美妙。接下来,我们就抛开复杂的数学外壳,看看怎么把这份“美妙”变成可以运行的代码。我们会聚焦于两个核心矩阵:本质矩阵(Essential Matrix)基础矩阵(Fundamental Matrix),它们是整个对极几何理论的数学结晶。

2. 核心矩阵解密:Essential Matrix 与 Fundamental Matrix

2.1 本质矩阵 E:理想相机下的几何本质

让我们先从最简单的情况开始。假设我们有两个“理想”的相机,就像我们课程里常说的“标准相机”(Canonical Cameras)。它们的内部参数都是最简化的:焦距为1,图像中心在(0,0),没有畸变。用矩阵表示就是内参矩阵 K 为单位矩阵 I。

在这种情况下,一个三维空间点 P 在这两个相机坐标系下的投影关系,可以被一个神奇的 3x3 矩阵联系起来,这就是本质矩阵 E。它的形式非常简洁:E = [t]_x * R。这里,R 是两个相机之间的旋转矩阵,t 是平移向量(注意不是矩阵T,而是它的向量形式),[t]_x 表示由向量 t 构成的反对称矩阵。

这个公式蕴含了什么?它直接编码了相机运动的 RtE 矩阵本身只有5个自由度(旋转3个,平移方向2个,因为平移向量的尺度无法确定),并且是一个秩为2的奇异矩阵(行列式为0)。在实际操作中,一旦我们通过匹配点计算出了 E,就可以通过矩阵分解(通常是SVD)来恢复出 Rt。不过这里有个著名的“尺度不确定性”和“四重解”问题,需要额外的约束(比如点的深度为正)来确定唯一正确的解。

一个生动的类比:想象你蒙上眼睛,被人带着从房间的A点走到B点,然后让你指出A点相对于B点的方向。你虽然不知道走了多远(尺度不确定),但你能知道朝哪个方向转了多少角度(旋转),以及是朝哪个方向移动的(平移方向)。本质矩阵 E 就像是这个过程中你的身体感知到的纯几何关系。

2.2 基础矩阵 F:真实世界相机的万能钥匙

然而,我们的手机、相机都不是“理想”的。它们有焦距、有像素偏移、还有镜头畸变。这些内部参数构成了相机的内参矩阵 K。当相机不“标准”时,本质矩阵 E 就无法直接描述图像像素点之间的关系了。

这时,基础矩阵 F 就登场了。它是本质矩阵在真实相机下的推广,公式为:F = K'^(-T) * E * K^(-1) = K'^(-T) * [t]_x * R * K^(-1)。这里 KK' 分别是左右相机的内参矩阵。

F 矩阵的强大之处在于,它直接建立了两个图像像素坐标之间的代数关系。对于一对匹配点 p(左图)和 p'(右图),它们满足一个极其重要的约束方程:p'^T * F * p = 0。这个方程是后续所有算法的基础。F 矩阵有7个自由度,比 E 多2个,因为它同时包含了相机内参的信息。

最实用的特性:给定左图的一个点 p,我们可以直接算出它在右图中对应的极线 l' = F * p。这意味着,当我们在右图中搜索 p 的匹配点时,不需要在整个图像上盲目寻找,只需要沿着这条极线搜索即可,这极大地提升了匹配效率和准确性。这个特性在立体匹配和运动恢复结构(SfM)中至关重要。

我自己的项目经验是,在实现视觉里程计(VO)时,首先做的就是计算连续帧之间的 F 矩阵。用它来验证特征匹配是否正确(偏离极线太远的匹配点可能是误匹配),并且为后续的 E 矩阵估计和位姿恢复提供一个干净的输入。

3. 八点法:从理论到代码的第一步

理论很美,但怎么算出来这个 F 矩阵呢?最经典、最直接的方法就是八点法。为什么是八个点?因为 F 矩阵有9个元素,但尺度不确定性可以去掉一个自由度(通常令 F(3,3)=1||F||=1),所以至少需要8对匹配点来建立8个方程。

3.1 标准八点法的原理与陷阱

我们从基础方程 p'^T * F * p = 0 出发。把 p = [u, v, 1]^T, p' = [u', v', 1]^TF = [f11, f12, f13; f21, f22, f23; f31, f32, f33] 代入并展开,你会得到一个关于 F 矩阵元素的线性方程: u'u*f11 + u'v*f12 + u'*f13 + v'u*f21 + v'v*f22 + v'*f23 + u*f31 + v*f32 + f33 = 0

对于每一对匹配点 (p_i, p_i'),我们都能得到这样一个方程。当我们有 N(N>=8)对点时,就可以构建一个齐次线性方程组:A * f = 0。其中 f 是将 F 矩阵按行拉直成一个9维向量 [f11, f12, f13, f21, f22, f23, f31, f32, f33]^TA 是一个 N x 9 的矩阵,每一行由一对点的坐标构成。

接下来,我们通过求解这个方程组的最小二乘解来得到 f。这通常通过奇异值分解(SVD) 来完成。对矩阵 A 进行 SVD:A = U * Σ * V^T,那么解 f 就是 V 矩阵的最后一列(对应最小奇异值的那个右奇异向量)。

但是,这里有两个大坑等着你:

  1. 数值稳定性问题:图像像素坐标的数值范围(比如几百到几千)和齐次坐标的1在量级上差异巨大,这会导致矩阵 A 的条件数很差,求出的 F 矩阵噪声极大。
  2. 秩约束问题:我们求出的 F_hat 是满秩(秩为3)的,但理论上真正的 F 矩阵秩必须为2。一个满秩的 F 矩阵意味着存在一个非零的三维点同时满足两个相机的投影方程,这在实际几何中是不成立的(除非该点正好在基线上)。

对于问题二,标准的处理方法是进行第二次 SVD。对求出的 F_hat 进行 SVD:F_hat = U * diag(s1, s2, s3) * V^T,然后强制让最小的奇异值 s3 为零,得到秩为2的矩阵:F_rank2 = U * diag(s1, s2, 0) * V^T。这才是我们最终要的基础矩阵。

3.2 归一化八点法:让算法真正可用

为了解决数值稳定性的问题,归一化八点法 是必须的步骤。这也是我踩过坑后学到的:不归一化,你的八点法基本没法用在实际图像上。

归一化的思想很简单:在构建矩阵 A 之前,先对两幅图像上的所有特征点坐标进行一个平移和缩放的变换。

  • 平移:将所有点的坐标中心平移到原点。即,计算所有特征点坐标的均值 (mean_u, mean_v),然后每个点减去这个均值。
  • 缩放:将平移后的点进行缩放,使得它们到原点的平均距离(或RMS距离)为 √2。这个缩放因子通常是 scale = √2 / avg_distance

这个变换可以用一个 3x3 的相似变换矩阵 T 来表示。对左图点集应用变换 T,对右图点集应用变换 T'。经过变换后,点的坐标值大致在 [-1, 1] 范围内,数值量级统一,极大地改善了矩阵 A 的条件数。

算法步骤变为:

  1. 归一化左右图的点集:q = T * p, q' = T' * p'
  2. 用归一化后的点对 (q, q') 构建矩阵 A_q,并用标准八点法求解出归一化坐标系下的基础矩阵 F_q
  3. F_q 反变换回原始像素坐标系:F = T'^T * F_q * T

实测下来,这一步是成败的关键。 我曾经在无人机图像上测试,未归一化的八点法得到的 F 矩阵完全无法用来绘制正确的极线;而进行归一化后,极线约束立刻变得清晰准确。下面的代码片段展示了这个核心过程:

import numpy as np

def normalize_points(points):
    """
    归一化点集
    points: (N, 2) 或 (N, 3) 的数组,假设最后一维为1
    返回: 归一化后的点 (N, 3),以及变换矩阵 T (3x3)
    """
    # 计算质心
    centroid = np.mean(points[:, :2], axis=0) # (2,)
    # 计算到质心的平均距离
    mean_dist = np.mean(np.linalg.norm(points[:, :2] - centroid, axis=1))
    # 计算缩放因子,使平均距离为 sqrt(2)
    scale = np.sqrt(2) / mean_dist
    # 构建变换矩阵 T
    T = np.array([[scale, 0, -scale*centroid[0]],
                  [0, scale, -scale*centroid[1]],
                  [0, 0, 1]])
    # 应用变换
    points_homo = np.hstack([points[:, :2], np.ones((points.shape[0], 1))]) # (N, 3)
    points_norm = (T @ points_homo.T).T # (N, 3)
    return points_norm, T

def compute_fundamental_matrix_normalized(pts1, pts2):
    """
    使用归一化八点法计算基础矩阵 F
    pts1, pts2: (N, 2) 数组,N >= 8
    """
    # 1. 归一化
    pts1_norm, T1 = normalize_points(pts1)
    pts2_norm, T2 = normalize_points(pts2)
    
    # 2. 构建方程 A * f = 0
    N = pts1.shape[0]
    A = np.zeros((N, 9))
    for i in range(N):
        u1, v1, _ = pts1_norm[i]
        u2, v2, _ = pts2_norm[i]
        A[i] = [u2*u1, u2*v1, u2, v2*u1, v2*v1, v2, u1, v1, 1]
    
    # 3. SVD 求解最小二乘解
    U, S, Vt = np.linalg.svd(A)
    F_norm_vec = Vt[-1, :] # 取最后一个右奇异向量
    F_norm = F_norm_vec.reshape(3, 3)
    
    # 4. 强制秩为2约束
    U_F, S_F, Vt_F = np.linalg.svd(F_norm)
    S_F[2] = 0 # 将最小奇异值设为0
    F_norm_rank2 = U_F @ np.diag(S_F) @ Vt_F
    
    # 5. 反归一化
    F = T2.T @ F_norm_rank2 @ T1
    
    # 6. 可选:将 F 的最后一个元素归一化为1(或保持F的Frobenius范数为1)
    F = F / F[2, 2]
    return F

4. 实战演练:用OpenCV和Python实现极线几何

光说不练假把式。我们现在就用 OpenCV 和 Python,从头走一遍流程:特征点匹配 -> 计算 F 矩阵 -> 绘制极线。我会分享一些代码细节和调试技巧。

4.1 特征点检测与匹配

首先,我们需要在两幅图像中找到足够多且正确的匹配点对。这里我选择使用 SIFT 特征和 FLANN 匹配器,因为它比传统的 ORB 在旋转和尺度变化上更稳定。

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

def find_keypoints_and_matches(img1, img2):
    """
    使用SIFT和FLANN进行特征匹配
    返回: pts1, pts2 (N, 2) 数组,代表匹配点的坐标
    """
    # 初始化SIFT检测器
    sift = cv2.SIFT_create()
    # 寻找关键点和描述符
    kp1, des1 = sift.detectAndCompute(img1, None)
    kp2, des2 = sift.detectAndCompute(img2, None)
    
    # FLANN 参数
    FLANN_INDEX_KDTREE = 1
    index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
    search_params = dict(checks=50)
    flann = cv2.FlannBasedMatcher(index_params, search_params)
    
    # KNN匹配,k=2
    matches = flann.knnMatch(des1, des2, k=2)
    
    # 应用 Lowe's ratio test 筛选好的匹配
    good_matches = []
    pts1 = []
    pts2 = []
    for m, n in matches:
        if m.distance < 0.7 * n.distance: # 经典比例阈值
            good_matches.append(m)
            pts1.append(kp1[m.queryIdx].pt)
            pts2.append(kp2[m.trainIdx].pt)
    
    # 转换为numpy数组
    pts1 = np.float32(pts1).reshape(-1, 2)
    pts2 = np.float32(pts2).reshape(-1, 2)
    
    print(f"找到 {len(good_matches)} 对良好匹配点。")
    return pts1, pts2, good_matches

4.2 使用RANSAC鲁棒估计F矩阵

直接使用所有匹配点计算F矩阵风险很高,因为匹配中难免存在误匹配(外点)。RANSAC 是处理这个问题的利器。OpenCV 已经为我们提供了 cv2.findFundamentalMat 函数,它内部就集成了归一化八点法和 RANSAC。

def compute_f_matrix_with_ransac(pts1, pts2):
    """
    使用RANSAC和八点法计算基础矩阵,并剔除外点
    返回: F矩阵, 内点掩码 (inlier_mask)
    """
    # 使用RANSAC方法计算基础矩阵
    # method=cv2.FM_RANSAC 表示使用RANSAC算法
    # ransacReprojThreshold 是点到极线的最大像素距离,用于判断内点,通常设为1-3像素
    # confidence 是置信度
    F, inlier_mask = cv2.findFundamentalMat(pts1, pts2, cv2.FM_RANSAC, ransacReprojThreshold=1.0, confidence=0.99)
    
    if F is None or F.shape != (3, 3):
        raise ValueError("未能计算出有效的基础矩阵。")
    
    # 打印内点数量
    num_inliers = np.sum(inlier_mask)
    print(f"RANSAC找到 {num_inliers} 个内点 (总共 {len(pts1)} 个匹配点)。")
    
    # 只保留内点
    pts1_inlier = pts1[inlier_mask.ravel() == 1]
    pts2_inlier = pts2[inlier_mask.ravel() == 1]
    
    return F, pts1_inlier, pts2_inlier

4.3 绘制极线:可视化验证

计算出的 F 矩阵对不对?最直观的验证方法就是绘制极线。我们在左图选几个点,用 F 矩阵计算出它们在右图对应的极线,看看这些极线是否都穿过右图中对应的匹配点。

def draw_epipolar_lines(img1, img2, pts1, pts2, F, sample_num=10):
    """
    在图像上绘制极线
    sample_num: 随机选取多少个点进行绘制
    """
    # 随机选择一些点
    indices = np.random.choice(len(pts1), min(sample_num, len(pts1)), replace=False)
    sample_pts1 = pts1[indices]
    sample_pts2 = pts2[indices]
    
    # 计算右图对应点的极线 (在左图坐标系下: l' = F * p)
    # 注意:OpenCV的绘图函数需要直线的系数 (A, B, C) 满足 Ax + By + C = 0
    lines2 = cv2.computeCorrespondEpilines(sample_pts1.reshape(-1, 1, 2), 1, F)
    lines2 = lines2.reshape(-1, 3)
    
    # 创建一个画布,左右并排显示两幅图
    h, w = img1.shape[:2]
    vis_img = np.hstack([img1, img2])
    
    # 为左右图分别生成颜色
    colors = [tuple(np.random.randint(0, 255, 3).tolist()) for _ in range(len(sample_pts1))]
    
    for (x1, y1), (x2, y2), line, color in zip(sample_pts1, sample_pts2, lines2, colors):
        # 颜色转换
        color = tuple(map(int, color))
        
        # 在左图绘制选中的点
        cv2.circle(vis_img, (int(x1), int(y1)), 8, color, -1)
        
        # 在右图绘制对应的匹配点
        cv2.circle(vis_img, (int(x2) + w, int(y2)), 8, color, -1)
        
        # 计算右图极线的两个端点 (在右图坐标系下)
        # 直线方程: line[0]*x + line[1]*y + line[2] = 0
        a, b, c = line
        # 选择两个x值,计算对应的y值
        x0, x1 = 0, w
        y0 = int(-(c + a * x0) / b) if b != 0 else 0
        y1 = int(-(c + a * x1) / b) if b != 0 else 0
        
        # 在右图(画布的右半部分)绘制极线
        cv2.line(vis_img, (x0 + w, y0), (x1 + w, y1), color, 2)
        
        # 可选:在左图也绘制对应极线(需要计算左图的极线 l = F^T * p')
        # lines1 = cv2.computeCorrespondEpilines(sample_pts2.reshape(-1, 1, 2), 2, F)
        # ... 绘制代码类似
    
    plt.figure(figsize=(15, 8))
    plt.imshow(cv2.cvtColor(vis_img, cv2.COLOR_BGR2RGB))
    plt.title('Epipolar Lines Visualization (Points in left image, Lines in right image)')
    plt.axis('off')
    plt.show()

4.4 从F矩阵恢复相机运动

得到可靠的 F 矩阵后,如果我们已知相机的内参矩阵 K,就可以进一步计算本质矩阵 E,并恢复出相机间的相对运动 Rt

def recover_pose_from_E(F, K, pts1, pts2):
    """
    从基础矩阵F和相机内参K恢复相对姿态R, t
    注意:这里需要一组匹配点来从四组解中选出正确的一个。
    """
    # 计算本质矩阵 E = K'^T * F * K
    # 假设两个相机内参相同
    E = K.T @ F @ K
    
    # 对E进行SVD分解
    U, S, Vt = np.linalg.svd(E)
    
    # 确保E的奇异值满足 [s, s, 0] 的性质
    S = np.diag([1.0, 1.0, 0.0]) # 或者用 (S[0] + S[1]) / 2
    E_corrected = U @ S @ Vt
    
    # 重新SVD分解修正后的E
    U, S, Vt = np.linalg.svd(E_corrected)
    
    # 定义两个可能的旋转矩阵
    W = np.array([[0, -1, 0],
                  [1, 0, 0],
                  [0, 0, 1]])
    
    R1 = U @ W @ Vt
    R2 = U @ W.T @ Vt
    
    # 确保旋转矩阵的行列式为+1 (排除反射)
    if np.linalg.det(R1) < 0:
        R1 = -R1
    if np.linalg.det(R2) < 0:
        R2 = -R2
    
    # 可能的平移向量(方向)
    t1 = U[:, 2].reshape(3, 1)
    t2 = -U[:, 2].reshape(3, 1)
    
    # 四组可能的解: (R1, t1), (R1, t2), (R2, t1), (R2, t2)
    solutions = [(R1, t1), (R1, t2), (R2, t1), (R2, t2)]
    
    # 使用一组匹配点进行三角化,选择使点在两个相机前都具有正深度的解
    # 这里需要将像素坐标归一化(去除内参影响)
    pts1_norm = cv2.undistortPoints(pts1.reshape(-1, 1, 2), K, None).reshape(-1, 2)
    pts2_norm = cv2.undistortPoints(pts2.reshape(-1, 1, 2), K, None).reshape(-1, 2)
    
    # 通常选取第一个点进行测试
    p1 = np.hstack([pts1_norm[0], 1]).reshape(3, 1)
    p2 = np.hstack([pts2_norm[0], 1]).reshape(3, 1)
    
    correct_solution = None
    for R, t in solutions:
        # 构造两个相机的投影矩阵 P1 = [I | 0], P2 = [R | t]
        P1 = np.hstack([np.eye(3), np.zeros((3, 1))])
        P2 = np.hstack([R, t])
        
        # 使用线性三角化方法(DLT)计算三维点
        # 构建A矩阵, A = [p1叉乘P1; p2叉乘P2] (取前两行)
        # ... 这里省略具体的三角化代码
        
        # 计算三维点X
        # X = linear_triangulation(p1, p2, P1, P2)
        
        # 检查深度:在相机1坐标系下,X的z坐标应为正;在相机2坐标系下, (R*X + t)的z坐标应为正
        # if depth1 > 0 and depth2 > 0:
        #     correct_solution = (R, t)
        #     break
    
    # 在实际项目中,应使用多个点进行验证,选择内点最多的解
    # 更稳健的方法是使用OpenCV的cv2.recoverPose函数
    return solutions # 返回所有可能解,实际应用中需进一步筛选

在实际开发中,我强烈推荐直接使用 OpenCV 的 cv2.recoverPose 函数,它封装了从 E 矩阵恢复姿态并利用点集选择正确解的全过程,比自己实现要稳健得多。

5. 图像校正:将问题“拉直”的实用技巧

对极几何还有一个非常实用的应用:图像校正。它的目标是将一对立体图像进行变换,使得它们对应的极线变成水平且共线的。换句话说,让两幅图像“行对齐”。校正之后,右图中任意一点的匹配点,必然位于左图同一行上。这为立体匹配算法(如计算视差图)带来了巨大的便利,因为搜索从二维降低到了一维。

5.1 校正的原理

校正的核心思想是寻找两个单应性矩阵 H1H2,分别作用于左图和右图,使得变换后的图像满足:

  1. 极点被映射到无穷远e1 被映射到 [1, 0, 0]^Te2 被映射到 [1, 0, 0]^T。这意味着极线变成了水平线。
  2. 匹配点对的垂直坐标尽可能对齐:即最小化 (H1 * p)_y - (H2 * p')_y

OpenCV 提供了 cv2.stereoRectifyUncalibrated 函数来实现基于基础矩阵 F 的图像校正。它内部使用的就是类似 Hartley 的算法,通过计算 H1H2 来最小化重投影误差。

5.2 代码实现与效果

def stereo_rectify_uncalibrated(img1, img2, pts1, pts2, F):
    """
    使用基础矩阵F进行非标定立体校正
    """
    # 计算校正变换单应性矩阵
    h1, w1 = img1.shape[:2]
    h2, w2 = img2.shape[:2]
    
    # 注意:此函数需要足够多的内点匹配对
    ret, H1, H2 = cv2.stereoRectifyUncalibrated(pts1.reshape(-1, 1, 2).astype(np.float32),
                                                 pts2.reshape(-1, 1, 2).astype(np.float32),
                                                 F, imgSize=(w1, h1))
    if not ret:
        print("校正失败!")
        return None, None, None, None
    
    # 对图像应用校正变换
    img1_rectified = cv2.warpPerspective(img1, H1, (w1, h1))
    img2_rectified = cv2.warpPerspective(img2, H2, (w2, h2))
    
    # 绘制校正后的图像和水平线
    vis_rectified = np.hstack([img1_rectified, img2_rectified])
    for y in range(0, h1, 50): # 每隔50像素画一条水平线
        cv2.line(vis_rectified, (0, y), (w1 + w2, y), (0, 255, 0), 1)
    
    plt.figure(figsize=(15, 8))
    plt.subplot(1, 2, 1)
    plt.imshow(cv2.cvtColor(np.hstack([img1, img2]), cv2.COLOR_BGR2RGB))
    plt.title('Original Stereo Pair')
    plt.axis('off')
    
    plt.subplot(1, 2, 2)
    plt.imshow(cv2.cvtColor(vis_rectified, cv2.COLOR_BGR2RGB))
    plt.title('Rectified Stereo Pair (Epipolar lines are horizontal)')
    plt.axis('off')
    plt.show()
    
    return img1_rectified, img2_rectified, H1, H2

运行这段代码后,你会看到校正后的图像对,绿色的水平线贯穿左右图。这意味着,现在在左图中选择一个特征点,在右图中只需要在同一行进行水平搜索就能找到它的匹配点,极大地简化了后续的立体匹配或深度估计流程。我在做双目深度估计项目时,图像校正是预处理中不可或缺的一步,它能将立体匹配的准确率提升一个档次。

6. 避坑指南与性能优化

纸上得来终觉浅,绝知此事要躬行。在实现和应用对极几何的过程中,我遇到过不少坑,这里分享几个关键点。

1. 匹配点质量是生命线 八点法、RANSAC 都假设内点(正确匹配)占多数。如果特征匹配本身就很差,再好的算法也无力回天。务必花时间调优特征检测和匹配的参数:

  • 特征检测器:对于纹理丰富的场景,SIFT/SURF 表现稳健;对于实时应用,ORB 是很好的选择,但要注意其旋转不变性稍弱。
  • 匹配筛选:Lowe‘s ratio test(比例测试)是必须的。我通常将比值阈值设在 0.7~0.8。比值越接近1,匹配越模糊,误匹配越多。
  • 交叉验证:使用 cv2.BFMatcher 时,可以尝试双向匹配(即从图1到图2和从图2到图1都匹配一次,只保留一致的匹配),这能进一步过滤外点。

2. RANSAC 参数设置

  • ransacReprojThreshold:这个参数非常关键。它表示一个点与其对应极线之间的最大像素距离,超过此距离则被视为外点。对于标定良好的相机和精确匹配,可以设小(如 0.5~1.0);对于噪声较大或广角镜头图像,可以适当放宽(如 1.5~3.0)。
  • confidence:置信度,通常设为 0.990.999。越高,RANSAC 迭代次数越多,计算越慢,但找到正确模型的概率越大。

3. 归一化的重要性再强调 我见过很多初学者直接拿像素坐标去算 F 矩阵,结果完全不对。务必先进行坐标归一化。OpenCV 的 findFundamentalMatFM_8POINT 方法下不会自动归一化,但在 FM_RANSAC 等方法内部,通常会包含归一化步骤。不过,为了理解和控制流程,自己实现一遍归一化八点法非常有价值。

4. 尺度不确定性与三角化EF 恢复出的平移向量 t 是单位向量,没有真实的尺度。这意味着我们恢复出的三维场景和相机运动也是在一个未知的尺度下。要获得真实尺度,你需要:

  • 已知场景尺寸:在场景中放置一个已知长度的物体。
  • 传感器运动信息:例如,从IMU或轮速计获取移动的实际距离。
  • 后续的Bundle Adjustment:在完整的SfM或SLAM管道中,通过优化来统一尺度。

5. 退化配置 有些相机运动会导致对极几何约束失效或变得不稳定,例如:

  • 纯旋转:如果相机只有旋转没有平移 (t=0),则 E 矩阵为零矩阵,极线几何不成立。所有匹配点都满足 p' = R * p,无法恢复深度。
  • 共面点:如果所有三维点都位于一个平面上,则存在一个单应性矩阵 H 使得 p' = H * p。这种情况下,F 矩阵和 H 矩阵都会拟合数据,算法可能混淆。通常需要同时估计 FH,并选择重投影误差更小的模型。

处理这些情况需要更鲁棒的模型选择策略,例如在RANSAC框架内同时尝试对极几何模型和单应性模型。

对极几何是连接二维图像与三维世界的基石。从理解极线约束,到亲手实现八点法计算基础矩阵,再到用它来校正图像、恢复相机运动,这个过程充满了挑战,也充满了乐趣。我建议你找一些公开的数据集(如Middlebury Stereo Dataset, KITTI),从头开始实现这个流程。当你看到自己代码计算出的极线精准地穿过匹配点时,当你成功将一对歪斜的图像校正得横平竖直时,那种成就感就是学习计算机视觉最好的动力。记住,理论是地图,代码是脚步,只有亲自走过一遍,路才能真正属于你。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值