圆点标定板的标志点提取、标定实验总结

〇、环境

OpenCV 3.4+https://opencv.org/releases/
Matlab 带有Matlab calib toolbox:http://www.vision.caltech.edu/bouguetj/calib_doc/
(注:Matlab calib toolbox的好处就在于,你可以直接编辑、修改它的函数,以及看它的操作流程,同时在你的程序里也可以直接调用它的函数,自动化处理实验数据)
VS2015 msvc 的64位编译器 

一、总述&背景

Matlab只能标定棋盘格类型的靶标。尝试用OepnCV去标定圆点靶标,进行一系列的实验过程。
之前看到了一篇写了标定的文章,但是没给源码,决定自己动手试一试。
https://blog.csdn.net/a361251388leaning/article/details/54171233

先放出结论:用OpenCV进行圆点靶标的标定是挺准确的。同样它也考虑到了去透视投影变换去处理问题。而且这种方法只是作为它自己的方法的一种补救手段(说明它自己的方法效果更好),我们可以放心大胆的使用OpenCV的圆点标定。

二、调研&原理

圆点靶标相对于棋盘格靶标来说,具有一定的局限性,同时又有其独特的优势。

优点:在针对一些诸如投影仪和相机的标定过程中,需要知道特征点中心的投影仪投射的光的信息(如相移法)。但是我们的棋盘格由于是特征点是角点,所以不容易获得特征点中心的光信息。这是圆点靶标相对于棋盘格的一个优势。如华中科技大学的一篇关于相机和投影仪的标定文章《Accurate calibration method for a structured light system》,目前圆点标定板在三维扫描仪中应用更加广泛。

缺点:缺点也十分明显,圆点靶标摆放的位姿在与相机光轴不垂直的情况下,特征点的中心拍摄图像的特征点的中心(或者说是重心)这个时候不论用Steger方法提取光点中心,还是用OpenCV原生的blob方法获取斑点中心,效果理论上来说应该都不是可靠的,或者说精度较高的。在实际的拍摄过程中,我们不可能保证圆点靶标的位姿与相机光轴垂直。

针对上述的缺点进行了很多的研究。《机器视觉》(张广军)详细阐述了圆点在相机下的成像模型,写明了一般的椭圆(圆斑也是一种椭圆)在经过透视投影变换后的数学模型,但是未给出如何获得标志点的中心的方法。《光栅投影 三维精密测量》(达飞鹏)中不仅提到了如何获得圆点的中心,也提出了一种矫正圆点提取不准确的方法——采用同心圆环进行标定,通过圆环数学模型解出圆心的实际位置,可以得到更好的圆点中心,但是需要特殊形式的靶标。《Robust Detection and Ordering of Ellipses on a Calibration Pattern》(LPGC Luis Alvarez等人的讲义)中提到了如何用椭圆霍夫变换结合切线的方法获得更加精密的圆心坐标。但是同样的,一个是无法确定椭圆中心就是圆的中心,还一个问题就是用Hough变换去算圆的中心(或者说一系列基于图像边缘的方法获得中心),如果不采用作者类似的比较复杂的方法去获得中心,结果都将会有较大的偏差,且图像大了之后这样的方法将严重影响执行效率(因为复杂)。还有一些依据几何关系的方法,一篇中文文献《视觉标定中圆心投影点精确定位
》,感觉理解起来挺容易,但是上手也不好弄,而且采用边缘的方法个人还是觉得误差稍微较大。。在《光栅投影 三维精密测量》里也提到了一种基于边缘的方法,实现起来也较为复杂。
OpenCV原生的圆点提取尽管不是十分的鲁棒(有时候不容易检测出圆点的位置),但背后也有很复杂的原理,诸如网格分析以及聚类等。

实验以双目系统为例,参考了《Mastering Opencv》第三章的marklessAR的一些小技巧,思想很简单,我们首先用blobdetecto提取标定板四个角点的圆点的中心,然后求出单应矩阵H,调用openCV的Warpperspective函数,基于H矩阵转换拍摄图像,获得“正视“的靶标图像,为了减少噪声影响,采用一层高斯滤波处理转换图像,之后调用OpenCV的findCirclesGrid函数获得各标志点的中心,最后再用H矩阵反算回去。注意,这里由于采用的是一个H矩阵,所以并不存在计算误差(即结果与H无关),单应矩阵H仅仅用来视角转变。

