[双目视差] 立体校正源码分析(opencv)

[双目视差] 立体校正源码分析(opencv)

一、源码解析

立体校正:把实际中非共面行对准的两幅图像,校正成共面行对准
stereoRectify(cameraMatrixL, distCoeffL, cameraMatrixR, distCoeffR, imageSize, R, T, Rl, Rr, Pl, Pr, Q, CALIB_ZERO_DISPARITY,0, imageSize, &validROIL, &validROIR);
为每个摄像头计算立体校正的映射矩阵。所以其运行结果并不是直接将图片进行立体矫正,而是得出进行立体矫正所需要的映射矩阵。
立体矫正前:

立体矫正后:

过程:
(1)共面:先把旋转矩阵变为旋转向量,对旋转向量的模长平分,这样使两个图像平面共面,此时行未对齐。

cvConvert(matR, &om);
cvConvertScale(&om, &om, -0.5);

(2)行对准:建立行对准换行矩阵Rrect使极点转换到无穷远处。

首先创建平移向量T方向的旋转矩阵Rrec=[e1,e2,e3],其中e1为与平移向量T同方向的极点,e2为图像与平移向量同一方向的向量,e3为垂直于e1与e2所在平面的向量,通过叉乘方式获得,RL=Rrect rl ,RRrect rr,最后转为旋转矩阵,在通过转置就得到最终的RL和RR,这里求得的RL和RR是用来校正左右图像到第三平面,进行行对齐。

    cvRodrigues2(&om, &r_r);        // 旋转向量转换为旋转矩阵
    cvMatMul(&r_r, matT, &t);     //两个数组对应元素的乘法
    int idx = fabs(_t[0]) > fabs(_t[1]) ? 0 : 1;
    _uu[2] = 1;
    cvCrossProduct(&uu, &t, &ww); //对两个三维向量做叉乘
    nt = cvNorm(&t, 0, CV_L2); //计算t的绝对范数
    CV_Assert(fabs(nt) > 0); //捕获异常而不是程序崩溃
    nw = cvNorm(&ww, 0, CV_L2);
    CV_Assert(fabs(nw) > 0);
    cvConvertScale(&ww, &ww, 1 / nw);
    cvCrossProduct(&t, &ww, &w3);
    nw = cvNorm(&w3, 0, CV_L2);
    CV_Assert(fabs(nw) > 0);
    cvConvertScale(&w3, &w3, 1 / nw);
    _uu[2] = 0;
    for (i = 0; i < 3; ++i)
    {
        _wr[idx][i] = -_t[i] / nt;
        _wr[idx ^ 1][i] = -_ww[i];
        _wr[2][i] = _w3[i] * (1 - 2 * idx);
    }
        cvGEMM(&wR, &r_r, 1, 0, 0, &Ri, CV_GEMM_B_T);
	    cvConvert( &Ri, _R1 );
    	cvGEMM(&wR, &r_r, 1, 0, 0, &Ri, 0);
    	cvConvert( &Ri, _R2 );

左右两个摄像机共面、行对齐后,分别计算两个相机的内参矩阵,即投影矩阵,过程为:
通过原始两相机的内参矩阵,与当前共面对齐后的图像进行比例计算,得到新的内参信息(fx,fy,这里的fx=fy)

newImgSize = newImgSize.width * newImgSize.height != 0 ? newImgSize : imageSize;
    const double ratio_x = (double)newImgSize.width / imageSize.width / 2;
    const double ratio_y = (double)newImgSize.height / imageSize.height / 2;
    const double ratio = idx == 1 ? ratio_x : ratio_y;
    fc_new = (cvmGet(_cameraMatrix1, idx ^ 1, idx ^ 1) + cvmGet(_cameraMatrix2, idx ^ 1, idx ^ 1)) * ratio;

