OpenCV-14-相机模型与标定

1 前言

对现实世界的光线感受最终形成视觉,这些光线从一些光源发出,如灯泡和太阳,它们穿越空间直至碰到一些物体。然后大多数光被物体吸收,剩下的光反射进入了我们的眼睛或者相机,最终被视网膜或者成像器收集,从而形成图像。这些过程中的几何学,尤其是光的传播对于计算机视觉而言尤其重要。

一个演示该过程的简单有效模型是针孔相机(Pinhole Camera)模型,针孔相机基本模型是一个在中心有着很小孔洞的虚拟墙壁,光线只能通过小孔透过。本章我们首先会通过该模型介绍投影光线的基本几何学知识。遗憾的是,真正的针孔并不是一个很好的成像方式,因为它不能够收集到足够的光线。这也是为什么我们的眼睛和相机都使用了透镜收集更多的光线。但是这种方式也有缺点,它会使用到更复杂的几何学知识,并且还会因为透镜本身引入畸变(Distoritions)现象。

本章我们会学习通过数学手段,如何使用相机校准技术(Mathematically)消除因使用透镜导致的,和使用简单针孔模型的主要误差。相机校准对于联系相机测量和真实三维世界中的测量也很重要,因为场景不仅仅是三维的,并且它们还是有着物理单位的物理空间。因此相机自然单位(像素)和物理世界单位(如米)是重建三维场景的关键组成部分。

相机校准的过程不仅给出了相机的几何模型,还给出了透镜的畸变模型(Distortion Model)。这两个包含丰富信息的模型定义了相机的内在参数(Intrinsic Parameters)。本章我们将使用这些模型来校准透镜畸变。下一章讲使用它们来阐述物理场景的整个几何结构。

首先我们会讨论相机模型和透镜畸变的原因,然后会讲到单应变换,它是能够反应相机基本行为、畸变和校准特征的数学工具。接下来会仔细讨论这种能够归纳特定相机特征的变换是如何通过数学方法被计算出来的。最后会介绍OpenCV中提供的相关函数,以及如何使用它们来完成特定任务。

本章阐述的所有知识都是为了让你有足够的理论知识储备,从而能够真正理解函数cv::calibrateCamera(),以及它内部的原理。如果你想要正确的使用该函数,这些知识是必要的。当然如果你已经是这方面的专家,并且只想知道OpenCV中是如何实现这些你已经了解到知识的话,那么你可以直接跳到Calibration Function部分。另外在附录B的ccalib部分还介绍了更多的校准模式和技术。

2 相机模型

首先介绍最简单的针孔相机模型,物体每个点只发出一条光线穿过针孔,并投影到成像平面(Image Plane,也称Projective Plane)上。图像平面和光轴的交点称为焦点,需要这里的焦点和后文讲到的透镜的焦点含义并不相同。下图是一个理想的针孔相机。

则针孔到成像平面的距离f正好是相机的焦距(Focal Length)(其实应该称为投影距离, Projection Distance),Z是物体到相机的距离即物距,X和x分别表示物体和图像的长度,则他们之间满足如下关系。

接下来使用一个等效的模型来演示针孔成像,从而让我们更容易理解其中的数学原理。在下图中,将成像平面放在小孔平面和物体之间,仍然假定物体的每个点只有一束光线射向针孔,即投影中心,这样光线和成像平面点交点就能显示出图像。图像平面和光轴点交点称为主点(Principal Point)。显然在该模型中运用相似三角形原理更容易推导出公式x/f = X/Z

图中点Q(X,YZ)经过投影后,在图像平面上的到点q(x,y,z)。通常物体的位置参数以米等物理度量为单位,而成像平面上的点在表示时使用的单位为像素,即点Point(xscreen, yscreen)。另外通常相机的成像平面即内部的感光原件,的中心并不是严格和光轴对齐的,可能存在偏移,偏移量使用Dis(cx, cy)表示,其单位为像素。结合上文的成像公式,则点Point的计算公式如下。

上式中fx等于f✖️sx,fy等于f✖️sy,其中f为相机焦距,而sx和sy分别是x方向和y方向上的像素和距离比例,即每毫米啊包含多少个像素。这里使用sx和sy两个变量的原因是有些低成本的相机感光原件在x和y轴方向上的像素密度并不相同。需要注意焦距f和像素密度系数(sx, sy)并不能在相机校准过程中直接测量,但是fx和和fy可以直接计算。

2.1 投影几何

将现实空间中的一系列点集Q(Xi, Yi, Zi)映射到屏幕上的点集q(xi, yi)的过程称为投影变换(Projective Transform)。在这个过程中需要使用齐次坐标是非常方便的,它将n维投影空间中的点坐标扩展到n+1维上,如三位投影空间中的点p(x, y, z)使用点pn(x, y, z, w),并认为当两点的坐标成比例时它们实际上是同一个点。

在上述的例子中,投影平面为二维图像平面,因此我们使用三维向量q=(q1, q2, q3)来表示其中的点。考虑到最终的坐标为qf=(q1/q3, q2/q3, 1),因此上文的乘法公式可以表示为如下矩阵形式。其中Q为现实世界坐标,q为投影变换后的坐标,矩阵M称为相机内参矩阵。需要注意为qf=(q1/q3, q2/q3, 1)才是最终坐标。

OpenCV内部提供的坐标转换函数如下。

// 从笛卡尔坐标转换为齐次坐标
// src:N维笛卡儿坐标
// dst:N+1维齐次坐标,第N+1维度用1补齐
void cv::convertPointsToHomogeneous(cv::InputArray src, cv::OutputArray dst);

// 从齐次坐标转换为笛卡尔坐标
// src:N维齐次坐标
// dst:N-1维笛卡儿坐标,前N-1维分量都除以第N维分量,最后再丢弃最后一维
void cv::convertPointsFromHomogeneous(cv::InputArray src, cv::OutputArray dst);

上述两个函数内部使用的数学公式如下。

尽管理想的针孔是一个可用的视觉三维几何模型,但是因为针孔通过的光线很有限,因此在实际使用时成像很慢,传感器需要时间收集到足够的光线。为了使相机成像更快,我们需要在更宽的区域收集光线,并将这些光线弯曲并汇聚到投影平面的同一个点上,因此我们使用透镜,但是透镜同时也会产生扭曲。

2.2 Rodrigues变换

在处理三维空间中的向量旋转时,大多数时候都是用3✖️3的矩阵完成。但是矩阵形式不利于直观理解,另外一张旋转的表现形式是是用一个向量,向量的方向表示旋转轴的方向,向量的模表示绕轴逆时针旋转的角度。

向量形式和矩阵形式的关系用Rodrigues变换表示,假定一个旋转过程是用香料形式表示为r(rx, ry, rz),它的方向是旋转轴的正方向,这里使用右手螺旋法则,其长度θ为逆时针旋转的角度,旋转矩阵计算公式如下。这里省略了推导过程,其中rrt表示使用标准化后的r向量构建一个矩阵,矩阵的每个元素都是,对应行列序号在原向量内部元素的值的乘积。

类似的通过旋转矩阵我们也能计算出向量形式。

OpenCV提供如下函数用于处理这两种形式的转换。

