OpenCv相机标定——圆形标定板标定[通俗易懂]

2022-09-02 17:01:32 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

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.

代码语言:javascript复制
#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;
		else
			return fprintf(stderr, "Invalid pattern type: must be chessboard or circlesn"), -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");
	else
		inputFilename = parser.get<string>("input_data");
		
	if (!parser.check())
	{ 
   
		help();
		parser.printErrors();
		return -1;
	}
	if (squareSize <= 0)
		return fprintf(stderr, "Invalid board square widthn"), -1;
	if (nframes <= 3)
		return printf("Invalid number of imagesn"), -1;
	if (aspectRatio <= 0)
		return printf("Invalid aspect ration"), -1;
	if (delay <= 0)
		return printf("Invalid delayn"), -1;
	if (boardSize.width <= 0)
		return fprintf(stderr, "Invalid board widthn"), -1;
	if (boardSize.height <= 0)
		return fprintf(stderr, "Invalid board heightn"), -1;

	if (!inputFilename.empty())
	{ 
   
		if (!videofile && readStringList(inputFilename, imageList))
			mode = CAPTURING;
		else
			capture.open(inputFilename);
	}
	else
		capture.open(cameraId);

	if (!capture.isOpened() && imageList.empty())
		return fprintf(stderr, "Could not initialize video (%d) capturen", 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 typen"), -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);
			else
				msg = 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;
			else
				mode = 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,图像尺寸为2040×1200。   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:如有错误,谢谢指出。转载请注明出处。

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/140493.html原文链接:https://javaforall.cn

0 人点赞