分别对左右摄像机进行图像矫正为正常的视角,将变化后的点转换为齐次坐标系,同时改变相机内参,计算三维点在平面中的坐标,为简单起见,将两个摄影机的主要点设置为平均值。

    for( k = 0; k < 2; k++ )
    {
        const CvMat* A = k == 0 ? _cameraMatrix1 : _cameraMatrix2;
        const CvMat* Dk = k == 0 ? _distCoeffs1 : _distCoeffs2;
        CvPoint2D32f _pts[4] = {};
        CvPoint3D32f _pts_3[4] = {};
        CvMat pts = cvMat(1, 4, CV_32FC2, _pts);
        CvMat pts_3 = cvMat(1, 4, CV_32FC3, _pts_3);

        for( i = 0; i < 4; i++ )
        {
            int j = (i<2) ? 0 : 1;
            _pts[i].x = (float)((i % 2)*(nx));
            _pts[i].y = (float)(j*(ny));
        }
		//利用undistortPoints()函数将拍摄的图像矫正为正常的视角,便于检测。
        cvUndistortPoints( &pts, &pts, A, Dk, 0, 0 );
		//将变换后的点先变化为齐次坐标系
        cvConvertPointsHomogeneous( &pts, &pts_3 );

        //Change camera matrix to have cc=[0,0] and fc = fc_new
        double _a_tmp[3][3];
        CvMat A_tmp  = cvMat(3, 3, CV_64F, _a_tmp);
        _a_tmp[0][0]=fc_new;
        _a_tmp[1][1]=fc_new;
        _a_tmp[0][2]=0.0;
        _a_tmp[1][2]=0.0;
		//计算三维点在平面中的坐标.
        cvProjectPoints2( &pts_3, k == 0 ? _R1 : _R2, &Z, &A_tmp, 0, &pts );
        CvScalar avg = cvAvg(&pts);
        cc_new[k].x = (nx)/2 - avg.val[0];
        cc_new[k].y = (ny)/2 - avg.val[1];
    }

设置CALIB_ZERO_DISPARITY,让两幅校正后的图像的主点有相同的像素坐标

 if( flags & CALIB_ZERO_DISPARITY )
    {
        cc_new[0].x = cc_new[1].x = (cc_new[0].x + cc_new[1].x)*0.5;
        cc_new[0].y = cc_new[1].y = (cc_new[0].y + cc_new[1].y)*0.5;
    }

获取左右相机的投影P1,P2矩阵

 cvZero( &pp );
    _pp[0][0] = _pp[1][1] = fc_new;
    _pp[0][2] = cc_new[0].x;
    _pp[1][2] = cc_new[0].y;
    _pp[2][2] = 1;
    cvConvert(&pp, _P1);

    _pp[0][2] = cc_new[1].x;
    _pp[1][2] = cc_new[1].y;
    _pp[idx][3] = _t[idx]*fc_new; // baseline * focal length
    cvConvert(&pp, _P2);

校正映射:立体校正之后求得左右相机旋转矩阵R、投影矩阵P、重投影矩阵Q后,使用initUndistortRectifyMap()函数,调⽤两次:⼀次为左侧图像,⼀次为右侧图像,求映射变换矩阵
initUndistortRectifyMap(cameraMatrixL, distCoeffL, Rl, Pl, imageSize, CV_32FC1, mapLx, mapLy);
initUndistortRectifyMap(cameraMatrixR, distCoeffR, Rr, Pr, imageSize, CV_32FC1, mapRx, mapRy);

输入单相机内参,畸变参数,旋转矩阵R,投影参数矩阵P(R和P是通过立体矫正stereoRectify()得到),原图像size大小,类型CV_32FC1,输出映射矩阵mapx,mapy

校正映射过程:
获取相机内参cameraMatrix、畸变矩阵distCoeffs、旋转矩阵matR、摄像机投影参数矩阵newCameraMatrix

	//相机内参、畸变矩阵
    Mat cameraMatrix = _cameraMatrix.getMat(), distCoeffs = _distCoeffs.getMat();
	//旋转矩阵、摄像机参数矩阵
    Mat matR = _matR.getMat(), newCameraMatrix = _newCameraMatrix.getMat();
创建相机内参矩阵A、投影参数矩阵Ar,旋转矩阵R、畸变矩阵distCoeffs:
    Mat_<double> R = Mat_<double>::eye(3, 3);
    Mat_<double> A = Mat_<double>(cameraMatrix), Ar;
    //A为相机内参   
    //Ar为摄像机坐标参数,
    if( !newCameraMatrix.empty() )
        Ar = Mat_<double>(newCameraMatrix);
    else
        Ar = getDefaultNewCameraMatrix( A, size, true );

    //R为旋转矩阵
    if( !matR.empty() )
        R = Mat_<double>(matR);

    //distCoeffs为畸变矩阵
    if( !distCoeffs.empty() )
        distCoeffs = Mat_<double>(distCoeffs);
    else
    {
        distCoeffs.create(14, 1, CV_64F);
        distCoeffs = 0.;
    }

通过LU分解求新的内参矩阵Ar与旋转矩阵R乘积的逆矩阵iR

Mat_<double> iR = (Ar.colRange(0,3)*R).inv(DECOMP_LU);
const double* ir = &iR(0,0);

从旧的内参矩阵中取出光心位置u0,v0作为主坐标点,和归一化焦距fx,fy

    double u0 = A(0, 2),  v0 = A(1, 2);
    double fx = A(0, 0),  fy = A(1, 1);

