一、相机标定的原理
相机标定的目的?
找出三维空间与二维图像的对应关系。
1.坐标系
在这里总共涉及到四个坐标系的变化。这四个坐标系分别是:(1)像素坐标系;(2)成像坐标系;(3)相机坐标系;(4)世界坐标系。
1.像素坐标系
像素坐标系是一个直角坐标系,其坐标轴为,其定义方式是:原点位于图像的左上角,
轴向右与
轴平行,
轴向下与
轴平行,其反映了相机CCD/CMOS芯片中像素的排列情况,在像素坐标系中单位是像素。像素坐标系与成像平面之间,相差了一个缩放和一个原点的平移。
2.成像坐标系
成像坐标系是用来表示图像的物理位置的坐标系,定义此坐标轴为,则此坐标系中的坐标
表示图像上点的位置坐标。
对应的成像坐标系中的物理坐标的单位是毫米。
3.相机坐标系
相机坐标系是一个三维直角坐标系,其坐标轴为,原点位于镜头光心处,其中
轴称为相机的光轴,垂直于图像平面,
轴平行于成像坐标系的
轴,
轴平行于成像坐标系的
轴。
4.世界坐标系
世界坐标系作为一个基准,选择世界坐标系没有固定的方式,具体情况具体来考虑。其可以用来描述待测物体与相机的相对位置关系。其坐标轴为。
2.坐标系与坐标系之间的关系
1.世界坐标系与相机坐标系
设在相机坐标系下有一个点,它的坐标为
。
在世界坐标系下对应的坐标为
。
因此,世界坐标系与相机坐标系中的坐标存在如下的关系:
其中,是旋转矩阵,为3x3的正交单位矩阵;t是一个平移矩阵,
。
同时这里用了一个数学技巧(《视觉SLAM14讲》第43页):我们在一个三维向量的末尾添加1,将其变成了四维向量,称为齐次坐标。
其中称为外参数矩阵。
2.像素坐标系与成像坐标系
在二维成像平面坐标系中,相机光轴与图像平面的交点即为原点,该点称为图像的主点,如果我们不去考虑畸变,图像的主点就是图像的中心点,但是由于相机系统加工,外力作用等原因会产生一定的偏移。设主点
在像素坐标系中的坐标为
,则像素坐标系与成像坐标系对应关系如下:
3.线性模型
相机的线性模型,即是设空间中的任意一点在图像上所成的像可以近似认为是通过小孔成像原理在像平面成像的,所以又称作针孔模型。用简单的针孔相机模型来对这种映射关系进行建模。同时,由于相机镜头上的透镜的存在,使得光线投影到成像平面的过程会产生畸变。
按照这个模型,记空间中的一点在相机坐标系中的坐标为
,在成像平面坐标系中的坐标为
,相机的焦距为
,则根据比例关系:
可以得到投影矩阵:
根据世界坐标系与相机坐标系的坐标存在如下的关系:
可以推出
再根据像素坐标系与成像坐标系存在如下关系:
可以推出
记,
;
,则有
其中,为3x3矩阵称为相机内参数矩阵,包含4个未知数;
称为相机外参数矩阵包含了一个
和
。
这里我们可以看到将一个3x4的矩阵记为了,变成了一个3x3的矩阵。后面的
为一个4x4的矩阵,显然是不能进行计算的。那么,为什么我们这里会这样考虑呢?具体可以看高博十四讲中的一些数学技巧,主要是关于齐次坐标和非齐次坐标的转化。
二、Matlab具体标定步骤
在开始标定之前,需要准备好标定板。这个可以从网上下载打印。
我使用的Matlab版本是R2017b。打开Matlab后在APP中找到camera calibration。
注意:matlab打开camera calibration后可能找不到摄像头,这时候需要注册账号然后去安装驱动。我所使用的是罗技C270USB摄像头,如果只是为了理解其整个原理及标定的步骤,也可以使用笔记本电脑自带的摄像头进行操作。
安装好其驱动过后,就可以找到摄像头了。如下图所示。
下面开始添加照片,总共有两种办法,一种是直接从文件读取照片(这个也需要提前用准备标定的相机拍摄),另外一种办法是直接调用摄像头进行拍摄,然后再用照片。
标定的图片最少不能低于2张,最好控制在10到20张左右。张数比较少,可能存在的偏差比较大。按道理讲,张数越多也就是样本越多应该越准确才对。但是事实不是越多越好。据我猜测,应该是后面计算的畸变系数,涉及到有
注意:在这里我犯了一个很笨的错误,我用自己的手机去拍摄标定板的照片,然后直接用手机拍摄的照片进行标定操作。这样导致最后得出来的标定结果是我自己手机的参数。
然后添加完图片之后会出来一个窗口,这里需要输入你的标定板真实的尺寸,由于实验室有一块非常大的标定板,在这里,我直接进行使用。该标定板的是9cmx9cm的正方形。
点击确定过后,会出现下面这张图。
然后点击Export Camera Parameters
点击确定。会看到如下结果。
cameraParams =
cameraParameters - 属性:
Camera Intrinsics
IntrinsicMatrix: [3×3 double]
FocalLength: [813.8289 813.8800]
PrincipalPoint: [344.9927 248.5357]
Skew: 0
RadialDistortion: [0.0201 0.3590]
TangentialDistortion: [0 0]
ImageSize: [480 640]
Camera Extrinsics
RotationMatrices: [3×3×10 double]
TranslationVectors: [10×3 double]
Accuracy of Estimation
MeanReprojectionError: 0.2204
ReprojectionErrors: [99×2×10 double]
ReprojectedPoints: [99×2×10 double]
Calibration Settings
NumPatterns: 10
WorldPoints: [99×2 double]
WorldUnits: 'millimeters'
EstimateSkew: 0
NumRadialDistortionCoefficients: 2
EstimateTangentialDistortion: 0
然后再Matlab中输入以下三行命令即可得到我们要标定的一些参数。
>> cameraParams.IntrinsicMatrix
ans =
813.8289 0 0
0 813.8800 0
344.9927 248.5357 1.0000
>> cameraParams.RadialDistortion
ans =
0.0201 0.3590
>> cameraParams.TangentialDistortion
ans =
0 0
到这里,对罗技C270摄像头的标定就完成了。
三、C++实现
主要参考博客
张正友相机标定Opencv实现以及标定流程&&标定结果评价&&图像矫正流程解析(附标定程序和棋盘图)
代码参考:
#include "opencv2/core/core.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/calib3d/calib3d.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
#include <fstream>
#include <string>
using namespace cv;
using namespace std;
int main()
{
ifstream fin("calibdata.txt"); /* 标定所用图像文件的路径 */
ofstream fout("caliberation_result.txt"); /* 保存标定结果的文件 */
//读取每一幅图像,从中提取出角点,然后对角点进行亚像素精确化
cout<<"开始提取角点………………";
int image_count=0; /* 图像数量 */
Size image_size; /* 图像的尺寸 */
Size board_size = Size(7,7); /* 标定板上每行、列的角点数 */
vector<Point2f> image_points_buf; /* 缓存每幅图像上检测到的角点 */
vector<vector<Point2f> > image_points_seq; /* 保存检测到的所有角点 */
string filename;
int count= -1 ;//用于存储角点个数。
cv::namedWindow("test",CV_WINDOW_AUTOSIZE);
while (getline(fin,filename))
{
image_count++;
// 用于观察检验输出
cout<<"image_count = "<<image_count<<endl;
/* 输出检验*/
cout<<"-->count = "<<count;
// Mat haha=imread("/home/zzn/projects/qt59-c or c++/calib/build-calib-Desktop-Default/chess1.jpg");
// imshow("test",haha);
// waitKey(0);
string filename1="/home/zzn/projects/qt59-c or c++/calib/build-calib-Desktop-Default/"+filename;
std::cout<<"filename1: "<<filename1<<std::endl;
Mat imageInput=imread("/home/zzn/projects/qt59-c or c++/calib/build-calib-Desktop-Default/chess1.jpg");
imshow("test",imageInput);
if (image_count == 1) //读入第一张图片时获取图像宽高信息
{
image_size.width = imageInput.cols;
image_size.height =imageInput.rows;
cout<<"image_size.width = "<<image_size.width<<endl;
cout<<"image_size.height = "<<image_size.height<<endl;
}
/* 提取角点 */
if (0 == findChessboardCorners(imageInput,board_size,image_points_buf))
{
cout<<"size: "<<image_points_buf.size()<<endl;
cout<<"can not find chessboard corners!\n"; //找不到角点
exit(1);
}
else
{
Mat view_gray;
cvtColor(imageInput,view_gray,CV_RGB2GRAY);
/* 亚像素精确化 */
find4QuadCornerSubpix(view_gray,image_points_buf,Size(5,5)); //对粗提取的角点进行精确化
//cornerSubPix(view_gray,image_points_buf,Size(5,5),Size(-1,-1),TermCriteria(CV_TERMCRIT_EPS+CV_TERMCRIT_ITER,30,0.1));
image_points_seq.push_back(image_points_buf); //保存亚像素角点
/* 在图像上显示角点位置 */
drawChessboardCorners(view_gray,board_size,image_points_buf,false); //用于在图片中标记角点
imshow("Camera Calibration",view_gray);//显示图片
waitKey(500);//暂停0.5S
}
}
int total = image_points_seq.size();
cout<<"total = "<<total<<endl;
int CornerNum=board_size.width*board_size.height; //每张图片上总的角点数
for (int ii=0 ; ii<total ;ii++)
{
if (0 == ii%CornerNum)// 24 是每幅图片的角点个数。此判断语句是为了输出 图片号,便于控制台观看
{
int i = -1;
i = ii/CornerNum;
int j=i+1;
cout<<"--> 第 "<<j <<"图片的数据 --> : "<<endl;
}
if (0 == ii%3) // 此判断语句,格式化输出,便于控制台查看
{
cout<<endl;
}
else
{
cout.width(10);
}
//输出所有的角点
cout<<" -->"<<image_points_seq[ii][0].x;
cout<<" -->"<<image_points_seq[ii][0].y;
}
cout<<"角点提取完成!\n";
//以下是摄像机标定
cout<<"开始标定………………";
/*棋盘三维信息*/
Size square_size = Size(18,18); /* 实际测量得到的标定板上每个棋盘格的大小 */
vector<vector<Point3f> >object_points; /* 保存标定板上角点的三维坐标 */
/*内外参数*/
Mat cameraMatrix=Mat(3,3,CV_32FC1,Scalar::all(0)); /* 摄像机内参数矩阵 */
vector<int> point_counts; // 每幅图像中角点的数量
Mat distCoeffs=Mat(1,5,CV_32FC1,Scalar::all(0)); /* 摄像机的5个畸变系数:k1,k2,p1,p2,k3 */
vector<Mat> tvecsMat; /* 每幅图像的平移向量 */
vector<Mat> rvecsMat; /* 每幅图像的旋转向量 */
/* 初始化标定板上角点的三维坐标 */
int i,j,t;
for (t=0;t<image_count;t++)
{
vector<Point3f> tempPointSet;
for (i=0;i<board_size.height;i++)
{
for (j=0;j<board_size.width;j++)
{
Point3f realPoint;
/* 假设标定板放在世界坐标系中z=0的平面上 */
realPoint.x = i*square_size.width;
realPoint.y = j*square_size.height;
realPoint.z = 0;
tempPointSet.push_back(realPoint);
}
}
object_points.push_back(tempPointSet);
}
/* 初始化每幅图像中的角点数量,假定每幅图像中都可以看到完整的标定板 */
for (i=0;i<image_count;i++)
{
point_counts.push_back(board_size.width*board_size.height);
}
/* 开始标定 */
calibrateCamera(object_points,image_points_seq,image_size,cameraMatrix,distCoeffs,rvecsMat,tvecsMat,0);
cout<<"标定完成!\n";
//对标定结果进行评价
cout<<"开始评价标定结果………………\n";
double total_err = 0.0; /* 所有图像的平均误差的总和 */
double err = 0.0; /* 每幅图像的平均误差 */
vector<Point2f> image_points2; /* 保存重新计算得到的投影点 */
cout<<"\t每幅图像的标定误差:\n";
fout<<"每幅图像的标定误差:\n";
for (i=0;i<image_count;i++)
{
vector<Point3f> tempPointSet=object_points[i];
/* 通过得到的摄像机内外参数,对空间的三维点进行重新投影计算,得到新的投影点 */
projectPoints(tempPointSet,rvecsMat[i],tvecsMat[i],cameraMatrix,distCoeffs,image_points2);
/* 计算新的投影点和旧的投影点之间的误差*/
vector<Point2f> tempImagePoint = image_points_seq[i];
Mat tempImagePointMat = Mat(1,tempImagePoint.size(),CV_32FC2);
Mat image_points2Mat = Mat(1,image_points2.size(), CV_32FC2);
for (int j = 0 ; j < tempImagePoint.size(); j++)
{
image_points2Mat.at<Vec2f>(0,j) = Vec2f(image_points2[j].x, image_points2[j].y);
tempImagePointMat.at<Vec2f>(0,j) = Vec2f(tempImagePoint[j].x, tempImagePoint[j].y);
}
err = norm(image_points2Mat, tempImagePointMat, NORM_L2);
total_err += err/= point_counts[i];
std::cout<<"第"<<i+1<<"幅图像的平均误差:"<<err<<"像素"<<endl;
fout<<"第"<<i+1<<"幅图像的平均误差:"<<err<<"像素"<<endl;
}
std::cout<<"总体平均误差:"<<total_err/image_count<<"像素"<<endl;
fout<<"总体平均误差:"<<total_err/image_count<<"像素"<<endl<<endl;
std::cout<<"评价完成!"<<endl;
//保存定标结果
std::cout<<"开始保存定标结果………………"<<endl;
Mat rotation_matrix = Mat(3,3,CV_32FC1, Scalar::all(0)); /* 保存每幅图像的旋转矩阵 */
fout<<"相机内参数矩阵:"<<endl;
fout<<cameraMatrix<<endl<<endl;
fout<<"畸变系数:\n";
fout<<distCoeffs<<endl<<endl<<endl;
for (int i=0; i<image_count; i++)
{
fout<<"第"<<i+1<<"幅图像的旋转向量:"<<endl;
fout<<rvecsMat[i]<<endl;
/* 将旋转向量转换为相对应的旋转矩阵 */
Rodrigues(rvecsMat[i],rotation_matrix);
fout<<"第"<<i+1<<"幅图像的旋转矩阵:"<<endl;
fout<<rotation_matrix<<endl;
fout<<"第"<<i+1<<"幅图像的平移向量:"<<endl;
fout<<tvecsMat[i]<<endl<<endl;
}
std::cout<<"完成保存"<<endl;
fout<<endl;
/************************************************************************
显示定标结果
*************************************************************************/
Mat mapx = Mat(image_size,CV_32FC1);
Mat mapy = Mat(image_size,CV_32FC1);
Mat R = Mat::eye(3,3,CV_32F);
std::cout<<"保存矫正图像"<<endl;
string imageFileName;
std::stringstream StrStm;
for (int i = 0 ; i != image_count ; i++)
{
std::cout<<"Frame #"<<i+1<<"..."<<endl;
initUndistortRectifyMap(cameraMatrix,distCoeffs,R,cameraMatrix,image_size,CV_32FC1,mapx,mapy);
StrStm.clear();
imageFileName.clear();
string filePath="chess";
StrStm<<i+1;
StrStm>>imageFileName;
filePath+=imageFileName;
filePath+=".bmp";
Mat imageSource = imread(filePath);
Mat newimage = imageSource.clone();
//另一种不需要转换矩阵的方式
//undistort(imageSource,newimage,cameraMatrix,distCoeffs);
remap(imageSource,newimage,mapx, mapy, INTER_LINEAR);
StrStm.clear();
filePath.clear();
StrStm<<i+1;
StrStm>>imageFileName;
imageFileName += "_d.jpg";
imwrite(imageFileName,newimage);
}
std::cout<<"保存结束"<<endl;
return 0;
}
使用CMake的编译方法
CMakelists编写
cmake_minimum_required(VERSION 2.8)
project(calib)
set(OpenCV_DIR /opt/ros/kinetic/share/OpenCV-3.3.1-dev)
find_package(OpenCV REQUIRED)
INCLUDE_DIRECTORIES(${OpenCV_INCLUDE_DIRS})
add_executable(${PROJECT_NAME} "main.cpp")
TARGET_LINK_LIBRARIES(${PROJECT_NAME} ${OpenCV_LIBS})
终端输入
cmake .
make
经过调试发现与输入的图片格式无关,可能编译不成功和IDE有关,故采取CMake的方法,编译成功。
以上有的地方理解的并不是特别深刻,所以难免会有错误。
大家多多交流,相互学习。
参考资料
- 摄像机标定方法的研究
- 《视觉SLAM十四讲 从理论到实践》高翔 张涛 等著