1. 从像素到世界为什么三维重建绕不开摄像机几何如果你刚接触三维重建可能会觉得它很酷——不就是用几张照片就能在电脑里还原出一个立体的模型吗但当你真正上手试图把第一行代码跑起来时大概率会卡在第一步为什么我拍的照片在计算机眼里只是一堆颜色值它怎么知道物体离我有多远这个问题的答案就藏在“摄像机几何”这个看似枯燥、实则是一切基石的核心概念里。简单来说摄像机几何研究的是三维空间中的一个点如何通过一个光学成像系统比如我们的手机摄像头最终变成我们屏幕上二维图像中的一个像素。这个过程就是三维重建的逆过程。重建之所以可能正是因为我们理解了摄像机如何“看”世界从而能从它“看到”的结果二维图像中反推出世界的原貌三维结构。没有对摄像机几何的深刻理解所有的三维重建算法都将是空中楼阁。无论是你想做AR/VR的内容生成、自动驾驶的环境感知还是文物数字化、工业质检摄像机几何都是你必须跨过的第一道也是最关键的一道门槛。2. 针孔相机模型三维投影的数学基石几乎所有计算机视觉中的摄像机模型都始于一个极度简化的物理模型针孔相机模型。别被这个名字吓到你可以把它想象成一个没有镜头的、密封的盒子只在盒子的一面戳了一个小孔。盒子内部对着小孔的另一面贴着一张感光纸成像平面。外界的景物发出的光线只有穿过那个小孔的光线才能在感光纸上形成一个倒立的像。这个模型虽然简单但它完美地抓住了成像最核心的几何关系透视投影。在数学上我们用一组坐标变换来描述这个过程。2.1 从世界坐标到像素坐标的完整链路假设在真实三维世界中有一个点P [X, Y, Z]^T这里的^T表示转置即这是一个列向量。我们的目标是找到它在照片上对应的像素坐标p [u, v]^T。这个过程需要四步变换第一步世界坐标系到相机坐标系刚体变换世界坐标系是描述整个场景的绝对坐标系。而相机坐标系是以相机光心为原点光轴为Z轴的坐标系。将世界点P_w转换到相机点P_c需要一个旋转矩阵R和一个平移向量t。P_c R * P_w t这个[R|t]合起来被称为相机的外参它描述了相机在三维世界中的位置和朝向。这是重建中需要求解的关键参数之一。第二步相机坐标系到归一化像平面坐标透视投影在相机坐标系下点P_c [X_c, Y_c, Z_c]^T。根据针孔模型它在相机后方虚拟成像平面距离光心焦距f处上的投影点p_n坐标为x_n X_c / Z_c y_n Y_c / Z_c注意这里的(x_n, y_n)是去除了物理单位如米的纯比例值它们位于一个“归一化”的平面上。这一步丢失了深度信息Z_c这正是三维信息在二维投影中丢失的关键也是后续重建需要恢复的。第三步归一化坐标到图像物理坐标内参变换归一化坐标(x_n, y_n)对应的是一个理想成像平面。真实的相机传感器CMOS/CCD有像素大小和可能的不对称性。我们用内参矩阵K来描述这个变换K [ f_x, s, c_x; 0, f_y, c_y; 0, 0, 1 ]其中f_x f / dx,f_y f / dyf是物理焦距dx, dy是单个像素在x和y方向的实际物理尺寸单位米/像素。f_x, f_y是以像素为单位的焦距。(c_x, c_y)主点坐标理论上应该是图像的中心但实际由于制造工艺可能略有偏移。s倾斜因子描述图像坐标轴不垂直的程度现代相机通常为0。 将归一化坐标转换为图像物理坐标单位像素[u_phy, v_phy, 1]^T K * [x_n, y_n, 1]^T第四步纠正镜头畸变非线性变换上述模型是线性的但真实相机镜头存在畸变主要是径向畸变“桶形畸变”或“枕形畸变”和切向畸变。这需要用非线性模型来纠正。常用的布朗-康拉德模型为x_corrected x * (1 k1*r^2 k2*r^4 k3*r^6) [2*p1*x*y p2*(r^22*x^2)] y_corrected y * (1 k1*r^2 k2*r^4 k3*r^6) [p1*(r^22*y^2) 2*p2*x*y]其中(x, y)是纠正前的归一化坐标r^2 x^2 y^2k1, k2, k3是径向畸变系数p1, p2是切向畸变系数。这些畸变系数也是相机内参的一部分。注意在实际操作中步骤顺序通常是先对理想的归一化坐标(x_n, y_n)应用畸变模型得到畸变后的坐标(x_d, y_d)然后再用内参矩阵K乘以[x_d, y_d, 1]^T得到最终的像素坐标。这个顺序不能错。2.2 内参与外参的物理意义与获取内参Intrinsics描述了相机自身的属性就像一个人的“视力特征”。一旦相机出厂在焦距不变的情况下内参基本是固定的。获取内参的过程称为“相机标定”。最经典的方法是使用张正友标定法拍摄不同角度的棋盘格图案通过角点检测和优化算法求解出K矩阵和畸变系数。OpenCV中的cv2.calibrateCamera()函数就是实现此方法的利器。外参Extrinsics描述了相机与世界的位置关系就像一个人“站在哪里看向何方”。在三维重建中对于每张输入图像我们都需要估计其独特的外参[R|t]。这是通过特征点匹配和求解本质矩阵/单应性矩阵再分解得到的。理解这两组参数是理解后续所有多视图几何如对极几何、三角测量的前提。一个常见的误区是认为内参只需要标定一次就一劳永逸。实际上如果拍摄时使用了变焦改变了物理焦距f或者对图像进行了裁剪、缩放内参都会发生变化需要重新标定或估算。3. 对极几何解锁多视图三维信息的钥匙单张图片丢失了深度那么两张图片呢对极几何研究的就是同一个三维点在两个不同视角的相机成像平面上的投影点之间的几何约束关系。它是双目视觉、SFM运动恢复结构、SLAM同步定位与地图构建的核心。3.1 基本矩阵F与本质矩阵E假设有两个相机它们的投影矩阵分别为P K[I | 0]和P K[R | t]这里假设第一个相机坐标系是世界坐标系。对于一个空间点X在两个像平面上的投影点为x和x这里的x, x是归一化相机坐标或去除了内参的坐标。它们满足一个关键的约束方程x^T * E * x 0其中E [t]_x * R被称为本质矩阵。[t]_x是由平移向量t构成的反对称矩阵。本质矩阵E只与外参R, t有关与相机内参无关。如果我们知道相机的内参K和K可以将像素坐标p, p转换到归一化坐标x K^{-1}p,x K^{-1}p。那么约束变为p^T * F * p 0其中F K^{-T} * E * K^{-1}被称为基本矩阵。基本矩阵F同时包含了内参和外参的信息。这个方程x^T E x 0的几何意义非常深刻它意味着点x位于由E x定义的直线极线上。换句话说给定第一幅图像中的一个点x它在第二幅图像中的对应点x必然落在一条特定的直线上而不是在整幅图像中盲目搜索。这极大地缩小了匹配搜索的范围。3.2 八点法求解与实操中的坑如何从一组匹配的点对{(p_i, p_i)}中求解出F或E最经典的方法是八点法。将p^T F p 0方程展开每个点对可以构造一个关于F矩阵9个元素的线性方程。由于F具有尺度不确定性乘以任意非零常数等式仍成立且其秩为2行列式为0实际上只需要8对匹配点就可以线性求解。OpenCV中对应的函数是cv2.findFundamentalMat()它内部就实现了八点法及其改进算法如RANSAC用于剔除误匹配。实操心得八点法理论很美但对噪声极其敏感。在实际应用中直接使用未经处理的像素坐标求解结果往往很差。必须进行归一化Normalization。归一化的步骤包括1) 将每一幅图像的所有特征点平移使其质心位于原点2) 缩放这些点使其到原点的平均距离为√2。这个预处理步骤能显著提高数值稳定性是高质量求解F/E矩阵的必备操作但很多初学者教程会忽略这一点。求解出F后如果我们已知内参K和K就可以通过E K^T F K计算出本质矩阵E。接着可以通过SVD分解从E中恢复出四组可能的[R|t]。如何选出正确的一组需要利用三角测量和正深度约束将空间点根据每组[R|t]三角化出来检查该点在两个相机坐标系下的深度Z坐标是否都为正。只有唯一一组解能满足所有点深度为正的条件。4. 三角测量从二维观测恢复三维坐标当我们通过特征匹配找到了多张图像中同一个物理点的对应像素并且知道了拍摄这些图像的相机参数内参和外参后就可以通过三角测量来计算出这个点的三维坐标。其原理就是最简单的几何交汇两条来自不同相机光心、穿过各自像平面上对应像素的射线在空间中应该相交于那个物理点。4.1 线性三角测量法DLT假设我们有n个视图对于第i个视图其投影矩阵为P_i K_i [R_i | t_i]对应的像素齐次坐标为p_i [u_i, v_i, 1]^T。根据投影方程s_i * p_i P_i * X其中s_i是一个非零尺度因子我们可以消去s_i得到两个线性方程u_i * (P_i^{(3)} * X) - (P_i^{(1)} * X) 0 v_i * (P_i^{(3)} * X) - (P_i^{(2)} * X) 0其中P_i^{(j)}表示P_i矩阵的第j行。对于n个视图我们可以得到2n个关于三维点X [X, Y, Z, 1]^T的线性方程写成A X 0的形式。通过对A进行SVD分解最小特征值对应的特征向量就是X的解。这就是线性三角测量法也称为直接线性变换。4.2 非线性优化与光束法平差线性法求出的解只是一个初值因为它没有考虑噪声的最小二乘意义下的最优解。更精确的方法是通过非线性优化最小化重投影误差。 重投影误差的定义是将估计的三维点X重新投影到各个图像上得到预测的像素坐标p_i然后计算其与实际观测到的像素坐标p_i之间的距离平方和。min_X Σ_i || p_i - π(P_i, X) ||^2其中π(P_i, X)表示用投影矩阵P_i将点X投影到图像上的函数包含畸变纠正。当我们需要同时优化所有三维点的坐标和所有相机的参数时这个问题就扩展成了光束法平差。BA的目标函数是min_{X_j, P_i} Σ_i Σ_j || p_{ij} - π(P_i, X_j) ||^2这里p_{ij}是第i个相机观测到的第j个三维点的像素坐标。BA是一个大规模的非线性最小二乘问题通常使用Levenberg-Marquardt算法求解工具如Ceres Solver、g2o等被广泛使用。踩坑实录三角测量对相机姿态外参的精度非常敏感。如果R, t估计有微小误差两条射线就可能不相交而是呈“歪斜”状态。此时线性法求出的“交点”实际上是两条射线公垂线的中点误差很大。因此高质量的初始外参估计是成功三角测量的前提。在实践中我通常会先用线性法得到初始三维点然后立即做一个仅优化三维点坐标的BA固定相机参数观察重投影误差。如果误差仍然很大说明外参估计可能有问题需要回头检查特征匹配和对极几何的求解质量而不是盲目地进行全局BA。5. 实践指南从理论到代码的跨越理解了原理最终要落地到代码。这里以Python和OpenCV为例勾勒出一个经典双目立体视觉三维重建流程的核心代码骨架和注意事项。5.1 相机标定与图像校正首先你需要标定你的相机或双目相机对。import cv2 import numpy as np # 准备棋盘格角点世界坐标 (假设棋盘格在Z0平面上) objp np.zeros((6*9, 3), np.float32) # 假设棋盘格内角点为6行9列 objp[:,:2] np.mgrid[0:9, 0:6].T.reshape(-1,2) * square_size # square_size 是棋盘格方格实际尺寸 objpoints [] # 3D点 imgpoints [] # 2D点 images glob.glob(calibration_images/*.jpg) for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 查找角点 ret, corners cv2.findChessboardCorners(gray, (9,6), None) if ret: objpoints.append(objp) corners2 cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria) # 亚像素精细化 imgpoints.append(corners2) # 标定相机 ret, K, dist, rvecs, tvecs cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None) print(相机内参矩阵 K:\n, K) print(畸变系数 dist:\n, dist)标定后对于双目相机还需要进行立体标定求出两个相机之间的旋转矩阵R和平移向量T即右相机相对于左相机的外参以及立体校正。# 立体标定 ret, K1, dist1, K2, dist2, R, T, E, F cv2.stereoCalibrate( objpoints, imgpoints_left, imgpoints_right, K1, dist1, K2, dist2, image_size ) # 立体校正计算校正映射表 R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( K1, dist1, K2, dist2, image_size, R, T ) map1x, map1y cv2.initUndistortRectifyMap(K1, dist1, R1, P1, image_size, cv2.CV_32FC1) map2x, map2y cv2.initUndistortRectifyMap(K2, dist2, R2, P2, image_size, cv2.CV_32FC1) # 校正图像 img_left_rectified cv2.remap(img_left, map1x, map1y, cv2.INTER_LINEAR) img_right_rectified cv2.remap(img_right, map2x, map2y, cv2.INTER_LINEAR)立体校正的目的是使两幅图像的极线变为水平对齐这样对应点的搜索只需要在同一行进行极大简化了后续的立体匹配。5.2 特征匹配与稀疏点云重建对于非结构化的多视图图像如SFM流程如下特征提取与匹配使用SIFT、ORB等算法。detector cv2.SIFT_create() kp1, des1 detector.detectAndCompute(img1, None) kp2, des2 detector.detectAndCompute(img2, None) # 使用FLANN或BFMatcher进行匹配 bf cv2.BFMatcher(cv2.NORM_L2, crossCheckTrue) matches bf.match(des1, des2)估计基础矩阵并剔除误匹配使用RANSAC。pts1 np.float32([kp1[m.queryIdx].pt for m in matches]) pts2 np.float32([kp2[m.trainIdx].pt for m in matches]) F, mask cv2.findFundamentalMat(pts1, pts2, cv2.FM_RANSAC) # 使用mask筛选出内点 pts1 pts1[mask.ravel()1] pts2 pts2[mask.ravel()1]从本质矩阵恢复姿态已知内参K。E K.T F K # 假设两个相机内参相同 # 从E恢复Rt _, R, t, mask_pose cv2.recoverPose(E, pts1, pts2, K)三角测量初始点云# 构造两个相机的投影矩阵 P1, P2 P1 K np.hstack((np.eye(3), np.zeros((3,1)))) # 第一个相机作为世界坐标系 P2 K np.hstack((R, t)) # 对每一对匹配点进行三角测量 points_4d cv2.triangulatePoints(P1, P2, pts1.T, pts2.T) points_3d points_4d / points_4d[3] # 齐次坐标转非齐次增量式SFM与全局BA以上是两视图的初始化。实际的SFM系统如OpenMVG, COLMAP会以此为基础增量地添加新图像不断扩展点云并定期进行全局光束法平差以优化所有参数。5.3 深度图生成与稠密重建对于校正后的双目图像可以通过立体匹配计算每个像素的视差进而得到深度图。# 创建立体匹配器这里以SGBM为例 window_size 5 min_disp 0 num_disp 16*5 # 必须是16的整数倍 stereo cv2.StereoSGBM_create( minDisparity min_disp, numDisparities num_disp, blockSize window_size, P1 8*3*window_size**2, P2 32*3*window_size**2, disp12MaxDiff 1, uniquenessRatio 15, speckleWindowSize 100, speckleRange 32 ) disparity stereo.compute(img_left_rectified, img_right_rectified).astype(np.float32) / 16.0 # 将视差图转换为深度图 # Q是stereoRectify得到的重投影矩阵其元素Q[2,3] -1/Tx (Tx是基线长度) depth_map cv2.reprojectImageTo3D(disparity, Q)[:, :, 2] # 取Z坐标即为深度得到深度图后结合相机参数可以将每个像素反投影回三维空间得到稠密点云进而通过泊松重建等算法生成网格模型。在整个实践过程中最大的挑战往往不是算法本身而是数据的质量和参数的调优。光照变化、弱纹理区域、重复纹理、遮挡等都会导致特征匹配失败或立体匹配出错。一个鲁棒的三维重建系统需要大量的工程技巧来处理这些边缘情况例如多尺度匹配、左右一致性检查、后处理滤波等。摄像机几何提供了理论的框架和武器而如何用好这些武器在复杂真实世界中披荆斩棘则是更长期的修炼。