当前位置: 首页 > 聚焦 > > 内容页

【光电智造】AVM环视系统:鱼眼相机去畸变算法及实战-世界新要闻

时间:2023-04-24 11:59:59 来源:面包芯语 分享至:

{ "focal_length": 950, "dx": 3, "dy": 3, "cx": 640, "cy": 480 },

通过这些参数可以计算出内参矩阵:


(相关资料图)

"intrinsic" : [ 316.66, 0.0, 640, 0.0, 316.66, 480, 0.0, 0.0, 1.0 ]

内参计算公式:

中间矩阵为内参计算公式
畸变表

通过畸变表可以得到畸变前后像素坐标的映射关系:其中angle表示光线的入射角,Real_Image_Height表示入射光线经过鱼眼相机透镜折射(出射角为)与成像平面的交点(畸变点)。

opencv Kannala-Brandt模型与畸变表之间的差异为:

两者之间相差一个系数focal_length。即,如果使用畸变表拟合opencv Kannala-Brandt数学公式中的畸变参数,必须已知相机焦距focal_length,注意:这个focal_length是实际的物理概念,正儿八经的相机焦距,而不是相机内参矩阵中的f/dx。

寻找与r的关系,是一种曲线拟合的问题。畸变表中提供了数据和 r() ,拟合的多项式为:

具体的实现方法可以使用python的curve_fit函数,即可拟合出合适的k0,k1,k2,k3,k4系数。上面提到opencv Kannala-Brandt与厂家给的畸变表之间相差一个系数:focal_length,因此,在做曲线拟合的时候,要把这部分考虑进去:

theta_input=data[:,0]*3.14/180theta_fit=np.arctan(self.data[:,1]/0.95)#focal_lenth=0.95distort_data,_=curve_fit(func1,theta_input,theta_fit)

综上,我们通过曲线拟合的方法得到了畸变参数。

2 Opencv API 鱼眼图像去畸变方法

Opencv提供了基于Kannala-Brandt数学模型的鱼眼去畸变方法:cv::fisheye::initUndistortRectifyMap,该函数使用相机的内参和畸变参数计算出映射图mapx和mapy。

2.1 基础鱼眼图像去畸变

其中入参K为鱼眼相机内参,D为,,,畸变参数,R我们一般设置为单位阵,P为去畸变图像的相机内参,size为输出图像的大小;map1,map2为输出的映射图。

@paramKCameraintrinsicmatrix\f$cameramatrix{K}\f$.@paramDInputvectorofdistortioncoefficients\f$\distcoeffsfisheye\f$.@paramRRectificationtransformationintheobjectspace:3x31-channel,orvector:3x1/1x31-channelor1x13-channel@paramPNewcameraintrinsicmatrix(3x3)ornewprojectionmatrix(3x4)@paramsizeUndistortedimagesize.@paramm1typeTypeofthefirstoutputmapthatcanbeCV_32FC1orCV_16SC2.SeeconvertMaps()fordetails.@parammap1Thefirstoutputmap.@parammap2Thesecondoutputmap.*/CV_EXPORTS_WvoidinitUndistortRectifyMap(InputArrayK,InputArrayD,InputArrayR,InputArrayP,constcv::Size&size,intm1type,OutputArraymap1,OutputArraymap2);

相机内参矩阵表示如下,其中表示相机焦距 f 与相机cmos参数的比值,这个的物理意义为每个像素的实际长度,单位可以是mm/像素。表示相机主点,即光心与图像平面相交的坐标,单位为像素。

那么问题来了,为什么既需要鱼眼相机的内参,又需要输出图像的相机内参呢,它们之间是什么关系呢?最开始的时候,很多同学肯定是把这两个相机内参设置成一样的,即都设置成鱼眼相机的大小,如下图所示。代码中去畸变之后图像的内参是从鱼眼相机内参深拷贝过来的。

cv::MatR=cv::Mat::eye(3,3,CV_32F);cv::Matmapx_open,mapy_open;cv::Matintrinsic_undis;fish_intrinsic.copyTo(intrinsic_undis);//intrinsic_undis.at(0,2)*=2;//intrinsic_undis.at(1,2)*=2;cv::fisheye::initUndistortRectifyMap(fish_intrinsic,m_undis2fish_params,R,intrinsic_undis,cv::Size(intrinsic_undis.at(0,2)*2,intrinsic_undis.at(1,2)*2),CV_32FC1,mapx_open,mapy_open);cv::Mattest;cv::remap(disImg[3],test,mapx_open,mapy_open,cv::INTER_LINEAR);
左侧为鱼眼图,右侧为去畸变图

