基于opencv3的本质矩阵(essential matrix)估计算例

至今opencv3面世快两年了,五一期间抽空看了下新增的几个关于双目视觉标定算法的使用。


关于双目的标定有一类方法是基于本质矩阵(Essential matrix)的标定方法,本质方程与基础方程都是刻画双目视觉极几何关系的两个方程,只不过它们所使用的坐标系不一样而已,本质方程是在相机光心坐标系下描述的双目成像几何关系,而基础方程是本质方程在相机像素坐标系下的表示。


由于本质矩阵是平移量t叉乘旋转矩阵R的结果。所以通过本质方程估计出本质矩阵,然后再对本质矩阵进行运动分解,即可得到双目摄像头之间的相互位置关系,即双目的标定。值得一提的是,由于本质矩阵的秩为5,分解出来的平移向量t只能得到方向,其模需要使用其它方法来获取。


理论上可以直接使用估计基础矩阵(Fundamental matrix)的方法来估计本质矩阵,如:8点算法,7点算法,及与之结合的最小中值法,RANSAC(随机抽样一致)法等。但是实际中使用这些方法估计出的本质矩阵的结果并不太令人满意,尤其当匹配点对趋于共面时和平移量较小时,这些方法获得的结果完全就不能用。如果用于标定的匹配点对,是在一个平面上的话,这些方法将完全失效,因为平面是一种退化结构或者叫临界曲线(Critical surfaces)。而且在opencv3之前的opencv版本只提供了求取基础矩阵F的API,虽然cv::stereoCalibrate(...)可以得到双目标定结果以及本质矩阵E,但该方法是使用平面标定版辅助的plane2plane(单应性矩阵)估计出各相机的外参数,然后由外参数直接计算出图像相互之间的旋转矩阵R和平移量t,此时获取的刚体运动参数自由度为6。


此外双目标定还有一些方法并不是基于本质矩阵来完成的,如:光束法平差(Bundle adjustment, 摄影测量学叫法)、结构重投影法(机器视觉叫法)等。在此就不进行讨论了。


由于基础矩阵的自由度为7,而本质矩阵的自由度为5。直接使用基础矩阵的估计方法来估计本质矩阵并不适合,而且本质矩阵还有一种叫做5点算法的方法可以估计本质矩阵,该算法比较复杂,涉及到高次多项式方程的求解。但是对于估计本质矩阵,5点算法会得到比7点或8点算法更好的结果,而且不受匹配点集共面的影响。该方法的具体原理可见论文David Nistér. An efficient solution to the five-point relative pose problem. Pattern Analysis and Machine Intelligence, IEEE Transactions on, 26(6):756–770, 2004.和Henrik Stewenius. Calibrated fivepoint solver.


5点算法的大概思路就是把本质方程展开,再加上本质矩阵的充要条件。化简后得到一个高次多项式方程,该多项式实数解中的一个确定出的本质矩阵是系统的真实本质矩阵,对伪解进行剔除。然后对获得的本质矩阵进行使用SVD分解而得到旋转矩阵R和单位平移量t,通常有四对旋转平移组合,进行cheirality check(三维点在相机前方约束),留下一组物理可实现的R,T。即完成双目的相互位置标定。


opencv3中的cv::findEssentialMat(...)即是基于该五点算法实现的本质矩阵的估计,附上使用代码


/**************************************************************
* an example to estimate essential matrix and relative pose of 
* binocular vision sensor using the API function of opencv3.0.0
*  
* by jah10527@126.com
* May, 1st, 2016
* 
***************************************************************/


#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;


static void calcChessboardCorners(Size boardSize, float squareSize, vector<Point3f>& corners)
{
    corners.resize(0);

    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));
}

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.getFirstTopLevelNode();
    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;
}

