立体视觉图像校正

最近在学习李博的一系列博客,在这里想做个记录(对原文简洁化了一下并加了代码实现),如果有朋友想看原文的话末尾有链接

研究对极约束的目的

对两幅图像的二维匹配搜索变成一维,节省计算量,排除虚假匹配点,让匹配的搜索空间变小,略去完全不可能是解的像素。

对极约束,是将搜索空间约束到像平面内的一条直线上。

极平面和极线

                                                 

                                                                                              图1 对极约束

两个相机的光心O1, O2(对应上图中的C1和C2)和空间点P形成一个三角关系,它们确定了一个空间平面,这个平面和两个像平面都相交,交线分别是l1, l2,在l1上的所有点,其匹配点一定在l2上,这就是对极约束,它将匹配的搜索空间限制到了一条直线上,大大减少了搜索的空间大小,提高匹配效率。而两条直线l1, l2就叫做极线,称它们为极线对

极线校正

极线校正是通过两个相机进行旋转,并重新定义新的平面,让极线对共线且平行于像平面的某条坐标轴(通常是水平轴),该操作同时建立了新的立体像对。纠正完成后,同一匹配点对,位于两个视图的同一行内,这意味着它们只有水平坐标(列坐标)的差异,这个差异称为视差(d),数学定义上,视差d = col(p1) - col(p2)(col指水平方向坐标,或者说列坐标)

                                           

                                                                                   图2 极线校正

 极线校正的方法

(1)Fusiello校正法

目标:

1.极线对平行于某条坐标轴。

2.极线对共线,匹配点对位于像平面的同一行。

为达到这两个目标,极线校正需要通过旋转相机和重新定义像平面来做,实际上,两个操作的本质在于重新定义投影矩阵M= K[R| - RC]。通过旋转相机重定义旋转矩阵R -> Rn让新的像平面共面且平行于相机基线,则可以满足目标1,而重定义像平面的内参K -> Kn使双相机有同样的内参数,可以满足目标2。

如下图所示,纠正后的像平面,水平u轴和基线C1C2平行,焦距f和主点坐标相等。

                                               

                                                                图3 Fusiello校正法

 

 具体地,第一步是旋转相机让像平面共面且平行于相机基线,实际上是重新定义一个相机坐标系,首先新相机坐标系设计如下:

1.首先是X轴,显然要和相机基线平行,才能让像平面平行于相机基线,所以X轴基向量为rx = (C1-C2) / || C2 - C1||。

2.其次是Y轴,它是和X轴正交的,可以设置一个任意的向量k, 让Y轴和X轴以及向量k正交,所以Y轴的基向量为ry = K X rx。关于这个k,理论上任意都可以,但是我们希望新坐标系下的图像和原图的范围尽量一致,所以尽量选择和旧的Y轴近似的朝向,在Fusiello法中,k为旧的Z轴所表示的单位向量。

3.最后是Z轴,X和Y轴确定后,Z轴的基向量就可以通过两者的叉乘基于右手法则得到:rz = rx X ry

确定了新坐标的3个基向量,就可以确定新的旋转矩阵

                                                                                             

 第二步,是重新设计新的内参矩阵Kn,理论上,K是可以任意设置的,但是为了和旧相机尽量保持一致,Fusiello法选择的是Kn = (Kleft + Kright) / 2。且把倾斜因子设置为0。

确定K和R后,我们得到新的投影矩阵Mn = Kn[Rn| - RnC]。

接下来就是校正过程,具体的过程是对于新图像像素pn,通过变换矩阵T计算旧图像上的像素p:p = T pn,再通过双线性内插获取像素值赋给新图像。所以关键就在于如何得到变换矩阵T。

目前来说,我们旋转了相机以及重定义内参,这样变换了旋转矩阵R -> Rn和内参矩阵K -> Kn,而没有改变的是相机中心C。所以现在有新旧两组投影矩阵

                                                                      

其中, Q = K R,Qn = Kn Rn。

经过推导可得:

                      

 这就是新旧图像的转换公式,而T=Qn Q-1即是转换矩阵。

对于两个相机,根据公式可以计算各自的转换矩阵T1,T2。                     

(2) Bouguet校正法

将左右相机之间的旋转矩阵R拆分,从而两相机平面处于同一平面上,此时并没有行对齐。

                                                             

然后求解对齐校正矩阵。定义左右相机光心之间的单位平移向量e1

                                                             

