Part III · 第 10 章 相机与投影 · 10.3

内参、外参与畸变

针孔模型里的 \(P=K[R|t]\) 把一个三维点变成一个像素,可是真正的相机还有三组要弄清的数:相机在世界里的位姿(外参),光线怎样落到像素格子上(内参 K),镜头偏离理想针孔的程度(畸变)。这一课拆开这三组参数,讲清每个数的单位、方向和约定,并用一面"墙"检验你有没有搞对。

约 35 分钟 前置:10.1 针孔相机 · 10.2 射影几何 · 5.1 坐标系与位姿 · 5.3 SE(3) 6 个交互

    1为什么机器人需要它

    一台手眼系统里的深度相机看见一只杯子,杯子在像素 \((u,v)\),深度是 \(z\)。要抓它,机器人得把这个像素变成基座坐标系里的一个三维点,路上要走三步:像素 \(\to\) 相机坐标系里的射线(靠内参 K),修正镜头弯曲了的光线(靠畸变系数),相机坐标系 \(\to\) 基座坐标系(靠外参,也就是手眼标定的结果,见 7.3 的工具系与基座系)。每一步都有几个数,每个数都有单位和方向的约定。

    这些数错一点,后果比想象中大。取 \(f=600\) 像素、深度 \(z=2\) m:横向坐标是 \(X=(u-c_x)\,z/f\),如果主点 \(c_x\) 偏了 10 像素,杯子的横向位置就偏了 \(10\cdot2/600\approx3.3\) cm,已经超出大多数夹爪的容差;焦距偏 5%,离光轴 0.5 m 的点偏 2.5 cm。更糟的是误差和位置有关:一面平整的墙在点云里会弯成"香蕉",两面垂直的墙夹角变成 85°。

    10.1 节给出针孔模型,10.2 节把投影写成了 \(\lambda\tilde{\mathbf x}=K[R|t]\tilde{\mathbf X}\)。这一课打开这个式子:K 里每个元素是什么,\([R|t]\) 为什么是相机位姿的逆,镜头畸变怎么建模、为什么不能直接求逆,以及 OpenCV、OpenGL、ROS 三套坐标约定怎么互相换算。下一课 10.4 会反过来从图像里估计这些参数。

    2直觉:内参 K 是一次二维仿射变换

    先把投影拆成两半。第一半与相机的"硬件"无关:相机坐标系(OpenCV 约定:x 向右,y 向下,z 向前)里的点 \((X,Y,Z)\) 除以深度,得到归一化坐标 \((x,y)=(X/Z,\,Y/Z)\)。它没有单位,就是光线方向的斜率,等价于一张放在 \(z=1\) 处的"理想底片"。第二半才是传感器:K 把这张理想底片上的坐标(以"焦距"为单位)换成像素。它只做三件事:

    • 缩放:\(f_x,f_y\),单位是像素,等于"一个单位斜率对应多少像素"。镜头变长(变焦),\(f\) 变大,视场变窄。
    • 平移:主点 \((c_x,c_y)\),光轴与传感器的交点。它不是平移相机,也不是转动相机:所有深度上的点都平移同样的像素数,没有视差。理想情况在图像中心,实际偏几个到几十个像素。
    • 变形:\(f_x\neq f_y\)(像素不是正方形)和斜切 \(s\)(传感器行列不垂直)。现代相机几乎总是 \(f_x\approx f_y\),\(s\approx0\),但模型里留着它们。

    下面的实验把这几件事摆在一起。左边从外面看相机和它的视锥,右边是相机拍到的图像(像素坐标 \(v\) 向下;图中坐标轴已按这个方向标出)。注意:改 K 时相机的位置和朝向一点没变,只有"底片上的刻度"变了。

    FIG 1内参 K:缩放、平移、变形,以及改分辨率
    外部视角,可拖动旋转
    右图:同一个场景(方盒、球、立杆、地面网格)在当前 K 下的像。紫色十字是主点,琥珀色的平行四边形是归一化平面上的小方块 \([0,0.2]^2\) 在 K 下的像(缩放、纵横比和斜切一目了然),蓝点 P 是盒子的一个顶点(读数给出它的像素坐标和相对默认 K 的位移)。左图琥珀色是视锥,紫色虚线是光轴,它的形状直接由 K 决定:主点偏离中心时视锥变斜,\(f\) 变大时视锥变细。在"缩小一半"和"裁剪"两种模式里,红色虚线是忘记更新 K(仍用原来的 K)时场景会落到的位置。
    试一试
    • 把 \(f_x\) 和 \(f_y\) 都拉到 600 以上。水平视场角变成了多少?用读数里的 \(2\arctan(w/2f_x)\) 验算,再看视锥怎么变。
    • 把 \(c_x\) 向右挪 40 像素以上。蓝点 P 的像素位移是多少?它和 \(c_x\) 的变化有什么关系?相机的外部位置变了吗?
    • 让 \(f_x\) 和 \(f_y\) 相差 60 以上。圆形的球变成什么形状?
    • 选"缩小一半"或"裁剪"。对照读数里的 \(K'\) 和 \(K\):哪些元素乘了 0.5?哪些只是减去了窗口偏移?红色虚线为什么错开了?

    焦距的单位,和视场角

    \(f\) 以像素计,和镜头标称的毫米焦距 \(f_{\text{mm}}\) 差一个像素间距:\(f_x=f_{\text{mm}}/p_x\)(\(p_x\) 是像素在传感器上的宽度)。4 mm 的镜头配 3 µm 的像素,\(f_x\approx1333\) px。同一个镜头换成更高分辨率的传感器(像素更小,或者同样的传感器读出更多像素),\(f_x\) 的数字就变了,镜头和视场角却没变,这就是为什么 K 必须绑定到具体的分辨率上。视场角只由 \(f\) 和图像宽度的比值决定:

    $$ \tan\frac{\theta_h}{2}=\frac{w}{2f_x},\qquad \tan\frac{\theta_v}{2}=\frac{h}{2f_y},\qquad \theta_h=2\arctan\frac{w}{2f_x}. $$

    这就是视场角公式(见"数学"一节的推导)。例:\(w=640\),\(\theta_h=90^\circ\) 时 \(f_x=320\);\(60^\circ\) 时 \(f_x\approx554\);\(120^\circ\) 时 \(f_x\approx185\)。主点偏离中心时左右两侧的半角不再相等:左边是 \(\arctan(c_x/f_x)\),右边是 \(\arctan((w-c_x)/f_x)\),FIG 1 的读数里能看到。

    改变图像大小时 K 怎么跟着变?如果把图像缩放 \(a\) 倍,每个像素坐标都乘 \(a\),所以 \(f_x,f_y,c_x,c_y\) 都乘 \(a\)(\(s\) 也是);如果裁出一个左上角在 \((x_0,y_0)\) 的窗口,像素坐标整体平移,只有 \(c_x,c_y\) 减去偏移,\(f\) 不变。归一化坐标和畸变系数与像素无关,所以畸变系数 D 在缩放和裁剪下保持不变。写成矩阵是 \(K'=S\,T\,K\),在"数学"一节推导。

    3动手玩:外参与整条投影链

    内参管"相机内部",外参管"相机在世界里的哪里"。世界系里一个点 \(\mathbf X_w\) 要经过三次变换才变成像素:

    1. 世界 \(\to\) 相机:\(\mathbf X_c={}^{c}T_{w}\,\mathbf X_w=R\mathbf X_w+\mathbf t\)。这里的 \([R|t]\) 就是外参,它把世界坐标变成相机坐标。
    2. 相机 \(\to\) 归一化平面:\((x,y)=(X_c/Z_c,\;Y_c/Z_c)\),除以深度(这一步丢掉了深度信息)。
    3. 归一化 \(\to\) 像素:\((u,v)\) 由 K 给出(再加上上一节说的畸变,我们放在后面)。

    最容易混淆的是方向:外参是相机位姿 \({}^{w}T_{c}\) 的逆。相机位姿说"相机坐标系的原点和三根轴在世界里的样子",它的平移部分就是相机中心 \(C\);外参是把世界点搬到相机里去,它的平移 \(\mathbf t=-R\,C\) 是"世界原点在相机坐标系里的位置",不是相机的位置。FIG 2 让你自己摆相机,看读数里的 \([R|t]\) 和 \(C=-R^{\top}\mathbf t\)。

    FIG 2世界 → 相机 → 归一化 → 像素
    拖动旋转视角
    ② 世界 → 相机(外参 \([R|\mathbf t]\))
    ③ 除以深度(丢掉深度)
    ④ 乘以 K(换成像素刻度)
    黄点是世界点 \(\mathbf X_w\)。阶段 ①:沿世界三轴的分量(红绿蓝折线);阶段 ②:沿相机三轴的分量 \((X_c,Y_c,Z_c)\),折线从相机中心出发;阶段 ③:射线穿过 \(z=1\) 的归一化平面(视锥的底面),交点就是 \((x,y)\);阶段 ④:把这张平面按 K 标上像素刻度。右图是像平面:阶段 ③ 的刻度是归一化坐标,阶段 ④ 的刻度是像素,点的位置完全一样,K 只是换了刻度。相机位姿用平移(相机中心的世界坐标)加偏航 \(\psi\)、俯仰 \(\phi\)、滚转 \(\rho\) 给出:\(R_{wc}=R_z(\psi)\,R_0\,R_x(\phi)\,R_z(\rho)\),\(R_0\) 是"朝向 +x、头顶朝 +z"的基准姿态。
    试一试
    • 依次点一遍四个阶段,看每一阶段"改变了什么、丢掉了什么"(阶段 ② \(\to\) ③ 丢掉了深度,③ \(\to\) ④ 只是换刻度)。
    • 先把相机移到离原点 4 m 的地方(例如位置 \((-4,0,0)\)),再按"朝向原点",让外参读数成为 \(\mathbf t=(0,0,4)\)。这时世界原点在相机前方多远?\(\mathbf t\) 等于相机位置吗?(试着解释为什么 \(\mathbf t\) 里没有 x、y。)
    • 把偏航角转过 120° 以上,让点跑到相机背后:\(Z_c\lt0\)。读数怎么说?为什么 \(x=X_c/Z_c\) 的公式在这里会给出一个"看似正常"的像素?

    4再换几个视角:畸变、去畸变与鱼眼

    镜头不是针孔:Brown–Conrady 模型

    真实镜头会把光线"弯"一点,所以直线在图像里不再是直线。标准做法是在归一化坐标里先弯一下,再交给 K 换成像素:\((x,y)\to(x_d,y_d)\to(u,v)\)。弯的方式用两组项描述:径向项 \(k_1,k_2,k_3\),让点沿半径方向向内或向外移动,移动量随半径 \(r=\sqrt{x^2+y^2}\) 迅速增大;切向项 \(p_1,p_2\),来自镜片与传感器没有完全对中("偏心畸变")。

    \(k_1<0\) 时放大率随半径减小,图像边缘被"收"回来,直线向外鼓,叫桶形畸变,广角镜头常见;\(k_1>0\) 反过来,叫枕形,长焦常见;\(k_1\) 与 \(k_2\) 符号相反时可以得到先桶后枕的"胡须形"。FIG 3 里的棋盘格是真实算出来的:对输出图像的每个像素,用迭代去畸变求出它对应的归一化射线,再查表格上的颜色。

    FIG 3畸变实验室:x_d = D(x)
    左:相机(\(f=300\) 像素,\(320\times240\))在当前畸变下拍到的图像。虚线是无畸变针孔应该给出的直线网格,实线(琥珀色,"网格曲线"模式)是同一批直线经过 \(D\) 后的样子;"位移箭头"模式画出每个网格节点从理想位置到畸变位置的位移(红色,真实大小)。灰色区域是 \(D\) 不可逆、无法去畸变的地方(见 FIG 4)。右:沿半径的曲线 \(r_d=r\,(1+k_1r^2+k_2r^4+k_3r^6)\)(切向项不画),和对角线 \(r_d=r\) 的差就是径向位移;水平虚线是图像边缘和角点(以归一化坐标的半径 \(r_d\) 计);红点是 \(\mathrm d r_d/\mathrm d r=0\) 的"折返点",越过它 \(D\) 就不再单调,而且曲线的最大值 \(r_{d,\max}\) 低于某条虚线时,那部分图像找不到对应的射线(左图灰色)。
    试一试
    • 先让 \(k_1\lt-0.15\)(桶形),再让 \(k_1\gt0.15\)(枕形)。分别看棋盘格的边怎么弯、位移箭头指向中心还是指离中心。角点的位移有多少像素?

    反过来:去畸变没有闭式解

    前向的 \(D\) 是一个多项式,随便算。可是图像里测到的是畸变后的点 \((x_d,y_d)\),要做三角化、PnP、极线几何都需要无畸变的 \((x,y)\),也就是解 \(D(x,y)=(x_d,y_d)\)。这是两个变量的高次方程组(\(k_3\ne0\) 时 \(r\) 的七次),没有闭式解,只能迭代。常见的三种:

    • A. 残差修正:\(\mathbf x\leftarrow\mathbf x-\bigl(D(\mathbf x)-\mathbf x_d\bigr)\),即把 \(D\) 近似成恒等映射。本课的 Vision.undistort 就是它。
    • B. 不动点(OpenCV 的做法):\(\mathbf x\leftarrow(\mathbf x_d-\Delta_{\text{tan}}(\mathbf x))/\rho(r^2)\),其中 \(\rho=1+k_1r^2+k_2r^4+k_3r^6\);OpenCV 的 undistortPoints 就是这类迭代,经典接口默认只迭代几次(5 次)。
    • N. Newton:\(\mathbf x\leftarrow\mathbf x-J_D(\mathbf x)^{-1}(D(\mathbf x)-\mathbf x_d)\),二次收敛,每步多算一个 \(2\times2\) 雅可比。

    A 和 B 是线性收敛,收敛因子等于迭代映射的雅可比范数;N 则几步就到机器精度。FIG 4 把它们同时跑出来。这里还藏着一个重要的事实:当 \(k_1\) 很大时,\(r_d(r)\) 在 \(r^\ast\) 处到达最大值后往回走(FIG 3 右图的红点),超过它的 \(r_d\) 根本没有原像,迭代无论怎么做都收敛不到零残差。畸变系数是在标定图像覆盖的范围内拟合的,范围之外的"逆"没有意义。

    FIG 4去畸变的迭代:A、B 与 Newton
    左:归一化平面(\(y\) 向下,与图像一致)。拖动紫色圆圈设定测到的畸变点 \(\mathbf x_d\);大圆点是迭代 \(n\) 步后的估计(颜色随方法:A 琥珀、B 紫、Newton 青),路径是所选方法的全部迭代,蓝色小点是 Newton 迭代 100 次得到的"真值"。红色虚线圆是折返半径 \(r^\ast\)(理想坐标里),紫色虚线圆是 \(r_{d,\max}\)(畸变坐标里):\(\mathbf x_d\) 落在它外面就没有解。右:残差 \(\lVert D(\mathbf x_i)-\mathbf x_d\rVert\) 的以 10 为底的对数随迭代次数的变化,三条曲线对应三种方法。
    试一试
    • 选"超出折返点",把紫色圆圈拖到 \(r_d\) 大于读数里的 \(r_{d,\max}\)。三种方法的残差还能降到 \(10^{-6}\) 以下吗?这个点对应哪个"真实"的射线?
    去畸变一张图,不需要求逆

    求点的无畸变坐标要迭代,可是求一整张图的无畸变图像不用:对输出图(理想针孔图)的每个像素 \((u,v)\),先算归一化坐标,用前向的 \(D\) 算出它在原始图里的位置,再在那里插值取色。OpenCV 的 initUndistortRectifyMap + remap 就是这样做的:映射表只算一次,之后每帧只是查表。反过来,想"把无畸变图渲染成带畸变的图"(比如仿真里模拟真实镜头)就要对每个像素求逆,FIG 3 的棋盘格就是这样画出来的。

    视场角超过 180°:鱼眼

    针孔的像距满足 \(r=f\tan\theta\)(\(\theta\) 是光线与光轴的夹角)。\(\theta\to90^\circ\) 时 \(r\to\infty\):针孔相机的视场角永远到不了 180°,而且越靠近极限边缘被拉伸得越厉害。鱼眼镜头换一种映射,让大角度的光线也能落进有限的传感器:最常用的等距模型 \(r=f\theta\),还有立体投影 \(r=2f\tan(\theta/2)\)、等立体角 \(r=2f\sin(\theta/2)\)、正交 \(r=f\sin\theta\)。Kannala–Brandt 通用模型在等距模型上再加一个角度的多项式:\(r=f\,\theta_d\),\(\theta_d=\theta(1+k_1\theta^2+k_2\theta^4+k_3\theta^6+k_4\theta^8)\),OpenCV 的 fisheye 模块用的就是这个。

    FIG 5像距 r 随入射角 θ:针孔 vs 鱼眼
    传感器半宽固定为 160 像素(水平线);每条曲线与水平线的交点就是该模型能看到的最大半角。琥珀色的针孔曲线向上冲走,永远到不了 90°;等距模型是一条直线。右侧是各模型在这个 \(f\) 下的水平视场角;带 "≥" 的表示这个模型的可用角度范围(正交模型只到 90°)结束时曲线都没有碰到传感器边缘。
    试一试
    • 把 \(f\) 调小,直到等距模型的水平视场角达到 180°(需要 \(f\le101.9\))。此时针孔模型的视场角是多少?它在任何 \(f\) 下能不能到 180°?

    鱼眼图像不能先"去畸变"成针孔图再用:180° 的视场投到一个有限的平面上是不可能的,只能裁掉边缘,或者投到球面/立方体贴图上。实用的做法是直接在单位球上的射线里工作(11.1 节的对极几何可以写成射线的形式),或把鱼眼切成几块虚拟针孔相机。

    5再看数学

    提示:把鼠标悬停在公式中带底色的符号上,FIG 1 中对应的元素会被高亮。

    内参矩阵与像素 ↔ 归一化坐标

    $$ K=\begin{bmatrix}\hl{f}{\cv{f_x}} & \hl{s}{\cx{s}} & \hl{c}{\cw{c_x}}\\ 0 & \hl{f}{\cv{f_y}} & \hl{c}{\cw{c_y}}\\ 0&0&1\end{bmatrix},\qquad \begin{aligned}u&=f_x\,x+s\,y+c_x\\ v&=f_y\,y+c_y\end{aligned}\qquad\Longleftrightarrow\qquad \begin{aligned}y&=(v-c_y)/f_y\\ x&=(u-c_x-s\,y)/f_x\end{aligned} $$

    \(\tilde{\mathbf u}=K\tilde{\mathbf x}_n\),其中 \(\tilde{\mathbf x}_n=(x,y,1)^\top\) 是归一化坐标,\(\tilde{\mathbf u}=(u,v,1)^\top\)。K 是上三角矩阵,逆矩阵也是:\(K^{-1}=\begin{bmatrix}1/f_x & -s/(f_xf_y) & (sc_y-c_xf_y)/(f_xf_y)\\ 0&1/f_y&-c_y/f_y\\0&0&1\end{bmatrix}\)。所以像素 \(\to\) 射线也是线性的:\(\mathbf d\propto K^{-1}\tilde{\mathbf u}\),射线上深度为 \(Z\) 的点是 \(\mathbf X_c=Z\,K^{-1}\tilde{\mathbf u}\)(\(Z\) 是沿光轴的深度,不是到相机中心的距离,见 FIG 6)。

    5 个自由度。K 只有 5 个自由度:\(f_x,f_y,s,c_x,c_y\),右下角固定为 1,因为 \(\tilde{\mathbf u}\) 是齐次坐标,整个矩阵乘一个常数不改变像素。为什么最后一行是 \((0,0,1)\)?因为相机坐标系的 z 轴就是光轴,齐次像素的第三个分量只是深度,不能混入 \(x,y\)。为什么左下的 \(K_{21}\) 也是 0?因为相机的 x 轴取得与传感器的像素行平行;剩下的任何转动都算进外参 \(R\)(这也是 RQ 分解唯一的原因)。

    外参、相机位姿与相机中心

    用 5.1、5.3 节的记号,\({}^{a}T_{b}\) 把 \(b\) 系里的坐标变到 \(a\) 系(\(\mathbf X_a={}^{a}T_{b}\mathbf X_b\)),相继变换按 1.4 节"从右往左"复合。相机位姿 \({}^{w}T_{c}\) 和外参 \({}^{c}T_{w}\) 互为逆:

    $$ {}^{c}T_{w}=\begin{bmatrix}R&\mathbf t\\ \mathbf 0^\top&1\end{bmatrix},\qquad {}^{w}T_{c}=\bigl({}^{c}T_{w}\bigr)^{-1}=\begin{bmatrix}R^\top&-R^\top\mathbf t\\ \mathbf 0^\top&1\end{bmatrix}=\begin{bmatrix}{}^{w}R_{c}&C\\ \mathbf 0^\top&1\end{bmatrix},\qquad \boxed{C=-R^\top\mathbf t,\quad \mathbf t=-R\,C.} $$

    相机中心为什么是 \(-R^\top\mathbf t\)。相机中心是投影不到任何地方的那个点:\(P\tilde C=\mathbf 0\),即 \([R|\mathbf t]\tilde C=R\,C+\mathbf t=\mathbf 0\),所以 \(C=-R^{-1}\mathbf t=-R^\top\mathbf t\)(\(R\) 正交)。\(\mathbf t\) 本身是世界原点在相机系里的坐标:\(R\cdot\mathbf 0+\mathbf t\)。只有 \(R=I\) 时 \(\mathbf t=-C\)。另外,\(R\) 的行是相机三根轴在世界系里的方向(\({}^{w}R_c=R^\top\) 的列);第三行就是光轴方向。

    装在机器人上的相机。外参往往是一串变换的复合:\({}^{c}T_{w}={}^{c}T_{b}\,{}^{b}T_{w}\),其中 \({}^{b}T_{w}\) 是机器人的定位结果(或者臂的正运动学,7.3 节),\({}^{c}T_{b}\) 是相机相对机器人本体的安装位姿的逆,由手眼标定得到(10.4 节)。

    整条链与 11 个自由度

    $$ \lambda\,\tilde{\mathbf u}=K\,[I\,|\,\mathbf 0]\;{}^{c}T_{w}\,\tilde{\mathbf X}_w=K\,[R\,|\,\mathbf t]\,\tilde{\mathbf X}_w=P\,\tilde{\mathbf X}_w,\qquad \lambda=Z_c. $$

    \(P\) 是 \(3\times4\),12 个数,整体乘常数不改变投影,所以最多 11 个自由度。数一下参数:K 有 5 个,\(R\) 有 3 个(SO(3) 的维数),\(\mathbf t\) 有 3 个,恰好 \(5+3+3=11\):P 里没有多余的东西,也没有缺少的东西。这也是一个测得的 P 能用"RQ 分解"唯一地拆回 K、R、t 的原因(Hartley–Zisserman 第 6 章)。带畸变的完整模型是 \(\tilde{\mathbf u}=K\,D\bigl(\pi(\,{}^{c}T_{w}\tilde{\mathbf X}_w)\bigr)\),\(\pi\) 是除以深度,D 在 \(\pi\) 之后、K 之前。

    改变分辨率:K 怎么变

    设新旧像素坐标之间是仿射关系 \(u'=a_xu-x_0\),\(v'=a_yv-y_0\)(先缩放 \(a\) 倍,再把窗口左上角平移到原点)。写成齐次形式 \(\tilde{\mathbf u}'=A\tilde{\mathbf u}\),那么 \(\tilde{\mathbf u}'=A\,K\,\tilde{\mathbf x}_n\),也就是说新的内参是

    $$ K'=\underbrace{\begin{bmatrix}a_x&0&-x_0\\0&a_y&-y_0\\0&0&1\end{bmatrix}}_{A}K=\begin{bmatrix}a_xf_x & a_xs & a_xc_x-x_0\\ 0&a_yf_y&a_yc_y-y_0\\0&0&1\end{bmatrix},\qquad D'=D. $$

    归一化坐标和 D 都在 K 之前,不受影响。一个细节:本课(和 FIG 1)把像素当作面积,坐标原点在左上角像素的角上,理想相机的主点是 \((w/2,h/2)\),缩放时 \(c'=a\,c\)。OpenCV 把整数坐标放在像素中心,理想主点是 \(((w-1)/2,(h-1)/2)\),缩放变成 \(c'=a(c+\tfrac12)-\tfrac12\);差半个像素,对精细的三维重建不是小事。

    畸变模型、折返半径与逆

    $$ \begin{aligned} x_d&=x\,\rho(r^2)+2p_1xy+p_2(r^2+2x^2),\\ y_d&=y\,\rho(r^2)+p_1(r^2+2y^2)+2p_2xy,\end{aligned}\qquad \rho(r^2)=1+k_1r^2+k_2r^4+k_3r^6,\quad r^2=x^2+y^2. $$

    这是 Brown–Conrady 模型(OpenCV 的 plumb_bob;Conrady 1919 提出偏心畸变,Brown 1966 给出摄影测量中的公式)。OpenCV 把系数存成 \((k_1,k_2,p_1,p_2,k_3)\),本课代码里的数组是 \([k_1,k_2,k_3,p_1,p_2]\),换库时注意顺序。更复杂的变体包括有理模型(\(\rho\) 的分母再加 \(k_4r^2+k_5r^4+k_6r^6\))、薄棱镜和倾斜传感器项。

    径向部分对 \(r\) 求导:\(\dfrac{\mathrm d r_d}{\mathrm d r}=1+3k_1r^2+5k_2r^4+7k_3r^6\)。例如只有 \(k_1<0\) 时,导数在 \(r^\ast=1/\sqrt{3|k_1|}\) 处为零,\(r_d\) 的最大值是 \(r_{d,\max}=\tfrac23r^\ast\);FIG 3 的红点和 FIG 4 的虚线圆就是它(\(k_1=-0.3\) 时 \(r^\ast\approx1.05\),已经超出常见相机的图像范围,\(k_1=-0.9\) 时 \(r^\ast\approx0.61\),图像里就能碰到)。折返点之外 D 不是单射,任何"逆"都不是唯一的,常见的办法是把去畸变限制在标定覆盖的半径之内。

    Newton 迭代需要 \(J_D\),它是 \(2\times2\) 矩阵,对 \(x_d\) 的两个分量求导即可得到。若需要对整个图像去畸变,则像上面的"视角"提示那样,预先算好查表。

    深度图 → 三维点

    $$ \mathbf X_c=Z\,K^{-1}\tilde{\mathbf u}\ \ (\text{after undistorting }\mathbf u),\qquad \mathbf X_w={}^{w}T_{c}\,\mathbf X_c,\qquad \text{range }d\ \Rightarrow\ \mathbf X_c=\dfrac{d}{\lVert K^{-1}\tilde{\mathbf u}\rVert}\,K^{-1}\tilde{\mathbf u}. $$

    这就是 FIG 6 的全部算法:像素中心 \((i+\tfrac12,j+\tfrac12)\) 先去畸变,再乘 \(Z\,K^{-1}\),最后用相机位姿搬到世界系。大多数深度相机的深度图存的是 \(Z\)(沿光轴),有些传感器(激光雷达、部分 ToF)给的是距离 \(d\),两者差一个因子 \(\lVert K^{-1}\tilde{\mathbf u}\rVert\),把它们混用会让视野边缘的墙往外鼓。

    视场角公式的推导

    与光轴夹角 \(\alpha\)(在 \(xz\) 平面内)的光线有 \(x=\tan\alpha\),落在 \(u=f_x\tan\alpha+c_x\)。图像右边缘 \(u=w\) 对应 \(\tan\alpha_R=(w-c_x)/f_x\),左边缘 \(u=0\) 对应 \(\tan\alpha_L=c_x/f_x\)。总视场角 \(\theta_h=\alpha_L+\alpha_R=\arctan\frac{c_x}{f_x}+\arctan\frac{w-c_x}{f_x}\),主点居中时 \(c_x=w/2\),化为 \(2\arctan\frac{w}{2f_x}\)。垂直方向同理;对角线 \(2\arctan\frac{\sqrt{w^2+h^2}}{2f}\)(\(f_x=f_y=f\),主点居中)。鱼眼的等距模型 \(r=f\theta\) 给出 \(\theta_h=w/f\)(弧度),所以 \(f\le w/\pi\) 时视场角 \(\ge180^\circ\)。

    6机器人中的它:坐标约定与深度图 → 点云

    一台机器人上同时有好几套相机坐标系的说法。OpenCV 与 ROS 的光学系(optical frame):x 向右,y 向下,z 向前,本课一直用它。OpenGL(以及 Blender、glTF 里的相机):x 向右,y 向上,相机看向 \(-z\),所以和 OpenCV 差一个绕 x 轴的 180° 旋转 \(\mathrm{diag}(1,-1,-1)\)(Unity 还是左手系)。ROS 本体系(REP-103):x 向前,y 向左,z 向上,相机的本体系 camera_link 沿用它,而 camera_..._optical_frame 才是光学系。从本体系到光学系是一个固定的旋转,URDF 里常写成 rpy="-π/2 0 -π/2"。它们三个都是同一个相机,只是轴的取法不同,换算与相机在哪里无关。

    FIG 6 有两种模式。"坐标约定"把三套坐标系画在同一台相机上,并列出它们之间的固定旋转。"深度图 → 点云"是整课的综合检验:一个房间角(两面垂直的墙和地面)和一个球,从一台镜头带桶形畸变的深度相机看过去,得到 \(64\times48\) 的深度图。你来选择重建时用的 K,看点云是不是还贴着真墙(虚线是墙上的真实直线)。

    FIG 6三套坐标约定,和一个能"看见"错误的点云
    拖动旋转视角
    深度图(颜色 = 深度 Z:琥珀近,蓝远)
    "坐标约定":粗箭头是所选约定的三根轴(红 x,绿 y,蓝 z),淡色的是另外两套。"点云":虚线是真实的直线(地面与墙的交线、墙角,以及墙上各画的两道水平条纹),点按所属表面着色(青:墙 A,绿:墙 B,灰:地面,紫:球),琥珀色的点是真实位于这两道条纹上的点:重建得对,它们就落在虚线上。读数里的"三维误差"是重建点与真实点的均方根距离;"墙 A 与墙 B 夹角"是对两面墙的点分别拟合平面后算出的夹角(真值 \(90^\circ\))。如果某个像素在错误的 K 下无法去畸变(落到了 FIG 4 的折返点之外),它会被丢掉,计入"无效点"。
    试一试
    • 切到"点云",关掉"去畸变"(只用 K,把镜头当成针孔)。三维误差有多大?墙 A 的平整度(拟合平面的 RMS)是多少,两面墙的夹角偏了多少?再打开去畸变,对比。
    • 保持去畸变开启,只调焦距误差,让两面墙的夹角偏离 \(90^\circ\) 超过 \(4^\circ\)。然后把 \(c_x\) 的误差调到 \(\pm4\) 像素:哪一个更伤?
    真实系统里

    ROS 的 sensor_msgs/CameraInfo:K(3×3,行主序)是原始(带畸变)图像的内参;D 是畸变系数,长度取决于 distortion_model(plumb_bob 是 \((k_1,k_2,t_1,t_2,k_3)\),rational_polynomial 是 8 个,另有 equidistant 鱼眼模型);R 是双目校正用的旋转(单目为单位阵);P 是 3×4 的校正后图像的投影矩阵,双目的右相机 \(P_{03}=-f_x'B\)。还有 binning 与 roi,用来描述缩小和裁剪过的图像,image_geometry 之类的工具据此调整 K,就是上面的 \(K'=AK\)。image_proc 用这些做校正;标定可用 camera_calibration 或 Kalibr。

    深度相机:Intel RealSense 在出厂时按每路数据流标定好了内参(rs2_intrinsics:宽高、ppx/ppy 即主点、fx/fy、畸变模型与 5 个系数)和各路流之间的外参,SDK 里的 rs2_deproject_pixel_to_point 就是 \(Z\,K^{-1}\tilde{\mathbf u}\)(再加上模型自己的去畸变),对齐彩色与深度用的是流间外参;D400 系列深度流常报告为无畸变(已校正)。特征匹配前先去畸变:ORB-SLAM 一类的系统对检测到的关键点去畸变(而不是整张图),再在归一化平面上做几何。AMR 与全景:鱼眼相机常用于环视和近场避障,需要 OpenCV fisheye 或 Kalibr 里的等距/全向模型,而不是 plumb_bob。坐标约定:REP-103 规定本体系与光学系,REP-105 规定 map → odom → base_link 的 TF 树;相机的光学系挂在 base_link 下面,这条外参链正是上面 \({}^{c}T_{w}={}^{c}T_{b}\,{}^{b}T_{w}\)。典型事故:驱动把流从 1280×720 切到 640×480,下游节点却仍然用 1280×720 的 K,整个点云的横向尺度错了 2 倍。

    7常见误区

    误区 1:把相机位姿和外参搞反

    OpenCV 的 solvePnP 返回的 rvec, tvec、标定得到的每张图的 \([R|\mathbf t]\),都是世界 \(\to\) 相机:\({}^{c}T_{w}\)。TF、URDF 里的相机"位置"和 SLAM 输出的轨迹是 \({}^{w}T_{c}\)。直接把 tvec 当作相机位置,在 \(R=I\) 时差一个符号,一般情况下既差符号又差一个旋转。直觉在这里失效的原因:\(\mathbf t\) 的单位是长度,看起来像"相机在哪儿",其实是"世界原点在相机里的哪儿"。验证办法:用 FIG 2 的做法,把相机移到已知位置,检查 \(-R^\top\mathbf t\) 是不是它。

    误区 2:在像素里做畸变,或者弄错系数顺序

    畸变系数是在归一化坐标里定义的:\(r\) 的单位是"焦距",\(r=0.5\) 表示 26.6°,与分辨率无关。如果把 \(r\) 当成像素半径,同一组 \(k_1\) 在 640×480 和 1280×960 下就得到完全不同的弯曲。这也是为什么缩放图像时 D 不变而 K 要变。顺序同样致命:OpenCV 是 \((k_1,k_2,p_1,p_2,k_3)\),把 \(p_1\) 当成 \(k_3\) 不会报错,只会让边缘几十个像素出错。

    误区 3:K 用错了分辨率,或者以为主点就是图像中心

    K 属于一个具体的分辨率、裁剪窗口和像素 binning 设置。换了任何一个而没有按 \(K'=AK\) 更新,点云的尺度(缩放)或位置(裁剪)就是错的,而且错得"很整齐",不容易被肉眼发现,FIG 1 的红色虚线展示了这一点。反方向的错误是假设 \(c=(w/2,h/2)\)、\(f_x=f_y\):真实相机的主点常常偏离中心几到几十个像素,偏差对近处物体的影响 \(\Delta X=\Delta c\cdot Z/f\) 随深度线性增长。而且 OpenCV 与"面积型"约定差半个像素。

    误区 4:以为畸变有闭式逆,或者在标定范围之外使用它

    去畸变需要迭代(FIG 4),迭代次数太少(比如只迭代 1 次)会在图像边缘留下几个像素的残差。更隐蔽的是外推:标定板一般只覆盖图像的中间部分,边角没有数据时,高阶项 \(k_2,k_3\) 会拟合出一个在中间很准、边缘乱飞的多项式。一旦越过折返半径,去畸变在数学上就没有答案。对策:让标定板覆盖到图像的边角,限制 \(k_3\)(很多人固定 \(k_3=0\)),并检查 \(\mathrm d r_d/\mathrm d r>0\) 在整幅图像内成立。

    误区 5:OpenGL 约定喂给 OpenCV 函数,深度和距离不分

    渲染器、NeRF 数据集(如 Blender 导出的位姿)常用 OpenGL 相机系,用它去调 OpenCV 的投影函数会得到上下颠倒、前后不分的结果:先右乘 \(\mathrm{diag}(1,-1,-1)\) 再用。图像里 \(v\) 是向下的,矩阵画成 \(y\) 向上的图时也容易再翻一次。另外,深度图存的是沿光轴的 \(Z\) 还是到相机中心的距离,要看传感器的文档;混用的后果是边缘的点比中心的点多"鼓"出 \(1/\cos\) 倍。

    8小练习

    1. \(K=\begin{bmatrix}800&0&320\\0&800&240\\0&0&1\end{bmatrix}\),相机系中的点 \(\mathbf X_c=(0.3,\,-0.15,\,2)\)(单位 m)落在哪个像素?
    先除以深度:\(x=0.3/2=0.15\),\(y=-0.15/2=-0.075\)。再乘 K:\(u=800\cdot0.15+320=440\),\(v=800\cdot(-0.075)+240=180\)。\(y<0\) 在光轴上方(y 向下),所以 \(v\) 比 \(c_y\) 小。B 忘了除以深度(\(800\cdot0.3+320=560\));C 的符号全反了;D 的 \(v\) 符号反了。
    2. 外参 \(R=\begin{bmatrix}0&-1&0\\1&0&0\\0&0&1\end{bmatrix}\),\(\mathbf t=(1,2,0)\)(世界 \(\to\) 相机)。相机中心在世界系的哪里?
    \(C=-R^\top\mathbf t\)。\(R^\top=\begin{bmatrix}0&1&0\\-1&0&0\\0&0&1\end{bmatrix}\),\(R^\top\mathbf t=(2,-1,0)\),取负得 \((-2,1,0)\)。验算:\(RC+\mathbf t=(0\cdot(-2)-1\cdot1,\ 1\cdot(-2),\,0)+(1,2,0)=(0,0,0)\)。A 是 \(-\mathbf t\)(只在 \(R=I\) 时对),B 是 \(\mathbf t\) 本身,D 是 \(-R\mathbf t\)(用了 \(R\) 而不是 \(R^\top\))。
    3. 640×480 图像的内参 \(f_x=f_y=500\),\(c=(320,240)\)。取出正中间 320×240 的窗口(左上角在 \((160,120)\),不缩放)。新的内参是?
    裁剪只平移像素坐标:\(c'=c-(x_0,y_0)=(160,120)\),焦距(像素/单位斜率)不变,视场角缩小了,因为同一个 \(f\) 现在只覆盖更少的像素。A 是把图像缩小一半(那样视场角不变);C 忘了更新主点;D 没有道理。
    4. 只有 \(k_1=-0.2\),\(f=500\),归一化坐标 \((0.6,\,0)\) 的点,它的水平像素偏移 \(u-c_x\) 是多少?
    \(r^2=0.36\),\(\rho=1-0.2\cdot0.36=0.928\),\(x_d=0.6\cdot0.928=0.5568\),\(u-c_x=500\cdot0.5568\approx278\)。\(k_1<0\) 是桶形:点被拉向中心。C 是 \(k_1>0\)(枕形)的结果。畸变在归一化坐标里作用,再乘 \(f\)。
    5. 一个渲染器(OpenGL 相机系:x 右,y 上,看向 \(-z\))给出相机系坐标 \((1,\,2,\,-5)\)。同一点在 OpenCV 相机系里是?
    OpenCV 的 y 向下、z 向前,OpenGL 的 y 向上、z 向后,两者差 \(\mathrm{diag}(1,-1,-1)\):\((1,2,-5)\mapsto(1,-2,5)\)。结果 \(z=5>0\),点在相机前方,符合预期(在 OpenGL 里在前方的点 \(z<0\))。A 只翻了 z,C 把 x 也翻了,D 什么也没做。

    9延伸阅读

    • R. Hartley & A. Zisserman, Multiple View Geometry in Computer Vision, 2nd ed., Cambridge University Press, 2004, Ch. 6 (Camera Models) — 有限相机、\(P=K[R|t]\)、相机中心与主点、内参的 5 个自由度;第 7 章是如何从点对应估计 P
    • R. Szeliski, Computer Vision: Algorithms and Applications, Ch. 2 (Image Formation), Sec. 2.1 — 3D 到 2D 的投影、相机内参,以及镜头畸变
    • Y. Ma, S. Soatto, J. Košecká & S. S. Sastry, An Invitation to 3-D Vision, Springer, 2004, Ch. 3 — 图像形成与内参矩阵的另一种推导
    • D. C. Brown, "Decentering distortion of lenses", Photogrammetric Engineering 32(3), 444–462, 1966; A. E. Conrady, "Decentred lens-systems", Monthly Notices of the Royal Astronomical Society 79, 384–390, 1919 — 径向与偏心(切向)畸变模型的来源
    • J. Kannala & S. S. Brandt, "A generic camera model and calibration method for conventional, wide-angle, and fish-eye lenses", IEEE TPAMI 28(8), 1335–1340, 2006 — 通用的鱼眼镜头模型(OpenCV fisheye 的基础)
    • Z. Zhang, "A flexible new technique for camera calibration", IEEE TPAMI 22(11), 1330–1334, 2000 — 用平面棋盘格标定 K 与畸变(10.4 节详述)
    • OpenCV documentation, "Camera Calibration and 3D Reconstruction" (calib3d) — 针孔与鱼眼模型、畸变公式、undistortPoints 与 initUndistortRectifyMap 的定义
    • ROS REP 103 (Standard Units of Measure and Coordinate Conventions) and REP 105 (Coordinate Frames for Mobile Platforms) — 本体系与光学系约定,map/odom/base_link