【图像】【OpenCV鱼眼矫正】二、fisheye::initUndistortRectifyMap()源码分析

一、fisheye::initUndistortRectifyMap() 之 功能介绍

上一篇文章的第 2. 部分中,我们已对fisheye::initUndistortRectifyMap()的功能做过详细介绍,这里再重复一下:

fisheye::initUndistortRectifyMap()的作用是根据无畸变图的像素位置(i, j),推出它对应的畸变图中的像素位置(u, v),然后把畸变图中的(u, v)复制到新图中的(i, j),就得到了矫正图像。

fisheye::initUndistortRectifyMap()函数在OpenCV中的声明如下:

void cv::fisheye::initUndistortRectifyMap	(	InputArray 	K,
												InputArray 	D,
												InputArray 	R,
												InputArray 	P,
												const cv::Size & 	size,
												int 	m1type,
												OutputArray 	map1,
												OutputArray 	map2 
)	

其中各形参的作用在网上有很多文章介绍,本文不再赘述。

这里只简单介绍其输入输出

输入:

  • 内参矩阵K
  • 畸变系数D
  • R并非旋转向量/矩阵,矫正单目鱼眼相机时,R默认为单位矩阵
  • 若想使矫正后图片对应的内参与原相机内参不同,则把P设为新内参;否则,P与原内参相同
  • 输出矩阵map1的类型m1type
  • 其他参数

输出:

  • map1
  • map2

我们通常把参数m1type设为CV_16SC2时,此时最重要的输出是map1

根据CV_16SC2,map1此时是一个2通道的矩阵,每个点(i, j)都是一个2维向量,包含:

  • map1(i, j) [0]
  • map2(i, j) [1]

现在,我们令u = map1(i, j)[0], v= map1(i, j)[1]

那么,ijuv的含义是:

畸变图中坐标为(u, v)的像素点,在无畸变图中应该处于(i, j)位置。

这样,把畸变图(u, v)处的像素,复制到(i, j)处,就得到了矫正后的图像。

即如下图所示:

而此时的map2是为了做插值、让矫正后图像更清晰而用,本文不做介绍。


注:

若把参数m1type设为CV_32FC1,则此时map1不再是二通道矩阵,而是一通道矩阵

此时map1保存m1typeCV_16SC2map10通道,map2保存m1typeCV_16SC2map11通道。

即,此时令u = map1(i, j),v = map2(i, j),那么才有:

畸变图中坐标为(u, v)的像素点,在无畸变图中应该处于(i, j)位置。


二、fisheye::initUndistortRectifyMap() 之 源码分析

1. 源码分析

把参数m1type设为CV_16SC2,再**抛去*断言、类型判断等非必要的部分,我们把fisheye::initUndistortRectifyMap简化如下:

