相机标定系列---opencv相关标定算子

简介: 相机标定系列---opencv相关标定算子



【1】标定的相关介绍

(1)标定的目的

       在进行相机标定之前一定要搞清楚相机标定的目的,简单的说,相机标定主要是根据标定板在相机下的不同位置去求解出相机的内参和外参。内参是相机的固有属性,也就是说无论你标定板如何放置,你的相机固不固定,相机的内参不受影响,但是内参的准确性与标定板图像的个数有关,一般而言标定板放置的位置越全面,相机的内参越准确,误差越小。对于相机的外参而言,是世界坐标系相对于相机坐标系的空间变换(旋转矩阵和平移矩阵)。

(2)内参介绍

        :单位毫米内X方向上像素的个数;

        :单位毫米内Y方向上像素的个数;

        :表示图像中心的x、y的坐标;

内参矩阵(3 x 3)如下图所示:

在实际应用中,使用的是远心镜头,在景深范围内,图像的视野大小一定,也就是说远心镜头不会发生畸变,而且内参是一定的,使用时候按照相机的参数配置即可。但对于普通相机,类似鱼眼镜头,使用时需要需要对内参进行校正,相机是否固定无所谓。

(3)畸变系数

      畸变系数主要有5个参数:k1,k2,k3径向畸变系数,p1,p2是切向畸变系数。径向畸变发生在相机坐标系转图像物理坐标系的过程中。而切向畸变是发生在相机制作过程,其是由于感光元平面跟透镜不平行。径向畸变,即由于透镜的不同区域的焦距的不同而引起的畸变,分为枕形畸变和桶形畸变如下图所示,越靠近镜头边缘畸变越明显。形变如图所示:

(4)相机外参

相机的外参是世界坐标系在相机坐标系下的描述。R是旋转参数是每个轴的旋转矩阵的乘积,其中每个轴的旋转参数( ϕ , ω , θ ),平移参数( Tx , Ty , Tz ) ,矩阵为(4 x 4齐次矩阵)如下:

【2】算法流程及相关算子简介

(1)算法流程主要有五部分:

       1.提取标定板的角点。

       2.亚像素角点精确化。

       3.可视化角点。

       4.相机标定。

       5.误差计算(重投影误差)。

(2)相关算子介绍

1.棋盘标定板查找角点

bool findChessboardCorners(
    InputArray image, 
    Size patternSize, 
    OutputArray corners,
    int flags = CALIB_CB_ADAPTIVE_THRESH + CALIB_CB_NORMALIZE_IMAGE 
);

第一个参数是传入的图像,该图像的类型是8UC1和8UC3。

第二参数是棋盘标定板标中内角点的个数(一行几个,一列几个),例如

Size size(9,6)
//一行九个内角点,一列6个内角点

第三个参数是用来记录查找的角点像素坐标

其数据类型是vector<point2f> 将一副图的角点坐标存入,对于多副标定板角点的存储是定义

vector<vector<point2f>>

第四个参数:

CALIB_CB_ADAPTIVE_THRESH = 1,通过自适应阈值法查找标定板;

CALIB_CB_NORMALIZE_IMAGE = 2,如果设置此标志,则在应用阈值之前使用CV::equalizeHist()来归一化图像;                                                                                              CALIB_CB_FILTER_QUADS    = 4,当标定板图像存在畸变时,就会对四边形应用加以约束,防止出现错误的四边形;

CALIB_CB_FAST_CHECK      = 8,对图像进行快速扫描,以确保图像中存在角点,如果不存在角点,则直接跳过此图像。

该算子的返回值是是否成功查找到角点。

(查找圆网格标定板于此类似,暂不介绍。)

2.亚像素角点准确化

主要是两个算子cv::cornerSubPix和find4QuadCornerSubpix 二者参数一样。

bool cv::find4QuadCornerSubpix  ( InputArray  img,  
  InputOutputArray  corners,  
  Size  region_size  
 )

img:输入的Mat矩阵,最好是8位灰度图像,检测效率更高;                                                    corners:初始的角点坐标向量,同时作为亚像素坐标位置的输出,所以需要是浮点型数据,一般用元素是Pointf2f/Point2d的向量来表示。

region_size:角点搜索窗口的尺寸,优化坐标时考虑的邻域范围。

3.可视化角点

对于角点的可视化算子是drawChessboardCorners。

void cv::drawChessboardCorners  ( InputOutputArray  image,  
  Size  patternSize,  
  InputArray  corners,  
  bool  patternWasFound  
 )

