用OpenCV进行摄像机标定
阅读原文时间:2023年07月09日阅读:1

用OpenCV进行摄像机标定

照相机已经存在很长时间了。然而,随着廉价针孔相机在20世纪末的引入,日常生活中变得司空见惯。不幸的是,这种廉价伴随着它的代价:显著的扭曲。幸运的是,这些常数,通过校准和一些重新映射,可以纠正这一点。此外,通过校准,还可以确定相机的自然单位(像素)和真实世界单位(例如毫米)之间的关系。

原理

对于畸变,OpenCV考虑了径向和切向因素。对于径向,使用以下公式:

So for an old pixel point at  coordinate in the input image, for a corrected output image its position will be  .

径向畸变的存在表现为“桶”或“鱼眼”效应。

由于摄像镜头与成像平面不完全平行,因此会发生切向失真。通过以下公式进行修正:

有五个失真参数,在OpenCV中被组织在一个1行5列的矩阵中:

Now for the unit conversion, we use the following formula:

Here the presence of the  is cause we use a homography coordinate system (and 

). The unknown parameters are  

and 

(camera focal lengths) and

what are the optical centers expressed in pixels coordinates. If for both axes a common focal length is used with a given  

aspect ratio (usually 1), then  

and in the upper formula we will have a single  focal length. 包含这四个参数的矩阵称为摄像机矩阵。尽管失真系数是相同的,不管使用何种摄像头分辨率,但这些失真系数应与当前校准分辨率一起缩放。

确定这两个矩阵的变换过程就是校准。这些参数的计算是通过一些基本的几何方程来完成的。使用的方程式,取决于使用的校准对象。目前OpenCV支持三种类型的对象进行校准:

经典黑白棋盘

对称圆模式

不对称圆形图案

基本上,需要用相机拍下这些图案的快照,然后让OpenCV找到它们。在一个新的方程中,每一个可见的模式都相等。为了解这个方程,至少需要预定数量的模式快照,来形成一个适配的方程组。这个数字具有较高的棋盘图案和较少的圆的。例如,在理论上,一个棋盘至少需要两个快照。然而,实际上,输入图像中存在大量的噪声,因此为了获得好的结果,可能需要至少10个不同位置的输入模式的良好快照。

目标

示例应用程序将:

确定失真矩阵

确定摄像机矩阵

摄像机、视频和图像文件列表的输入

从XML/YAML文件进行配置

将结果保存到XML/YAML文件中

计算重投影误差

Source code

You may also find the source code in the samples/cpp/tutorial_code/calib3d/camera_calibration/ folder of the OpenCV source library or download it from here.

可以在OpenCV源代码库的samples/cpp/tutorial\u code/calib3d/camera\u calibration/文件夹中找到源代码。程序只有一个参数。其配置文件的名称。如果没有,它将尝试打开一个名为“default.xml”. 下面是一个XML格式的示例配置文件。在配置文件中,可以选择使用相机、视频文件或图像列表作为输入。如果选择后者,则需要创建一个配置文件,在其中枚举要使用的映像。下面是一个例子。需要记住的重要一点是,需要使用应用程序工作目录中的绝对路径或相对路径来指定映像。可以在前面提到的目录中找到所有这些。

应用程序首先从配置文件中读取设置。尽管这是其中的一个重要部分,但与本文的主题无关:相机校准。因此,选择不在这里发布代码部分。关于如何做到这一点的技术背景可以在XML YAML中找到。

Explanation

  1. Read the settings.

  2. Settings s;

  3. const string inputSettingsFile = argc > 1 ? argv[1] : "default.xml";

  4. FileStorage fs(inputSettingsFile, FileStorage::READ); // Read the settings

  5. if (!fs.isOpened())

  6. {

  7. cout << "Could not open the configuration file: \"" << inputSettingsFile << "\"" << endl;

  8. return -1;

  9. }

10.fs["Settings"] >> s;

11.fs.release();                                         // close Settings file

13.if (!s.goodInput)

14.{

17.}

使用简单的OpenCV类输入操作。在读取文件之后,有一个额外的后处理函数,来检查输入的有效性。只有当它们都正常,goodInput变量才是正常。

