相机的标定之手机相机的标定_相机标定的相机可以是手机吗-程序员宅基地

技术标签: SLAM  

相机的标定是 SLAM 最开始的部分,由于设备原因,这个星期只做了手机相机的标定。这篇文章主要就是介绍一下相机标定的原理以及用OpenCV中现有的函数或是Matlab做相机标定的过程。

0. 资料

先把相机标定过程中看过的资料摆一下:
摄像机内参标定《A Flexible New Technique for Camera Calibration》
摄像机-激光雷达静态外参联合标定《Extrinsic calibration of a camera and laser range finder (improves camera calibration)》
注意结合运动信息,物体的运动与激光雷达的旋转扫描同时发生
运动补偿激光雷达与相机之间的标定

在本篇报告中只用到了第一篇资料中所述的内容。

1. 原理

以下步骤出自《A Flexible New Technique for Camera Calibration》

设一个 2D 平面上的点 m = [ u , v ] T \textbf{m}=[u,v]^T m=[u,v]T,与之相对应的 3D 空间中的点为 M = [ X , Y , Z ] T M=[X,Y,Z]^T M=[X,Y,Z]T 。则根据针孔相机的模型,我们有:
s m = A [ R t ] M s\textbf{m}=\textbf{A}\begin{bmatrix}\textbf{R}& \textbf{t}\end{bmatrix}M sm=A[Rt]M

这里式子中的 m \textbf{m} m M M M 均用了齐次坐标的形式,即 m = [ u , v , 1 ] T \textbf{m}=[u,v,1]^T m=[u,v,1]T M = [ X , Y , Z , 1 ] T M=[X,Y,Z,1]^T M=[X,Y,Z,1]T [ R t ] \begin{bmatrix}\textbf{R}& \textbf{t}\end{bmatrix} [Rt] 为相机的位姿(Extrinsic), A \textbf{A} A 为相机的内参矩阵(Intrinsic matrix):
A = [ α γ u 0 0 β v 0 0 0 1 ] \textbf{A}=\begin{bmatrix}\alpha&\gamma&u_0\\0&\beta&v_0\\0&0&1\end{bmatrix} A=α00γβ0u0v01

我们标定时采用黑白方格的标定版,则 M M M 的的坐标我们是已知的,令 Z = 0 Z=0 Z=0,则有:
s [ u v 1 ] = A [ r 1 r 2 r 3 t ] [ X Y 0 1 ] = A [ r 1 r 2 t ] [ X Y 1 ] s\begin{bmatrix}u\\v\\1\end{bmatrix}=\textbf{A}\begin{bmatrix}\textbf{r}_1&\textbf{r}_2&\textbf{r}_3& \textbf{t}\end{bmatrix}\begin{bmatrix}X\\Y\\0\\1\end{bmatrix}=\textbf{A}\begin{bmatrix}\textbf{r}_1&\textbf{r}_2& \textbf{t}\end{bmatrix}\begin{bmatrix}X\\Y\\1\end{bmatrix} suv1=A[r1r2r3t]XY01=A[r1r2t]XY1

M = [ X , Y , 1 ] T M=[X,Y,1]^T M=[X,Y,1]T s   H = A [ r 1 r 2 t ] s\ \textbf{H}=\textbf{A}\begin{bmatrix}\textbf{r}_1&\textbf{r}_2& \textbf{t}\end{bmatrix} s H=A[r1r2t],则有 m = H M \textbf{m}=\textbf{H}M m=HM
H \textbf{H} H 记成 [ h 1 h 2 h 3 ] \begin{bmatrix}\textbf{h}_1&\textbf{h}_2&\textbf{h}_3\end{bmatrix} [h1h2h3] ,则有:
h 1 T A − T A − 1 h 2 = 0 \textbf{h}_1^T\textbf{A}^{-T}\textbf{A}^{-1}\textbf{h}_2=0 h1TATA1h2=0 h 1 T A − T A − 1 h 1 = h 2 T A − T A − 1 h 2 \textbf{h}_1^T\textbf{A}^{-T}\textbf{A}^{-1}\textbf{h}_1=\textbf{h}_2^T\textbf{A}^{-T}\textbf{A}^{-1}\textbf{h}_2 h1TATA1h1=h2TATA1h2

