OpenCv相机标定——圆形标定板标定
0.前言
OpenCv中,相机标定所使用的标定图案分为棋盘格、对称圆形及非对称圆形特征图、ArUco板和ChArUco板等。在OpenCV的官方例程中,采用的是棋盘格图案,因为其操作简单、快速,标定精度满足一般应用场景的需求。对于标定精度要求高的场景,则一般采用圆形标定图案。本文主要介绍如何使用圆形标定图案(对称和非对称)完成相机的标定,并将OpenCv标定结果与Halcon标定结果进行对比分析。
1.标定图案
OpenCv中使用的圆形标定图案如图1所示:
OpenCv中,使用圆形标定图案用到的函数为 cv::findCirclesGrid()。函数原型如下:
bool cv::findCirclesGrid(//找到圆心坐标返回True
cv::InputArray,//输入标定图像,8位单通道或三通道
cv::Size patternSize,//标定图案的尺寸
cv::OutputArray centers,//输出数组,为检测到的圆心坐标
int flags,//标志位,对称图案——cv::CALIB_CB_SYMMETRIC_GRID,非对称图案—— cv::CALIB_CB_ASYMMETRIC_GRID
const cv::Ptrcv::FeatureDetector&blobDetector=new SimpleBlobDetector()
);
图1所示的非对称圆形标定图案,其width=11,height=6。在计算标定图案上标志点圆心的世界坐标时,参数squareSize即为图1中标注的圆心距。关于圆的半径大小,可以自行设定,因为在提取圆心坐标时不涉及圆的半径(这点和halcon标定不同,halcon在进行相机标定时,圆的半径作为标定文件中的已知参数)。圆心距一般取圆直径的4倍左右。
图2为本文使用的标定板,其为高精度铝制标定板,精度为±0.01mm,是200x200mm的halcon标准标定板,圆的直径为12.5mm,圆心距为25mm。
2.OpenCv标定
本文采用的标定为离线标定,先由相机采集N幅图像,再由标定程序读取图像。为了保证标定精度,建议采集10幅或更多的视图,尽量使得标定板的移动范围覆盖相机视野。
在OpenCv官方相机标定代码的基础上进行了修改,得到了下面的对圆形标定图案标定的代码。由于代码近500行,为了缩短篇幅,省略的一些头文件、说明性文字、函数的实现。省略部分可参考:OpenCv/sources/samples/cpp/tutorial_code/calib3d/camera_calibration/camera_calibration.cpp.
#include "stdafx.h"
//此处省略各种头文件
using namespace cv;
using namespace std;
//此处省略help()函数
enum { DETECTION = 0, CAPTURING = 1, CALIBRATED = 2 };
enum Pattern { CHESSBOARD, CIRCLES_GRID, ASYMMETRIC_CIRCLES_GRID };
//计算重投影误差函数
static double computeReprojectionErrors(
const vector<vector<Point3f> >& objectPoints,
const vector<vector<Point2f> >& imagePoints,
const vector<Mat>& rvecs, const vector<Mat>& tvecs,
const Mat& cameraMatrix, const Mat& distCoeffs,
vector<float>& perViewErrors)
{
//此处省略...
}
static void calcChessboardCorners(Size boardSize, float squareSize, vector<Point3f>& corners, Pattern patternType = CIRCLES_GRID)
{
//省略...
//本文中用到的标定板,在该函数中的参数为:boardSize.width=7,boardSize.height=7,squareSize=0.025(此处单位为米)
}
//执行标定,包括计算重投影误差
static bool runCalibration(vector<vector<Point2f> > imagePoints,
Size imageSize, Size boardSize, Pattern patternType,
float squareSize, float aspectRatio,
int flags, Mat& cameraMatrix, Mat& distCoeffs,
vector<Mat>& rvecs, vector<Mat>& tvecs,
vector<float>& reprojErrs,
double& totalAvgErr)
{
//省略...
}
//保存相机参数
static void saveCameraParams(const string& filename,
Size imageSize, Size boardSize,
float squareSize, float aspectRatio, int flags,
const Mat& cameraMatrix, const Mat& distCoeffs,
const vector<Mat>& rvecs, const vector<Mat>& tvecs,
const vector<float>& reprojErrs,
const vector<vector<Point2f> >& imagePoints,
double totalAvgErr)
{
//省略...
}
//读取字符串
static bool readStringList(const string& filename, vector<string>& l)
{
l.resize(0);
FileStorage fs(filename, FileStorage::READ);
if (!fs.isOpened())
return false;
FileNode n = fs["images"];
if (n.type() != FileNode::SEQ)
return false;
FileNodeIterator it = n.begin(), it_end = n.end();
for (; it != it_end; ++it)
l.push_back((string)*it);
return true;
}
//运行并保存
static bool runAndSave(const string& outputFilename,
const vector<vector<Point2f> >& imagePoints,
Size imageSize, Size boardSize, Pattern patternType, float squareSize,
float aspectRatio, int flags, Mat& cameraMatrix,
Mat& distCoeffs, bool writeExtrinsics, bool writePoints)
{
//省略...
}
int main(int argc, char** argv)
{
cout << argc << endl;
for (size_t i = 0; i < argc; i++)
{
cout << argv[i] << endl;
}
Size boardSize, imageSize;
float squareSize, aspectRatio;
Mat cameraMatrix, distCoeffs;
string outputFilename;
string inputFilename = "";
int i, nframes;
bool writeExtrinsics, writePoints;
bool undistortImage = false;
int flags = 0;
VideoCapture capture;
bool flipVertical;
bool showUndistorted;
bool videofile;
int delay;
clock_t prevTimestamp = 0;
int mode = DETECTION;
int cameraId = 0;
vector<vector<Point2f> > imagePoints;
vector<string> imageList;
Pattern pattern = CIRCLES_GRID;//标定图案类型,对称圆形图案
cv::CommandLineParser parser(argc, argv,
"{help ||}{w|7|}{h|7|}{pt|circles|}{n|30|}{d|1000|}{s|0.025|}{o|D:/opencv/cameracalibration/out_camera_params_25x25_circleboard.yml|}"
"{op|D:/opencv/cameracalibration/Detected_feature_points.yml|}{oe|D:/opencv/cameracalibration/Extrinsic_parameters_circleboard.yml|}{zt||}{a|1|}{p||}{v||}{V||}{su||}"
"{input_data|D:/opencv/cameracalibration/VID25x25_CircleGrid.xml|}");
//命令行参数赋值,参数说明:w,h为标定板宽,高; pt为标定图案类型; n为读取图片的张数; d为相机在线抓图的时间间隔(ms)(本代码
//为离线标定,该参数可以不设置); o为程序输出的相机内参、外参文件(自定义的文件); op为输出检测到特征点的文件(自定义的文件);
//oe为输出的相机外参数(这里可以不用设置,因为外参数已经在o中输出了,标定完后该文件为空文件); a为比例系数,默认为1;
//input_data为存放图片路径的xml文件,本代码读取的VID25X25_CircleGrid.xml文件内容见图3。
if (parser.has("help"))
{
help();
return 0;
}
boardSize.width = parser.get<int>("w");
boardSize.height = parser.get<int>("h");
if (parser.has("pt"))
{
string val = parser.get<string>("pt");
if (val == "circles")
pattern = CIRCLES_GRID;
else if (val == "acircles")
pattern = ASYMMETRIC_CIRCLES_GRID;
else if (val == "chessboard")
pattern = CHESSBOARD;
else
return fprintf(stderr, "Invalid pattern type: must be chessboard or circles\n"), -1;
}
squareSize = parser.get<float>("s");
nframes = parser.get<int>("n");
aspectRatio = parser.get<float>("a");
delay = parser.get<int>("d");
writePoints = parser.has("op");
writeExtrinsics = parser.has("oe");
if (parser.has("a"))
flags |= CALIB_FIX_ASPECT_RATIO;
if (parser.has("zt"))
flags |= CALIB_ZERO_TANGENT_DIST;
if (parser.has("p"))
flags |= CALIB_FIX_PRINCIPAL_POINT;
flipVertical = parser.has("v");
videofile = parser.has("V");
if (parser.has("o"))
outputFilename = parser.get<string>("o");
showUndistorted = parser.has("su");
if (isdigit(parser.get<string>("input_data")[0]))
cameraId = parser.get<int>("input_data");
else
inputFilename = parser.get<string>("input_data");
if (!parser.check())
{
help();
parser.printErrors();
return -1;
}
if (squareSize <= 0)
return fprintf(stderr, "Invalid board square width\n"), -1;
if (nframes <= 3)
return printf("Invalid number of images\n"), -1;
if (aspectRatio <= 0)
return printf("Invalid aspect ratio\n"), -1;
if (delay <= 0)
return printf("Invalid delay\n"), -1;
if (boardSize.width <= 0)
return fprintf(stderr, "Invalid board width\n"), -1;
if (boardSize.height <= 0)
return fprintf(stderr, "Invalid board height\n"), -1;
if (!inputFilename.empty())
{
if (!videofile && readStringList(inputFilename, imageList))
mode = CAPTURING;
else
capture.open(inputFilename);
}
else
capture.open(cameraId);
if (!capture.isOpened() && imageList.empty())
return fprintf(stderr, "Could not initialize video (%d) capture\n", cameraId), -2;
if (!imageList.empty())
nframes = (int)imageList.size();
if (capture.isOpened())
printf("%s", liveCaptureHelp);
namedWindow("Image View", 1);
for (i = 0;; i++)
{
Mat view, viewGray;
bool blink = false;
if (capture.isOpened())
{
Mat view0;
capture >> view0;
view0.copyTo(view);
}
else if (i < (int)imageList.size())
view = imread(imageList[i], 1);
if (view.empty())
{
if (imagePoints.size() > 0)
runAndSave(outputFilename, imagePoints, imageSize,
boardSize, pattern, squareSize, aspectRatio,
flags, cameraMatrix, distCoeffs,
writeExtrinsics, writePoints);
break;
}
imageSize = view.size();
if (flipVertical)
flip(view, view, 0);
vector<Point2f> pointbuf;
cvtColor(view, viewGray, COLOR_BGR2GRAY);
bool found;
switch (pattern)
{
case CHESSBOARD:
found = findChessboardCorners(view, boardSize, pointbuf,
CALIB_CB_ADAPTIVE_THRESH | CALIB_CB_FAST_CHECK | CALIB_CB_NORMALIZE_IMAGE);
break;
case CIRCLES_GRID:
found = findCirclesGrid(view, boardSize, pointbuf,CALIB_CB_SYMMETRIC_GRID);
break;
case ASYMMETRIC_CIRCLES_GRID:
found = findCirclesGrid(view, boardSize, pointbuf, CALIB_CB_ASYMMETRIC_GRID);
break;
default:
return fprintf(stderr, "Unknown pattern type\n"), -1;
}
if (found)
drawChessboardCorners(view, boardSize, Mat(pointbuf), found);//在原图中绘制找到的圆心点,图4为其中的一幅图
string msg = mode == CAPTURING ? "100/100" :
mode == CALIBRATED ? "Calibrated" : "Press 'g' to start";
int baseLine = 0;
Size textSize = getTextSize(msg, 1, 1, 1, &baseLine);
Point textOrigin(view.cols - 2 * textSize.width - 10, view.rows - 2 * baseLine - 10);
if (mode == CAPTURING)
{
if (undistortImage)
msg = format("%d/%d Undist", (int)imagePoints.size(), nframes);
else
msg = format("%d/%d", (int)imagePoints.size(), nframes);
}
putText(view, msg, textOrigin, 1, 1,
mode != CALIBRATED ? Scalar(0, 0, 255) : Scalar(0, 255, 0));
if (blink)
bitwise_not(view, view);
if (mode == CALIBRATED && undistortImage)
{
Mat temp = view.clone();
undistort(temp, view, cameraMatrix, distCoeffs);
}
imshow("Image View", view);
char key = (char)waitKey(capture.isOpened() ? 50 : 500);
if (key == 27)
break;
if (key == 'u' && mode == CALIBRATED)
undistortImage = !undistortImage;
if (capture.isOpened() && key == 'g')
{
mode = CAPTURING;
imagePoints.clear();
}
if (mode == CAPTURING && imagePoints.size() >= (unsigned)nframes)
{
if (runAndSave(outputFilename, imagePoints, imageSize,
boardSize, pattern, squareSize, aspectRatio,
flags, cameraMatrix, distCoeffs,
writeExtrinsics, writePoints))
mode = CALIBRATED;
else
mode = DETECTION;
if (!capture.isOpened())
break;
}
}
if (!capture.isOpened() && showUndistorted)
{
Mat view, rview, map1, map2;
initUndistortRectifyMap(cameraMatrix, distCoeffs, Mat(),
getOptimalNewCameraMatrix(cameraMatrix, distCoeffs, imageSize, 1, imageSize, 0),
imageSize, CV_16SC2, map1, map2);
for (i = 0; i < (int)imageList.size(); i++)
{
view = imread(imageList[i], 1);
if (view.empty())
continue;
//undistort( view, rview, cameraMatrix, distCoeffs, cameraMatrix );
remap(view, rview, map1, map2, INTER_LINEAR);
imshow("Image View", rview);
char c = (char)waitKey();
if (c == 27 || c == 'q' || c == 'Q')
break;
}
}
return 0;
}
3.标定结果分析
OpenCv标定得到的相机参数矩阵为:
本次标定使用的镜头焦距 f=8mm, 像元尺寸为3.45μm,图像尺寸为2040x1200。
Halcon标定得到的内参为(k,sx,sy,cx,cy)将其转换为式(1)中的矩阵。表1为OpenCv和Halcon标定的对比数据。
本实验中,镜头与世界坐标系z=0平面的距离为112cm左右。从表中可以看出,OpenCv标定的重投影误差为0.01759,精度较高,小于Halcon标定的0.069。(OpenCv标定过程中采用了5项畸变系数k1,k2,p1,p2,k3;Halcon标定中只考虑径向畸变k,表中没有列出)
需要指出的是,实验数据来源于对同一组图片的标定。Halcon中对相机的标定,采用的方法是Tsai两步标定法,需要预先给出相机的内参数,理论上具有较高的标定精度。但是在本次的Halcon标定中,由于采用的是离线采集的图片,在标定过程中提示图片过曝、旋转角度没有覆盖全、标定图案偏小、光照不均匀等图像品质问题,因此标定的精度不高。如果使用halcon在线抓图标定,可以有效避免图像品质问题,从而大幅度提高标定精度,预计标定精度和OpenCv标定相当或者更高。标定结果表明,OpenCv标定算法的鲁棒性更好,而Halcon标定算法对采集到的图像品质要求较高,也可以理解为高精度标定下对图像品质的高要求。
ps:如有错误,谢谢指出。转载请注明出处。
来源:CSDN
作者:Ketal_N
链接:https://blog.csdn.net/Ketal_N/article/details/88829741