写在题前:这篇文章磨磨蹭蹭了好久,曾经两次接近完稿而丢失。我想任何事情在起步时都会有类似的囧境,还好我还有恒心继续下去。
摄像头标定的目的有两个。第一,要还原摄像头成像的物体在真实世界的位置就需要知道世界中的物体到计算机图像平面是如何变换的,摄像头标定的目的之一就是为了搞清楚这种变换关系,求解内外参数矩阵。第二,针孔摄像头的发明使得摄像头变成了亲民物品,大行于世,但是针孔摄像头有个很大的问题——畸变。摄像头标定的另一个目的就是求解畸变系数,然后用于计算求解正确的成像。
-
数学原理
-
映射矩阵-内部参数(intrinsic parameters)和外部参数(extrinsic parameters)
-
前面所说的真实世界到成像平面的变换过程牵扯到四个坐标系,变换矩阵可以大致分为两组参数——内部参数和外部参数。下面依次根据四种坐标系的关系来推导内外参数的形式。
计算机坐标系和成像平面坐标系
计算机坐标系是指数字图像在计算机中的保存形式-二维数组的坐标形式。成像平面坐标系是摄像机镜头的成像平面上建立的坐标系,一般是位于感光器件上(如CCD)所建立的以镜头光轴与成像平面交点为原点的二维坐标系。由于二维数组以离散的形式保存,所以计算机坐标系的长度单位为1,一般感光器件大小约为指甲盖大小,上面又密集的分别很多感光单元,每个感光单元采集的图像都会计算成计算机坐标系中的像素值,所以一般长度单位为微米。具体二者关系见下图,
图中坐标系O-uv为计算机坐标系,其坐标轴方向从右上到左下递增。坐标系Q-xy为成像平面坐标系,其坐标原点Q在计算机坐标系的坐标为(u0,v0)。假设有一点P,其在计算机坐标系中的坐标为(u,v),在成像平面坐标系中的坐标为(x,y),并设像素单元在x,y方向的尺寸分别为a,b。则有如下等式成立,
整理成齐次变换矩阵的形式如下,
成像平面坐标系和摄像机坐标系
摄像机坐标系是一个以摄像机镜头的光心为原点而建立的空间三维坐标系。成像平面坐标系可以看成摄像机坐标系在其Zc轴上投影而成的(其x和y轴与成像平面坐标系方向一致)。其与成像平面坐标系的关系如下图,
这里需要详细解释一下上图怎样理解。图中Oc-XcYcZc为摄像机坐标系,M点为物点,而Q-xy为成像平面坐标系。实际情况下成像平面与物体应该分居摄像头两侧,但是在这里我们将成像平面以摄像头坐标系原点为中心对称点对称过来。这样的好处是本来倒立的像变成正立的,且大小不变。如上一篇博客写到的那样,中心对称的后的像点和物点连线必定过光心。我们要记住小孔成像的性质是,成像平面到镜头的距离始终等于焦距f。由相似三角形,有如下等式成立,
整理成齐次变换矩阵的形式如下,
摄像机坐标系和世界坐标系
世界坐标系是区别于摄像机坐标系的一个空间坐标系,其依附于我们标定时要使用的物点而存在。既我们观察的物点相对于世界坐标系的位置是固定的,如张正友标定法中就是选的世界坐标系为以chessboard平面为XOY平面的相对于chessboard不动的空间坐标系。下图是我费了老大劲绘制的摄像机坐标系和世界坐标系之间的位置关系,
我们知道空间中的两个坐标系可以通过平移旋转变换而重合,那么现在我们来简单粗暴解释一下物点M在两个坐标系下坐标的代数关系是什么样的。假设摄像机坐标系可以通过旋转平移变换与世界坐标系重合,其齐次变换矩阵形式如下,
也就是说摄像机坐标系上的每个点(在世界坐标系中的坐标)通过上述旋转平移变换后与世界坐标系中的相应点重合。我们可以理解的是,对摄像机坐标系上的每个点和物点M(在世界坐标系下的坐标)做相同的旋转平移变换后其相对位置不会改变。也就是说物点M在摄像机坐标系中的坐标不会改变。我们考察一下变换完后是什么情况。这个时候世界坐标系和摄像机坐标系重合,且物点M相对于摄像机坐标系的坐标没有改变,那么这个时候物点M在世界坐标系中的坐标应该变成了变换前其在摄像机坐标系下的坐标。所以有如下关系成立,
综合上面的三组关系,我们可以推导出世界坐标系和计算机坐标系的直接代数关系,
进一步化简,
我们记,
A矩阵式刻画摄像机的内部参数,包括焦距f、成像中心的位置、及成像单元尺寸,因此称为内参矩阵。M描述的摄像头的运动关系,所以称为外参矩阵。其中R有三个相关参数,分别是绕x,y,z轴的旋转角度。T也有三个相关参数,分别是在三个坐标轴上的平移量Tx、Ty、Tz。
-
摄像头的畸变参数(distortion parameters)
摄像头的畸变是由于成像模型的不精确造成的。人们为了提高光通量用透镜代替小孔来成像,由于这种代替不能完全符合小孔成像的性质,因此畸变就产生了。另外这里再插句额外的话,现在大量使用的透镜为球面镜,原因是其廉价易得。但是真正的完全符合理想光学系统的透镜实际是个四次曲面(很好证明,依据光程不变),制造成本很大哦。
畸变可以分为两大类,径向畸变和切向畸变。详细的畸变介绍可以参考工程光学的相关课程,下面简单介绍相关畸变及其修正。
径向畸变(radial distortion)
径向畸变的效应有两种,一种是枕形效应,另一种是桶形效应,具体见下图(图片来自互联网),
径向畸变可用下面公式修正,
切向畸变(tangential distortion)
径向畸变是由于透镜与成像平面不严格的平行,其可以用如下公式修正,
这样又引入了五个畸变参数,
-
小结
我们记fx=f/a,fy=f/b。通过上面的介绍,我们了解到,摄像机标定共有4个内参,6个外参和五个畸变参数要求。下面就介绍怎么基于OpenCV函数库标定求得这三组参数。其求解原理放在以后的博客中叙述。
-
基于OpenCV的标定程序
OpenCV中有标定实例哦,写的很好,功能很完善。一个是基于命令行标定参数读入的标定程序,另一个是基于xml文件参数读入的标定程序。它们的位置分别为,
...\opencv\sources\samples\cpp\calibration.cpp
...\opencv\sources\samples\cpp\tutorial_code\calib3d\camera_calibration\ camera_calibration.cpp
但是为了更深入的理解OpenCV的标定方法库,我自己写了一个简单粗暴易读的标定程序。下面简单介绍一下标定的过程。
-
标定过程简介
标定过程如下,
- 图像获取
- 角点检测,如果没有检测到角点重复第一步
- 亚像素检测以提高角点检测精度
- 标记检测出的角点
- 如果成功检测到角点图片小于预设的数目,重复上面四个步骤
- 标定
- 标定结果保存
我写的标定程序如下,
1 /* 2 Writer: Wang Xianshun 3 Email: [email protected] 4 */ 5 #include6 #include 7 #include 8 #include <string.h> 9 10 #include 11 #include 12 #include 13 #include 14 #include 15 16 using namespace std; 17 using namespace cv; 18 19 static void calcChessboardCorners(Size boardSize, float squareSize, vector & corners) 20 { 21 corners.resize(0); 22 for (int i = 0; i < boardSize.height; i++) //height和width位置不能颠倒 23 for (int j = 0; j < boardSize.width; j++) 24 { 25 corners.push_back(Point3f(j*squareSize, i*squareSize, 0)); 26 } 27 } 28 29 int main(int argc, char** argv) 30 { 31 int success = 0; 32 int cameraId = 0; 33 int nFrames = 10; 34 int w = 6; 35 int h = 9; 36 clock_t prevTimestamp = 0; 37 int delay = 1000; 38 39 //相关参数初始化 40 Size boardSize, imageSize; 41 boardSize.width = w; 42 boardSize.height = h; 43 vector > imagePoints; 44 float squareSize = 1.f; 45 Mat intrMatrix, distCoeffs; 46 vector rvecs, tvecs; 47 48 //标定参数读取 49 if (argc < 5) 50 { 51 cout << "参数不足" << endl; 52 return 0; 53 } 54 55 for (int i = 1; i < argc; i++) 56 { 57 if (!strcmp(argv[i], "-w")) 58 { 59 if (!sscanf(argv[++i], "%u", &boardSize.width)) 60 { 61 return fprintf(stderr, "无效的标定角点宽度\n"), -1; 62 } 63 } 64 else if (!strcmp(argv[i], "-h")) 65 { 66 if (!sscanf(argv[++i], "%u", &boardSize.height)) 67 { 68 return fprintf(stderr, "无效的标定角点高度\n"), -1; 69 } 70 } 71 else if (!strcmp(argv[i], "-s")) 72 { 73 if (!sscanf(argv[++i], "%f", &squareSize) != 1 || squareSize <= 0) 74 { 75 return fprintf(stderr, "无效的方格尺寸\n"), -1; 76 } 77 } 78 else 79 return fprintf(stderr, "未知参数\n"), -1; 80 } 81 82 //图像采集 83 VideoCapture capture; 84 capture.open(cameraId); 85 namedWindow("Image View", 1); 86 87 if (!capture.isOpened()) 88 { 89 cout << "无法打开摄像头,(づ ̄3 ̄)づ╭❤~……" << endl; 90 return -1; 91 } 92 93 for (int i = 0; success < nFrames; i++) 94 { 95 string msg = "CAPTURING"; 96 Mat viewGray, view; 97 capture >> view; 98 imageSize = view.size(); 99 vector pointBuf; 100 cvtColor(view, viewGray, COLOR_BGR2GRAY); 101 102 //寻找角点 103 bool found = findChessboardCorners(view, boardSize, pointBuf, 104 CV_CALIB_CB_ADAPTIVE_THRESH | CV_CALIB_CB_FAST_CHECK | CV_CALIB_CB_NORMALIZE_IMAGE); 105 106 if (found) 107 { 108 //亚像素检测以提高精度 109 cornerSubPix(viewGray, pointBuf, Size(11, 11), 110 Size(-1, -1), TermCriteria(CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 30, 0.1)); 111 //标记出检测到的角点 112 drawChessboardCorners(view, boardSize, Mat(pointBuf), found); 113 } 114 115 //等待用户改变姿态 116 if (found && clock() - prevTimestamp > delay*1e-3*CLOCKS_PER_SEC) 117 { 118 imagePoints.push_back(pointBuf); 119 prevTimestamp = clock(); 120 success = success + 1; 121 bitwise_not(view, view); 122 } 123 124 imshow("Image View", view); 125 waitKey(20); 126 } 127 128 cout << "图像采集完成,开始标定……" << endl; 129 130 //标定 131 vector > ObjectPoints(1); 132 calcChessboardCorners(boardSize, squareSize, ObjectPoints[0]); 133 ObjectPoints.resize(imagePoints.size(), ObjectPoints[0]); 134 calibrateCamera(ObjectPoints, imagePoints, imageSize, intrMatrix, 135 distCoeffs, rvecs, tvecs); 136 bool ok = checkRange(intrMatrix) && checkRange(distCoeffs); 137 138 if (!ok) 139 { 140 cout << "标定失败,再来一次" << endl; 141 return -3; 142 } 143 144 //标定结果保存 145 FileStorage fs("caliResult.xml", FileStorage::WRITE); 146 fs << "cameraId" << cameraId; 147 fs << "intrinsic_parameters" << intrMatrix; 148 fs << "distortion_parametes" << distCoeffs; 149 fs.release(); 150 151 return 0; 152 }
该程序有三个参数输入,基于命令行读入参数
-w #标定板一个方向上的角点数
-h #标定板另一个方向上的角点数
-s #标定板上正方形的边长,默认为1
另,发表下-s参数设置的观点。之所以该参数在很多标定实例程序中设置为默认1,是因为该参数的改变确实是会影响到标定结果,但是不会影响到摄像头的矫正。因为标定和矫正类似一个逆运算过程,单位定义对其没有影响。
-
实验结果
标定过程,
标定结果,
xml version="1.0"?> <opencv_storage> <cameraId>0cameraId> <intrinsic_parameters type_id="opencv-matrix"> <rows>3rows> <cols>3cols> <dt>ddt> <data> 7.7881772950073355e+002 0. 3.1562441595543476e+002 0. 7.8624564811643825e+002 2.5630331974129393e+002 0. 0. 1.data>intrinsic_parameters> <distortion_parametes type_id="opencv-matrix"> <rows>1rows> <cols>5cols> <dt>ddt> <data> -7.2660835182078581e-002 2.0765291395491934e+000 5.9477659924542790e-004 -8.2981148319346263e-004 -7.0307616798578119e+000data>distortion_parametes> opencv_storage>