定义与e1正交的向量为e2,归一化后表示为

                                                     

 将单位向量e1和e2作叉积,得到向量e3,表示为

                                                           

因此,得到行对齐矩阵Rrect,表示为

                                                      

综合左右相机旋转矩阵和行对齐校正矩阵,得到左右相机的立体校正矩阵为

                                                         

根据立体校正矩阵变换左右相机图像,使得左右相机图像共面且完全行对齐,完成立体校正。

Bouguet校正实现代码

#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/calib3d.hpp>
// #include "Matrix.h"


using namespace cv;
using namespace std;

//cv::Mat imageSize(1024, 1280, CV_8UC3, cv::Scalar(100));
cv::Size imagesize(1024, 1280);
cv::Mat cameraMatrix1;   //左相机矩阵
cv::Mat distCoeffs1;    //左相机畸变系数矩阵
cv::Mat cameraMatrix2;   //右相机内参矩阵
cv::Mat distCoeffs2;//右相机畸变系数矩阵
cv::Mat R, T, E, F; //R 旋转矢量 T平移矢量 E本征矩阵 F基础矩阵
cv::Mat R1, R2, P1, P2, Q; //校正旋转矩阵R,投影矩阵P 重投影矩阵Q 
cv::Mat mapx1, mapy1, mapx2, mapy2; //映射表  
Rect validROI1, validROI2; //图像校正之后,会对图像进行裁剪,这里的validROI就是指裁剪之后的区域  
bool isopen = true;//用于判断相机获取图像对时是否打开
cv::Mat imgLeft;//校正后的左图像
cv::Mat imgRight;//校正后的右图像
cv::Mat disparity8U;//将视差值范围投影到0-255的视差图
cv::Mat disparity_real;//真实视差值的视差图
cv::Mat disparity;//调用函数得到的原始视差图
int numDisparities = 7;//匹配搜索的视差范围,规定必须能被16整除
int blockSize = 7;//匹配窗口大小
int UniquenessRatio = 5;

cv::Mat _3dImage;//三维坐标图

bool readFile(string filename)
{
    FileStorage fs(filename, FileStorage::READ);
    if (fs.isOpened())
    {
        fs["cameraMatrix1"] >> cameraMatrix1;
        fs["distCoeffs1"] >> distCoeffs1;
        fs["cameraMatrix2"] >> cameraMatrix2;
        fs["distCoeffs2"] >> distCoeffs2;
        fs["R"] >> R;
        fs["T"] >> T;
        fs.release();
       /* cout<<"Succeed to read the Calibration result!!!"<<endl;
        cout<<"左相机内参矩阵:"<<endl<<cameraMatrix1<<endl;
        cout<<"右相机内参矩阵:"<<endl<<cameraMatrix2<<endl;
        cout<<"distCoeffs1:"<<endl<< distCoeffs1 <<endl;
        cout<<"distCoeffs2:"<<endl<< distCoeffs2 <<endl;
        cout<<"R:"<<endl<<R<<endl;
        cout<<"T:"<<endl<<T<<endl;*/
        
        return true;
    }
    else
    {
        cerr << "Error: can not open the Calibration result file!!!!!" << endl;
        return false;
    }

}





