赞
踩
机器视觉是采用相机成像来实现对三维场景的测量、定位、重建等过程。也是一个利用二维图像进行三维反推的过程,我们所处的世界是三维的,而图像或者照片是二维的。我们可以把相机认为是一个函数,输入量是一个三维场景,输出量是一幅二维图像。 正常来说,三维到二维这个过程是不可逆的。
如果我们能够找到一个合适的数学模型,来近似以上这个三维到二维的过程,然后找到这个数学模型的反函数,就可以实现二维到三维的反过程。
即: 用简单的数学模型来表达复杂的成像过程,并且求出成像的反过程。
为什么要使用相机标定?
如上我们知道三维到二维是通过成像的原理,那么这个过程中就会因为相机的出厂参数、或者畸变参数导致成像的图像跟原始图像差距很大,标定就是为了确定这些参数,然后将要处理实际工作时,通过这些参数将图像校正OK。
标定会涉及到的参数:
内参矩阵 | 外参矩阵 | 畸变参数 |
---|---|---|
f/dx,f/dy,u0,v0 | 相机位姿、平移、旋转 | k1,k2,p1,p2,k3 |
畸变参数中,k1,k2,k3代表了径向畸变参数, p1,p2代表了切向畸变参数。
外参矩阵与相机的摆放等等很多因素有关,所以,在非特定应用不理会。
得出结论:相机标定就是确定相机的内外参数、畸变参数的过程。
相机标定常用方法:
标定方法 | 优点 | 缺点 | 常见方法 |
---|---|---|---|
相机自标定法 | 灵活性强、可在线标定 | 精度低、鲁棒性差 | 分层逐步标定、基于Kruppa方程 |
主动视觉相机标定法 | 不需要标定物体、算法简单、鲁棒性高 | 成本高、设备昂贵 | 主动系统控制相机做特定运动 |
标定物标定法 | 可使用与任意的相机模型、精度高 | 需要标定物、算法复杂 | Tsai两步法、张正友标定法(本文所有方法) |
鲁棒性 :指控制系统在一定(结构,大小)的参数摄动下,维持某些性能的特性。
以上三种也有另外一种官方说法: 线性标定法、非线性优化标定法、两步法。
视觉的理论说实话我自己也是很迷糊的,看不懂就去查阅,进入代码环节吧。
使用OpenCV进行张正友标定,总结有以下几步:
标定图片尽量使用棋盘格哈,使用OpenCV可以直接用包括的图片,路径在安装目录下面,如下:
如果没有,可以自己绘制一个,绘制程序如下:
// 生成棋盘格(demo) void CreateGridironPattern() { // 单位转换 int dot_per_inch = 108; /* * 这里以我惠普 光影精灵9的参数计算如下: * 公式: DPI = 1920 / sqrt(15.6 ^ 2 + (1920 / 1080 * 15.6)^2) * sqrt(15.6 ^ 2 + (1920 / 1080 * 15.6)^2) ≈ 17.76 */ double cm_to_inch = 0.3937; // 1cm = 0.3937inch double inch_to_cm = 2.54; // 1inch = 2.54cm( 1 英寸 = 2.54 厘米 是一个国际公认的单位) double inch_per_dot = 1.0 / 96.0; // 自定义标定板 double blockSize_cm = 1.5; // 方格尺寸: 边长1.5cm的正方形 // 设置横列方框数目 int blockcol = 10; int blockrow = 8; int blockSize = (int)(blockSize_cm / inch_to_cm * dot_per_inch); cout << "标定板尺寸: " << blockSize << endl; int imageSizeCol = blockSize * blockrow; int imageSizeRow = blockSize * blockcol; Mat chessBoard(imageSizeCol, imageSizeRow, CV_8UC3, Scalar::all(0)); unsigned char color = 0; for (int i = 0; i < imageSizeRow; i = i + blockSize) { color = ~color; // 将颜色值取反,如果开始为0,取反后为255(即黑白互换) for (int j = 0; j < imageSizeCol; j = j + blockSize) { Mat ROI = chessBoard(Rect(i, j, blockSize, blockSize)); ROI.setTo(Scalar::all(color)); color = ~color; } } imshow("chess board", chessBoard); imwrite("chessBard.jpg", chessBoard); waitKey(0); return; }
然后拍照,准备好10~20张照片为宜(使用校正相机拍照)。
函数原型:
CV_EXPORTS_W bool findChessboardCorners( InputArray image, Size patternSize, OutputArray corners,
int flags = CALIB_CB_ADAPTIVE_THRESH + CALIB_CB_NORMALIZE_IMAGE );
参数解释:
image
: 传入拍摄的棋盘格Mat图像,必须是8位的灰度或者彩色图像patternSize
: 每个棋盘格上内角点的行列数,一般情况下,行列数不要相同,便于后续待定程序识别标定板的 方向。corners
: 用于存储检测到的内角点图像坐标为止,一般用元素是Point2f的向量来表示,如:vector<Point2f> image_points_buf
。函数原型:
CV_EXPORTS_W bool find4QuadCornerSubpix( InputArray img, InputOutputArray corners, Size region_size );
参数解释:
img
:输入的Mat矩阵,最好是8位灰度图像,检测效率更高。corners
:初始的角点坐标向量,同时作为亚像素坐标位置的输出,所以需要时浮点型数据,一般用元素是Point2f/Point2d的向量来表示,如: vector<Point2f> imagePointBuf
。region_size
:角点搜索窗口的尺寸。函数原型:
CV_EXPORTS_W void cornerSubPix( InputArray image, InputOutputArray corners,
Size winSize, Size zeroZone,
TermCriteria criteria );
参数解释:
image
:输入的Mat矩阵,最好是8位灰度图像,检测效率更高。corners
:初始的角点坐标向量,同时作为亚像素坐标位置的输出,所以需要是浮点型数据,一般用元素是Point2f/Point2d的向量来表示,如:vector<Point2f> imagePointBuf
。winSize
:大小位搜索窗口的一半。zeroZone
:死区的一半尺寸,死区为不对搜索区的中央位置做求和运算的区域。它是用来避免自相关矩阵出现某些可能的奇异性。当值为(-1,-1)时表示没有死区。criteria
:定义求角点的迭代过程的终止条件,可以为迭代次数和角点精度两者的组合。网上看大佬们说两个函数测出来的结果,偏差基本都控制在0.5个像素之内。
函数原型:
CV_EXPORTS_W void drawChessboardCorners( InputOutputArray image, Size patternSize,
InputArray corners, bool patternWasFound );
参数解释:
image
:8位灰度或者彩色图像。patternSize
:每张标定棋盘上内角点的行列数。corners
:初始的角点坐标向量,同时作为亚像素坐标位置的输出,所以需要是浮点型数据,一般用元素是Pointf2f/Point2d的向量来表示:vector<Point2f> iamgePointsBuf
。patternWasFound
:标志位,用来指示定义的棋盘内角点是否被完整的探测到,true表示被完整的探测到,函数会用直线依次连接所有的内角点,作为一个整体,false表示有未被探测到的内角点,这时候函数会以(红色)圆圈标记处检测到的内角点。函数原型:
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) );
参数解释:
vector<vector<Point3f>> object_points
. 需要依据棋盘格上单个黑色矩阵的大小,计算(初始化)没一个内角点的世界坐标。vector<vector<Point2f>> image_points_seq
形式的变量。Mat cameraMatrix
即可,如: Mat cameraMatrix=Mat(3,3,CV_32FC1,Scalar::all(0))
。Mat distCoeffs = Mat(1,5CV_32FC1,Scalar::all(0))
。vector<Mat> rvecs
。vector<Mat> tvecs
。参数 | 解释 |
---|---|
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个畸变参数 |
在使用该函数进行标定运算之前,需要对棋盘上每一个内角点的空间坐标系的位置坐标进行初始化,标定的结果是生成相机的内参矩阵cameraMatrix、相机的5个畸变系数distCoeffs,另外每张图像都会生成属于自己的平移向量和旋转向量。
函数原型:
CV_EXPORTS_W void projectPoints( InputArray objectPoints,
InputArray rvec, InputArray tvec,
InputArray cameraMatrix, InputArray distCoeffs,
OutputArray imagePoints,
OutputArray jacobian = noArray(),
double aspectRatio = 0 );
函数原型:
CV_EXPORTS_W
void initUndistortRectifyMap(InputArray cameraMatrix, InputArray distCoeffs,
InputArray R, InputArray newCameraMatrix,
Size size, int m1type, OutputArray map1, OutputArray map2);
参数解释:
函数原型:
CV_EXPORTS_W void remap( InputArray src, OutputArray dst,
InputArray map1, InputArray map2,
int interpolation, int borderMode = BORDER_CONSTANT,
const Scalar& borderValue = Scalar());
参数解释:
函数原型:
CV_EXPORTS_W void undistort( InputArray src, OutputArray dst,
InputArray cameraMatrix,
InputArray distCoeffs,
InputArray newCameraMatrix = noArray() );
参数解释:
根据网上大佬测试,方法一相比方法二执行效率更高一些,推荐使用。
关于QT中使用OpenCV,这里不说了,详情可以查看以往的blog。
.pro
文件
#------------------------------------------------- # # Project created by QtCreator 2023-07-11T14:44:57 # #------------------------------------------------- QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = CalibrateDemo TEMPLATE = app # The following define makes your compiler emit warnings if you use # any feature of Qt which has been marked as deprecated (the exact warnings # depend on your compiler). Please consult the documentation of the # deprecated API in order to know how to port your code away from it. DEFINES += QT_DEPRECATED_WARNINGS # You can also make your code fail to compile if you use deprecated APIs. # In order to do so, uncomment the following line. # You can also select to disable deprecated APIs only up to a certain version of Qt. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 CONFIG += c++11 SOURCES += \ main.cpp \ mainwindow.cpp HEADERS += \ mainwindow.h FORMS += \ mainwindow.ui INCLUDEPATH += \ C:\opencv\install\install\include \ LIBS += \ C:\opencv\install\lib\libopencv_*.a \ # Default rules for deployment. qnx: target.path = /tmp/$${TARGET}/bin else: unix:!android: target.path = /opt/$${TARGET}/bin !isEmpty(target.path): INSTALLS += target
mainwindow.h
文件
#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <opencv2/opencv.hpp> #include <opencv2/core/core.hpp> #include <QMainWindow> #include <iostream> #include <fstream> #include <io.h> #include <QFileDialog> #include <QDebug> #include <vector> #include <QLabel> #include <QVBoxLayout> #include <QThread> using namespace std; using namespace cv; #define CALIBRATERESULTFILE "CalibrateResult.txt" namespace Ui { class MainWindow; } class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow(); private slots: void on_pushButton_LoadImage_clicked(); void on_pushButton_SaveResult_clicked(); void on_pushButton_StartCalibrate_clicked(); void on_pushButton_AppraiseCalibrate_clicked(); public: // QT图像 to openCV图像 和 openCV图像 to QT图像 QImage MatToQImage(Mat const& src); Mat QImageToMat(QImage const& src); void showCameraMatrix(Mat const& data); // 显示内参矩阵 void showDistCoeffs(Mat const& data); // 显示畸变系数 private: Ui::MainWindow *ui; // 保存不同图片标定板上角点的三维坐标 vector<vector<Point3f>> object_points; // 缓存每幅图像上检测到的角点 vector<Point2f> image_points_buf; // 保存检测到的所有角点 vector<vector<Point2f>> image_points_seq; // 相机内参数矩阵 cv::Mat cameraMatrix; // 相机的畸变系数 cv::Mat distCoeffs; // 每幅图像的平移向量 vector<cv::Mat> tvecsMat; // 每幅图像的旋转向量 vector<cv::Mat> rvecsMat; // 加载标定图片的文件夹 QString m_strCalibrateFolder; // 保存标定结果的文件夹 QString m_strSaveResultFolder; // 写入 std::ofstream fout; // 图像数量 int image_count = 0; // 每幅图像中角点的数量 vector<int> point_counts; }; #endif // MAINWINDOW_H
mainwindow.cpp
文件
#include "mainwindow.h" #include "ui_mainwindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); // 渲染设置为硬件加速 ui->label_showMat->setAttribute(Qt::WA_OpaquePaintEvent,true); ui->label_showMat->setAttribute(Qt::WA_NoSystemBackground,true); ui->label_showMat->setAutoFillBackground(false); cameraMatrix = cv::Mat(3,3,CV_32FC1, Scalar::all(0)); distCoeffs = cv::Mat(1,5,CV_32FC1, Scalar::all(0)); } MainWindow::~MainWindow() { delete ui; } void MainWindow::on_pushButton_LoadImage_clicked() { QString folderPath = QFileDialog::getExistingDirectory(this,QStringLiteral("选择标定图片文件夹"),tr(""),QFileDialog::ShowDirsOnly); if(!folderPath.isEmpty()) { //文件夹不为空 m_strCalibrateFolder = folderPath; } else { qDebug()<< "未选择任何文件夹"; return; } // 将加载的路径显示在界面 ui->lineEdit_CalibrateImagePath->setText(folderPath); // 设置文字左对齐 ui->lineEdit_SaveResultPath->setAlignment(Qt::AlignLeft); } void MainWindow::on_pushButton_SaveResult_clicked() { QString folderPath = QFileDialog::getExistingDirectory(this,QStringLiteral("选择保存结果文件夹"),tr(""),QFileDialog::ShowDirsOnly); if(folderPath.isEmpty()) { qDebug()<< "未选择任何文件夹"; return; } m_strSaveResultFolder = folderPath; // 设置路径到界面 ui->lineEdit_SaveResultPath->setText(folderPath); // 左对齐 ui->lineEdit_SaveResultPath->setAlignment(Qt::AlignLeft); } void MainWindow::on_pushButton_StartCalibrate_clicked() { // 保存标定结果的txt QString strResult = m_strSaveResultFolder + QString("/%1").arg(CALIBRATERESULTFILE); fout.open(strResult.toStdString().c_str()); // 1、加载标定图片 vector<QString> imageNames; QDir dir(m_strCalibrateFolder); QStringList fileNames = dir.entryList(QDir::Files | QDir::NoDotAndDotDot, QDir::Name); foreach(const QString& fileName, fileNames) { QString filePath = dir.filePath(fileName); imageNames.push_back(filePath); // 将完整的路径添加到图片路径容器 } // 2、分别对每张图片进行角点提取 Size image_size; // 图像尺寸 Size board_size = Size(9,6); // 标定板上每行、列的角点数 int count = -1; // 用于存储角点个数 for(int i = 0; i < imageNames.size(); i++) { image_count++; // 输出观察 qDebug()<< "image_count = " << image_count; // 输出校验 qDebug()<< "Check count = " << count; // 读取图片 Mat imageInput = imread(imageNames[i].toStdString().c_str()); if(image_count == 1) { // 读入第一张图片时获取图像宽高信息 image_size.width = imageInput.cols; image_size.height = imageInput.rows; } // 提取角点 if(0 == findChessboardCorners(imageInput, board_size, image_points_buf)) {// 未发现角点信息/找不到角点 qDebug()<< "未发现角点信息"; return; } else { // 3、对每一张标定图像进行亚像素化处理 Mat view_gray; // 将imageInput转为灰度图像 cvtColor(imageInput, view_gray, COLOR_RGB2GRAY); // 亚像素精准化(对粗提取的角点进行精准化) find4QuadCornerSubpix(view_gray,image_points_buf,Size(5,5)); image_points_seq.push_back(image_points_buf); // 尾插,保存亚像素角点 // 4、在棋盘格显示,并在界面刷新图片(显示找到的内角点绘制图片) // 在图像上显示角点位置 drawChessboardCorners(imageInput, board_size, image_points_buf, true); #if 0 imshow("Camera Calibration", imageInput); // 显示图片 imwrite("Calibration" + to_string(image_count) + ".png", imageInput); // 写入图片 waitKey(100); // 暂停0.1s #else QImage tmpImage = MatToQImage(imageInput); ui->label_showMat->setPixmap(QPixmap::fromImage(tmpImage.rgbSwapped())); ui->label_showMat->show(); QThread::msleep(100); // 延时0.1s QCoreApplication::processEvents(); #endif qDebug()<< "角点提取完成"; } } //destroyAllWindows(); // 5、相机标定 Size square_size = Size(5,5); // 初始化标定板上角点的三维坐标 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; // 假设标定板放在世界坐标系中,z=0的平面上 realPoint.x = i * square_size.height; realPoint.y = j * square_size.width; realPoint.z = 0; tempPointSet.push_back(realPoint); } } object_points.push_back(tempPointSet); } // 初始化每幅图像上的角点数量,假定每幅图像中都可以看到完整的标定板 for(i = 0; i < image_count; i++) { point_counts.push_back(board_size.width* board_size.height); } cv::calibrateCamera(object_points, image_points_seq,image_size,cameraMatrix,distCoeffs,rvecsMat,tvecsMat,0); qDebug()<< "标定完成!"; // 6/7对应下面1/2 } void MainWindow::on_pushButton_AppraiseCalibrate_clicked() { // 1、对标定结果进行评价 qDebug() << "开始评价标定结果....."; double total_err = 0.0; // 所有图像的平均误差的总和 double err = 0.0; // 每幅图像的平均误差 vector<Point2f> image_points2; // 保存重新计算得到的投影点 qDebug()<< "每幅图像的标定误差: "; fout << "每幅图像的标定误差: \n"; for(int 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 < tempPointSet.size(); j++) { // 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]; qDebug()<< "第" << i + 1 << "幅图像的平均误差: " << err << "像素"; fout << "第" << i + 1 << "幅图像的平均误差: " << err << "像素" << endl; } qDebug()<< "总体平均误差: " << total_err / image_count << "像素"; fout << "总体平均误差:" << total_err / image_count << "像素" << endl << endl; qDebug() << "评价完成!"; // 2、查看标定结果并保存 qDebug()<< "开始保存定标结果………………"; Mat rotation_matrix = Mat(3, 3, CV_32FC1, Scalar::all(0)); /* 保存每幅图像的旋转矩阵 */ fout << "相机内参数矩阵:" << endl; showCameraMatrix(cameraMatrix); fout << cameraMatrix << endl << endl; fout << "畸变系数:\n"; showDistCoeffs(distCoeffs); fout << distCoeffs << endl; for (int i = 0; i < image_count; i++) { fout << "第" << i + 1 << "幅图像的旋转向量:" << endl; fout << rvecsMat[i] << endl; /* 将旋转向量转换为相对应的旋转矩阵 */ Rodrigues(rvecsMat[i], rotation_matrix); fout << "第" << i + 1 << "幅图像的旋转矩阵:" << endl; fout << rotation_matrix << endl; fout << "第" << i + 1 << "幅图像的平移向量:" << endl; fout << tvecsMat[i] << endl << endl; } qDebug()<< "完成保存!"; fout << endl; } QImage MainWindow::MatToQImage(Mat const& src) { Mat temp; //make the same cv::Mat cvtColor(src,temp,COLOR_BGR2RGB); //cvtColor makes a copt, that what i need QImage dest((uchar*)temp.data,temp.cols,temp.rows,temp.step,QImage::Format_RGB888); dest.bits(); //enforce deep copy, see documentation return dest; } Mat MainWindow::QImageToMat(QImage const& src) { Mat tmp(src.height(),src.width(),CV_8UC4,(uchar*)src.bits(),src.bytesPerLine()); Mat result; cvtColor(tmp,result,COLOR_RGBA2BGR); return result; } void MainWindow::showCameraMatrix(const Mat &data) { std::ostringstream ss; ss << data; std::string strMatrix = ss.str(); QVBoxLayout* layout = new QVBoxLayout(ui->groupBox_CameraInParam); QLabel* label = new QLabel(); label->setText(QString::fromStdString(strMatrix)); label->setAlignment(Qt::AlignCenter); layout->addWidget(label); } void MainWindow::showDistCoeffs(const Mat &data) { std::ostringstream ss; ss << data; std::string strMatrix = ss.str(); QVBoxLayout* layout = new QVBoxLayout(ui->groupBox_DistortionParam); QLabel* label = new QLabel(); label->setText(QString::fromStdString(strMatrix)); label->setAlignment(Qt::AlignCenter); label->setWordWrap(true); layout->addWidget(label); }
使用比较简单,输入图像路径和输出标定结果路径,然后标定、评价,显示相机内参、相机畸变,这里没有涉及相机外参。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。