已知一个标记或物体上的三维点,又在图像里找到了对应像素,怎样求它相对相机的位置?这是 PnP 的典型输入。最常见的误用不是完全不会调用 solvePnP,而是把结果方向、尺度和误差口径理解错:tvec 通常不是相机在世界坐标中的位置,像素残差也不是毫米精度。
**阅读范围:**本文是三维视觉与医学影像软件工程系列的独立技术教程。数据和数值实验均为合成示例,不读取患者数据,不代表实际设备性能或临床精度。文中长度按示例明确的单位解释。资料核对日期:2026 年 10 月 16 日。

图 1:PnP 的输出:物体坐标 → 相机坐标(原创教学配图)
一、PnP 需要哪些输入?
每个物体点 X 都应有正确对应的像素 u,另外需要相机内参 K 和与图像处理状态一致的畸变参数。物体坐标用毫米,输出平移也按毫米解释;若模型尺寸写错一倍,单目系统没有额外信息自动纠正这个尺度。
内参描述成像模型,不能混入物体到相机的外参。图像缩放、裁剪或去畸变后,需要同步调整 K 或采用相应的新相机矩阵。已去畸变像素若再次使用原畸变参数,相当于重复补偿;归一化坐标与原始像素也不能混用。
二、rvec 与 tvec 的方向
OpenCV 的惯例是将物体坐标映射到相机坐标:X_c=R X_o+t。常用相机轴是 x 向右、y 向下、z 向前。rvec 是 Rodrigues 旋转向量,不是三个按任意顺序旋转的欧拉角;向量长度是旋转角,方向是旋转轴。
相机中心在物体坐标中为 C_o=−Rᵀt。逆变换是 Rᵀ,−Rᵀt,不能只给 t 加一个负号。检查方向的简单办法是把已知物体点送入相机系,确认深度符号,再通过 projectPoints 回到图像,而不是凭三维显示朝向猜。
三、先 RANSAC,再做内点精修
错误角点或错误关联会破坏普通最小二乘。solvePnPRansac 用候选模型筛选像素一致集,返回内点索引;随后只在这些内点上使用 solvePnPRefineLM 细化。本例使用非共面随机点和 EPNP 初始化,避免把平面专用条件混在基础实验中。
RANSAC 阈值是像素,受到角点噪声与图像分辨率影响。提高置信度不能替代点序检查。内点集中于图像中央或一个很小区域时,姿态约束可能仍弱,尤其在物体离相机远、可见尺寸小的情况下。
四、平面目标与多解不能忽略
平面标记在某些视角下可能出现姿态歧义。需要根据求解器条件考虑多个候选,结合正深度、历史位姿、重投影和额外结构判断。IPPE_SQUARE 对四点顺序有专门要求,不能把任意顺序的四个角直接传进去。
低重投影残差说明模型能解释当前像素,不证明内参、物体尺寸或外部坐标链都正确。独立三维检查点、不同距离与角度的数据,能揭示只在训练图像上看不出的系统偏差。
五、可运行的完整合成实验
生成 60 个非共面物体点,真值平移为 (15,−8,550) mm,投影后加入 0.2 px 标准差噪声,将前 12 个像素替换为随机错误点。RANSAC 后做 LM 精修,并用另外 20 个物体点比较估计和真实相机坐标。
环境说明:代码在本机 Python 3.14 环境中实际执行。为便于复现,请保存为 demo.py,在支持这些库的独立环境中安装依赖;若目标解释器没有兼容 wheel,应先选择受支持的解释器。环境差异可能影响最后几位数值,不影响本文所检查的几何或统计关系。
python -m pip install numpy opencv-python
python demo.py
import numpy as np
import cv2
rng=np.random.default_rng(16)
obj=rng.uniform([-40,-30,-15],[40,30,15],(60,3)).astype(np.float64)
K=np.array([[900.,0,640],[0,910.,360],[0,0,1.]])
dist=np.zeros(5)
rv=np.array([.12,-.2,.08]);tv=np.array([15.,-8.,550.])
pixels,_=cv2.projectPoints(obj,rv,tv,K,dist)
observed=pixels.reshape(-1,2)+rng.normal(0,.2,(60,2))
observed[:12]=rng.uniform([50,50],[1200,650],(12,2))
cv2.setRNGSeed(16)
ok,r,t,ids=cv2.solvePnPRansac(obj,observed,K,dist,
iterationsCount=500,reprojectionError=2.,confidence=.999,
flags=cv2.SOLVEPNP_EPNP)
assert ok and ids is not None and len(ids)>40
idx=ids.ravel()
r,t=cv2.solvePnPRefineLM(obj[idx],observed[idx],K,dist,r,t)
R,_=cv2.Rodrigues(r)
camera_center=-R.T@t.reshape(3)
proj,_=cv2.projectPoints(obj[idx],r,t,K,dist)
rms=np.sqrt(np.mean(np.sum((proj.reshape(-1,2)-observed[idx])**2,axis=1)))
hold=rng.uniform(-20,20,(20,3))
R0,_=cv2.Rodrigues(rv)
error=np.linalg.norm(hold@R.T+t.reshape(3)-(hold@R0.T+tv),axis=1)
assert rms<1 and np.sqrt(np.mean(error**2))<1
assert ((obj@R.T+t.reshape(3))[:,2]>0).all()
print('opencv',cv2.__version__,'inliers',len(idx),'reprojection_RMS_px',rms)
print('holdout_camera_position_RMS_mm',np.sqrt(np.mean(error**2)))
print('camera_center_in_object_frame_mm',camera_center)
print('PASS: PnP outlier recovery, cheirality and inverse direction')
六、运行结果与应该怎样解读
实际执行环境 cv2.version 为 5.0.0,使用了与所列官方文档一致的经典接口。选出 48 个内点,重投影 RMS 约 0.24749 px,独立点的相机坐标 RMS 约 0.49477 mm。检查了正深度与求解返回状态;未宣称所有版本或求解器都产生相同末位数。