void calc3Dpts(vector<Point2f> &pr, vector<Point2f> &pl,Mat &MR,Mat &ML, vector<Point3f> &pts)
{
	
	Mat cof(4,4,CV_64F), z;
	double u1,v1,u2,v2;
	int len, i, j;
	len = pr.size();
	pts.reserve(len);
	for (i=0; i < len; i++)
	{
		u1=pr[i].x;
		v1=pr[i].y;
		u2=pl[i].x;
		v2=pl[i].y;
		
		*((double*)cof.data)=*((double*)MR.data+8)*u1-*((double*)MR.data);
		*((double*)cof.data+1)=*((double*)MR.data+9)*u1-*((double*)MR.data+1);
		*((double*)cof.data+2)=*((double*)MR.data+10)*u1-*((double*)MR.data+2);
		*((double*)cof.data+3)=*((double*)MR.data+11)*u1-*((double*)MR.data+3);
		*((double*)cof.data+4)=*((double*)MR.data+8)*v1-*((double*)MR.data+4);
		*((double*)cof.data+5)=*((double*)MR.data+9)*v1-*((double*)MR.data+5);
		*((double*)cof.data+6)=*((double*)MR.data+10)*v1-*((double*)MR.data+6);
		*((double*)cof.data+7)=*((double*)MR.data+11)*v1-*((double*)MR.data+7);
		*((double*)cof.data+8)=*((double*)ML.data+8)*u2-*((double*)ML.data);
		*((double*)cof.data+9)=*((double*)ML.data+9)*u2-*((double*)ML.data+1);
		*((double*)cof.data+10)=*((double*)ML.data+10)*u2-*((double*)ML.data+2);
		*((double*)cof.data+11)=*((double*)ML.data+11)*u2-*((double*)ML.data+3);
		*((double*)cof.data+12)=*((double*)ML.data+8)*v2-*((double*)ML.data+4);
		*((double*)cof.data+13)=*((double*)ML.data+9)*v2-*((double*)ML.data+5);
		*((double*)cof.data+14)=*((double*)ML.data+10)*v2-*((double*)ML.data+6);
		*((double*)cof.data+15)=*((double*)ML.data+11)*v2-*((double*)ML.data+7);
		
	//	cout << "cof:" << endl << cof << endl;
		SVD::solveZ(cof, z);
		for (j=0;j<3;j++) *((double*)z.data+j)/=*((double*)z.data+3);
		pts.push_back(Point3f(float(*((double*)z.data)), float(*((double*)z.data+1)), float(*((double*)z.data+2))));
	}
}


