(一)畸变

定义

对于理想光学系统,在一对共轭的物像平面上,像的放大率是一个常数。但是对于实际的光学系统,当视场较大或很大时,像的放大率就要随视场而异,这样就会使像相对于物体失去相似性。这种使像变形的成像缺陷称为畸变。

畸变定义为实际像高与理想像高差,而在实际应用中经常将其与理想像高之比的百分数来表示畸变,称为相对畸变,即:

分类

通常来说镜头的畸变分为径向畸变和切向畸变两类。图像径向畸变是图像像素点以畸变中心为中心点,沿着径向产生的位置偏差,从而导致图像中所成的像发生形变。径向畸变分为桶形畸变和枕形畸变。

  • 桶形畸变(Barrel Distortion),又称桶形失真,是由镜头中透镜物理性能以及镜片组结构引起的成像画面呈桶形膨胀状的失真现象。我们在使用广角镜头或使用变焦镜头的广角端时,最容易察觉桶形失真现象。

    下面是桶形畸变的真实场景,本该是垂直的房屋,却发生了变形。

    桶形畸变真实场景

  • 枕形畸变(Pincushion Distortion),又称枕形失真,它是由镜头引起的画面向中间“收缩”的现象。我们在使用长焦镜头或使用变焦镜头的长焦端时,最容易察觉枕形失真现象。

下图是镜头畸变的对比示意图,第一幅图像是无畸变,第二幅图像是桶形畸变,第三幅图像是枕形畸变。

镜头畸变的对比示意图

切向畸变是由于透镜本身与相机传感器平面(成像平面)或图像平面不平行而产生的,这种情况多是由于透镜被粘贴到镜头模组上的安装偏差导致。我们一般只考虑径向畸变。

下图是切向畸变的示意图。

切向畸变

产生的原因

来源于博客

畸变的常见原因是因为在镜头系统中引入光圈,光圈的位置决定了失真的类别与程度。如下图:

畸变原因示意图

对于恒定的物体尺寸y,图像尺寸 h有所不同 。

像点的位置由主光线(实线)确定,主光线是穿过光圈中心的光线。当光圈位于透镜上时,也就是中间那幅图像,主光线穿过光学中心,并以与入射角相同的角度离开透镜。这样的系统不会使图像失真,因此没有畸变。

这个时候引入一个镜头常见的像差概念:慧差。

球面镜的慧差

球面镜的慧差,可以看到凸透镜边缘对光线的折射能力更强。

如图所示,当光圈在透镜前面或后面时,主光线并不经过透镜的中心,经过的是透镜的边缘,因此光线会被折射的更厉害一些。当光圈在镜头前面,光线折射更强后,所成的像距离光轴越近了,也就是像缩小了,就造成了桶形失真。复杂的镜头,例如后焦距广角镜头,往往就会出现桶形失真,因为前组镜头会充当后组的光圈。

当光圈在镜头后面,像的高度增加了,也就是放大了,就是枕形失真。远焦镜头的后组为负,会导致枕形失真。

下面这幅图片是对应的三维图,更加清晰的看出光圈与镜头的关系。

畸变原因三维示意图

这里可以引申一个问题,投影仪同样也是利用凸透镜,但是畸变就很小,而相机的畸变就相对比较大。因为它可以采用比较接近对称和比较紧凑的结构,镜头就很少有畸变。

简单的说,镜头畸变是由远离光圈的镜片的球面像差造成的。

如果镜头结构关于光圈基本对称,光圈前后的畸变互相抵消,不会有畸变。
如果镜头镜片都在光圈很近距离内,也不会有多少畸变。
如果远离光圈的镜片校正了球面像差,比如采用非球面镜,也不会产生畸变。
如果光圈放在物方焦平面和像方焦平面上,使主光线平行于光轴,也不会产生畸变(远心镜头)。

畸变矫正的方法

一般情况下,改善畸变有2种办法:

  1. 一种是通过软件算法把镜头的畸变系数(也就是该镜头在当前距离下拍照时的变形特点)计算出来,一般常用的图像处理平台都包含有标定模块,像OpenCV、Halcon、CCAS等;

    软件算法的畸变矫正是通过标定,把像的实际的位置和理想的位置做了一个映射,把图片处理成没有畸变的图片。

    缺点是:

    1. 降低清晰度
    2. 由于矫正后边角损失部分像素,会对图像进行裁剪,视场角减小
    3. 由于需要一直运行算法,增加功耗

    优点是:降低硬件成本

  2. 另一种就是通过光路设计,从镜头硬件本身消除畸变的影响,如双远心镜头就是一种非常典型的“零畸变”镜头,利用非球面镜减小畸变等等。

    优点是不会损失清晰度。不会裁剪图像,也就不会损失视场角,不需要软件处理,降低功耗。缺点是需要定制,比较昂贵。

(二)畸变计算方法

这里遇到一个有趣的事情

为什么公安一所测出的畸变值远大于镜头手册的畸变值?答案是计算方法不同。

光学畸变和tv畸变

先来了解一下光学畸变(也称几何畸变)和tv畸变,他们的计算方法如下图所示。实线是实际成的像,虚线是理想中的像。

光学畸变和TV畸变示意图

光学畸变和tv畸变是有换算关系的,以1080P16:9的CMOS为例,tv畸变是光学畸变的0.38倍。

tv畸变有两种计算方法,SMIA TV 畸变和ISO TV畸变:

tv畸变计算方法

SMIA TV Distortion = ( A-B )/B ; A = ( A1+A2 )/2

ISO TV Distortion = ( A-B )/(2 * B) ; A = ( A1+A2 )/2

