网上绝大部分博客关于相机标定的讲解全都关于理论上的,很少有代码的实现。因此,打算写这篇博客。博客并不涉及相关公式推导,假设大家都已经懂三大世界坐标系、内外参等名词含义。
一:相机标定的作用
(1):求解内外参数。
(2):用于处理畸变矫正。
二:标定的流程
(1):准备若干张标定图片(至少四张)
(2):图像预处理,清除图像上无关的轮廓信息
(3):提取角点信息。(同心圆的圆心即为角点)
(4):调用相机标定函数
(5):计算重投影误差
2.1:
实验所用的靶标图像如下所示,在相机标定实验中一般要求图片最少为四张。在文章最后,我会给出本实验所用的图像,方便大家使用。
2.2:
下图为输入的靶标图像Canny后的效果,可以看到,图像上有许多干扰的边缘轮廓,我们仅仅是对图像上11*9的同心圆轮廓感兴趣,因此需要进行图像预处理操作。具体的操作打算在另一篇博客里进行阐述,此处简单了解一下。
2.3:
在图像预处理之后,利用opencv中的fitEllipse椭圆拟合函数,得到同心圆的中心坐标。并在原始图像上画了出来,我已经把此次标定实验所提取的中心坐标存放到了文本当中,会附录在百度云盘里。
2.4:
其实,opencv中已经封装好了相机标定函数,我们仅仅是搬运工,会调用函数,知道其中参数的含义即可。利用calibrateCamera函数计算出所需的内外参数。
double cv::calibrateCamera (
InputArrayOfArrays objectPoints,
InputArrayOfArrays imagePoints,
Size imageSize,
InputOutputArray cameraMatrix,
InputOutputArray distCoeffs,
OutputArrayOfArrays rvecs,
OutputArrayOfArrays tvecs,
int flags = 0,
TermCriteria criteria = TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, DBL_EPSILON)
)
2.5:
重投影误差主要是用来评价所求的内外参数的精度,误差越小,说明求出的内外参数比较靠谱,能作为下一步的输入。
the 1 image of average error: 0.0143862pix
the 2 image of average error: 0.0155335pix
the 3 image of average error: 0.0138766pix
the 4 image of average error: 0.0140222pix
the 5 image of average error: 0.0144393pix
the 6 image of average error: 0.0135796pix
the 7 image of average error: 0.0147223pix
the 8 image of average error: 0.0143567pix
上面为本次实验重投影的平均误差,误差越小越好。
三:代码部分
#include "widget.h"
#include <QApplication>
#include<stdlib.h>
#include <iostream>
#include <fstream>
#include <vector>
#include<opencv2/calib3d/calib3d.hpp>
#include<opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/calib3d.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;
void convert_float(char name[],double temp[2])
{
char filename[20]; char tt[10];
int i=0,num=0,k=0;
for(k;name[k]!='\0';k++)
{
filename[k]=name[k];
}
filename[k]='\0';
temp[0]=atof(filename);
for(i;filename[i]!=' ';i++);
for(i;filename[i]!='\0';i++)
{
tt[num]=filename[i];
num++;
}
tt[num]='\0';
temp[1]=atof(tt);
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
char filename[100];
string infilename = "H:/Image/cc/center4.txt"; //注意改下各自电脑上的路径
cv::Size imageSize;
imageSize.width=1280;
imageSize.height=1024;
//标定板上每行每列的角点数
cv::Size boardSize=cv::Size(11,9);
//缓存每幅图片上检测到的角点
std::vector<Point2f> imagePointsBuf;
//保存检测到的所有角点
std::vector<std::vector<Point2f>> imagePointsSeq;
ifstream fin(infilename);
if(fin.is_open())
{
while(!fin.eof())
{
fin.getline(filename,sizeof(filename)/sizeof(char));
if(filename[0]=='#')
{
imagePointsSeq.push_back(imagePointsBuf);
imagePointsBuf.clear();
continue;
}
Point2f temp_coordinate; double temp[2];
convert_float(filename,temp);
temp_coordinate.x=temp[0];
temp_coordinate.y=temp[1];
imagePointsBuf.push_back(temp_coordinate);
}
}
for(int i=0;i<imagePointsSeq.size();i++)
{
string imagePath ="H:/Image/cc/"+to_string(i+1)+".bmp";
Mat image=imread(imagePath);
vector<Point2f> temp=imagePointsSeq[i];
for(int j=0;j<temp.size();j++)
{
Point2f tt=temp[j];
circle(image,tt,2,Scalar(0,0,255),2,8);
}
imshow(to_string(i+1),image);
}
//保存标定板上角点的三维坐标
vector<vector<Point3f>> objectPoints;
//相机内参数矩阵 M=[fx γ u0,0 fy v0,0 0 1]
Mat cameraMatrix=cv::Mat(3,3,CV_64F,Scalar::all(0));
//相机的五个畸变系数 k1 k2 p1 p2 p3
Mat distCoeffs=Mat(1,5,CV_64F,Scalar::all(0));
//每幅图片的旋转向量
vector<Mat> tvecsMat;
//每幅图片的平移向量
vector<Mat> rvecsMat;
//初始化标定板上角点的三维坐标 给出每个角点的世界坐标系下坐标
int i,j,t;
for(t=0;t<8;t++) //我只用八张图像进行标定
{
vector<Point3f> tempPointSet;
//行数
for(i=0;i<boardSize.height;i++) // 9
{
//列数
for(j=0;j<boardSize.width;j++) // 11
{
Point3f realPoint; //每一幅图片上有11*9 个角点
//假定标定板放在世界坐标系中Z=0的平面上
realPoint.x=i*30.0;
realPoint.y=j*30.0;
realPoint.z=0;
tempPointSet.push_back(realPoint);
}
}
objectPoints.push_back(tempPointSet);
}
// //开始标定
calibrateCamera(objectPoints,imagePointsSeq,imageSize,cameraMatrix,distCoeffs,rvecsMat,tvecsMat);
cout<<cameraMatrix<<endl;// 输出相机内参数矩阵
// 输出每张图片的旋转矩阵、平移向量
for(int i=0;i<8;i++)
{
cout<<i+1<<" "<<"picture"<<endl;
cout<<rvecsMat[i]<<endl;
cout<<tvecsMat[i]<<endl;
cout<<endl;
}
cout<<"calibration is over"<<endl;
cout<<"start to estimate result "<<endl;
//所有图像的平均误差总和
double totalErr=0.0;
//每幅图像的平均误差
double err=0.0;
//保存重新计算得到的投影点
vector<Point2f> imagePoints2;
for(i=0;i<8;i++)
{
vector<Point3f> tempPointSet=objectPoints[i]; //每幅图片 角点的世界坐标
//通过对得到的相机内外参数 对空间的三维点进行重新投影计算,得到新的投影点 imagePoints2(在像素坐标系下的点坐标)
//就是利用 得到的内外参数 世界坐标系下角点坐标 计算出每个角点的像素坐标
//每幅图片的 所有角点世界坐标 旋转矩阵 平移向量 内参数矩阵 畸变系数 保存计算到的每幅图片 角点的二维坐标
projectPoints(tempPointSet,rvecsMat[i],tvecsMat[i],cameraMatrix,distCoeffs,imagePoints2);
//计算 新投影点和 旧投影点之间的误差
vector<Point2f> tempImagePoint=imagePointsSeq[i];
Mat tempImagePointMat=Mat(1,tempImagePoint.size(),CV_32FC2); //原始二维点
Mat imagePoints2Mat=Mat(1,imagePoints2.size(),CV_32FC2); //新计算出来的二维点
for( j=0;j<tempImagePoint.size();j++)
{
imagePoints2Mat.at<cv::Vec2f>(0,j)=cv::Vec2f(imagePoints2[j].x,imagePoints2[j].y);
tempImagePointMat.at<cv::Vec2f>(0,j)=cv::Vec2f(tempImagePoint[j].x,tempImagePoint[j].y);
}
//计算误差
err=norm(imagePoints2Mat,tempImagePointMat,NORM_L2);
err/=99;
cout<<"the "<<i+1<<" image of average error: "<<err<<"pix"<<endl;
}
return a.exec();
}
代码部分大多加了注释,一步一步的看,还是能够看到明白的。当然,这只是基础的单目相机标定,仅仅适合入门。想要提高精度,任重而道远。
若有问题,敬请指出。
另外,附上靶标图像以及保存椭圆中心的文本center4.txt.
链接:https://pan.baidu.com/s/1XcZzLUWPHSNQ_f6bxABEnw
提取码:6tg8