双摄像头立体成像(二)-摄像头标定

写在题前:这篇文章磨磨蹭蹭了很久,曾经两次接近完稿而丢失。我想任何事情在起步时都会有相似的囧境,还好我还有恒心继续下去。 ios

摄像头标定的目的有两个。第一,要还原摄像头成像的物体在真实世界的位置就须要知道世界中的物体到计算机图像平面是如何变换的,摄像头标定的目的之一就是为了搞清楚这种变换关系,求解内外参数矩阵。第二,针孔摄像头的发明使得摄像头变成了亲民物品,大行于世,可是针孔摄像头有个很大的问题——畸变。摄像头标定的另外一个目的就是求解畸变系数,而后用于计算求解正确的成像。 数组

  • 数学原理

    • 映射矩阵-内部参数(intrinsic parameters)和外部参数(extrinsic parameters)

前面所说的真实世界到成像平面的变换过程牵扯到四个坐标系,变换矩阵能够大体分为两组参数——内部参数和外部参数。下面依次根据四种坐标系的关系来推导内外参数的形式。ide

计算机坐标系和成像平面坐标系

计算机坐标系是指数字图像在计算机中的保存形式-二维数组的坐标形式。成像平面坐标系是摄像机镜头的成像平面上创建的坐标系,通常是位于感光器件上(如CCD)所创建的以镜头光轴与成像平面交点为原点的二维坐标系。因为二维数组以离散的形式保存,因此计算机坐标系的长度单位为1,通常感光器件大小约为指甲盖大小,上面又密集的分别不少感光单元,每一个感光单元采集的图像都会计算成计算机坐标系中的像素值,因此通常长度单位为微米。具体两者关系见下图,函数

图中坐标系O-uv为计算机坐标系,其坐标轴方向从右上到左下递增。坐标系Q-xy为成像平面坐标系,其坐标原点Q在计算机坐标系的坐标为(u0,v0)。假设有一点P,其在计算机坐标系中的坐标为(u,v),在成像平面坐标系中的坐标为(x,y),并设像素单元在x,y方向的尺寸分别为a,b。则有以下等式成立,ui

整理成齐次变换矩阵的形式以下,spa

成像平面坐标系和摄像机坐标系

摄像机坐标系是一个以摄像机镜头的光心为原点而创建的空间三维坐标系。成像平面坐标系能够当作摄像机坐标系在其Zc轴上投影而成的(其x和y轴与成像平面坐标系方向一致)。其与成像平面坐标系的关系以下图,命令行

这里须要详细解释一下上图怎样理解。图中Oc-XcYcZc为摄像机坐标系,M点为物点,而Q-xy为成像平面坐标系。实际状况下成像平面与物体应该分居摄像头两侧,可是在这里咱们将成像平面以摄像头坐标系原点为中心对称点对称过来。这样的好处是原本倒立的像变成正立的,且大小不变。如上一篇博客写到的那样,中心对称的后的像点和物点连线一定过光心。咱们要记住小孔成像的性质是,成像平面到镜头的距离始终等于焦距f。由类似三角形,有以下等式成立,3d

整理成齐次变换矩阵的形式以下,code

摄像机坐标系和世界坐标系

世界坐标系是区别于摄像机坐标系的一个空间坐标系,其依附于咱们标定时要使用的物点而存在。既咱们观察的物点相对于世界坐标系的位置是固定的,如张正友标定法中就是选的世界坐标系为以chessboard平面为XOY平面的相对于chessboard不动的空间坐标系。下图是我费了老大劲绘制的摄像机坐标系和世界坐标系之间的位置关系,xml

咱们知道空间中的两个坐标系能够经过平移旋转变换而重合,那么如今咱们来简单粗暴解释一下物点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. 角点检测,若是没有检测到角点重复第一步
  3. 亚像素检测以提升角点检测精度
  4. 标记检测出的角点
  5. 若是成功检测到角点图片小于预设的数目,重复上面四个步骤
  6. 标定
  7. 标定结果保存

我写的标定程序以下,

  1 /*
  2     Writer: Wang Xianshun
  3     Email:    german_iris@outlook.com
  4 */
  5 #include <iostream>
  6 #include <stdio.h>
  7 #include <time.h>
  8 #include <string.h>
  9 
 10 #include <cv.hpp>
 11 #include <highgui\highgui.hpp>
 12 #include <calib3d\calib3d.hpp>
 13 #include <imgproc\imgproc.hpp>
 14 #include <core\core.hpp>
 15 
 16 using namespace std;
 17 using namespace cv;
 18 
 19 static void calcChessboardCorners(Size boardSize, float squareSize, vector<Point3f>& 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<vector<Point2f>> imagePoints;
 44     float squareSize = 1.f;
 45     Mat intrMatrix, distCoeffs;
 46     vector<Mat> 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<Point2f> 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<vector<Point3f>> 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>0</cameraId>
<intrinsic_parameters type_id="opencv-matrix">
  <rows>3</rows>
  <cols>3</cols>
  <dt>d</dt>
  <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>1</rows>
  <cols>5</cols>
  <dt>d</dt>
  <data>
    -7.2660835182078581e-002 2.0765291395491934e+000
    5.9477659924542790e-004 -8.2981148319346263e-004
    -7.0307616798578119e+000</data></distortion_parametes>
</opencv_storage>