镜头畸变现象及其校正方法

      摄像机校准一般采用小孔成像模型,理想的小孔模型是线性模型,但是由于存在镜头畸变等原因,线性模型通常要加上一些内部参数,变成非线性模型。现对产生这一现象的原理以及解决方法进行整理,如下:

一、镜头畸变现象介绍

     相机的成像过程实质上是坐标系的转换。首先空间中的点由 “世界坐标系” 转换到 “像机坐标系”,然后再将其投影到成像平面 ( 图像物理坐标系 ) ,最后再将成像平面上的数据转换到 图像像素坐标系。但是由于透镜制造精度以及组装工艺的偏差会引入畸变,导致原始图像的失真。镜头的畸变分为径向畸变和切向畸变两类。参见:

http://blog.csdn.net/dcrmg/article/details/52950141

http://blog.csdn.net/waeceo/article/details/50580808

1. 径向畸变

径向畸变是沿着透镜半径方向分布的畸变,产生原因是光线在原理透镜中心的地方比靠近中心的地方更加弯曲,这种畸变在普通廉价的镜头中表现更加明显,径向畸变主要包括桶形畸变和枕形畸变两种。

镜头畸变现象及其校正方法_第1张图片

成像仪光轴中心的畸变为0,沿着镜头半径方向向边缘移动,畸变越来越严重。畸变的数学模型可以用主点(principle point)周围的泰勒级数展开式的前几项进行描述,通常使用前两项,即k1和k2,对于畸变很大的镜头,如鱼眼镜头,可以增加使用第三项k3来进行描述,成像仪上某点根据其在径向方向上的分布位置,调节公式为:
镜头畸变现象及其校正方法_第2张图片
式里(x0,y0)是畸变点在成像仪上的原始位置,(x,y)是畸变较真后新的位置,下图是距离光心不同距离上的点经过透镜径向畸变后点位的偏移示意图,距离光心越远,径向位移越大,表示畸变也越大,在光心附近,几乎没有偏移。

镜头畸变现象及其校正方法_第3张图片

2. 切向畸变

切向畸变是由于透镜本身与相机传感器平面(成像平面)或图像平面不平行而产生的,这种情况多是由于透镜被粘贴到镜头模组上的安装偏差导致。畸变模型可以用两个额外的参数p1和p2来描述:

镜头畸变现象及其校正方法_第4张图片

大体上畸变位移相对于左下——右上角的连线是对称的,说明该镜头在垂直于该方向上有一个旋转角度。

镜头畸变现象及其校正方法_第5张图片

二、物象坐标映射转换

opencv中,函数findhomography可以找到这一单应性矩阵。

   Mat findHomography( const Mat& srcPoints, const Mat& dstPoints,  
    Mat& status, int method=0,  
    double ransacReprojThreshold=3 );  
    Mat findHomography( const Mat& srcPoints, const Mat& dstPoints,  
    vector& status, int method=0,  
    double ransacReprojThreshold=3 );  
    Mat findHomography( const Mat& srcPoints, const Mat& dstPoints,  
    int method=0, double ransacReprojThreshold=3 );  

1、srcPoints,dstPoints为CV_32FC2或者vector类型
2、method:0表示使用所有点的常规方法;CV_RANSAC 基于RANSAC鲁棒性的方法;CV_LMEDS 最小中值鲁棒性方法
3、ransacReprojThreshod 仅在RANSAC方法中使用,一个点对被认为是内层围值(非异常值)所允许的最大投影误差。如果srcPoints和dstPoints单位是像素,通常意味着在某些情况下这个参数的范围在1到10之间。
4、status,可选的输出掩码,用在CV_RANSAC或者CV_LMEDS方法中。注意输入掩码将被忽略。
如果参数method设置为默认值0,该函数使用一个简单的最小二乘方案来计算初始的单应性估计。然而,如果不是所有的点对(srcPoints,dstPoints)都适应这个严格的透视变换。(也就是说,有一些异常值),这个初始估计值将很差。在这种情况下,我们可以使用两个鲁棒性算法中的一个。RANSCA和LMEDS这两个方法都尝试不同的随机的相对应点对的子集,每四对点集一组,使用这个子集和一个简单的最小二乘算法来估计单应性矩阵,然后计算得到单应性矩阵的质量quality/goodness。(对于RANSAC方法是内层围点的数量,对于LMeDs是中间的重投影误差)。然后最好的子集用来产生单应性矩阵的初始化估计和inliers/outliers的掩码。

