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

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

智能推荐

什么是内部类?成员内部类、静态内部类、局部内部类和匿名内部类的区别及作用?_成员内部类和局部内部类的区别-程序员宅基地

文章浏览阅读3.4k次,点赞8次,收藏42次。一、什么是内部类?or 内部类的概念内部类是定义在另一个类中的类;下面类TestB是类TestA的内部类。即内部类对象引用了实例化该内部对象的外围类对象。public class TestA{ class TestB {}}二、 为什么需要内部类?or 内部类有什么作用?1、 内部类方法可以访问该类定义所在的作用域中的数据,包括私有数据。2、内部类可以对同一个包中的其他类隐藏起来。3、 当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷。三、 内部类的分类成员内部_成员内部类和局部内部类的区别

分布式系统_分布式系统运维工具-程序员宅基地

文章浏览阅读118次。分布式系统要求拆分分布式思想的实质搭配要求分布式系统要求按照某些特定的规则将项目进行拆分。如果将一个项目的所有模板功能都写到一起,当某个模块出现问题时将直接导致整个服务器出现问题。拆分按照业务拆分为不同的服务器,有效的降低系统架构的耦合性在业务拆分的基础上可按照代码层级进行拆分(view、controller、service、pojo)分布式思想的实质分布式思想的实质是为了系统的..._分布式系统运维工具

用Exce分析l数据极简入门_exce l趋势分析数据量-程序员宅基地

文章浏览阅读174次。1.数据源准备2.数据处理step1:数据表处理应用函数:①VLOOKUP函数; ② CONCATENATE函数终表:step2:数据透视表统计分析(1) 透视表汇总不同渠道用户数, 金额(2)透视表汇总不同日期购买用户数,金额(3)透视表汇总不同用户购买订单数,金额step3:讲第二步结果可视化, 比如, 柱形图(1)不同渠道用户数, 金额(2)不同日期..._exce l趋势分析数据量

宁盾堡垒机双因素认证方案_horizon宁盾双因素配置-程序员宅基地

文章浏览阅读3.3k次。堡垒机可以为企业实现服务器、网络设备、数据库、安全设备等的集中管控和安全可靠运行,帮助IT运维人员提高工作效率。通俗来说,就是用来控制哪些人可以登录哪些资产(事先防范和事中控制),以及录像记录登录资产后做了什么事情(事后溯源)。由于堡垒机内部保存着企业所有的设备资产和权限关系,是企业内部信息安全的重要一环。但目前出现的以下问题产生了很大安全隐患:密码设置过于简单,容易被暴力破解;为方便记忆,设置统一的密码,一旦单点被破,极易引发全面危机。在单一的静态密码验证机制下,登录密码是堡垒机安全的唯一_horizon宁盾双因素配置

谷歌浏览器安装(Win、Linux、离线安装)_chrome linux debian离线安装依赖-程序员宅基地

文章浏览阅读7.7k次,点赞4次,收藏16次。Chrome作为一款挺不错的浏览器,其有着诸多的优良特性,并且支持跨平台。其支持(Windows、Linux、Mac OS X、BSD、Android),在绝大多数情况下,其的安装都很简单,但有时会由于网络原因,无法安装,所以在这里总结下Chrome的安装。Windows下的安装:在线安装:离线安装:Linux下的安装:在线安装:离线安装:..._chrome linux debian离线安装依赖

烤仔TVの尚书房 | 逃离北上广?不如押宝越南“北上广”-程序员宅基地

文章浏览阅读153次。中国发达城市榜单每天都在刷新,但无非是北上广轮流坐庄。北京拥有最顶尖的文化资源,上海是“摩登”的国际化大都市,广州是活力四射的千年商都。GDP和发展潜力是衡量城市的数字指...

随便推点

java spark的使用和配置_使用java调用spark注册进去的程序-程序员宅基地

文章浏览阅读3.3k次。前言spark在java使用比较少,多是scala的用法,我这里介绍一下我在项目中使用的代码配置详细算法的使用请点击我主页列表查看版本jar版本说明spark3.0.1scala2.12这个版本注意和spark版本对应,只是为了引jar包springboot版本2.3.2.RELEASEmaven<!-- spark --> <dependency> <gro_使用java调用spark注册进去的程序

汽车零部件开发工具巨头V公司全套bootloader中UDS协议栈源代码,自己完成底层外设驱动开发后,集成即可使用_uds协议栈 源代码-程序员宅基地

文章浏览阅读4.8k次。汽车零部件开发工具巨头V公司全套bootloader中UDS协议栈源代码,自己完成底层外设驱动开发后,集成即可使用,代码精简高效,大厂出品有量产保证。:139800617636213023darcy169_uds协议栈 源代码

AUTOSAR基础篇之OS(下)_autosar 定义了 5 种多核支持类型-程序员宅基地

文章浏览阅读4.6k次,点赞20次,收藏148次。AUTOSAR基础篇之OS(下)前言首先,请问大家几个小小的问题,你清楚:你知道多核OS在什么场景下使用吗?多核系统OS又是如何协同启动或者关闭的呢?AUTOSAR OS存在哪些功能安全等方面的要求呢?多核OS之间的启动关闭与单核相比又存在哪些异同呢?。。。。。。今天,我们来一起探索并回答这些问题。为了便于大家理解,以下是本文的主题大纲:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JCXrdI0k-1636287756923)(https://gite_autosar 定义了 5 种多核支持类型

VS报错无法打开自己写的头文件_vs2013打不开自己定义的头文件-程序员宅基地

文章浏览阅读2.2k次,点赞6次,收藏14次。原因:自己写的头文件没有被加入到方案的包含目录中去,无法被检索到,也就无法打开。将自己写的头文件都放入header files。然后在VS界面上,右键方案名,点击属性。将自己头文件夹的目录添加进去。_vs2013打不开自己定义的头文件

【Redis】Redis基础命令集详解_redis命令-程序员宅基地

文章浏览阅读3.3w次,点赞80次,收藏342次。此时,可以将系统中所有用户的 Session 数据全部保存到 Redis 中,用户在提交新的请求后,系统先从Redis 中查找相应的Session 数据,如果存在,则再进行相关操作,否则跳转到登录页面。此时,可以将系统中所有用户的 Session 数据全部保存到 Redis 中,用户在提交新的请求后,系统先从Redis 中查找相应的Session 数据,如果存在,则再进行相关操作,否则跳转到登录页面。当数据量很大时,count 的数量的指定可能会不起作用,Redis 会自动调整每次的遍历数目。_redis命令

URP渲染管线简介-程序员宅基地

文章浏览阅读449次,点赞3次,收藏3次。URP的设计目标是在保持高性能的同时,提供更多的渲染功能和自定义选项。与普通项目相比,会多出Presets文件夹,里面包含着一些设置,包括本色,声音,法线,贴图等设置。全局只有主光源和附加光源,主光源只支持平行光,附加光源数量有限制,主光源和附加光源在一次Pass中可以一起着色。URP:全局只有主光源和附加光源,主光源只支持平行光,附加光源数量有限制,一次Pass可以计算多个光源。可编程渲染管线:渲染策略是可以供程序员定制的,可以定制的有:光照计算和光源,深度测试,摄像机光照烘焙,后期处理策略等等。_urp渲染管线