十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++实现单像空间后方交会:从共线方程到最小二乘迭代

C++实现单像空间后方交会:从共线方程到最小二乘迭代 简介这是一份面向摄影测量与遥感初学者的C版单像空间后方交会实验报告可作为测绘、遥感专业课程设计或毕业设计的参考材料。报告系统梳理了后方交会的基本原理与算法流程从摄影机主距、像片比例尺、控制点坐标等已知数据出发逐步完成像点坐标系统误差改正、初始值确定、旋转矩阵R计算、共线方程近似值求解与误差方程组建帮助读者理解如何由像点坐标与控制点地面坐标解算像片外方位元素。资源包共1个文件格式为docx文档整体大小约111KB正文后附有C源程序、运行截图及Matlab对照程序便于查看代码与实验细节。该资源已有466人学习下载从内容预览看报告还包含具体已知条件数据、分步计算公式和结果分析读者可直接借鉴其C代码框架与实验报告排版快速复现完整的单像空间后方交会计算流程。 做摄影测量和计算机视觉相关工作的朋友对单像空间后方交会这个名字一定不陌生。它是摄影测量里最基础、也最核心的一步已知相机内参拿一张照片再从照片上找到若干个坐标已知的地面控制点反推出拍照时相机的位置和姿态——也就是6个外方位元素。网上一搜这类实验大多是用Matlab写的公式一贴、几条脚本一跑结果就出来了。但真到了工程里想把算法嵌进C的数据处理流程或者和OpenCV的标定、特征匹配串在一起还是得老老实实动手写一套C实现。这篇文章就完整讲一遍我自己做单像空间后方交会实验C版的思路、代码和踩过的坑适合正在做课程实验的学生也适合想把理论算法落地成代码的工程师。1. 单像空间后方交会到底在解什么1.1 从共线方程说起先讲清楚问题的数学本质。理想情况下物方点、摄影中心、像点三者共线这就是共线方程collinearity equation[ x - x_0 -f\frac{a_1(X-X_s)b_1(Y-Y_s)c_1(Z-Z_s)}{a_3(X-X_s)b_3(Y-Y_s)c_3(Z-Z_s)} ][ y - y_0 -f\frac{a_2(X-X_s)b_2(Y-Y_s)c_2(Z-Z_s)}{a_3(X-X_s)b_3(Y-Y_s)c_3(Z-Z_s)} ]其中 (x, y) 是像点坐标(x_0, y_0, f) 是内方位元素主点坐标和主距(X, Y, Z) 是物方坐标(X_s, Y_s, Z_s) 是摄影中心坐标旋转矩阵 (R) 的9个元素 (a_1 \sim c_3) 由三个角元素 (\varphi, \omega, \kappa) 决定。所谓单像空间后方交会就是已知内方位元素 (x_0, y_0, f)利用一定数量的地面控制点及其对应像点坐标反求6个外方位元素(X_s, Y_s, Z_s, \varphi, \omega, \kappa)。这里单像是强调只用一张影像后方指的是从像方反推物方姿态区别于前方交会。每个控制点可以列出上面两个方程所以理论上至少需要3个控制点才能解出6个未知数。实际做实验时通常用4到6个点甚至更多多余观测会进入最小二乘平差这样能提高解算的稳健性也能顺便评定精度。1.2 为什么必须线性化迭代初学者最容易疑惑的一点是公式明明已经写出来了为什么不能直接解方程关键在于共线方程对三个角元素是非线性函数——旋转矩阵里全是三角函数直接把 (X_s, Y_s, Z_s, \varphi, \omega, \kappa) 作为未知数去解方程没法写成线性方程组的形式。工程上的标准做法是线性化先给一组外方位元素的初值把共线方程在初值处做泰勒展开舍弃二次以上项得到关于6个修正量 (\Delta X_s, \Delta Y_s, \Delta Z_s, \Delta\varphi, \Delta\omega, \Delta\kappa) 的线性误差方程[ v_x a_{11}\Delta X_s a_{12}\Delta Y_s a_{13}\Delta Z_s a_{14}\Delta\varphi a_{15}\Delta\omega a_{16}\Delta\kappa - l_x ][ v_y a_{21}\Delta X_s a_{22}\Delta Y_s a_{23}\Delta Z_s a_{24}\Delta\varphi a_{25}\Delta\omega a_{26}\Delta\kappa - l_y ]其中 (l_x x_{\text{测}} - x_{\text{计}})、(l_y y_{\text{测}} - y_{\text{计}})是观测像点坐标和当前外方位元素算出的像点坐标之差。然后用法方程解出修正量更新外方位元素再重复计算直到修正量足够小。整个过程就是线性化—解方程—更新—再线性化的迭代。这个流程想通了后面写代码就顺了无非是算残差、算系数、解法方程、更新参数、判收敛。2. C程序整体设计2.1 需求拆解与模块划分我在动手写代码之前先把整个程序按功能拆成了几个独立模块这样每个部分都能单独测试出问题也容易定位数据定义模块像点坐标、地面点坐标、外方位元素的结构体旋转矩阵与投影模块根据角元素计算旋转矩阵正算像点坐标误差方程系数生成模块计算每个控制点对应的1行6列系数线性方程组求解模块高斯消元法解6元法方程主迭代模块组装法方程、解算、更新、判断收敛精度评定模块计算单位权中误差、各未知数的中误差实际工程里可能还要加文件读写模块把控制点数据从文本或Excel读进来。课程实验阶段数据量不大直接在代码里初始化几个数组也行但建议还是养成读文件的好习惯后面数据多了不用反复改代码。2.2 数据结构与接口设计我用的数据结构非常简单三个结构体搞定struct Point2D { double x, y; // 像点坐标 }; struct Point3D { double X, Y, Z; // 地面控制点坐标 }; struct EO { double Xs, Ys, Zs; // 摄影中心坐标线元素 double phi, omega, kappa; // 姿态角角元素弧度制 };核心接口我设计成三个函数void buildRotation(double phi, double omega, double kappa, double R[9]); void projectPoint(const double R[9], const Point3D g, double f, double x0, double y0, double Xs, double Ys, double Zs, double xCalc, double yCalc); void computeRow(const EO eo, const Point3D g, double f, double x0, double y0, double rowX[6], double rowY[6]);这样每个函数职责单一buildRotation负责把三个角转成旋转矩阵projectPoint负责正算computeRow负责生成误差方程系数。主程序里就是一个循环对每个控制点调用computeRow把系数累加到法方程里。这种写法虽然朴素但可读性非常好出问题也容易断点调试。2.3 系数矩阵解析公式还是数值微分这是我在代码实现里比较坚持的一个取舍。教科书上会把系数 (a_{11} \sim a_{26}) 的解析公式列一大串每个系数都是关于旋转矩阵元素、像点坐标、地面坐标的复杂组合背下来或者抄下来都容易出错。我第一次手推这些偏导数时抄错了好几个符号导致迭代死活不收敛最后花了半天才定位到是系数公式的问题。所以后来我改用了一种工程上非常实用的做法数值微分具体就是中心差分法。比如要计算 (\partial x / \partial X_s)就对 (X_s) 加一个微小扰动 (h)分别计算加扰动前后的像点投影坐标用差分近似代替导数[ \frac{\partial x}{\partial X_s} \approx \frac{x(X_s h) - x(X_s - h)}{2h} ]理论上中心差分有二阶精度双精度浮点下扰动步长取线元素 (10^{-5})、角元素 (10^{-7}) 左右就够了。每个控制点有6个未知量中心差分需要额外算12次投影一次迭代就算有几十个控制点计算量也可以忽略。这个方案既避免了抄错解析公式又有足够的精度我强烈建议在课程实验里也这么干——推导过程写在报告里代码用数值微分做交叉验证两条路结果一致报告反而更有说服力。3. 关键代码实现3.1 旋转矩阵与投影正算我这里用的转角系统是 (\varphi-\omega-\kappa)这也是摄影测量教材里最常见的。旋转矩阵展开后是void buildRotation(double phi, double omega, double kappa, double R[9]) { double cf cos(phi), sf sin(phi); double cw cos(omega), sw sin(omega); double ck cos(kappa), sk sin(kappa); R[0] cf*ck - sf*sw*sk; R[1] -cf*sk - sf*sw*ck; R[2] -sf*cw; R[3] cw*sk; R[4] cw*ck; R[5] -sw; R[6] sf*ck cf*sw*sk; R[7] -sf*sk cf*sw*ck; R[8] cf*cw; }注意这里下标顺序(R[0]\sim R[2]) 是第一行 (a_1, a_2, a_3)(R[3]\sim R[5]) 是第二行 (b_1, b_2, b_3)(R[6]\sim R[8]) 是第三行 (c_1, c_2, c_3)。这个对应关系如果弄反后面共线方程计算就全乱了。有了旋转矩阵投影正算就很简单void projectPoint(const double R[9], const Point3D g, double f, double x0, double y0, double Xs, double Ys, double Zs, double xCalc, double yCalc) { double dX g.X - Xs; double dY g.Y - Ys; double dZ g.Z - Zs; double Xbar R[0]*dX R[1]*dY R[2]*dZ; double Ybar R[3]*dX R[4]*dY R[5]*dZ; double Zbar R[6]*dX R[7]*dY R[8]*dZ; xCalc x0 - f * Xbar / Zbar; yCalc y0 - f * Ybar / Zbar; }这里有个隐含的物理检查点理论上摄影中心到物方点的深度 (Zbar) 应该始终为负因为物方点在摄影中心前方视线方向与像平面法向相反如果算出来为正说明外方位元素初值离谱或者旋转矩阵角度错了这时候先停下来检查别急着往下迭代。3.2 数值微分生成误差方程系数computeRow就是核心的数值微分函数我贴出对 (X_s) 的求导作为示例其他5个参数完全同理void computeRow(const EO eo, const Point3D g, double f, double x0, double y0, double rowX[6], double rowY[6]) { double h 1e-5; // 线元素扰动步长和坐标单位一致 double hAng 1e-7; // 角元素扰动步长弧度 double R[9]; double xp, yp, xm, ym; EO ep, em; // 对 Xs 求偏导 ep eo; ep.Xs h; em eo; em.Xs - h; buildRotation(ep.phi, ep.omega, ep.kappa, R); projectPoint(R, g, f, x0, y0, ep.Xs, ep.Ys, ep.Zs, xp, yp); buildRotation(em.phi, em.omega, em.kappa, R); projectPoint(R, g, f, x0, y0, em.Xs, em.Ys, em.Zs, xm, ym); rowX[0] (xp - xm) / (2 * h); rowY[0] (yp - ym) / (2 * h); // 对 Ys、Zs、phi、omega、kappa 的偏导同理 // ... }符号约定一定要想清楚误差方程 (v A\delta - l) 里的系数 (A) 是像点计算值对未知数的偏导常数项 (l) 是观测值减计算值。所以这里差分求的是projectPoint计算值的偏导不是残差的偏导。如果你写代码时直接用残差去差分最后法方程的常数项和系数会差一个负号解出来的修正量方向相反迭代必发散。3.3 法方程组装与高斯消元最小二乘平差的法方程是 (A^T A \delta A^T l)其中 (A) 是 (2n \times 6) 的系数矩阵(l) 是 (2n) 维残差观测向量。我按照单位权来处理也就是权阵 (P I)double AtA[6][6] {0}; double AtL[6] {0}; for (int i 0; i n; i) { double rowX[6], rowY[6]; computeRow(eo, ground[i], f, x0, y0, rowX, rowY); double xCalc, yCalc; buildRotation(eo.phi, eo.omega, eo.kappa, R); projectPoint(R, ground[i], f, x0, y0, eo.Xs, eo.Ys, eo.Zs, xCalc, yCalc); double lx img[i].x - xCalc; double ly img[i].y - yCalc; for (int r 0; r 6; r) { AtL[r] rowX[r] * lx rowY[r] * ly; for (int c 0; c 6; c) { AtA[r][c] rowX[r] * rowX[c] rowY[r] * rowY[c]; } } }解这个 (6 \times 6) 的对称正定方程组用高斯消元就够了完全没必要上什么高端库。我通常写一个通用的gaussSolve带列主元交换防止对角线出现接近零的小数导致除零或精度崩坏bool gaussSolve(double A[6][6], double b[6], double x[6]) { double M[6][7]; for (int i 0; i 6; i) { for (int j 0; j 6; j) M[i][j] A[i][j]; M[i][6] b[i]; } for (int col 0; col 6; col) { int pivot col; for (int i col 1; i 6; i) if (fabs(M[i][col]) fabs(M[pivot][col])) pivot i; if (fabs(M[pivot][col]) 1e-12) return false; if (pivot ! col) swap(M[pivot], M[col]); for (int i col 1; i 6; i) { double factor M[i][col] / M[col][col]; for (int j col; j 7; j) M[i][j] - factor * M[col][j]; } } for (int i 5; i 0; --i) { double sum M[i][6]; for (int j i 1; j 6; j) sum - M[i][j] * x[j]; x[i] sum / M[i][i]; } return true; }主迭代逻辑我放在spaceResection函数里给初值进入循环法化之后解方程得到6个修正量更新外方位元素判断最大修正量是否小于阈值。角元素修正量单位是弧度一般阈值设 (10^{-6})线元素阈值设 (10^{-4}) 到 (10^{-6}) 都可以。迭代次数上限我习惯设30正常情况几次就收敛了超过20次还不收敛基本就是初值或代码有问题。4. 实验设计与结果分析4.1 先做一组自洽数据验证程序程序写完之后第一步建议不要直接上真实数据否则出了问题根本说不清是代码错还是数据错。我每次做这类算法实验都会先用模拟数据验证一遍方法很朴素给定一套已知的外方位元素和若干地面控制点坐标用共线方程正算出像点坐标再加一点微小的高斯噪声模拟观测误差然后把这组数据交给后方交会程序去反算。比如设计一组这样的真值// 设计真值 EO truth; truth.Xs 5020.0; truth.Ys 4980.0; truth.Zs 1500.0; truth.phi 0.1745; // 约10度 truth.omega -0.0873; // 约-5度 truth.kappa 0.5236; // 约30度控制点选5到6个分布在一个大约200米乘150米的范围内高程略有起伏。用共线方程正算出像点坐标后加上均值为0、标准差2微米左右的随机噪声作为观测值。如果程序正确反算出的外方位元素应该和真值非常接近偏差在噪声水平对应的精度范围内。我第一次跑通这个验证流程时解算出的 (X_s, Y_s, Z_s) 与真值差了不到0.1米角度差了不到0.01度那个感觉是相当踏实的——说明从旋转矩阵到误差方程再到高斯消元整条链路都是对的。这一步验证非常有价值哪怕你现在手上有真实的实验数据我也建议先走一遍模拟验证把程序正确性确认好再上真实数据。4.2 迭代收敛过程与精度评定程序正确之后可以打印出每轮迭代的修正量和残差变化观察收敛过程。我自己的经验是在初值合理的情况下后续会详细说初始化方法第一次迭代的修正量最大尤其是角元素可能还有 (10^{-2}) 弧度量级第二次就降到 (10^{-3})、(10^{-4}) 量级到第五六次迭代基本就稳定在 (10^{-7}) 以下了。完成迭代后精度评定是实验报告里不能缺的部分。计算单位权中误差的公式是[ \sigma_0 \sqrt{\frac{\sum (v_x^2 v_y^2)}{2n - 6}} ]其中 (n) 是控制点个数分母 (2n-6) 是自由度。如果模拟数据加的噪声标准差是2微米那么 (\sigma_0) 应该在2微米左右这能反过来验证平差模型正确。进一步每个外方位元素的中误差可以通过法方程系数矩阵的逆对角线元素 (Q_{ii}) 来计算(\sigma_i \sigma_0 \sqrt{Q_{ii}})。这一块对应实验报告里的精度分析部分写好之后整个实验的完整度会高一个档次。5. 踩坑实录与常见问题排查5.1 初值不合适导致不收敛后方交会迭代能不能收敛初值影响极大尤其是角元素。我见过太多新手上来把 (X_s, Y_s, Z_s, \varphi, \omega, \kappa) 全设成0结果迭代直接放飞自我。我常用的初始化策略很实用(X_s, Y_s) 初值取所有控制点地面坐标的重心因为摄影中心一般不会离测区中心太远(Z_s) 初值取控制点平均高程加上一个预估航高比如航拍场景按比例尺和焦距估算地面拍摄场景估计一个到目标的概略距离三个角元素初值如果完全没概念就都设0如果知道大概姿态比如无人机下视拍摄(\varphi, \omega) 接近0、(\kappa) 接近航向角就给个近似值按这套方式初始化绝大多数情况下五六次迭代就收敛了。如果迭代过程中某一步修正量突然变得非常大或者解算出的角元素超过90度直接判定发散先回去检查初值和系数符号别硬调迭代次数。5.2 单位混用让法方程病态这个坑非常隐蔽。我最初做实验时像点坐标用的是毫米地面坐标用的是米主距又是毫米三个量纲混在一起。虽然表面看公式能算但法方程里不同未知数的尺度差异巨大导致数值稳定性极差高斯消元甚至可能出现近奇异。解决的办法是统一单位最省事的方案是把像点坐标和主距都换算成米也就是毫米值除以1000和地面坐标保持一致或者反过来把地面坐标也换算成毫米虽然数值会很大但至少量纲一致。我更推荐全部换成米制这样和真实物理尺寸直接对应后面误差分析也更直观。如果某些同学确实想保留毫米那至少要意识到数值尺度问题必要时对观测方程做归一化处理。5.3 控制点分布不合理控制点的数量和分布决定了法方程的可解性和解算精度。最少3个控制点是理论下界但3个点往往导致法方程系数矩阵条件数很差解算结果对像点误差非常敏感。我做实验一般至少用4个点并且尽量让控制点均匀分布在像幅的四角而不是挤在一起。如果控制点近似共线法方程会退化某个方向的修正量约束不住迭代要么不收敛要么收敛到一个看起来很合理但实际完全错误的解。这和地方控制网测量里的图形强度是一个道理——三角形越接近正三角形点位精度越好控制点越分散后方交会的外方位元素精度越高尤其对角元素的影响非常显著。5.4 细节问题速查表下面这张表是我在实际调试中沉淀下来的遇到问题基本可以按图索骥现象可能原因解决思路迭代发散修正量越来越大初值太差或系数符号错误按重心法赋初值用模拟数据逐项校验数值微分系数符号一次迭代后结果仍然偏差很大角度单位混用弧度制没统一检查角元素是否用了弧度角度转弧度公式是否正确法方程奇异高斯消元返回失败控制点数量不足或分布近似共线增加控制点四角均匀布点解算结果与真值差异在合理范围外单位不统一导致法方程病态像点、地面坐标全部统一成米制正算投影时 (Zbar) 出现正数初值或旋转矩阵角度异常检查旋转矩阵公式与转角系统是否匹配同一份数据换了编译器结果有细微差异浮点累积误差属正常现象偏差在 (10^{-6}) 量级即可忽略最后多说一句如果你的项目里已经集成了OpenCV完全可以用cv::solvePnP对同一份数据做一次交叉验证两个结果互相印证比单独信一套实现要可靠得多。我自己把这套代码融入到无人机影像批处理流程后又在上面加了对每张影像独立初始化初值和迭代收敛的自动判据但核心仍然是最基本的共线方程和6个修正量的最小二乘迭代。把这套基础扎实了后面再上多片前方交会、光束法平差都是同一套思路的延伸。本文还有配套的精品资源点击获取
返回列表