三、相机校正

在第二部分,求得的单应性矩阵,是由两部分构成的:内参矩阵和外参矩阵,在OpenCV的3D重建中,对摄像机的内参外参有讲解:
外参:摄像机的旋转平移属于外参,用于描述相机在静态场景下相机的运动,或者在相机固定时,运动物体的刚性运动。因此,在图像拼接或者三维重建中,就需要使用外参来求几幅图像之间的相对运动,从而将其注册到同一个坐标系下面来。
内参:下面给出了内参矩阵,需要注意的是,真实的镜头还会有径向和切向畸变,而这些畸变是属于相机的内参的。
摄像机内参矩阵:

         fx    s    x0
    K =  0    fy    y0
         0    0     1

其中,fx,fy为焦距,一般情况下,二者相等,x0、y0为主点坐标(相对于成像平面),s为坐标轴倾斜参数,理想情况下为0。
摄像机外参矩阵:包括旋转矩阵和平移矩阵,旋转矩阵和平移矩阵共同描述了如何把点从世界坐标系转换到摄像机坐标系。
旋转矩阵:描述了世界坐标系的坐标轴相对于摄像机坐标轴的方向。
平移矩阵:描述了在摄像机坐标系下,空间原点的位置。

博文http://blog.csdn.net/dcrmg/article/details/52939318中给出了opencv校正流程,表示感谢.

另外还可以参见:

http://www.cnblogs.com/tianya2543/p/3894644.html,http://www.eepw.com.cn/article/201706/352360.htm

http://blog.csdn.net/dcrmg/article/details/52929669

1. 准备标定图片

标定图片需要使用标定板在不同位置、不同角度、不同姿态下拍摄,最少需要3张,以10~20张为宜。标定板需要是黑白相间的矩形构成的棋盘图,制作精度要求较高,如下图所示:



2.对每一张标定图片,提取角点信息

需要使用findChessboardCorners函数提取角点,这里的角点专指的是标定板上的内角点,这些角点与标定板的边缘不接触。

 findChessboardCorners函数原型:

  //! finds checkerboard pattern of the specified size in the image  
    CV_EXPORTS_W bool findChessboardCorners( InputArray image, Size patternSize,  
              OutputArray corners, int flags=CALIB_CB_ADAPTIVE_THRESH+CALIB_CB_NORMALIZE_IMAGE );  

第一个参数Image,传入拍摄的棋盘图Mat图像,必须是8位的灰度或者彩色图像;

第二个参数patternSize,每个棋盘图上内角点的行列数,一般情况下,行列数不要相同,便于后续标定程序识别标定板的方向;

第三个参数corners,用于存储检测到的内角点图像坐标位置,一般用元素是Point2f的向量来表示:vector image_points_buf;

第四个参数flage:用于定义棋盘图上内角点查找的不同处理方式,有默认值。

3. 对每一张标定图片,进一步提取亚像素角点信息

为了提高标定精度,需要在初步提取的角点信息上进一步提取亚像素信息,降低相机标定偏差,常用的方法是cornerSubPix,另一个方法是使用find4QuadCornerSubpix函数,这个方法是专门用来获取棋盘图上内角点的精确位置的,或许在相机标定的这个特殊场合下它的检测精度会比cornerSubPix更高?

cornerSubPix函数原型:

