上篇讲了畸变模型 k1,k2,k3k_1,k_2,k_3k1,k2,k3p1,p2p_1,p_2p1,p2,以及棋盘标定如何得到 KKKDDD。本篇承接这条链条,回答:有了 KKKDDD,如何得到「无畸变」图像? 同时说明 典型拼接实现 的柱面/透视投影与经典 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)。标定完成后,我们得到 KKKDDD,但原始图像上的像素仍是畸变的。要用于拼接、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 =K1 uv1

xd=(u−cx)/fxx_d = (u - c_x)/f_xxd=(ucx)/fxyd=(v−cy)/fyy_d = (v - c_y)/f_yyd=(vcy)/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+yn2L(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} uv1 K xnyn1

u′=fxxn+cxu' = f_x x_n + c_xu=fxxn+cxv′=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)=K1(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)=K1(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)K1 (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}K1、畸变、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 足够;双通道时 map1uuumap2vvv


4 OpenCV undistort / remap 的典型用法


4.1 一站式 undistort

cv::undistort(src, dst, cameraMatrix, distCoeffs);

内部等价于:initUndistortRectifyMap + remap,且默认用 cameraMatrix 作为新相机矩阵(即去畸变后内参不变)。若要去畸变的同时做去畸变 + 裁剪,可用 undistortP 参数或 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 等。若想裁剪掉黑边,可计算有效区域,用 getOptimalNewCameraMatrixalpha 参数调节。


5 典型拼接实现的投影:柱面与透视校正

典型拼接实现 没有 传统意义上的畸变矫正(无 DDD、无 undistort)。它的「变换」主要是 柱面投影透视校正,用于把多张平面图统一到柱面/平面坐标系,便于拼接。

如果和前两节对比,可以这样理解它在整条流程中的位置:

(本节)   选择一个更适合拼接的“公共投影面”(平面 / 柱面 / 球面)
        ↓
前两节   在这个公共投影面上,用反向映射 + 插值生成输出图
        ↓
后续篇   在公共投影面上做特征匹配、MST 初始化、BA 和融合

5.1 柱面投影的几何

柱面投影把图像平面上的点投影到「以相机为轴、半径为 rrr 的圆柱」上。设图像中心为 (cx,cy)(c_x, c_y)(cx,cy),点 (x,y)(x,y)(x,y) 在图像上,则:

前向(图像 → 柱面):

θ=arctan⁡x−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}} θ=arctanrxcx,y=(xcx)2+r2 (ycy)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θyr+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.266rhypot(w,h)f35/43.266。对整张图做柱面投影时,用 反向映射:对输出每个像素,用 proj_r 反推源图坐标,再双线性插值采样。


5.3 透视校正

柱面拼接后,全景图是「弯曲」的(柱面展开)。透视校正用透视变换把四角拉成矩形,使输出更接近「平铺」的宽幅图。流程:

  1. 取首尾图像的四个角点,经单应 HHH 变换到参考帧,再投影到柱面坐标
  2. 计算从当前四角到标准矩形的单应(4 点法)
  3. 按该单应重采样,得到矩形输出

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=KRK1 假设 理想针孔、无畸变。典型拼接实现 的柱面投影是在 图像平面到柱面 的映射,相当于在针孔投影之后又做了一层「平面 → 柱面」的几何变换。柱面投影后的图像,可视为在「柱面坐标系」下的针孔投影;后续匹配、单应估计仍可沿用 HHHRRR 等概念,只是坐标系从平面换成了柱面。

总结:畸变矫正是「归一化平面上去畸变」,使图像满足理想针孔;柱面/球面投影是「换一个目标坐标系」,投影后在该坐标系下仍可视为针孔成像的某种展开。


7 常见坑:LUT 分辨率、边界、GPU 加速

坑点 说明 建议
LUT 分辨率 LUT 与输出尺寸绑定,缩放输出需重算或插值 LUT 按实际输出尺寸生成 LUT
边界黑边 去畸变后四角可能越界,产生黑边 getOptimalNewCameraMatrixalpha 或手动裁剪
插值方式 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 与从 HHHRRR

本篇总结

  1. 矫正流程:像素 → 归一化 → 去畸变(反向需迭代)→ 再投影;反向映射时用前向畸变公式,无需迭代
  2. LUT:预计算 map_xmap_yremap 时按 LUT 插值,避免每像素重复计算
  3. OpenCVundistort 一站式,initUndistortRectifyMap + remap 可自定义
  4. 典型拼接实现:无传统 undistort,用柱面投影统一坐标系;柱面模式下用 Affine;homo 在 half-shifted 坐标 [−w/2,w/2][-w/2,w/2][w/2,w/2]

自测

  1. 反向映射时,对输出像素 (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)=K1(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)
  2. 柱面投影的反向映射 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_xmap_y
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