当前位置:首页 > 新型工业化 >鱼眼相机畸变校正(鱼眼图像去畸变)

鱼眼相机畸变校正(鱼眼图像去畸变)

前言

在过去一年的AVM算法开发工作中,我基本走遍了鱼眼相机畸变去除的方法。从一开始调用Opencv API,到后来需要自己实现算法,正向undis2fish、反向fish2undis、鱼眼检测、undis标定。总之遇到了很多坑,不过好在都解决了。就在最近,有同学在AVM帖子下询问这个东西的实现。今天我就在这里讨论一下。本篇文章从鱼眼相机模型开始,包括Opencv API参数调整、基于畸变表的参数拟合、鱼眼相机去畸变算法原理和C++实现。

1. 鱼眼相机基础

鱼眼相机畸变校正(鱼眼图像去畸变)

1.1 鱼眼相机模型

如果通过鱼眼相机模型的P 点的入射光没有镜头,则应在相机成像平面的e 点相交。然而,经过鱼眼相机折射后,光线会在相机成像平面的d点相交,从而产生畸变。因此,畸变图像整体呈现出像素向图像中心点聚集的趋势。去畸变是将折射到d点的点重新映射回e点。因此,与原始鱼眼图像相比,去畸变后的图像似乎是向心聚集的像素再次向周围扩散。下表两张图是鱼眼图和去畸变后的展开图: 鱼眼相机的投影方式有多种假设,如等距投影、等立体角投影、正交投影、立体投影、线性投影投影。但真正的鱼眼相机镜头并不完全遵循这些模型假设。因此Kannala-Brandt提出了一种适用于不同类型鱼眼相机的通用估计形式:这也被纳入opencv中的鱼眼相机畸变模型中。现在基本上默认的鱼眼相机模型都遵循上述公式。式中为光线的入射角,r()为上图中od的长度。

1.2 基于畸变表的拟合方法