获取下一个输入,如果失败,或者有足够的输入。此后,有一个大的循环,在这里,做以下操作:从图像列表中获取下一个图像,相机或视频文件。如果这失败了,或者有足够的图像,运行校准过程。在图像的情况下,步出循环,否则其余的帧将不失真(如果选项设置),通过改变检测模式到校准一。

18.for(int i = 0;;++i)

19.{

For some cameras we may need to flip the input image. Here we do this too.

  1. Find the pattern in the current input. The formation of the equations I mentioned above consists of finding the major patterns in the input: in case of the chessboard this is their corners of the squares and for the circles, well, the circles itself. The position of these will form the result and is collected into the pointBuf vector.

42.vector pointBuf;

44.bool found;

45.switch( s.calibrationPattern ) // Find feature points on the input format

46.{

47.case Settings::CHESSBOARD:

  1. found = findChessboardCorners( view, s.boardSize, pointBuf,
  2. CV_CALIB_CB_ADAPTIVE_THRESH | CV_CALIB_CB_FAST_CHECK | CV_CALIB_CB_NORMALIZE_IMAGE);
  3. break;

51.case Settings::CIRCLES_GRID:

  1. found = findCirclesGrid( view, s.boardSize, pointBuf );
  2. break;

54.case Settings::ASYMMETRIC_CIRCLES_GRID:

57.}

根据输入模式的类型,可以使用findChessboardCorners或findCirclesGrid函数。对于这两种情况,会传递当前的图像、电路板的大小,会得到图案的位置。此外,还返回一个布尔变量,说明在输入中是否可以找到模式(只需要考虑图像中的情况)。

同样,对于相机,只在输入延迟时间,过后才拍摄相机图像。用户移动棋盘,获得不同的图像。相同的图像意味着相同的方程,在标定时,相同的方程将形成不适定问题,因此标定将失败。对于正方形图像,角点的位置只是近似值。可以通过调用cornerSubPix函数来改进这一点。这样可以得到更好的标定结果。之后,将一个有效的输入结果添加到imagePoints向量中,将所有方程收集到一个容器中。最后,为了实现可视化反馈,将使用findChessboardCorners函数在输入图像上绘制找到的点。

if ( found)                // If done with success,

{

// improve the found corners' coordinate accuracy for chessboard

if( s.calibrationPattern == Settings::CHESSBOARD)

{

Mat viewGray;

cvtColor(view, viewGray, CV_BGR2GRAY);

cornerSubPix( viewGray, pointBuf, Size(11,11),

Size(-1,-1), TermCriteria( CV_TERMCRIT_EPS+CV_TERMCRIT_ITER, 30, 0.1 ));

}

if( mode == CAPTURING &&  // For camera only take new samples after delay time

(!s.inputCapture.isOpened() || clock() - prevTimestamp > s.delay*1e-3*CLOCKS_PER_SEC) )

{

imagePoints.push_back(pointBuf);

prevTimestamp = clock();

blinkOutput = s.inputCapture.isOpened();

}

// Draw the corners.

drawChessboardCorners( view, s.boardSize, Mat(pointBuf), found );

}

为用户显示状态和结果,以及应用程序的命令行控制。显示部分由实时文本输出组成,对于要显示“捕获”帧的视频或相机输入,只需按位求反输入图像。

58.//----------------------------- Output Text ------------------------------------------------

59.string msg = (mode == CAPTURING) ? "100/100" :

  1. mode == CALIBRATED ? "Calibrated" : "Press 'g' to start";

61.int baseLine = 0;

62.Size textSize = getTextSize(msg, 1, 1, 1, &baseLine);

63.Point textOrigin(view.cols - 2*textSize.width - 10, view.rows - 2*baseLine - 10);

65.if( mode == CAPTURING )

66.{

  1. if(s.showUndistorsed)
  2. msg = format( "%d/%d Undist", (int)imagePoints.size(), s.nrFrames );
  3. else
  4. msg = format( "%d/%d", (int)imagePoints.size(), s.nrFrames );

71.}

73.putText( view, msg, textOrigin, 1, 1, mode == CALIBRATED ?  GREEN : RED);

75.if( blinkOutput )

If we only ran the calibration and got the camera matrix plus the distortion coefficients we may just as correct the image with the undistort function:

//------------------------- Video capture  output  undistorted ------------------------------

if( mode == CALIBRATED && s.showUndistorsed )

{

Mat temp = view.clone();

undistort(temp, view, cameraMatrix, distCoeffs);

}

//------------------------------ Show image and check for input commands -------------------

imshow("Image View", view);

Then we wait for an input key and if this is u we toggle the distortion removal, if it is g we start all over the detection process (or simply start it), and finally for the ESC key quit the application:

char key =  waitKey(s.inputCapture.isOpened() ? 50 : s.delay);

if( key  == ESC_KEY )

break;

if( key == 'u' && mode == CALIBRATED )

s.showUndistorsed = !s.showUndistorsed;

if( s.inputCapture.isOpened() && key == 'g' )

{

mode = CAPTURING;

imagePoints.clear();

}

显示图像的失真消除。使用图像列表时,无法消除循环中的失真。因此,必须在循环之后附加。利将展开无失真函数,实际上是首先调用initundortrectitymap来找出变换矩阵,然后用remap函数进行变换。在成功校准后,地图计算只需进行一次,通过使用此扩展表格,可以加快应用程序的速度:

77.if( s.inputType == Settings::IMAGE_LIST && s.showUndistorsed )

78.{

95.}

The calibration and save

每个摄像机只需要校准一次,在成功校准后,保存它们是有意义的。这样以后就可以将这些值加载到程序中。因此,首先进行校准,如果校准成功,将结果保存到OpenCV样式的XML或YAML文件中,具体取决于在配置文件中给出的扩展名。

因此,在第一个函数中,只是将这两个过程分开。因为想保存许多校准变量,所以将在这里创建这些变量,并将它们传递给校准和保存函数。再次,将不显示保存部分,因为与校准几乎没有共同点。浏览源文件以了解如何和内容:

bool runCalibrationAndSave(Settings& s, Size imageSize, Mat&  cameraMatrix, Mat& distCoeffs,vector > imagePoints )

{

vector rvecs, tvecs;

vector reprojErrs;

double totalAvgErr = 0;

bool ok = runCalibration(s,imageSize, cameraMatrix, distCoeffs, imagePoints, rvecs, tvecs,

reprojErrs, totalAvgErr);

cout << (ok ? "Calibration succeeded" : "Calibration failed")

<< ". avg re projection error = "  << totalAvgErr ;

if( ok )   // save only if the calibration was done with success

saveCameraParams( s, imageSize, cameraMatrix, distCoeffs, rvecs ,tvecs, reprojErrs,

imagePoints, totalAvgErr);

return ok;

}

在calibleCamera函数进行校准。具有以下参数:

物体角点。这是点3f向量的向量,对于每个输入图像,它描述了模式的外观。如果有一个平面图案(如棋盘),可以简单地将所有Z坐标设置为零。这是这些要点所在的要点的集合。对所有的输入图像使用一个模式,只需计算一次,然后乘以所有其他的输入视图。使用CalcBoardCornePositions函数计算角点,如下所示:

  • * void calcBoardCornerPositions(Size boardSize, float squareSize, vector& corners,
    • Settings::Pattern patternType /*= Settings::CHESSBOARD*/)
    • {
    • corners.clear();
    • switch(patternType)
    • {
    • case Settings::CHESSBOARD:
    • case Settings::CIRCLES_GRID:
    • for( int i = 0; i < boardSize.height; ++i )
    • for( int j = 0; j < boardSize.width; ++j )
    • corners.push_back(Point3f(float( j*squareSize ), float( i*squareSize ), 0));
    • break;
    • case Settings::ASYMMETRIC_CIRCLES_GRID:
    • for( int i = 0; i < boardSize.height; i++ )
    • for( int j = 0; j < boardSize.width; j++ )
    • corners.push_back(Point3f(float((2*j + i % 2)*squareSize), float(i*squareSize), 0));
    • break;
    • }
    • }

And then multiply it as:

vector > objectPoints(1);

calcBoardCornerPositions(s.boardSize, s.squareSize, objectPoints[0], s.calibrationPattern);

objectPoints.resize(imagePoints.size(),objectPoints[0]);

  • 图像点。这是点2f向量的向量,对于每个输入图像,该向量包含找到重要点(棋盘的角点和圆模式的圆心)的位置。已经从findChessboardCorners或findCirclesGrid函数返回的内容中,收集了这个。只需要把它传下去。

  • 从相机、视频文件或图像中获取的图像大小。

  • 摄像机矩阵。如果使用“固定纵横比”选项,则需要将设置

    为零:

  • cameraMatrix = Mat::eye(3, 3, CV_64F);

  • if( s.flag & CV_CALIB_FIX_ASPECT_RATIO )

  • cameraMatrix.at(0,0) = 1.0;

  • The distortion coefficient matrix. Initialize with zero.

    • distCoeffs = Mat::zeros(8, 1, CV_64F);
  • 该函数将为所有视图计算旋转和平移矢量,该矢量将对象点(在模型坐标空间中给出)转换为图像点(在世界坐标空间中给出)。第7和第8参数是矩阵的输出向量,该矩阵在第i个位置中,包含第i个目标角点到第i个图像点的旋转和平移向量。

  • 最后一个参数是flag。需要在这里指定选项,如固定焦距的纵横比、假设零切向失真或固定主点。

double rms = calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix,

distCoeffs, rvecs, tvecs, s.flag|CV_CALIB_FIX_K4|CV_CALIB_FIX_K5);

  • 函数返回平均重投影误差。这个数字很好地估计了,找到的参数到底有多精确。应该尽可能接近于零。在给定内禀矩阵、畸变矩阵、旋转矩阵和平移矩阵的情况下,可以利用投影点,将物体点变换为像点,来计算一个视图的误差。然后,计算得到的绝对范数之间的转换和角/圆发现算法。为了找出平均误差,计算了所有校准图像的误差算术平均值。

  • double computeReprojectionErrors( const vector >& objectPoints,

  • const vector >& imagePoints,

  • const vector& rvecs, const vector& tvecs,

  • const Mat& cameraMatrix , const Mat& distCoeffs,

  • vector& perViewErrors)

  • {

  • vector imagePoints2;

  • int i, totalPoints = 0;

  • double totalErr = 0, err;

  • perViewErrors.resize(objectPoints.size());

  • for( i = 0; i < (int)objectPoints.size(); ++i )

  • {

  • projectPoints( Mat(objectPoints[i]), rvecs[i], tvecs[i], cameraMatrix,  // project

  • distCoeffs, imagePoints2);

  • err = norm(Mat(imagePoints[i]), Mat(imagePoints2), CV_L2);              // difference

  • int n = (int)objectPoints[i].size();

  • perViewErrors[i] = (float) std::sqrt(err*err/n);                        // save for this view

  • totalErr        += err*err;                                             // sum it up

  • totalPoints     += n;

  • }

  • return std::sqrt(totalErr/totalPoints);              // calculate the arithmetical mean

  • }