image:需要绘制角点的目标图像,它必须是一个8位彩色图像。

patternSize:每张标定棋盘上内角点的行列数。

corners:检测到的角点坐标数组。

patternWasFound:绘制角点样式的标志,用于显示是否找到完整标定板。

patternWasFound=ture时,依次连接各个内角点。

patternWasFound=false时,以(红色)圆圈标记处角点位置。

4.相机标定

calibrateCamera是相机标定的算子,具体函数如下:

double calibrateCamera(        
        InputArrayOfArrays objectPoints,        //输入:目标点位 的集合的集合
        InputArrayOfArrays imagePoints,         //输入:图像点位 的集合的集合
        Size imageSize,                         //输入:图像尺寸 
        InputOutputArray cameraMatrix,          //输出:相机内参矩阵(fx,fy,cx,cy)
        InputOutputArray distCoeffs,            //输出:畸变矫正多项式(k1,k2,p1,p2,k3...)
        OutputArrayOfArrays rvecs,              //输出:旋转矩阵 的集合
        OutputArrayOfArrays tvecs,              //输出:平移矩阵 的集合
        int flags = 0,                          //输入:对标定过程方法进行更精细的控制
        TermCriteria criteria =                 //输入:迭代参数
        TermCriteria(TermCriteria::COUNT + TermCriteria::EPS, 30, DBL_EPSILON));

第一个参数objectPoints,为世界坐标系中的三维点。在使用时,应该输入一个三维坐标点的向量的向量,即vector<vector<Point3f>> object_points。需要依据棋盘上单个黑白矩阵的大小,计算出(初始化)每一个内角点的世界坐标。

第二个参数imagePoints,为每一个内角点对应的图像坐标点。和objectPoints一样,应该输入vector<vector<Point2f>> image_points_seq形式的变量;

第三个参数imageSize,为图像的像素尺寸大小,在计算相机的内参和畸变矩阵时需要使用到该参数;

第四个参数cameraMatrix为相机的内参矩阵。输入一个Mat cameraMatrix即可,如Mat cameraMatrix=Mat(3,3,CV_32FC1,Scalar::all(0));

第五个参数distCoeffs为畸变矩阵。输入一个Mat distCoeffs=Mat(1,5,CV_32FC1,Scalar::all(0))即可;

第六个参数rvecs为旋转向量;应该输入一个Mat类型的vector,即vector<Mat>rvecs;

第七个参数tvecs为位移向量,和rvecs一样,应该为vector<Mat> tvecs;

第八个参数flags为标定时所采用的算法。有如下几个参数:

CV_CALIB_USE_INTRINSIC_GUESS:使用该参数时,在cameraMatrix矩阵中应该有fx,fy,u0,v0的估计值。否则的话,将初始化(u0,v0)图像的中心点,使用最小二乘估算出fx,fy。

CV_CALIB_FIX_PRINCIPAL_POINT:在进行优化时会固定光轴点。当CV_CALIB_USE_INTRINSIC_GUESS参数被设置,光轴点将保持在中心或者某个输入的值。

CV_CALIB_FIX_ASPECT_RATIO:固定fx/fy的比值,只将fy作为可变量,进行优化计算。当CV_CALIB_USE_INTRINSIC_GUESS没有被设置,fx和fy将会被忽略。只有fx/fy的比值在计算中会被用到。

CV_CALIB_ZERO_TANGENT_DIST:设定切向畸变参数(p1,p2)为零。

CV_CALIB_FIX_K1,…,CV_CALIB_FIX_K6:对应的径向畸变在优化中保持不变。

CV_CALIB_RATIONAL_MODEL:计算k4,k5,k6三个畸变参数。如果没有设置,则只计算其它5个畸变参数。

第九个参数criteria是最优迭代终止条件设定。

在使用该函数进行标定运算之前,需要对棋盘上每一个内角点的空间坐标系的位置坐标进行初始化,标定的结果是生成相机的内参矩阵cameraMatrix、相机的5个畸变系数distCoeffs,另外每张图像都会生成属于自己的平移向量和旋转向量。

返回值:重投影的均方根误差

5.误差计算

重投影误差projectPoints,对标定结果进行评价的方法是通过得到的摄像机内外参数,对空间的三维点进行重新投影计算,得到空间三维点在图像上新的投影点的坐标,计算投影坐标和亚像素角点坐标之间的偏差,偏差越小,标定结果越好。