int main()
{
	int i, point_count = 54;
	
	Mat Kl, Dl, imagePointsL, rtL;
	Mat Kr, Dr, imagePointsR, rtR;
	
  FileStorage fs("out_camera_left.yml", FileStorage::READ);
  if( !fs.isOpened() )
    return -1;
  fs["camera_matrix"] >> Kl;
  fs["distortion_coefficients"] >> Dl;
  fs["image_points"] >> imagePointsL;
  fs["extrinsic_parameters"] >> rtL;
	fs.release(); 
	
	fs.open("out_camera_right.yml", FileStorage::READ);
    if( !fs.isOpened() )
      return -1;
  fs["camera_matrix"] >> Kr;
  fs["distortion_coefficients"] >> Dr;
  fs["image_points"] >> imagePointsR;
  fs["extrinsic_parameters"] >> rtR;
	fs.release(); 
	
	cout << "Kr:" << endl << Kr << endl;
	cout << "Kl:" << endl << Kl << endl;
	cout << "Dr:" << endl << Dr << endl;
	cout << "Dl:" << endl << Dl << endl;
	cout << "imagePointsL channels:" << endl << imagePointsL.channels() << endl;
	cout << "imagePointsR channels:" << endl << imagePointsR.channels() << endl;
	
	Mat rvecL(3,1,CV_64F,rtL.data);
	Mat tvecL(3,1,CV_64F,rtL.data+3*sizeof(double));
	Mat rvecR(3,1,CV_64F,rtR.data);
	Mat tvecR(3,1,CV_64F,rtR.data+3*sizeof(double));
	Mat rotL, rotR;
	Rodrigues(rvecL, rotL);
	Rodrigues(rvecR, rotR);
	
	vector<vector<Point2f>> ptsL, ptsR, ptsLu, ptsRu;
	ptsL.resize(1);
	ptsR.resize(1);
	ptsLu.resize(1);
	ptsRu.resize(1);

	for (int j=0;j<1;j++)
	{
		for (i=0;i<imagePointsL.cols;i++)
		{
//		  cout << imagePointsL.at<Point2f>(j,i) << endl;
			ptsL[j].push_back(imagePointsL.at<Point2f>(j,i));
			ptsR[j].push_back(imagePointsR.at<Point2f>(j,i));
		}
		undistortPoints(ptsL[j], ptsLu[j], Kl, Dl);
	  undistortPoints(ptsR[j], ptsRu[j], Kr, Dr);
	}
/*  FILE *fp;
  fp = fopen("data.txt","w");
	fprintf(fp,"L:\n");
	for (i=0;i<imagePointsL.cols;i++)
	{
    fprintf(fp,"%f,%f\n",ptsLu[0][i].x,ptsLu[0][i].y);
	  cout << ptsLu[0][i] << endl;
	}
  cout << "distorted---undistorted:R" << endl;
  fprintf(fp,"L:\n");
  for (i=0;i<imagePointsL.cols;i++)
	{
    fprintf(fp,"%f,%f\n",ptsRu[0][i].x,ptsRu[0][i].y);
	  cout << ptsRu[0][i] << endl;
	}
	fclose(fp);*/
	cout << "out" << endl;
	Mat E_mat;
	E_mat = findEssentialMat(ptsLu[0], ptsRu[0], 1, Point2d(0, 0), RANSAC, 0.999, 1.f);
	cout << "E:" << endl << E_mat/E_mat.at<double>(2,2) << endl;

	for (i=0;i<ptsLu[0].size();i++)
	{
	  Mat ptL, ptR, ptL64, ptR64;
	  ptL = Mat(ptsLu[0][i]);
	  ptL.push_back(1.0f);
	  ptR = Mat(ptsRu[0][i]);
	  ptR.push_back(1.0f);
	  ptL.convertTo(ptL64,CV_64F);
	  ptR.convertTo(ptR64,CV_64F);
	  CV_Assert(ptL64.depth() == E_mat.depth());
		cout << ptR64.t()*E_mat*ptL64 << endl;
//		cout << ptL64.t()*E_mat*ptR64 << endl;
	}
  /*
	Mat R1, R2, t_;
	decomposeEssentialMat(E_mat, R1, R2, t_);
	cout << "R1:" << endl << R1 << endl;
	cout << "R2:" << endl << R2 << endl;
	cout << "t_:" << endl << t_ << endl;*/
	
	Mat R, t;
	recoverPose(E_mat, ptsLu[0], ptsRu[0], R, t);
	cout << "t:" << endl << t << endl;
	cout << "R:" << endl << R << endl;

	vector<vector<Point3f>> objpt(ptsL.size());
	for (i=0;i<ptsL.size();i++)
		calcChessboardCorners(Size(6,9), 0.03, objpt[i]);

	Mat RR, TT, E, F;
	double err = stereoCalibrate(objpt, ptsL, ptsR, Kl, Dl, Kr, Dr,
								Size(640,480), RR, TT, E, F, CALIB_FIX_INTRINSIC,
								TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 100, 1e-6) );
	cout << "Pair calibration reprojection error = " << err << endl;
	cout << "TT:" << endl << TT << endl;
	cout << "RR:" << endl << RR << endl;
	cout << "E:" << endl << E/E.at<double>(2,2) << endl;
  cout << "R:" << rotR*rotL.t() << endl;
  cout << "t:" << tvecR-rotR*rotL.t()*tvecL << endl;
	
	Mat ML(3,4,CV_64F);
	Mat MR(3,4,CV_64F);
	Kl.copyTo(ML.colRange(0,3));
  ML.col(3)=Mat::zeros(3,1,CV_64F);
	MR.colRange(0,3)=Kr*R;
	MR.col(3)=Kr*t;
	cout << "ML:" << endl << ML << endl;
	cout << "MR:" << endl << MR << endl;
	
	
	vector<Point3f> pts;
	calc3Dpts(ptsLu[0], ptsRu[0], MR, ML, pts);
	cout << pts.size() << endl;
	for (i=0;i<pts.size();i++) cout << pts[i] << endl;

	return 0;
}



可以看出两种标定方法获取的结果不太一样。


重构出的标定版的三维坐标如下图所示,红色为本质矩阵方法获得的标定版空间情况,蓝色为stereoCalibrate(...)的运动参数三维重构标定版角点坐标的结果。



源码及数据下载点击打开链接


在实际使用中,建议尽量避免匹配点对来自于同一个平面,且必须有平移量,这样估计出的本质矩阵会好很多!如果只有旋转的存在,无平移量,可以参考http://blog.csdn.net/j10527/article/details/79077739

  • 9
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值