每个相机都有其固定的相机参数,包括内部参数和畸变系数。这些相机参数可以通过特定的相机标定方法获得。通常,我们可以使用相机对着标定板在不同位置和角度拍摄数十张照片,然后使用一些优化方法来计算相机参数的最优解,例如张正友的棋盘标定方法。然而,基于标定板方法的标定结果取决于光照是否充足、图像序列是否充足、整个标定板序列是否能够填充整个图像格式等因素。也就是说,汽车标定的过程需要手动放置标定板。指望算法工程师将生产线上的工人培训得像他们一样专业,显然是不现实的。大多数标定车间,车辆行驶到标定场中间,十几秒就标定好AVM系统所需的参数。它们主要是四台鱼眼相机的外部参数,而不是相机本身的参数(内部参数、畸变系数)。完成一辆汽车大约需要几十秒,并且不可能手动校准相机的内部参数。幸运的是,制造商通常会在相机出厂时提供相机的必要参数。内参{"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模型与畸变表之间的差异为:opencv Kannala-Brandt相机畸变模型描述的是光线的入射角与其经过折射后在相机归一化平面上的投影点距离归一化平面中心的距离r畸变表描述的是光线的入射角与其经过折射后在相机的真实的成像平面上的投影点距离成像中心的距离r两者之间相差一个系数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/180 theta_fit=np.arctan(self.data[:,1]/0.95)#focal_lenth=0.95 distort_data,_=curve_fit(func1,theta_input,theta_fit) 综上,我们通过曲线拟合的方法得到了畸变参数。

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

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

2.1 基础鱼眼图像去畸变

其中入参K为鱼眼相机内参,D为,,, 畸变参数,R我们一般设置为单位阵,P为去畸变图像的相机内参,size为输出图像的大小;map1,map2为输出的映射图。@paramKCameraintrinsicmatrixf$cameramatrix{K}f$. @paramDInputvectorofdistortioncoefficientsf$distcoeffsfisheyef$. @paramRRectificationtransformationintheobjectspace:3x31-channel,orvector:3x1/1x3 1-channelor1x13-channel @paramPNewcameraintrinsicmatrix(3x3)ornewprojectionmatrix(3x4) @paramsizeUndistortedimagesize. @paramm1typeTypeofthefirstoutputmapthatcanbeCV_32FC1orCV_16SC2.SeeconvertMaps() fordetails. @parammap1Thefirstoutputmap. @parammap2Thesecondoutputmap. */ CV_EXPORTS_WvoidinitUndistortRectifyMap(InputArrayK,InputArrayD,InputArrayR,InputArrayP, constcv::Sizesize,intm1type,OutputArraymap1,OutputArraymap2); 相机内参矩阵表示如下,其中 表示相机焦距 f 与相机cmos参数 的比值,这个 的物理意义为每个像素的实际长度,单位可以是mm/像素。 表示相机主点,即光心与图像平面相交的坐标,单位为像素。那么问题来了,为什么既需要鱼眼相机的内参,又需要输出图像的相机内参呢,它们之间是什么关系呢?最开始的时候,很多同学肯定是把这两个相机内参设置成一样的,即都设置成鱼眼相机的大小,如下图所示。代码中去畸变之后图像的内参是从鱼眼相机内参深拷贝过来的。cv::MatR=cv::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::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::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::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::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::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::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::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 鱼眼去畸变算法及其实现

0 10-59000

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

畸变映射关系鱼眼去畸变的算法实现就是遍历去畸变图像上的每一个点,寻找它们在鱼眼图像上的像素点坐标,计算它们之间的映射关系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;//normalizedplane floatx_=(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_);//theta floatangle_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); //scale floatscale=angle_distorted/(r_+0.00001f);//scale=r_disonthecameraimgplane //divider_undisonthenormalizedplane cv::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::Vec2fwarp_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.2 进阶的 鱼眼去畸变(如何调整f)

正如第2 节中提到的,我们需要在小图像上呈现大方形网格。这需要调整f。这个过程并不容易理解。我们画个图来理解一下:相机调焦图。上图中,相机的真实焦距为f。假设cmos长度不变,我们就把成像平面放在f处。在/2的位置,可以明显看出调整焦距后的相机FOV更大,能看到的东西也更多。同理,对于标定车间的大方格,假设我们调整参数,使, 减小一定倍数,理论上我们可以看到更多的内容。将相机内在参数f 降低到f/2 意味着我们从相机的光学中心推导出相机在f/2 处的cmos,并将其映射到该平面上。算法流程如下:将相机焦距调整为f/2后,利用新的焦距将点转换到归一化平面,并获得去畸变参数,计算其在归一化平面上的位置扭曲状态。前两步的结果,计算去畸变前后线段的长度比尺度。根据已知和先前计算的比例计算*2。将点映射到f平面以获得f/2成像平面上的去畸变映射。关系。

上面的最后一步不太好理解:实际真实的畸变图像是在f平面上。也就是说,我们仅仅计算f/2平面上畸变图上的点到去畸变后位置的映射是不够的。我们还需要进一步找到真实的畸变图上的坐标就是f平面上的位置,因为我们最终会去

真正的畸变图上找点的, f/2 平面只是我们虚构的,只是假设cmos在 f/2 平面上。这个图中我们最后一步×2,假设我们想把相机内参设为 f/3 ,那最后一步要×3。总之一句话:f平面才是我们真实拿到的fish图,我们最终还是要在这个原始的fish图上找点。在实验中我们得到结论:当我们调整 f 使其更小时,相同的内容集中于更小的分辨率上,对于后续的图像处理算法会更友好。很多锯齿和模糊的问题都能得到改善在鱼眼上检测棋盘格角点要比在去畸变图上检测棋盘格角点更加准确,因为去畸变后大方格被拉伸的很严重。这个结论的依据是重投影误差以及将鱼眼检测到的角点坐标映射到去畸变图上后与直接在畸变图上的位置做视觉上的比较。

3.3 Opencv API undistortPoints的实现

前面所有讨论的都是undis2fish的过程。在实际的AVM标定中,通常是对鱼眼相机检测角点,因为去畸变之后图像拉伸效果严重,提取的角点不准确。参考张正友标定法标定相机参数时,也是在获取到的图像上直接提取角点,解一个全局优化问题。因此,除了前面讲到的undis2fish映射过程以外,我们还需要实现fish2undis的过程。这个过程Opencv提供了函数undistortPoints,即输入为鱼眼相机上点的坐标,输出为去畸变图像上点的坐标。这个过程是一个解方程的问题,用到非线性优化,速度很慢。因此我们通过畸变表,构建了一个多项式,通过反向拟合的方法,提前拟合出fish2undis的方程系数:#forward self.distor_para,_=curve_fit(self.func,self.data[:,0],self.data[:,1]) #inverse f_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;//normalizedplane floatx_=(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); //scale floatr_undistorted=tanf(angle_undistorted); floatscale=r_undistorted/(r_distorted+0.00001f);//scale=r_disonthecameraimgplane //divider_undisonthenormalizedplane cv::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::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供应商的鱼眼畸变参数并不一定是依赖畸变表,有的也会拿来一批摄像头自行标定。具体那种方法更优,可能需要更多同行同学的实验和讨论得到结论。 审核编辑 :李倩

最新资讯

推荐资讯