void projectPoints( InputArray objectPoints,
                                 InputArray rvec, InputArray tvec,
                                 InputArray cameraMatrix, InputArray distCoeffs,
                                 OutputArray imagePoints,
                                 OutputArray jacobian=noArray(),
                                 double aspectRatio=0 );

第一个参数objectPoints,为相机坐标系中的三维点坐标;

第二个参数rvec为旋转向量,每一张图像都有自己的选择向量;

第三个参数tvec为位移向量,每一张图像都有自己的平移向量;

第四个参数cameraMatrix为求得的相机的内参数矩阵;

第五个参数distCoeffs为相机的畸变矩阵;

第六个参数imagePoints为每一个内角点对应的图像上的坐标点;

第七个参数jacobian是雅可比行列式;

第八个参数aspectRatio是跟相机传感器的感光单元有关的可选参数,如果设置为非0,则函数默认感光单元的dx/dy是固定的,会依此对雅可比矩阵进行调整;

【3】完整代码

#include<opencv2\opencv.hpp>
#include<opencv2\highgui\highgui.hpp>
#include<opencv2\imgproc\imgproc.hpp>
#include<iostream>
#include<fstream>
#include<io.h>
#include<string>
using namespace cv;
using namespace std;
const char* path = "E:\\乔大花进度\\11-24\\相机标定\\calibration2\\*.jpg";//标定板存放地址
string pic_path = "E:\\乔大花进度\\11-24\\相机标定\\calibration2\\"; 
int main(int argc, char** argv)
{
     //【1】 从文件夹中读取标定图片
  
  intptr_t handle;  //用于文件查找的句柄
  struct _finddata_t fileinfo;   //定义文件存储的结构体
  handle = _findfirst(path, &fileinfo); //用于第一次查找,handle记录查找结果,未找到返回-1;
  if (handle==-1)
  {
    return -1;    //未成功读取图片结束程序
  }
  if (fileinfo.attrib!= _A_SUBDIR)
  {
    printf("%s\n",fileinfo.name); //如果不是子文件夹则输出名字
  }
  ofstream outdata; //定义输出流
  outdata.open(pic_path + "list.txt",ios::trunc);//打开存放标定板名字的txt文件,并且清空其内容
  outdata << fileinfo.name << endl;
  outdata.close();
  while (!_findnext(handle,&fileinfo))//继续在文件夹下搜索标定板jpg格式文件,未找到返回-1,找到返回0;
  {
    ofstream outdata; //定义输出流
    if (fileinfo.attrib != _A_SUBDIR)//
      printf("%s\n", fileinfo.name);
    outdata.open(pic_path + "list.txt", ios::app);
    outdata << fileinfo.name << endl;
  }
  outdata.close();//关闭输出流
  _findclose(handle);//关闭查找句柄
  //定义vector用于存放图片名字;
  vector<string> pic_name;
  ifstream indata;
  indata.open(pic_path + "list.txt", ios::in);
  if (!indata)//未打开成功返回0
  {
    cout << "读取txt文件失败" << endl;
    return -1;
  }
  
  while (!indata.eof())
  {
    string str;
    getline(indata,str);
    if (str.length()>2)
    {
      pic_name.push_back(str);
    }
  }
  cout << "标定板照片的数量为" << pic_name.size() << endl;
  //【2】 依次读取每张照片,提取角点
  cout << "开始提取角点……" << endl;
  int image_nums = 0;  //图片数量
  Size image_size; //图片尺寸
  int points_per_row = 9;  //每行的内点数
  int points_per_col = 6;   //每列的内点数
  Size corner_size = Size(points_per_row, points_per_col); //标定板每行每列角点个数,共12*12个角点
  vector<Point2f> points_per_image;                            //缓存每幅图检测到的角点
  vector<vector<Point2f>> points_all_images;                   //用一个二维数组保存检测到的所有角点
  string image_file_name;                                          //声明一个文件名的字符串
  for (int i = 0; i < pic_name.size(); i++)
  {
    image_nums++;
    //读入图片
    Mat image_raw = imread(pic_path + pic_name[i]);
    if (image_nums == 1)
    {
      // cout<<"channels = "<<image_raw.channels()<<endl;
      // cout<<image_raw.type()<<endl;  //CV_8UC3
      image_size.width = image_raw.cols;  //图像的宽对应着列数
      image_size.height = image_raw.rows; //图像的高对应着行数
      cout << "image_size.width = " << image_size.width << endl;
      cout << "image_size.height = " << image_size.height << endl;
    }
    //角点检测
    Mat image_gray;                               
    cvtColor(image_raw, image_gray,COLOR_BGR2GRAY); //将BGR图转化为灰度图
                            
     //step1 提取角点,并且返回布尔值
    bool success = findChessboardCorners(image_gray, corner_size, points_per_image, CALIB_CB_FAST_CHECK);
    if (!success)
    {
      cout << pic_name[i]<<":图片未找到角点" << endl;
      continue;
    }
    else
    {
      //亚像素精确化(两种方法)
      //step2 亚像素角点
      find4QuadCornerSubpix(image_gray, points_per_image, Size(5, 5));
      // cornerSubPix(image_gray,points_per_image,Size(5,5));
      points_all_images.push_back(points_per_image); //保存亚像素角点
                               
      //step3 角点可视化,//在图中画出角点位置
      drawChessboardCorners(image_raw, corner_size, points_per_image, success); //将角点连线
      imshow("Camera calibration", image_raw);
      waitKey(0); //等待按键输入
      cout << "照片的序号为:" << image_nums << endl;
    }
  }
  //destroyAllWindows();
  int nums = points_all_images.size();
  cout << "处理标定照片的数量为:" << nums << endl;
  cout << "开始进行相机标定........." << endl;
  //【3】将标定板中二维像素点位转为三维空间点
  //开始相机标定
  Size2f block_size(0.023, 0.023);                            //每个小方格实际大小, 只会影响最后求解的平移向量t(标定板中每个小方块的实际大小)
  Mat camera_K(3, 3, CV_32FC1, Scalar::all(0));   //内参矩阵3*3
  Mat distCoeffs(1, 5, CV_32FC1, Scalar::all(0)); //畸变矩阵1*5
  vector<cv::Mat> rotationMat;                            //旋转矩阵
  vector<cv::Mat> translationMat;                         //平移矩阵
                              //初始化角点三维坐标,从左到右,从上到下!!!
  vector<Point3f> points3D_per_image;
  for (int i = 0; i < corner_size.height; i++)
  {
    for (int j = 0; j < corner_size.width; j++)
    {
      points3D_per_image.push_back(Point3f(block_size.width * j, block_size.height * i, 0));
    }
  }
  //生成与图片数量相同的标定板照片向量,也就是一张拍摄照片和一张标准标定板照片对应
  vector<vector<Point3f>> points3D_all_images(nums, points3D_per_image);  //保存所有图像角点的三维坐标, z=0 
  int point_counts = corner_size.area(); //每张图片上角点个数
  cout << "标定板的角点数量为:" << point_counts << endl;
  for (int i = 0; i < points3D_per_image.size(); i++)
  {
    cout << points3D_per_image[i] << endl;
  }
  //【4】开始进行标定
  calibrateCamera(points3D_all_images, points_all_images, image_size, camera_K, distCoeffs, rotationMat, translationMat, 0);
  //【5】对标定结果进行评价,计算标定误差
  ofstream fout;
  //将标定出来的误差存到该文件下;
  fout.open(pic_path+"calibration.txt", ios::app);
  double total_err = 0.0;               //所有图像平均误差总和
  double err = 0.0;                     //每幅图像的平均误差
  vector<cv::Point2f> points_reproject; //重投影点
  cout << "\n\t每幅图像的标定误差:\n";
  fout << "每幅图像的标定误差:\n";
  for (int i = 0; i < nums; i++)
  {
    vector<cv::Point3f> points3D_per_image = points3D_all_images[i];//标准的标定板图像
    //通过之前标定得到的相机内外参,对三维点进行重投影
    cv::projectPoints(points3D_per_image, rotationMat[i], translationMat[i], camera_K, distCoeffs, points_reproject);
    //计算两者之间的误差,转为双通道,一个通道存储x,一个通道存储y
    vector<cv::Point2f> detect_points = points_all_images[i];  //提取到的图像角点
    cv::Mat detect_points_Mat = cv::Mat(1, detect_points.size(), CV_32FC2); //变为1*144的矩阵,2通道保存提取角点的像素坐标
    cv::Mat points_reproject_Mat = cv::Mat(1, points_reproject.size(), CV_32FC2);  //2通道保存投影角点的像素坐标
    for (int j = 0; j < detect_points.size(); j++)
    {
      detect_points_Mat.at<cv::Vec2f>(0, j) = cv::Vec2f(detect_points[j].x, detect_points[j].y);
      points_reproject_Mat.at<cv::Vec2f>(0, j) = cv::Vec2f(points_reproject[j].x, points_reproject[j].y);
    }
    err = cv::norm(points_reproject_Mat, detect_points_Mat, cv::NormTypes::NORM_L2);
    total_err += err /= point_counts;
    cout << "第" << i + 1 << "幅图像的平均误差为: " << err << "像素" << endl;
    fout << "第" << i + 1 << "幅图像的平均误差为: " << err << "像素" << endl;
  }
  cout << "总体平均误差为: " << total_err / nums << "像素" << endl;
  fout << "总体平均误差为: " << total_err / nums << "像素" << endl;
  cout << "评价完成!" << endl;
  //将标定结果写入txt文件
  Mat rotate_Mat = Mat(3, 3, CV_32FC1, cv::Scalar::all(0)); //保存旋转矩阵
  cout << "\n相机内参数矩阵:" << endl;
  cout << camera_K << endl << endl;
  fout << "\n相机内参数矩阵:" << endl;
  fout << camera_K << endl << endl;
  cout << "畸变系数:\n";
  cout << distCoeffs << endl << endl << endl;
  fout << "畸变系数:\n";
  fout << distCoeffs << endl << endl << endl;
  for (int i = 0; i < nums; i++)
  {
    cv::Rodrigues(rotationMat[i], rotate_Mat); //将旋转向量通过罗德里格斯公式转换为旋转矩阵
    fout << "第" << i + 1 << "幅图像的旋转矩阵为:" << endl;
    fout << rotate_Mat << endl;
    fout << "第" << i + 1 << "幅图像的平移向量为:" << endl;
    fout << translationMat[i] << endl
      << endl;
  }
  fout << endl;
  fout.close();
  system("pause");
  return 0;
}

