手把手教你OpenCV利用张氏标定法进行相机标定(二)

2024-01-20 08:20

本文主要是介绍手把手教你OpenCV利用张氏标定法进行相机标定(二),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

由于刚到德国,办手续、买东西花了快一周的时间,今天刚有课余闲暇时间,就赶紧接着上一讲继续写。
在上一讲 手把手教你OpenCV利用张氏标定法进行相机标定(一)中,我们完成了标定板的制作,得到了不同相机位姿下的标定板图片。这一讲中,我们将利用强大的图像处理工具——OpenCV,教大家如何进行相机的标定。

1.函数介绍

张氏相机标定法简便易用且精度较高,因此相关的数学基础早已被转化为程序封装在了OpenCV的函数库里。其算法将会在下一章详细介绍,这里我们只需知道其流程就可以了。

1.1程序流程

(1)提取角点
(2)提取亚像素角点
(3)画出角点
(4)参数标定
(5)评价标定结果
以上步骤可以简记为“一提二亚三画四标五评”。接下来,我们就每一步所用到的函数进行介绍。

1.2角点提取函数

OpenCV中我们用到的提取角点的函数是findChessboardCorners(),这真正实现了一行代码走天下的方便快捷,之后还有好几个类似函数,我们先来看看提取角点函数的内部结构吧。

C++:
bool cv::findChessboardCorners ( InputArray image,
Size patternSize,
OutputArray corners,
int flags = CALIB_CB_ADAPTIVE_THRESH+CALIB_CB_NORMALIZE_IMAGE
)
Python:
retval, corners = cv.findChessboardCorners( image, patternSize[, corners[, flags]] )

注:以上函数摘自OpenCV-3.4.7文档(下同)。该函数属于OpenCV的calib3d库。
文档很贴心的提供了C++和Python的函数写法,方便大家调用。我就以C++为例,讲解一下各参数的意义。
首先,从整体来看,这个函数实现了对图像是否为棋盘格的判断并定位出棋盘的内角点。函数类型为bool型,因此最终会返回0(未检测到或没检测全)或者非0值。
再具体到每一个参数:
image:输入的棋盘格原图。必须是8位灰度图或彩色图像。
patternSize:指棋盘格图片中的每一行每一列的内角点数。例如,前边我们拍照采用的8x8的图在这里就是(7,7)。
corners:输出检测到的角点阵列。通常定义为vector类型。
flags:默认为0。其他取值为文档所给几个数值的结合。由于这几个数值不太常用,这里就不赘述了,有兴趣的同学可以参考官方文档。OpenCV: Camera Calibration and 3D Reconstruction

1.3亚像素角点提取函数

通过上一个步骤我们检测出了角点的位置,但是这个坐标只是一个大概。为了精确地确定其位置,我们还需要对角点进行亚像素化。
那何谓亚像素?
数码摄像机的成像面的分辨率以像素数量来衡量, 像素中心之间的距离有几个至十几个微米不等。为了最大限度利用图像信息来提高分辨率,人们提出了亚像素概念。意思是说,在两个物理像素之间还有像素,称之为亚像素。它是通过插值计算得出来的。
OpenCV提供了两个可以提取亚像素角点信息的函数:cornerSubPix()和find4QuadCornerSubpix(). 两者并无较大差异,这里都列出来供大家参考一下吧。

C++:
void cv::cornerSubPix ( InputArray image,
InputOutputArray corners,
Size winSize,
Size zeroZone,
TermCriteria criteria
)
Python:
corners = cv.cornerSubPix( image, corners, winSize, zeroZone, criteria )

该函数属于OpenCV的imgproc库。
参数说明:
image:输入一个单通道、8位或者浮点型图片。
corners:用来储存角点的初始坐标和精确化后的角点坐标。
winSize:搜索窗口边长的一半。 例如,如果winSize = Size(5,5),则使用(5 * 2 + 1)×(5 * 2 + 1)= 11×11搜索窗口。选择一定的窗口后,它会不断移动,并计算窗口中的亚像素角点。
zeroZone:死区的一般尺寸,死区为不对搜索区的中央位置做求和运算的区域。值(-1,-1)表示没有这个区域大小。
criteria:终止角点优化迭代过程的条件。一般省略。
关于该函数详细的注释见这里:cornerSubPix 这里还有亚像素角点的搜索机制。