//本文对程序做了详细注释
//转发请注明出处https://editor.csdn.net/md?not_checkout=1&articleId=112751432。
void cv::fisheye::initUndistortRectifyMap( InputArray K, InputArray D, InputArray R, InputArray P,
    const cv::Size& size, int m1type, OutputArray map1, OutputArray map2 )
{
    map1.create( size, CV_16SC2);  //创建2通道的map1矩阵
    map2.create( size, CV_16UC1);  //创建1通道的map2矩阵

    cv::Vec2d f, c; //创建f、c两个2维向量,用于存储内参fx、fy及cx、cy

    Matx33d camMat = K.getMat(); //把内参矩阵K复制给camMat变量
    f = Vec2d(camMat(0, 0), camMat(1, 1)); //二维向量f存储fx、fy
    c = Vec2d(camMat(0, 2), camMat(1, 2)); //二维向量f存储cx、cy

    Vec4d k = Vec4d::all(0); //创建4维向量k,用于存储畸变系数k1、k2、k3、k4
    k = (Vec4d)*D.getMat().ptr<Vec4f>(); //把畸变系数k1、k2、k3、k4复制给向量k

    cv::Matx33d RR  = cv::Matx33d::eye(); //在单目鱼眼相机的矫正中,RR设为单位向量即可

    cv::Matx33d PP = cv::Matx33d::eye(); //创建矩阵PP,用于存储参数P
    P.getMat().colRange(0, 3).convertTo(PP, CV_64F);  //把P复制给PP
    //其中,P是我们想使矫正后图片看起来像内参为怎样的相机得到的,就把P设为这个内参。
    //一般令P等于原鱼眼相机的内参

    cv::Matx33d iR = (PP * RR).inv(cv::DECOMP_SVD); //令iR为P的逆,也就是内参矩阵的逆

//==================================两层for循环更新 i 和 j =======================================

/************************************************************************************************
* i和j的实际意义是:
* (j, i)是矫正后图像(输出图像)的像素点坐标,根据(j, i)计算出它对应的畸变图中的像素点坐标(u, v)
************************************************************************************************/

    for( int i = 0; i < size.height; ++i)
    {
        float* m1f = map1.getMat().ptr<float>(i);
        float* m2f = map2.getMat().ptr<float>(i);
        short*  m1 = (short*)m1f; //令m1为指向map1每个元素的指针,初始m1为&map1(0,0)[0],加1后为&map1(0,0)[1],再加1后为&map1(0,1)[0],再加1后为&map1(0,1)[1]……
        ushort* m2 = (ushort*)m2f; //令m2为指向map2每个元素的指针,map2为一维矩阵

		//计算相机坐标系下的坐标
        double _x = i*iR(0, 1) + iR(0, 2),
               _y = i*iR(1, 1) + iR(1, 2),
               _w = i*iR(2, 1) + iR(2, 2);

        for( int j = 0; j < size.width; ++j)
        {
        	//(i, j)是无畸变图上的像素点坐标
//===========================================矫正过程=============================================
            double x = _x/_w, y = _y/_w;

			//计算无畸变情况下的r、theta
            double r = sqrt(x*x + y*y);
            double theta = atan(r);

            double theta2 = theta*theta, theta4 = theta2*theta2, theta6 = theta4*theta2, theta8 = theta4*theta4;
          	//计算畸变情况下的theta_d
            double theta_d = theta * (1 + k[0]*theta2 + k[1]*theta4 + k[2]*theta6 + k[3]*theta8);

			//计算畸变因子scale
            double scale = (r == 0) ? 1.0 : theta_d / r;
            //计算无畸变图上的点(i, j)对应的畸变图上的像素点(u, v)
            double u = f[0]*x*scale + c[0];
            double v = f[1]*y*scale + c[1];

			//将(u, v)转化为整数,并存入map1
            m1[j*2+0] = (int)(u);
            m1[j*2+1] = (int)(v);	
			
			//将插值所用的数据存入map2
			//插值只是为了让图像更清晰一些,作用不大,不作讲解
            m2[j] = (ushort)((iv & (cv::INTER_TAB_SIZE-1))*cv::INTER_TAB_SIZE + (iu & (cv::INTER_TAB_SIZE-1)));

			//计算相机坐标系下的坐标
            _x += iR(0, 0);
            _y += iR(1, 0);
            _w += iR(2, 0);
        }
    }
}

注: 在上述程序中,由于i、j意义的不同,像素坐标的表示方法不是(i, j),而应该是(j, i),即(width, height),符合像素坐标系,不需过分在意。

因此,上述程序中由(j, i )计算得到的(u, v)的意义应该是:

畸变图中坐标为(u, v)的像素点,在无畸变图中应该处于(j, i)位置。

也就是说,上述程序的作用是,从无畸变图的像素坐标(j, i),反推出了它在畸变图中对应的像素坐标(u, v)

其中,比较难理解的是两个for循环,以及_x、_y、_w的计算,我们把这一部分代码单独摘出来:

    for( int i = 0; i < size.height; ++i)
    {
        double _x = i*iR(0, 1) + iR(0, 2),
               _y = i*iR(1, 1) + iR(1, 2),
               _w = i*iR(2, 1) + iR(2, 2);
               
        for( int j = 0; j < size.width; ++j)
        {
        	doSomeThingsForAectify();
        	
        	_x += iR(0, 0);
            _y += iR(1, 0);
            _w += iR(2, 0);
        }
    }

上面的程序,可如下书写:

	double _x, _y, _w;
           
    for( int i = 0; i < size.height; ++i)    
        for( int j = 0; j < size.width; ++j)
        {
            _x = i*iR(0, 1) + iR(0, 2) + j * iR(0, 0);
            _y = i*iR(1, 1) + iR(1, 2) + j * iR(1, 0);
            _w = i*iR(2, 1) + iR(2, 2) + j * iR(2, 0);
            
        	doSomeThingsForAectify();
        }