最后,我们将生成的点列存储进txt文件,然后通过Matlab导入,再用Matlab标定工具箱进行标定,便可以获得最后的标定结果,以及不确定度和精度评价(重投影误差),与OpenCV原生的圆点提取方法进行对比。

三、代码

C++:提取点

/*
	By J.A 2019.6.12

	提取圆点靶标圆的中心,为了实现较好的标定手法,需要提高我们的提点的准确度
	圆形在透视投影变换之后,不再是一个圆形,而是椭圆形。
	一般的方法用于获取椭圆的中心,但是椭圆的中心不是圆的中心。
	所以我们需要先提取四个最远端的特征点的坐标,求解H矩阵,并进行透视畸变矫正。
	之后针对矫正图,调用OpenCV的函数获得圆点靶标的特征点的中心。
	最后将提到的点重建到图像平面(反透视投影变换, 这样我们获得的圆心才是真正的圆心。

	注:
	1、	Halcon的标定板,采用一个五边形作为外框,外框是一种线特征,不会随着透视投影产生位置畸变。(实际上,外框如果是一系列棋盘格构成的也行)。
		在这种情况下,我们可以先识别外框,然后依然依照上述的思想,获得特征点的坐标。据传,这也是Halcon的标定所采用的方法,由于本方法提取的坐标一开始并不是准确的,所以难免产生误差
		可以预计,本方法相对于上述的方法精度低一些。但是我们可以采用迭代的思想,在变换完成求得h矩阵后,再进行之前的操作,获得更加细化的H矩阵,效率上不高,但是效果应该可以做到相当
	2、 OpenCV寻找圆点标定板的函数 调用的是featuredector寻找点的中心,需要进行阈值化操作,并采用一定的过滤手段,由于我们的靶标上反射的光均匀程度不一,而且我们的相机也有一定的噪声
		个人建议首先采用一定大小的高斯滤波对矫正了透视投影畸变的图案进行滤波,以使灰度值变均匀,同时也不太影响我们的中心提取。结合圆中心提取的思想,我们也可以用steger方法获得更加
		精准的圆心,从而提高标定的精度
	3、	在诸如相移法等一些涉及到投影仪和相机的标定过程中,我们需要能够知道特征点的具体的其他信息,这个时候采用角点靶标(张正友)的方法就不合适了,必须采用圆点靶标,从而能够获得圆
		心的相位信息,提高整体测量系统的精度。
*/

//openCV 标准头文件
#include 
#include 
#include 
#include 
#include 

//c++ 标准头文件
#include 
#include 
#include 
#include 

#include 
#include 

//嗯 尽管不是一个好习惯,但是我还是用了(方便&人生苦短)
using namespace std;
using namespace cv;

//一些全局变量
const int WINDOW_SIZE_HEIGHT = 61;
const int WINDOW_SIZE_WIDTH = 61;
int g_rough_center_cnt = 0;

//用于参数回调
struct Params
{
	Mat p_ori_img;
	vector point;
	string this_window_name;
};

//靶标的具体尺寸信息,注意单位全部为mm
struct Calib_Board
{
	Size spots_size;	//靶标上点的尺寸
	Size mm_size;		//毫米尺寸
	float dx;			//纵向点间距
	float dy;			//横向点间距
	float ds;			//小点直径
	float dl;			//大点直径
};

//注 本次使用的靶标是11*9的靶标,点距15mm,大圆直径7mm 小圆直径3.5mm 具体参见 GR180-11*9靶标 外形尺寸为180*150mm(注意,这个参数也用于重建我们的图像 大小为900*750)
Calib_Board board = { Size(11,9),Size(180,150),15,15,3.5,7 };

void drawCross(Mat& pattern, Point2f center,Scalar color)
{
	if (pattern.channels() == 1)
	{
		cvtColor(pattern, pattern, COLOR_GRAY2RGB);
	}
	line(pattern, Point2f(center.x + 5, center.y), Point2f(center.x - 5, center.y), color);
	line(pattern, Point2f(center.x, center.y + 5), Point2f(center.x, center.y - 5), color);
}

void drawSport(Mat& pattern, Point2f center, Scalar color)
{
	if (pattern.channels() == 1)
	{
		cvtColor(pattern, pattern, COLOR_GRAY2RGB);
	}
	circle(pattern, center, 4, color, -1);
}

