opencv---相机标定

来源:互联网 发布:求生之路2 mac 编辑:程序博客网 时间:2024/05/16 15:10

参考文章

http://blog.csdn.net/aptx704610875/article/details/48914043
https://my.oschina.net/abcijkxyz/blog/787659
http://blog.csdn.net/hust_bochu_xuchao/article/details/51838732

坐标系之间的关系

计算机视觉领域中常见的三个坐标系:图像坐标系,相机坐标系,世界坐标系

图像坐标系—理想图像坐标系和实际图像坐标系

这里写图片描述

上图中:

这里写图片描述
实际的图像坐标系原点为这里写图片描述
二者之间的关系式为(1)(2),(1)(2)也可以用矩阵(3)表示出来.

相机坐标系(C)和世界坐标系(W)

这里写图片描述
通过相机与图像的投影关系,我们得到了等式(4)和等式(5),可以用矩阵形式(6)表示

同时,相机坐标系与世界坐标的关系可以用等式(7)来表示
这里写图片描述

图像坐标系和世界坐标系的关系

由等式(3),等式(6)和等式(7)我们可以推导二者之间的关系。

这里写图片描述

其中M1称为相机的内参矩阵,包含内参(fx,fy,u0,v0)。
M2称为相机的外参矩阵,包含外参(R:旋转矩阵,T:平移矩阵)

畸变

这里写图片描述
k1,k2,k3,k4,k5,k6为径向畸变,p1,p2为切向畸变

张正友相机标定法

在OpenCV中我们使用张正友相机标定法通过10幅不同角度的棋盘图像来标定相机获得相机内参和畸变系数。

相机标定的基本概念

相机标定的目的

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

相机标定的输入

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

相机标定的输出

摄像机的内参、外参系数。

矫正原始图像的完整流程

1. 准备标定图片

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

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

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

findChessboardCorners函数原型
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;
  • flags:用于定义棋盘图上内角点查找的不同处理方式,有默认值。

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

为了提高标定精度,需要在初步提取的角点信息上进一步提取亚像素信息,降低相机标定偏差,常用的方法是cornerSubPix

cornerSubPix函数原型
void cornerSubPix( InputArray image, InputOutputArray corners,                                Size winSize, Size zeroZone,                                TermCriteria criteria );

-image,输入的Mat矩阵,最好是8位灰度图像,检测效率更高;
- corners,初始的角点坐标向量,同时作为亚像素坐标位置的输出,所以需要是浮点型数据,一般用元素是Pointf2f/Point2d的向量来表示:vector

find4QuadCornerSubpix函数原型
bool find4QuadCornerSubpix( InputArray img, InputOutputArray corners, Size region_size );
  • 第一个参数img,输入的Mat矩阵,最好是8位灰度图像,检测效率更高;
  • 第二个参数corners,初始的角点坐标向量,同时作为亚像素坐标位置的输出,所以需要是浮点型数据,一般用元素是Pointf2f/Point2d的向量来表示:vector

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

drawChessboardCorners函数用于绘制被成功标定的角点

 drawChessboardCorners( InputOutputArray image, Size patternSize,                        InputArray corners, bool patternWasFound );
  • image,8位灰度或者彩色图像;
  • patternSize,每张标定棋盘上内角点的行列数;
  • corners,初始的角点坐标向量,同时作为亚像素坐标位置的输出,所以需要是浮点型数据,一般用元素是Pointf2f/Point2d的向量来表示:vector

相机标定

  • 对标定结果进行评价
double calibrateCamera( InputArrayOfArrays objectPoints,                        InputArrayOfArrays imagePoints, Size imageSize,                        InputOutputArray cameraMatrix, InputOutputArray distCoeffs,                        OutputArrayOfArrays rvecs, OutputArrayOfArrays tvecs,                        int flags = 0, TermCriteria criteria = TermCriteria(                        TermCriteria::COUNT + TermCriteria::EPS, 30, DBL_EPSILON) );
  • objectPoints: 一组世界坐标系中的3D
  • imagePoints: 超过10张图片的角点集合
  • imageSize: 每张图片的大小
  • cameraMatrix: 内参矩阵
  • distCoeffs: 畸变矩阵(默认获得5个即便参数k1,k2,p1,p2,k3,可修改)
  • rvecs: 外参:旋转向量
  • tvecs: 外参:平移向量
  • flag: 标定时的一些选项:
    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个畸变参数。