C++:
bool cv::find4QuadCornerSubpix ( InputArray img,
InputOutputArray corners,
Size region_size
)
Python:
retval, corners = cv.find4QuadCornerSubpix( img, corners, region_size )

该函数与角点提取函数findChessboardCorners()一样同属calib3d库。这可能也是一般相机标定用这个函数用的比cornerSubPix()多的原因。
参数说明(基本同上一个函数一样):
img:同样也是输入图片矩阵,最好是8位灰度图。
corners:初始的角点坐标,同时也会作为亚像素角点坐标的输出。
region_size:角点搜索窗口的大小。
最终的结果储存在参数“corners”里面。
该函数的链接:find4QuadCornerSubpix

1.4角点绘制函数

drawChessboardCorners()这个函数顾名思义,就是让程序在照片上画出找到的角点,可以说是编程过程中的检验,可视化的角点也会让动手实践变得更有激励性。
如果代码出了bug,大家也可以通过这一步来判断是之前的步骤出了问题还是之后的出了问题。
下面我们来看看函数组成吧:

C++:
void cv::drawChessboardCorners ( InputOutputArray image,
Size patternSize,
InputArray corners,
bool patternWasFound
)
Python:
image = cv.drawChessboardCorners( image, patternSize, corners, patternWasFound )

该函数同样属于calib3d库。其实呢,想必大家也发现了,calib就是calibration(标定)的缩写,因此OpenCV的这个库就是用来完成相机标定、3D重建等工作的。
参数说明:
image:目标图片,必须是一个8位的彩色图(三通道)。
patternSize:每一幅棋盘格图片中,每行和每列角点的个数。patternSize = cv::Size(points_per_row,points_per_column)。
corners:检测到的角点阵列,也就是上面角点检测函数findChessboardCorners()里的corners。
patternWasFound:标志位,用来判断是否检测到所有的棋盘内角点。也就是findChessboardCorners这个函数的返回值。
链接在这儿:drawChessboardCorners

1.5相机标定函数

前面铺垫了那么多,其实为的就是给这个函数提供参数。由OpenCV calib3d库提供的calibrateCamera()函数由九个参数组成,仅用一句话完成对相机的标定(求得内参外参)。
OpenCV文档中提供了两种calibrateCamera()函数,我们这里用的是第二种,两者区别是第一种多输出了内外偏差和视差参数。

C++:
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)
)
Python:
retval, cameraMatrix, distCoeffs, rvecs, tvecs = cv.calibrateCamera( objectPoints, imagePoints, imageSize, cameraMatrix, distCoeffs[, rvecs[, tvecs[, flags[, criteria]]]] )

参数说明如下:
objectPoints:一系列角点的三维坐标。它是校准图案坐标空间中校准图案点的向量的向量,这话说的非常拗口,简单来说这就是个三维坐标点,通过两层的向量来表示。申明方式如下:std::vector(std::vector(cv::Vec3f))objectPoints.
imagePoints:角点投影到标定图案平面上的二维坐标点。
imageSize:图片的尺寸大小,用以初始化相机内参。
cameraMatrix:相机的内参矩阵,即 A = [ f x 0 c x 0 f y c y 0 0 1 ] A=\begin{bmatrix} f_x&0&c_x\\0&f_y&c_y\\0&0&1\end{bmatrix} A=fx000fy0cxcy1
此处内参矩阵的参数是否需要初始化与标志位选取值有关。
distCoeffs:相机的畸变参数矩阵,有5个畸变参数:k1,k2,p1,p2,k3。
rvecs:旋转向量
tvecs:平移向量
flags:表示标定时采用的算法,默认为0。其他选项详见官方文档calibrateCamera
criteria:迭代的终止条件,通常忽略。
这个函数解决的就是一个优化问题——重投影误差,一般使用的算法叫Bundle Adjustment。本着前两讲尽量少些公式的原则,详细的数学推导就留到下一讲好了,这里你只需知道OpenCV默默地帮你完成了一个计算量很大的优化迭代。

1.6重投影函数