2.2 相机主点参数调节

我们发现,上图中右侧去畸变之后虽然图像幅面大小与鱼眼图相同都是1280*960,但是可视范围变得很小。标定所需要的大方格没有包含进来。因此,需要进一步调参,下面代码中将去畸变之后图像相机参数中的主点,扩大为原来的两倍,且initUndistortRectifyMap函数输出的去畸变图像大小size是与去畸变之后图像相机参数主点相关的,也就是图像大小同样跟着放大了两倍。记住一点:initUndistortRectifyMap函数中的size参数一般都是与去畸变之后图像的相机参数中主点大小强相关的。这一点在后面C++代码手撕算法流程时候会提到。

cv::MatR=cv::Mat::eye(3,3,CV_32F);cv::Matmapx_open,mapy_open;cv::Matintrinsic_undis;fish_intrinsic.copyTo(intrinsic_undis);intrinsic_undis.at(0,2)*=2;intrinsic_undis.at(1,2)*=2;cv::fisheye::initUndistortRectifyMap(fish_intrinsic,m_undis2fish_params,R,intrinsic_undis,cv::Size(intrinsic_undis.at(0,2)*2,intrinsic_undis.at(1,2)*2),CV_32FC1,mapx_open,mapy_open);cv::Mattest;cv::remap(disImg[3],test,mapx_open,mapy_open,cv::INTER_LINEAR);
去畸变图像相机参数的主点扩大了两倍,同时生成图像大小扩到两倍

从上图中我们依然不能获得到右侧完整的黑色大方格,因此需要进一步扩大去畸变后图像相机主点位置以及生成图像的分辨率:

cv::MatR=cv::Mat::eye(3,3,CV_32F);cv::Matmapx_open,mapy_open;cv::Matintrinsic_undis;fish_intrinsic.copyTo(intrinsic_undis);intrinsic_undis.at(0,2)*=4;intrinsic_undis.at(1,2)*=4;cv::fisheye::initUndistortRectifyMap(fish_intrinsic,m_undis2fish_params,R,intrinsic_undis,cv::Size(intrinsic_undis.at(0,2)*2,intrinsic_undis.at(1,2)*2),CV_32FC1,mapx_open,mapy_open);cv::Mattest;cv::remap(disImg[3],test,mapx_open,mapy_open,cv::INTER_LINEAR);

现在我已经把去畸变图像相机内参的主点扩大为fish相机内参的4倍了,生成图像的长宽也放大了4倍,像素数量总体放大16倍,这样才勉强把大方格完全显示出来。我们知道提取角点需要用到图像处理算法,显然对这么大的图像做处理的效率非常低。

2.3 相机f参数调节

到目前位置,我们只讨论了相机参数中主点的调参,想要解决上述问题还需要调整相机的,先不说理论,直接看调参结果,这里我们代码中只调整了去畸变图像相机参数中的,使它们缩小为原来的1/4。

cv::MatR=cv::Mat::eye(3,3,CV_32F);cv::Matmapx_open,mapy_open;cv::Matintrinsic_undis;fish_intrinsic.copyTo(intrinsic_undis);intrinsic_undis.at(0,0)/=4;intrinsic_undis.at(1,1)/=4;/*intrinsic_undis.at(0,2)*=4;intrinsic_undis.at(1,2)*=4;*/cv::fisheye::initUndistortRectifyMap(fish_intrinsic,m_undis2fish_params,R,intrinsic_undis,cv::Size(intrinsic_undis.at(0,2)*2,intrinsic_undis.at(1,2)*2),CV_32FC1,mapx_open,mapy_open);cv::Mattest;cv::remap(disImg[3],test,mapx_open,mapy_open,cv::INTER_LINEAR);
左侧为鱼眼图,右侧为去畸变图,分辨率均为1280*960

从图中可以看出,当我们仅将相机焦距缩小时,可以看到更多的东西。虽然去畸变之后的图像很小只有1280*960,但是却可以看到完整的方格。

本节我们讨论了opencv API initUndistortRectifyMap函数的主点和f参数调节对于去畸变图像的影响,接下来的第3节,我们将会从去畸变算法原理入手,C++实现一波该算法。做这件事的原因很简单:opencv只提供了整张图像从undis2fish的映射,在avm的视角转换中,我们需要进行单个像素点的undis2fish,因此,我们需要自己实现一波这个去畸变过程。

结论:缩小相机焦距可以使FOV增大,在更小分辨率的图像上呈现出更多的内容,看上去也是更加清晰。

3 鱼眼去畸变算法及其实现

