zoukankan      html  css  js  c++  java
  • Opencv——相机标定

    相机标定的目的:获取摄像机的内参和外参矩阵(同时也会得到每一幅标定图像的选择和平移矩阵),内参和外参系数可以对之后相机拍摄的图像就进行矫正,得到畸变相对很小的图像。

    相机标定的输入:标定图像上所有内角点的图像坐标,标定板图像上所有内角点的空间三维坐标(一般情况下假定图像位于Z=0平面上)。

    相机标定的输出:摄像机的内参、外参系数。

    标定流程

    1. 准备标定图片

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

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

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

    5. 相机标定

    6. 对标定结果进行评价

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

    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<Point2f> image_points_buf;

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

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

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

    cornerSubPix函数原型:

    //! adjusts the corner locations with sub-pixel accuracy to maximize the certain cornerness criteria
    CV_EXPORTS_W void cornerSubPix( InputArray image, InputOutputArray corners,
                                    Size winSize, Size zeroZone,
                                    TermCriteria criteria );

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

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

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

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

    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<Point2f/Point2d> iamgePointsBuf;

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

    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<vector<Point3f>> object_points。需要依据棋盘上单个黑白矩阵的大小,计算出(初始化)每一个内角点的世界坐标。

    第二个参数imagePoints,为每一个内角点对应的图像坐标点。和objectPoints一样,应该输入vector<vector<Point2f>> 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,即vector<Mat>rvecs;

    第七个参数tvecs为位移向量,和rvecs一样,应该为vector<Mat> 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是固定的,会依此对雅可比矩阵进行调整;

    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 <iostream>
    #include <fstream>
     
    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<Point2f> image_points_buf;  /* 缓存每幅图像上检测到的角点 */
        vector<vector<Point2f>> image_points_seq; /* 保存检测到的所有角点 */
        string filename;
        int count= -1 ;//用于存储角点个数。
        while (getline(fin,filename))
        {
            image_count++;        
            // 用于观察检验输出
            cout<<"image_count = "<<image_count<<endl;        
            /* 输出检验*/
            cout<<"-->count = "<<count;        
            Mat imageInput=imread(filename);
            if (image_count == 1)  //读入第一张图片时获取图像宽高信息
            {
                image_size.width = imageInput.cols;
                image_size.height =imageInput.rows;            
                cout<<"image_size.width = "<<image_size.width<<endl;
                cout<<"image_size.height = "<<image_size.height<<endl;
            }
     
            /* 提取角点 */
            if (0 == findChessboardCorners(imageInput,board_size,image_points_buf))
            {            
                cout<<"can not find chessboard corners!
    "; //找不到角点
                exit(1);
            } 
            else 
            {
                Mat view_gray;
                cvtColor(imageInput,view_gray,CV_RGB2GRAY);
                /* 亚像素精确化 */
                find4QuadCornerSubpix(view_gray,image_points_buf,Size(11,11)); //对粗提取的角点进行精确化
                image_points_seq.push_back(image_points_buf);  //保存亚像素角点
                /* 在图像上显示角点位置 */
                drawChessboardCorners(view_gray,board_size,image_points_buf,true); //用于在图片中标记角点
                imshow("Camera Calibration",view_gray);//显示图片
                waitKey(500);//暂停0.5S        
            }
        }
        int total = image_points_seq.size();
        cout<<"total = "<<total<<endl;
        int CornerNum=board_size.width*board_size.height;  //每张图片上总的角点数
        for (int ii=0 ; ii<total ;ii++)
        {
            if (0 == ii%CornerNum)// 24 是每幅图片的角点个数。此判断语句是为了输出 图片号,便于控制台观看 
            {    
                int i = -1;
                i = ii/CornerNum;
                int j=i+1;
                cout<<"--> 第 "<<j <<"图片的数据 --> : "<<endl;
            }
            if (0 == ii%3)    // 此判断语句,格式化输出,便于控制台查看
            {
                cout<<endl;
            }
            else
            {
                cout.width(10);
            }
            //输出所有的角点
            cout<<" -->"<<image_points_seq[ii][0].x;
            cout<<" -->"<<image_points_seq[ii][0].y;
        }    
        cout<<"角点提取完成!
    ";
     
        //以下是摄像机标定
        cout<<"开始标定………………";
        /*棋盘三维信息*/
        Size square_size = Size(10,10);  /* 实际测量得到的标定板上每个棋盘格的大小 */
        vector<vector<Point3f>> object_points; /* 保存标定板上角点的三维坐标 */
        /*内外参数*/
        Mat cameraMatrix=Mat(3,3,CV_32FC1,Scalar::all(0)); /* 摄像机内参数矩阵 */
        vector<int> point_counts;  // 每幅图像中角点的数量
        Mat distCoeffs=Mat(1,5,CV_32FC1,Scalar::all(0)); /* 摄像机的5个畸变系数:k1,k2,p1,p2,k3 */
        vector<Mat> tvecsMat;  /* 每幅图像的旋转向量 */
        vector<Mat> rvecsMat; /* 每幅图像的平移向量 */
        /* 初始化标定板上角点的三维坐标 */
        int i,j,t;
        for (t=0;t<image_count;t++) 
        {
            vector<Point3f> tempPointSet;
            for (i=0;i<board_size.height;i++) 
            {
                for (j=0;j<board_size.width;j++) 
                {
                    Point3f realPoint;
                    /* 假设标定板放在世界坐标系中z=0的平面上 */
                    realPoint.x = i*square_size.width;
                    realPoint.y = j*square_size.height;
                    realPoint.z = 0;
                    tempPointSet.push_back(realPoint);
                }
            }
            object_points.push_back(tempPointSet);
        }
        /* 初始化每幅图像中的角点数量,假定每幅图像中都可以看到完整的标定板 */
        for (i=0;i<image_count;i++)
        {
            point_counts.push_back(board_size.width*board_size.height);
        }    
        /* 开始标定 */
        calibrateCamera(object_points,image_points_seq,image_size,cameraMatrix,distCoeffs,rvecsMat,tvecsMat,0);
        cout<<"标定完成!
    ";
        //对标定结果进行评价
        cout<<"开始评价标定结果………………
    ";
        double total_err = 0.0; /* 所有图像的平均误差的总和 */
        double err = 0.0; /* 每幅图像的平均误差 */
        vector<Point2f> image_points2; /* 保存重新计算得到的投影点 */
        cout<<"	每幅图像的标定误差:
    ";
        fout<<"每幅图像的标定误差:
    ";
        for (i=0;i<image_count;i++)
        {
            vector<Point3f> tempPointSet=object_points[i];
            /* 通过得到的摄像机内外参数,对空间的三维点进行重新投影计算,得到新的投影点 */
            projectPoints(tempPointSet,rvecsMat[i],tvecsMat[i],cameraMatrix,distCoeffs,image_points2);
            /* 计算新的投影点和旧的投影点之间的误差*/
            vector<Point2f> 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<Vec2f>(0,j) = Vec2f(image_points2[j].x, image_points2[j].y);
                tempImagePointMat.at<Vec2f>(0,j) = Vec2f(tempImagePoint[j].x, tempImagePoint[j].y);
            }
            err = norm(image_points2Mat, tempImagePointMat, NORM_L2);
            total_err += err/=  point_counts[i];   
            std::cout<<""<<i+1<<"幅图像的平均误差:"<<err<<"像素"<<endl;   
            fout<<""<<i+1<<"幅图像的平均误差:"<<err<<"像素"<<endl;   
        }   
        std::cout<<"总体平均误差:"<<total_err/image_count<<"像素"<<endl;   
        fout<<"总体平均误差:"<<total_err/image_count<<"像素"<<endl<<endl;   
        std::cout<<"评价完成!"<<endl;  
        //保存定标结果      
        std::cout<<"开始保存定标结果………………"<<endl;       
        Mat rotation_matrix = Mat(3,3,CV_32FC1, Scalar::all(0)); /* 保存每幅图像的旋转矩阵 */
        fout<<"相机内参数矩阵:"<<endl;   
        fout<<cameraMatrix<<endl<<endl;   
        fout<<"畸变系数:
    ";   
        fout<<distCoeffs<<endl<<endl<<endl;   
        for (int i=0; i<image_count; i++) 
        { 
            fout<<""<<i+1<<"幅图像的旋转向量:"<<endl;   
            fout<<tvecsMat[i]<<endl;   
            /* 将旋转向量转换为相对应的旋转矩阵 */   
            Rodrigues(tvecsMat[i],rotation_matrix);   
            fout<<""<<i+1<<"幅图像的旋转矩阵:"<<endl;   
            fout<<rotation_matrix<<endl;   
            fout<<""<<i+1<<"幅图像的平移向量:"<<endl;   
            fout<<rvecsMat[i]<<endl<<endl;   
        }   
        std::cout<<"完成保存"<<endl; 
        fout<<endl;
        system("pause");    
        return ;
    }

     操作说明:

    运行前需要先准备标定图片和记录标定图片列表的文本文件,并放入程序所在目录下,如下图所示:

    文本文件内容如下

     其他标定工具:

    OpenCV: https://docs.opencv.org/master/d4/d94/tutorial_camera_calibration.html 

    Matlab:  https://www.mathworks.com/help/vision/ug/single-cameracalibrator-app.html 

    ROS:  http://wiki.ros.org/camera_calibration

  • 相关阅读:
    Windows远程桌面连接CentOS 7
    CentOS7 系统菜单中添加快捷方式
    CentOS 7 创建桌面快捷方式
    Centos7 可执行程序自定义为系统服务
    CentOS 7 中 Docker 的安装
    CentOS 7 需要安装的常用工具,及centos安装fcitx 搜狗输入法的坑旅
    C++中结构体与类的区别 2
    C++中结构体与类的区别 1
    C++ 11 自旋锁
    Imply.io单机安装
  • 原文地址:https://www.cnblogs.com/long5683/p/10094122.html
Copyright © 2011-2022 走看看