观察上面的程序,不难看出,_x、_y、_w其实就是矩阵iR齐次坐标(j, i, 1)的乘积,即如下所示:

而在程序注释中我们已说明,矩阵iR相机内参的逆。而相机内参的作用是:缩放因子 x 像素坐标 = 相机内参 x 相机坐标系坐标,如下(注: 根据下图可以看出,缩放因子其实就是相机坐标系下的Z坐标):

(下面把相机坐标系下的坐标简称为相机坐标

那么,把内参矩阵求逆后得到iR,再与(j, i)相乘,得到的其实就是归一化后的相机坐标

结论:

[_x, _y, _w] 实际上就是 [Xc / Zc, Yc / Zc, 1] !

而由于我们假设(j, i)是矫正后的、无畸变图像中的像素点坐标,那么根据相机内参的逆反推回去的归一化后的相机坐标,实际上就是无畸变情况下的归一化后的相机坐标!

也就是说,求**[_x, _y, _w],实际上就是在求无畸变情况下的归一化相机坐标**。

2. 更进一步

更进一步来说,鱼眼畸变发生在相机坐标 -> 像素坐标的过程,而现实世界中的一个点,无论怎样,它的相机坐标是不会发生畸变的,只有像素坐标会发生畸变。

因此,在上面我们说 [_x, _y, _w]无畸变情况下的归一化相机坐标,是不太准确的,更准确的说法应该是:

归一化相机坐标 [_x, _y, _w] 处的点,在无畸变镜头的投影作用下,对应的像素坐标应该是程序中的 (j, i) ;而由于鱼眼镜头有畸变,本应该在 (j, i) 处的点,现在跑到了 (u, v) 处!

3. 如何由 (j, i) 算出 (u, v) ?

那么程序是怎样由(j, i)计算得到(u, v)的呢?OpenCV官方给出了公式:

其中,第一行的x、y、z即程序中的_x、_y、_w

公式摆在这里,但原理如何,OpenCV却没有详细给出。博主大致研究了其原理,不知正确与否,将在下一篇博文给出。

  • 28
    点赞
  • 82
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
OpenCV C++ 中,可以使用 `cv::fisheye::stereoRectify()` 函数进行鱼眼相机的双目矫正,其函数原型如下: ```cpp void cv::fisheye::stereoRectify( InputArray K1, InputArray D1, InputArray K2, InputArray D2, Size imageSize, InputArray R, InputArray t, OutputArray R1, OutputArray R2, OutputArray P1, OutputArray P2, OutputArray Q, int flags = cv::CALIB_ZERO_DISPARITY, Size newImageSize = Size() ); ``` 其中各参数的含义如下: - `K1`:左相机的内参矩阵。 - `D1`:左相机的畸变参数。 - `K2`:右相机的内参矩阵。 - `D2`:右相机的畸变参数。 - `imageSize`:图像的分辨率大小。 - `R`:左相机到右相机的旋转矩阵。 - `t`:左相机到右相机的平移向量。 - `R1`:输出参数,左相机矫正旋转矩阵。 - `R2`:输出参数,右相机矫正旋转矩阵。 - `P1`:输出参数,左相机投影矩阵。 - `P2`:输出参数,右相机投影矩阵。 - `Q`:输出参数,重投影矩阵。 - `flags`:标志位,可选参数,默认值为 `cv::CALIB_ZERO_DISPARITY`。 - `newImageSize`:新图像的分辨率大小,可选参数,默认值为空 `Size()`。 下面是一个示例代码: ```cpp cv::Mat K1, D1, K2, D2, R, T; // 填充内参矩阵、畸变参数、旋转矩阵和平移向量 cv::Size imageSize; // 填充图像分辨率大小 cv::Mat R1, R2, P1, P2, Q; cv::fisheye::stereoRectify( K1, D1, K2, D2, imageSize, R, T, R1, R2, P1, P2, Q, cv::CALIB_ZERO_DISPARITY ); // 输出矫正后的参数 std::cout << "R1: " << R1 << std::endl; std::cout << "R2: " << R2 << std::endl; std::cout << "P1: " << P1 << std::endl; std::cout << "P2: " << P2 << std::endl; std::cout << "Q: " << Q << std::endl; ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值