完成了相机标定只算完成了标定工作的90%,还有剩下的10%就是对结果的评定。我们对标定结果评价时,就是计算投影点与检测到的亚像素角点坐标的差值。由于是二维的,所以分别对x和y坐标求差值,再求平方根,即求L2范数。
对空间中的三维坐标点进行反向投影由projectPoints() 完成。

C++:
void cv::fisheye::projectPoints ( InputArray objectPoints,
OutputArray imagePoints,
InputArray rvec,
InputArray tvec,
InputArray K,
InputArray D,
double alpha = 0,
OutputArray jacobian = noArray()
)
Python:
imagePoints, jacobian = cv.fisheye.projectPoints( objectPoints, rvec, tvec, K, D[, imagePoints[, alpha[, jacobian]]] )

这里需要注意的是,projectPoints()也有两种,我们这里用的是第二种。第一种是仿射变换,第二种才是我们这的旋转+平移变换。projectPoints
参数说明:
objectPoints:对象点的数组,大小为1xN / Nx1 ,3通道(vector (Point3f)),其中N为图中的点数。
imagePoints:若干张图片对应的若干的内角点的坐标。
rvecs:旋转向量
tvecs:平移向量
K:即上文的矩阵A,相机的内参矩阵。
D:即上文的畸变参数矩阵。
alpha:偏斜系数,这里取0。
jacobian:可选择是否计算雅可比行列式,一般不用。

到此为止,标定程序所要用到的几大函数就介绍完了。

2.编程前注意事项

需要在此说明的是,为了方便程序的调用及结果的显示,事先需要创建两个文本文档“calibimage.txt”和“calibration_result.txt”。第一个文档里存着所有图片的标号,因为程序是按照这个标号顺序来依次读取图片的。像这样:
在这里插入图片描述
切记:文档开头一定要顶格写,不能有空格或者空行,否则读取指令会出现问题!
接下来就可以开始愉(ku)快(bi)的敲代码了。

3.完整代码展示

