搞视觉定位的朋友,估计都绕不开一个问题:拿到一张图,怎么算出相机相对于某个参考点的位姿。你可能会看到“旋转矩阵R”“平移向量t”这些词,翻资料翻到头大,最后在代码里调一个函数就出来一堆数值,也不知道对不对、稳不稳。
这篇文章就把“相机相对D点的位姿”这件事彻底讲透。它是什么、背后的数学模型怎么搭、代码里怎么调用、结果怎么验证、踩了哪些坑。内容按照一个典型应用场景来组织:我们用二维码(比如ArUco)贴在D点附近,把二维码中心定义为世界坐标系原点,通过识别二维码角点,就能求出相机相对这个D点的旋转矩阵R和平移向量t。适合正在做视觉定位、无人机定点降落、机器人抓取、AR叠加导航的开发者参考。我用模拟项目X的完整过程来演示,所有步骤都能直接抄作业。
1. 先搞清楚要算什么:相机相对D点的位姿到底是什么
1.1 “D点”与坐标系:先约定参考系再谈位姿
位姿这个词听起来玄乎,说白了就是两个问题:相机往哪边看(姿态),相机在哪(位置)。但这两个问题必须放在某个参考系里才有意义。所谓“相机相对D点的位姿”,就是把D点当作一个基准点,以它为原点建立一个世界坐标系,然后描述相机坐标系在这个世界坐标系中的朝向和位置。
我在模拟项目X里的做法很直观:把一张ArUco二维码平贴在D点,二维码中心就是世界坐标系原点O,二维码平面上的两个边分别作为世界坐标系的X轴和Y轴,垂直二维码平面向上的方向作为Z轴。这样,世界坐标系就是“D点坐标系”,相机相对D点的位姿,就等于相机坐标系相对这个世界坐标系的刚体变换。
这个约定很重要,因为后边所有计算都依赖坐标系的定义。如果你把世界坐标系原点放在D点,但Z轴方向定义成向下,那求出来的t符号就会完全反过来。很多人在这一步栽跟头,不是算法错了,是坐标系的“约定”没统一。
在这个约定下,我们需要求解的相机位姿就分成两部分:
- 旋转矩阵R:描述相机坐标系相对D点世界坐标系的姿态旋转关系。它是一个3×3矩阵。
- 平移向量t:描述D点世界坐标系原点在相机坐标系下的位置。它是一个3×1向量。
经常有人把t理解成相机的空间坐标,这是不对的。solvePnP返回的t,指的是世界坐标系的点在相机坐标系下的坐标。相机在世界坐标系中的位置,需要用 (-R^T t) 才能算出来。这个细节后面我会专门演示。
1.2 为什么用旋转矩阵R而不是欧拉角
既然旋转只有3个自由度,为什么代码里经常给一个9个数的3×3矩阵?原因有两个。
第一,旋转矩阵是一个正交矩阵,满足 (R^T R = I),并且行列式等于1。这两个性质保证了R描述的确实是纯旋转,不会夹带缩放或镜像。初学时可以把旋转矩阵理解成“一个不会变形、只负责转方向的变换”,就像拧瓶盖,瓶盖本身没变形,只是方向变了。
第二,在PnP求解过程中,旋转矩阵可以直接参与矩阵运算,和三维点坐标做乘法非常方便。欧拉角虽然直观,但有万向锁问题,而且在优化迭代时数值稳定性不如旋转矩阵。四元数虽然紧凑无万向锁,但是对新手来说理解门槛更高。
所以在工程中,我一般用旋转矩阵作为中间计算格式,需要给人看结果时再转成欧拉角,需要插值时再转成四元数。理解R的基本性质就够了:它是一个3×3的正交矩阵,行列式为1,每一列代表相机坐标系的轴在世界坐标系中的方向。
2. 求解之前的数学地基:从3D点到2D像素的完整映射
2.1 针孔成像模型:世界坐标是怎么变成像素坐标的
要算出相机位姿,先得知道三维世界里的点是怎么投影到图像上的。这里用最简单的针孔相机模型,整个过程可以拆成三步。
第一步,世界坐标转相机坐标。设D点坐标系下某个三维点的坐标为 (P_w),相机坐标系下同一个点的坐标为 (P_c)。刚体变换写成:
[ P_c = R P_w + t ]
R和t就是我们要解的位姿。这个公式里,R是3×3旋转矩阵,t是3×1平移向量。
第二步,相机坐标转归一化平面坐标。把 (P_c = (X_c, Y_c, Z_c)) 的三个分量同时除以 (Z_c),得到 ((x_n, y_n) = (X_c / Z_c, Y_c / Z_c))。这一步是把三维点“压”到相机前方一米处的虚拟成像平面上,也是针孔模型最核心的透视投影过程。
第三步,归一化坐标转像素坐标。内参矩阵K负责这一步:
[ \begin{bmatrix} u \ v \ 1 \end{bmatrix} = K \begin{bmatrix} x_n \ y_n \ 1 \end{bmatrix} ]
内参矩阵K由焦距 (f_x, f_y) 和主点 (c_x, c_y) 组成:
[ K = \begin{bmatrix} f_x & 0 & c_x \ 0 & f_y & c_y \ 0 & 0 & 1 \end{bmatrix} ]
整个映射可以概括为一句话:世界坐标系下的3D点,先经过外参R、t变换到相机坐标系,再通过内参K投影到图像平面。求解相机位姿,本质上就是已知一组3D点坐标和它们对应的2D像素坐标,反求出中间那层外参R、t。
2.2 PnP问题:需要几个对应点才能求出位姿
我们把上述过程倒过来看。已知一个3D点在世界坐标系下的坐标 (P_w),也知道它投影到图像上的像素坐标 ((u, v)),那么在R和t未知的情况下,能列出几个方程?
每个3D-2D对应点可以列出两个约束方程(u和v各一个)。而R有9个元素但只有3个自由度,t有3个自由度,总共6个未知数。理论上3组对应点提供6个方程,刚好足够求解。
不过实际中3点求解(也就是P3P问题)会得到多个解。原因是三点约束给出的方程组是非线性的,可能有4个甚至更多的候选解。所以需要第4个点来筛选。这也是为什么OpenCV的P3P解法要传4组对应点。工程上更不会只用3组点,一般至少用10组以上,配合最小二乘或随机采样一致性算法来提升稳定性。
在模拟项目X里,我用的ArUco二维码有4个角点。4组点能做P3P解算,但点数太少,抗噪声能力差。所以我通常会把二维码角点、周围的特征点混在一起,凑到20组以上的3D-2D对应关系,再交给PnP求解。
2.3 PnP求解算法怎么选:从DLT到EPnP再到迭代优化
PnP问题发展出很多种解法,对应的就是OpenCV solvePnP里的flags参数。这块如果选错,结果可能千差万别,我逐个说下适用场景。
DLT(直接线性变换)是最朴素的思路。把R和t组合成一个3×4矩阵,用6组以上的对应点直接建立线性方程组求解。优点是不需初值,速度极快;缺点是没有考虑旋转矩阵的正交约束,解出来的矩阵未必是真正的旋转矩阵,精度也一般。适合用来做初值。
P3P解法只用4组点,在几何上非常优雅,但点少了抗噪能力差。图像上1个像素的误差,可能导致位姿偏出好几度。工程上不建议单独使用。
EPnP是当前最常用的选择。它的核心思想是用4个控制点表示所有参考点,将问题转化为估计控制点在相机坐标系下的坐标,计算复杂度O(n),点数越多优势越明显。实测下来,EPnP在点数足够时精度和速度都很均衡。
迭代法(ITERATIVE)则是用Levenberg-Marquardt算法最小化重投影误差,精度最高,但需要靠谱的初始值,否则容易陷入局部极小值。
我把几种方法的对比总结成一张表,方便实际选型时参考:
| 方法 | 所需点数 | 精度 | 速度 | 适用场景 |
|---|---|---|---|---|
| DLT | ≥6 | 中 | 快 | 用作初值 |
| P3P | 4 | 中低 | 快 | 几何求解,抗噪弱 |
| EPnP | ≥4 | 较高 | 快 | 点较多时的首选 |
| 迭代法 | ≥4 | 高 | 慢 | 有初值时精修 |
我个人在实际项目中固定一套组合拳:先用solvePnPRansac配合EPnP做一次粗解,把误匹配点剔掉,然后把R、t作为初值,再用迭代法对全部内点做一次精优化。这套流程兼顾了鲁棒性和精度,几乎适用于所有视觉定位场景。
3. 实操落地:从检测二维码到输出相机位姿
3.1 完整流程概览
模拟项目X的完整流程可以分成五个环节:
- 相机内参标定,拿到K和畸变系数。
- 检测ArUco二维码,提取角点的像素坐标。
- 根据二维码尺寸,计算角点在世界坐标系下的3D坐标。
- 调用solvePnP解算R和t。
- 验证结果:计算重投影误差,反推相机位置。
下面我按实际操作的顺序把每个环节的细节过一遍。
3.2 准备3D点坐标和2D像素坐标
ArUco二维码的最大好处是:它的角点在世界坐标系下的坐标可以精确给出。设二维码边长为L米,世界坐标系原点在二维码中心,四个角点的3D坐标分别是:
import numpy as np # 假设二维码边长0.16m,世界系原点在二维码中心 L = 0.16 half = L / 2.0 # 角点顺序:左上、右上、右下、左下 object_points = np.array([ [-half, -half, 0.0], # 左上 [ half, -half, 0.0], # 右上 [ half, half, 0.0], # 右下 [-half, half, 0.0], # 左下 ], dtype=np.float64)这里Z坐标全部是0,因为二维码平面就是世界坐标系的XOY平面。这样设置是最自然的一种做法,也方便后续在二维码周围额外放一些已知3D位置的点,和角点一起送入PnP。
2D像素坐标可以通过OpenCV的aruco模块提取。注意提取角点时要用cornerSubPix做亚像素细化,这一步能显著提升位姿精度。粗糙的角点坐标会直接转化为位姿误差,尤其是在相机距离二维码较远时。
import cv2 # 读取图像并检测ArUco gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50) parameters = cv2.aruco.DetectorParameters() detector = cv2.aruco.ArucoDetector(aruco_dict, parameters) corners, ids, rejected = detector.detectMarkers(gray) # 取第一个二维码的四个角点 if ids is not None: pts_2d = corners[0].reshape(4, 2).astype(np.float64)如果检测到的二维码不是一个而是多个,可以在所有码的角点之间建立一个本地坐标系,统一转换到一个基准D点坐标系。这个扩展后面讲,先把单码的情况跑通。
3.3 调用solvePnP并解析结果
有了3D点和2D点,就可以调用solvePnP。我建议按下面的方式来做双阶段求解:
K = np.array([ [800.0, 0.0, 320.0], [0.0, 800.0, 240.0], [0.0, 0.0, 1.0], ], dtype=np.float64) dist_coeffs = np.zeros((4, 1)) # 假设已标定,这里填实际畸变系数 # 第一阶段:EPnP求初值 success, rvec, tvec = cv2.solvePnP( object_points, pts_2d, K, dist_coeffs, flags=cv2.SOLVEPNP_EPNP ) # 第二阶段:用初值做迭代精化 success, rvec, tvec = cv2.solvePnP( object_points, pts_2d, K, dist_coeffs, rvec, tvec, useExtrinsicGuess=True, flags=cv2.SOLVEPNP_ITERATIVE ) # 旋转向量转旋转矩阵 R, _ = cv2.Rodrigues(rvec)这里必须说明一个高频误区:solvePnP返回的rvec是旋转向量,不是旋转矩阵。旋转向量用Rodrigues公式转为3×3矩阵,如果你直接把rvec当成矩阵用,后面所有计算都会错。
tvec是D点世界坐标系原点在相机坐标系下的坐标。这句描述实践性很强。在上面这组代码里,SolvePnP返回的t表示的是“二维码中心D点在相机坐标系下的位置”,而不是“相机在世界系中的位置”。
想求相机在世界坐标系下的位置,需要做一个反向变换:
# 相机原点在世界坐标系下的坐标 camera_pos_world = -R.T @ tvec.reshape(3, 1)为什么是 (-R^T t)?因为根据公式 (P_c = R P_w + t),把相机原点 (P_c = 0) 代进去,得到 (0 = R P_w + t),所以 (P_w = -R^T t)。
3.4 用模拟数据验证整个计算链路
为了让上面的流程可以完整跑通,我准备了一个模拟数据的小实验。设定相机内参 (f_x = f_y = 800),主点 ((320, 240)),无畸变,相机在D点前方1米处正对D点。也就是说真实位姿是 (R = I),(t = [0, 0, 1]^T),世界坐标系的三个点分别是D点 ((0,0,0))、X轴方向0.1m处的点 ((0.1,0,0))、Y轴方向0.1m处的点 ((0,0.1,0))。
它们投影到图像上,理论上应该出现在图像中心附近。然后我用这3组对应点调用solvePnP,看解出来是否接近真实值。
# 模拟3D点 world_pts = np.array([ [0.0, 0.0, 0.0], [0.1, 0.0, 0.0], [0.0, 0.1, 0.0], ], dtype=np.float64) # 模拟投影:Pc = R * Pw + t,R=I,t=[0,0,1] R_true = np.eye(3, dtype=np.float64) t_true = np.array([[0.0], [0.0], [1.0]], dtype=np.float64) Pc = (R_true @ world_pts.T) + t_true # 3xN # 归一化并投影 x_n = Pc[0, :] / Pc[2, :] y_n = Pc[1, :] / Pc[2, :] pixel_pts = np.stack([ K[0, 0] * x_n + K[0, 2], K[1, 1] * y_n + K[1, 2], ], axis=1).astype(np.float64) # 求解 s, rvec, tvec = cv2.solvePnP(world_pts, pixel_pts, K, None, flags=cv2.SOLVEPNP_EPNP) R_est, _ = cv2.Rodrigues(rvec) print("R_est:\n", R_est) print("t_est:", tvec.reshape(-1))在这个模拟中,求解结果应该非常接近单位阵和 ([0, 0, 1])。如果你跑出来是这个结果,就说明你的坐标定义、数据通道、调用方式全链路都是通的。如果跑出来偏差巨大,大概率不是算法问题,而是3D点与2D点的对应顺序发生了错位。
4. 稳定性升级:误匹配剔除与迭代精化
4.1 现实场景中的噪声与误匹配
模拟数据里一切完美,但真实场景中,图像会有噪点,特征点检测会有误差,甚至可能出现错误的3D-2D对应关系。比如二维码某个角点被遮挡后,程序可能拿其他角点的误检结果硬凑。这种情况下直接用EPnP求解,一个离群点就可能把位姿带偏好几度。
所以在工程实现里,我会用solvePnPRansac替代直接调用solvePnP。RANSAC的思路是随机抽取一小撮对应点求解位姿,然后用这个位姿统计有多少点满足重投影误差阈值,重复多次后保留内点最多的那个解。
success, rvec, tvec, inliers = cv2.solvePnPRansac( object_points, pts_2d, K, dist_coeffs, reprojectionError=2.0, # 重投影误差阈值,单位像素 confidence=0.99, # 置信度 iterationsCount=1000, # 迭代次数 flags=cv2.SOLVEPNP_EPNP )这里的reprojectionError是核心参数。设小了会误杀正常匹配,设大了又挡不住误匹配。我在实际项目里的经验是:图像分辨率在640×480左右时设2.0比较稳妥;如果图像分辨率在1920×1080,可以适当放宽到3.0~4.0。这两个值都需要根据自己的标定质量和特征点提取精度来微调。
4.2 用迭代法做最终精修
RANSAC得到的R、t是“基于内点集合的粗解”,精度还能再上一个台阶。把RANSAC返回的rvec和tvec作为初值,再用ITERATIVE法在全部内点上做一次最小化重投影误差的精优化:
# 提取内点 if inliers is not None: obj_inliers = object_points[inliers.reshape(-1)] img_inliers = pts_2d[inliers.reshape(-1)] else: obj_inliers = object_points img_inliers = pts_2d # 精优化 success, rvec, tvec = cv2.solvePnP( obj_inliers, img_inliers, K, dist_coeffs, rvec, tvec, useExtrinsicGuess=True, flags=cv2.SOLVEPNP_ITERATIVE )为什么EPnP给了初值之后还要再迭代一遍?因为EPnP本身并不是以“最小化重投影误差”为目标,它是一个近似解析方法。而迭代法直接优化几何误差,精度上限更高。两步结合起来,既避免迭代法陷入局部极小值,又能拿到更准的最终结果。
4.3 重投影误差:判断结果好不好的量化指标
算法算完,怎么知道结果靠不靠谱?看重投影误差。做法是把解出来的R、t代回投影公式,算出每个3D点对应的像素坐标,然后和检测到的像素坐标比较:
def compute_reprojection_error(obj_pts, img_pts, rvec, tvec, K, dist): proj_pts, _ = cv2.projectPoints(obj_pts, rvec, tvec, K, dist) error = np.linalg.norm(proj_pts.reshape(-1, 2) - img_pts, axis=1) return error.mean(), error mean_err, per_point_err = compute_reprojection_error( obj_inliers, img_inliers, rvec, tvec, K, dist_coeffs ) print("mean reprojection error:", mean_err)正常情况下,平均重投影误差应该小于0.5像素。如果到了1像素以上,位姿可信度就会明显下降。我自己判断项目是否稳定的标准是:静态场景下重投影误差跑满30帧,均值稳定在0.3像素以内,才敢拿去给机器人做伺服控制。
5. 常见问题与排查笔记
5.1 坐标对应关系错乱导致的“幽灵位姿”
最典型的问题:3D点顺序和2D点顺序没对齐。ArUco检测出的角点顺序是固定的(左上、右上、右下、左下),如果你在构造object_points时手一抖把顺序写成左上、左下、右下、右上,那么一切数学都是白搭,算法会给出一个看似合理但完全错误的位姿。
排查方法也很简单:写一个调试函数,把检测到的2D角点和由求解结果投影回去的2D角点画在同一张图上,如果对应点之间没有形成整齐的连线,说明顺序或求解有问题。这个可视化手段比看数字有效得多。
5.2 畸变没校正导致的边缘误差
相机内参不是只包含一个K矩阵,畸变系数同样重要。很多入门者以为标定完内参就万事大吉,忽略了画面边缘的桶形畸变。D点距离光轴越远,畸变对像素坐标的影响越大。求解时传入dist_coeffs能自动补偿,但前提是你的标定数据足够准。
更进一步的做法是在提取特征点之前,把整张图像去畸变:
undistorted = cv2.undistort(image, K, dist_coeffs)不过整图去畸变有代价:会裁剪边缘视野,增加计算耗时。如果相机视场角不大、畸变影响轻微,直接在solvePnP里传dist_coeffs就够了。我一般只在视场角大于60度时才做整图去畸变。
5.3 平面目标带来的退化问题
ArUco二维码本身是一个平面目标,世界坐标系的Z值全部为0。平面目标在PnP求解中会遇到退化问题,尤其是当相机正对着二维码时,深度方向上的误差会比较明显。这不是算法bug,而是几何本身提供的约束不足。
缓解退化问题的几个手段:
- 不要只用单个二维码,尝试同时检测多个不同位置的二维码,扩展3D点的空间分布。
- 在二维码周围放置已知3D位置的标定点,让3D点不共面。
- 如果只能用单平面目标,尽量保证相机视角不要过于垂直于平面,稍微倾斜一点反而能提供更多约束。
在模拟项目X里,我用了三张ArUco码组成一个“品字形”布局,各自设置不同的高度和朝向,这样3D点的深度范围大了不少,Z轴方向的定位稳定性明显改善。
5.4 内参矩阵与实际图像分辨率不匹配
如果标定内参时用的是1280×720的图像,实际运行输入却是640×480,那主点和焦距全都会对不上,结果自然崩。这种情况下重投影误差会特别大,而且呈现系统性偏置。
解决办法很简单:标定内参的运行分辨率和实际使用分辨率必须一致。如果不得不改变分辨率,注意内参里的 (f_x, f_y, c_x, c_y) 都要按比例缩放,不能只缩放 (c_x, c_y)。
5.5 为什么相机位置算出来是负的
有人会问,明明无人机在D点上方,为什么用solvePnP返回的t算相机位置,Z坐标是负的?这里就是1.1和3.3里反复强调的定义问题。t表示D点世界原点在相机坐标系下的位置,对无人机场景来说,D点位于相机下方,在相机坐标系中Z坐标是负值,所以t的Z分量是负的。而要求相机在世界坐标系中的位置(也就是D点上方的位置),必须用 (-R^T t),转出来才是正的。
这个点值得反复自查:每次用solvePnP求解后,先明确自己要的是“目标点相对相机的位置”,还是“相机相对目标点的位置”,两个答案之间差一个 (-R^T t) 的变换。
5.6 输入数据类型要谨慎
OpenCV的solvePnP对输入类型有要求,3D点和2D点都必须是float64。如果你传入的是float32数组,某些版本下会静默出错或精度降低。我习惯在构造数据时统一加一句:
object_points = object_points.astype(np.float64) pts_2d = pts_2d.astype(np.float64)这种小问题不容易发现,但在对比精度时差之毫厘谬以千里。
6. 一套可以直接抄的完整流程
我把模拟项目X的一个最小可运行流程整理在下面,代码里没有复杂的工程依赖,复制下来改改内参和图像路径就能跑。
import cv2 import numpy as np # 1. 内参(自己标定后替换) K = np.array([ [800.0, 0.0, 320.0], [0.0, 800.0, 240.0], [0.0, 0.0, 1.0], ], dtype=np.float64) dist_coeffs = np.zeros((4, 1), dtype=np.float64) # 2. 读取图像、检测ArUco image = cv2.imread("aruco_sample.jpg") gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50) parameters = cv2.aruco.DetectorParameters() detector = cv2.aruco.ArucoDetector(aruco_dict, parameters) corners, ids, _ = detector.detectMarkers(gray) if ids is None: raise RuntimeError("no aruco detected") # 3. 构造3D-2D对应点 marker_length = 0.16 half = marker_length / 2.0 obj_pts_all = [] img_pts_all = [] # 这里支持多二维码:每个码的四个角点都加入 for i, corner in enumerate(corners): # 角点顺序: 左上 右上 右下 左下 obj = np.array([ [-half, -half, 0.0], [ half, -half, 0.0], [ half, half, 0.0], [-half, half, 0.0], ], dtype=np.float64) obj_pts_all.append(obj) img_pts_all.append(corner.reshape(4, 2).astype(np.float64)) object_points = np.vstack(obj_pts_all) image_points = np.vstack(img_pts_all) # 4. RANSAC粗解 + 迭代精解 success, rvec, tvec, inliers = cv2.solvePnPRansac( object_points, image_points, K, dist_coeffs, reprojectionError=2.0, confidence=0.99, iterationsCount=1000, flags=cv2.SOLVEPNP_EPNP ) if success: if inliers is not None: object_points_in = object_points[inliers.reshape(-1)] image_points_in = image_points[inliers.reshape(-1)] else: object_points_in = object_points image_points_in = image_points success, rvec, tvec = cv2.solvePnP( object_points_in, image_points_in, K, dist_coeffs, rvec, tvec, useExtrinsicGuess=True, flags=cv2.SOLVEPNP_ITERATIVE ) # 5. 转矩阵、求相机位置 R, _ = cv2.Rodrigues(rvec) camera_pos_world = -R.T @ tvec.reshape(3, 1) print("R:\n", R) print("t:", tvec.reshape(-1)) print("camera pos in D-world frame:", camera_pos_world.reshape(-1)) # 6. 重投影误差 proj_pts, _ = cv2.projectPoints(object_points_in, rvec, tvec, K, dist_coeffs) errors = np.linalg.norm(proj_pts.reshape(-1, 2) - image_points_in, axis=1) print("mean reproj error:", errors.mean())这套代码跑完后,你会得到三个关键结果:R、t、相机在世界系中的位置。对着图像再叠加画出检测角点和投影角点的连线,确认误差在可接受范围内,整个流程就算闭环了。
我个人在实际操作中的一个体会是:位姿求解本身并不难,难的是让输入数据的质量配得上算法的能力。同样一套solvePnP代码,有人跑出来精度0.1度,有人跑出来飘到天上去,差别基本都在标定精度、特征点提取精度和对应关系是否准确这三个环节上。所以每次定位不准,我第一反应都是回去查内参标定和角点提取,而不是怀疑PnP算法本身。
这套方案后续还有很大的扩展空间。当前做的是静态图像的位姿求解,如果把它接到视频流上,配合时间序列的滤波,比如卡尔曼滤波或者位姿图优化,就能输出更稳定的动态位姿。再进一步,如果有多台相机同时观测D点,还可以做全局优化,把每一帧的位姿估计提升到厘米级甚至毫米级。对大多数项目来说,先把单帧的R和t搞扎实,后边所有进阶玩法才站得住脚。