畸变参数计算,14个畸变系数,不过大多用到的只有(k1,k2,p1,p2),k1,k2为径向畸变系数,p1,p2为切向畸变系数,用不到的置为0,tauX,tauY是梯形畸变

 	const double* const distPtr = distCoeffs.ptr<double>();
    double k1 = distPtr[0];
    double k2 = distPtr[1];
    double p1 = distPtr[2];
    double p2 = distPtr[3];
    double k3 = distCoeffs.cols + distCoeffs.rows - 1 >= 5 ? distPtr[4] : 0.;
    double k4 = distCoeffs.cols + distCoeffs.rows - 1 >= 8 ? distPtr[5] : 0.;
    double k5 = distCoeffs.cols + distCoeffs.rows - 1 >= 8 ? distPtr[6] : 0.;
    double k6 = distCoeffs.cols + distCoeffs.rows - 1 >= 8 ? distPtr[7] : 0.;
    double s1 = distCoeffs.cols + distCoeffs.rows - 1 >= 12 ? distPtr[8] : 0.;
    double s2 = distCoeffs.cols + distCoeffs.rows - 1 >= 12 ? distPtr[9] : 0.;
    double s3 = distCoeffs.cols + distCoeffs.rows - 1 >= 12 ? distPtr[10] : 0.;
    double s4 = distCoeffs.cols + distCoeffs.rows - 1 >= 12 ? distPtr[11] : 0.;
    double tauX = distCoeffs.cols + distCoeffs.rows - 1 >= 14 ? distPtr[12] : 0.;
    double tauY = distCoeffs.cols + distCoeffs.rows - 1 >= 14 ? distPtr[13] : 0.;

通过tauX,tauY计算倾斜图像传感器的梯形畸变矩阵matTilt,其中tauX,tauY用不到的话matTilt为单位矩阵

cv::Matx33d matTilt = cv::Matx33d::eye();
cv::detail::computeTiltProjectionMatrix(tauX, tauY, &matTilt);

求得上述提到的逆矩阵ir、梯形矩阵、主坐标点(u0,v0)、焦距(fx,fy)及畸变参数后,反向映射,遍历目标图像所有像素位置,找到畸变图像中对应位置坐标(u,v),并分别保存坐标(u,v)到mapx和mapy中。
过程如下:


 parallel_for_(Range(0, size.height), 
 initUndistortRectifyMapComputer(size, map1, map2, m1type, ir, matTilt, u0, v0,fx, fy, k1, k2, p1, p2, k3, k4, k5, k6, s1, s2, s3, s4));

const int begin = range.start;
const int end = range.end;	
for( int i = begin; i < end; i++ )
 {        //定义映射表mapx和mapy行元素指针  
            float* m1f = map1.ptr<float>(i);//指向第i+1行第一个元素指针
            float* m2f = map2.empty() ? 0 : map2.ptr<float>(i);
            short* m1 = (short*)m1f;
            ushort* m2 = (ushort*)m2f;
			
			 //利用逆矩阵iR将二维图像坐标(j,i)转换到摄像机坐标系(_x,_y,_w)
            double _x = i*ir[1] + ir[2], _y = i*ir[4] + ir[5], _w = i*ir[7] + ir[8];

            int j = 0;
            //遍历每个像机坐标位置
            for( ; j < size.width; j++, _x += ir[0], _y += ir[3], _w += ir[6] )
            {
				//摄像机坐标系归一化,令Z=1
                double w = 1./_w, x = _x*w, y = _y*w;
				//根据畸变模型进行变换
                double x2 = x*x, y2 = y*y;
                double r2 = x2 + y2, _2xy = 2*x*y;
                double kr = (1 + ((k3*r2 + k2)*r2 + k1)*r2)/(1 + ((k6*r2 + k5)*r2 + k4)*r2);
                double xd = (x*kr + p1*_2xy + p2*(r2 + 2*x2) + s1*r2+s2*r2*r2);
                double yd = (y*kr + p1*(r2 + 2*y2) + p2*_2xy + s3*r2+s4*r2*r2);
				//根据求取的xd,yd将三维坐标重投影到二维畸变图像坐标(u,v)
                cv::Vec3d vecTilt = matTilt*cv::Vec3d(xd, yd, 1);
                double invProj = vecTilt(2) ? 1./vecTilt(2) : 1;
                double u = fx*invProj*vecTilt(0) + u0;
                double v = fy*invProj*vecTilt(1) + v0;
				//保存u,v的值到Mapx,Mapy中
                if( m1type == CV_16SC2 )
                {
                    int iu = cv::saturate_cast<int>(u*cv::INTER_TAB_SIZE);
                    int iv = cv::saturate_cast<int>(v*cv::INTER_TAB_SIZE);
                    m1[j*2] = (short)(iu >> cv::INTER_BITS);
                    m1[j*2+1] = (short)(iv >> cv::INTER_BITS);
                    m2[j] = (ushort)((iv & (cv::INTER_TAB_SIZE-1))*cv::INTER_TAB_SIZE + (iu & (cv::INTER_TAB_SIZE-1)));
                }
                else if( m1type == CV_32FC1 )
                {
                    m1f[j] = (float)u;
                    m2f[j] = (float)v;
                    // map_x实际上记录的是对应原图像中(i,j)位置的横坐标,map_y实际上记录的是(i,j)位置的纵坐标
                }
                else
                {
                    m1f[j*2] = (float)u;
                    m1f[j*2+1] = (float)v;
                }
            }
        }