代码运行显示为:

相关文章
|
14天前
|
存储 新制造 开发工具
Baumer工业相机堡盟工业相机如何联合NEOAPI SDK和OpenCV实现相机图像转换为Mat图像格式(C++)
Baumer工业相机堡盟工业相机如何联合NEOAPI SDK和OpenCV实现相机图像转换为Mat图像格式(C++)
45 2
|
14天前
|
存储 资源调度 算法
Opencv(C++)系列学习---SIFT、SURF、ORB算子特征检测
Opencv(C++)系列学习---SIFT、SURF、ORB算子特征检测
|
14天前
|
存储 监控 开发工具
Baumer工业相机堡盟工业相机如何联合NEOAPI SDK和OpenCV实现相机图像转换为AVI视频格式(C++)
Baumer工业相机堡盟工业相机如何联合NEOAPI SDK和OpenCV实现相机图像转换为AVI视频格式(C++)
38 0
|
14天前
|
算法 计算机视觉 Python
【OpenCV】-算子(Sobel、Canny、Laplacian)学习
【OpenCV】-算子(Sobel、Canny、Laplacian)学习
|
14天前
|
算法 计算机视觉 C++
Opencv(C++)学习系列---Sobel索贝尔算子边缘检测
Opencv(C++)学习系列---Sobel索贝尔算子边缘检测
|
14天前
|
传感器 存储 开发工具
Baumer工业相机堡盟工业相机如何联合NEOAPI SDK和OpenCV实现Mono12和Mono16位深度的图像保存(C++)
Baumer工业相机堡盟工业相机如何联合NEOAPI SDK和OpenCV实现Mono12和Mono16位深度的图像保存(C++)
25 0
|
14天前
|
存储 新制造 开发工具
Baumer工业相机堡盟工业相机如何使用OpenCV实现相机图像的显示(C++)
Baumer工业相机堡盟工业相机如何使用OpenCV实现相机图像的显示(C++)
31 0
|
14天前
|
存储 新制造 C#
Baumer工业相机堡盟工业相机如何使用OpenCV实现相机图像的显示(C#)
Baumer工业相机堡盟工业相机如何使用OpenCV实现相机图像的显示(C#)
34 1
|
14天前
|
监控 算法 开发工具
Baumer工业相机堡盟工业相机如何联合NEOAPI SDK和OpenCV实现获取图像并对图像进行边缘检测(C++)
Baumer工业相机堡盟工业相机如何联合NEOAPI SDK和OpenCV实现获取图像并对图像进行边缘检测(C++)
33 1
|
14天前
|
算法 API 开发工具
Baumer工业相机堡盟工业相机如何使用BGAPISDK和OpenCV生成视频(C++)
Baumer工业相机堡盟工业相机如何使用BGAPISDK和OpenCV生成视频(C++)
26 0