畸变映射关系

鱼眼去畸变的算法实现就是遍历去畸变图像上的每一个点,寻找它们在鱼眼图像上的像素点坐标,计算它们之间的映射关系

C++实现:

/*func:warpfromdistorttoundistort@paramf_dx:f/dx@paramf_dy:f/dy@paramlarge_center_h:undisimagecentery@paramlarge_center_w:undisimagecenterx@paramfish_center_h:fishimagecentery@paramfish_center_w:fishimagecenterx@paramundis_param:factoryparam@paramx:inputcoordinatexontheundisimage@paramy:inputcoordinateyontheundisimage*/cv::Vec2fwarpUndist2Fisheye(floatfish_scale,floatf_dx,floatf_dy,floatlarge_center_h,floatlarge_center_w,floatfish_center_h,floatfish_center_w,cv::Vec4dundis_param,floatx,floaty){f_dx*=fish_scale;f_dy*=fish_scale;floaty_=(y-large_center_h)/f_dy;//normalizedplanefloatx_=(x-large_center_w)/f_dx;floatr_=static_cast(sqrt(pow(x_,2)+pow(y_,2)));//Lookuptable/*intnum=atan(r_)/atan(m_d)*1024;floatangle_distorted=m_Lut[num];*/floatangle_undistorted=atan(r_);//thetafloatangle_undistorted_p2=angle_undistorted*angle_undistorted;floatangle_undistorted_p3=angle_undistorted_p2*angle_undistorted;floatangle_undistorted_p5=angle_undistorted_p2*angle_undistorted_p3;floatangle_undistorted_p7=angle_undistorted_p2*angle_undistorted_p5;floatangle_undistorted_p9=angle_undistorted_p2*angle_undistorted_p7;floatangle_distorted=static_cast(angle_undistorted+undis_param[0]*angle_undistorted_p3+undis_param[1]*angle_undistorted_p5+undis_param[2]*angle_undistorted_p7+undis_param[3]*angle_undistorted_p9);//scalefloatscale=angle_distorted/(r_+0.00001f);//scale=r_disonthecameraimgplane//divider_undisonthenormalizedplanecv::Vec2fwarp_xy;floatxx=(x-large_center_w)/fish_scale;floatyy=(y-large_center_h)/fish_scale;warpPointOpencv(warp_xy,fish_center_h,fish_center_w,xx,yy,scale);returnwarp_xy;}voidwarpPointOpencv(cv::Vec2f&warp_xy,floatmap_center_h,floatmap_center_w,floatx_,floaty_,floatscale){warp_xy[0]=x_*scale+map_center_w;warp_xy[1]=y_*scale+map_center_h;}

针对上述代码,我们由浅入深地讲述算法流程

3.1 基础的鱼眼去畸变(主点相关)

鱼眼相机成像模型

上述代码中令fish_scale为1,先讨论最简单的,即让去畸变图像相机参数中的,大小与鱼眼图相同,对照鱼眼相机模型这张图和代码,我们来梳理一下算法流程:

算法流程

对于算法流程中的3,4:

总体来讲这个基础的鱼眼去畸变算法的实现思路就是:在归一化平面上计算去畸变前后的像素坐标scale,然后运用到图像平面上。

主点位置示意图

如上图所示,输出的去畸变图大小为Size,去畸变图相机内参中的主点位置为,在算法的实现中,首先会创建Size大小的mesh_grid,即map,确定主点的位置,然后根据mesh_grid上每个点的坐标,计算其与主点的相对位置,然后进行后续的计算(转换到归一化平面、计算scale等),因此根据这个逻辑如果上面的主点不在Size的中心,就会导致相机实际拍摄到内容的中心在主点区域,但是右下角会有很多的延申。例如下图这种:

从上图原始的鱼眼图中可以看出相机拍摄的内容中心大概在棋盘格附近,然而去畸变了之后棋盘格却跑到了左上角。这就是因为我们设置的主点偏左上,没有位于生成的去畸变图的中心。这就是2.2节中提到的:initUndistortRectifyMap函数中的size参数一般都是与去畸变之后图像的相机参数中主点大小强相关的。

3.2 进阶的 鱼眼去畸变(如何调整f)

正如第2节所说,我们需要在很小的图像上呈现出大方格。这就需要调整f,这个过程不太容易理解,我们画个图来理解一下:

相机焦距调整示意图

上图中相机的真实焦距为f,假设cmos长度不变,我们只是把成像平面放在了 f/2 的位置上,显然调整焦距后的相机FOV更大,能够看到的东西越多。同理,对于标定车间中的大方格,假设我们调参使得,缩小一定的倍数,理论上就可以看到更多的内容。

将相机内参 f 缩小为 f/2 意味着我们将相机的cmos推导距离相机光心 f/2 处,在这个平面上做映射。算法流程如下:

上述最后一步不太容易理解:实际真实的畸变图像是在 f 平面上,也就是说我们算出了 f/2 平面上畸变图上点映射到去畸变后的位置还不够,还需要进一步找到真实的畸变图上的坐标,也就是 f 平面上的位置,因为我们最后都是要去真正的畸变图上找点的, f/2 平面只是我们虚构的,只是假设cmos在 f/2 平面上。这个图中我们最后一步×2,假设我们想把相机内参设为 f/3 ,那最后一步要×3。总之一句话:f平面才是我们真实拿到的fish图,我们最终还是要在这个原始的fish图上找点。

在实验中我们得到结论:

3.3 Opencv API undistortPoints的实现

前面所有讨论的都是undis2fish的过程。在实际的AVM标定中,通常是对鱼眼相机检测角点,因为去畸变之后图像拉伸效果严重,提取的角点不准确。参考张正友标定法标定相机参数时,也是在获取到的图像上直接提取角点,解一个全局优化问题。

因此,除了前面讲到的undis2fish映射过程以外,我们还需要实现fish2undis的过程。这个过程Opencv提供了函数undistortPoints,即输入为鱼眼相机上点的坐标,输出为去畸变图像上点的坐标。这个过程是一个解方程的问题,用到非线性优化,速度很慢。因此我们通过畸变表,构建了一个多项式,通过反向拟合的方法,提前拟合出fish2undis的方程系数:

#forwardself.distor_para,_=curve_fit(self.func,self.data[:,0],self.data[:,1])#inversef_inverse_para,_=curve_fit(self.func_inverse,self.data[:,1],self.data[:,0])

计算fish2undis的过程与undis2fish(3.1,3.2)的过程略有不同,但都是寻找之间的映射关系,因为 f 平面才是我们真实拿到的fish图,我们最终还是要在这个原始的fish图上找点。

实现代码:

cv::Vec2fCalibrateInit::warpFisheye2Undist(floatfish_scale,floatf_dx,floatf_dy,floatundis_center_h,floatundis_center_w,floatfish_center_h,floatfish_center_w,cv::Vec4dundis_param,floatx,floaty){//f_dx*=fish_scale;//f_dy*=fish_scale;floaty_=(y-fish_center_h)/f_dy;//normalizedplanefloatx_=(x-fish_center_w)/f_dx;floatr_distorted=static_cast(sqrt(pow(x_,2)+pow(y_,2)));floatr_distorted_p2=r_distorted*r_distorted;floatr_distorted_p3=r_distorted_p2*r_distorted;floatr_distorted_p4=r_distorted_p2*r_distorted_p2;floatr_distorted_p5=r_distorted_p2*r_distorted_p3;floatangle_undistorted=static_cast(r_distorted+undis_param[0]*r_distorted_p2+undis_param[1]*r_distorted_p3+undis_param[2]*r_distorted_p4+undis_param[3]*r_distorted_p5);//scalefloatr_undistorted=tanf(angle_undistorted);floatscale=r_undistorted/(r_distorted+0.00001f);//scale=r_disonthecameraimgplane//divider_undisonthenormalizedplanecv::Vec2fwarp_xy;floatxx=(x-fish_center_w)*fish_scale;floatyy=(y-fish_center_h)*fish_scale;warpPointInverse(warp_xy,undis_center_h,undis_center_w,xx,yy,scale);returnwarp_xy;}voidCalibrateInit::warpPointInverse(cv::Vec2f&warp_xy,floatmap_center_h,floatmap_center_w,floatx_,floaty_,floatscale){warp_xy[0]=x_*scale+map_center_w;warp_xy[1]=y_*scale+map_center_h;}

总结

本贴讨论的内容为鱼眼相机图像基于畸变表的处理方法,AVM中畸变的运用非常灵活,所以笔者必须对它进行实现才可以灵活运用。据笔者所知有些AVM供应商的鱼眼畸变参数并不一定是依赖畸变表,有的也会拿来一批摄像头自行标定。具体那种方法更优,可能需要更多同行同学的实验和讨论得到结论。

----与智者为伍为创新赋能----

联系邮箱:uestcwxd@126.com

QQ:493826566

标签:

Copyright ©  2015-2022 北冰洋财富网版权所有  备案号:沪ICP备2020036824号-3   联系邮箱:562 66 29@qq.com