//选点的回调函数
void On_pickpoints(int event, int x0, int y0, int flags, void *v_params)
{
	string outlog;
	Params* params = (Params*) v_params;	//将图像转出来
	cv::Mat ori_img = params->p_ori_img;	//浅拷贝 注意 不要用标记污染了我们的图像
	cv::Mat show_img;

	int height_2 = (WINDOW_SIZE_HEIGHT - 1) / 2;
	int width_2 = (WINDOW_SIZE_WIDTH - 1) / 2;

	if (event == CV_EVENT_LBUTTONDOWN)	//左键按下
	{
		if (params->point.size() == 4)
			params->point.clear();

		//在这个周围圈出一个方形,这个方形的大小可以指定
		Rect roi_rect = Rect(
			(x0 - width_2) < 0 ? 0 : (x0 - width_2),
			(y0 - height_2) < 0 ? 0 : (y0 - height_2),
			(x0 + width_2) >= (ori_img.cols) ? (ori_img.cols - x0) * 2 : (width_2 * 2 + 1),
			(y0 + height_2) >= (ori_img.rows) ? (ori_img.rows - y0) * 2 : (height_2 * 2 + 1)
		);
		Mat roi_img = ori_img(roi_rect).clone();
		SimpleBlobDetector::Params d_params;
		d_params.minThreshold = 20;
		d_params.maxThreshold = 150;
		d_params.filterByArea = false;
		d_params.filterByColor = false;
		d_params.filterByInertia = false;
		d_params.filterByCircularity = false;
		d_params.filterByConvexity = true;
		d_params.minConvexity = 0.9;
		d_params.maxConvexity = 1;

		Ptr blobsDetector = SimpleBlobDetector::create(d_params);
		vector centers;
		blobsDetector->detect(roi_img, centers);

#if _DEBUG
		//绘制圆斑的中心位置
		namedWindow("circle pattern", 0);
		drawKeypoints(roi_img, centers, roi_img);
		resizeWindow("circle pattern", roi_img.cols * 4, roi_img.rows * 4);
		imshow("circle pattern", roi_img);
#endif

		//如果检测到唯一的计算点,那么就拿来当我的标志点了
		if (centers.size() == 1)
		{
			//将这个点进行平移
			centers[0].pt.x += roi_rect.x;
			centers[0].pt.y += roi_rect.y;
			params->point.push_back(centers[0]);
		}
		drawKeypoints(ori_img, params->point, show_img, Scalar(0, 0, 200));
		imshow(params->this_window_name, show_img);

		if (params->point.size() == 4)
			cout << "If you want to pick them again, just chose the first point. Or press the enter." << endl;
	}
}


void Get_All_Files(const string& path, const string& format, vector& files)
{
	files.clear();
	intptr_t  hFile = 0;//文件句柄  
	struct _finddata_t fileinfo;//文件信息 
	string p;
	if ((hFile = _findfirst(p.assign(path).append("\\*" + format).c_str(), &fileinfo)) != -1) //文件存在
	{
		do
		{
			files.push_back(fileinfo.name);//如果不是文件夹,储存文件名
		} while (_findnext(hFile, &fileinfo) == 0);
		_findclose(hFile);
	}
}

void Save_Points2File(const string& file_name, vector& points)
{
	//转成Matlab可以读取的形式
	ofstream outfile;
	outfile.open(file_name);
	if (!outfile)
	{
		return;
	}
	else
	{
		for (int i = 0; i < points.size(); i++)
		{
			outfile << points[i].x << " " << points[i].y << endl;
		}
	}
}

