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

2023-10-31

至今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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

随机推荐

  • ajax地址url和action,jsp,ajax_JSP中 post url地址重写or action如何实现,jsp,ajax - phpStudy...

    JSP中 post url地址重写or action如何实现 求解在JSP中 post url地址 ctx web webuser getRights 这个是获取导航JSON ctx 是应用地址知道 后面 web webuser getRi
  • Docker实践(一)应用场景和安装

    随着互联网的发展 容器技术越来越得到大规模应用厂家的重视 Docker作为paas平台的容器 最近几年引起众多技术人士的关注 并且也越来越流行 一 Docker是什么 Docker 是一个开源的应用容器引擎 让开发者可以打包他们的应用以及依
  • (c/c++)——类型转换

    文章目录 一 C风格的类型转换 二 C 的强制类型转换 1 static cast 最常用 2 reinterpret cast 只用于指针或引用转换 3 const cast 去除指针或引用的const限定 4 dynamic cast
  • QT 项目中 配置文件 pro详解

    QT 的配置文件 pro 解析 1 注释 单行注释 2 QT core gui QT gui 新增 或者减去 QT使用的模块 各个模块间使用空格分开 默认会包含 core 与 gui两个模块 3 TEMPLATE VALUE模板 VALUE
  • mysql group by 后求和_MySQL分组求和GROUP BY

    源代码实例 工资统计表 SELECT wages header bill date wages name SUM wages money AS moneys FROM wages header LEFT JOIN wages ON wage
  • 输入字符串,统计各个字符出现的个数(C语言实现)

    include
  • java 线程同步的list_java集合框架线程同步代码详解

    List接口的大小可变数组的实现 实现了所有可选列表操作 并允许包括null在内的所有元素 除了实现List接口外 此类还提供一些方法来操作内部用来存储列表的数组的大小 此类大致上等同于Vector类 除了此类是不同步的 size isEm
  • C语言——输入3个整数进行比较,要求从小到大输出

    方法一 3个数值从小到大进行排序 把所有可能罗列出来 输出的时候选择运行其中的一种可能输出 include
  • MySQL集群高可用架构

    MySQL集群高可用架构 前言 高可用架构对于互联网服务基本是标配 无论是应用服务还是数据库服务都需要做到高可用 对于一个系统而言 可能包含很多模块 比如前端应用 缓存 数据库 搜索 消息队列等 每个模块都需要做到高可用 才能保证整个系统的
  • java中的IO和NIO的区别原理

    Java NIO和IO的主要区别 IO NIO 面向Stream 面向Buffer 阻塞IO 非阻塞IO Selectors 面向Stream和面向Buffer Java NIO和IO之间最大的区别是IO是面向流 Stream 的 NIO是
  • Hadoop学习笔记二 - kNN算法实现用户风险分类

    在银行这个极其庞杂的交易体系中 每天都能收集到大量的用户信息和用户数据 而伴随着我国利率市场化的不断深化 净利差的迅速收窄 各大银行都希望利用手头的大量用户信息和实时产生的用户数据 对用户进行深度分析挖掘以便进行风险管控和个性化营销 降低银
  • minikube安装

    linux下安装minikube 下载minikube sudo curl Lo minikube https storage googleapis com minikube releases latest minikube linux a
  • 1、node.js实现的时钟案例

    目录 1 JavaScript相关知识回顾 1 1JavaScript 1 2浏览器中JavaScript的运行环境 前端运行环境 2 初始Node js 2 1什么是Node js 2 2Node js中JavaScript的运行环境 后
  • 学会怎样使用Jsp 内置标签、jstl标签库及自定义标签

    学习jsp不得不学习jsp标签 一般来说 对于一个jsp开发者 可以理解为jsp页面中出现的java代码越少 对jsp的掌握就越好 而替换掉java代码的重要方式就是使用jsp标签 jsp标签的分类 1 内置标签 动作标签 不需要在jsp页
  • Matplotlib(8)——图片读取,背景绘制等

    获取当前图像gca Get Current Axes 获取当前axes 方法 plt gca kwargs 实例 x np arange 0 2 np pi 0 1 y x 2 fig plt figure plt subplot 211
  • iOS 快速设置Launch Image 启动图片

    Step1 1 点击Image xcassets 进入图片管理 然后右击 弹出 New Launch Image 2 如图 右侧的勾选可以让你选择是否要对ipad 横屏 竖屏 以及低版本的ios系统做支持 这边我选了ios8 0 ios7
  • std::deque、std::queue、std::stack、std::list 内容概要

    deque deque的简介 deque是 double ended queue 的缩写 和vector一样都是STL的容器 deque是双端的 而vector是单端的 deque在接口上和vector非常相似 在许多操作的地方可以直接替换
  • .bat文件闪退,原因及解决

    之前学习mongodb 创建了一个txt文件 内容是 D cd D Program Files MongoDB bin mongod exe dbpath G data db 然后更改文件名称为 bat时 一运行就一闪就没了 后来 用cmd
  • 篇七:二分法查找

    二分查找也称折半查找 Binary Search 它是一种效率较高的查找方法 但是 二分查找要求线性表必须采用顺序存储结构 不宜用于链式结构 而且表中元素按关键字有序排列 解释 所以二分查找的时候一定要是有序的数组 二分法查找适用于数据量较
  • 基于opencv3的本质矩阵(essential matrix)估计算例

    至今opencv3面世快两年了 五一期间抽空看了下新增的几个关于双目视觉标定算法的使用 关于双目的标定有一类方法是基于本质矩阵 Essential matrix 的标定方法 本质方程与基础方程都是刻画双目视觉极几何关系的两个方程 只不过它们