#include <iostream>
#include <fstream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/calib3d/calib3d.hpp>using namespace std;
using namespace cv;int main(int argc, char **argv) {ifstream fin("calibimage.txt");            //读取标定图片的路径,与cpp程序在同一路径下if (!fin)                                  //检测是否读取到文件{cerr<<"没有找到文件"<<endl;}ofstream fout("calibration_result.txt");   //输出结果保存在此文本文件下//依次读取每一幅图片,从中提取角点cout<<"开始提取角点……"<<endl;int image_count = 0;                       //图片数量Size image_size;                           //图片尺寸Size board_size = Size(7,7);               //标定板每行每列角点个数,共7*7个角点vector<Point2f> image_points_buf;          //缓存每幅图检测到的角点vector<vector<Point2f>> image_points_seq;  //用一个二维数组保存检测到的所有角点string filename;                           //申明一个文件名的字符串while (getline(fin,filename))              //逐行读取,将行读入字符串   {image_count++;cout<<"image_count = "<<image_count<<endl;//读入图片Mat imageInput=imread(filename);if(image_count == 1){image_size.height = imageInput.rows;//图像的高对应着行数image_size.width = imageInput.cols; //图像的宽对应着列数cout<<"image_size.width = "<<image_size.width<<endl;cout<<"image_size.height = "<<image_size.height<<endl;}//角点检测if (findChessboardCorners(imageInput, board_size, image_points_buf) == 0){cout<<"can not find the corners "<<endl;exit(1);}else{Mat view_gray;                      //存储灰度图的矩阵cvtColor(imageInput, view_gray, CV_RGB2GRAY);//将RGB图转化为灰度图//亚像素精确化(两种方法)find4QuadCornerSubpix(view_gray, image_points_buf, Size(5,5));//cornerSubPix(view_gray,image_points_buf,Size(5,5));image_points_seq.push_back(image_points_buf);//保存亚像素角点//在图中画出角点位置drawChessboardCorners(view_gray, board_size, image_points_buf, true);//将角点连线imshow("Camera calibration", view_gray);waitKey(100);                         //等待按键输入}}//输出图像数目int total = image_points_seq.size();cout<<"total = "<<total<<endl;int CornerNum = board_size.width*board_size.height;//一幅图片中的角点数//以第一幅图片为例,下同cout<<"第一副图片的角点数据:"<<endl;for (int i=0; i<CornerNum; i++){cout<<"x= "<<image_points_seq[0][i].x<<" ";cout<<"y= "<<image_points_seq[0][i].y<<" ";cout<<endl;}cout<<"角点提取完成!\n";//开始相机标定cout<<"开始标定……"<<endl;Size square_size = Size(10,10);              //每个小方格实际大小vector<vector<Point3f>> object_points;         //保存角点的三维坐标Mat cameraMatrix = Mat(3,3,CV_32FC1,Scalar::all(0));//内参矩阵3*3Mat distCoeffs = Mat(1,5,CV_32FC1,Scalar::all(0));//畸变矩阵1*5vector<Mat> rotationMat;                       //旋转矩阵vector<Mat> translationMat;                    //平移矩阵//初始化角点三维坐标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;realpoint.x = i*square_size.width;realpoint.y = j*square_size.height;realpoint.z = 0;tempPointSet.push_back(realpoint);}}object_points.push_back(tempPointSet);}vector<int> point_counts;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,rotationMat, translationMat,0);   //拥有八个参数的标定函数,不过一句话搞定cout<<"标定完成!"<<endl;//对标定结果进行评价double total_err = 0.0;                      //所有图像平均误差总和double err = 0.0;                            //每幅图像的平均误差vector<Point2f> image_pointsre;              //重投影点cout<<"\t每幅图像的标定误差:\n";fout<<"每幅图像的标定误差:\n";for (i=0; i<image_count; i++){vector<Point3f> tempPointSet = object_points[i];//通过之前标定得到的相机内外参,对三维点进行重投影projectPoints(tempPointSet, image_pointsre, rotationMat[i], translationMat[i], cameraMatrix, distCoeffs);//计算两者之间的误差vector<Point2f> tempImagePoint = image_points_seq[i];Mat tempImagePointMat = Mat(1, tempImagePoint.size(), CV_32FC2);//变为1*20的矩阵Mat image_pointsreMat = Mat(1, image_pointsre.size(), CV_32FC2);for (int j = 0 ; j < tempImagePoint.size(); j++){tempImagePointMat.at<Vec2f>(0,j) = Vec2f(tempImagePoint[j].x, tempImagePoint[j].y);image_pointsreMat.at<Vec2f>(0,j) = Vec2f(image_pointsre[j].x, image_pointsre[j].y);}err = norm(image_pointsreMat, tempImagePointMat, NORM_L2);total_err += err/=  point_counts[i];cout<<"第"<<i+1<<"幅图像的平均误差为: "<<err<<"像素"<<endl;fout<<"第"<<i+1<<"幅图像的平均误差为: "<<err<<"像素"<<endl;}cout<<"总体平均误差为: "<<total_err/image_count<<"像素"<<endl;fout<<"总体平均误差为: "<<total_err/image_count<<"像素"<<endl;cout<<"评价完成!"<<endl;//将标定结果写入txt文件cout<<"开始保存结果……"<<endl;Mat rotate_Mat = 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++){Rodrigues(rotationMat[i], rotate_Mat); //将旋转向量通过罗德里格斯公式转换为旋转矩阵fout<<"第"<<i+1<<"幅图像的旋转矩阵为:"<<endl;fout<<rotate_Mat<<endl;fout<<"第"<<i+1<<"幅图像的平移向量为:"<<endl;fout<<translationMat[i]<<endl<<endl;}cout<<"保存完成"<<endl;fout<<endl;return 0;
}

4.运行结果

在Ubuntu下用cmake完成C++的编译后,执行程序:
圈出角点位置并连线
程序执行后开始计数
在这里插入图片描述
在这里插入图片描述

最终求得的相机内参矩阵和畸变系数分别为:
在这里插入图片描述
还记得上一讲中一开始我们进行理论计算得到的 f x = 1124.50 f_x=1124.50 fx=1124.50 f y = 1260 f_y=1260 fy=1260 c x = 720 c_x=720 cx=720 c y = 540 c_y=540 cy=540。比照上面的结果有一些误差,不过基本还是接近的。可能是计算所采取的数据不太精确所致。

最后,附上整个工程的全部代码文件:张氏相机标定法源码+标定板制作代码(附标定图片)

参考链接:
https://blog.csdn.net/hongbin_xu/article/details/78988450