所以,SMIA TV畸变=2*ISO TV畸变,上图的tv畸变指是ISO TV畸变。从公式可以看出,TV畸变是基于图像的高度和参考中心线的高度差异,如果需要计算图像水平方向的畸变,可以导入图像之前,先把图像旋转90°再分析。

公安一所的畸变计算方法

拍两条横向直线,让他们与图像的上下边相切。两条直线与图像左侧相交,得到两个坐标,计算出纵向像素差A1,同样的方法可以得到右侧的纵向像素差A2。这两条直线的最大距离,也就是中间的纵向像素差是c,畸变值就是((A1+A2)/2 - B)/B

举例:

用画图工具打开后,可以看到每个点的像素位置,先算出左侧的像素差(881-181=700),右侧的像素差(912-207=705),中间的像素差为1067,就可以计算出畸变值为((700+705)/2 - 1067)/1067 = -0.3416)

区别

同一款镜头,手册上的光学畸变只有21%,理论上的SMIA tv畸变只有16%,但是用一所的方法计算出来却有37%,这是为什么呢?

如上图所示,红线的方法与tv畸变不同的是,它将上边线延长到了图像左右边框,计算出来的Δh明显比tv畸变大得多。一所测试的时候需要找到两条线与上下边框相切,而不是像tv畸变那种处于中间的一条线,因此一所方法得到的畸变值比红线的方法还要大,这样就能解释为什么一所的方法类似tv畸变,却又比tv畸变大得多。因此我们选择镜头的时候,需要先计算一下是否能够真的符合一所的标准。

(三)畸变矫正算法

1. 原理

径向失真和切向失真对畸变的影响

径向畸变的形成原因包括枕形畸变和桶形畸变等,可以用如下表达式来描述:

公式里(x0,y0)是畸变点在成像仪上的原始位置,(x,y)是校正后新的位置。

注意这个公式,(x0,y0)是畸变的原图上像素点的位置;(x,y)是校正后输出图像上像素点的位置。

实现过程是,对输出图的点做遍历——以1080p的图像为例,从点(0,0)到点(1919,1079),一行一行的遍历——依次找到输出点(x, y)对应的原图点(x0, y0)的像素值,再将(x0, y0)的值赋给(x, y)。如果计算出来的对应的原图的点(x0, y0)不是整数,则用二次线性插值计算此点,然后赋值给(x, y)。

切向畸变又分为薄透镜畸变和离心畸变等,薄透镜畸变则是因为透镜存在一定的细微倾斜造成的;离心畸变的形成原因是镜头是由多个透镜组合而成的,而各个透镜的光轴不在同一条中心线上。切向畸变可以用如下数学表达式来描述:

在引入镜头的畸变后,成像点从理想图像坐标系到真实图像坐标系的变换关系可以表示为:

在目前的摄像机标定研究中,对镜头畸变考虑较多的是镜头径向畸变,而忽略了镜头的切向和薄棱镜等其它非线性畸变因素。

实际计算过程中,如果考虑太多高阶的畸变参数,会导致标定求解的不稳定。

操作步骤

1. 图像采集

首先在opencv官网下载棋盘格图像进行打印。

2. 标定

主要过程:用matlab工具箱进行标定,得到畸变矫正参数,并转换成opencv的参数,参考这个博客的步骤

这里主要记录一下,matlab参数与opencv参数的转换:

  1. 在使用opencv中的undistort进行畸变矫正时,需要使用8个参数即fc1, fc2, cc1, cc2, kc1, kc2, kc3, kc4;

  2. MATLAB的参数有三个:

    • RadialDistorion中的参数分别是:kc1,kc2,kc5(不常用)

    • TangentialDistortion中的参数分别是:kc3,kc4

    • IntrinsicMatrix中的参数分别是: [[fc1,无用,0],[无用,fc2,0],[cc1,cc2,1]]

3. 畸变矫正

利用undistort函数

def test_calibration():
# 设置畸变校正的参数值
fc1 = 1.107859597442719e+03
fc2 = 1.110705995095509e+03
cc1 = 9.701443009307349e+02
cc2 = 5.619129804989138e+02
kc1 = -0.321797722411564
kc2 = 0.075142421519568
kc3 = 0
kc4 = 0
cameraMatrix = np.array([
[fc1, 0 , cc1],
[0 , fc2, cc2],
[0 , 0 , 1 ]
])
distCoeffs = np.array([kc1, kc2, kc3, kc4])

# 获取视频中的一帧图像
vidcap = cv2.VideoCapture(video)
success, frame = vidcap.read()
imageSize = (frame.shape[1],frame.shape[0]);

# 畸变校正
frameCalibration = cv2.undistort(frame,cameraMatrix,distCoeffs)

# 绘图
plt.subplot(211)
plt.imshow(frame)
plt.subplot(212)
plt.imshow(frameCalibration)
plt.show()
return map1,map2

4. 算法加速

可以参考博客

主要思路是根据内参和畸变系数,建立了一个查找表(实际位置与理想位置的映射表)

def test_calibration():
...

# 畸变校正
#根据内参和畸变系数,建立了一个查找表,map1 map2设置为全局变量,所以这个函数只需要运行一次就好,不需要重复计算查找表。
newCameraMatrix = cv2.getOptimalNewCameraMatrix(cameraMatrix, distCoeffs, imageSize, 1, imageSize, 0)
map1, map2 = cv2.initUndistortRectifyMap(cameraMatrix, distCoeffs, None, newCameraMatrix[0], imageSize, cv2.CV_16SC2);

# 此函数根据map1 map2查找表进行畸变矫正,同一个镜头下的视频中map1 map2可以重复使用
frameCalibration = cv2.remap(frame, map1, map2, cv2.INTER_LINEAR);

...
return map1,map2

5. 校正效果

下图是没有经过裁剪的畸变校正效果

没有经过裁剪的畸变校正效果