int main()
{
	string dir_path = "test_pic";
	vector file_names;
	Get_All_Files(dir_path, ".bmp", file_names);
	/*一个pos的角点提取过程*/
	for (int file_idx = 0; file_idx < file_names.size(); file_idx++)
	{

		/*
		数据准备工作,这一段不需要重复调用运行
		*/

		vector check_points;
		Mat img;
		string windows_name;
		Params params = { img,check_points,windows_name };
		vector calib_points_2d(4);
		calib_points_2d[0] = Point2f(1.0*board.dx, 1.0*board.dy);
		calib_points_2d[1] = Point2f(board.spots_size.width*board.dx, 1.0*board.dy);
		calib_points_2d[2] = Point2f(board.spots_size.width*board.dx, board.spots_size.height*board.dy);
		calib_points_2d[3] = Point2f(1.0*board.dx, board.spots_size.height*board.dy);
		vector img_points_2d(4);
		vector calib_points_2d_f(4);
		vector circle_centers_p;
		vector circle_centers;
		vector old_circle_centers;

		/*
			首先 提取四个边角上的点 这个坐标不用过于精准,只需要能够获得就行,当然,可以做到更加的精准,这个准确度越高,效果越好
		*/

		g_rough_center_cnt = 0;
		//读取图像,灰度化
		string file_name = dir_path + "/" + file_names[file_idx];
		Mat calib_img = imread(file_name, 0);

		if (calib_img.empty())
		{
			cout << "Error: Image loading failed." << endl;
			return -1;
		}
		else
		{
			cout << "Loaded the picture " + file_name + "." << endl;
		}
		Mat show_img = calib_img.clone();
		//显示图像,并创建鼠标点击的方式(回调函数)
		cout << "Please chose the four corner points on the border." << endl;

		params.point.clear();
		params.p_ori_img = calib_img.clone();
		params.this_window_name = file_name;

		namedWindow(file_name, 0);
		setMouseCallback(file_name, On_pickpoints, (void*)¶ms);
		resizeWindow(file_name, calib_img.cols / 2, calib_img.rows / 2);
		imshow(file_name, show_img);
		waitKey(0);		//选择完成后按回车结束

		/*
			矫正透视投影畸变
		*/

		cout << "Warp the calibration board." << endl;
		img_points_2d[0] = params.point[0].pt;
		img_points_2d[1] = params.point[1].pt;
		img_points_2d[2] = params.point[2].pt;
		img_points_2d[3] = params.point[3].pt;
		int factor = min(calib_img.cols / board.mm_size.width, calib_img.rows / board.mm_size.height);
		calib_points_2d[0] = Point2f(calib_points_2d[0].x*factor, calib_points_2d[0].y*factor);
		calib_points_2d[1] = Point2f(calib_points_2d[1].x*factor, calib_points_2d[1].y*factor);
		calib_points_2d[2] = Point2f(calib_points_2d[2].x*factor, calib_points_2d[2].y*factor);
		calib_points_2d[3] = Point2f(calib_points_2d[3].x*factor, calib_points_2d[3].y*factor);

		Mat rough_H = findHomography(calib_points_2d, img_points_2d);
		Mat warp_calib_img;

		warpPerspective(calib_img, warp_calib_img, rough_H, Size(board.mm_size.width*factor, board.mm_size.height*factor), cv::WARP_INVERSE_MAP | cv::INTER_CUBIC);

		/*
			提取圆点,采用openCV提供的函数,找到圆点的中心,并显示
		*/

		GaussianBlur(warp_calib_img, warp_calib_img, Size(0, 0), board.ds*factor / 8);
		circle_centers_p.clear();
		circle_centers.clear();
		bool pattern_found_flag = findCirclesGrid(255 - warp_calib_img, board.spots_size, circle_centers_p, CALIB_CB_SYMMETRIC_GRID);	//注意,OpenCV的只能识别白底黑点,我们是黑底白点,可以对照进行修改
		if (pattern_found_flag)
		{
			cout << "Warp pattern found." << endl;
			/*
				这里可以针对提取出的点做一定的精化处理,如对每一个角点,采用steger方法获得更加准确的点的坐标(对OpenCV的simpleBlob实在不放心)
				或者再进行一次透视投影变换,将图像变换成更好的角度再去提点。
			*/
		}
#if _DEBUG
		drawChessboardCorners(warp_calib_img, board.spots_size, circle_centers_p, pattern_found_flag);
		namedWindow("rough perspective calib board", 0);
		resizeWindow("rough perspective calib board", Size(board.mm_size.width * 3, board.mm_size.height * 3));
		imshow("rough perspective calib board", warp_calib_img);
		waitKey(0);
#endif
		/*
			再次利用H矩阵,将之前的坐标反算到图像中,并显示其在图像上的位置以检验正确性 并和一般的提取方法进行对比
		*/

		for (int i = 0; i < circle_centers_p.size(); i++)
		{
			Mat p_point = (Mat_(3, 1) << (double)circle_centers_p[i].x, (double)circle_centers_p[i].y, 1);
			Mat point = rough_H*p_point;
			circle_centers.push_back(Point2f(float(point.ptr(0)[0] / point.ptr(2)[0]), float(point.ptr(1)[0] / point.ptr(2)[0])));
			//绘制在图像上
			drawSport(show_img, Point2f(float(point.ptr(0)[0] / point.ptr(2)[0]), float(point.ptr(1)[0] / point.ptr(2)[0])), Scalar(200, 0, 0));
		}

		//原始方法获得点的位置
		old_circle_centers.clear();
		bool old_found = findCirclesGrid(255 - calib_img, board.spots_size, old_circle_centers, CALIB_CB_SYMMETRIC_GRID);
		if (old_found)
		{
			cout << "Original pic found." << endl;
			for (int i = 0; i < old_circle_centers.size(); i++)
			{
				drawSport(show_img, old_circle_centers[i], Scalar(0, 0, 200));
			}
		}
		imshow(file_name, show_img);
		waitKey(0);
		destroyWindow(file_name);

		/*
			可以将提取的点进行保存,输入Matlab进行处理,也可以将其直接用OpenCV进行处理。 但是OepnCV不会输出不确定度等一些信息
		*/
		
		int length_str = file_names[file_idx].length();
		string txt_name = file_names[file_idx];
		txt_name.erase(length_str - 4, 4);
		string old_txt_path = dir_path + "/old_" + txt_name + ".txt";
		string new_txt_path = dir_path + "/" + txt_name + ".txt";
		Save_Points2File(old_txt_path, old_circle_centers);
		Save_Points2File(new_txt_path, circle_centers);
	}
}