// 在向量形式和矩阵形式之间转换,输入和输出一定为不同的形式// src:输入旋转向量(3✖️1)或者矩阵(3✖️3)
// dst:输出旋转矩阵(3✖️3)或者向量(3✖️1)
// jacobian:输出矩阵元素相对于输入矩阵元素的偏导数,尺寸为3✖️9或者9✖️3
// 该矩阵用于函数cv::solvePnP()和函数cv::calibrateCamera()内部优化
// 大多数时间我们的使用场景都是将上述两个函数的输出数据,即向量形式转换为矩阵形式,此时该参数使用默认值即可
void cv::Rodrigues(cv::InputArray src, cv::OutputArray dst,
                   cv::OutputArray jacobian = cv::noArray());

2.3 透镜畸变

理论上透镜可以是没有扭曲的,但是实际上制造出的滤镜并不完美。这主要是因为制造工艺的原因,生产球面透镜比制造数学上更理想的非球面透镜,抛物面透镜)更容易。同样将透镜的中心和成像传感器的中心严格对准也很困难。这里介绍两种主要的透镜扭曲,以及如何对其建模。径向扭曲(Radial Distoritions)是由透镜的形状造成的,而切向扭曲(Tangential Distorations)是由相机组装过程中的瑕疵造成的。

2.3.1 径向扭曲

首先介绍径向扭曲,相机透镜经常会明显扭曲位于成像传感器边缘的图像。这种膨胀现象是桶装和鱼眼效应的主要原因。下图演示了径向扭曲产生的原因。由于透镜从中心到边缘的形状变化,因此其折射率也随着某点到透镜中心的距离变换而改变,远离中心的光线会被扭曲得更厉害,从而使得穿过透镜的平行光线都被聚焦到一点。但是对于市场上广泛使用廉价透镜而言,特别是球面透镜,折射率的变化过快,导致平行光不能聚焦到一点,发生球面扭曲。即光线扭曲超过合理值,从而使得图中的正方形直边成像后被弯曲。这在网络摄像头中很常见,答案是在高端相机中少见,因为这些相机对透镜系统做了很多优化从而最大程度消除径向扭曲。

在成像传感器的光学中心处径向扭曲为0,随着向其边缘移动,扭曲不断加大。实际上这个扭曲量是很低的,可以通过麦克劳林级数。其实麦克劳林级数是泰勒级数的特殊情况,这里其展开式为f(r)=a0+a1r+a2r2+…,由于r=0时,f(r)也等于0,因此这里a0=0。又因为该函数关于y轴对称,因此奇次项的系数都为0。因此校正公式可以表示为如下,其中点(x, y)表示实际的投影坐标,而(xcorrected, ycorrected)表示校准后的坐标。对于如全景摄像机等更复杂的相机校准请参考opencv_contrib/modules/ccalib/samples。

对于廉价的网络相机而言,通常只需要使用到前面两项,即k1和k2,对于鱼眼透镜这类高扭曲相机而言可以使用到第三项,即k3。下图演示了矩形网格点在一个特定相机上的扭曲情况,其中的箭头起点表示正常的矩形网格,箭头的方向和长度分别表示成像后扭曲的方向和程度。可以明显看到随着离光学中心距离的增加,扭曲的程度也不断增加。

2.3.1 切向扭曲

第二种较大的常见扭曲是切向扭曲(Tangential Distortion),它是由于透镜和成像传感器并不严格平行导致的。如下图所示,当成像平面倾斜时,则首先垂直或者平行方向上两点到成像平面的距离不一样会导致它在成像平面上的投影在对应方向上的距离大于或小于正确距离,导致右图的变形。

切向扭曲的校准公式如下,这类不具体讨论推导过程,如有兴趣请参考《Decentering Distortion of Lenses》。

对于一个特定相机,在拍摄矩形网格时,其切向扭曲示意图如下,其中的箭头起点表示正常的矩形网格,箭头的方向和长度分别表示成像后扭曲的方向和程度。

因此相机的校准需要5个参数,分别是k1,k2,p1,p2和k3,大多数涉及相机校准等OpenCV的函数中,这些参数都是必须等,因此它们按这种顺序表示为扭曲向量(Distortion Vector)。实际上成像系统中还有一些其他类型的畸变,但是它们的影响并没有径向畸变和切向畸变严重,因此OpenCV并未考虑这些情况。

3 标定

关于摄像头校准在Jean-Yues Bouguet的校准网站上还能找到更多资料,OpenCV中提供的函数是cv::calibrateCamera()。该校准方法将相机对准一个具有很多独立标识点的已知结构。通过从不同的角度观察该结构,可以计算出在每个拍摄图片的时刻相机的相对位置和相机的方向,以及相机的内部参数。在本章的后面部分讲到函数cv::findChessboardCorners()中会有更详细的介绍。为了提供多个视角,需要旋转和平移目标,因此接下来先花一点时间了解关于旋转和平移的相关数学知识。

OpenCV在不断提高校准技术,现在已经有很多不同类型的校准模式,在后面小节讲到校准板时会详细介绍。对于那些特殊相机,也有特殊的校准技术。对于鱼眼透镜,需要使用类cv::fisheye的相关方法。对于全景相机(Omnidrectional Camera)和多重相机(Multicamera Calibration)校准,参考官方示例opencv_contrib/modules/ccalib/samples,opencv_contrib/modules/ccalib/tutorial/omnidir_tutorial.markdown,opencv_contrib/modules/ccalib/tutorial/multi_camera_tutorial.markdown。或者在官方文档中搜索omnidir和multiCameraCalibration。

一个全景相机的扭曲示意图如下,其内部的数学知识已经超出了本文的讨论范围。

3.1 旋转矩阵和平移向量

对于相机拍摄特定目标的每个图像而言,如下图所示我们可以通过旋转和平移的方式来描述目标相对于相机的位置。图中Q点是相机坐标系中的点,而q点是该点在模型坐标系内的点在成像平面的投影。则可以通过旋转矩阵R和平移向量t,以及前文介绍过的相机内参矩阵就可以计算出p。

通常情况下任意维度的旋转都可以通过方针和向量相乘的方式表示。另外旋转点等同于反方向旋转坐标系,即点旋转后的坐标等于在这个新坐标系内的坐标。一个二维系统下的旋转示意图如下,其中点P(x, y)是在坐标系(X-Y)中的某个点,点绕圆心逆时针旋转等同于顺时针旋转坐标系至(X’Y‘),旋转后的点坐标为p(x’,y’)。

三维空间内的旋转可以被分解为绕每个固定轴的二维旋转,如果模型坐标系(这里是相机坐标系)为参照,分别以绕xyz轴的顺序旋转ψ、φ和θ度,则三个分步的旋转矩阵可以表示如下。

则将点从世界(相机)坐标系转换为模型坐标系的矩阵是,将坐标从模型坐标系转换到世界(相机)坐标系的逆变换,即R=R(ψ)R(φ)R(θ)。旋转矩阵的转至矩阵为逆变化矩阵,即RTR=I,此处RT为矩阵R的转置矩阵,I为单位矩阵。

在应用旋转矩阵之前,我们应当将模型坐标系先平移至世界(相机)坐标系,则借助平移向量可以完成坐标变换。平移向量可以通过两个坐标系的原点坐标计算,即T=OriginObject - OriginCamera,则模型坐标系中的点PC坐标和世界坐标系中点PO的计算公式为PC=R(P0-T)。