设:
B = A − T A − 1 = [ B 11 B 12 B 13 B 12 B 22 B 23 B 13 B 23 B 33 ] \textbf{B}=\textbf{A}^{-T}\textbf{A}^{-1}=\begin{bmatrix}B_{11}&B_{12}&B_{13}\\B_{12}&B_{22}&B_{23}\\B_{13}&B_{23}&B_{33}\end{bmatrix} B=ATA1=B11B12B13B12B22B23B13B23B33

B \textbf{B} B 可以由一个 6D 向量定义:
b = [ B 11 , B 2 , B 22 , B 13 , B 23 , B 33 ] T \textbf{b}=[B_{11},B_{2},B_{22},B_{13},B_{23},B_{33}]^T b=[B11,B2,B22,B13,B23,B33]T

h i , i = 1 , 2 , 3 = [ h i 1 , h i 2 , h i 3 ] T \textbf{h}_{i,i=1,2,3}=[h_{i1},h_{i2},h_{i3}]^T hi,i=1,2,3=[hi1,hi2,hi3]T,则有:
h i T B h j = v i j b \textbf{h}_i^T\textbf{B}\textbf{h}_j=\textbf{v}_{ij}\textbf{b} hiTBhj=vijb

其中:
v i j = [ h i 1 h j 1 , h i 1 h j 2 + h i 2 h j 1 , h i 2 h j 2 , h i 3 h j 1 + h i 1 h j 3 , h i 3 h j 2 + h i 2 h j 3 , h i 3 h j 3 ] T \textbf{v}_{ij}=[h_{i1}h_{j1},h_{i1}h_{j2}+h_{i2}h_{j1},h_{i2}h_{j2},h_{i3}h_{j1}+h_{i1}h_{j3},h_{i3}h_{j2}+h_{i2}h_{j3},h_{i3}h_{j3}]^T vij=[hi1hj1,hi1hj2+hi2hj1,hi2hj2,hi3hj1+hi1hj3,hi3hj2+hi2hj3,hi3hj3]T

上式也可以写成:
[ v 12 T ( v 11 − v 22 ) T ] b = 0    o r    Vb = 0 \begin{bmatrix}\textbf{v}_{12}^T\\(\textbf{v}_{11}-\textbf{v}_{22})^T\end{bmatrix}\textbf{b}=\textbf{0} \ \ or\ \ \textbf{V}\textbf{b}=0 [v12T(v11v22)T]b=0  or  Vb=0

式子中, V \textbf{V} V 是我们可以测得的多组数据,根据上式便可以简单估计出 b \textbf{b} b 的取值,从而求出 A \textbf{A} A H \textbf{H} H 。在求出了这些量之后,我们还可以求出相机的位姿,即外参矩阵。

但正如《A Flexible New Technique for Camera Calibration》中所说,上述步骤求得内参矩阵使用了使得代数距离最小,这并没有什么物理意义,只是给了接下来的测量一个初始值。于是在实践中我们采用最大似然估计,在这里是最小化重投影误差

误差项可以写为:
e i j = m i j − f ( A , R i , t i , M j ) \textbf{e}_{ij}=\textbf{m}_{ij}-\textbf{f}(\textbf{A},\textbf{R}_i,\textbf{t}_i,M_j) eij=mijf(A,Ri,ti,Mj)其中 f \textbf{f} f 为点 M j M_j Mj 到相机图像平面的重投影函数。