Matlab: 标定

%用于获得图像的所有提点的结果 并初始化整个标定过程,确定标定的参数。
%调用 Calib_toolbox 去实现

%% 初始化标定流程 包括调用Calib_tool时所需要调用的参数

%不存储一些东西,但是也无关紧要 需要自行修改 saving_calib.m 207行
saving_ascii = 0;  
%靶标点间距 单位m
dX = 0.15;        
dY = 0.15;
%图像像素大小 单位pix
nx = 2048;
ny = 1088;
%图案点的数量 横向 纵向
Target_Nx = 11;
Target_Ny = 9;
%图案点的间距数量 
Target_dx = 1;
Target_dy = 1;
%生成靶标点的位置,起始坐标为(0,0)
Points_Num = Target_Nx*Target_Ny;
X = zeros(Points_Num,3);
index = 1;
for i = 1:Target_Ny
    for j = 1:Target_Nx
        X(index,1) = (j-1)*Target_dx;
        X(index,2) = (i-1)*Target_dy;
        X(index,3) = 1;
        index = index+1;
    end
end
X=X';
%% 获得存储的数据 包括调用Calib_tool时所需要调用的参数

%注意输入正则表达式
base_name_left = 'old_l*.txt';  %old_l*.txt 
base_name_right = 'old_r*.txt'; %old_r*.txt

src_dir_name = 'src_data';
src_dir_path = [pwd,filesep,src_dir_name];
all_left_txt = dir([src_dir_name,filesep,base_name_left]);
all_right_txt = dir([src_dir_name,filesep,base_name_right]);

%% 左相机 标定流程
ind_read = 1:length(all_left_txt);
n_ima = length(all_left_txt);

