畸变矫正、投影与重映射(图像拼接系列 · 第 3 篇)
本文介绍了如何利用相机标定得到的参数K和D对畸变图像进行矫正,生成无畸变图像。主要内容包括:1)详细解析了矫正流程,即从畸变像素到归一化坐标、去畸变、再投影到理想像素的完整步骤;2)对比了前向映射和反向映射两种方法,说明工程实践中多采用反向映射结合插值的原因;3)介绍了预计算查找表(LUT)的优化方法,通过提前存储映射关系大幅提升处理速度;4)提供了OpenCV中undistort和remap函数
上篇讲了畸变模型 k1,k2,k3k_1,k_2,k_3k1,k2,k3、p1,p2p_1,p_2p1,p2,以及棋盘标定如何得到 KKK 和 DDD。本篇承接这条链条,回答:有了 KKK 和 DDD,如何得到「无畸变」图像? 同时说明 典型拼接实现 的柱面/透视投影与经典 undistort 的差异。
本篇会回答:
- 矫正流程是什么?像素 → 归一化 → 去畸变 → 再投影
- 前向 vs 反向映射,为何工程多用反向 + 插值?
- LUT 的动机:逐像素算太慢,预计算查找表
- OpenCV
undistort/remap怎么用? - 典型拼接实现 的柱面投影、透视校正与经典 undistort 有何不同?
0 动机:标定得到 K/D 后,如何得到「无畸变」图像?
上篇的畸变模型定义在 归一化平面 上:(xn,yn)⇒(xd,yd)(x_n,y_n) \Rightarrow (x_d,y_d)(xn,yn)⇒(xd,yd)。标定完成后,我们得到 KKK 和 DDD,但原始图像上的像素仍是畸变的。要用于拼接、SLAM、三维重建等,需要把畸变像素映射到 理想针孔模型 下的像素。
两类需求:
| 需求 | 说明 |
|---|---|
| 畸变矫正(undistort) | 输入畸变图像,输出无畸变图像,仍为平面透视投影 |
| 投影变换(warp) | 把平面图像投影到柱面、球面等目标坐标系,用于全景拼接 |
本篇先讲 畸变矫正 的通用流程(像素 → 归一化 → 去畸变 → 再投影),再讲 投影变换 与 典型拼接实现 的实现。
1 矫正流程:像素 → 归一化 → 去畸变 → 再投影
矫正的完整流程可以拆成四步。
1.0 流程概览
畸变图像像素 (u,v) 归一化坐标 去畸变后 理想像素 (u',v')
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ (u,v) │ K⁻¹ │ (x_d,y_d) │ │ (x_n,y_n) │ │ (u',v') │
│ 畸变像素 │ ──────> │ 畸变归一化 │ ─>│ 理想归一化 │ ─>│ 理想像素 │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
↑ ↑
│ 畸变公式 │ K
│ (前向) │
└──────────────────┘
矫正时需反向:x_d,y_d → x_n,y_n
1.1 第一步:像素 → 归一化(去内参)
给定畸变图像上的像素 (u,v)(u,v)(u,v),先去掉内参 KKK,得到归一化平面上的坐标:
[xdyd1]=K−1[uv1] \begin{bmatrix} x_d \\ y_d \\ 1 \end{bmatrix} = K^{-1} \begin{bmatrix} u \\ v \\ 1 \end{bmatrix} xdyd1 =K−1 uv1
即 xd=(u−cx)/fxx_d = (u - c_x)/f_xxd=(u−cx)/fx,yd=(v−cy)/fyy_d = (v - c_y)/f_yyd=(v−cy)/fy。这里的 (xd,yd)(x_d, y_d)(xd,yd) 是 畸变后 的归一化坐标(上篇符号)。
1.2 第二步:去畸变(反向)
上篇的畸变公式是 前向:(xn,yn)→(xd,yd)(x_n,y_n) \to (x_d,y_d)(xn,yn)→(xd,yd)。矫正需要 反向:给定 (xd,yd)(x_d,y_d)(xd,yd),求 (xn,yn)(x_n,y_n)(xn,yn)。
径向畸变:(xd,yd)=L(r)(xn,yn)(x_d,y_d) = L(r)(x_n,y_n)(xd,yd)=L(r)(xn,yn),其中 r2=xn2+yn2r^2 = x_n^2 + y_n^2r2=xn2+yn2,L(r)=1+k1r2+k2r4+k3r6L(r) = 1 + k_1 r^2 + k_2 r^4 + k_3 r^6L(r)=1+k1r2+k2r4+k3r6。反向即求 rd=xd2+yd2r_d = \sqrt{x_d^2 + y_d^2}rd=xd2+yd2,再求 rnr_nrn 使得 rd=rnL(rn)r_d = r_n L(r_n)rd=rnL(rn),最后 (xn,yn)=(xd,yd)/L(rn)(x_n,y_n) = (x_d,y_d) / L(r_n)(xn,yn)=(xd,yd)/L(rn)。该方程无闭式解,需 迭代 或 多项式近似。
迭代法(OpenCV 常用):初值 rn(0)=rdr_n^{(0)} = r_drn(0)=rd,迭代 rn(k+1)=rd/L(rn(k))r_n^{(k+1)} = r_d / L(r_n^{(k)})rn(k+1)=rd/L(rn(k)),收敛后得到 (xn,yn)(x_n,y_n)(xn,yn)。
1.3 第三步:再投影到理想像素
得到理想归一化坐标 (xn,yn)(x_n,y_n)(xn,yn) 后,乘以内参 KKK 得到理想像素:
[u′v′1]∼K[xnyn1] \begin{bmatrix} u' \\ v' \\ 1 \end{bmatrix} \sim K \begin{bmatrix} x_n \\ y_n \\ 1 \end{bmatrix} u′v′1 ∼K xnyn1
即 u′=fxxn+cxu' = f_x x_n + c_xu′=fxxn+cx,v′=fyyn+cyv' = f_y y_n + c_yv′=fyyn+cy。
1.4 小结:矫正的数学链
| 步骤 | 变换 | 公式 |
|---|---|---|
| 1 | 像素 → 归一化 | (xd,yd,1)⊤=K−1(u,v,1)⊤(x_d,y_d,1)^\top = K^{-1}(u,v,1)^\top(xd,yd,1)⊤=K−1(u,v,1)⊤ |
| 2 | 去畸变(反向) | (xd,yd)→(xn,yn)(x_d,y_d) \to (x_n,y_n)(xd,yd)→(xn,yn),迭代或 LUT |
| 3 | 再投影 | (u′,v′,1)⊤∼K(xn,yn,1)⊤(u',v',1)^\top \sim K(x_n,y_n,1)^\top(u′,v′,1)⊤∼K(xn,yn,1)⊤ |
符号:(u,v)(u,v)(u,v) 畸变像素;(xd,yd)(x_d,y_d)(xd,yd) 畸变归一化坐标;(xn,yn)(x_n,y_n)(xn,yn) 理想归一化坐标;(u′,v′)(u',v')(u′,v′) 理想像素。
2 前向 vs 反向映射:为何工程多用“反查原图”
生成无畸变图像时,有两种思路:前向映射 和 反向映射。
2.1 前向映射
对畸变图像的每个像素 (u,v)(u,v)(u,v),计算它对应的理想像素 (u′,v′)(u',v')(u′,v′),把颜色写到输出图的 (u′,v′)(u',v')(u′,v′)。
问题:(u′,v′)(u',v')(u′,v′) 往往不是整数,且多个 (u,v)(u,v)(u,v) 可能映射到同一 (u′,v′)(u',v')(u′,v′) 附近,产生空洞或重叠,需要额外处理。
2.2 反向映射(常用)
对输出图的每个像素 (u′,v′)(u',v')(u′,v′),反推它在畸变图像上该采样的位置 (u,v)(u,v)(u,v),用插值得到颜色,写入 (u′,v′)(u',v')(u′,v′)。
优点:
- 输出图每个像素有且仅有一个来源,无空洞
- 插值在源图上做,实现简单(双线性、双三次等)
- OpenCV
remap就是反向映射 + 插值
流程:对每个 (u′,v′)(u',v')(u′,v′) → (xn,yn)(x_n,y_n)(xn,yn) → (xd,yd)(x_d,y_d)(xd,yd)(畸变公式前向)→ (u,v)=K(xd,yd,1)⊤(u,v) = K(x_d,y_d,1)^\top(u,v)=K(xd,yd,1)⊤,在 (u,v)(u,v)(u,v) 处插值采样。
2.3 反向映射为何只需前向畸变?
反向映射时,对每个输出像素 (u′,v′)(u',v')(u′,v′),要找源图上的采样位置 (u,v)(u,v)(u,v)。输出为无畸变图,故 (u′,v′)(u',v')(u′,v′) 对应理想归一化坐标 (xn,yn)=K−1(u′,v′,1)⊤(x_n,y_n) = K^{-1}(u',v',1)^\top(xn,yn)=K−1(u′,v′,1)⊤。源图有畸变,其像素 (u,v)(u,v)(u,v) 是由理想点 (xn,yn)(x_n,y_n)(xn,yn) 经 前向畸变 得到的:(xd,yd)=distort(xn,yn)(x_d,y_d) = \text{distort}(x_n,y_n)(xd,yd)=distort(xn,yn),(u,v)=K(xd,yd,1)⊤(u,v) = K(x_d,y_d,1)^\top(u,v)=K(xd,yd,1)⊤。
因此反向映射的完整链为:
(u′,v′)→K−1(xn,yn)→前向畸变(xd,yd)→K(u,v) (u',v') \xrightarrow{K^{-1}} (x_n,y_n) \xrightarrow{\text{前向畸变}} (x_d,y_d) \xrightarrow{K} (u,v) (u′,v′)K−1(xn,yn)前向畸变(xd,yd)K(u,v)
只需前向畸变公式,无需迭代求逆。这就是工程上偏爱反向映射的原因之一:计算简单、无迭代。
2.4 小结:这两种“反向”各管哪摊事
到这里可以先做一个小结,把“第 1 节的去畸变逆”和“本节的反向映射”放在一起看:
| 场景 | 出发点 | 终点 | 是否要解畸变逆? |
|---|---|---|---|
| 第 1 节:理论上的去畸变链 | 畸变像素 (u,v)(u,v)(u,v) | 理想像素 (u′,v′)(u',v')(u′,v′) | 是,要在归一化平面上做 (xd,yd)→(xn,yn)(x_d,y_d)\to(x_n,y_n)(xd,yd)→(xn,yn) |
| 本节:工程里的反向映射 | 输出像素 (u′,v′)(u',v')(u′,v′) | 原图像素 (u,v)(u,v)(u,v) | 否,只需用前向畸变 (xn,yn)→(xd,yd)(x_n,y_n)\to(x_d,y_d)(xn,yn)→(xd,yd) |
后面讲 LUT 和 remap 时,默认采用的是“第 2 行”这种做法:
把输出图看成理想图,从理想那一端出发,反查回原图的采样位置。
3 LUT 的动机:逐像素算太慢,预计算查找表
即使用反向映射 + 前向畸变,每个输出像素仍要算一次 K−1K^{-1}K−1、畸变、KKK。若输出 1920×1080,就要 200 多万次。预计算查找表(LUT) 可大幅加速。
3.1 思路
对输出图每个像素 (i,j)(i,j)(i,j),预计算对应的源图采样坐标 (mapx(i,j),mapy(i,j))(map_x(i,j), map_y(i,j))(mapx(i,j),mapy(i,j)),存成两个与输出同尺寸的浮点图。运行时只需一次 remap(img, map_x, map_y, ...),按 LUT 做插值采样,无需再算畸变。
从坐标系角度看,LUT 里存的是整条几何链的结果:
(i,j) → (u',v') → (x_n,y_n) → (x_d,y_d) → (u,v)
输出像素 理想归一化 畸变归一化 原图像素
当输出图是柱面 / 球面坐标时,只需要把链条中间那一段换成对应的投影公式即可。
3.2 典型用法
cv::Mat map1, map2;
cv::initUndistortRectifyMap(cameraMatrix, distCoeffs, cv::Mat(), cameraMatrix,
imageSize, CV_32FC1, map1, map2);
cv::remap(src, dst, map1, map2, cv::INTER_LINEAR);
initUndistortRectifyMap 预计算 LUT,remap 按 LUT 重采样。同一相机、同一分辨率,LUT 只需算一次,可复用。
3.3 分辨率与精度
LUT 分辨率通常等于输出分辨率。若输出图会缩放,可对 LUT 插值,或按输出尺寸重新计算。LUT 存的是源图坐标,用 CV_32FC1 足够;双通道时 map1 存 uuu,map2 存 vvv。
4 OpenCV undistort / remap 的典型用法
4.1 一站式 undistort
cv::undistort(src, dst, cameraMatrix, distCoeffs);
内部等价于:initUndistortRectifyMap + remap,且默认用 cameraMatrix 作为新相机矩阵(即去畸变后内参不变)。若要去畸变的同时做去畸变 + 裁剪,可用 undistort 的 P 参数或 initUndistortRectifyMap 自定义新 KKK。
4.2 分步 remap(需自定义映射时)
cv::Mat map1, map2;
cv::initUndistortRectifyMap(K, D, R, P, size, CV_32FC1, map1, map2);
cv::remap(src, dst, map1, map2, cv::INTER_LINEAR, cv::BORDER_CONSTANT);
R 为可选的旋转(立体校正时用),P 为新相机矩阵。单目去畸变时 R 为单位阵,P 常取 K。
4.3 输出边界
去畸变后,图像四角可能映射到源图外,产生黑边。可设置 BORDER_CONSTANT 填常数,或用 BORDER_REPLICATE 等。若想裁剪掉黑边,可计算有效区域,用 getOptimalNewCameraMatrix 的 alpha 参数调节。
5 典型拼接实现的投影:柱面与透视校正
典型拼接实现 没有 传统意义上的畸变矫正(无 DDD、无 undistort)。它的「变换」主要是 柱面投影 和 透视校正,用于把多张平面图统一到柱面/平面坐标系,便于拼接。
如果和前两节对比,可以这样理解它在整条流程中的位置:
(本节) 选择一个更适合拼接的“公共投影面”(平面 / 柱面 / 球面)
↓
前两节 在这个公共投影面上,用反向映射 + 插值生成输出图
↓
后续篇 在公共投影面上做特征匹配、MST 初始化、BA 和融合
5.1 柱面投影的几何
柱面投影把图像平面上的点投影到「以相机为轴、半径为 rrr 的圆柱」上。设图像中心为 (cx,cy)(c_x, c_y)(cx,cy),点 (x,y)(x,y)(x,y) 在图像上,则:
前向(图像 → 柱面):
θ=arctanx−cxr,y′=(y−cy)⋅r(x−cx)2+r2 \theta = \arctan\frac{x - c_x}{r}, \quad y' = \frac{(y - c_y) \cdot r}{\sqrt{(x-c_x)^2 + r^2}} θ=arctanrx−cx,y′=(x−cx)2+r2(y−cy)⋅r
反向(柱面 → 图像,用于重采样):
x=rtanθ+cx,y=y′⋅rcosθ+cy x = r \tan\theta + c_x, \quad y = \frac{y' \cdot r}{\cos\theta} + c_y x=rtanθ+cx,y=cosθy′⋅r+cy
符号:rrr 圆柱半径,与焦距相关;(cx,cy)(c_x, c_y)(cx,cy) 投影中心;(θ,y′)(\theta, y')(θ,y′) 柱面坐标。
5.2 柱面投影的伪代码
前向(图像 → 柱面):
proj(x, y):
θ ← atan((x - cx) / r)
y' ← (y - cy) / sqrt((x - cx)² + r²)
return (θ, y')
反向(柱面 → 图像,用于重采样):
proj_r(θ, y'):
x ← r * tan(θ) + cx
y ← y' * r / cos(θ) + cy
return (x, y)
圆柱半径 rrr 由 35mm 等效焦距换算:r≈hypot(w,h)⋅f35/43.266r \approx \text{hypot}(w, h) \cdot f_{35} / 43.266r≈hypot(w,h)⋅f35/43.266。对整张图做柱面投影时,用 反向映射:对输出每个像素,用 proj_r 反推源图坐标,再双线性插值采样。
5.3 透视校正
柱面拼接后,全景图是「弯曲」的(柱面展开)。透视校正用透视变换把四角拉成矩形,使输出更接近「平铺」的宽幅图。流程:
- 取首尾图像的四个角点,经单应 HHH 变换到参考帧,再投影到柱面坐标
- 计算从当前四角到标准矩形的单应(4 点法)
- 按该单应重采样,得到矩形输出
5.4 投影类型:flat、cylindrical、spherical
| 类型 | 说明 |
|---|---|
| flat | 平面透视,p=[x/z,y/z]p = [x/z, y/z]p=[x/z,y/z] |
| cylindrical | 柱面,θ=atan2(x,z)\theta = \text{atan2}(x,z)θ=atan2(x,z),y′=y/x2+z2y' = y / \sqrt{x^2+z^2}y′=y/x2+z2 |
| spherical | 球面(equirectangular),θ=atan2(x,z)\theta = \text{atan2}(x,z)θ=atan2(x,z),ϕ=atan2(y,x2+z2)\phi = \text{atan2}(y, \sqrt{x^2+z^2})ϕ=atan2(y,x2+z2) |
柱面拼接模式下,单张图先做柱面投影,再在 flat 投影下做范围估计与融合。
5.5 柱面模式下的 Affine 与 half-shifted 坐标
柱面投影后,相邻图的变换近似仿射(缩放、旋转、剪切),用 Affine 而非 Homography 更稳定、参数更少。
half-shifted 坐标:单应 HHH 将图 jjj 变换到图 iii 时,坐标在 [−w/2,w/2][-w/2, w/2][−w/2,w/2] 下,即原点在图像中心,而非左上角。渲染前需将 homo 从 half-shifted 转为图像坐标。
6 与第 1 篇的衔接:投影后仍满足针孔模型
第 1 篇的 H=KRK−1H = K R K^{-1}H=KRK−1 假设 理想针孔、无畸变。典型拼接实现 的柱面投影是在 图像平面到柱面 的映射,相当于在针孔投影之后又做了一层「平面 → 柱面」的几何变换。柱面投影后的图像,可视为在「柱面坐标系」下的针孔投影;后续匹配、单应估计仍可沿用 HHH、RRR 等概念,只是坐标系从平面换成了柱面。
总结:畸变矫正是「归一化平面上去畸变」,使图像满足理想针孔;柱面/球面投影是「换一个目标坐标系」,投影后在该坐标系下仍可视为针孔成像的某种展开。
7 常见坑:LUT 分辨率、边界、GPU 加速
| 坑点 | 说明 | 建议 |
|---|---|---|
| LUT 分辨率 | LUT 与输出尺寸绑定,缩放输出需重算或插值 LUT | 按实际输出尺寸生成 LUT |
| 边界黑边 | 去畸变后四角可能越界,产生黑边 | 用 getOptimalNewCameraMatrix 的 alpha 或手动裁剪 |
| 插值方式 | INTER_NEAREST 快但锯齿,INTER_LINEAR 常用,INTER_CUBIC 更平滑 |
按质量/速度权衡选择 |
| GPU 加速 | OpenCV cuda::remap 可加速,需 GPU 模块 |
大图、实时场景可考虑 |
| 柱面半径 rrr | rrr 与焦距相关,焦距不准会导致柱面「过弯」或「过平」 | 用估计或标定获取焦距 |
| half-shifted | homo 在 [−w/2,w/2][-w/2,w/2][−w/2,w/2] 下,与像素坐标混用会错 | 渲染前需做坐标转换 |
8 下一篇预告
矫正/投影后,图像满足(近似)针孔模型,可以建立图间对应关系。
下一篇:
《特征匹配与单应估计(RANSAC)》
将讲:
- 特征点与描述子
- 匹配与比率检验
- 单应 HHH 的线性估计(4 点法、DLT)
- RANSAC 与从 HHH 求 RRR
本篇总结
- 矫正流程:像素 → 归一化 → 去畸变(反向需迭代)→ 再投影;反向映射时用前向畸变公式,无需迭代
- LUT:预计算
map_x、map_y,remap时按 LUT 插值,避免每像素重复计算 - OpenCV:
undistort一站式,initUndistortRectifyMap+remap可自定义 - 典型拼接实现:无传统 undistort,用柱面投影统一坐标系;柱面模式下用 Affine;homo 在 half-shifted 坐标 [−w/2,w/2][-w/2,w/2][−w/2,w/2] 下
自测
- 反向映射时,对输出像素 (u′,v′)(u',v')(u′,v′),如何得到源图采样坐标 (u,v)(u,v)(u,v)?(答:(xn,yn)=K−1(u′,v′)(x_n,y_n) = K^{-1}(u',v')(xn,yn)=K−1(u′,v′),(xd,yd)=distort(xn,yn)(x_d,y_d) = \text{distort}(x_n,y_n)(xd,yd)=distort(xn,yn),(u,v)=K(xd,yd)(u,v) = K(x_d,y_d)(u,v)=K(xd,yd))
- 柱面投影的反向映射
proj_r输入输出分别是什么坐标系?(答:输入柱面坐标 (θ,y′)(\theta, y')(θ,y′),输出图像平面坐标 (x,y)(x,y)(x,y))
符号速查(本篇新增)
| 符号 | 含义 |
|---|---|
| (u,v)(u,v)(u,v) | 畸变图像像素 |
| (u′,v′)(u',v')(u′,v′) | 理想/输出图像像素 |
| (xd,yd)(x_d,y_d)(xd,yd) | 畸变后归一化坐标 |
| (xn,yn)(x_n,y_n)(xn,yn) | 理想归一化坐标 |
| rrr | 柱面投影的圆柱半径 |
| (θ,y′)(\theta, y')(θ,y′) | 柱面坐标 |
| LUT | 查找表,存 map_x、map_y |
更多推荐
所有评论(0)