结合PC的计算公式和前文的相机内参-校正可以组成方程组,OpenCV可以对这些方程组求解,这些方程的解将会包含我们需要的相机校准参数。三维旋转可以通过三个角度参数确定,三维平移可以通过向量(x,y,z)确定,这样就得到6个参数。OpenCV的内参矩阵包含(fx, fy, cx, cy)四个参数,这样在每个图片中总共需要求解10个参数,但是需要注意的是4个相机内参矩阵的参数对于不同的图片而言是相同的。使用一个平面目标能够很快的每个图片能够固定8个参数。因为在不同图片中旋转和平移相关6个参数是变换的,对于每个图片我们需要限制额外的两个参数,通过它们来求解相机内参矩阵。因此我们至少需要两个视图来求解所有的几何参数。

在本章的后续部分会对这些参数的细节以及它们的限制进行深入介绍,在这之前我们需要先认识校准目标(Calibration Object)。

3.2 标定板

理论上,任何具有合适特征的对象都可以是校准目标,但是实际上通常使用位于平面上的规则图形。例如棋盘(Chessboard)、圆网格(CircleGrid),随机图案(Randpattern),ArUco和ChArUco图案。其中ArUco和ChArUco图案和二维码相关,建议使用ChArUco,详细使用方式参考OpenCV官方文档,教程和代码位于OpenCV_Contrib/modules/aruco目录中。附录C中包含所有OpenCV可用校准模式的示例。在文献中使用的一些校准方法依赖于三维目标,如覆盖有标记的盒子,但是平面的棋盘模型更容易处理,而且制造、存储和分发精确的三维校准对象也是比较困难的。

OpenCV选择使用一个平面对象的多个不同角度视图,而不是具有特别构造的三维校准目标的单个视图。目前我们主要讨论棋盘模式。使用黑白交替的方块确保来在测量时不会偏向某一侧。另外得到的网格角点也可以利用在【关键点和描述子】章节中介绍到的亚像素坐标的计算函数。我们也会讨论圆网格校准板,它具有一些期待的性质,在某些情况下能够给出比棋盘更好的结果。对于其他模式,请参考图中引用的相关文档。

下图是使用棋盘校准板的示意图,通过各种姿势手持棋盘得到的图像能够提供足够的信息定位它们在世界(相机)坐标系中的位置,并计算出相机的内在参数。

下图是由高度纹理化的随机图案组成的校准板示例,参考opencv_contrib/modules/ccalib/tutorial目录中的multicamera校准教程。

下图是由ArUco(二维码)方格组成的校准图案。因为每个方块都通过自己的ArUco图案标识,当校准板的大部分被遮挡时,仍然包含足够的空间标记点用于校准计算。参考OpenCV官方文档中的ArUco标记检测(aruco)模块。

ChArUco是棋盘校准板和ArUco校准板的组合,其示意图如下。这种方式不仅在校准板部分遮挡的情况下仍能很好工作,同时在计算角度位置时也能保持更高的精度。参考OpenCV相关文档Aruco模块中的ArUco标记检测。

3.2.1 棋盘角点检测函数

提供一张棋盘图片,也可以是由人手持的棋盘图片,或者是任何包含无干扰背景的棋盘图片,都可以通过如下函数寻找棋盘的角点。

// 返回值:是否检测到所有角点并排序
// image:输入棋盘图片,元素格式为8UC1或者8UC3
// patternSize:每行和每列的内角点数量cv::Size(cols, rows)
// corners:检测到的角点
// flags:算法策略,下文介绍
bool cv::findChessboardCorners(cv::InputArray image, cv::Size patternSize,
                               cv::OutputArray corners,
                               int flags = cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE);

对于标准的国际象棋棋盘而言,内角点为7,因此参数patternSize应该传入cv::Size(7, 7),但是使用行列不同的奇偶棋盘更方便,因为此时棋盘只有一个对称轴,因此棋盘的方向更容易确定。参数flags指定了算法帮助寻找角点的过滤策略,可选参数如下,当然也可以使用逻辑与符号选择它们之中的任意项组合。

cv::CALIB_CB_ADAPTIVE_THRESH,默认该函数基于亮度均值对图像进行阈值处理,但是当该项被设置时,算法内部会使用自适应阈值技术处理图像。

cv::CALIB_CB_NORMALIZE_IMAGE,在应用阈值处理之前,如果该值被设定,则会使用函数cv::equalizeHist()预处理图像,从而让有限的颜色带宽分布更广,提高图像对比度。

cv::CALIB_CB_FILTER_QUADS,在图像经过阈值处理后,算法会尝试定位棋盘上黑色方块的投影视图中的四边形。这是一个近似四边形,因为四边形的边原则上都应该是直线,但是当图像中存在径向畸变时可能并不会这样。如果该标记被设置,则一系列额外的限制将被附加到这些四边形上,从而避免出现错误四边形。

cv::CALIB_CB_CV_FAST_CHECK,当该选项被设置时,算法首先会快速扫描图像判断内部是否包含任何角点,如果不包含则不会进行后续处理。当不确定图像中是否包含棋盘时,将该选项打开能够节约大量时间,但是当确定图像一定包含棋盘时,建议不要设置该值。

函数的返回值表示是否检测到角点并排序,这里排序意味着可以为寻找到的点构建一个模型,该模型和它们实际上时平面上的共线点集这个命题一致。该函数内部会调用函数cv::cornerSubPix()计算出较精确的角点坐标,但是当你需要更高精度的时候可以对其结果再次调用函数cv::cornerSubPix(),并设置更严格的参数。

3.2.2 绘制棋盘角点

OpenCV提供如下函数用于绘制找到的棋盘角点,这在调试时很方便。

// image:绘制底图,元素格式为8UC3
// patternSize:每行和每列的内角点数量cv::Size(cols, rows)
// corners:通过函数findChessboardCorners()找到的需要绘制的角点
// patternWasFound:是否找到了所有的角点,并排序,函数findChessboardCorners()返回值
void cv::drawChessboardCorners(cv::InputOutputArray image, cv::Size patternSize,
                               cv::InputArray corners, bool patternWasFound);

如果参数patternWasFound设置为false,则表示未找到所有的角点,则该函数会讲找到的角点绘制为红圈。如果该参数为yes,则表示找到了所有的角点,则每一行的角点都会被绘制为不同的颜色,并且用线连接以表示它们之间的顺序。

下图是该函数的一个使用示例。

现在可能会好奇这些角点对相机校准的作用,的当通过透镜观察这些被找到的角点时,可以使用3✖️3的齐次矩阵来完成透视变换进行坐标转换。但是在我们讨论这个话题之前,先简单讨论下另外方形网格的一种替代图案。

3.2.3 圆形网格

圆形网格是棋盘校准板的一种替代方案,概念上圆形网格和棋盘类似,和棋盘使用黑白相间方块不同,圆形网格在白色背景上,使用了一组黑色圆。OpenCV中实现寻找圆形网格校准板的圆形中心点坐标函数定义如下。

// 返回值:是否寻找到了圆心
// image:用于寻找圆形网格圆心坐标的图像,元素格式为8UC1或者8UC3
// patternSize:每行和每列的圆数量cv::Size(cols, rows)
// centers:检测到的圆心坐标
// flags:算法策略,下文介绍
// blobDetector:用于寻找圆形图案的特征检测器
bool cv::findCirclesGrid(cv::InputArray image, cv::Size patternSize,
                         cv::OutputArray centers, int flags = cv::CALIB_CB_SYMMETRIC_GRID,
                         const cv::Ptr<cv::FeatureDetector>& blobDetector = new SimpleBlobDetector());

