目录
1.张氏标定法基本原理
1.1相机针孔模型
首先,我们知道利用针孔模型建立的相机模型。假设相机二维相面齐次坐标表示为,三维世界坐标系齐次坐标表示为。相机相面坐标系和世界坐标系的关系如下:
其中s为比例因子,表示外参矩阵,R和t代表旋转矩阵和平移矩阵,该矩阵表示了相机坐标系和世界坐标系的关系。A为相机的内参矩阵,为主点坐标,α和β为相面坐标系u轴和v轴的尺度因子,γ表示两个图像轴倾斜的参数。为方便处理,外参矩阵进一步表示为:
为了不失一般性,假设靶标位于世界坐标系Z=0上,则列去掉,中的Z去掉。
1.2单应矩阵H
我们设3×3的单应矩阵
则靶标上的点在相面(图像)中的对应关系为:
考虑比例因子后,单应矩阵可以重新表示为:
需要注意的是,由于我们准备的靶标数据是已知的(世界坐标系坐标),且对应相面坐标可通过图像处理算法得到,因此可以求得H,即均已知。
1.3求相机内参
单应矩阵H有8个*度,以及6个外参数(3个用于旋转,3个用于平移),因此我们只能获得两个对内参的约束。利用旋转矩阵的性质,我们可以得到这两个约束。
旋转矩阵的性质
1.旋转矩阵是正交的,任意两列的点积为0
2.旋转矩阵的每个分量为单位矢量,三项平方和为1
得到的约束为:
令
则利用上述约束可以解出B矩阵,与(B的闭式解)对应列方程组即可解出相机内参如下:
1.4求相机外参
由上一步我们得到了A矩阵后,求外参就较为容易。由
可知有如下对应关系:
结合旋转矩阵的性质2,可得
考虑到噪声,可能得不到精确的旋转矩阵,需要通过奇异值分解等方法来获得吻合的旋转矩阵。
1.5优化参数
上述步骤得到的m的解是通过最小化一个没有物理意义的代数距离得到的。我们可以通过最大似然估计来改进它。文献给出了一个相面的n幅图像,相面上有m个点。假设图像点的噪声是独立且均匀分布的。最大似然估计可通过最小化以下函数获得:
其中,是第i幅图像中的投影点,旋转矩阵R可以通过罗德里格斯公式转换成旋转向量(角轴)。这是一个非线性最小化问题,可以用Levenberg-Marquardt算法求解上式。值得注意的是,该过程考虑了镜头的径向畸变问题。
1.6总结
张正友的相机标定程序如下:
1.打印图案并将其附着到平面上。
2.通过移动平面或相机,在不同方向上拍摄模型平面的一些图像。
3.检测图像中的特征点。
4.使用闭式解估计五个内参数和所有外参数。
5. 通过最大似然估计,细化所有参数,包括镜头畸变参数。
在实际应用时,我们准备好靶标并为靶标拍摄好图像后,主要工作内容为3-5步
2.OpenCV实现
2.1特征点检测与靶标坐标初始化
对不同的标定靶标,特征点检测方法有所不同。OpenCV推荐使用国际象棋棋盘的图案作为标定靶标,并将方块的角点作为特征点,因此这里仅列出基于该靶标的检测方法。
输入一幅图像后,提供棋盘的尺寸(棋盘内角点的数量),调用findChessboardCorners函数,就会返回所有棋盘角点的位置。注意,该函数返回值为布尔类型。
CV_EXPORTS_W bool findChessboardCorners(
InputArray image, //包含棋盘图像的8位灰度或彩色图案
Size patternSize, //图案的尺寸(内角点行列数)
OutputArray corners,//将检测到的角点输出的数组
int flags = CALIB_CB_ADAPTIVE_THRESH + CALIB_CB_NORMALIZE_IMAGE );
调用该函数后,检测到的角点的像素坐标会储存到OutputArray corners对应参数中。为了提高角点精度至亚像素级,可以进一步调用find4QuadCornerSubpix函数。注意,该函数第一个参数输入图像应为8位灰度图像。第三个参数为角点搜索窗口的尺寸。
//! finds subpixel-accurate positions of the chessboard corners
CV_EXPORTS_W bool find4QuadCornerSubpix( InputArray img, InputOutputArray corners, Size region_size );
我们可以调用drawChessboardCorners在棋盘图像中画出角点,并用线条连接起来(连接次序为角点在向量中的储存次序)。
CV_EXPORTS_W void drawChessboardCorners( InputOutputArray image, Size patternSize,
InputArray corners, bool patternWasFound );
单幅图像的角点检测及显示使用示例如下:
#include "opencv2/opencv.hpp"
#include "fstream"
#include "iostream"
using namespace std;
using namespace cv;
void main(){
ifstream fin("calibdata.txt"); /* 标定所用图像文件的路径 */
std::vector<cv::Point2f> imageCorners;
cv::Size boardSize(9, 6);
cv::Mat image; // to contain chessboard image
std::string filename;
std::getline(fin, filename);
// Open the image
image = cv::imread(filename);
// Get the chessboard corners
bool found = cv::findChessboardCorners(image, // image of chessboard pattern
boardSize, // size of pattern
imageCorners); // list of detected corners
// Get subpixel accuracy on the corners
if (found) {
Mat view_gray;
cvtColor(image, view_gray, COLOR_RGB2GRAY);
cv::find4QuadCornerSubpix(view_gray, imageCorners, cv::Size(11, 11));
//Draw the corners
cv::drawChessboardCorners(view_gray, boardSize, imageCorners,1);
cv::namedWindow("Detected points");
cv::imshow("Detected points", view_gray);
cv::waitKey(0);
}
}
为了使检测到的角点更多,提高标定精度,需要从不同角度对同一图案拍摄更多的照片。 为了接受不同图片的角点坐标,我们使用vector容器嵌套容器的方式保存角点的坐标,容器类型由OpenCV实例化的模板实现。
// (each square is one unit)
std::vector<std::vector<cv::Point3f> > objectPoints;
// the image point positions in pixels
std::vector<std::vector<cv::Point2f> > imagePoints;
接下来的工作就是将每一幅图片采集到的角点逐个放入该容器中,编写一个循环程序即可。
在获得了图像坐标后,还需要对靶标坐标系进行初始化,为简单起见,我们以方块长度为单位初始化靶标坐标系坐标。(我们将世界坐标系建立在了靶标上)
// 3D Scene Points:
// Initialize the chessboard corners
// in the chessboard reference frame
// The corners are at 3D location (X,Y,Z)= (i,j,0)
for (int i = 0; i < boardSize.height; i++) {
for (int j = 0; j < boardSize.width; j++) {
objectCorners.push_back(cv::Point3f(i, j, 0.0f));
}
}
到此为止,我们得到了相面坐标与对应的靶标坐标系坐标,并传入了两个容器,接下来利用OpenCV提供的calibrateCamera函数就可以计算得到内外参数。
2.2相机标定
在调用calibrateCamera之前,我们需要了解其定义,以准备相应的参数。
CV_EXPORTS_W double 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) );
由此可见,我们需要准备四个参数来接收标定结果。
cv::Mat cameraMatrix = Mat(3, 3, CV_32FC1, Scalar::all(0)); /* 摄像机内参数矩阵 */
cv::Mat distCoeffs = Mat(1, 5, CV_32FC1, Scalar::all(0)); /* 摄像机的5个畸变系数:k1,k2,p1,p2,k3 */
std::vector<Mat> tvecsMat; /* 每幅图像的旋转向量 */
std::vector<Mat> rvecsMat; /* 每幅图像的平移向量 */
注意,flag参数可以根据需求设置以下功能:(这里参考皆成旧梦博主的翻译)
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个畸变参数。
将准备好的参数输入函数即可。
2.3结果评估
得到相机的内外参数后,对靶标上的角点重新进行投影计算,即反投影,然后计算投影坐标和实际坐标的偏差,可以评估标定结果的精度。OpenCV提供的反投影函数为projectPoints()
CV_EXPORTS_W
void projectPoints(
InputArray objectPoints,//靶标坐标系上角点的坐标
InputArray rvec, //旋转矩阵
InputArray tvec, //平移矩阵
InputArray cameraMatrix,//相机矩阵
InputArray distCoeffs,//相机畸变系数
OutputArray imagePoints,//反投影点坐标
OutputArray jacobian = noArray(),
double aspectRatio = 0 );
每幅图片都调用该函数得到的反投影点坐标与实际坐标进行范数运算,将得到的误差进行累积,就得到了重投影误差。使用示例如下,这里的代码摘自-牧野-博主的Opencv 张正友相机标定傻瓜教程。
//对标定结果进行评价
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;
2.4去除图像畸变
我们注意到,在标定完成后,还得到了畸变系数,我们可以利用畸变系数及相机内外参数据去除畸变,这样标定后的相机拍摄的所有图像都不会有畸变。我们可以调用initUndistortRectifyMap()用来计算畸变映射,调用remap()把求得的映射应用到图像上。
CV_EXPORTS_W
void initUndistortRectifyMap(
InputArray cameraMatrix, //相机矩阵
InputArray distCoeffs, //畸变系数
InputArray R, //可选项
InputArray newCameraMatrix,//用于生成去除畸变的矩阵
Size size, //去除畸变后的大小
int m1type, //输出映射类型
OutputArray map1, OutputArray map2);//x和y映射函数
CV_EXPORTS_W
void remap(
InputArray src, //输入的原图像
OutputArray dst,//去除畸变后的图像
InputArray map1, InputArray map2,//x和y映射函数
int interpolation,//插值方法(参见#InterpolationFlags)
int borderMode = BORDER_CONSTANT,//像素外推方法(参见#BorderTypes)
const Scalar& borderValue = Scalar());//常量边界值,默认为0
使用示例如下:
// remove distortion in an image (after calibration)
cv::Mat remap(const cv::Mat& image, cv::Size& outputSize) {
cv::Mat undistorted;
if (outputSize.height == -1) {
outputSize = image.size();}
if (mustInitUndistort) { // called once per calibration
// dist = distCoeffs;
cv::initUndistortRectifyMap(
cameraMatrix, // computed camera matrix
distCoeffs, // computed distortion matrix
cv::Mat(), // optional rectification (none)
cv::Mat(), // camera matrix to generate undistorted
outputSize, // size of undistorted
CV_32FC1, // type of output map
map1, map2); // the x and y mapping functions
mustInitUndistort = false;
}
// Apply mapping functions
cv::remap(image, undistorted, map1, map2,
cv::INTER_LINEAR); // interpolation type
return undistorted;
}
// Exampple of Image Undistortion
std::cout << filelist[5] << std::endl;
image = cv::imread(filelist[5], 0);
cv::Size newSize(static_cast<int>(image.cols * 1.5), static_cast<int>(image.rows * 1.5));
cv::Mat uImage = remap(image, newSize);
2.5总结
总体来看,基于OpenCV的张正友标定法主要有以下步骤:
1.准备标定用图片
2.提取特征点坐标
3.得到世界坐标系坐标
4.相机标定
5.结果评估
6.去除畸变
3.参考资料
[1]Z. Zhang, "A flexible new technique for camera calibration," in IEEE Transactions on Pattern Analysis and Machine Intelligence, vol. 22, no. 11, pp. 1330-1334, Nov. 2000, doi: 10.1109/34.888718.
[2]Opencv 张正友相机标定傻瓜教程_牧野的博客-CSDN博客_opencv相机标定
https://blog.csdn.net/dcrmg/article/details/52929669
[3]OpenCV学习笔记(七) 相机标定的函数理解与学习_蹦蹦蹦的博客-CSDN博客
https://blog.csdn.net/xuxunjie147/article/details/79219774
[4]OpenCV 3 Computer Vision Application Programming Cookbook - Third Edition,Robert Laganiere,February 2017,Packt Publishing,ISBN: 9781786469717