CV_EXPORTS_W void cornerSubPix( InputArray image, InputOutputArray corners,  
                                Size winSize, Size zeroZone,  
                                TermCriteria criteria ); 

第一个参数image,输入的Mat矩阵,最好是8位灰度图像,检测效率更高;

第二个参数corners,初始的角点坐标向量,同时作为亚像素坐标位置的输出,所以需要是浮点型数据,一般用元素是Pointf2f/Point2d的向量来表示:vector iamgePointsBuf;

第三个参数winSize,大小为搜索窗口的一半;

第四个参数zeroZone,死区的一半尺寸,死区为不对搜索区的中央位置做求和运算的区域。它是用来避免自相关矩阵出现某些可能的奇异性。当值为(-1,-1)时表示没有死区;

第五个参数criteria,定义求角点的迭代过程的终止条件,可以为迭代次数和角点精度两者的组合;

find4QuadCornerSubpix函数原型:

//! finds subpixel-accurate positions of the chessboard corners  
    CV_EXPORTS bool find4QuadCornerSubpix(InputArray img, InputOutputArray corners, Size region_size);

第一个参数img,输入的Mat矩阵,最好是8位灰度图像,检测效率更高;

第二个参数corners,初始的角点坐标向量,同时作为亚像素坐标位置的输出,所以需要是浮点型数据,一般用元素是Pointf2f/Point2d的向量来表示:vector iamgePointsBuf;

第三个参数region_size,角点搜索窗口的尺寸;

在其中一个标定的棋盘图上分别运行cornerSubPix和find4QuadCornerSubpix寻找亚像素角点,两者定位到的亚像素角点坐标分别为:

   cornerSubPix:                                                 find4QuadCornerSubpix:

镜头畸变现象及其校正方法_第6张图片                       镜头畸变现象及其校正方法_第7张图片

4. 在棋盘标定图上绘制找到的内角点(非必须,仅为了显示)

drawChessboardCorners函数用于绘制被成功标定的角点,函数原型:

    //! draws the checkerboard pattern (found or partly found) in the image  
    CV_EXPORTS_W void drawChessboardCorners( InputOutputArray image, Size patternSize,  
                                             InputArray corners, bool patternWasFound );  
第一个参数image,8位灰度或者彩色图像;

第二个参数patternSize,每张标定棋盘上内角点的行列数;

第三个参数corners,初始的角点坐标向量,同时作为亚像素坐标位置的输出,所以需要是浮点型数据,一般用元素是Pointf2f/Point2d的向量来表示:vector iamgePointsBuf;

第四个参数patternWasFound,标志位,用来指示定义的棋盘内角点是否被完整的探测到,true表示别完整的探测到,函数会用直线依次连接所有的内角点,作为一个整体,false表示有未被探测到的内角点,这时候函数会以(红色)圆圈标记处检测到的内角点;

以下是drawChessboardCorners函数中第四个参数patternWasFound设置为true和false时内角点的绘制效果:

patternWasFound=ture时,依次连接各个内角点:

镜头畸变现象及其校正方法_第8张图片

patternWasFound=false时,以(红色)圆圈标记处角点位置:

镜头畸变现象及其校正方法_第9张图片

5. 相机标定

获取到棋盘标定图的内角点图像坐标之后,就可以使用calibrateCamera函数进行标定,计算相机内参和外参系数,

calibrateCamera函数原型:

    //! finds intrinsic and extrinsic camera parameters from several fews of a known calibration pattern.  
    CV_EXPORTS_W double calibrateCamera( InputArrayOfArrays objectPoints,  
                                         InputArrayOfArrays imagePoints,  
                                         Size imageSize,  
                                         CV_OUT InputOutputArray cameraMatrix,  
                                         CV_OUT InputOutputArray distCoeffs,  
                                         OutputArrayOfArrays rvecs, OutputArrayOfArrays tvecs,  
                                         int flags=0, TermCriteria criteria = TermCriteria(  
                                             TermCriteria::COUNT+TermCriteria::EPS, 30, DBL_EPSILON) );  

