OpenCv相机标定——圆形标定板标定
- 0.前言
- 1.标定图案
- 2.OpenCv标定
- 3.标定结果分析
0.前言
OpenCv中,相机标定所使用的标定图案分为棋盘格、对称圆形及非对称圆形特征图、ArUco板和ChArUco板等。在OpenCV的官方例程中,采用的是棋盘格图案,因为其操作简单、快速,标定精度满足一般应用场景的需求。对于标定精度要求高的场景,则一般采用圆形标定图案。本文主要介绍如何使用圆形标定图案(对称和非对称)完成相机的标定,并将OpenCv标定结果与Halcon标定结果进行对比分析。
1.标定图案
OpenCv中使用的圆形标定图案如图1所示:
OpenCv中,使用圆形标定图案用到的函数为 cv::findCirclesGrid()。函数原型如下:
bool cv::findCirclesGrid(//找到圆心坐标返回True
cv::InputArray,//输入标定图像,8位单通道或三通道
cv::Size patternSize,//标定图案的尺寸
cv::OutputArray centers,//输出数组,为检测到的圆心坐标
int flags,//标志位,对称图案——cv::CALIB_CB_SYMMETRIC_GRID,非对称图案—— cv::CALIB_CB_ASYMMETRIC_GRID
const cv::Ptrcv::FeatureDetector&blobDetector=new SimpleBlobDetector()
);
图1所示的非对称圆形标定图案,其width=11,height=6。在计算标定图案上标志点圆心的世界坐标时,参数squareSize即为图1中标注的圆心距。关于圆的半径大小,可以自行设定,因为在提取圆心坐标时不涉及圆的半径(这点和halcon标定不同,halcon在进行相机标定时,圆的半径作为标定文件中的已知参数)。圆心距一般取圆直径的4倍左右。
图2为本文使用的标定板,其为高精度铝制标定板,精度为±0.01mm,是200x200mm的halcon标准标定板,圆的直径为12.5mm,圆心距为25mm。
2.OpenCv标定
本文采用的标定为离线标定,先由相机采集N幅图像,再由标定程序读取图像。为了保证标定精度,建议采集10幅或更多的视图,尽量使得标定板的移动范围覆盖相机视野。
在OpenCv官方相机标定代码的基础上进行了修改,得到了下面的对圆形标定图案标定的代码。由于代码近500行,为了缩短篇幅,省略的一些头文件、说明性文字、函数的实现。省略部分可参考:OpenCv/sources/samples/cpp/tutorial_code/calib3d/camera_calibration/camera_calibration.cpp.
#include "stdafx.h"
//此处省略各种头文件
using namespace cv;
using namespace std;
//此处省略help()函数
enum { DETECTION = 0, CAPTURING = 1, CALIBRATED = 2 };
enum Pattern { CHESSBOARD, CIRCLES_GRID, ASYMMETRIC_CIRCLES_GRID };//计算重投影误差函数
static double computeReprojectionErrors(const vector<vector<Point3f> >& objectPoints,const vector<vector<Point2f> >& imagePoints,const vector<Mat>& rvecs, const vector<Mat>& tvecs,const Mat& cameraMatrix, const Mat& distCoeffs,vector<float>& perViewErrors)
{//此处省略...
}static void calcChessboardCorners(Size boardSize, float squareSize, vector<Point3f>& corners, Pattern patternType = CIRCLES_GRID)
{//省略...//本文中用到的标定板,在该函数中的参数为:boardSize.width=7,boardSize.height=7,squareSize=0.025(此处单位为米)
}
//执行标定,包括计算重投影误差
static bool runCalibration(vector<vector<Point2f> > imagePoints,Size imageSize, Size boardSize, Pattern patternType,float squareSize, float aspectRatio,int flags, Mat& cameraMatrix, Mat& distCoeffs,vector<Mat>& rvecs, vector<Mat>& tvecs,vector<float>& reprojErrs,double& totalAvgErr)
{//省略...
}//保存相机参数
static void saveCameraParams(const string& filename,Size imageSize, Size boardSize,float squareSize, float aspectRatio, int flags,const Mat& cameraMatrix, const Mat& distCoeffs,const vector<Mat>& rvecs, const vector<Mat>& tvecs,const vector<float>& reprojErrs,const vector<vector<Point2f> >& imagePoints,double totalAvgErr)
{//省略...
}//读取字符串
static bool readStringList(const string& filename, vector<string>& l)
{l.resize(0);FileStorage fs(filename, FileStorage::READ);if (!fs.isOpened())return false;FileNode n = fs["images"];if (n.type() != FileNode::SEQ)return false;FileNodeIterator it = n.begin(), it_end = n.end();for (; it != it_end; ++it)l.push_back((string)*it);return true;
}//运行并保存
static bool runAndSave(const string& outputFilename,const vector<vector<Point2f> >& imagePoints,Size imageSize, Size boardSize, Pattern patternType, float squareSize,float aspectRatio, int flags, Mat& cameraMatrix,Mat& distCoeffs, bool writeExtrinsics, bool writePoints)
{//省略...
}int main(int argc, char** argv)
{cout << argc << endl;for (size_t i = 0; i < argc; i++){cout << argv[i] << endl;}Size boardSize, imageSize;float squareSize, aspectRatio;Mat cameraMatrix, distCoeffs;string outputFilename;string inputFilename = "";int i, nframes;bool writeExtrinsics, writePoints;bool undistortImage = false;int flags = 0;VideoCapture capture;bool flipVertical;bool showUndistorted;bool videofile;int delay;clock_t prevTimestamp = 0;int mode = DETECTION;int cameraId = 0;vector<vector<Point2f> > imagePoints;vector<string> imageList;Pattern pattern = CIRCLES_GRID;//标定图案类型,对称圆形图案cv::CommandLineParser parser(argc, argv,"{help ||}{w|7|}{h|7|}{pt|circles|}{n|30|}{d|1000|}{s|0.025|}{o|D:/opencv/cameracalibration/out_camera_params_25x25_circleboard.yml|}""{op|D:/opencv/cameracalibration/Detected_feature_points.yml|}{oe|D:/opencv/cameracalibration/Extrinsic_parameters_circleboard.yml|}{zt||}{a|1|}{p||}{v||}{V||}{su||}""{input_data|D:/opencv/cameracalibration/VID25x25_CircleGrid.xml|}");//命令行参数赋值,参数说明:w,h为标定板宽,高; pt为标定图案类型; n为读取图片的张数; d为相机在线抓图的时间间隔(ms)(本代码//为离线标定,该参数可以不设置); o为程序输出的相机内参、外参文件(自定义的文件); op为输出检测到特征点的文件(自定义的文件); //oe为输出的相机外参数(这里可以不用设置,因为外参数已经在o中输出了,标定完后该文件为空文件); a为比例系数,默认为1; //input_data为存放图片路径的xml文件,本代码读取的VID25X25_CircleGrid.xml文件内容见图3。if (parser.has("help")){help();return 0;}boardSize.width = parser.get<int>("w");boardSize.height = parser.get<int>("h");if (parser.has("pt")){string val = parser.get<string>("pt");if (val == "circles")pattern = CIRCLES_GRID;else if (val == "acircles")pattern = ASYMMETRIC_CIRCLES_GRID;else if (val == "chessboard")pattern = CHESSBOARD;elsereturn fprintf(stderr, "Invalid pattern type: must be chessboard or circles\n"), -1;}squareSize = parser.get<float>("s");nframes = parser.get<int>("n");aspectRatio = parser.get<float>("a");delay = parser.get<int>("d");writePoints = parser.has("op");writeExtrinsics = parser.has("oe");if (parser.has("a"))flags |= CALIB_FIX_ASPECT_RATIO;if (parser.has("zt"))flags |= CALIB_ZERO_TANGENT_DIST;if (parser.has("p"))flags |= CALIB_FIX_PRINCIPAL_POINT;flipVertical = parser.has("v");videofile = parser.has("V");if (parser.has("o"))outputFilename = parser.get<string>("o");showUndistorted = parser.has("su");if (isdigit(parser.get<string>("input_data")[0]))cameraId = parser.get<int>("input_data");elseinputFilename = parser.get<string>("input_data");if (!parser.check()){help();parser.printErrors();return -1;}if (squareSize <= 0)return fprintf(stderr, "Invalid board square width\n"), -1;if (nframes <= 3)return printf("Invalid number of images\n"), -1;if (aspectRatio <= 0)return printf("Invalid aspect ratio\n"), -1;if (delay <= 0)return printf("Invalid delay\n"), -1;if (boardSize.width <= 0)return fprintf(stderr, "Invalid board width\n"), -1;if (boardSize.height <= 0)return fprintf(stderr, "Invalid board height\n"), -1;if (!inputFilename.empty()){if (!videofile && readStringList(inputFilename, imageList))mode = CAPTURING;elsecapture.open(inputFilename);}elsecapture.open(cameraId);if (!capture.isOpened() && imageList.empty())return fprintf(stderr, "Could not initialize video (%d) capture\n", cameraId), -2;if (!imageList.empty())nframes = (int)imageList.size();if (capture.isOpened())printf("%s", liveCaptureHelp);namedWindow("Image View", 1);for (i = 0;; i++){Mat view, viewGray;bool blink = false;if (capture.isOpened()){Mat view0;capture >> view0;view0.copyTo(view);}else if (i < (int)imageList.size())view = imread(imageList[i], 1);if (view.empty()){if (imagePoints.size() > 0)runAndSave(outputFilename, imagePoints, imageSize,boardSize, pattern, squareSize, aspectRatio,flags, cameraMatrix, distCoeffs,writeExtrinsics, writePoints);break;}imageSize = view.size();if (flipVertical)flip(view, view, 0);vector<Point2f> pointbuf;cvtColor(view, viewGray, COLOR_BGR2GRAY);bool found;switch (pattern){case CHESSBOARD:found = findChessboardCorners(view, boardSize, pointbuf,CALIB_CB_ADAPTIVE_THRESH | CALIB_CB_FAST_CHECK | CALIB_CB_NORMALIZE_IMAGE);break;case CIRCLES_GRID:found = findCirclesGrid(view, boardSize, pointbuf,CALIB_CB_SYMMETRIC_GRID);break;case ASYMMETRIC_CIRCLES_GRID:found = findCirclesGrid(view, boardSize, pointbuf, CALIB_CB_ASYMMETRIC_GRID);break;default:return fprintf(stderr, "Unknown pattern type\n"), -1;}if (found)drawChessboardCorners(view, boardSize, Mat(pointbuf), found);//在原图中绘制找到的圆心点,图4为其中的一幅图string msg = mode == CAPTURING ? "100/100" :mode == CALIBRATED ? "Calibrated" : "Press 'g' to start";int baseLine = 0;Size textSize = getTextSize(msg, 1, 1, 1, &baseLine);Point textOrigin(view.cols - 2 * textSize.width - 10, view.rows - 2 * baseLine - 10);if (mode == CAPTURING){if (undistortImage)msg = format("%d/%d Undist", (int)imagePoints.size(), nframes);elsemsg = format("%d/%d", (int)imagePoints.size(), nframes);}putText(view, msg, textOrigin, 1, 1,mode != CALIBRATED ? Scalar(0, 0, 255) : Scalar(0, 255, 0));if (blink)bitwise_not(view, view);if (mode == CALIBRATED && undistortImage){Mat temp = view.clone();undistort(temp, view, cameraMatrix, distCoeffs);}imshow("Image View", view);char key = (char)waitKey(capture.isOpened() ? 50 : 500);if (key == 27)break;if (key == 'u' && mode == CALIBRATED)undistortImage = !undistortImage;if (capture.isOpened() && key == 'g'){mode = CAPTURING;imagePoints.clear();}if (mode == CAPTURING && imagePoints.size() >= (unsigned)nframes){if (runAndSave(outputFilename, imagePoints, imageSize,boardSize, pattern, squareSize, aspectRatio,flags, cameraMatrix, distCoeffs,writeExtrinsics, writePoints))mode = CALIBRATED;elsemode = DETECTION;if (!capture.isOpened())break;}}if (!capture.isOpened() && showUndistorted){Mat view, rview, map1, map2;initUndistortRectifyMap(cameraMatrix, distCoeffs, Mat(),getOptimalNewCameraMatrix(cameraMatrix, distCoeffs, imageSize, 1, imageSize, 0),imageSize, CV_16SC2, map1, map2);for (i = 0; i < (int)imageList.size(); i++){view = imread(imageList[i], 1);if (view.empty())continue;//undistort( view, rview, cameraMatrix, distCoeffs, cameraMatrix );remap(view, rview, map1, map2, INTER_LINEAR);imshow("Image View", rview);char c = (char)waitKey();if (c == 27 || c == 'q' || c == 'Q')break;}}return 0;
}
3.标定结果分析
OpenCv标定得到的相机参数矩阵为:
本次标定使用的镜头焦距 f=8mm, 像元尺寸为3.45μm,图像尺寸为2040x1200。
Halcon标定得到的内参为(k,sx,sy,cx,cy)将其转换为式(1)中的矩阵。表1为OpenCv和Halcon标定的对比数据。
本实验中,镜头与世界坐标系z=0平面的距离为112cm左右。从表中可以看出,OpenCv标定的重投影误差为0.01759,精度较高,小于Halcon标定的0.069。(OpenCv标定过程中采用了5项畸变系数k1,k2,p1,p2,k3;Halcon标定中只考虑径向畸变k,表中没有列出)
需要指出的是,实验数据来源于对同一组图片的标定。Halcon中对相机的标定,采用的方法是Tsai两步标定法,需要预先给出相机的内参数,理论上具有较高的标定精度。但是在本次的Halcon标定中,由于采用的是离线采集的图片,在标定过程中提示图片过曝、旋转角度没有覆盖全、标定图案偏小、光照不均匀等图像品质问题,因此标定的精度不高。如果使用halcon在线抓图标定,可以有效避免图像品质问题,从而大幅度提高标定精度,预计标定精度和OpenCv标定相当或者更高。标定结果表明,OpenCv标定算法的鲁棒性更好,而Halcon标定算法对采集到的图像品质要求较高,也可以理解为高精度标定下对图像品质的高要求。
ps:如有错误,谢谢指出。转载请注明出处。