圓點標定板的標誌點提取、標定實驗總結

〇、環境

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 <opencv2\core.hpp>
#include <opencv2\features2d.hpp>
#include <opencv2\highgui.hpp>
#include <opencv2\imgproc.hpp>
#include <opencv2\calib3d.hpp>

//c++ 標準頭文件
#include <iostream>
#include <vector>
#include <string>
#include <fstream>

#include <direct.h>
#include <io.h>

//嗯 儘管不是一個好習慣,但是我還是用了(方便&人生苦短)
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<KeyPoint> 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<FeatureDetector> blobsDetector = SimpleBlobDetector::create(d_params);
		vector<KeyPoint> 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<string>& 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<Point2f>& 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<string> file_names;
	Get_All_Files(dir_path, ".bmp", file_names);
	/*一個pos的角點提取過程*/
	for (int file_idx = 0; file_idx < file_names.size(); file_idx++)
	{

		/*
		數據準備工作,這一段不需要重複調用運行
		*/

		vector<KeyPoint> check_points;
		Mat img;
		string windows_name;
		Params params = { img,check_points,windows_name };
		vector<Point2f> 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<Point2f> img_points_2d(4);
		vector<Point2f> calib_points_2d_f(4);
		vector<Point2f> circle_centers_p;
		vector<Point2f> circle_centers;
		vector<Point2f> 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*)&params);
		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_<double>(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<double>(0)[0] / point.ptr<double>(2)[0]), float(point.ptr<double>(1)[0] / point.ptr<double>(2)[0])));
			//繪製在圖像上
			drawSport(show_img, Point2f(float(point.ptr<double>(0)[0] / point.ptr<double>(2)[0]), float(point.ptr<double>(1)[0] / point.ptr<double>(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<FeatureDetector> &blobDetector,
                      CirclesGridFinderParameters2 parameters)
{
    CV_INSTRUMENT_REGION();

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

    std::vector<KeyPoint> keypoints;
    blobDetector->detect(image, keypoints);
    std::vector<Point2f> 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的圓點標定方法獲得的結果其實還是很準確的,也不太容易受到透視投影變換的影響。

五、參考資料

上述所有引用的鏈接和文獻。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章