第一个参数objectPoints,为世界坐标系中的三维点。在使用时,应该输入一个三维坐标点的向量的向量,即vector> object_points。需要依据棋盘上单个黑白矩阵的大小,计算出(初始化)每一个内角点的世界坐标。

第二个参数imagePoints,为每一个内角点对应的图像坐标点。和objectPoints一样,应该输入vector> image_points_seq形式的变量;

第三个参数imageSize,为图像的像素尺寸大小,在计算相机的内参和畸变矩阵时需要使用到该参数;

第四个参数cameraMatrix为相机的内参矩阵。输入一个Mat cameraMatrix即可,如Mat cameraMatrix=Mat(3,3,CV_32FC1,Scalar::all(0));

第五个参数distCoeffs为畸变矩阵。输入一个Mat distCoeffs=Mat(1,5,CV_32FC1,Scalar::all(0))即可;

第六个参数rvecs为旋转向量;应该输入一个Mat类型的vector,即vectorrvecs;

第七个参数tvecs为位移向量,和rvecs一样,应该为vector tvecs;

第八个参数flags为标定时所采用的算法。有如下几个参数:

CV_CALIB_USE_INTRINSIC_GUESS:使用该参数时,在cameraMatrix矩阵中应该有fx,fy,u0,v0的估计值。否则的话,将初始化(u0,v0)图像的中心点,使用最小二乘估算出fx,fy。 
CV_CALIB_FIX_PRINCIPAL_POINT:在进行优化时会固定光轴点。当CV_CALIB_USE_INTRINSIC_GUESS参数被设置,光轴点将保持在中心或者某个输入的值。 
CV_CALIB_FIX_ASPECT_RATIO:固定fx/fy的比值,只将fy作为可变量,进行优化计算。当CV_CALIB_USE_INTRINSIC_GUESS没有被设置,fx和fy将会被忽略。只有fx/fy的比值在计算中会被用到。 
CV_CALIB_ZERO_TANGENT_DIST:设定切向畸变参数(p1,p2)为零。 
CV_CALIB_FIX_K1,…,CV_CALIB_FIX_K6:对应的径向畸变在优化中保持不变。 
CV_CALIB_RATIONAL_MODEL:计算k4,k5,k6三个畸变参数。如果没有设置,则只计算其它5个畸变参数。

第九个参数criteria是最优迭代终止条件设定。

在使用该函数进行标定运算之前,需要对棋盘上每一个内角点的空间坐标系的位置坐标进行初始化,标定的结果是生成相机的内参矩阵cameraMatrix、相机的5个畸变系数distCoeffs,另外每张图像都会生成属于自己的平移向量和旋转向量。

6. 对标定结果进行评价

对标定结果进行评价的方法是通过得到的摄像机内外参数,对空间的三维点进行重新投影计算,得到空间三维点在图像上新的投影点的坐标,计算投影坐标和亚像素角点坐标之间的偏差,偏差越小,标定结果越好。

对空间三维坐标点进行反向投影的函数是projectPoints,函数原型是:

    //! projects points from the model coordinate space to the image coordinates. Also computes derivatives of the image coordinates w.r.t the intrinsic and extrinsic camera parameters  
    CV_EXPORTS_W void projectPoints( InputArray objectPoints,  
                                     InputArray rvec, InputArray tvec,  
                                     InputArray cameraMatrix, InputArray distCoeffs,  
                                     OutputArray imagePoints,  
                                     OutputArray jacobian=noArray(),  
                                     double aspectRatio=0 );  

第一个参数objectPoints,为相机坐标系中的三维点坐标;

第二个参数rvec为旋转向量,每一张图像都有自己的选择向量;

第三个参数tvec为位移向量,每一张图像都有自己的平移向量;

第四个参数cameraMatrix为求得的相机的内参数矩阵;

第五个参数distCoeffs为相机的畸变矩阵;