考虑径向畸变
{ x c o r r e c t e d = x ( 1 + k 1 r 2 + k 2 r 2 ) y c o r r e c t e d = y ( 1 + k 1 r 2 + k 2 r 2 ) \left\{\begin{matrix} x_{corrected}=x(1+k_1r^2+k_2r^2)\\ \\ y_{corrected}=y(1+k_1r^2+k_2r^2) \end{matrix}\right. xcorrected=x(1+k1r2+k2r2)ycorrected=y(1+k1r2+k2r2)

误差项变为:
e i j = m i j − g ( A , k 1 , k 2 , R i , t i , M j ) \textbf{e}_{ij}=\textbf{m}_{ij}-\textbf{g}(\textbf{A},k_1,k_2,\textbf{R}_i,\textbf{t}_i,M_j) eij=mijg(A,k1,k2,Ri,ti,Mj)

其中 g \textbf{g} g 为点 M j M_j Mj 到相机图像平面的重投影函数。

于是我们便得到了一个最小二乘问题:
∑ i = 1 n ∑ j = 1 m ∣ ∣ e i j ∣ ∣ 2 \sum_{i=1}^n\sum_{j=1}^m||\textbf{e}_{ij}||^2 i=1nj=1meij2

这个问题应该可以用G2O或者Ceres来求解(我还没有实现用这些库的求解)。


2. 实现过程

我用的是黑白格的标定板,直接放在Ipad上,然后pad放地上不动,使图片中的所有点都在同一个平面上。标定版的大小是 8 × 11 8\times11 8×11一奇一偶的大小是为了使OpenCV找角点的函数或者是Matlab能够确定起始点的位置。照片如下:

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述
在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

2.1. OpenCV实现过程

因为 OpenCV 直接找角点和相机标定的函数,所以就直接用了。代码如下:

#include "common_include.h"
#include <boost/format.hpp>
#include "config.h"
#include <string>

int main( int argc, char** argv ) {
    

	//set parameters
	boost::format fmt( "%d.jpg" );
	Config::setParameterFile("../config/default.yaml");
	int h = Config::get<int> ("Size.height");
	int w = Config::get<int> ("Size.width");
	int img_num = Config::get<int> ("ImageNumber");
	int image_h = Config::get<int> ("Image.height");
	int image_w = Config::get<int> ("Image.width");
	string image_path = Config::get<string> ("ImagePath");
	float scale = Config::get<float> ("Scale");
	
	Size boardSize(w, h);
	Size imageSize(image_w, image_h);
	cout << "---------------------------------------------------------\n";
	cout << "The board size is " << boardSize.width << " * " << boardSize.height << endl;
	cout << "---------------------------------------------------------\n";
	cout << "The image size is " << imageSize.width << " * " << imageSize.height << endl;
	cout << "---------------------------------------------------------\n";
	
	//initialization
	Mat img;
	vector<Point3f> objects;
	vector<vector<Point2f>> imageCorners;
	vector<vector<Point3f>> objectCorners;
	vector<Point2f> corners;
	int temp = 0;
	
	//The points in world coodinate
	for (int i=0; i<h; i++)
		for (int j=0; j<w; j++)
			objects.push_back( Point3f(j, i, 0.0) );

	for (int i=0; i<img_num; i++) {
    
		cout << "Read the image \"" << ((fmt%(i+1)).str()) << "\" ...\n";
		img = imread( image_path + (fmt%(i+1)).str(), 0 );
		if ( img.empty() ) {
    
			cout << "The image \"" << ((fmt%(i+1)).str()) << "\" does not exits!\n\n";
			continue;
		}
		resize( img, img, Size(), scale, scale, INTER_AREA );
		
		cout << "Find the corner of the image ...\n";
		bool found = findChessboardCorners( img, boardSize, corners );
		
		if (found) {
    
			cornerSubPix( img, corners, Size(5, 5), Size(-1, -1),
				cv::TermCriteria(
					cv::TermCriteria::MAX_ITER + cv::TermCriteria::EPS,
                	30, 0.01
            	)
        	);
        	
        	for (int i=0; i<corners.size(); i++) {
    
        		corners[i] *= 1/scale;
        	}        	
        	cout << "Add the corner of the image ...\n";
			if ( h*w == corners.size() ) {
    
				imageCorners.push_back( corners );
				objectCorners.push_back( objects );
				temp++;
			}
			else cout << "The image is rejected!\n\n";
        }
		else cout << "The corners does not found!\n\n";
	}
	/*
	cout << "---------------------------------------------------------\n";
	cout << objectCorners[0] << endl << imageCorners[0];
	*/
	cout << "---------------------------------------------------------\n";
	cout << "The total number of images which is successfully readed is: " << temp << endl;
	cout << "---------------------------------------------------------\n";
	
	Mat A = (Mat_<float>(3, 3) << 1, 0, 0, 0, 1, 0, 0, 0, 1);
	Mat D = (Mat_<float>(4, 1) << 0, 0, 0, 0);
	vector<Mat> R;
	vector<Mat> t;
	
	calibrateCamera(objectCorners, imageCorners, imageSize, A, D, R, t);
	
	double error=0;
	double total=0;
	for (int i=0; i<R.size(); i++) {
    
		vector<Point2f> repoints;
		projectPoints(objectCorners[i], R[i], t[i], A, D, repoints);
		total += repoints.size();
		for (int j; j<repoints.size(); j++) error += norm(imageCorners[i][j] - repoints[j]);
	}
	
	cout << "The intrinsic matrix is: \n" << A <<endl;
	cout << "---------------------------------------------------------\n";
	cout << "The distortion vector is: \n" << D << endl;
	cout << "---------------------------------------------------------\n";
	cout << "The reproject error is: " << error/total << endl;
	cout << "The total number of points is: " << total << endl;
	return 0;
}

实现过程中的问题与解决: 这里我写了一个 Config 的类来读取一些初始值,具体代码在《SLAM 14讲》的第九章,我也是直接拿来用的。由于开始的时候很多图片找不到角点,所以我在读取图片读取角点时都会在屏幕上输出相关的文字以判断是否成功读取了。后来发现读取不到角点是因为手机拍的图片太大了,于是我在读取图片后先把图片缩小,测出角点坐标后再把坐标按相同的比例放大。另外要注意的是,3D 点的顺序和角点的顺序必须一一对应,于是在生成 3D 点的坐标 vector 时顺序是从左往右,由上至下。

以下时运行的结果:

在这里插入图片描述

2.2 Matlab 实现过程

Matlab 有直接的APP可以直接作相机的标定,直接把图片放进去就可以了。结果如下:

在这里插入图片描述

2.3. 总结和问题

可以看出不论是哪种方法,手机相机的内参矩阵几乎是一样的。这间接的可以说明我们得到的结果还是比较准确的。
但用 Matlab 时得到的平均重投影误差比较大,我现在还没找到缩小的办法。另外,两个方法测出来的径向畸变的系数也有较大不同,这可能是因为这个系数本来较小,所以显示出来的相对误差就会比较大。

3. OpenCV的补充

摘抄一段官网上的说明

double cv::calibrateCamera 	( 	InputArrayOfArrays  	objectPoints,
								InputArrayOfArrays  	imagePoints,
								Size  					imageSize,
								InputOutputArray  		cameraMatrix,
								InputOutputArray  		distCoeffs,
								OutputArrayOfArrays  	rvecs,
								OutputArrayOfArrays  	tvecs,
								OutputArray  			stdDeviationsIntrinsics,
								OutputArray  			stdDeviationsExtrinsics,
								OutputArray  			perViewErrors,
								int  					flags = 0,
								TermCriteria  			criteria = TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, DBL_EPSILON) 
	) 	

objectPoints: 输入参数,类型类似 vector< vecotor < Point3f > >
imagePoints: 输入参数,类型类似 vector< vecotor < Point2f > >
imageSize: 输入参数,类型是 cv::Size
cameraMatrix: 输入输出参数,类型是 cv::Mat
distCoeffs: 输入输出参数,类型是 cv::Mat
rvecs: 输出参数,类型是 vector< cv::Mat >
tvecs: 输出参数,类型是 vector< cv::Mat >
stdDeviationsIntrinsics: 相机内参矩阵和畸变系数的偏差值。
stdDeviationsExtrinsics: 相机外参的偏差值。
perViewErrors: 每张图的重投影误差。
flags:

  • CALIB_USE_INTRINSIC_GUESS: 标定时使用初始值。
  • CALIB_FIX_PRINCIPAL_POINT: 中心点不变。
  • CALIB_FIX_ASPECT_RATIO: f x f y \frac{f_x}{f_y} fyfx 的值不变。
  • CALIB_ZERO_TANGENT_DIST: 切向畸变值设为0。
  • CALIB_FIX_K1,…,CALIB_FIX_K6: 相应的径向畸变系数不变,若不是用初始值,则设为0。
  • CALIB_RATIONAL_MODEL: 使用 k 4 , k 5 , k 6 k_4, k_5, k_6 k4,k5,k6 ,若没有这个 distCoeffs 只返回五个参数。
  • CALIB_THIN_PRISM_MODEL: 使用 s 1 , s 2 , s 3 s_1,s_2,s_3 s1,s2,s3
  • CALIB_FIX_S1_S2_S3_S4: 固定 s 1 , s 2 , s 3 s_1,s_2,s_3 s1,s2,s3
  • CALIB_TILTED_MODEL: 使用 τ x , τ y \tau_x,\tau_y τx,τy
  • CALIB_FIX_TAUX_TAUY: 固定 τ x , τ y \tau_x,\tau_y τx,τy
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_41343094/article/details/105160263

智能推荐

oracle 12c 集群安装后的检查_12c查看crs状态-程序员宅基地

文章浏览阅读1.6k次。安装配置gi、安装数据库软件、dbca建库见下:http://blog.csdn.net/kadwf123/article/details/784299611、检查集群节点及状态:[root@rac2 ~]# olsnodes -srac1 Activerac2 Activerac3 Activerac4 Active[root@rac2 ~]_12c查看crs状态

解决jupyter notebook无法找到虚拟环境的问题_jupyter没有pytorch环境-程序员宅基地

文章浏览阅读1.3w次,点赞45次,收藏99次。我个人用的是anaconda3的一个python集成环境,自带jupyter notebook,但在我打开jupyter notebook界面后,却找不到对应的虚拟环境,原来是jupyter notebook只是通用于下载anaconda时自带的环境,其他环境要想使用必须手动下载一些库:1.首先进入到自己创建的虚拟环境(pytorch是虚拟环境的名字)activate pytorch2.在该环境下下载这个库conda install ipykernelconda install nb__jupyter没有pytorch环境

国内安装scoop的保姆教程_scoop-cn-程序员宅基地

文章浏览阅读5.2k次,点赞19次,收藏28次。选择scoop纯属意外,也是无奈,因为电脑用户被锁了管理员权限,所有exe安装程序都无法安装,只可以用绿色软件,最后被我发现scoop,省去了到处下载XXX绿色版的烦恼,当然scoop里需要管理员权限的软件也跟我无缘了(譬如everything)。推荐添加dorado这个bucket镜像,里面很多中文软件,但是部分国外的软件下载地址在github,可能无法下载。以上两个是官方bucket的国内镜像,所有软件建议优先从这里下载。上面可以看到很多bucket以及软件数。如果官网登陆不了可以试一下以下方式。_scoop-cn

Element ui colorpicker在Vue中的使用_vue el-color-picker-程序员宅基地

文章浏览阅读4.5k次,点赞2次,收藏3次。首先要有一个color-picker组件 <el-color-picker v-model="headcolor"></el-color-picker>在data里面data() { return {headcolor: ’ #278add ’ //这里可以选择一个默认的颜色} }然后在你想要改变颜色的地方用v-bind绑定就好了,例如:这里的:sty..._vue el-color-picker

迅为iTOP-4412精英版之烧写内核移植后的镜像_exynos 4412 刷机-程序员宅基地

文章浏览阅读640次。基于芯片日益增长的问题,所以内核开发者们引入了新的方法,就是在内核中只保留函数,而数据则不包含,由用户(应用程序员)自己把数据按照规定的格式编写,并放在约定的地方,为了不占用过多的内存,还要求数据以根精简的方式编写。boot启动时,传参给内核,告诉内核设备树文件和kernel的位置,内核启动时根据地址去找到设备树文件,再利用专用的编译器去反编译dtb文件,将dtb还原成数据结构,以供驱动的函数去调用。firmware是三星的一个固件的设备信息,因为找不到固件,所以内核启动不成功。_exynos 4412 刷机

Linux系统配置jdk_linux配置jdk-程序员宅基地

文章浏览阅读2w次,点赞24次,收藏42次。Linux系统配置jdkLinux学习教程,Linux入门教程(超详细)_linux配置jdk

随便推点

matlab(4):特殊符号的输入_matlab微米怎么输入-程序员宅基地

文章浏览阅读3.3k次,点赞5次,收藏19次。xlabel('\delta');ylabel('AUC');具体符号的对照表参照下图:_matlab微米怎么输入

C语言程序设计-文件(打开与关闭、顺序、二进制读写)-程序员宅基地

文章浏览阅读119次。顺序读写指的是按照文件中数据的顺序进行读取或写入。对于文本文件,可以使用fgets、fputs、fscanf、fprintf等函数进行顺序读写。在C语言中,对文件的操作通常涉及文件的打开、读写以及关闭。文件的打开使用fopen函数,而关闭则使用fclose函数。在C语言中,可以使用fread和fwrite函数进行二进制读写。‍ Biaoge 于2024-03-09 23:51发布 阅读量:7 ️文章类型:【 C语言程序设计 】在C语言中,用于打开文件的函数是____,用于关闭文件的函数是____。

Touchdesigner自学笔记之三_touchdesigner怎么让一个模型跟着鼠标移动-程序员宅基地

文章浏览阅读3.4k次,点赞2次,收藏13次。跟随鼠标移动的粒子以grid(SOP)为partical(SOP)的资源模板,调整后连接【Geo组合+point spirit(MAT)】,在连接【feedback组合】适当调整。影响粒子动态的节点【metaball(SOP)+force(SOP)】添加mouse in(CHOP)鼠标位置到metaball的坐标,实现鼠标影响。..._touchdesigner怎么让一个模型跟着鼠标移动

【附源码】基于java的校园停车场管理系统的设计与实现61m0e9计算机毕设SSM_基于java技术的停车场管理系统实现与设计-程序员宅基地

文章浏览阅读178次。项目运行环境配置:Jdk1.8 + Tomcat7.0 + Mysql + HBuilderX(Webstorm也行)+ Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。项目技术:Springboot + mybatis + Maven +mysql5.7或8.0+html+css+js等等组成,B/S模式 + Maven管理等等。环境需要1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。_基于java技术的停车场管理系统实现与设计

Android系统播放器MediaPlayer源码分析_android多媒体播放源码分析 时序图-程序员宅基地

文章浏览阅读3.5k次。前言对于MediaPlayer播放器的源码分析内容相对来说比较多,会从Java-&amp;amp;gt;Jni-&amp;amp;gt;C/C++慢慢分析,后面会慢慢更新。另外,博客只作为自己学习记录的一种方式,对于其他的不过多的评论。MediaPlayerDemopublic class MainActivity extends AppCompatActivity implements SurfaceHolder.Cal..._android多媒体播放源码分析 时序图

java 数据结构与算法 ——快速排序法-程序员宅基地

文章浏览阅读2.4k次,点赞41次,收藏13次。java 数据结构与算法 ——快速排序法_快速排序法