参数flags用于指定使用的圆形网格校准板是对称网格(Symmetric Grid)还是非对称网格(Asymmetric Grid),前者使用值cv::CALIB_CB_SYMMETRIC_GRID,后者使用值cv::CALIB_CB_ASYMMETRIC_GRID。对称网格指的是网格的排列上每行和每列的圆圈是对齐的,而非对称网格是交错排列的。下图是一个非对称网格校准板的示意图。其中左上角图像是平视的画面,而右下图是通过相机拍摄到的透视图示意效果。

在使用非对称网格时需要仔细确定网格的行列,如上图由于非对称网格的行上的园是交错排列的,因此其中行数和列数分别为4和11。参数flag的另外一个可选值是cv::CALIB_CB_CLUSTERING,他可以和另外两个选项的任意值通过逻辑与符号组合。如果该值被指定,算法会使用一个略微不同的算法寻找圆。这个备选算法对于透视变形更稳定,但是缺点是对背景的干扰更敏感。当你尝试校准一个视野非常宽的相机时,该值是一个不错的选择。

通常情况下你会发现非对称网格相对于棋盘而言,无论是在结果的质量上,还是多次运行结果的稳定性上都更好。因此它也逐渐成为相机校准标准工具的一部分。当然在最近ChArUco(参考库的contrib部分示例代码)等模式同样也很受欢迎。

3.3 单应性

数学上的单应性具有更广泛的定义,但是这里定义的平面单应性(Planar Homography)是从一个平面到另外一个平面的投影映射,则二维平面上的点投影到相机成像传感器上的过程就是一个很好的例子。使用如下齐次坐标表示模型坐标系中的点Q以及投影到成像平面的点q,则它们可以通过矩阵关系表示。

这里引入了一个缩放参数s,它通常是从矩阵H中分离而出,从而确保计算得到的向量是正确的投影。使用一些几何学和矩阵代数的知识,就可以求解该仿射矩阵。矩阵H包含两个部分,第一部分是定位我们观察到的校准板平面,即通过一个旋转矩阵W来将点坐标从模型坐标系转换到相机坐标系。第二部分和投影相关,这里会使用到之前介绍的相机内参矩阵。单应性的示意图如下,其中Q‘是模型坐标系的点,q是在投影平面的点。

物理变换部分是旋转矩阵和平移向量的结合,该部分的变换矩阵可以表示如下。

则结合投影矩阵M,投影平面上的点q和模型坐标系上点点Q可以表示为如下公式。

实际上点Q的z轴分量一直为0,因此我们可以通过如下方式对等式进行简化。需要注意这里r3向量被移除后,缩放的部分被叠加到的参数s中,即两个等号右侧的s并不相同。

使用H表示单应矩阵,Q‘表示移除Z轴分量的模型坐标系中的点。则坐标变换公式可以表示如下,其中H为3✖️3矩阵。

OpenCV使用上述公式计算单应矩阵,它通过使用同一个目标对象的多个不同角度视图得到足够的方程组,来计算每个视图的旋转、平移矩阵,以及它们共同的内参矩阵。旋转矩阵可以通过三个角度定义,平移矩阵可以通过一个三维向量定义,因此对于每个视图而言有6个未知变量。这没有关系,因为一个已知的平面,如前文提到过的棋盘,能够给出8个等式。也就是说将一个正方形映射为一个四边形可以通过4个顶点(x, y)描述。则每个图像得到的八个方程,但是引入了6个新的未知变量,再加上已有的内参矩阵变量,因此只要有足够的图像,我们就能计算出任意数量内在参数。

位于物体空间的点Psrc和位于图像平面点Pdst它们之间的计算公式如下。

实际上OpneCV中是使用多个视图来计算出多个单应矩阵,OpenCV提供如下函数来完成该计算。他通过一系列由源坐标和目标坐标构成的点对,至少是4组,来计算单应矩阵。当然也可以提供更多的点对,因为噪声和不确定因数总是存在,更多的数据能够消除这些影响。当然有些点可能不会参与计算,详见下文RANSAC的介绍。

// 返回值:计算得到的单应矩阵
// srcPoints:点对的原始二维坐标
// dstPoints:点对的目标二维坐标
// method:算法策略,0、cv::RANSAC、cv::LMEDS等,下文介绍
// ransacReprojThreshold:参数method选择cv::RANSAC时生效的额外参数,最大的重投影距离,下文介绍
// mask:再计算单应矩阵H时,实际使用的点对
cv::Mat cv::findHomography(cv::InputArray srcPoints, cv::InputArray dstPoints,
                           cv::int method = 0, double ransacReprojThreshold = 3,
                           cv::OutputArray mask = cv::noArray());

输入参数srcPointsdstPoints可以是N✖️2的矩阵,或者元素格式为CV_32FC2的N✖️1矩阵,或者是元素类型为cv::Point2f的STL向量。

参数method的默认值为0,当使用默认值时所有的点对都会参与运算,并且得到的是使重投影误差(Reprojection Error)最小的单应矩阵。这里的重投影误差指的是对于所有点对,矩阵H乘以源坐标和目标坐标之间的欧式距离平方之和。方便的是,再使用这种误差度量的情况下,存在快速算法可以计算单应矩阵。不幸的是这种误差度量方式,会使得离群点单个点的解和主要解差异较大的那些点,对最终的解造成很大的影响。在实际情况下,如对于相机校准而言,测量误差产生离群点是一个很常见的现象,因此得到的解经常很大程度的偏离正确答案。为此OpenCV提供了三个稳定的拟合方法(Robust Fitting Methos)作为备选方案,通常它们在存在噪声的情况下表现更好。

第一个方法是一致性随机抽样法(Random Sampling with Consensus, RANSAC),对应的method选项是cv::RANSAC。该方法只会使用部分点对进行计算,首先随机选择样本的多个子集,并计算每个子集点单应矩阵,然后使用除该子集外的所有和最初估计大值一致的数据点对最初的估计进行优化。内部点指的是那些一致的点,而外部点指的是那些不一致的点。算法对多个子集计算后只会保留对应子集剩余点中内部点比例最高的一个子集的计算结果。该方法在实践中有效的避免了离群点的干扰,并得到了正确的答案。

第二个方法是最小平方中值(Least Median of Squares, LMeDS)算法,对应的method选项是cv::LMEDS。正如该算法的名字,它的目的是使中位数误差最小,而不是默认的使平方误差最小。算法的具体逻辑超出本文探讨的范围,请参考对应的论文。

LMeDS的优点是它不需要任何的信息或者参数就能运行,但是它的缺点是只有在内部点占整个数据集点大部分时才能得到较好结果。而RANSAC则对任何离群点比例的数据集合都能正常工作,并取得满意的答案,但是需要通过参数ransacReprojThreshold指定多大范围内的点被认为是内部点。该参数的单位为像素,大多数情况下,该参数设置为一个较小的整型值即可,如小于10。但是对于高分辨率的图像,则必须使用更大的值。

另外还有RHO算法,它是通过加权的方式对RANSAC算法进行改进,被称为PROSAC,在存在很多异常点的情况下该算法运行速度更快,关于算法的具体逻辑请参考相关论文。