第六个参数iamgePoints为每一个内角点对应的图像上的坐标点;

第七个参数jacobian是雅可比行列式;

第八个参数aspectRatio是跟相机传感器的感光单元有关的可选参数,如果设置为非0,则函数默认感光单元的dx/dy是固定的,会依此对雅可比矩阵进行调整;

下边显示了某一张标定图片上的亚像素角点坐标和根据标定结果把空间三维坐标点映射回图像坐标点的对比:

find4QuadCornerSubpix查找到的亚像素点坐标:                           projectPoints映射的坐标:

             镜头畸变现象及其校正方法_第10张图片                                      镜头畸变现象及其校正方法_第11张图片 

以下是每一幅图像上24个内角点的平均误差统计数据:

镜头畸变现象及其校正方法_第12张图片

7. 查看标定效果——利用标定结果对棋盘图进行矫正

利用求得的相机的内参和外参数据,可以对图像进行畸变的矫正,这里有两种方法可以达到矫正的目的,分别说明一下。

方法一:使用initUndistortRectifyMap和remap两个函数配合实现。

initUndistortRectifyMap用来计算畸变映射,remap把求得的映射应用到图像上。

initUndistortRectifyMap的函数原型:

    //! initializes maps for cv::remap() to correct lens distortion and optionally rectify the image  
    CV_EXPORTS_W void initUndistortRectifyMap( InputArray cameraMatrix, InputArray distCoeffs,  
                               InputArray R, InputArray newCameraMatrix,  
                               Size size, int m1type, OutputArray map1, OutputArray map2 );  

第一个参数cameraMatrix为之前求得的相机的内参矩阵;

第二个参数distCoeffs为之前求得的相机畸变矩阵;

第三个参数R,可选的输入,是第一和第二相机坐标之间的旋转矩阵;

第四个参数newCameraMatrix,输入的校正后的3X3摄像机矩阵;

第五个参数size,摄像机采集的无失真的图像尺寸;

第六个参数m1type,定义map1的数据类型,可以是CV_32FC1或者CV_16SC2;

第七个参数map1和第八个参数map2,输出的X/Y坐标重映射参数;

remap函数原型:

    //! warps the image using the precomputed maps. The maps are stored in either floating-point or integer fixed-point format  
    CV_EXPORTS_W void remap( InputArray src, OutputArray dst,  
                             InputArray map1, InputArray map2,  
                             int interpolation, int borderMode=BORDER_CONSTANT,  
                             const Scalar& borderValue=Scalar());  

第一个参数src,输入参数,代表畸变的原始图像;

第二个参数dst,矫正后的输出图像,跟输入图像具有相同的类型和大小;

第三个参数map1和第四个参数map2,X坐标和Y坐标的映射;

第五个参数interpolation,定义图像的插值方式;

第六个参数borderMode,定义边界填充方式;

方法二:使用undistort函数实现

undistort函数原型:

    //! corrects lens distortion for the given camera matrix and distortion coefficients  
    CV_EXPORTS_W void undistort( InputArray src, OutputArray dst,  
                                 InputArray cameraMatrix,  
                                 InputArray distCoeffs,  
                                 InputArray newCameraMatrix=noArray() );  

第一个参数src,输入参数,代表畸变的原始图像;

第二个参数dst,矫正后的输出图像,跟输入图像具有相同的类型和大小;

第三个参数cameraMatrix为之前求得的相机的内参矩阵;

第四个参数distCoeffs为之前求得的相机畸变矩阵;

第五个参数newCameraMatrix,默认跟cameraMatrix保持一致;

方法一相比方法二执行效率更高一些,推荐使用。

