之前熟悉了openCV怎么调整图片对比度和亮度(通过线性和非线性的方法),都是很基础的图形操作,这边开始介绍openCV图像处理很重要的工具方法:傅里叶变换。
这部分内容涉及到复杂的傅里叶公式、也涉及到很多数学上的原理,单单通过官网是根本不会理解这部分内容的,我也是翻阅了很多的资料才对这部分内容有了比较清晰的了解,这边我通过用openCV的傅里叶变换方法实现图片校正的功能,里面涉及到的方法和原理我尽量详细的介绍。
上一篇内容:http://blog.csdn.net/jbl20078/article/details/78854660
openCV 傅里叶变换实际应用:图像旋转校正
第一部分:什么是傅里叶变换
光是学习傅里叶变换就花了我多半天的时间,后悔大学没好好学呀,这部分内容如果想深入了解的话看这个神人写的吧,已经非常用心并且很耐看:
如果对傅里叶变换还模糊或者看不下去的话也没关系,我们了解它在图形处理方面的意义和能处理什么问题就可以了。
第二部分:傅里叶变换在图形处理方面的实际意义和应用
如果第一部分详读了之后,应该会清楚傅里叶变换后其实是描述频率的二维数据,有了可以描述图像的频率(或者波频)我们能分辨出图像的高频部分和低频部分,高频部分就是图像对比度很大的区域(高频点趋近白色),低频部分是图像对比度比较小的部分(低频趋近于暗色),图像的边缘通常就是高频,因为这个区域色差普遍很大,所以傅里叶变换后的图像数据会比较方便的处理边缘(为我们边缘检测和图片方向检测提供了可能),如果你了解图像噪音的话(由于图像的传输等原因,图像中会有噪点)傅里叶通过对频率值的分析可以去除图像噪音,达到增强图像的效果。
当然,傅里叶应用的地方还有很多,比如图像特征值提取等,正是由于它的用途这么大,我们才一定要花大力气搞懂它的使用方法和原理,对我们后面图形的处理会有极大的帮助。
下面是傅里叶变换得到的波谱图(或者叫频率图,幅度图,无所谓,我们知道它是描述图形变化频率的数据就ok了)。
后面我会介绍怎么通过傅里叶变换后的波频图来给上面的图片扶正。
第三部分:实现图片的傅里叶变换
傅里叶变换需要经历很多步骤,我们分解一步步来实现:
1、读取原始图形数据,并将图像转化成灰度图(因为彩色图很多数据对我们没用,灰度值就够了,而且计算量小)
Mat mat = [[CVUtil sharedHelper]cvMatFromUIImage:_buildImg.image];
Mat grayImg;
cvtColor(mat, grayImg, COLOR_BGR2GRAY);
我读取图形的方式跟官网不同,因为我是在IOS系统上模拟测试的,这个根据自己测试的平台来修改
2、扩充灰度图的尺寸并且填充像素(图形尺寸是2,3,5倍数的时候傅里叶变换计算速度最快),这些接口openCV都提供了
Mat padded;
int m = getOptimalDFTSize(grayImg.rows);
int n = getOptimalDFTSize(grayImg.cols);
//通过copyMakeBorder进行填充像素和颜色(多出的像素点全部用0填充)
copyMakeBorder(grayImg, padded, 0, m-grayImg.rows, 0, n-grayImg.cols, BORDER_CONSTANT, Scalar::all(0));
这部分涉及到的接口都很简单,看参数都能明白是做什么,如果不清楚,调到代码中看方法描述,或者去下载源码阅读。
3、扩充图形的通道,准备调用离散傅里叶变换方法
看了第一部分内容或者了解傅里叶变换公式的同学知道,傅里叶变换得到的结果是一个复数,实数和虚数部分为了都能保存到我们傅里叶变换后的结果,我们要给上面的灰度图
扩充一个值全部为0的通道,并且把数值转化成float类型。最后得到的傅里叶变换结果我们会将实数部分和虚数部分拿出来计算幅度值(就是我们想要得到的频率图)做后面的校正
图形,下面会说。
//傅里叶变化后的结果是一个复数,也就是转化到频域中会有两个图像值,我们将图像转化成float类型 并且添加一个额外通道来存储复数部分
Mat planes[] = {Mat_(padded),Mat::zeros(padded.size(), CV_32F)}; //复制了一个padded,然后添加了一个全是0的通道
Mat complex;
//进行通道的混合
//merge函数是合并多个array 成为一个多通道的array,比方说array1 array2 合并成array3 那么array3[1][0] = array1[1]
//array3[1][2] = array2[1] 它的逆向操作是split方法
merge(planes,2,complex);
//合并好了 相当于给傅里叶变换的结果预先分配了存储空间(因为结果是复数)所以下面就可以进行傅里叶变换了
//dft函数(离散傅里叶变换)
//输入输出支持同一个图像
dft(complex,complex);
4、计算幅度图,得到频域图谱
傅里叶计算结束,我们得到了一个双通道的Mat数据,第一个通道是傅里叶变换数据的实数,第二个通道是傅里叶变换数据的虚数,这两者具体几何意义是什么,第一部分的连接
文章都有讲,不清楚的小伙伴可以回头看下,或者我们清楚下面的概念:
我们一般用幅度图像来标识图像傅里叶的变换结果,幅度的计算公式:
Re(DFT)是实数,IM(DFT)是虚数
magnitude = sqrt(Re(DFT)^2 + Im(DFT)^2)。
计算过程:
//现在的complex就是一个傅里叶变换的结果,是一个包含实数部分(Re)和复数部分(imaginary-im),我们要的是幅度公式:
// M = \sqrt[2]{ {Re(DFT(I))}^2 + {Im(DFT(I))}^2}
//转化为openCV代码
split(complex,planes);
magnitude(planes[0], planes[1], planes[0]);
//我们得到了幅度数据
Mat magI = planes[0];
5、对数变换
又遭遇了一个陌生的概念,对数变换是什么?
对数变换就是将上面我们得到的幅度数据转化成自然对数值,为什么要转换,看下面对数曲线:
首先,对数曲线在像素值较低的区域斜率很大,在像素值较高的区域斜率比较低,所以对数转化后,比较暗的区域将得到提升,增强了暗部的细节
其次:傅里叶变换后计算的幅度数值范围很大0~10^6,这种数值无法清晰的反应我们0~255像素区间的变化,所以我们通过对数变换,将数据缩小到一定的范围。
上面就是对数变换的原因,下面是方法:
//由于幅度值范围大到不适合在屏幕上显示 数值太大 都是白色,数值就算比较低也可能大于255 所以无法分辨高低 ,所以转化为对数范围
//公式:M_1 = \log{(1 + M)}
magI += Scalar::all(1);
log(magI,magI);
6、剪切和重分布
这部分官网解释的不清楚,我们上面计算出来的幅度值,大概是这样的,下面左边图
重分布就是将这个幅度数据 切成四个象限,然后对角线对调,让图形的边界点(四个角)转移到图形的中心点,效果如上面右边,这样确实更方便观察频率的变化,同时因为源图像
的边界一定是高频(色差很大),所以经过变换后,很容易可以得到频率变化的直线,我们通过计算这些直线的角度就知道了这个图片偏移的角度。
代码如下:
magI = magI(cv::Rect(0,0,magI.cols&-2,magI.rows&-2));
int cx = magI.cols/2;
int cy = magI.rows/2;
Mat q0(magI,cv::Rect(0,0,cx,cy));
Mat q1(magI,cv::Rect(cx,0,cx,cy));
Mat q2(magI,cv::Rect(0,cy,cx,cy));
Mat q3(magI,cv::Rect(cx,cy,cx,cy));
Mat tmp;
q0.copyTo(tmp);
q3.copyTo(q0);
tmp.copyTo(q3);
q1.copyTo(tmp);
q2.copyTo(q1);
tmp.copyTo(q2);
7、重分布完图形,后面就要归一操作和转化成灰度图。
其实上面的效果图我已经归一并灰度处理了,原因上之前我们给原图添加了一个通道,这里我们要将这个通道去除,通过归一将这些频率值圈定在(0~1)之间,在通过线性转换
(之前讲到的convertTo)把图形像素值拉大到【0,255】,这样得到的灰度图就是我们想要的最终图形频谱。这一步其实是可以放在第6步前面的,顺序无所谓。
这里大家可能有疑问:上面不是对数变换,将数值圈定在一定范围了吗,这边为什么归一?
解释一下:log方法进行对数变换一是将很大范围的数缩小,最重要的是增强了图像,让它大部分的低频区域细节更加明显。最后我们的归一方法和转化成灰度图方法是因为我们要将它
的值调整到255以内最适合人视觉观察的效果,灰度图(定义了一个通道一定是灰度图)过滤了其他颜色的干扰,我们想要的数据信息不需要其他通道提供。
代码如下:
//归一
normalize(magI, magI, 0, 1,CV_MINMAX);
//转化成单通道灰度图(这一步官网没有给出)
Mat magImg(magI.size(),CV_8UC1);
magI.convertTo(magImg, CV_8UC1,255,0);
经过上面的一步步操作,我们就得到了原图的傅里叶变换幅度图,效果如下:
下面的幅度图频率变化线已经很明显了,我们其实已经看到了图片旋转的方向,下面就是把这些直线检测出来,并计算旋转角度,将图片扶正。
校正图片
这部分内容用到了霍夫直线检测方法(Hough),openCV已经提供了接口。
但是,首先我们要将图片进行二值化,因为霍夫变换需要图像是二值的。
但是,首先我们要将图片进行二值化,因为霍夫变换需要图像是二值的。
// 图像的二值化就是将图像上的像素点的灰度值设置为0或255,这样将使整个图像呈现出明显的黑白效果。在数字图像处理中,二值图像占有非常重要的地位,图像的二值化使图像中数据量大为减少,从而能凸显出目标的轮廓。
//CV_THRESH_BINARY参数的作用就是 大于阈值(133) 就是最大值(255) 否则就是0
//有点像LUT 查找表哦 查找表也能实现这个功能
threshold(magImg,magImg,130,255,CV_THRESH_BINARY);
就调用一个方法即可,注释已经写上了,如果还想深入研究openCV二值化阈值的使用,可以看这篇文章(二值化也是很常用的哦):
如果看过我之前对LUT方法的总结,大家会发现,LUT通过查找表也可以很轻易的实现二值化的。
好了,经过上面的处理,我们的图像应该只有黑白两中颜色,看一下效果吧:
两条线中间还聚集了这么多点,看来阈值设置可以再多调整下(这个值影响后面hough中的另一个阈值,调整视情况而定)
下面就是用hough方法检测直线,并且绘图(绘图部分我没有整理,请看官网部分,很简单,这边我们就是绘制几条直线就可以了)
vector lines;
float pi180 = (float)CV_PI/180;
//准备在lineImg画直线
Mat linImg(magImg.size(),CV_8UC3);
//hough变换可以看这个:http://blog.csdn.net/qq_18343569/article/details/48006453
HoughLines(magImg, lines, 1, pi180, 400,0,0);
int numlines = (int)lines.size();
cout << "检测到的直线的个数为 " << numlines << endl;
for(int l = 0; l < numlines; l++){
//现在就是画出检测到的直线
//拿出极径 和 极角
float rho = lines[l][0];
float theta = lines[l][1];
//根据公式 (极坐标下的直线公式)
cv::Point pt1,pt2;
double a= cos(theta);
double b = sin(theta);
double x0 = a*rho;
double y0 = b*rho;
//cvRound对double四舍五入到int类型
pt1.x = cvRound(x0 + 1000*(-b));
pt1.y = cvRound(y0 + 1000*(a));
pt2.x = cvRound(x0 - 1000*(-b));
pt2.y = cvRound(y0 - 1000*(a));
line(linImg,pt1,pt2,Scalar(255,0,0),3,8,0);
}
houghLines检测直线的方法原理推荐大家看下相关介绍,它是通过用极坐标系表示直线的,相关连接:
建议大家要去了解霍夫检测直线的原理,知道怎么通过阈值的合理设置检测直线。包括上面二值化中的阈值,这两个值的设置非常重要,如果两个值设置有问题,很可能检测不出直线或者上千条直线。而且二值化的阈值设定是会影响霍夫方法的,这里说不太清,请大家认真阅读上面文档链接和亲身实例体验。
下面我们绘图之后的效果如下:
斜着的两条线看来是我们想要的(中间的横竖 是怎么检测出来的?貌似应该没有这两条线,没搞清楚这里)。
下面我们计算两条线的角度,把图片方向校正回去:
//实际开发中可能找到 几十条直线(通过调整阈值前提是能看到检测的直线数量,所以很难锁定到一两条直线),其实这些直线无非就一两种斜率,可以算出角度,进行剔除就可以了
//下面我们剔除不要的直线 并且让图旋转回去。
// 由于DFT的特点,只有输入图像是正方形时,检测到的角才是文本真正旋转的角度。但我们的输入图像不一定是正方形的,所以要根据图像的长宽比改变这个角度。
//还有一个需要注意的细节,虽然HoughLines()输出的倾斜角在[0,180)之间,但在[0,90]和(90,180)之间这个角的含义是不同的。
float angel=0;
float piThresh = (float)CV_PI/90;
float pi2 = CV_PI/2;
for(int l=0; l
关于旋转角度的实现,我参考的这篇文章:
最后效果如下:
图像被旋转过来了。
总结:主要是傅里叶变换原理的理解是最重要的,具体实现中也有一些优化数据的步骤,比如扩充Mat,合并通道、对数变换,归一,直线检测等,相关数学公式很多,
我这里只是把我参考的内容和理解的部分记录下来,里面还有很多不是很清晰的东西,但是一口气搞懂也是不现实的,后面继续深入研究openCV的时候逐渐去学习吧。