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

技术标签: 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

智能推荐

字典树_字典树建树-程序员宅基地

文章浏览阅读271次。原创字典树字典树,又称单词查找树,Trie树,是一种树形结构,哈希表的一个变种。用于统计,排序和保存大量的字符串(也可以保存其的)。优点就是利用公共的前缀来节约存储空间。在这举个简单的例子:比如说我们想储存3个单词,nyist、nyistacm、nyisttc。如果只是单纯的按照以前的字符数组存储的思路来存储的话,那么我们需要定义三个字符串数组。但是_字典树建树

Android framework--谈谈AMS.updateOomAdjLocked-程序员宅基地

文章浏览阅读3.1k次。关于Android系统的内存回收机制,相信大家都不陌生,Android基于各个应用进程承载四大组件的状态对应用进程进行重要性评估,并在系统内存紧张时根据重要性由低到高来选择杀死应用进程,以达到释放内存的目的。重要性评估由AMS执行,具体来说就是AMS.updateOomAdjLocked函数,反过来说,AMS.updateOomAdjLocked的作用就是更新应用进程的重要性。应用进程(Pro..._updateoomadjlocked

计算机基础——操作系统-程序员宅基地

文章浏览阅读8.5k次,点赞28次,收藏38次。本章将会讲解计算机的操作系统。操作系统(Operating System,OS)就好比一个计算机内部的管理者,是管理和控制计算机硬件与软件资源的计算机程序,直接运行在“裸机”上的最基本的系统软件,任何其他应用软件都必须在操作系统的支持下才能运行,操作系统是用户和计算机的接口,同时也是计算机硬件和其他软件的接口。操作系统的功能包括管理计算机系统的硬件,软件及数据资源,控制程序运行,为其他应用软件提供支持等。_操作系统

Python之pip download 命令用法-下载指定平台和python版本的依赖包-程序员宅基地

文章浏览阅读1.9w次,点赞7次,收藏27次。pip download 和 pip install 有着相同的解析和下载过程,不同的是,pip install 会安装依赖项,而 pip download 会把所有已下载的依赖项保存到指定的目录 ( 默认是当前目录 ),此目录稍后可以作为值传递给 pip install --find-links 以便离线或锁定下载包安装_pip download

centos7设置密码策略_CentOS7 设置密码复杂度-程序员宅基地

文章浏览阅读3.4k次。在CentOS下设置密码复杂度分为两步(1)修改/etc/login.defs文件vim /etc/login.defsPASS_MAX_DAYS90   # 密码最长过期天数PASS_MIN_DAYS80    # 密码最小过期天数PASS_MIN_LEN10    # 密码最小长度PASS_WARN_AGE7    # 密码过期警告天数(2)..._echo 'mypassword' | openssl passwd -6 -stdin centos7

王斌老师的博客_王斌 github-程序员宅基地

文章浏览阅读480次。http://blog.sina.com.cn/s/blog_736d0b9101018cgc.html_王斌 github

随便推点

Java递归实现Fibonacci数列计算_用递归方法编程计算fibonacci数列:(n=10),fac.jpg-程序员宅基地

文章浏览阅读2.8k次。实现代码如下:public static int factorial(int n){ if (n <= 1){ return 1; } return factorial(n-1) + factorial(n-2); }测试代码如下:System.out.println(factorial(40));测..._用递归方法编程计算fibonacci数列:(n=10),fac.jpg

scratch班级名称 电子学会图形化编程scratch等级考试四级真题和答案解析B卷2020-9-程序员宅基地

文章浏览阅读1.3k次。scratch班级名称一、题目要求1、准备工作 保留小猫角色,白色背景 2、功能实现 点击绿旗后,询问请输入年级数,等待输入年级数 询问请输入班级数,等待输入班级数 定义列表“全校班级”,假设每个班级的班级数相同,所有班级名称自动生成并保存到全校班级中。 例如,输入年级数为5,输入班级数为8,可以看到舞台上列表全校班级的内容为:1(1)班、1(2)班、...5(7)班、5(8)班 二、案例分析1、角色分析角色:小猫2、背景_scratch班级名称

郁金香2021年游戏辅助技术中级班(七)_squad辅助科技-程序员宅基地

文章浏览阅读379次。郁金香2021年游戏辅助技术中级班(七)058-C,C++写代码HOOK分析封包数据格式A059-C,C++写代码HOOK分析封包数据格式B-detours劫持060-C,C++写代码HOOK分析封包数据格式C-过滤和格式化061-C,C++写代码HOOK分析封包数据格式D-写入配置文件062-C,C++写代码HOOK分析封包数据格式D-读取配置文件058-C,C++写代码HOOK分析封包数据格式A_squad辅助科技

ssh登录qemu虚拟机里的linux系统_qemu ssh连接-程序员宅基地

文章浏览阅读350次。上面的命令启动了一个带有NAT网络的QEMU虚拟机,并设置了端口转发,将主机的2222端口映射到虚拟机的22端口(SSH端口)。1、安装openssh,如果是根文件系统用buildroot构建,打开 BR2_PACKAGE_OPENSSH 开关。2、在qemu的启动脚本里增加。3、在虚拟机里增加一个新用户。4、向虚拟机里发送文件。_qemu ssh连接

用netty实现zcool_Netty框架入门-程序员宅基地

文章浏览阅读63次。一、概述Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。二、体系结构图三、Netty的核心结构Netty是典型的Reactor模型结构,在实现上,Netty中的Boss类充当mainReactor,NioWorker类充当subReactor(默认NioWorker的个数是当前服务器的..._channelconnected

SpringBoot 过滤器 filter 3种方法_spring boot filter 配置-程序员宅基地

文章浏览阅读4.7k次。最近Spring Boot项目做单点登录对接的时候,在配置过滤器的时候,找了几种方法,记录一下。欢迎评论补充沟通~由于之前JAVA Web项目最开始都有web.xml配置,随着框架慢慢的进化,从Spring Boot开始,已经没有了web.xml配置文件。那原来在web.xml里,配置的filter过滤器,在Spring Boot中怎么配置呢?注意,这个自定义类,也不能加@Component或@Configuration注解,加了就会初始化Filter了,过滤全部的路径了。_spring boot filter 配置

推荐文章

热门文章

相关标签