首先安装vs2019并配置OpenCV3.x.xx
安装方法可自行网上去查怎么配置,不是本文重点,所以略过
opencv下载网址:https://opencv.org/releases/
准备需要的文件
在OpenCV文件D:\opencv\sources\samples\cpp中找到 stereo_calib.cpp 和 stereo_match.cpp
在D:\opencv\sources\samples\data中找到stereo_calib.xml和26张图片了(left01-14 right01-14)。
其中,stereo_calib.cpp是使用张正友的方法进行双目标定,stereo_calib.xml 是输入图片的列表,stereo_match.cpp是立体匹配、计算视差的部分。
运行stereo_calib.cpp
运行的目的是可以得到 intrinsics.yml和 extrinsics.yml 文件用于后面的stereo_match.cpp。这里注意所有文件和照片要在同一文件夹下。
在vs2019中
步骤:新建项目,导入现有项,选择stereo_calib.cpp。配置OpenCV属性管理器,一切准备就绪
其他教程会说在main()函数下加入代码:
argc = 6;
argv[0] = "OpenCVrectify";//项目名称
argv[1] = "-w";
argv[2] = "9";
argv[3] = "-h";
argv[4] = "6";
argv[5] = "stereo_calib.xml";
其实现在21年了。其实早在19年就不用在main函数里加那段代码了,因为源码里已经替我们设置好了
是通过下面函数设置的:w|9|中的9可以修改,其他同理
cv::CommandLineParser parser(argc, argv, "{w|9|}{h|6|}{s|1.0|}{nr||}{help||}{@input|stereo_calib.xml|}");
{w|9|}{h|6|}为你所使用的棋盘格的内角点数
{s|1.0|}为棋盘格的正方形边长(单位m)
{@input_data|stereo_calib.xml|}你要准备的xml文件的名字(里面指定了你准备的棋盘格的文件名称)
导入现有项,配置好就可以直接运行了
运行程序之后会生成intrinsics.yml和 extrinsics.yml 文件分别是相机的内参和外参
内外参数据说明
内参 intrinsic.yml
M1: cameraMatrix1 First camera matrix. (相机1矩阵)
D1: distCoeffs1 First camera distortion parameters.(distortion coefficient,相机1畸变系数)
M2: cameraMatrix2 Second camera matrix.(相机2矩阵)
D2: distCoeffs2 Second camera distortion parameters.(相机2畸变系数)
来源函数
/// @overload
CV_EXPORTS_W double stereoCalibrate( InputArrayOfArrays objectPoints,
InputArrayOfArrays imagePoints1, InputArrayOfArrays imagePoints2,
InputOutputArray cameraMatrix1, InputOutputArray distCoeffs1,
InputOutputArray cameraMatrix2, InputOutputArray distCoeffs2,
Size imageSize, OutputArray R,OutputArray T, OutputArray E, OutputArray F,
int flags = CALIB_FIX_INTRINSIC,
TermCriteria criteria = TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, 1e-6) );
外参 extrinsic.yml
R: @param R Rotation matrix between the coordinate systems of the first and the second cameras.(两相机坐标系的旋转矩阵)
T: @param T Translation vector between coordinate systems of the cameras.(两相机坐标系的平移向量)
R1: @param R1 Output 3x3 rectification transform (rotation matrix) for the first camera.(相机1的3x3整流变换(旋转矩阵))
R2: @param R2 Output 3x3 rectification transform (rotation matrix) for the second camera.(相机2的3x3整流变换(旋转矩阵))
P1: @param P1 Output 3x4 projection matrix in the new (rectified) coordinate systems for the first camera.(相机1对于新坐标系的(调整后的)投影矩阵)
P2: @param P2 Output 3x4 projection matrix in the new (rectified) coordinate systems for the second camera.(相机2对于新坐标系的(调整后的)投影矩阵)
Q: @param Q Output 4x4 disparity-to-depth mapping matrix (see reprojectImageTo3D ).(4X4视差 到 深度映射矩阵)
来源函数
CV_EXPORTS_W void stereoRectify( InputArray cameraMatrix1, InputArray distCoeffs1,
InputArray cameraMatrix2, InputArray distCoeffs2,
Size imageSize, InputArray R, InputArray T,
OutputArray R1, OutputArray R2,
OutputArray P1, OutputArray P2,
OutputArray Q, int flags = CALIB_ZERO_DISPARITY,
double alpha = -1, Size newImageSize = Size(),
CV_OUT Rect* validPixROI1 = 0, CV_OUT Rect* validPixROI2 = 0 );
代码解读
/* This is sample from the OpenCV book. The copyright notice is below */
/* *************** License:**************************
Oct. 3, 2008
Right to use this code in any way you want without warranty, support or any guarantee of it working.
BOOK: It would be nice if you cited it:
Learning OpenCV: Computer Vision with the OpenCV Library
by Gary Bradski and Adrian Kaehler
Published by O'Reilly Media, October 3, 2008
AVAILABLE AT:
http://www.amazon.com/Learning-OpenCV-Computer-Vision-Library/dp/0596516134
Or: http://oreilly.com/catalog/9780596516130/
ISBN-10: 0596516134 or: ISBN-13: 978-0596516130
OPENCV WEBSITES:
Homepage: http://opencv.org
Online docs: http://docs.opencv.org
Q&A forum: http://answers.opencv.org
Issue tracker: http://code.opencv.org
GitHub: https://github.com/opencv/opencv/
************************************************** */
#include "opencv2/calib3d.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
#include <vector>
#include <string>
#include <algorithm>
#include <iostream>
#include <iterator>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
using namespace cv; //包含相应的Opencv命名空间
using namespace std;
static int print_help()
{
cout <<
" Given a list of chessboard images, the number of corners (nx, ny)\n"
" on the chessboards, and a flag: useCalibrated for \n"
" calibrated (0) or\n"
" uncalibrated \n"
" (1: use cvStereoCalibrate(), 2: compute fundamental\n"
" matrix separately) stereo. \n"
" Calibrate the cameras and display the\n"
" rectified results along with the computed disparity images. \n" << endl;
cout << "Usage:\n ./stereo_calib -w=<board_width default=9> -h=<board_height default=6> -s=<square_size default=1.0> <image list XML/YML file default=../data/stereo_calib.xml>\n" << endl;
return 0;
}
/************相机标定程序***************/
static void
StereoCalib(const vector<string>& imagelist, Size boardSize, float squareSize, bool displayCorners = false, bool useCalibrated=true, bool showRectified=true)
{
if( imagelist.size() % 2 != 0 ) //判断图片是否成对
{
cout << "Error: the image list contains odd (non-even) number of elements\n";
return;
}
const int maxScale = 2; //设定寻找角点的图像尺寸,若scale未找到,则将图像放大寻找角点
// ARRAY AND VECTOR STORAGE:
vector<vector<Point2f> > imagePoints[2]; //分别为左右图像的角点
vector<vector<Point3f> > objectPoints; //物体
Size imageSize; //??
int i, j, k, nimages = (int)imagelist.size()/2; //nimages为左或右图像的个数
imagePoints[0].resize(nimages);
imagePoints[1].resize(nimages);
vector<string> goodImageList;
for( i = j = 0; i < nimages; i++ ) //依次寻找13对图片
{
for( k = 0; k < 2; k++ ) //依次寻找左右图片
{
const string& filename = imagelist[i*2+k];
Mat img = imread(filename, 0); //载入灰度图 0代表灰度图
if(img.empty())
break;
if( imageSize == Size() ) //判断图像尺寸是否达到预先设置的要求
imageSize = img.size();
else if( img.size() != imageSize )
{
cout << "The image " << filename << " has the size different from the first image size. Skipping the pair\n";
break;
}
bool found = false;
//设置图像矩阵的引用(指针),此时指向左右视图的矩阵首地址
vector<Point2f>& corners = imagePoints[k][j]; //赋值角点个数及位置
for( int scale = 1; scale <= maxScale; scale++ )
{
Mat timg;
if( scale == 1 )
timg = img;
else
resize(img, timg, Size(), scale, scale, INTER_LINEAR_EXACT);
found = findChessboardCorners(timg, boardSize, corners, //找角点函数,得到内角点坐标
CALIB_CB_ADAPTIVE_THRESH | CALIB_CB_NORMALIZE_IMAGE);
// drawChessboardCorners(timg, boardSize, corners, found); //画出角点,自己调试时使用,但会降低精度,正式运行时应该注释掉
if( found )
{
if( scale > 1 )
{
Mat cornersMat(corners);
cornersMat *= 1./scale;
}
break;
}
}
if( displayCorners )
{
cout << filename << endl;
Mat cimg, cimg1;
cvtColor(img, cimg, COLOR_GRAY2BGR);
drawChessboardCorners(cimg, boardSize, corners, found);
double sf = 640./MAX(img.rows, img.cols);
resize(cimg, cimg1, Size(), sf, sf, INTER_LINEAR_EXACT);
imshow("corners", cimg1);
char c = (char)waitKey(500);
if( c == 27 || c == 'q' || c == 'Q' ) //Allow ESC to quit
exit(-1);
}
else
putchar('.');
if( !found )
break;
cornerSubPix(img, corners, Size(11,11), Size(-1,-1),
TermCriteria(TermCriteria::COUNT+TermCriteria::EPS,
30, 0.01));
}
if( k == 2 )
{
goodImageList.push_back(imagelist[i*2]);
goodImageList.push_back(imagelist[i*2+1]);
j++;
}
}
cout << j << " pairs have been successfully detected.\n"; //命令行打印检测到的图片
nimages = j; //nimages为左右图像个数
if( nimages < 2 )
{
cout << "Error: too little pairs to run the calibration\n";
return;
}
imagePoints[0].resize(nimages); //左相机 角点位置
imagePoints[1].resize(nimages); //右相机 角点位置
objectPoints.resize(nimages);
for( i = 0; i < nimages; i++ )
{
for( j = 0; j < boardSize.height; j++ )
for( k = 0; k < boardSize.width; k++ )
objectPoints[i].push_back(Point3f(k*squareSize, j*squareSize, 0));
}
cout << "Running stereo calibration ...\n";
Mat cameraMatrix[2], distCoeffs[2]; //相机参数 畸变矩阵
cameraMatrix[0] = initCameraMatrix2D(objectPoints,imagePoints[0],imageSize,0);
cameraMatrix[1] = initCameraMatrix2D(objectPoints,imagePoints[1],imageSize,0);
Mat R, T, E, F; //R旋转矩阵 T平移矩阵 E本征矩阵 F输出基本矩阵
//最关键的地方,求解校正后的相机参数
double rms = stereoCalibrate(objectPoints, imagePoints[0], imagePoints[1],
cameraMatrix[0], distCoeffs[0],
cameraMatrix[1], distCoeffs[1],
imageSize, R, T, E, F,
CALIB_FIX_ASPECT_RATIO +
CALIB_ZERO_TANGENT_DIST +
CALIB_USE_INTRINSIC_GUESS +
CALIB_SAME_FOCAL_LENGTH +
CALIB_RATIONAL_MODEL +
CALIB_FIX_K3 + CALIB_FIX_K4 + CALIB_FIX_K5,
TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 100, 1e-5) );
cout << "done with RMS error=" << rms << endl;
// CALIBRATION QUALITY CHECK
// because the output fundamental matrix implicitly
// includes all the output information,
// we can check the quality of calibration using the
// epipolar geometry constraint: m2^t*F*m1=0
double err = 0; //计算投影标定误差
int npoints = 0;
vector<Vec3f> lines[2]; //极线
for( i = 0; i < nimages; i++ ) //水平校正
{
int npt = (int)imagePoints[0][i].size();
Mat imgpt[2];
for( k = 0; k < 2; k++ )
{
imgpt[k] = Mat(imagePoints[k][i]);
undistortPoints(imgpt[k], imgpt[k], cameraMatrix[k], distCoeffs[k], Mat(), cameraMatrix[k]);
computeCorrespondEpilines(imgpt[k], k+1, F, lines[k]);
}
for( j = 0; j < npt; j++ )
{
double errij = fabs(imagePoints[0][i][j].x*lines[1][j][0] +
imagePoints[0][i][j].y*lines[1][j][1] + lines[1][j][2]) +
fabs(imagePoints[1][i][j].x*lines[0][j][0] +
imagePoints[1][i][j].y*lines[0][j][1] + lines[0][j][2]);
err += errij;
}
npoints += npt;
}
cout << "average epipolar err = " << err/npoints << endl;
// save intrinsic parameters
FileStorage fs("intrinsics.yml", FileStorage::WRITE); //存储内参
if( fs.isOpened() )
{
fs << "M1" << cameraMatrix[0] << "D1" << distCoeffs[0] <<
"M2" << cameraMatrix[1] << "D2" << distCoeffs[1];
fs.release();
}
else
cout << "Error: can not save the intrinsic parameters\n";
//外参数
// R--右相机相对左相机的旋转矩阵
// T--右相机相对左相机的平移矩阵
// R1,R2--左右相机校准变换(旋转)矩阵 3×3
// P1,P2--左右相机在校准后坐标系中的投影矩阵 3×4
// Q--视差-深度映射矩阵,我利用它来计算单个目标点的三维坐标
Mat R1, R2, P1, P2, Q;
Rect validRoi[2]; //图像校正之后,会对图像进行裁剪,这里的validROI就是指裁剪之后的区域
//进行立体校正
stereoRectify(cameraMatrix[0], distCoeffs[0],
cameraMatrix[1], distCoeffs[1],
imageSize, R, T, R1, R2, P1, P2, Q,
CALIB_ZERO_DISPARITY, 1, imageSize, &validRoi[0], &validRoi[1]);
/*
* R T 左相机到右相机的旋转平移矩阵 R 3*3 T 3*1 T中第一个Tx为 基线长度
立体校正的时候需要两幅图像共面并且行对准 以使得立体匹配更加的可靠
使得两幅图像共面的方法就是把两个摄像头的图像投影到一个公共成像面上,这样每幅图像从本图像平面投影到公共图像平面都需要一个旋转矩阵R
stereoRectify 这个函数计算的就是从图像平面投影都公共成像平面的旋转矩阵Rl,Rr。 Rl,Rr即为左右相机平面行对准的校正旋转矩阵。
左相机经过Rl旋转,右相机经过Rr旋转之后,两幅图像就已经共面并且行对准了。
其中Pl,Pr为两个相机的投影矩阵,其作用是将3D点的坐标转换到图像的2D点的坐标:P*[X Y Z 1]' =[x y w]
Q矩阵为重投影矩阵,即矩阵Q可以把2维平面(图像平面)上的点投影到3维空间的点:Q*[x y d 1] = [X Y Z W]。其中d为左右两幅图像的视差
*/
fs.open("extrinsics.yml", FileStorage::WRITE);
if( fs.isOpened() )
{
fs << "R" << R << "T" << T << "R1" << R1 << "R2" << R2 << "P1" << P1 << "P2" << P2 << "Q" << Q;
fs.release();
}
else
cout << "Error: can not save the extrinsic parameters\n";
//opencv可以辨认相机的物理位置
// OpenCV can handle left-right
// or up-down camera arrangements
bool isVerticalStereo = fabs(P2.at<double>(1, 3)) > fabs(P2.at<double>(0, 3));
// COMPUTE AND DISPLAY RECTIFICATION 计算并显示校正信息
if( !showRectified )
return;
Mat rmap[2][2];
// IF BY CALIBRATED (BOUGUET'S METHOD)
if( useCalibrated )
{
// we already computed everything
}
// OR ELSE HARTLEY'S METHOD
else
// use intrinsic parameters of each camera, but
// compute the rectification transformation directly
// from the fundamental matrix
{
vector<Point2f> allimgpt[2];
for( k = 0; k < 2; k++ )
{
for( i = 0; i < nimages; i++ )
std::copy(imagePoints[k][i].begin(), imagePoints[k][i].end(), back_inserter(allimgpt[k]));
}
F = findFundamentalMat(Mat(allimgpt[0]), Mat(allimgpt[1]), FM_8POINT, 0, 0);
Mat H1, H2;
stereoRectifyUncalibrated(Mat(allimgpt[0]), Mat(allimgpt[1]), F, imageSize, H1, H2, 3);
R1 = cameraMatrix[0].inv()*H1*cameraMatrix[0];
R2 = cameraMatrix[1].inv()*H2*cameraMatrix[1];
P1 = cameraMatrix[0];
P2 = cameraMatrix[1];
}
/*
根据stereoRectify 计算出来的R 和 P 来计算图像的映射表 mapx,mapy
mapx,mapy这两个映射表接下来可以给remap()函数调用,来校正图像,使得两幅图像共面并且行对准
ininUndistortRectifyMap()的参数newCameraMatrix就是校正后的摄像机矩阵。在openCV里面,校正后的计算机矩阵Mrect是跟投影矩阵P一起返回的。
所以我们在这里传入投影矩阵P,此函数可以从投影矩阵P中读出校正后的摄像机矩阵
*/
//Precompute maps for cv::remap()
initUndistortRectifyMap(cameraMatrix[0], distCoeffs[0], R1, P1, imageSize, CV_16SC2, rmap[0][0], rmap[0][1]);
initUndistortRectifyMap(cameraMatrix[1], distCoeffs[1], R2, P2, imageSize, CV_16SC2, rmap[1][0], rmap[1][1]);
/*
显示校正结果
把左右两幅图像显示到同一个画面上
这里只显示了最后一副图像的校正结果。并没有把所有的图像都显示出来
*/
Mat canvas;
double sf;
int w, h;
if( !isVerticalStereo )
{
sf = 600./MAX(imageSize.width, imageSize.height);
w = cvRound(imageSize.width*sf);
h = cvRound(imageSize.height*sf);
canvas.create(h, w*2, CV_8UC3);
}
else
{
sf = 300./MAX(imageSize.width, imageSize.height);
w = cvRound(imageSize.width*sf);
h = cvRound(imageSize.height*sf);
canvas.create(h*2, w, CV_8UC3);
}
for( i = 0; i < nimages; i++ )
{
for( k = 0; k < 2; k++ )
{
Mat img = imread(goodImageList[i*2+k], 0), rimg, cimg;
remap(img, rimg, rmap[k][0], rmap[k][1], INTER_LINEAR); //对准左右图像,
cvtColor(rimg, cimg, COLOR_GRAY2BGR);
Mat canvasPart = !isVerticalStereo ? canvas(Rect(w*k, 0, w, h)) : canvas(Rect(0, h*k, w, h));
resize(cimg, canvasPart, canvasPart.size(), 0, 0, INTER_AREA);
if( useCalibrated )
{
Rect vroi(cvRound(validRoi[k].x*sf), cvRound(validRoi[k].y*sf),
cvRound(validRoi[k].width*sf), cvRound(validRoi[k].height*sf));
rectangle(canvasPart, vroi, Scalar(0,0,255), 3, 8);
}
}
if( !isVerticalStereo ) //画上对应的线条
for( j = 0; j < canvas.rows; j += 16 )
line(canvas, Point(0, j), Point(canvas.cols, j), Scalar(0, 255, 0), 1, 8); //画平行线
else
for( j = 0; j < canvas.cols; j += 16 )
line(canvas, Point(j, 0), Point(j, canvas.rows), Scalar(0, 255, 0), 1, 8);
imshow("rectified", canvas); //输出立体校正后的图片
char c = (char)waitKey();
if( c == 27 || c == 'q' || c == 'Q' )
break;
}
}
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.getFirstTopLevelNode();
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;
}
int main(int argc, char** argv)
{
Size boardSize;
string imagelistfn;
bool showRectified;
cv::CommandLineParser parser(argc, argv, "{w|9|}{h|6|}{s|1.0|}{nr||}{help||}{@input|stereo_calib.xml|}");
if (parser.has("help"))
return print_help();
showRectified = !parser.has("nr");
imagelistfn = parser.get<string>("@input");
boardSize.width = parser.get<int>("w"); //获取图像的宽
boardSize.height = parser.get<int>("h"); //获取图像的高
float squareSize = parser.get<float>("s"); //正方形棋盘格子 边长
if (!parser.check()) //标定板 格子尺寸参数错误
{
parser.printErrors();
return 1;
}
vector<string> imagelist; //定义个一个字符串容器 存放图像列表
bool ok = readStringList(imagelistfn, imagelist);
if(!ok || imagelist.empty())
{
cout << "can not open " << imagelistfn << " or the string list is empty" << endl;
return print_help();
}
//标定的主程序
StereoCalib(imagelist, boardSize, squareSize, false, true, showRectified);
return 0;
}