工程代码如下,亲测,可用,再次感谢该博文的博主~

    #include "opencv2/core/core.hpp"  
    #include "opencv2/imgproc/imgproc.hpp"  
    #include "opencv2/calib3d/calib3d.hpp"  
    #include "opencv2/highgui/highgui.hpp"  
    #include   
    #include   
      
    using namespace cv;  
    using namespace std;  
      
    void main()   
    {  
        ifstream fin("calibdata.txt"); /* 标定所用图像文件的路径 */  
        ofstream fout("caliberation_result.txt");  /* 保存标定结果的文件 */    
        //读取每一幅图像,从中提取出角点,然后对角点进行亚像素精确化   
        cout<<"开始提取角点………………";  
        int image_count=0;  /* 图像数量 */  
        Size image_size;  /* 图像的尺寸 */  
        Size board_size = Size(4,6);    /* 标定板上每行、列的角点数 */  
        vector image_points_buf;  /* 缓存每幅图像上检测到的角点 */  
        vector> image_points_seq; /* 保存检测到的所有角点 */  
        string filename;  
        int count= -1 ;//用于存储角点个数。  
        while (getline(fin,filename))  
        {  
            image_count++;        
            // 用于观察检验输出  
            cout<<"image_count = "<count = "< 第 "< : "<"<"<> object_points; /* 保存标定板上角点的三维坐标 */  
        /*内外参数*/  
        Mat cameraMatrix=Mat(3,3,CV_32FC1,Scalar::all(0)); /* 摄像机内参数矩阵 */  
        vector point_counts;  // 每幅图像中角点的数量  
        Mat distCoeffs=Mat(1,5,CV_32FC1,Scalar::all(0)); /* 摄像机的5个畸变系数:k1,k2,p1,p2,k3 */  
        vector tvecsMat;  /* 每幅图像的旋转向量 */  
        vector rvecsMat; /* 每幅图像的平移向量 */  
        /* 初始化标定板上角点的三维坐标 */  
        int i,j,t;  
        for (t=0;t tempPointSet;  
            for (i=0;i image_points2; /* 保存重新计算得到的投影点 */  
        cout<<"\t每幅图像的标定误差:\n";  
        fout<<"每幅图像的标定误差:\n";  
        for (i=0;i tempPointSet=object_points[i];  
            /* 通过得到的摄像机内外参数,对空间的三维点进行重新投影计算,得到新的投影点 */  
            projectPoints(tempPointSet,rvecsMat[i],tvecsMat[i],cameraMatrix,distCoeffs,image_points2);  
            /* 计算新的投影点和旧的投影点之间的误差*/  
            vector tempImagePoint = image_points_seq[i];  
            Mat tempImagePointMat = Mat(1,tempImagePoint.size(),CV_32FC2);  
            Mat image_points2Mat = Mat(1,image_points2.size(), CV_32FC2);  
            for (int j = 0 ; j < tempImagePoint.size(); j++)  
            {  
                image_points2Mat.at(0,j) = Vec2f(image_points2[j].x, image_points2[j].y);  
                tempImagePointMat.at(0,j) = Vec2f(tempImagePoint[j].x, tempImagePoint[j].y);  
            }  
            err = norm(image_points2Mat, tempImagePointMat, NORM_L2);  
            total_err += err/=  point_counts[i];     
            std::cout<<"第"<>imageFileName;  
            filePath+=imageFileName;  
            filePath+=".bmp";  
            Mat imageSource = imread(filePath);  
            Mat newimage = imageSource.clone();  
            //另一种不需要转换矩阵的方式  
            //undistort(imageSource,newimage,cameraMatrix,distCoeffs);  
            remap(imageSource,newimage,mapx, mapy, INTER_LINEAR);         
            StrStm.clear();  
            filePath.clear();  
            StrStm<>imageFileName;  
            imageFileName += "_d.jpg";  
            imwrite(imageFileName,newimage);  
        }  
        std::cout<<"保存结束"<
另外,还可以使用matlab进行校正,具体请参见:

http://blog.csdn.net/Loser__Wang/article/details/51811347,

http://www.cnblogs.com/li-yao7758258/p/5929145.html


你可能感兴趣的:(机器视觉与图像处理)