这篇关于手把手教你OpenCV利用张氏标定法进行相机标定(二)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/625348

相关文章

Nginx中配置使用非默认80端口进行服务的完整指南

《Nginx中配置使用非默认80端口进行服务的完整指南》在实际生产环境中,我们经常需要将Nginx配置在其他端口上运行,本文将详细介绍如何在Nginx中配置使用非默认端口进行服务,希望对大家有所帮助... 目录一、为什么需要使用非默认端口二、配置Nginx使用非默认端口的基本方法2.1 修改listen指令

MySQL按时间维度对亿级数据表进行平滑分表

《MySQL按时间维度对亿级数据表进行平滑分表》本文将以一个真实的4亿数据表分表案例为基础,详细介绍如何在不影响线上业务的情况下,完成按时间维度分表的完整过程,感兴趣的小伙伴可以了解一下... 目录引言一、为什么我们需要分表1.1 单表数据量过大的问题1.2 分表方案选型二、分表前的准备工作2.1 数据评估

MySQL进行分片合并的实现步骤

《MySQL进行分片合并的实现步骤》分片合并是指在分布式数据库系统中,将不同分片上的查询结果进行整合,以获得完整的查询结果,下面就来具体介绍一下,感兴趣的可以了解一下... 目录环境准备项目依赖数据源配置分片上下文分片查询和合并代码实现1. 查询单条记录2. 跨分片查询和合并测试结论分片合并(Shardin

SpringBoot结合Knife4j进行API分组授权管理配置详解

《SpringBoot结合Knife4j进行API分组授权管理配置详解》在现代的微服务架构中,API文档和授权管理是不可或缺的一部分,本文将介绍如何在SpringBoot应用中集成Knife4j,并进... 目录环境准备配置 Swagger配置 Swagger OpenAPI自定义 Swagger UI 底

基于Python Playwright进行前端性能测试的脚本实现

《基于PythonPlaywright进行前端性能测试的脚本实现》在当今Web应用开发中,性能优化是提升用户体验的关键因素之一,本文将介绍如何使用Playwright构建一个自动化性能测试工具,希望... 目录引言工具概述整体架构核心实现解析1. 浏览器初始化2. 性能数据收集3. 资源分析4. 关键性能指

Nginx进行平滑升级的实战指南(不中断服务版本更新)

《Nginx进行平滑升级的实战指南(不中断服务版本更新)》Nginx的平滑升级(也称为热升级)是一种在不停止服务的情况下更新Nginx版本或添加模块的方法,这种升级方式确保了服务的高可用性,避免了因升... 目录一.下载并编译新版Nginx1.下载解压2.编译二.替换可执行文件,并平滑升级1.替换可执行文件

Python进行JSON和Excel文件转换处理指南

《Python进行JSON和Excel文件转换处理指南》在数据交换与系统集成中,JSON与Excel是两种极为常见的数据格式,本文将介绍如何使用Python实现将JSON转换为格式化的Excel文件,... 目录将 jsON 导入为格式化 Excel将 Excel 导出为结构化 JSON处理嵌套 JSON:

OpenCV在Java中的完整集成指南分享

《OpenCV在Java中的完整集成指南分享》本文详解了在Java中集成OpenCV的方法,涵盖jar包导入、dll配置、JNI路径设置及跨平台兼容性处理,提供了图像处理、特征检测、实时视频分析等应用... 目录1. OpenCV简介与应用领域1.1 OpenCV的诞生与发展1.2 OpenCV的应用领域2

在Java中使用OpenCV实践

《在Java中使用OpenCV实践》用户分享了在Java项目中集成OpenCV4.10.0的实践经验,涵盖库简介、Windows安装、依赖配置及灰度图测试,强调其在图像处理领域的多功能性,并计划后续探... 目录前言一 、OpenCV1.简介2.下载与安装3.目录说明二、在Java项目中使用三 、测试1.测

一文解密Python进行监控进程的黑科技

《一文解密Python进行监控进程的黑科技》在计算机系统管理和应用性能优化中,监控进程的CPU、内存和IO使用率是非常重要的任务,下面我们就来讲讲如何Python写一个简单使用的监控进程的工具吧... 目录准备工作监控CPU使用率监控内存使用率监控IO使用率小工具代码整合在计算机系统管理和应用性能优化中,监