Results

设置这个输入棋盘图案的大小为9 X 6。用一个AXIS IP摄像头,创建几个板的快照,并将其保存到VID5目录中。我已将其放入工作目录的images/cameracalibration文件夹中,并创建了以下VID5.XML文件,该文件描述了要使用的图像:

images/CameraCalibraation/VID5/xx1.jpg

images/CameraCalibraation/VID5/xx2.jpg

images/CameraCalibraation/VID5/xx3.jpg

images/CameraCalibraation/VID5/xx4.jpg

images/CameraCalibraation/VID5/xx5.jpg

images/CameraCalibraation/VID5/xx6.jpg

images/CameraCalibraation/VID5/xx7.jpg

images/CameraCalibraation/VID5/xx8.jpg

然后将images/cameracalibration/VID5/VID5.XML指定为配置文件中的输入。在应用程序运行时,可见棋盘模式:

应用失真消除后,得到:

通过将“输入宽度”(input width)设置为4,将“高度”(height)设置为11,同样的方法也适用于这种不对称的圆形图案。使用了一个实时摄像机feed,为输入指定了它的ID(“1”)。以下是检测到的模式的照片:

在这两种情况下,在指定的输出XML/YAML文件中,将发现摄影机和失真系数矩阵:

3

3

d

6.5746697944293521e+002 0. 3.1950000000000000e+002 0.

6.5746697944293521e+002 2.3950000000000000e+002 0. 0. 1.

5

1

d

-4.1802327176423804e-001 5.0715244063187526e-001 0. 0.

-5.7843597214487474e-001

将这些值作为常量添加到程序中,调用initundortrectitymap和remap函数以消除失真,并使用廉价和低质量的相机享受无失真输入。

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章