int main(int argc, char** argv)
{
    if (argc < 3) {
        std::cout << "参数过少,请至少指定左右影像路径!" << std::endl;
        return -1;
    }

    //···············································································//
    // 读取影像
    std::string path_left = argv[1];
    std::string path_right = argv[2];

    cv::Mat img_left_c = cv::imread(path_left, cv::IMREAD_COLOR);
    cv::Mat img_left = cv::imread(path_left, cv::IMREAD_GRAYSCALE);
    cv::Mat img_right = cv::imread(path_right, cv::IMREAD_GRAYSCALE);

    if (img_left.data == nullptr || img_right.data == nullptr) {
        std::cout << "读取影像失败!" << std::endl;
        return -1;
    }
    if (img_left.rows != img_right.rows || img_left.cols != img_right.cols) {
        std::cout << "左右影像尺寸不一致!" << std::endl;
        return -1;
    }

    //************从标定文件中读取标定结果
    const string filename = "D:\\Postgraduate\\VS_Vision_Code\\stereoRectify\\cameraParameter.yaml";
    bool readOK = readFile(filename);
    if (!readOK)
    {
        cerr << "failed to readfile!" << endl;
        return -1;
    }
    /**************************************************************************************/
    /*
    要实现立体校正,使左右图像共平面且行对准,需要用到以下参数:
    cameraMatrix1, distCoeffs1, R1, P1
    cameraMatrix2, distCoeffs2, R2, P2
    其中内参矩阵和畸变系数通过标定程序获得,但R1、P1、R2、P2的值,opencv提供了两种方法:
    1. Hartley方法;
    这种方法称为“非标定立体校正方法”,也就是说不用通过标定获得的内参矩阵和畸变系数获取
    R1、P1、R2、P2的值,直接根据匹配点计算基础矩阵F,再进一步计算R1、P1、R2、P2。
    这种方法主要用到两个函数:findFundamentalMat()和stereoRectifyUncalibrated()
    具体的原理说明参考《Learning opencv》中文版498页。
    2. Bouguet方法
    这种方法称为“标定立体校正方法”,它是根据立体标定获得的内参矩阵、畸变系数、R和T作为
    输入,利用stereoRectify()函数得到R1、P1、R2、P2的值。
    两种方法的选用根据bool类型的useCalibrated变量决定,
    当useCalibrated=true时,调用Bouguet方法;
    当useCalibrated=false时,调用Hartley方法;
    */
    /**************************************************************************************/
    cv::stereoRectify(cameraMatrix1, distCoeffs1, cameraMatrix2, distCoeffs2, imagesize, R, T, R1, R2, P1, P2, Q,
        CALIB_ZERO_DISPARITY, -1, imagesize, &validROI1, &validROI2);
   /* cout << "重投影矩阵Q:" << endl << Q << endl;
    cout << "打印左摄像机投影矩阵P1:" << endl << P1 << endl;
    cout << "打印右摄像机投影矩阵P2:" << endl << P2 << endl;
    cout << "打印新的左摄像机旋转矩阵R1:" << endl << R1 << endl;
    cout << "打印新的右摄像机旋转矩阵R2:" << endl << R2 << endl;*/
    /*根据stereoRectify计算出来的R和P来计算图像的映射表mapx, mapy
        mapx, mapy这两个映射表接下来可以给remap()函数调用,来校正图像,使得两幅图像共面并且行对准
        initUndistortRectifyMap()的参数newCameraMatrix就是校正后的摄像机矩阵。
        在openCV里面,校正后的计算机矩阵Mrect是跟投影矩阵P一起返回的。
        所以我们在这里传入投影矩阵P,此函数可以从投影矩阵P中读出校正后的摄像机矩阵
    */
    cv::initUndistortRectifyMap(cameraMatrix1, distCoeffs1, R, P1, imagesize, CV_32FC1, mapx1, mapy1);
    cv::initUndistortRectifyMap(cameraMatrix2, distCoeffs2, R, P2, imagesize, CV_32FC1, mapx2, mapy2);
    //校正
    cv::remap(img_left, imgLeft, mapx1, mapy1, INTER_LINEAR);
    cv::remap(img_right, imgRight, mapx2, mapy2, INTER_LINEAR);

    /***************把左右图像的校正结果显示到同一画面上进行对比*********************/
    cv::Mat canvas;
    double sf = 0.7;
    int w, h;
    w = cvRound(imagesize.width * sf);
    h = cvRound(imagesize.height * sf);
    canvas.create(h, w * 2, CV_8UC1);
    //左图像画到画布上
    //得到画布的左半部分 
    Mat canvasPart = canvas(Rect(w * 0, 0, w, h));
    //把图像缩放到跟canvasPart一样大小并映射到画布canvas的ROI区域中  
    resize(imgLeft, canvasPart, canvasPart.size(), 0, 0, INTER_AREA);
    //右图像画到画布上 
    canvasPart = canvas(Rect(w, 0, w, h));
    resize(imgRight, canvasPart, canvasPart.size(), 0, 0, INTER_AREA);
    //画上对应的线条
    for (int i = 0; i < canvas.rows; i += 32)
        line(canvas, Point(0, i), Point(canvas.cols, i), Scalar(255, 255, 255), 1, 8);
    imshow("rectified", canvas);
    cv::waitKey(0);


    /*cv::imshow("原左图", img_left);
    cv::waitKey(0);*/


    return 0;
}

 

 

 

参考博客:https://blog.csdn.net/rs_lys/article/details/119837782

 

posted @ 2022-11-17 15:33  路人加  阅读(1155)  评论(0编辑  收藏  举报