2. 畸变校正数学基础:针孔模型、径向畸变与切向畸变模型

聊到内窥镜的畸变校正,咱们得先回到最基础的成像模型上。说白了,就是搞清楚「理想情况下图像应该长什么样」,然后才能去修正那些「不理想」的变形。

我个人习惯,每次拿到一个新的内窥镜镜头,第一件事不是急着调算法,而是先理解它的数学模型。你想想看,连相机是怎么成像的都没搞明白,怎么去校正畸变呢?

2.1 针孔模型:理想世界的成像法则

针孔模型,是所有相机模型的起点。它假设光线通过一个无限小的孔,在成像平面上投影。虽然现实中不存在完美的针孔相机,但这个模型给了我们一个「理想基准」。

数学上,针孔模型描述的是三维空间点 P(X, Y, Z) 到二维图像点 p(u, v) 的映射关系。公式长这样:

u = f * (X / Z) + cx
v = f * (Y / Z) + cy

这里 f 是焦距,(cx, cy) 是图像中心(也叫主点)。

嗯,这里要注意:这个公式假设镜头是完美的,光线是直线传播的。但现实中的内窥镜镜头,尤其是广角镜头,光线经过透镜组后会发生偏折。这就引出了畸变。

核心理解:针孔模型是「理想状态」,畸变是「现实偏差」。我们的任务就是找到这个偏差,然后把它消除。

2.2 径向畸变:镜头弯曲带来的「桶形」与「枕形」

径向畸变,是内窥镜图像中最常见、也最明显的畸变。我在项目中遇到过很多次,尤其是那些大视场角的镜头,拍出来的图像边缘都是弯的。

为什么会这样?因为光线通过透镜边缘时,折射角度和中心区域不一样。离中心越远,变形越严重。

径向畸变主要分两种:

  • 桶形畸变:图像边缘向外凸出,像木桶的形状。内窥镜广角镜头最常见。
  • 枕形畸变:图像边缘向内凹陷,像枕头。长焦镜头容易出现。

数学上,我们用多项式来建模径向畸变。假设 (x, y) 是理想坐标,(x_distorted, y_distorted) 是实际畸变后的坐标:

x_distorted = x * (1 + k1 * r^2 + k2 * r^4 + k3 * r^6)
y_distorted = y * (1 + k1 * r^2 + k2 * r^4 + k3 * r^6)

其中 r^2 = x^2 + y^2k1, k2, k3 是径向畸变系数。

我的经验:对于大多数内窥镜镜头,只用 k1 和 k2 就够用了。k3 通常用于鱼眼镜头那种极端畸变。我曾经为了省事,直接用了三个参数,结果过拟合了,校正后的图像反而出现奇怪的波纹。后来我学乖了,先试两个参数,不行再加。

2.3 切向畸变:镜头装配误差带来的「倾斜」

切向畸变,说白了就是镜头和成像平面不平行造成的。你想想看,镜头在装配时,如果有一点点倾斜,光线穿过透镜后就会产生切向的偏移。

这种畸变不像径向畸变那么明显,但在高精度测量场景下,绝对不能忽略。我记得有一次做手术导航项目,径向畸变校正后图像看起来挺直的,但三维重建的误差就是降不下来。查了半天,最后发现是切向畸变在作怪。

切向畸变的数学模型:

x_distorted = x + (2 * p1 * x * y + p2 * (r^2 + 2 * x^2))
y_distorted = y + (p1 * (r^2 + 2 * y^2) + 2 * p2 * x * y)

这里 p1, p2 是切向畸变系数。

避坑指南:我曾经在标定一个内窥镜时,发现切向畸变系数特别大。一开始以为是镜头质量问题,后来检查发现是标定板没放平。所以,标定前一定要确保标定板与镜头光轴垂直。否则你校正的不是镜头畸变,而是标定板的倾斜。

2.4 完整畸变模型:把两个畸变合在一起

实际应用中,我们通常把径向畸变和切向畸变合并成一个模型。OpenCV 里用的就是这种模型:

x_distorted = x * (1 + k1 * r^2 + k2 * r^4 + k3 * r^6) + (2 * p1 * x * y + p2 * (r^2 + 2 * x^2))
y_distorted = y * (1 + k1 * r^2 + k2 * r^4 + k3 * r^6) + (p1 * (r^2 + 2 * y^2) + 2 * p2 * x * y)

这个模型有 5 个畸变参数:k1, k2, k3, p1, p2。标定的过程,就是通过拍摄多张标定板图像,求解出这些参数。

参数 含义 典型范围
k1, k2, k3 径向畸变系数 -1.0 ~ 1.0
p1, p2 切向畸变系数 -0.1 ~ 0.1

关键点:畸变参数的大小和镜头本身有关。广角内窥镜的 k1 通常是负数(桶形畸变),而长焦镜头可能是正数(枕形畸变)。拿到一个新镜头,先看看它的畸变参数符号,心里就有数了。

2.5 从理论到实践:一个简单的畸变校正流程

理论讲完了,咱们看看实际怎么用。我个人习惯的流程是这样的:

  1. 标定:拍摄 10-20 张不同角度的棋盘格图像,用 OpenCV 的 calibrateCamera() 求出内参和畸变系数。
  2. 生成映射表:initUndistortRectifyMap() 生成从畸变图像到校正图像的映射关系。
  3. 重映射:remap() 对每一帧图像进行校正。

这里给一段简单的代码示例,展示核心步骤:

import cv2
import numpy as np

# 假设你已经通过标定得到了以下参数
camera_matrix = np.array([[fx, 0, cx],
                          [0, fy, cy],
                          [0, 0, 1]], dtype=np.float32)
dist_coeffs = np.array([k1, k2, p1, p2, k3], dtype=np.float32)

# 生成映射表
h, w = image.shape[:2]
mapx, mapy = cv2.initUndistortRectifyMap(
    camera_matrix, dist_coeffs, None, camera_matrix,
    (w, h), cv2.CV_32FC1
)

# 校正图像
undistorted_img = cv2.remap(image, mapx, mapy, cv2.INTER_LINEAR)

小技巧:如果你处理的视频流,建议只生成一次映射表,然后对每一帧重复使用 remap()。这样能省下大量计算时间。我最早做实时内窥镜系统时,每帧都重新生成映射表,结果帧率惨不忍睹。后来改成预生成,帧率直接翻倍。

好了,这一章的内容就到这里。针孔模型是基础,径向畸变和切向畸变是我们要解决的核心问题。下一章,我会带你手把手实现一个完整的标定流程,到时候咱们再细聊。