【图像】【OpenCV鱼眼矫正】二、fisheye::initUndistortRectifyMap()源码分析
目录
- 一、fisheye::initUndistortRectifyMap() 之 功能介绍
- 二、fisheye::initUndistortRectifyMap() 之 源码分析
- 1. 源码分析
- 2. 更进一步
- 3. 如何由 (j, i) 算出 (u, v) ?
一、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]
。
那么,i
和j
、u
和v
的含义是:
畸变图中坐标为
(u, v)
的像素点,在无畸变图中应该处于(i, j)
位置。
这样,把畸变图(u, v)处的像素,复制到(i, j)处,就得到了矫正后的图像。
即如下图所示:
而此时的map2是为了做插值、让矫正后图像更清晰而用,本文不做介绍。
注:
若把参数m1type
设为CV_32FC1,则此时map1不再是二通道矩阵,而是一通道矩阵。
此时map1保存m1type
为CV_16SC2时map1的0通道,map2保存m1type
为CV_16SC2时map1的1通道。
即,此时令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、cyMatx33d camMat = K.getMat(); //把内参矩阵K复制给camMat变量f = Vec2d(camMat(0, 0), camMat(1, 1)); //二维向量f存储fx、fyc = Vec2d(camMat(0, 2), camMat(1, 2)); //二维向量f存储cx、cyVec4d k = Vec4d::all(0); //创建4维向量k,用于存储畸变系数k1、k2、k3、k4k = (Vec4d)*D.getMat().ptr<Vec4f>(); //把畸变系数k1、k2、k3、k4复制给向量kcv::Matx33d RR = cv::Matx33d::eye(); //在单目鱼眼相机的矫正中,RR设为单位向量即可cv::Matx33d PP = cv::Matx33d::eye(); //创建矩阵PP,用于存储参数PP.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、thetadouble r = sqrt(x*x + y*y);double theta = atan(r);double theta2 = theta*theta, theta4 = theta2*theta2, theta6 = theta4*theta2, theta8 = theta4*theta4;//计算畸变情况下的theta_ddouble theta_d = theta * (1 + k[0]*theta2 + k[1]*theta4 + k[2]*theta6 + k[3]*theta8);//计算畸变因子scaledouble 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)转化为整数,并存入map1m1[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却没有详细给出。博主大致研究了其原理,不知正确与否,将在下一篇博文给出。
【图像】【OpenCV鱼眼矫正】二、fisheye::initUndistortRectifyMap()源码分析相关推荐
- C#调用obs studio 二次开发 源码分析 编译
C#二次开发obs studio obs studio二次开发视频教程,录制.推流.调整分辨率.调整位置.画面回调.推流回调等功能 obs二次开发还是比较繁琐的,我在学习的时候也是很痛苦,有需要的朋友 ...
- Java源码详解二:HashMap源码分析--openjdk java 11源码
文章目录 HashMap.java介绍 1.HashMap的get和put操作平均时间复杂度和最坏时间复杂度 2.为什么链表长度超过8才转换为红黑树 3.红黑树中的节点如何排序 本系列是Java详解, ...
- AXI_lite代码简解(二)-AXI-Lite 源码分析
AXI-Lite 源码分析 对于使用AXI总线,最开始肯定要了解顶层接口定义,这样才能针对顶层接口进行调用和例化,打开axi_lite_v1_0.v文件,第一段就是顶层的接口定义: 代码4 1 a ...
- Linux驱动学习--HDMI开发(二)HDMI驱动源码分析(RK平台)
目录 一.引言 二.驱动框架 ------> dts节点 ------> HDMI DDC 驱动 ------> HDMI HDCP驱动 ------> HDMI CEC驱动 ...
- LLVM每日谈之十二 LLVM的源码分析之Pass相关
作者:snsn1984 题记:在学习LLVM的过程中,要想学的更加深入,掌握更多的技能,LLVM的源码是必须要读的,但是在这么多的源码中,从哪里下手?很容易让人找不到头脑,本文这里就先拿出几个Pass ...
- Dagger2入门系列二:ModuleComponent源码分析,h5移动端开发面试题
} } 1.3.Component类 @Component(modules = Test1Module.class) public interface Test1Component { void in ...
- 流量回放repeater的原理分析二:repeater源码分析
前言 在上文中我们分析了sandbox-jvm(以下简称sandbox)的核心源码,了解了sandbox实现类增强的原理.并且了解了sandbox的模块化加载能力,repeater作为一个独立的模块, ...
- (二)Linux源码分析-init
main 功能 前面介绍了head.s将执行权交给main.c,内核初始化完成,现在开始进入C代码的分析了.Init主要是取得前面setup.s设置的根文件设备号和一下内存全局变量,初始化陷阱门,块设 ...
- Licode入门学习:MediaStream源码分析(二)
Licode服务与启动过程分析 MediaStream源码分析(一) MediaStream源码分析(二) MediaStream源码分析(三) WebRtcConnection源码分析(一) Web ...
最新文章
- 档案盒正面标签制作_包材工艺丨浅述模内标签印刷及材料的选择
- android studio 打开github开源代码
- LeetCode-剑指 Offer 10- II. 青蛙跳台阶问题
- webapi中的模型验证
- MongoDB入门_MongoDB安装与配置
- 八、IO优化(6)优化tempdb性能
- 将堆栈异常返回前端显示
- python怎么添加ui_如何在Python中创建UI
- 天池notebook
- 设计模式 C++外观者模式
- Quanergy联手思科为智能交通创建物联网解决方案
- 修改Linux默认启动级别或模式的方法
- 很多人已经不会用WINDOWS的命令行、DOS命令了
- ebay html描述模板,ebay产品描述模板
- 【人脸识别】基于PCA实现ORL人脸识别附matlab代码和报告
- mysql自定义函数的创建
- Excel函数SUMIFS和COUNTIFS详解
- 正版授权WiFi大师4.0.5 专业版流量主小程序源码部署教程
- 基于高光谱成像的苹果虫害检测特征向量的选取
- 使用Cerebro管理ES集群