对标定结果进行评价

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

对空间三维坐标点进行反向投影的函数是projectPoints
 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是固定的,会依此对雅可比矩阵进行调整;

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

利用求得的相机的内参和外参数据,可以对图像进行畸变的矫正

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

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

initUndistortRectifyMap的函数原型:

 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函数

 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函数原型:

 void undistort( InputArray src, OutputArray dst,                 InputArray cameraMatrix,                 InputArray distCoeffs,                 InputArray newCameraMatrix=noArray() );
  • 第一个参数src,输入参数,代表畸变的原始图像;
  • 第二个参数dst,矫正后的输出图像,跟输入图像具有相同的类型和大小;
  • 第三个参数cameraMatrix为之前求得的相机的内参矩阵;
  • 第四个参数distCoeffs为之前求得的相机畸变矩阵;
  • 第五个参数newCameraMatrix,默认跟cameraMatrix保持一致;

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

函数原型

The algorithm performs the following steps:-   Compute the initial intrinsic parameters (the option only available for planar calibration    patterns) or read them from the input parameters. The distortion coefficients are all set to    zeros initially unless some of CV_CALIB_FIX_K? are specified.-   Estimate the initial camera pose as if the intrinsic parameters have been already known. This is    done using solvePnP .-   Run the global Levenberg-Marquardt optimization algorithm to minimize the reprojection error,    that is, the total sum of squared distances between the observed feature points imagePoints and    the projected (using the current estimates for camera parameters and the poses) object points    objectPoints. See projectPoints for details.The function returns the final re-projection error.@note   If you use a non-square (=non-NxN) grid and findChessboardCorners for calibration, and    calibrateCamera returns bad values (zero distortion coefficients, an image center very far from    (w/2-0.5,h/2-0.5), and/or large differences between \f$f_x\f$ and \f$f_y\f$ (ratios of 10:1 or more)),    then you have probably used patternSize=cvSize(rows,cols) instead of using    patternSize=cvSize(cols,rows) in findChessboardCorners .@sa   findChessboardCorners, solvePnP, initCameraMatrix2D, stereoCalibrate, undistort
double calibrateCamera( InputArrayOfArrays objectPoints,                        InputArrayOfArrays imagePoints, Size imageSize,                        InputOutputArray cameraMatrix, InputOutputArray distCoeffs,                        OutputArrayOfArrays rvecs, OutputArrayOfArrays tvecs,                        int flags = 0, TermCriteria criteria = TermCriteria(                        TermCriteria::COUNT + TermCriteria::EPS, 30, DBL_EPSILON) );
  • objectPoints: 一组世界坐标系中的3D
  • imagePoints: 超过10张图片的角点集合
  • imageSize: 每张图片的大小
  • cameraMatrix: 内参矩阵
  • distCoeffs: 畸变矩阵(默认获得5个即便参数k1,k2,p1,p2,k3,可修改)
  • rvecs: 外参:旋转向量
  • tvecs: 外参:平移向量
  • flag: 标定时的一些选项:
    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个畸变参数。

运行代码

