上图为针孔相机成像原理,蓝色坐标中的O即为镜头光心。成像原理与小孔成像相同。
单目相机映射关系如下:
将上式进行变换,就可以从三位空间映射到2维平面的公式。
相机的畸变公式如下:
void cv:projectPoints ( InputArray objectPoints, InputArray rvec, InputArray tvec, InputArray cameraMatrix,InputArray distCoeffs,outputArray imagePolints, jacobian =, OutputArray naArray(), double aspectRatio = 0)
objectPoints:世界坐标系中3D点的三维坐标。
rvec:世界坐标系变换到相机坐标系的旋转向量。
tvec:世界坐标系变换到相机坐标系的平移向量。
cameraMatrix:相机的内参矩阵。
distCoeffs:相机的畸变系数矩阵。
imagcPoints:三维坐标点在像素坐标系中估计的坐标。
jacobian:可选输出的雅可比矩阵。
aspectRatio:是否固定“宽高比”参数标志。
本节应用案例如下:
int main() {
//输入计算的内参矩阵和畸变矩阵(在相机厂家说明或者标定获得)
Mat cameraMatrix = (Mat_<float>(3, 3) << 532.016297, 0, 332.172519,
0, 531.565159, 233.388075,
0, 0, 1);
Mat distCoeffs = (Mat_<float>(1, 5) << -0.285188, 0.080097, 0.001274,
0.002415, 0.106579);
//图像相机坐标系与世界坐标系之间的关系
Mat rvec = (Mat_<float>(1, 3) << -1.977853, -2.002220, 0.130029);
Mat tvec = (Mat_<float>(1, 3) << -26.88155, -42.79936, 159.19703);
//生成第一张图像中内角点的三位世界坐标
Size boardSize = Size(9,6);
//棋盘格每个方格的真实尺寸
Size squareSize = Size(10, 10);
vector<Point3f> PointSets;
for (int j = 0; j < boardSize.height; j++)
{
for (int k = 0; k < boardSize.width; k++)
{
Point3f realPoint;
//假设标定板为世界坐标系的Z平面,及z=0
realPoint.x = j * squareSize.width;
realPoint.y = k * squareSize.height;
realPoint.z = 0;
PointSets.push_back(realPoint);
}
}
//根据三维坐标和相机与世界坐标系时间的关系估计内角点像素坐标
vector<Point2f> imagePoints;
projectPoints(PointSets, rvec, tvec, cameraMatrix, distCoeffs, imagePoints);
for (int i = 0; i < imagePoints.size(); i++)
{
cout << imagePoints[i] << endl;
}
waitKey(0);
return 0;
}
标定原理:
从上图中可以看出,相机坐标系可以通过世界坐标系进行平移变换来得到。具体的变换矩阵如相机坐标系右侧的公式。
标定板的图例如下:
上图中为两种类型的标定板,左侧为常用标定板,在提取的时候是内角点。而右侧标定板中不存在内角点,所以我们提取的是每个圆的中心点。
内角点提取函数如下:
bool cv:.findChessboardCorners ( InputArray image, Size patternSize, OutputArray corners, int flags = CALIB_CB_ADAPTIVE_THRESH+CALIB_CB_NORNALIZE_IPAGE)
image:含有棋盘标定板的图像,图像必须是CV_8U的灰度图像或者彩色图像。
patternSize:图像中棋盘内角点行数和列数
corners:检测到的角点坐标
flags:检测角点方式的标志
圆形标定板中心提取:
bool cv::findCirclesGrid ( InputArray image, Size patternSize, OutputArray centers, int flags, const Ptr< FeatureDetector > & blolbDetector, const CirclesGridFinderParameters & parameters)
image:输入含有圆形网格的图像,图像必须是CV_8U的灰度图像或者彩色图像。
patternSize:图像中每行和每列圆形的数目。
corners:输出的圆形中心坐标。
flags:检测圆心的操作标志。
blobDetector:在浅色背景中寻找黑色圆形斑点的特征探测器。
角点位置优化函数:
bool cv.find4QuadCornerSubpix ( InputArray img, InputOutputArray corners, size region_size)
img:计算出内角点的图像。
patternSize:内角点坐标。
corners:优化坐标时考虑的邻域范围。
绘制内角点提取的结果:
void cv::drawChessboardCorners ( InputOutputArray image, size patternSize, InputArray corners, bool patternWasFound)
image:需要绘制角点的目标图像,必须是CU_8U的彩色图像。patternSize:标定板每行和每列角点的数目。
corners:检测到的角点坐标数组。
pattern WasFound:是否显示找到完成的标定板标志。
相机标定函数:
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)
objectPoints:棋盘格内角点的三维坐标。
imagePoints:棋盘格内角点在图像中的二维坐标。
imageSize:图像的像素尺寸大小。
cameraMatrix:相机的内参矩阵。
distCoeffs:相机的畸变系数矩阵。
rvecs:相机坐标系与世界坐标系之间的旋转向量。
tvecs:相机坐标系与世界坐标系之间的平移向量。
flags:选择标定算法的标志。
本节应用案例如下:
本案例中采用了两张标定板的提取图像:
并将两个图片的路径存放到ca.txt文件中
int main() {
//读取图像
vector<Mat>imgs;
string imgName;
//读取存放图片路径的txt文件
ifstream fin("ca.txt");
//挨个读取图片
while(getline(fin, imgName))
{
Mat img = imread(imgName);
imgs.push_back(img);
}
//方格标定板内角点的数目(行,列)
Size board_size = Size(9, 6);
//存放内角点坐标
vector<vector<Point2f>> imgsPoints;
//开始内角点提取
for (int i = 0; i < imgs.size(); i++)
{
Mat img1 = imgs[i];
Mat gray1;
cvtColor(img1, gray1, COLOR_BGR2GRAY);
vector<Point2f> img1_points;
//计算标定板的角点
findChessboardCorners(gray1, board_size, img1_points);
//细化方格标定板角点坐标
find4QuadCornerSubpix(gray1, img1_points, Size(5, 5));
bool pattern = true;
drawChessboardCorners(img1, board_size, img1_points, pattern);
namedWindow("img1", WINDOW_NORMAL);
imshow("img1", img1);
waitKey(0);
imgsPoints.push_back(img1_points);
}
//生成每个内角点的空间坐标
Size squareSize = Size(10, 10);
vector<vector<Point3f>> objectPoints;
for (int i = 0; i < imgsPoints.size(); i++)
{
vector<Point3f> tempPointSet;
for (int j = 0; j < board_size.height; j++)
{
for (int k = 0; k < board_size.width; k++)
{
Point3f realPoint;
//设标定板为世界坐标系的z平面,即z=0
realPoint.x = j * squareSize.width;
realPoint.y = k * squareSize.height;
realPoint.z = 0;
tempPointSet.push_back(realPoint);
}
}
objectPoints.push_back(tempPointSet);
}
//图像尺寸
Size imageSize;
imageSize.width = imgs[0].cols;
imageSize.height = imgs[0].rows;
//摄像机内参数矩阵
Mat cameraMatrix = Mat(3, 3, CV_32FC1, Scalar::all(0));
//摄像机的5个畸变系数:k1,k2,p1,p2,k3
Mat distCoeffs = Mat(1, 5, CV_32FC1, Scalar::all(0));
//每幅图像的旋转向量、平移量
vector<Mat> rvecs, tvecs;
calibrateCamera(objectPoints, imgsPoints, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs, 0);
cout << "相机的内参矩阵:" << cameraMatrix << endl;
cout << "相机畸变系数:" << distCoeffs << endl;
waitKey(0);
return 0;
}
最终的运行结果如下图:
这里只放一张图像。最终的输出结果为:
相机的内参矩阵:[120.8643306554273, 0, 94.55565247064737;
0, 119.979406894919, 55.48571212317609;
0, 0, 1]
相机畸变系数:[-0.5559208449775317, 3.15840209023594, -0.001816753642197531, -0.01817901488786, -7.629569308066959]
如上图(左)可以看出,相机采集的数据造成了一定的畸变,尤其是在边缘部分。
去畸变函数:
void cv::undistort ( InputArray src, OutputArray dst, InputArray cameraMatrix, InputArray distCoeffs, InputArray newCameraMatrix = noArray())
src:含有畸变的输入图像。
dst:去畸变后的输出图像,与输入图像具有相同的尺寸和数据类型。
cameraMatrix:相机内参矩阵(上节中相机标定获得)。
distCoeffis:相机的畸变矩阵,根据近似模型不同,参数数量可以为4、5、8、12或者14,如果是空矩阵表示没有畸变。
newCameraMatrix:畸变图像的相机内参矩阵,一般情况下与第三个参数相同或者使用默认值。
本节应用案例如下:
//用undiststort()函数直接计算校正图像
void undist(vector<Mat> imgs, //原有图像向量
Mat cameraMatrix, //计算得到的相机内存
Mat distCoeffs, //计算得到的相机畸变系数
vector<Mat> &undistImgs //校正后的输出图像
)
{
for (int i = 0; i < imgs.size(); i++)
{
Mat undisImg;
undistort(imgs[i], undistImgs, cameraMatrix, distCoeffs);
undistImgs.push_back(undisImg);
}
}
int main() {
//读取图像
vector<Mat>imgs;
string imgName;
//读取存放图片路径的txt文件
ifstream fin("ca.txt");
//挨个读取图片
while (getline(fin, imgName))
{
Mat img = imread(imgName);
imgs.push_back(img);
}
//输入计算的内参矩阵和畸变矩阵(在相机厂家说明或者标定获得)
Mat cameraMatrix = (Mat_<float>(3, 3) << 120.8643306554273, 0, 94.55565247064737,
0, 119.979406894919, 55.48571212317609,
0, 0, 1);
Mat distCoeffs = (Mat_<float>(1, 5) << -0.5559208449775317, 3.15840209023594, -0.001816753642197531, -0.01817901488786, -7.629569308066959);
//去畸变图像存放变量
vector<Mat> undistImags;
Size imageSize;
imageSize.width = imgs[0].cols;
imageSize.height = imgs[0].rows;
undist(imgs, cameraMatrix, distCoeffs, undistImags);
//显示校正后的图像
for (int i = 0; i < imgs.size(); i++)
{
string windowNumber = to_string(i);
imshow("矫正后的图像" + windowNumber, undistImags[i]);
waitKey(0);
destroyWindow("矫正后的图像" + windowNumber);
}
waitKey(0);
return 0;
}