initUndistortRectifyMap

  • 校正映射参数:
    输入单相机内参,畸变参数,旋转矩阵R,投影参数矩阵P(R和P是通过立体矫正stereoRectify()得到),原图像size大小,类型CV_32FC1,输出映射矩阵mapx,mapy

  • 校正映射过程:
    1、获取相机内参cameraMatrix、畸变矩阵distCoeffs、旋转矩阵matR、摄像机投影参数矩阵newCameraMatrix
    2、创建相机内参矩阵A、投影参数矩阵Ar,旋转矩阵R、畸变矩阵distCoeffs
    3、通过LU分解求新的内参矩阵Ar与旋转矩阵R乘积的逆矩阵iR
    4、从旧的内参矩阵中取出光心位置u0,v0作为主坐标点,和归一化焦距fx,fy
    5、畸变参数计算,14个畸变系数,不过大多用到的只有(k1,k2,p1,p2),k1,k2为径向畸变系数,p1,p2为切向畸变系数,用不到的置为0,tauX,tauY是梯形畸变
    6、通过tauX,tauY计算倾斜图像传感器的梯形畸变矩阵matTilt,其中tauX,tauY用不到的话matTilt为单位矩阵
    7、求得上述提到的逆矩阵ir、梯形矩阵、主坐标点(u0,v0)、焦距(fx,fy)及畸变参数后,反向映射,遍历目标图像所有像素位置,找到畸变图像中对应位置坐标(u,v),并分别保存坐标(u,v)到mapx和mapy中。
    ①定义映射表1、2行元素指针
    ②利用逆矩阵iR将二维图像坐标(j,i)转换到摄像机坐标系(_x,_y,_w)
    ③遍历每个相机坐标位置,将相机坐标系归一化,令Z=1平面上。
    ④畸变模型的转换,求得xd,yd
    ⑤根据求取的xd,yd将三维坐标重投影到二维畸变图像坐标(u,v)
    ⑥保存u,v的值到Mapx,Mapy中
    map_x实际上记录的是对应原图像中(i,j)位置的横坐标,map_y实际上记录的是(i,j)位置的纵坐标,而我们在这里把像素操作的i当做了横坐标,j当做了纵坐标

remap

remap( InputArray _src, OutputArray _dst,
                InputArray _map1, InputArray _map2,
                int interpolation, int borderType, const Scalar& borderValue )
  • 像素重映射:
    重映射,就是把一幅图像中某位置的像素放置到另一个图片指定位置的过程。为了完成映射过程, 我们需要获得一些插值为 非整数像素的坐标,因为源图像与目标图像的像素坐标不是一一对应的
    g(x,y) = f ( h(x,y) )
    g( ) 是目标图像, f() 是源图像, 而h(x,y) 是作用于 (x,y) 的映射方法函数。简单的说就是改变图片的位置(左,右,上,下,颠倒翻转)

  • 像素重映射参数:
    输入源图像src,目标图像dst,输入记录源图像位置的横坐标Mapx,输入记录源图像位置的纵坐标Mapy,使用双线性插值方式INTER_LINEAR,边界模式使用默认值BORDER_CONSTANT,表示目标图像中“离群点(outliers)”的像素值不会被此函数修改,边界颜色,默认Scalar()黑色

  • 对左右摄像头采集到的数据分别进行remap,使源图像中像素位置通过映射表mapx,mapy位置,映射到新的图像中。最后得到的图像就是共面、行对齐的图像。

二、源码中的方法