返回值是一个3✖️3矩阵,因为它只有8个自由参数,因此我们可以对齐进行标准化处理使得H33=1,除了极其特殊的情况未标准化的矩阵H33=0外,这种处理都是可行的。这种方式和前文的等式中分离出系数s是等效的。

3.4 相机标定

本节会介绍如何使用函数cv::calibrateCamera()计算相机的内在参数和畸变参数,以及如何使用这些模型去校准已经标定好的相机得到的图像中的畸变。首先说明为了求解这些参数需要多少个棋盘模型的视图,然后会介绍具体的代码之前会对OpenCV内部是如何求解该系统进行概述。

3.4.1 计算需要的角点数量

前文提到过相机内参矩阵(Camera Intrinsic Matrix)相关的参数有4个(fx, fy, cx, cy),扭曲参数有5个或者更多,其中径向扭曲参数有3个或者更多(k1, k2, k3[,k4, k5, k6]),切向扭曲参数有两个(p1, p2)。内参矩阵相关参数控制了从模型坐标系到相机坐标系的投影变换,有时也称为线性固有参数。而畸变参数与点在最终图像中扭曲的二维几何学相关,也被称为非线性固有参数。

理论上已知图案点三个角点能够产生6则信息得到点方程组能够求解5个畸变参数,这样一张棋盘校准视图就足够了。然而由于内在参数和外在参数之间存在耦合,事实证明一张图片是不够的。外部参数包含3个旋转参数(ψ、φ、θ),和三个平移参数(Tx, Ty, Tz),它们和相机矩阵的4个内部参数组成10个未知变量,并且每增加一幅图就会增加6个额外的参数。

假定每个图像存在N个角点,K个不同视角的棋盘图像。则:

  • K张棋盘图像提供了2NK条约束。这里乘2的原因是每个角点x和y坐标分量分别提供1条约束
  • 暂时不考虑畸变参数,此时总共包含4个内部参数和6K个外部参数
  • 系统有解的条件是2NK >= 6K+4,即(N-3)K >= 2

看上去当N=5点时候,k=1就满足条件,即我们只需要一张视图。但是注意⚠️,K的值必须大于1。原因是在使用校准棋盘拟合单应矩阵时,四个角点能够产生最多8个参数。这是因为平面透视图可以做的一切只需要4个点就能够表达,可以在正方形的四个不同方向延伸从而得到任意的四边形。因此无论在一个平面中检测到多少个顶点,只能得到4个点点有效信息。因此每个棋盘视图能够得到的有效的角点信息,即N-4,则(4-3)K>1,这意味着K必须大于1。也就是说两张不同角度的3✖️3大小(只算内部角点)的棋盘是求解校准问题的最低要求。考虑到噪声和数值稳定性问题,通常需要收集更多更大的不同角度棋盘图像。实际上,想要得到高质量的结果,你需要至少10张不同角度的7✖️8或者更大尺寸的棋盘图像,不同图像的棋盘位置移动足够大才能得到丰富的信息。实际需要的图像数远大于理论图像数的原因是因为相机的内部参数即使对很小的噪声也非常敏感。

3.4.2 底层数学知识

本小节将会深入讨论相机校准函数底层的数学原理,如果对此部分不感兴趣,请直接跳转至下一小节。OpenCV处理焦距和偏移的算法基于论文《A flexible new technique for camera calibration》,计算扭曲参数的算法则是基于论文《Close-range camera calibration》。

首先我们假定没有任何扭曲,并计算其他的相机校准参数。对于棋盘的每个视图,我们收集一个单应矩阵H,正如前文描述的那样,该矩阵表示坐标从物理空间到成像的映射关系。我们使用列向量的方式表示矩阵H,即H = [h1, h2, h3],其中hi为3✖️1的向量。根据前文对单应性矩阵的讨论,它可以改写成如下公式。其中r1和r2为两个控制旋转的列向量,而t为控制平移的列向量,s为缩放因子。

进一步分解可得如下4个等式。

旋转向量在构建的时候就是正交的,因为我们提取了缩放因子,因此可以认为r1和r2向量是标准正交向量。这意味着它们的点积为0,并且它们的模相等。根据点积为0,可以得到如下等式。

对于任意向量a和b,存在公式(ab)T = bTaT,因此由上式可以推导出如下等式。其中M-T是((M)-1)T的简写。

由向量的模相等,可以得到如下等式。

同样通过公式(ab)T = bTaT替换其中的变量可以得到如下等式。

为了使等式看上去更简洁,我们定义如下矩阵B。

通过计算可以得到矩阵B的值如下。

前文得到的两个约束等式中都包含和矩阵B相关的通用形式(hi)TBhj,这里我们计算出它的值。因为B是对称矩阵,因此该等式可以写为两个六维向量的点积,即可以表示为如下等式。

使用定义(Vij)T可将两个约束等式改写为如下形式。

如果我们采集到K个棋盘图像,则可以将它们写成复合方程Vb = 0,其中V是2K✖️6的矩阵。正如前文描述那样,当K大于等于2时,计算出向量V的值后(可以通过OpenCV的函数计算),通过该等式能够得到向量b(B11,B12,B22,B13,B23,B33)的解。则可以计算出 相机的内在参数如下。

通过前文提到的单应矩阵约束条件,可以计算出外部参数(旋转和平移向量)如下。

其中缩放系数λ可以通过标准正交条件λ = 1 / || (M)-1 h ||计算得到(这里的(M)-1是M的逆矩阵)。在计算的时候需要仔细处理旋转矩阵,因为当使用真实数据计算得到旋转矩阵R(r1, r2, r3)时,由于精度误差的累积,可能得到一个不是很准确的矩阵,即不满足RT✖️R = R✖️RT = I3(这里I3是尺寸为3✖️3的单位矩阵)。

为了处理这个问题,通常的方式是对矩阵R使用奇异值分解(Singular Value Decomposition)。将R矩阵分解为两个标准正交矩阵U和V,以及一个只在对角线上包含有效元素的缩放矩阵,即R = UDVT(这里VT是V的转置矩阵),由于R本身是标准正交矩阵,因此可以使用单位矩阵I3重新计算R的值,即R = UI3VT(这里I3是尺寸为3✖️3的单位矩阵)。

到目前为止我们已经初步计算出了相机的内在参数和外在参数,接下来我们考虑相机的畸变参数。在理想情况下点经过不包含任何畸变的完美针孔相机后,其物理空间内的坐标P(Xw, Yw)和成像平面内的坐标P(Xp, Yp)之间的计算公式如下。

考虑相机畸变的情况下,成像器上的坐标P(Xd, Yd)是被扭曲的结果,其计算公式如下。

通过收集一系列这样的等式组成方程组,就能够计算出相机的畸变参数。然后在重新估计相机的内在参数和外在参数。OpenCV提供函数cv::calibrateCamera()来负责处理这些复杂的工作。

3.4.3 标定函数

当准备好多张图片的角点后,就可以调用函数cv::calibrateCamera()计算相机的内参矩阵(Camera Intrinsics Matrix),扭曲系数(Distortion Coefficients),旋转向量(Rotation Vectors)和平移向量(Translation Vectors)。前两者构成了相机的内在参数,后两者为外部测量结果,它们告诉我们物体(如前文例子中的棋盘)在空间内的位置,以及它们的方向。扭曲系数(k1, k2, p1, p2以及更高阶的k)来自于前文提到过的径向和切向扭曲等式,它们用于校准扭曲误差。相机的内参矩阵可能是我们最感兴趣的计算结果,因为通过它可以将现实空间中的三维坐标映射到图像中的二维坐标。当然我们也可以使用它来做相反的操作,但是图像中的点投影到三位空间只能是一条线,稍后我们会再次介绍这个点。现在让我们正式了解下相机校准函数。

