ARTICLE DETAIL

资讯详情

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

从像素到归一化坐标:理解内参矩阵与相机标定的核心

从像素到归一化坐标:理解内参矩阵与相机标定的核心 做了几年视觉测量项目之后我渐渐发现一个反直觉的事实相机成像最核心的问题不是“怎么把世界坐标算成像素坐标”而是“怎么从像素坐标里找回那个不受镜头厚度、传感器尺寸、分辨率影响的标准化中间坐标”。这个中间坐标就是归一化平面坐标。回到成像侧的像素时起决定作用的则是内参矩阵。很多人把内参矩阵当成“相机标定跑出来的一个矩阵”背完公式、调完OpenCV就收工。可如果不搞明白它到底在做什么归一化平面为什么是所有视觉算法的默认舞台那后面做姿态估计、双目测距、三维重建时你大概率会被一堆叫不出名字的精度问题和离奇报错卡住。这篇文章我就用“从像素还原到归一化坐标”这条主线把针孔模型、内参矩阵、畸变模型和实际代码串起来聊清楚。适合刚学完相机标定准备做SLAM的朋友也适合已经在做3D视觉但坐标系关系还模棱两可的开发者——把这层纸捅破后面很多接口文档看起来会舒服得多。1. 先搞清楚一个问题像素坐标到底能不能直接换算成三维坐标1.1 针孔模型一张图讲清四个坐标系的关系很多教材一上来就抛针孔模型公式但初学者最容易忽略的是针孔模型本质上是在回答“一束光在穿过镜头中心之后打在传感器哪个位置”。这里的镜头中心就是光心也叫相机坐标系原点。整个成像过程可以拆成四个坐标系之间的变换世界坐标系、相机坐标系、图像物理坐标系、像素坐标系。世界坐标系是你随便选的比如标定板左上角的角点或者机器人底盘中心。相机坐标系以光心为原点Z轴指向相机正前方。图像物理坐标系在传感器平面内单位是毫米原点通常在光轴与传感器平面的交点。像素坐标系单位是像素原点一般在图像的左上角。这四个坐标系的关系是一层套一层的世界坐标通过外参旋转R和平移t变换到相机坐标相机坐标通过透视投影变换到图像物理坐标图像物理坐标再通过内参矩阵里的像素缩放和平移变成你最终看到的像素坐标。外参描述的是“相机在世界中的位置”内参描述的是“传感器怎么把物理图像变成像素图像”。这里有个很容易忽略的细节透视投影这一步数学上就是在做“除以Z”。一个三维点 (P_c(X_c, Y_c, Z_c)) 投影到图像物理平面上的坐标是 ((fX_c/Z_c, fY_c/Z_c))。这个“除以Z”的动作就是把三维信息压成二维信息也是深度信息丢失的地方。之所以要引入归一化平面正是为了先把“除以Z”这件事单独拎出来不被焦距、像素尺寸这些相机自身属性干扰。1.2 为什么投影的第一步是“归一化”而不是“直接出像素”你可能会想既然最终要的是像素坐标那为什么不一步到位理论上一台理想相机确实可以一步到位但问题是“一步到位”会把相机本身的属性混进几何关系里。举个例子两台相机分辨率不同、传感器大小不同同一个三维点投影到两台相机上的像素坐标肯定不一样。可如果你先把它投影到归一化平面得到的是两个完全相同的坐标 ((X_c/Z_c, Y_c/Z_c))。这个坐标与相机无关它只取决于三维点相对于相机的方向。方向相同、距离不同的点归一化坐标完全相同。这一点是后续所有多视角几何算法的根基。所以视觉算法界形成了一套默契所有几何计算都尽量在归一化坐标下做只有最后输出像素时才用内参矩阵做一次线性变换。这样做的好处非常直观——算法不再需要关心你用的是工业相机还是手机镜头也不关心传感器尺寸多大大家在一个统一的“标准舞台”上讨论问题。2. 内参矩阵K一部“物理单位转像素单位”的全套配置2.1 fx、fy、cx、cy、s这五个参数各管什么事内参矩阵通常写作[ K \begin{bmatrix} fx s cx \ 0 fy cy \ 0 0 1 \end{bmatrix} ]这五个参数各有各的物理含义。fx、fy不是焦距f本身而是“焦距的像素度量”。如果传感器像素是正方形的且x、y两方向的像素密度一致那fx和fy相等。可实际中像素往往不是完美正方形或者感光芯片在贴装时有微小拉伸导致两个方向的等效焦距有差异。cx、cy是主点坐标也就是光轴穿过传感器平面的那个点在像素坐标系中的位置。理想情况下它在图像正中心但镜头安装偏心、传感器切割误差都会让主点偏移。很多算法在初始化时直接假设主点在图像中心如果镜头畸变校正不彻底这个假设会在后续计算中埋下隐患。倾斜因子s则更微妙它描述的是像素行与列之间是否严格垂直。大多数消费级相机的s非常接近0很多标定程序直接忽略它。但如果你做的是高精度测量s不校准会导致重投影误差出现系统性偏置。我见过一些项目用Matlab标定出的s值不为零在OpenCV里却把它强制置零结果三角化精度下降了一个量级。2.2 一个数值算例内参矩阵如何把归一化点变成像素点假设某相机标定后得到fx800fy810cx320cy240s0。一个三维点在相机坐标系下的坐标是 (0.5, 0.3, 2.0)。它的归一化坐标就是 (0.25, 0.15, 1.0)。像素坐标计算如下[ u 800 \times 0.25 0 320 520 ] [ v 810 \times 0.15 0 240 361.5 ]注意fy与fx不同所以同样的归一化x、y在像素域里被拉伸的程度不一样。如果这时候你用fx代替fy去算v坐标误差是 (810-800)×0.15 1.5像素。看起来不大但在多视角匹配时1.5像素的偏差足以让极线约束出现明显误差误匹配率直线上升。这个算例还说明一件事内参矩阵本质上是把点从归一化坐标空间映射到像素坐标空间的一个线性变换。它的输入是归一化点输出是像素点。反过来从像素点还原归一化点时你做的就是乘以K的逆矩阵。2.3 标定得到的K怎么验证合理性拿到标定结果后不要急着扔进算法里。先做三件事第一看fx和fy是否在合理范围。可以用“分辨率宽度/传感器物理宽度×焦距毫米值”粗算一下等效像素焦距。比如传感器宽6.4mm、分辨率宽1280镜头焦距8mm那fx理论值约为1600。如果标定结果是800或者3200多半是标定板图像数量不够或者板子姿态覆盖不全。第二看主点是否偏离中心太多。一般偏差在几十像素内算正常如果cx、cy偏离中心超过图像宽度的20%就要怀疑标定板角点提取有误或者镜头畸变模型不匹配。第三看重投影误差。OpenCV标定输出的RMS误差通常在0.1到0.5像素之间。超过1像素就要警惕说明标定图片要么模糊、要么板子太倾斜、要么畸变模型没选对。3. 归一化平面视觉算法真正运行的“标准舞台”3.1 归一化平面到底是什么z1处的一张虚拟成像面字面上看归一化平面就是相机坐标系下 (Z1) 的那个平面。一个三维点 (P_c) 投影到这个平面上坐标变成 ((X_c/Z_c, Y_c/Z_c, 1))。这张平面是虚拟的相机里并不存在物理对应的传感器但它非常有用——你可以把它理解为“焦距为1像素的理想相机”的成像平面。为什么要特意构造这样一个虚拟平面因为投影几何里最难处理的就是那个“除以Z”。只要把“除以Z”这个非线性操作固定下来剩下的从归一化平面到像素平面的变换就是纯粹的线性变换可以用矩阵乘法完成。线性变换意味着你可以自由地求逆、复合、分解而不会被非线性操作搅得焦头烂额。从另一个角度看归一化平面坐标其实是一个方向向量。点 (0.25, 0.15, 1.0) 和点 (0.5, 0.3, 2.0) 的归一化坐标相同因为它们在同一条射线上。归一化坐标只关心方向不关心距离恰好符合透视成像的物理事实一个点在图像上的位置只由方向决定与深度无关。3.2 本质矩阵、PnP、三角化为什么都默认使用归一化坐标先看对极几何。本质矩阵E描述的是两帧图像之间对应点的约束关系它的定义式是[ p_2^T E p_1 0 ]这里的 (p_1)、(p_2) 是两帧图像中对应匹配点的归一化坐标。为什么不是像素坐标因为本质矩阵分解出来是旋转和平移这些量定义在相机坐标系下而不是像素坐标系下。如果你直接用像素坐标算算出来的矩阵叫基础矩阵F它混入了内参信息需要再乘上 (K^{-1}) 才能得到本质矩阵。再看PnP。求解相机位姿时经典做法是先把2D像素坐标反投影成归一化坐标然后建立3D点与2D归一化点之间的对应关系再求解旋转和平移。如果你直接用像素坐标做PnP内参会被吸收进位姿解里导致标定误差直接污染位姿结果。较好的做法是先归一化再求解让优化问题关注纯几何关系。三角化也一样。两条射线在归一化坐标下求交点本质上是两个方向向量做叉积和最小二乘。如果射线方向向量被像素坐标污染三角化出来的深度会出现系统性偏差尤其在基线和深度比值很小的场景下偏差会被放大到离谱的程度。3.3 归一化平面与内参矩阵的关系式把前面的关系串起来可以写出一个极简的成像链条[ \lambda \begin{bmatrix} u \ v \ 1 \end{bmatrix} K \begin{bmatrix} X_c / Z_c \ Y_c / Z_c \ 1 \end{bmatrix} ]这里的 (\lambda) 是尺度因子。读法是这样的先求归一化坐标再用内参矩阵做线性变换得到像素坐标。反过来从像素坐标求归一化坐标就是[ \begin{bmatrix} X_c / Z_c \ Y_c / Z_c \ 1 \end{bmatrix} K^{-1} \begin{bmatrix} u \ v \ 1 \end{bmatrix} ]注意这一步反求出来的归一化坐标是带尺度歧义的——它对应的是那条射线上任意一点方向确定但深度未知。这也是为什么单目相机无法从一帧图像直接恢复绝对深度只能恢复方向。4. 畸变处理要摆对位置去畸变其实发生在归一化前后4.1 径向畸变与切向畸变对坐标的影响真实镜头不是完美针孔光线经过透镜时会发生弯曲这就产生了畸变。OpenCV用了五参数畸变模型三个径向畸变系数k1、k2、k3和两个切向畸变系数p1、p2。径向畸变会让直线变弯。k1是主导项表现为画面边缘的桶形或枕形畸变k2负责更边缘的弯曲k3通常只在鱼眼镜头或畸变极大的广角镜头上才显著。切向畸变则是因为镜头与传感器平面不平行导致像点在切向方向发生偏移在画面边缘更明显。畸变模型的作用对象是归一化坐标这一点很多人会搞反。正确的做法是先把像素坐标还原成理想的归一化坐标再根据畸变模型计算实际畸变后的归一化坐标最后才投影到像素域。OpenCV的initUndistortRectifyMap和undistortPoints内部走的都是这条链路。4.2 完整的畸变校正顺序从像素到归一化的正确路径一句话总结畸变校正发生在归一化坐标系内不在像素坐标系内。从像素坐标求“无畸变归一化坐标”的标准流程是用 (K^{-1}) 把像素坐标转成归一化坐标。对这个归一化坐标应用畸变模型算出畸变后的归一化坐标。如需继续求像素坐标再用K把畸变后的归一化坐标转回像素坐标。很多人写代码时直接用undistort函数对整张图像去畸变然后从去畸变后的图上提取特征点。这样当然可以但性能开销大。更高效的做法是只对提取到的角点或特征点做undistortPoints一次性把像素坐标转成去畸变后的归一化坐标。我自己在项目里通常不直接调用cv2.undistort而是先提特征点再用undistortPoints批量转换。实测下来速度提升明显而且精度更高因为不用经过图像插值。5. OpenCV实操用好内参矩阵和归一化平面的几个关键场景5.1 从像素坐标反推归一化坐标的代码链路代码最能说明问题。假设你有一组像素坐标pts_pixel相机内参K和畸变系数dist已经标定好。要把这些点转成归一化坐标直接import cv2 import numpy as np K np.array([[800, 0, 320], [0, 810, 240], [0, 0, 1]], dtypenp.float64) dist np.array([0.05, -0.02, 0.001, 0.002, 0.0]) pts_pixel np.array([[520.0, 361.5], [100.0, 200.0]], dtypenp.float64) # 方法一用 undistortPoints直接得到归一化坐标 pts_norm cv2.undistortPoints(pts_pixel, K, dist) # 输出形状为 (N,1,2)即去畸变后的归一化坐标 x/z, y/z print(pts_norm)有个细节要记住OpenCV 4.x里的undistortPoints输出就是归一化坐标不需要再除以焦距。早年间有些版本默认输出的是带焦距的物理坐标需要手动归一化这坑踩过的人不少。如果你要的只是“像素转归一化坐标、不做畸变校正”代码更简单pts_homo cv2.convertPointsToHomogeneous(pts_pixel).reshape(-1, 3).T pts_norm np.linalg.solve(K, pts_homo) pts_norm pts_norm[:2] / pts_norm[2]5.2 solvePnP如何依赖内参与归一化平面solvePnP是视觉测量和位姿估计中最常用的函数之一。它的入参是3D点、2D点、内参K和畸变系数dist。内部流程是先用K和dist把2D像素点转成去畸变的归一化坐标再根据3D点和归一化坐标求解R、t。看一个实际例子假设你在标定板上检测到9个角点对应3D坐标已知像素坐标已知。求解相机位姿retval, rvec, tvec cv2.solvePnP(obj_points, img_points, K, dist)如果我故意不传K把对象点坐标直接和像素点坐标对应起来会发生什么算法会试图用一个“单位像素焦距”的相机模型去解释物理投影解出来的位姿会严重偏离真实值。这说明在PnP问题里内参矩阵的作用就是把像素坐标先拉回归一化坐标系让3D-2D对应关系建立在几何真实度量上。5.3 标定结果的合理性检查几行代码搞定前面提到要验证重投影误差这里给出一个免责最小案例# 假设已经得到标定结果 K, dist # obj_points: 标定板3D点 # img_points: 检测到的像素点 retval, rvec, tvec cv2.solvePnP(obj_points, img_points, K, dist) proj_points, _ cv2.projectPoints(obj_points, rvec, tvec, K, dist) error np.mean(np.linalg.norm(proj_points.reshape(-1,2) - img_points, axis1)) print(重投影误差, error)如果误差在0.2像素以内标定结果可以认为合格。超过0.5像素我通常不会继续往下做而是返回去检查标定采集流程。6. 四个典型的坑每个我都踩过6.1 坑一畸变与归一化顺序放反了这是最常见的坑。有人从像素坐标出发先除以焦距得到“归一化坐标”再应用畸变模型。表面上看都是归一化坐标但畸变模型中的x、y是理想归一化坐标不是畸变后的坐标。顺序一颠倒径向畸变的符号就反了本应外凸的边缘会变成内凹。你用这样的坐标去算极线会发现匹配点离极线越来越远。正确的处理方式只有一个先理想归一化再畸变——OpenCV内部也是这么做的。6.2 坑二把fx和fy当同一个焦距很多工程代码为了省事直接用fx代替fy或者手动把像素坐标归一化时只除以一个标量。如果你用的图像分辨率接近正方形且标定结果fx与fy相差很小可能毫无感觉。可一旦传感器像素不是正方形或图像经过程序拉伸缩放差别立刻显现。我在一个项目里遇到过诡异现象三角化出来的深度整体偏大。排查半天发现是有人对图像做了resize却用原内参去还原归一化坐标。resize之后的内参必须同步缩放fx、fy和cx、cy都要按缩放比例更新。这个错误隐蔽性极强因为图像看起来一切正常只有深度值对不上。6.3 坑三坐标系方向习惯不一致这里说的是图像坐标系y轴方向。图像物理坐标系通常以中心为原点、y轴向上而像素坐标系以左上角为原点、y轴向下。转换时如果忘记把v方向取反所有归一化坐标的y都会反号。在PnP位姿估计中这个反号会让旋转矩阵多一个旋转π的项位姿完全错误。我习惯在代码里显式定义坐标变换不用隐式约定。每次从像素到归一化都明确写出“这里y方向已经转换”防止过几天自己都看不明白。6.4 坑四把归一化坐标和相机坐标混为一谈归一化坐标和相机坐标的区别其实很简单归一化坐标的深度恒为1相机坐标的深度是真实Z值。两者方向相同但尺度不同。在做三角化时有人直接把归一化坐标当作方向向量来处理忘记乘以估计出的深度结果重建出的3D点全部落在单位球面上而不是真实位置。正确的做法是三角化先求归一化坐标方向再根据两条射线的交点求深度最后用深度乘以归一化方向得到相机坐标系下的真实3D坐标。这一步差了整个重建模型都会缩成一个极小的深度范围。写在最后的一点小经验如果你现在正被相机标定、PnP精度或者三角化结果折磨我建议你回到本源把每一个像素坐标都先还原成归一化坐标再谈几何关系。绝大多数精度问题追到根上都是归一化环节出了差错。我自己的习惯是在代码里专门封装一个坐标变换模块统一处理像素到归一化的转换和畸变校正所有下游算法都只接收归一化坐标。这样一来更换相机时只需要更新一个模块整个算法链路不用动。时间久了你会发现内参矩阵和归一化平面不是两个孤立的知识点而是整个三维视觉体系的地基。地基稳了上面盖什么楼都不慌。
返回列表