C++与OpenCV实现增量式三维重建:从对极几何到光束法平差
简介基于C与OpenCV构建的增量式三维重建算法项目内容覆盖计算机视觉与数字摄影测量课程设计任务。面向具备一定编程基础的初学者或进阶学习者可支撑毕业设计、课程设计、大作业或工程实训帮助理解从多视角影像恢复三维结构的完整流程。资源包共一百个文件压缩后约六十八点九兆主要包含两个源码文件、一个头文件、二十八张测试图像、六十六个数据文档及说明文档其中提供了经过背景处理的模型数据与检校相机内参可直接放入数据与相机参数文件夹使用。代码实现涉及运动恢复结构的航迹恢复、两两图像暴力匹配、基于匹配点数量的最优化建模及最小生成树求解适合在现有框架上自行调试、扩展功能。已有七十三人学习浏览可作为三维重建入门参考资料。1. 增量式三维重建算法把一次次两视图求解拼成一份可交付的稀疏点云增量式三维重建算法最反直觉的地方在于它把多视图重建拆成一次次两视图求解却没有哪一次两视图结果会在最后保持原样。每次只加入一张新影像先匹配特征估计位姿三角化新点再对局部模型做光束法平差直到所有影像都注册进同一个坐标系。用它做计算机视觉或数字摄影测量的课程设计最大优势是 OpenCV 把特征、对极几何、三角化、PnP 这些原子算子都封成了稳定接口C 则负责把数据流组织清楚最终输出稀疏点云和每张图的相机位姿也能顺带把 COLMAP 这类增量式工具的内部逻辑讲明白。这套流程适合要交计算机视觉大作业的学生也适合想在稀疏重建基础上继续往 3DGS 或 NeRF 走的工程师。2. 对极几何与针孔模型增量式三维重建前先统一坐标约定重建之前第一件事是把像素坐标和空间坐标之间的变换链写在同一页纸上。四个坐标系依次是世界坐标系物方、相机坐标系、归一化图像坐标系、像素坐标系。相机外参 R、t 把世界点搬到相机系内参 fx、fy、cx、cy 把相机系下的点投影到像素平面。OpenCV 里旋转参数默认用三维旋转向量表达矩阵存储类型用 CV_64F这个约定在后面的 projectPoints、solvePnP 里都一致最好从头统一。2.1 针孔相机模型与坐标系变换把 OpenCV 内参写进同一约定下面这段代码把世界点投影到像素坐标是后续所有验证脚本的公共基础#include opencv2/core.hpp #include opencv2/calib3d.hpp #include opencv2/imgproc.hpp cv::Point2d worldToPixel(const cv::Point3d Pw, const cv::Mat rvec, const cv::Mat tvec, const cv::Mat K) { cv::Mat R; cv::Rodrigues(rvec, R); // 旋转向量转旋转矩阵3x1 - 3x3 cv::Mat Pc R * (cv::Mat_double(3, 1) Pw.x, Pw.y, Pw.z) tvec; double z Pc.atdouble(2, 0); double x Pc.atdouble(0, 0) / z; double y Pc.atdouble(1, 0) / z; double u K.atdouble(0, 0) * x K.atdouble(0, 2); double v K.atdouble(1, 1) * y K.atdouble(1, 2); return cv::Point2d(u, v); }代码里的 Rodrigues 负责把旋转向量展开成旋转矩阵这一步在增量式重建里会反复出现建议把“旋转向量、旋转矩阵、四元数”的互相转换封装成小工具函数。K 矩阵是标定得到的相机内参OpenCV 的标定接口给出 fx、fy、cx、cy 和畸变系数注意访问 K.atdouble 时类型必须与矩阵创建时一致否则读出来的数是乱码。四个变换之间的关系可以收敛成一张表变换方向数学表达参数世界系 - 相机系P_c R·P_w tR 为 3x3t 为 3x1相机系 - 归一化像面x X_c/Z_cy Y_c/Z_c透视除法Z_c 0 才可见归一化像面 - 像素系u fx·x cxv fy·y cyfx、fy、cx、cy像素系 - 世界系反算需要深度 Z_c 参与外参 内参 深度最后一行的“深度”是单目重建不确定性的来源像素坐标只有配合深度才能反算物方点所以三角化必须放两幅图。数字摄影测量里这个深度由前方交会求出增量式 SfM 里则是三角化在给定两幅位姿下的最小二乘解。2.2 对极几何约束基础矩阵 F 与本质矩阵 E 怎么选对极几何解决的是“两幅图像之间的点如何约束”。设两个相机光心连线为基线空间点在两幅影像上的投影连成一对极线。基础矩阵 F 在像素坐标下成立本质矩阵 E 在归一化坐标下成立相机内参 K 已知时直接用 EOpenCV 的 findEssentialMat 内部会自动做去畸变后的归一化坐标转换推荐在标定完成后优先用 E。E 只有 5 个自由度最少 5 对同名点即可求解OpenCV 默认用五点算法加 RANSAC比 8 点法对外点更稳。2.3 增量式与全局式重建为什么先做两视图初始化全局式重建先把所有影像的匹配关系连成图再用姿态平均求每个相机的旋转和平移优点是误差不随图像顺序累积增量式每次只接一张新图把上一轮点云和位姿当作先验每次只做一次局部平差。课程设计常见的几十张影像增量式的主要风险是选图顺序和尺度漂移但它胜在工程实现直观、每步都能可视化。这也是多数课程设计选择增量式而不是全局式的原因两视图初始化一旦跑通视觉反馈立刻就有了。3. 用 C 在 OpenCV 中跑通增量式重建的三步管线增量式重建的每一轮插入都可以拆成三步特征匹配、位姿恢复、三角化。先给出特征匹配的最小实现再说明参数怎么调。3.1 SIFT 特征提取与匹配弱纹理影像的参数起点std::vectorcv::DMatch matchImages(const cv::Mat img1, const cv::Mat img2, std::vectorcv::KeyPoint kp1, std::vectorcv::KeyPoint kp2, std::vectorcv::Point2f pts1, std::vectorcv::Point2f pts2) { cv::Ptrcv::SIFT sift cv::SIFT::create(0, 3, 0.04, 10, 1.6); cv::Mat d1, d2; sift-detectAndCompute(img1, cv::noArray(), kp1, d1); sift-detectAndCompute(img2, cv::noArray(), kp2, d2); std::vectorstd::vectorcv::DMatch knn; cv::BFMatcher matcher(cv::NORM_L2); matcher.knnMatch(d1, d2, knn, 2); std::vectorcv::DMatch good; for (size_t i 0; i knn.size(); i) { if (knn[i].size() 2 knn[i][0].distance 0.75f * knn[i][1].distance) { good.push_back(knn[i][0]); } } for (const auto m : good) { pts1.push_back(kp1[m.queryIdx].pt); pts2.push_back(kp2[m.trainIdx].pt); } return good; }SIFT 的 create 参数分别是特征点数上限、每层金字塔层数、对比度阈值、边缘阈值和高斯尺度。0 表示特征点数量不受上限约束对比度阈值调小会留下更多弱纹理点但也更容易带进噪声。BFMatcher 对课程设计的几十张图完全够用不需要上 FLANNratio 阈值取 0.75 是一个稳定的起点匹配数太少时先把对比度阈值降到 0.03 再试。3.2 两视几何位姿恢复本质矩阵分解与四种组合位姿恢复推荐直接用 findEssentialMat 加 recoverPosecv::Mat E, mask; E cv::findEssentialMat(pts1, pts2, K, cv::RANSAC, 0.999, 1.0, mask); cv::Mat R, t; cv::recoverPose(E, pts1, pts2, K, R, t, mask);recoverPose 内部对 E 做 SVD 分解本质矩阵存在四种 R、t 组合只有同时满足三角化点在两个相机前方的组合才物理正确。最容易忽略的坑是镜像解如果不对深度做正负判断点云会出现在相机后方重建结果是一条扭曲的镜像。RANSAC 重投影阈值 1.0 表示允许内点到极线距离为一像素手机拍摄的照片建议放大到 1.5~2.0。3.3 三角化与投影矩阵构造把像素点变成三维点三角化阶段需要把内外参合成投影矩阵再调用 OpenCV 的 triangulatePointscv::Mat P1(3, 4, CV_64F); cv::Mat eye3 cv::Mat::eye(3, 3, CV_64F); cv::hconcat(eye3, cv::Mat::zeros(3, 1, CV_64F), P1); P1 K * P1; cv::Mat P2(3, 4, CV_64F); cv::hconcat(R, t, P2); P2 K * P2; cv::Mat pts4d; cv::triangulatePoints(P1, P2, pts1, pts2, pts4d); std::vectorcv::Point3d points3d; for (int i 0; i pts4d.cols; i) { cv::Mat X pts4d.col(i); double w X.atdouble(3, 0); cv::Point3d p3(X.atdouble(0, 0) / w, X.atdouble(1, 0) / w, X.atdouble(2, 0) / w); if (p3.z 0.1) points3d.push_back(p3); }这里把第一幅影像的相机坐标系当作世界系P1 K[I|0]P2 K[R|t]输入 pts1、pts2 是像素坐标。注意两个细节输入点必须来自去畸变影像否则三角化结果带系统误差纯旋转影像的基线接近零三角化点深度极不稳定应该在注册之前剔除。预处理阶段建议对每帧图像用 cv::undistort 一次性去畸变后面所有点坐标都是理想针孔模型下的像素坐标。环节主要参数课设建议值重建效果差先看哪SIFT 检测contrastThreshold0.04~0.03特征点数是否少于 500特征匹配ratio0.75重复纹理或纯旋转帧findEssentialMat重投影阈值1.0~2.0内参是否标定准确triangulatePoints输入类型像素坐标P1/P2 是否包含 K4. PnP、光束法平差与增量式重建的工程闭环增量式重建的核心循环是从现有模型中挑选下一张影像把它的二维特征和已有三维点做 2D-3D 匹配再用 PnP 求新影像位姿随后和新位姿一起重新三角化更多点。最后对滑动窗口或全局做光束法平差压平漂移。4.1 新影像注册solvePnPRansac 的参数选择cv::Mat rvec, tvec; cv::solvePnPRansac(objectPoints, imagePoints, K, cv::noArray(), rvec, tvec, false, 100, 2.0, 0.99, cv::noArray(), cv::SOLVEPNP_ITERATIVE);objectPoints 是三角化好的 3D 点imagePoints 是新影像上匹配到的 2D 点。RANSAC 迭代 100 次重投影阈值 2.0 像素false 表示不使用预测位姿初始化适合新加入的影像从未被估计过的情况。SOLVEPNP_ITERATIVE 对点数少、分布不均的情况比 P3P 更稳如果希望更快可换 SOLVEPNP_P3P但后续还要再对外点做一次筛选。4.2 光束法平差残差定义与求解器取舍BA 才是增量式重建质量的分水岭。它把所有相机位姿、三维点和观测像素放在一起最小化重投影残差。纯增量式如果不做平差误差会在影像链上不断累积点云出现螺旋形漂移只对最近几帧做局部 BA能让每步收敛更快全局 BA 留给最后一轮精化。操作待估变量为什么用课设推荐两视图初始化R,t 3D点建立基准坐标系选基线合适的首对影像增量注册PnP新影像 6DoF把新图接进模型每加入一帧做一次局部 BA滑窗内位姿点抑制局部漂移窗口 10~15 帧全局 BA全部变量最终精化全部点参与 1~2 轮BA 的 C 实现建议用现成求解器残差定义是核心struct ReprojectionResidual { ReprojectionResidual(const cv::Point2f obs, double fx, double fy, double cx, double cy) : obs_(obs), fx_(fx), fy_(fy), cx_(cx), cy_(cy) {} template typename T bool operator()(const T* const pose, const T* const pt, T* residual) const { // pose: [rx, ry, rz, tx, ty, tz]旋转向量在前 T p[3]; ceres::AngleAxisRotatePoint(pose, pt, p); p[0] pose[3]; p[1] pose[4]; p[2] pose[5]; residual[0] T(fx_) * p[0] / p[2] T(cx_) - T(obs_.x); residual[1] T(fy_) * p[1] / p[2] T(cy_) - T(obs_.y); return true; } };这段代码假定相机内参固定只优化每帧的 6 自由度位姿和三维点坐标。ceres::AngleAxisRotatePoint 是旋转向量与点相乘的辅助函数如果不用 Ceres用 Eigen 或自定义的 Rodrigues 乘点也可以核心是残差必须让优化后的点重新投影回观测位置。BA 运行前相对旋转很大但基线接近零的影像对、边缘遮挡处的观测都应该在注册之前剔除否则残差里会混入大外点。4.3 增量循环里的外点剔除与尺度漂移控制每次注册后保留哪些 3D 点需要定几个硬规则少于三张影像看到的三维点直接丢弃重投影误差大于 3 像素的观测先剔除再重新平差新影像匹配内点数少于 30 时不执行三角化和 BA继续找下一张。尺度漂移通常表现为点云整体被拉伸或压缩解决方法是固定第一对影像的基线长度并在局部 BA 时不释放这个约束。把注册帧数、匹配内点数、重投影误差均值打印到终端是调参时最直接的三条曲线。5. 数字摄影测量视角下的增量式重建共线方程、绝对定向与课程设计组织数字摄影测量课程设计和计算机视觉大作业对“三维重建”的表达方式不同底层约束其实相同。5.1 共线方程摄影测量对针孔模型的另一种写法摄影测量把它叫共线方程物方点、镜头中心、像点三点共线。如果把内外参代入它和 OpenCV 的 projectPoints 完全等价。差别主要在旋转参数视觉常用罗德里格斯向量摄影测量常用 phi、omega、kappa 角元素内参则用主距 f 和主点 x0、y0 表达。double collinearity(const double X, const double Y, const double Z, const double Xs, const double Ys, const double Zs, const double phi, const double omega, const double kappa, const double f, const double x0, const double y0, double x, double y) { const double sp std::sin(phi), so std::sin(omega), sk std::sin(kappa); const double cp std::cos(phi), co std::cos(omega), ck std::cos(kappa); double r11 cp * ck - sp * so * sk; double r12 -cp * sk - sp * so * ck; double r13 -sp * co; double r21 co * sk; double r22 co * ck; double r23 -so; double r31 sp * ck cp * so * sk; double r32 -sp * sk cp * so * ck; double r33 cp * co; double dX X - Xs, dY Y - Ys, dZ Z - Zs; double auxX r11 * dX r12 * dY r13 * dZ; double auxY r21 * dX r22 * dY r23 * dZ; double auxZ r31 * dX r32 * dY r33 * dZ; x x0 - f * auxX / auxZ; y y0 - f * auxY / auxZ; return auxZ; }这里演示的是欧拉角到旋转矩阵再投影的标准写法实际摄影测量软件中角元素顺序有不同约定写完最好先用已知矩形的影像打印几个点验证正负号。对比项计算机视觉 / OpenCV数字摄影测量旋转参数旋转向量、四元数phi、omega、kappa 角元素坐标基准任意世界系尺度自由物方坐标常是测区/大地坐标内参表达K 矩阵 fx fy cx cyf、x0、y0常有畸变系数控制信息少用自标定像控点参与区域网平差平差方法光束法平差区域网光束法平差5.2 绝对定向把重建点云转到物方坐标系的七参数估计视觉重建出来的点云坐标是任意定义的尺度没有物理意义。要把结果用于摄影测量就要用像控点做绝对定向求稀疏点云与已知物方坐标之间的相似变换共 7 个参数3 平移、3 旋转、1 尺度。3 个不共线的点提供初值更多点用最小二乘。SVD 是求旋转矩阵的标准做法// srcPts重建坐标dstPts物方坐标两组已重心化 cv::Mat cov (src_demean.t() * dst_demean) / n; cv::Mat U, W, Vt; cv::SVD::compute(cov, W, U, Vt); cv::Mat R Vt.t() * U.t(); // 最优旋转 if (cv::determinant(R) 0) R.col(2) * -1; // 防止镜像变换防止镜像这一步非常关键漏掉它会出现左右手坐标系翻转的点云重建结果无法用于任何测图。尺度 s 可以由两组点各自到重心距离的比值先估计再和非线性优化一起精化。5.3 课程设计如何把松散模块收敛成可论述的成果课程设计不需要从头手写 BA模块边界可以划成特征匹配与几何校验增量式注册与三角化绝对定向。验收时除了稀疏点云还需要给出每张影像的位姿轨迹图和重投影误差统计。经典的《计算机视觉算法与应用》第二版里有运动恢复结构的系统讲解COLMAP 的 pipeline 也值得对照想继续学“OpenCV 三维重建到 3DGS 分步学习路线”则从本套稀疏重建出发先补 MVS 稠密重建再做表面重建。数字摄影测量课程答辩通常会问两个问题物方坐标系怎么定义连接点如何保证在不同影像上的同一性对应到代码里分别是绝对定向和 track 管理报告里画清数据流比堆代码有效得多。6. 验证重建质量的三个具体手段残差、可视性与正射投影重建跑完不能只看点云“看起来像不像”建议用三个手段验证分别回答位姿准不准、点云是否被重复观测、坐标系是否一致。第一全量重投影误差。对每张已注册影像把每个可见三维点重新投影回像素与检测到的像素位置求差。误差单位是像素分布应以零为中心均值小于 1 像素说明内参和位姿合理大于 3 像素则说明有错配或 BA 没收敛。std::vectorcv::Point2d rep; cv::projectPoints(points3d, rvec, tvec, K, cv::noArray(), rep); double sum 0.0; for (size_t i 0; i rep.size(); i) sum cv::norm(obs2d[i] - rep[i]); double meanErr sum / rep.size();第二可视性检查。把重建三维点投影回原影像抽查应该落在显著边缘上的点是否贴着边缘统计每个三维点被多少影像看见只被一张图看见的点通常是误匹配外点少于三张视图的 track 在课程设计里可以直接剔除。第三正射投影对比。将点云按某一轴向投影成俯视或正视平面检查墙面是否成直线、地面点是否在同一高度这是快速发现尺度漂移和旋转方向颠倒的办法。把逐图平均重投影误差画成直方图叠加相机位姿轨迹能直接看出误差集中在哪几张影像上通常就是纯旋转或弱纹理帧。把这三项写成独立函数放进工程每一轮增量后跑一次比盯着终端日志更容易定位问题。本文还有配套的精品资源点击获取