// objectPoints:K维度向量,每个向量包含特定图像的某个校准模式下N个点在模型空间内的坐标
//   (例如以棋盘原点建立模型坐标系,在计算成像坐标点时通过旋转和平移向量移动至世界坐标系,
//    再通过内参矩阵投影到图像坐标系)
// imagePoints:K维度向量,每个向量包含特定图像的某个校准模式下N个点在图像空间内的坐标
// imageSize:输入图像的大小,单位为像素,即imagePoints的数据取样的图像大小
// cameraMatrix:3✖️3的相机内参矩阵
// distCoeffs:元素数为4、5或者8的相机扭曲系数
// rvecs:K维度向量,每个向量都包含1个旋转矩阵,表示为Rodrigues形式,即一个三元素向量
// tvecs:K维度向量,每个向量都包含1个平移矩阵
// flags:算法内部策略,下文讲解
// criteria:算法内部迭代的终止条件
double cv::calibrateCamera(cv::InputArrayOfArrays objectPoints,
                           cv::InputArrayOfArrays imagePoints,
                           cv::Size imageSize,
                           cv::InputOutputArray cameraMatrix,
                           cv::InputOutputArray distCoeffs,
                           cv::OutputArrayOfArrays rvecs,
                           cv::OutputArrayOfArrays tvecs,
                           int flags = 0,
                           cv::TermCriteria criteria = cv::TermCriteria(
                               cv::TermCriteria::COUNT | cv::TermCriteria::EPS, 30, DBL_EPSILON));

参数objectPoints可以使用简单的整型数据表示其在x和y轴上的坐标分量,z轴上的分量可以直接写为0,详细原因参考前文。并且理论上K个向量可以是不同校准图像的坐标,但是实际中通常使用同一个校准图像K个视角下相同的点集坐标。参数imagePoints类似,但是它是在图像坐标系下的坐标,并且当使用棋盘时,每个向量都是对应图像的角点输出矩阵。

需要注意参数objectPoints的输入参数的格式会影响输出参数tvecs的格式。即档期为整型数据时,即例如输入数据为[[0, 0, 0], [0, 1, 0], [0, 2, 0]等]时表示度量单位为格子单位,即第几格。假如每个格子的宽高为25毫米树,如输入的数据为[[0, 0, 0], [0, 0.025, 0], [0, 0.050, 0]等]时表示度量单位为米。而相机的内参矩阵的度量单位总是像素。

当参数distCoeffs包含4个元素时,其中每个元素依次为k1, k2, p1, p2。当包含5个元素时,依次为k1, k2, p1, p2, k3。当包含8个元素时,依次为k1, k2, p1, p2, k3, k4, k5, k6。通常5元素的模式只适用于鱼眼透镜,而8元素模式只适用于高精度的透镜,并且在参数flags中必须设置属性cv::CALIB_RATIONAL_MODEL。注意,计算所需要的图像数量随着待求解参数的数量增加而急剧增加。

注意,由于相机校准过程中精度极为重要,即使你在最开始时为参数cameraMatrixdistCoeffs分配内存时使用的数据格式不是double类型,它们都将以该类型计算和返回。

通过优化确定参数的解是一种艺术,有时尝试一次求解所有的参数可能得到不正确或者不稳定的结果,特别是当你在参数空间中选择的起始值偏离实际解时。因此通常通过分段计算较优的参数初始值不断的逼近实际解是一个更优的方法。为此,我们通常固定一些参数,解决其他参数,然后保持其他参数固定再求解原来的那些参数,并多次迭代。最终当我们认为所有的参数都接近真实的值后,我们再一次计算出所有需要的变量。OpenCV通过参数flags控制这些行为。

参数flags的取值如下,不同取值之间可以通过逻辑与符合多重选择。

cv::CALIB_USE_INTRINSIC_GUESS
当改值被设置时,算法内部会使用cameraMatrix内部的值作为初始的相机内参矩阵。在很多实际的应用中,我们可以通过从透镜旁边的信息读取出焦距,但是需要注意通常设备上标示的是毫米,我们需要将之换算成像素。如对于尺寸为1/8的传感器,如果总像素为2048✖️1536,焦距为25毫米,则可以计算出焦距为7246.38,有效值可以取7250。通常可以使用这些信息,将该值填入参数cameraMatrix,并在标记中设置cv::CALIB_USE_INTRINSIC_GUESS,另外同时设置标记cv::CALIB_FIX_ASPECT_RATIO也是一个不错的选择。

cv::CALIB_FIX_PRINCIPAL_POINT
当该标记和cv::CALIB_USE_INTRINSIC_GUESS同时被起用时,表示主点(中心点)从参数cameraMatrix中获取,当该标记被单独启用时,表示主点为图像中心。

cv::CALIB_FIX_ASPECT_RATIO
当该标记和cv::CALIB_USE_INTRINSIC_GUESS同时被起用时,在优化相机内参矩阵时fxfy保持从参数cameraMatrix中获取到的原始比例。当该标记被单独启用时,它们可以时任意值,但是它们的比值是相关的。

cv::CALIB_FIX_FOCAL_LENGTH
当该标记启用之后参数cameraMatrix中的焦距分量fx和fy将不会被更新。

cv::CALIB_FIX_K1, cv::CALIB_FIX_K2, … cv::CALIB_FIX_K6
当该标记启用之后distCoeffs中的径向扭曲参数K1, K2, … K6将不会被更新。

cv::CALIB_ZERO_TANGENT_DIST
对于高端相机而言,由于出色的制造工艺切向畸变很小,尝试拟合趋于0的参数可能导致噪声,错误值和数值稳定性问题。当该标记启用时,算法将不再拟合切向扭曲参数p1和p2,它们被直接设置为0。

cv::CALIB_RATIONAL_MODEL
默认模式下只会计算前3个径向扭曲k系数,即使提供的参数distCoeffs包含8个元素。当该标记被启用时,还会计算径向扭曲吸收k4,k5和k6。

参数criteria确定了算法内部迭代结束的条件,当包含精度条件时,指的是重投影误差(Reprojection Error)。它和函数cv::findHomography()中的含义相同,都值得是三维坐标重投影到图像平面上的位置和对应点点原始位置点距离平方和。

使用圆点网格校准相机的现象越来越频繁,在这种情况下尤其要小心设置参数objectPoints中的值。如第一行的前四个点坐标表示为(0, 0, 0), (1, 1, 0), (2, 0, 0), (3, 1, 0)时,接下来的第二行前四个点坐标则需要表示为(0, 2, 0), (1, 3, 0), (2, 2, 0), (3, 3, 0)。

3.4.4 仅计算外部参数
函数solvePnP

有时,你已经知道相机的内部参数,只想计算其外部参数,通常这种任务被称为N点透视(Perspective N-Point),或者被称为PnP问题。OpenCV负责完成此任务的函数原型如下,实际上函数cv::calibrateCamera内部也会调用该函数来完成对应任务。