图 2:物体投影与错配像素示意(原创教学配图)
代码中的断言验证了本篇明确描述的性质。配图中标为原理示意的部分用于解释模型,不是实测截图;由公式生成的曲线只在图注与正文列出的假设下成立。不要把合成实验输出当成真实产品的性能报告。
七、常见现象与排查方向
| 现象 | 优先检查 | 处理方向 |
|---|---|---|
| 姿态方向反了 | 把物体→相机当成相机→物体 | 使用完整逆变换 |
| 远近距离差固定倍数 | 三维模型尺寸或单位错误 | 核对物理尺度 |
| 图像缩放后误差明显 | 内参未同步变化 | 缩放焦距与主点,检查裁剪偏移 |
| 结果突然翻转 | 平面歧义、角点顺序或误检 | 多候选检查并保留历史证据 |
八、独立验证与适用边界
记录图像原始分辨率、处理步骤、内参与畸变版本、三维点单位和点序。把测试图像按距离、视角和覆盖范围分组,比较像素残差与独立位置误差。不能只用参与求解的角点评价完整空间精度。本例未处理滚动快门、运动模糊和内参不确定性,实际相机系统需单独检验。
建议把原始输入、参数、软件版本、输出结果和失败状态一起保留。若把同一份检查数据反复用于选择参数,它就不再是完全独立的最终验证集。将算法演示接入真实系统时,应另行验证输入质量、参考不确定性和完整处理链路。
参考资料
上述资料用于核对相关接口与基本约定;本文的合成数据、推导示例与代码不声称复刻库内全部实现。
写在最后
"温故而知新,可以为师矣。"
------ 孔子,《论语·为政》
先画清变换方向,再解释求解器输出,许多位姿问题就会清晰起来。