for i=1:length(all_left_txt)
    order = ['x_',num2str(i),'=(load([src_dir_path,filesep,''',all_left_txt(i).name,''']))'';'];
    eval(order);
    order = ['X_',num2str(i),'=X;'];
    eval(order);
end

%调用Matlab claib tool box标定
go_calib_optim_iter;
%显示标定结果(外参)
ext_calib;              %没有网格是因为 no_grid = 0
%保存 并且重命名
saving_calib;
movefile('Calib_Results.mat','Calib_Results_left.mat');
pause;
%% 右相机 标定流程
ind_read = 1:length(all_right_txt);
n_ima = length(all_right_txt);

for i=1:length(all_right_txt)
    order = ['x_',num2str(i),'=(load([src_dir_path,filesep,''',all_right_txt(i).name,''']))'';'];
    eval(order);
    order = ['X_',num2str(i),'=X;'];
    eval(order);
end

%调用Matlab calib tool box标定
go_calib_optim_iter;
%展示标定效果(外参)
ext_calib;
%保存 并且重命名
saving_calib;
movefile('Calib_Results.mat','Calib_Results_right.mat');
pause;

%% 双目标定
clear;
load_stereo_calib_files;
go_calib_stereo;

%展示标定效果
ext_calib_stereo;
saving_stereo_calib;
pause;

close;
    

GItHub 仓库 https://github.com/Joshua-Astray/CircleCalib

四、实验结果

左右相机各八张图。标定结果如下

1、原生OepnCV提点:

左相机:
Focal Length:          fc = [ 2315.41543   2314.44198 ] +/- [ 1.39444   1.55571 ]
Principal point:       cc = [ 1004.26881   546.86823 ] +/- [ 1.75118   1.19991 ]
Skew:             alpha_c = [ 0.00000 ] +/- [ 0.00000  ]   => angle of pixel axes = 90.00000 +/- 0.00000 degrees
Distortion:            kc = [ -0.11966   0.21574   0.00070   0.00034  0.00000 ] +/- [ 0.00185   0.00999   0.00013   0.00018  0.00000 ]
Pixel error:          err = [ 0.05132   0.04595 ]
右相机:
Focal Length:          fc = [ 2317.34738   2316.77124 ] +/- [ 2.56479   2.43035 ]
Principal point:       cc = [ 1033.39612   532.06437 ] +/- [ 1.46790   1.79289 ]
Skew:             alpha_c = [ 0.00000 ] +/- [ 0.00000  ]   => angle of pixel axes = 90.00000 +/- 0.00000 degrees
Distortion:            kc = [ -0.11789   0.20959   0.00072   0.00013  0.00000 ] +/- [ 0.00221   0.00859   0.00015   0.00023  0.00000 ]
Pixel error:          err = [ 0.04859   0.04606 ]
双目结合优化:
Intrinsic parameters of left camera:
Focal Length:          fc_left = [ 2315.41543   2314.44198 ] � [ 1.39444   1.55571 ]
Principal point:       cc_left = [ 1004.26881   546.86823 ] � [ 1.75118   1.19991 ]
Skew:             alpha_c_left = [ 0.00000 ] � [ 0.00000  ]   => angle of pixel axes = 90.00000 � 0.00000 degrees
Distortion:            kc_left = [ -0.11966   0.21574   0.00070   0.00034  0.00000 ] � [ 0.00185   0.00999   0.00013   0.00018  0.00000 ]
Intrinsic parameters of right camera:
Focal Length:          fc_right = [ 2317.34738   2316.77124 ] � [ 2.56479   2.43035 ]
Principal point:       cc_right = [ 1033.39612   532.06437 ] � [ 1.46790   1.79289 ]
Skew:             alpha_c_right = [ 0.00000 ] � [ 0.00000  ]   => angle of pixel axes = 90.00000 � 0.00000 degrees
Distortion:            kc_right = [ -0.11789   0.20959   0.00072   0.00013  0.00000 ] � [ 0.00221   0.00859   0.00015   0.00023  0.00000 ]
Extrinsic parameters (position of right camera wrt left camera):
Rotation vector:             om = [ -0.00607   -0.53968  0.12631 ]
Translation vector:           T = [ 15.22077   0.99095  4.43965 ]

2、经过透视投影变换,再使用OpenCV:

左相机:
Focal Length:          fc = [ 2316.13019   2315.17456 ] +/- [ 2.41018   2.68948 ]
Principal point:       cc = [ 1004.39188   545.99618 ] +/- [ 3.03002   2.07479 ]
Skew:             alpha_c = [ 0.00000 ] +/- [ 0.00000  ]   => angle of pixel axes = 90.00000 +/- 0.00000 degrees
Distortion:            kc = [ -0.12016   0.21661   0.00069   0.00048  0.00000 ] +/- [ 0.00320   0.01729   0.00022   0.00031  0.00000 ]
Pixel error:          err = [ 0.08572   0.08272 ]

右相机:
Focal Length:          fc = [ 2317.60885   2317.13978 ] +/- [ 3.98569   3.77646 ]
Principal point:       cc = [ 1033.67020   531.92901 ] +/- [ 2.28144   2.78857 ]
Skew:             alpha_c = [ 0.00000 ] +/- [ 0.00000  ]   => angle of pixel axes = 90.00000 +/- 0.00000 degrees
Distortion:            kc = [ -0.11869   0.21042   0.00075   0.00027  0.00000 ] +/- [ 0.00343   0.01337   0.00024   0.00035  0.00000 ]
Pixel error:          err = [ 0.07695   0.07001 ]

双目结合优化:
Intrinsic parameters of left camera:
Focal Length:          fc_left = [ 2319.15670   2317.64210 ] � [ 3.53257   3.65239 ]
Principal point:       cc_left = [ 1003.62948   548.01620 ] � [ 5.94986   4.89361 ]
Skew:             alpha_c_left = [ 0.00000 ] � [ 0.00000  ]   => angle of pixel axes = 90.00000 � 0.00000 degrees
Distortion:            kc_left = [ -0.11571   0.19998   0.00106   0.00020  0.00000 ] � [ 0.00830   0.04409   0.00053   0.00078  0.00000 ]
Intrinsic parameters of right camera:
Focal Length:          fc_right = [ 2312.39131   2312.83741 ] � [ 4.97272   4.85284 ]
Principal point:       cc_right = [ 1029.66665   537.78577 ] � [ 6.16933   5.05344 ]
Skew:             alpha_c_right = [ 0.00000 ] � [ 0.00000  ]   => angle of pixel axes = 90.00000 � 0.00000 degrees
Distortion:            kc_right = [ -0.12141   0.21993   0.00051   0.00002  0.00000 ] � [ 0.00931   0.03731   0.00048   0.00094  0.00000 ]
Extrinsic parameters (position of right camera wrt left camera)
Rotation vector:             om = [ -0.00574   -0.53852  0.12554 ] � [ 0.00272   0.00356  0.00087 ]
Translation vector:           T = [ 15.25232   0.93884  4.32328 ] � [ 0.02378   0.01589  0.07432 ]
 

实验表明,经过透视投影变换获得的图像用于标定实际上并不理想。

3、分析

在OpenCV的源码中找到了一定的答案。实际上OpenCV似乎已经考虑了透视投影变换造成的一系列问题。

采用OpenCV3.4.6的源码中,关于圆点标定函数的实现。(这里只截取了一部分,仅仅用来标定对称圆点靶标)

bool findCirclesGrid2(InputArray _image, Size patternSize,
                      OutputArray _centers, int flags, const Ptr &blobDetector,
                      CirclesGridFinderParameters2 parameters)
{
    CV_INSTRUMENT_REGION();

    Mat image = _image.getMat();
    std::vector centers;

    std::vector keypoints;
    blobDetector->detect(image, keypoints);
    std::vector points;
    for (size_t i = 0; i < keypoints.size(); i++)
    {
      points.push_back (keypoints[i].pt);
    }

    const int attempts = 2;
    const size_t minHomographyPoints = 4;
    Mat H;
    for (int i = 0; i < attempts; i++)
    {
      centers.clear();
      CirclesGridFinder boxFinder(patternSize, points, parameters);
      bool isFound = false;

      isFound = boxFinder.findHoles();

      if (isFound)
      {

        boxFinder.getHoles(centers);
        if (i != 0)
        {
          Mat orgPointsMat;
          transform(centers, orgPointsMat, H.inv());
          convertPointsFromHomogeneous(orgPointsMat, centers);
        }
        Mat(centers).copyTo(_centers);
        return true;
      }

      boxFinder.getHoles(centers);
      if (i < attempts)
      {
        if (centers.size() < minHomographyPoints)
          break;
        H = CirclesGridFinder::rectifyGrid(boxFinder.getDetectedGridSize(), centers, points, points);
      }
    }
    Mat(centers).copyTo(_centers);
    return false;
}

具体的源码分析在这里
http://yanghespace.com/2016/08/18/findCirclesGrid%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/

了解到采用了网格分析的方法去获得结果,当结果不太好的时候,同样也需要进行透视投影变换,从而再获得结果。
(能不准么。。一个易用的功能背后是多少人的辛劳。。)
结论就是,OpenCV的圆点标定方法获得的结果其实还是很准确的,也不太容易受到透视投影变换的影响。

五、参考资料

上述所有引用的链接和文献。

你可能感兴趣的:(标定,相机标定,圆点标定)