#include "mainwindow.h"#include "opencv2/core/core.hpp"#include "opencv2/imgproc/imgproc.hpp"#include "opencv2/calib3d/calib3d.hpp"#include "opencv2/highgui/highgui.hpp"#include <QTextCodec>#include <iostream>#include <fstream>#include<opencv2/features2d/features2d.hpp>using namespace std;using namespace cv;enum { DETECTION = 0, CAPTURING = 1, CALIBRATED = 2 };MainWindow::MainWindow(QWidget *parent)    : QMainWindow(parent){    /************************************************************************               从摄像机中读取多幅图像,从中提取出角点,然后对角点进行亚像素精确化     *************************************************************************/    int image_count=  10;                    /****    图像数量     ****/    Mat frame;    Size image_size;                         /****     图像的尺寸      ****/    Size board_size = Size(9,6);            /****    定标板上每行、列的角点数       ****/    vector<Point2f> corners;                  /****    缓存每幅图像上检测到的角点       ****/    vector<vector<Point2f>>  corners_Seq;    /****  保存检测到的所有角点       ****/    ofstream fout("calibration_result.txt");  /**    保存定标结果的文件     **/    int mode = DETECTION;//初始化模式为detection    VideoCapture cap(0);    cap.set(CV_CAP_PROP_FRAME_WIDTH,640);    cap.set(CV_CAP_PROP_FRAME_HEIGHT,480);    if(!cap.isOpened()){        std::cout<<"打开摄像头失败,退出";        exit(-1);    }    namedWindow("Calibration");    std::cout<<"Press 'g' to start capturing images!"<<endl;    int count = 0,n=0;    stringstream tempname;    string filename;    int key;    string msg;    int baseLine;    Size textSize;    while(n < image_count )    {        frame.setTo(0);//将frame设置为空        cap>>frame;        if(mode == DETECTION)        {            key = 0xff & waitKey(30);            if( (key & 255) == 27 )//如果key的值为ESC                break;            if( cap.isOpened() && key == 'g' )//如果按键为'g',将模式转为CAPTURING            {                mode = CAPTURING;            }        }        if(mode == CAPTURING)//当模式为CAPTURING时        {            key = 0xff & waitKey(30);            if( (key & 255) == 32 )//判断按键是否为‘space’            {                image_size = frame.size();                /* 提取角点 */                Mat imageGray;                cvtColor(frame, imageGray , CV_RGB2GRAY);                bool patternfound = findChessboardCorners(frame, board_size, corners,CALIB_CB_ADAPTIVE_THRESH + CALIB_CB_NORMALIZE_IMAGE + CALIB_CB_FAST_CHECK );                if (patternfound)                {                    n++;                    tempname<<n;                    tempname>>filename;                    filename+=".jpg";                    /* 亚像素精确化 */                    cornerSubPix(imageGray, corners, Size(11, 11), Size(-1, -1), TermCriteria(CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 30, 0.1));                    count += corners.size();                    corners_Seq.push_back(corners);                    imwrite(filename,frame);                    tempname.clear();                    filename.clear();                }                else                {                    std::cout<<"Detect Failed.\n";                }            }        }        msg = mode == CAPTURING ? "100/100/s" : mode == CALIBRATED ? "Calibrated" : "Press 'g' to start";        baseLine = 0;        textSize = getTextSize(msg, 1, 1, 1, &baseLine);        Point textOrigin(frame.cols - 2*textSize.width - 10, frame.rows - 2*baseLine - 10);        if( mode == CAPTURING )        {            msg = format( "%d/%d",n,image_count);        }        putText( frame, msg, textOrigin, 1, 1,mode != CALIBRATED ? Scalar(0,0,255) : Scalar(0,255,0));        imshow("Calibration",frame);        key = 0xff & waitKey(1);        if( (key & 255) == 27 )            break;    }    std::cout<<"角点提取完成!\n";    /************************************************************************               摄像机定标        *************************************************************************/    std::cout<<"开始定标………………"<<endl;    Size square_size = Size(25,25);                                      /**** 实际测量得到的定标板上每个棋盘格的大小   ****/    vector<vector<Point3f>>  object_Points;                                      /****  保存定标板上角点的三维坐标   ****/    Mat image_points = Mat(1, count , CV_32FC2, Scalar::all(0));          /*****   保存提取的所有角点   *****/    vector<int>  point_counts;                                          /*****    每幅图像中角点的数量    ****/    Mat intrinsic_matrix = Mat(3,3, CV_32FC1, Scalar::all(0));                /*****    摄像机内参数矩阵    ****/    Mat distortion_coeffs = Mat(1,5, CV_32FC1, Scalar::all(0));            /* 摄像机的5个畸变系数:k1,k2,p1,p2,k3 */    vector<Mat> rotation_vectors;                                      /* 每幅图像的旋转向量 */    vector<Mat> translation_vectors;                                  /* 每幅图像的平移向量 */    /* 初始化定标板上角点的三维坐标 */    for (int t=0;t<image_count;t++)    {        vector<Point3f> tempPointSet;        for (int i=0;i<board_size.height;i++)//9        {            for (int j=0;j<board_size.width;j++)//6            {                /* 假设定标板放在世界坐标系中z=0的平面上 */                Point3f tempPoint;                tempPoint.x = i*square_size.width;                tempPoint.y = j*square_size.height;                tempPoint.z = 0;                tempPointSet.push_back(tempPoint);            }        }        object_Points.push_back(tempPointSet);    }    /* 初始化每幅图像中的角点数,这里我们假设每幅图像中都可以看到完整的定标板 */    for (int i=0; i< image_count; i++)    {        point_counts.push_back(board_size.width*board_size.height);    }    /* 开始定标 */    calibrateCamera(object_Points, corners_Seq, image_size,  intrinsic_matrix  ,distortion_coeffs, rotation_vectors, translation_vectors);    std::cout<<"achievement calibaration\n";    /************************************************************************               对定标结果进行评价        *************************************************************************/    std::cout<<"开始评价定标结果………………"<<endl;    double total_err = 0.0;                   /* 所有图像的平均误差的总和 */    double err = 0.0;                        /* 每幅图像的平均误差 */    vector<Point2f>  image_points2;             /****   保存重新计算得到的投影点    ****/    std::cout<<"每幅图像的定标误差:"<<endl;    fout<<"每幅图像的定标误差:"<<endl<<endl;    for (int i=0;  i<image_count;  i++)    {        vector<Point3f> tempPointSet = object_Points[i];        /****    通过得到的摄像机内外参数,对空间的三维点进行重新投影计算,得到新的投影点     ****/        projectPoints(tempPointSet, rotation_vectors[i], translation_vectors[i], intrinsic_matrix, distortion_coeffs, image_points2);        /* 计算新的投影点和旧的投影点之间的误差*/        vector<Point2f> tempImagePoint = corners_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<<intrinsic_matrix<<endl<<endl;    fout<<"畸变系数:\n";    fout<<distortion_coeffs<<endl<<endl<<endl;    for (int i=0; i<image_count; i++)    {        fout<<"第"<<i+1<<"幅图像的旋转向量:"<<endl;        fout<<rotation_vectors[i]<<endl;        /* 将旋转向量转换为相对应的旋转矩阵 */        Rodrigues(rotation_vectors[i],rotation_matrix);        fout<<"第"<<i+1<<"幅图像的旋转矩阵:"<<endl;        fout<<rotation_matrix<<endl;        fout<<"第"<<i+1<<"幅图像的平移向量:"<<endl;        fout<<translation_vectors[i]<<endl<<endl;    }    std::cout<<"完成保存"<<endl;    fout<<endl;    /************************************************************************               显示定标结果        *************************************************************************/    Mat mapx = Mat(image_size,CV_32FC1);    Mat mapy = Mat(image_size,CV_32FC1);    Mat R = Mat::eye(3,3,CV_32F);    std::cout<<"保存矫正图像"<<endl;    string imageFileName;    std::stringstream StrStm;    for (int i = 0 ; i != image_count ; i++)    {        std::cout<<"Frame #"<<i+1<<"..."<<endl;        Mat newCameraMatrix = Mat(3,3,CV_32FC1,Scalar::all(0));        initUndistortRectifyMap(intrinsic_matrix,distortion_coeffs,R,intrinsic_matrix,image_size,CV_32FC1,mapx,mapy);        StrStm.clear();        imageFileName.clear();        StrStm<<i+1;        StrStm>>imageFileName;        imageFileName += ".jpg";        Mat t = imread(imageFileName);        Mat newimage = t.clone();        cv::remap(t,newimage,mapx, mapy, INTER_LINEAR);        StrStm.clear();        imageFileName.clear();        StrStm<<i+1;        StrStm>>imageFileName;        imageFileName += "_d.jpg";        imwrite(imageFileName,newimage);    }    std::cout<<"保存结束"<<endl;}MainWindow::~MainWindow(){}

代码知识点详解

相机标定步骤

  • 读取多幅图像,从中提取出角点,然后对角点进行亚像素精确化
    1)读取图像可以采用已经存在的图像;或者采用外置的摄像头进行读取
    2)提取角点
    3)亚像素精确化

  • 摄像机定标
    1)初始化定标板上角点的三维坐标
    2)初始化每幅图像中的角点数
    3)开始定标—calibrateCamera

  • 对定标结果进行评价
    通过得到的摄像机内外参数,对空间的三维点进行重新投影计算,得到新的投影点
    计算新的投影点和旧的投影点之间的误差

原创粉丝点击