// objectPoints:特定图像的某个校准模式下N个点在模型空间内的坐标(例如以棋盘原点建立模型坐标
//     系,在计算成像坐标点时通过旋转和平移向量移动至世界坐标系,再通过内参矩阵投影到图像坐标系)
// imagePoints:特定图像的某个校准模式下N个点在图像空间内的坐标
// cameraMatrix:3✖️3的相机内参矩阵
// distCoeffs:元素数为4、5或者8的相机扭曲系数
// rvecs:旋转矩阵,表示为Rodrigues形式,即一个三元素向量,可以通过函数cv::Rodirigues转化为矩阵形式
// tvecs:平移矩阵
// useExtrinsicGuess:是否使用参数rvecs和tvecs内部的数据作为初始值,并对其进行优化
// flags:算法内部策略,下文讲解
bool cv::solvePnP(cv::InputArray objectPoints, cv::InputArray imagePoints,
                  cv::InputArray cameraMatrix, cv::InputArray distCoeffs,
                  cv::OutputArray rvec, cv::OutputArray tvec,
                  bool useExtrinsicGuess = false, int flags = cv::ITERATIVE);

参数flags定义了函数的算法实现方式,其取值可以是cv::ITERATIVEcv::P3Pcv::EPNP。选项cv::ITERATIVE使用了Levenberg-Marquardt方法优化参数imagePoints和参数objectPoints经过重投影后的误差,使其最小化。选项cv::P3P基于论文【Complete solution classification for the perspective-three-point problem】中提供的方法,当选择该方式时,需要提供准确的4个物体坐标系下的点坐标,和对应的4个图像坐标系下的坐标。并且只有该方法成功执行后,函数返回值才为true。最后选项cv::EPNP基于论文【Accurate Non-Iterative O(n) Solution to the PnP Problem】中提供的方法。最后两种方法都不是迭代形式,因此它们都比第一种迭代形式的方法更快。

注意尽管上文介绍该函数的背景是考虑一个静止的相机,计算在多个视频帧内某个对象相对于静止相机的位置。但是该函数也能高效的处理逆向问题,如对于一个移动的机器人而言,此时镜头是移动的,我们更感兴趣的是那些静止的物体,可能是一些固定的物体,也可能是整个场景。在这种情况下也可以使用该函数,只需要以相反的方式理解参数rvectvec的含义即可。

函数solvePnPRansac

使用函数cv::solvePnP的一个缺点是其结构很容易受到异常点干扰。在相机校准中,这并不是一个问题,因为棋盘本身的特征提供了一个稳定的方式用于寻找我们关心的每个特征,并能够通过它们的相对空间关系验证我们得到的就是我们想要寻找的点。然而在真实世界中,确定相机和非棋盘上某个点的相对关系时(如使用稀疏关键特征点),可能会出现错误的匹配并造成严重的后果。回想在讨论单应性时提到过RANSAC方法能有效的处理异常点的影响,OpenCV也提供了类似函数来用于处理真实世界中的数据。

// objectPoints:特定图像的某个校准模式下N个点在模型空间内的坐标(例如以棋盘原点建立模型坐标
//     系,在计算成像坐标点时通过旋转和平移向量移动至世界坐标系,再通过内参矩阵投影到图像坐标系)
// imagePoints:特定图像的某个校准模式下N个点在图像空间内的坐标
// cameraMatrix:3✖️3的相机内参矩阵
// distCoeffs:元素数为4、5或者8的相机扭曲系数
// rvecs:旋转矩阵,表示为Rodrigues形式,即一个三元素向量,可以通过函数cv::Rodirigues转化为矩阵形式
// tvecs:平移矩阵
// useExtrinsicGuess:是否使用参数rvecs和tvecs内部的数据作为初始值,并对其进行优化
// iterationsCount:RANSAC迭代次数
// reprojectionError:得到的内部点集成立的最大允许重投影误差
// minInliersCount:如果寻找到的内部点超过该数值,认为该组点都为内部点,并结束算法
// inliers:寻找到的内部点索引数组
// flags:算法内部策略,和其在函数solvePnP()中的含义相同
bool cv::solvePnPRansac(cv::InputArray objectPoints, cv::InputArray imagePoints,
                        cv::InputArray cameraMatrix, cv::InputArray distCoeffs,
                        cv::OutputArray rvec, cv::OutputArray tvec,
                        bool useExtrinsicGuess = false, int iterationsCount = 100,
                        float reprojectionError = 8.0, int minInliersCount = 100,
                        cv::OutputArray inliers = cv::noArray(), int flags = cv::ITERATIVE)

对于PnP问题,需要寻找4个点确定投影变换,因此对于每个RANSAC迭代而言都会先选择4个点,而重投影误差reprojectionError的默认值和每个点在图像中的位置以及重投影位置的平均距离相关。为参数minInliersCount设置合理的值能够显著的提升算法的效率,但是该值过低也可能导致大量的问题。

4 矫正

通常我们使用已标定的相机完成两个任务,分别是校准扭曲图像,和重构接收到图像的三维场景。这里先介绍第一个任务,三维重建将在下个章节详细讨论。OpenCV提供一个便利的扭曲矫正函数cv::undistort(),也提供一对函数cv::initUndistortRectifyMap()cv::remap()用于图像矫正。前者使用函数cv::calibrateCamera()计算得到的扭曲系数,和一张扭曲的图像作为输入参数,函数返回校准后的图像。后者使我们可以更高效的处理视频或者来自于同一个相机的多张图片。下图是一副图像矫正前后的效果。

4.1 矫正映射

在对一张图像做矫正时,必须指定输入图像的每个像素将被映射到输出图像的位置,成为矫正映射(Undistortion Map)或者直接称为扭曲映射(Distortion Map)。它有如下几种可用的表示形式。

其中最直接的形式是双通道浮点型矩阵,矩阵中的每个元素都可以看作是一个二维浮点型向量,分别表示对应位置的输入图像像素应当映射到输出图像中的位置。需要注意这里使用的是浮点型数据,这意味着在计算最终输出图像数据时包含插值处理,不过这里通常的插值方法是确定能够映射输出图像中某个像素的多个源像素,再适当的在它们之间进行插值。该表示形式的矫正映射过程如下图。

第二种表示形式是使用两个浮点型的数组,和第一种表示形式含义类似,只是数据的组织方式有一点差异。

最后一种是定点表示形式,即使用一个双通道的有符号整型数据组成的矩阵(如使用CV_16SC2)。该矩阵用于插值的方式和使用双通道浮点型矩阵形式一致,只是这种数据格式处理速度更快。如果需要更高的精度,还可以使用第二个无符号整型数组(如CV_16UC1)来存储插值必要信息,即数组中的每个元素都指向了一个内部查询表。

4.2 在不同形式之间切换

OpenCV提供如下函数在三种表示形式之前自由切换。

// map1和map2为原始映射
// 由于映射的表示形式中存在两个矩阵的形式,因此这里提供两个参数,当原始映射只包含1个
// 矩阵时,参数map2传入cv::noArray()
// map1取值CV_16SC2/CV_32FC1/CV_32FC2,map2取值CV_16UC1/CV_32FC1/cv::noArray()
// dstmap1和dstmap2为转换后的映射
// dstmap1type:distmap1的类型,函数会根据该类型推断我们想要使用的映射
// nninterpolation:当转化为定点形式时是否需要计算插值表
void cv::convertMaps(cv::InputArray map1, cv::InputArray map2,
                     cv::OutputArray dstmap1, cv::OutputArray dstmap2,
                     int dstmap1type, bool nninterpolation = false);