OpenCV双目视觉:Bouguet立体校正https://jingyan.baidu.com/article/a681b0de74312a3b1843460d.html

  • 将旋转矩阵转换为旋转向量:
    cvConvert函数用于图像和矩阵之间的相互转换 为什么要用cvConvert 把IplImage转为矩阵? 因为IplImage里的数据,你只能用uchar的形式存放,当你需要这些图像数据看作数据矩阵来运算时,0~255的精度显然满足不了要求; 然而CvMat里却可以存放任意通道数、任意格式的数据,这个机制方便了研究中的这种需求,转化为矩阵就可以进行更自由的计算。

  • 获得平均旋转向量,等比缩放一半:
    函数 cvConvertScale 有多个不同的目的因此就有多个同义函数(如上面的#define所示)。该函数首先对输入数组的元素进行比例缩放,然后将shift加到比例缩放后得到的各元素上,即: dst(I)=src(I)*scale + (shift,shift,…)

  • 旋转向量转换为旋转矩阵:
    cvRodrigues2()

  • 旋转矩阵r_r 与平移矩阵相乘得到t(3,1)
    cvMatMul()

  • cvCrossProduct(&uu, &t, &ww); //对两个三维向量做叉乘

  • cvNorm(&t, 0, CV_L2); //计算t的绝对范数

  • CV_Assert(fabs(nt) > 0); //捕获异常而不是程序崩溃

  • 对WR矩阵值做变换,与旋转矩阵进行广义矩阵乘法分别得到左旋转矩阵和右旋转矩阵

  • void cvGEMM( const CvArr* src1, const CvArr* src2, double alpha,const CvArr* src3, double beta, CvArr* dst, int tABC=0 ); 广义矩阵的乘法
    src1:第一输入数组
    src2:第二输入数组
    alpha:系数
    src3“第三输入数组(偏移量),如果没有偏移量,可以为空(NULL)
    beta:表示偏移量的系数
    dst:输出数组
    tABC:
    转置操作标志,可以是0。当为0时,没有转置。或者还有下面的值的组合:
    CV_GEMM_A_T:表示src1转置
    CV_GEMM_B_T:表示src2转置
    CV_GEMM_C_T:表示src3转置
    例如,CV_GEMM_A_T+CV_GEMM_C_T对应
    alpha*(src1转置)src2+beta(src3转置)

  • opencv实现中要先把旋转矩阵变为旋转向量,对旋转向量的模长平分,就得到可以把光轴 摆平的左右矩阵,然后用这个矩阵乘以T,归一化得到e1,然后根据上面的公式构建e2,e3就可以通过叉乘获得,最后转为旋转矩阵,在通过转置就可以得到最终的RL和RR,RL和RR是用来校正左右图像到第三平面,行对齐

  • 获取左右摄像头的内参及畸变系数利用undistortPoints()函数将拍摄的图像矫正为正常的视角,便于检测
    cvUndistortPoints:https://yongqi.blog.csdn.net/article/details/52946821

  • cvConvertPointsHomogeneous//将变换后的点先变化为齐次坐标系

cvmGet直接存取矩阵元素

  • Opencv,计算三维点在平面中的坐标.
    void cvProjectPoints2
    (
    const CvMat* objectPoints, //是需要投影的点的序列,是一个点位置的N3的矩阵。
    const CvMat
    rvec,
    const CvMat* tvec, //建立两个坐标系的联系
    const CvMat* cameraMatrix,
    const CvMat* distCoeffs,//内参数矩阵和形变系数
    CvMat* imagePoints,//N2的矩阵将被写入计算结果
    CvMat
    dpdrot=NULL,
    CvMat* dpdt=NULL,
    CvMat* dpdf=NULL,
    CvMatdpdc=NULL,
    CvMat
    dpddist=NULL //偏导数的雅克比矩阵
    )

  • 如果设置为CALIB_ZERO_DISPARITY的话,该函数会让两幅校正后的图像的主点有相同的像素坐标。否则该函数会水平或垂直的移动图像,以使得其有用的范围最大。

  • // 获取左右相机的投影P1,P2矩阵

  • icvGetRectangles功能是获取标准图像的有效像素(有效像素指在畸变图像中有对应像素的像素)所构成的区域的最大内接矩阵和最小外接矩阵,其实现方式是:取畸变图像中的一些特殊点(四条边上的点和中间区域的一些点)

  • 当alpha为0时,取inner即内矩阵,用内矩阵大小作为新的图像大小,重新得到fx,fy,cx,cy,因此新的内参矩阵诞生了. 当alpha为1时,取outer即外矩阵。当alpha介于0~1时,则按照比例重新计算fx,fy,cx,cy。
    //事实上,内矩阵等同于不含任何黑色边框的图幅大小,而外矩阵等同于原图大小。

posted @ 2021-05-28 15:53  野哥李  阅读(388)  评论(0编辑  收藏  举报  来源