所有可用的映射转换使用的系数取值组合如下表。

map1 map2 dstmap1type nninterpolation dstmap1 dstmap2
CV_32FC1 CV_32FC1 CV_16SC2 true CV_16SC2 CV_16UC1
CV_32FC1 CV_32FC1 CV_16SC2 false CV_16SC2 cv::noArray()
CV_32FC2 cv::noArray() CV_16SC2 true CV_16SC2 CV_16UC1
CV_32FC2 cv::noArray() CV_16SC2 false CV_16SC2 cv::noArray()
CV_16SC2 CV_16UC1 CV_32FC1 true CV_32FC1 CV_32FC1
CV_16SC2 cv::noArray() CV_32FC1 false CV_32FC1 CV_32FC1
CV_16SC2 CV_16UC1 CV_32FC2 true CV_32FC2 cv::noArray()
CV_16SC2 cv::noArray() CV_32FC2 false CV_32FC2 cv::noArray()

需要注意即使使用了插值表,即参数nninterpolation设置为true,从浮点型转化到定点型数据会损失精度,因此不应期待再从定点类型转换会浮点型时得到的结果和原始值相同。

4.3 计算矫正映射

实际上扭曲映射的应用很多,但目前我们感兴趣的是如何使用相机校准的结果创建一个矫正效果不错的图像。目前为止我们讨论的都是单目图像,实际上矫正的其中一个更重要的应用是通过一组图像计算图像的深度。下一章我们会深入讨论立体图像话题,目前只需要知道有这个应用即可,因为映射计算函数中的部分参数主要就是用于立体视觉的。

基本的过程是先计算矫正映射,然后再将其应用在特定图像上。分开操作的原因是在大多数实际场景中,只需要计算一次矫正映射,然后对于每个从相机来的视频帧依次应用这些图像。函数cv::initUndistortRectifyMap()使用相机校准的信息计算矫正映射,其原型如下。

// cameraMatrix:相机内参矩阵,尺寸为3✖️3
// distCoeffs:相机的扭曲系数,元素数可以是4、5或者8
// R:旋转变换矩阵,用于补偿嵌入式相机像对于全局坐标系的旋转
// newCameraMatrix:额外的相机内参矩阵,用于立体场景
// size:生成的映射矩阵尺寸,其大小应当与我们要矫正的图像一致
// m1type:矫正映射的分量map1的数据格式,函数根据该值推测我们需要的映射类型
// 取值为16SC2, 32FC1, 或者32FC2
// map1:矫正映射矩阵1
// map2:矫正映射矩阵2
void cv::initUndistortRectifyMap(cv::InputArray cameraMatrix,
                                 cv::InputArray distCoeffs,
                                 cv::InputArray R,
                                 cv::InputArray newCameraMatrix,
                                 cv::Size size, int m1type,
                                 cv::OutputArray map1, cv::OutputArray map2);

函数的前两个参数为相机的内部参数,可以通过函数cv::calibrateCamera()计算。参数R用于补偿嵌入式相机像对于全局坐标系的旋转,它应用于矫正发生之前。如果不使用传入cv::noArray()即可,如果使用需要传入尺寸为3✖️3的旋转矩阵。和旋转矩阵类似参数newCameraMatrix也可以影响图像矫正的方式,不过它影响的是相机的中心位置,而不是焦距。在处理单目图像时不需要使用到该参数,传入cv::noArray()即可。但是在立体图像分析中,该参数十分重要,更多细节会在下一章继续讨论。

参数m1type决定了使用的映射形式,map1map2会填充上对应的数据。它们之间的对应关系可以参考上表,只是这里默认会生成插值表。

4.4 用函数remap矫正图像

在计算出矫正映射后,就可以通过函数cv::remap()矫正输入图像,它使用函数initUndistortRectifyMap计算得到的数据,并支持任意形式的矫正映射。

4.5 用函数undistort矫正图像

如果只矫正一副图像,或者对每幅图像都重新计算矫正映射,可以使用如下复合函数。

// src:待矫正图像,二维矩阵
// dst:矫正后的图像,二维矩阵
// cameraMatrix:相机内参矩阵,尺寸为3✖️3
// distCoeffs:相机的扭曲系数,元素数可以是4、5或者8
// newCameraMatrix:额外的相机内参矩阵,用于立体场景
void cv::undistort(cv::InputArray src, cv::OutputArray dst,
                  cv::InputArray cameraMatrix, cv::InputArray distCoeffs,
                  cv::InputArray newCameraMatrix = noArray());

4.6 用函数undistortPoints稀疏矫正

有时可能我们并不需要对整幅图像进行矫正,而是只希望处理图像中的部分点,此时可以通过如下函数做稀疏矫正,只处理指定的点。

// src:待处理的像素点集,二维点的向量
// dst:矫正后的像素点集,二维点的向量
// cameraMatrix:相机内参矩阵,尺寸为3✖️3
// distCoeffs:相机的扭曲系数,元素数可以是4、5或者8
// R:表示模型空间的矫正变换矩阵,尺寸为3✖️3,R1和R2可以由函数stereoRectify计算,
//     传入cv::noArray()时使用单位矩阵
// P:额外的相机内参矩阵(3✖️3)或者是投影矩阵(3✖️4),可以由函数stereoRectify计算,
//     传入cv::noArray()时使用单位矩阵
void cv::undistortPoints(cv::InputArray src, cv::OutputArray dst,
                         cv::InputArray cameraMatrix, cv::InputArray distCoeffs,
                         cv::InputArray R = cv::noArray(), cv::InputArray P = cv::noArray());

参数srcdst都是二维点点向量,可以是由cv::Vec2i组成的N✖️1数组,也可以是浮点型数据组成的数组,还可以是元素类型为cv::Vec2f的STL风格的向量。参数R和P主要用于立体视觉分析,在下一章会详细介绍。

5 综合程序

程序Undistortion首先查找用户提供的棋盘图像,提取尽量多的能够获取到棋盘角点的信息,并计算相机内参矩阵和扭曲系数。最后程序进入显示模式,从而展示未扭曲的相机图像。在运行该程序时,需要提供拍照角度显著不同的照片,否则用于求解矫正参数的点的矩阵可能是病态的矩阵(非满秩),可能得到一个糟糕的结果,或者什么也没有得到。

6 小结

本章首先简单介绍咯针孔相机模型并概述了投影机和的基本原理,在介绍完一种旋转变换的表示形式,即Rodrigues变换后,我们介绍咯透镜畸变的概念,并学习了在OpenCV中如何使用相机内参矩阵对其建模。

接下来我们学习了如何使用棋盘或者圆形网格去标定相机,当然还可以使用其他的校准模式,在本系列文章末尾的后记中会详细列举。另外我们深入讨论了如何使用棋盘等标定模版在不同角度下拍摄的到的图像计算内部和外部参数,也讨论了图像单应性的话题,并介绍了外部参数和内部参数之前的差异,还将外部参数与普遍的姿态估计问题关联。然后我们又介绍了如何使用计算得到的相机内部参数矫正图像,从而消除真实透镜成像时引起的图像扭曲现象。

最后我们用一个复合的示例程序结束本章,该程序从磁盘中加载了一系列的棋盘照片,提取这些照片内部的角点,计算相机的内部参数,并使用这些参数对图像进行矫正。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容