基于傅立叶变换简单实现旋转校正

前几天写过基于OpenCV处理实现身份证号码识别,里面实现了一个简单的拍照识别身份证号码功能,测试了一番感觉对于号码的识别准确率挺高。当然这需要建立在拍照清晰,背景简单,身份证摆放端正......一系列前提之下的。至于如何让程序能应对不同复杂的场景也可以保持较高的准确识别率,只能一点点去完善。今天这篇主要讲的是文本旋转校正,以应对在照片在旋转的情况下也能保证识别的准确率。

基于傅立叶变换简单实现旋转校正_第1张图片
识别失败

图像旋转矫正,以一般思维分析无非是:获取旋转角度、使用仿射变换进行旋转矫正。这里的关键在于获取旋转角度的问题,只要获取到准确的旋转角度,再通过仿射变换进行处理就比较简单了。所以接下来就需要对图像进行处理,以取到需要旋转的角度值。

搜了一些关于旋转矫正的文章,发现对于旋转矫正的处理一般都是基于傅立叶变化处理得到幅值图,幅值图经过中心化处理可以得到相应的倾斜直线,然后计算出角度,放射变换矫正即可。既然有了步骤,那就顺着这个流程继续整理下去。下面就简单了解下图像处理相关介绍:

图像变换:

  1. 图像变换包括两个过程:正变换和逆变换,通过正变换将图像变为新图像,然后进行处理。通过逆变换将处理后的图像还原为原始形式的图像,以方便对原始图像进行对比
  2. 图像变换的目的:简化图像处理,便于图像特征提取,图像压缩,从概念上增强对图像信息的理解。
  3. 图像变换主要有:傅立叶变换,主成份变换,缨帽变换,代数运算,彩色变换......
  4. 傅立叶变换是换域分析(空间域到频率域)是一种广泛使用的工具,在图像处理中是一种有效而重要的方法。

傅立叶变换:

  1. 傅立叶变换:对一张图像使用傅立叶变换就是将它分解成正弦和余弦两部分。也就是将图像从空间域(spatial domain)转换到频域(frequency domain)。 这一转换的理论基础来自于以下事实:任一函数都可以表示成无数个正弦和余弦函数的和的形式。傅立叶变换就是一个用来将函数分解的工具。 2维图像的傅立叶变换可以用以下数学公式表达:

    式中 f 是空间域(spatial domain)值, F 则是频域(frequency domain)值。 转换之后的频域值是复数, 因此,显示傅立叶变换之后的结果需要使用实数图像(real image) 加虚数图像(complex image), 或者幅度图像(magitude image)加相位图像(phase image)。 在实际的图像处理过程中,仅仅使用了幅度图像,因为幅度图像包含了原图像的几乎所有我们需要的几何信息。 然而,如果你想通过修改幅度图像或者相位图像的方法来间接修改原空间图像,你需要使用逆傅立叶变换得到修改后的空间图像,这样你就必须同时保留幅度图像和相位图像了。

  2. 空间频率的理解:对图像而言,空间频率是指单位长度内亮度(也就是灰度)作周期性变化的次数。是图像中灰度变化剧烈程度的指标,也可以理解为灰度在平面空间上的梯度。
  3. 频率域的理解:傅立叶变换以前,图像是由对在连续空间(现实空间)上的采样得到一些列点的集合,我们习惯用一个二维矩阵表示空间上各点,则图像可由z=f(x,y)来表示。实际上对图像进行二维傅立叶变换得到的频谱图,就是图像梯度的分布图,当然频谱图上的各点与原图像上各点并不存在一一对应的关系,即使在不移频的情况下也是没有。傅立叶频谱图上我们看到的明暗不一的亮点,实际上图像上某一点与邻域点差异的强弱,即梯度的大小,也即该点的频率的大小(可以这么理解,图像中的低频部分指低梯度的点,高频部分相反)。一般来讲,梯度大则该点的亮度强,否则该点亮度弱。这样通过观察傅立叶变换后的频谱图,也叫功率图,我们首先就可以看出,图像的能量分布,如果频谱图中暗的点数更多,那么实际图像是比较柔和的(因为各点与邻域差异都不大,梯度相对较小),反之,如果频谱图中亮的点数多,那么实际图像一定是尖锐的,边界分明且边界两边像素差异较大的。

看到这,实在是看不下去了。但是后面要用到,不得已网上搜了下发现这篇很有名的看不懂就掐死教程,还有篇也不错,文章写得很详细,图文并茂。虽然还是不大理解图像中的傅立叶变换,但相比之前根本不知道傅立叶变换是怎么回事要强得多,最起码简要了解过这个概念。

如果等把傅立叶变换弄透彻再进行项目,感觉是不可能了,下面就上代码,随代码解决问题。

// 加载图片
UIImageToMat(image, cvImage);
    
// 灰度化
cv::cvtColor(cvImage, cvImage, CV_RGB2GRAY);
    
// 图片非空判断
 if (cvImage.empty()) {
    return;
}
    
// 获取原图像中心点
CvPoint center(cvImage.cols/2 ,cvImage.rows/2);
    
/*
在使用DFT变换之前:
DFT变换在一个向量尺寸上不是一个单调函数,当计算两个数组卷积或对一个数组进行光学分析,
它常常会用0扩充一些数组来得到稍微大点的数组以达到比原来数组计算更快的目的。
一个尺寸是2阶指数(2,4,8,16,32…)的数组计算速度最快,
一个数组尺寸是2、3、5的倍数(例如:300 = 5*5*3*2*2)同样有很高的处理效率。
getOptimalDFTSize()函数返回大于或等于vecsize的最小数值N,
这样尺寸为N的向量进行DFT变换能得到更高的处理效率。在当前N通过p,q,r等一些整数得出N = 2^p*3^q*5^r.
这个函数不能直接用于DCT(离散余弦变换)最优尺寸的估计,可以通过getOptimalDFTSize((vecsize+1)/2)*2得到。
     
通俗一点:
dft这个函数虽然对于输入mat的尺寸不做要求,但是如果其行数和列数可以分解为2、3、5的乘积,那么对于dft运算的速度会加快很多。
而DFT函数并不具备计算虽有尺寸,所以需要用到getOptimalDFTSize()函数找到最适合的尺寸,然后用copyMakeBorder()函数填充多余的部分。
以便于DFT更快速的对图像进行变换,下面是需要用到的两个函数:
     
C++: int getOptimalDFTSize(int vecsize)
函数说明:返回给定向量尺寸经过DFT变换后结果的最优尺寸大小
参数解释:
int vecsize: 输入向量尺寸大小(vector size)
     
C++: void copyMakeBorder(InputArray src, OutputArray dst, int top, int bottom, int left, int right, 
     int borderType, const Scalar& value=Scalar() )
函数说明:扩充图像边界
参数解释:
InputArray src: 输入图像
OutputArray dst: 输出图像,与src图像有相同的类型,其尺寸应为Size(src.cols+left+right, src.rows+top+bottom)
int类型的top、bottom、left、right: 在图像的四个方向上扩充像素的值
int borderType: 边界类型,由borderInterpolate()来定义,常见的取值为BORDER_CONSTANT
const Scalar& value = Scalar(): 如果边界类型为BORDER_CONSTANT则表示为边界值
*/
cv::Mat padImg;
int opWidth = cv::getOptimalDFTSize(cvImage.rows);
int opHeight = cv::getOptimalDFTSize(cvImage.cols);
copyMakeBorder(cvImage, padImg, 0, opWidth - cvImage.rows, 0, opHeight - cvImage.cols, cv::BORDER_CONSTANT);
  
  
/*
1.为傅立叶变换的结果(实部和虚部)分配存储空间: 傅立叶变换的结果是复数,这就是说对于每个原图像值,结果是两个图像值。
2.获取两个Mat,一个用于存放dft变换的实部,一个用于存放虚部,初始的时候,实部就是图像本身,虚部全为0。
3.dft()输入和输出应该分别为单张图像,所以要先用merge()把几个单通道的mat融合成一个多通道的mat(这里是2通道),
这里融合的comImg即有实部,又有虚部。计算得到的实虚部仍然保存在comImg的两个通道内。
*/
cv::Mat planes[] = {cv::Mat_(padImg),cv::Mat::zeros(padImg.size(), CV_32F)};
cv::Mat comImg;
merge(planes, 2, comImg);
    

/*
进行离散傅立叶变换:
因为comImg本身就是两个通道的Mat,所以dft变换的结果也可以保存在其中。
     
C++: void dft(InputArray src, OutputArray dst, int flags=0, int nonzeroRows=0)
函数说明:傅里叶变换函数
参数解释:
InputArray src: 输入图像,可以是实数或虚数
OutputArray dst: 输出图像,其大小和类型取决于第三个参数flags
int flags = 0: 转换的标识符,有默认值0.其可取的值如下所示:
DFT_INVERSE: 用一维或二维逆变换取代默认的正向变换
DFT_SCALE: 缩放比例标识符,根据数据元素个数平均求出其缩放结果,如有N个元素,
           则输出结果以1/N缩放输出,常与DFT_INVERSE搭配使用。
DFT_ROWS: 对输入矩阵的每行进行正向或反向的傅里叶变换;
          此标识符可在处理多种适量的的时候用于减小资源的开销,
          这些处理常常是三维或高维变换等复杂操作。
DFT_COMPLEX_OUTPUT: 对一维或二维的实数数组进行正向变换,这样的结果虽然是复数阵列,
                    但拥有复数的共轭对称性(CCS),可以以一个和原数组尺寸大小相同的实数数组进行填充,
                    这是最快的选择也是函数默认的方法。你可能想要得到一个全尺寸的复数数组(像简单光谱分析等等),
                    通过设置标志位可以使函数生成一个全尺寸的复数输出数组。
DFT_REAL_OUTPUT: 对一维二维复数数组进行逆向变换,这样的结果通常是一个尺寸相同的复数矩阵,
                 但是如果输入矩阵有复数的共轭对称性(比如是一个带有DFT_COMPLEX_OUTPUT标识符的正变换结果),便会输出实数矩阵。
int nonzeroRows = 0: 当这个参数不为0,函数会假设只有输入数组(没有设置DFT_INVERSE)
                     的第一行或第一个输出数组(设置了DFT_INVERSE)包含非零值。
                     这样的话函数就可以对其他的行进行更高效的处理节省一些时间,
                     这项技术尤其是在采用DFT计算矩阵卷积时非常有效。
     
实际应用没这么复杂,只需要传入需要变换的图像,此函数支持图像原地计算 (输入输出为同一图像)
*/
dft(comImg, comImg);
    
    
 /*
对变换后的结果进行分割,我们只需要拿到幅度值并显示即可,
1.把变换的结果分割到各个数组的两页中,方便后续操作
2.求傅里叶变化各频率的幅值,将复数转换为幅度
  复数包含实数部分(Re)和复数部分 (M - Im),离散傅立叶变换的结果是复数,
     
幅度计算公式为:M = sqrt(Re(DFT)^2 + Im(DFT)^2)。
对应的OpenCV计算二维矢量的幅值函数是:
C++: void magnitude(InputArray x, InputArray y, OutputArray magnitude)
参数解释:
InputArray x: 浮点型数组的x坐标矢量,也就是实部
InputArray y: 浮点型数组的y坐标矢量,必须和x尺寸相同
OutputArray magnitude: 与x类型和尺寸相同的输出数组:
*/
split(comImg, planes);   // 分割函数
magnitude(planes[0], planes[1], planes[0]);
cv::Mat magMat = planes[0];
   
 
 /*
1.作归一化操作,幅值加1
2.傅里叶变换的幅度值范围大到不适合在屏幕上显示,高值在屏幕上显示为白点,而低值为黑点,
  高低值的变化无法有效分辨,为了在屏幕上凸显出高低的变化得连续性,我们可以用对数尺度来替换线性尺度
需要用到的函数:C++: void log(InputArray src,OutputArray dst)
参数解释:
InputArray src: 为输入图像
OutputArray dst: 为得到的对数值
*/
magMat += cv::Scalar::all(1);
log(magMat,magMat);
 
   
/*
1.修剪频谱,如果图像的行或者列是奇数的话,那其频谱是不对称的,因此要修剪。
  我们知道x&-2代表x与-2按位相与,而-2的二进制形式是2的二进制取反加一的结果(这是补码的问题)。
  2 的二进制结果是(假设用8位表示,实际整型是32位,但是描述方式是一样的,为便于描述,用8位表示)0000 0010,
  则-2的二进制形式为:1111 1110,在x与-2按位相与后,不管x是奇数还是偶数,最后x都会变成一个偶数。

2.这是对傅里叶变换结果进行中心化。将每张子图像看成幅度图的一个象限,
     重新分布即将四个角点重叠到图片中心)。 这样的话原点(0,0)就位移到图像中心。

3.象限换位的目的就是把频域原点移到图像中心,所以移位后低频是位于中心区域的。
     频谱图像中心的小亮点F(0,0)表示图像的平均灰度
*/
magMat = magMat(CvRect(0, 0, magMat.cols & -2, magMat.rows & -2));
int cx = magMat.cols/2;
int cy = magMat.rows/2;
    
cv::Mat q0(magMat, CvRect(0, 0, cx, cy));   // top-left - 为每一个象限创建ROI
cv::Mat q1(magMat, CvRect(0, cy, cx, cy));  // top-right
cv::Mat q2(magMat, CvRect(cx, cy, cx, cy)); // bottom-left
cv::Mat q3(magMat, CvRect(cx, 0, cx, cy));  // bottom-right
    
// 交换象限,(Top-Left with Bottom-Right)
cv::Mat tmp;
q0.copyTo(tmp);
q2.copyTo(q0);
tmp.copyTo(q2);
    
// 交换象限,(Top-Right with Bottom-Letf)
q1.copyTo(tmp);
q3.copyTo(q1);
tmp.copyTo(q3);
    
 
/*
1.归一化函数,有了重新分布后的幅度图,但是幅度值超过了[0,1],超过了显示范围,
使用normalize()函数可将幅度归一化到可显示范围:把要处理的数据经过某种算法的处理限制在所需要的范围内
C++: void normalize(InputArray src, OutputArray dst, double alpha=1, double beta=0, 
     int norm_type=NORM_L2, int dtype=-1, InputArray mask=noArray() )
参数解释:
InputArray src: 输入图像
OutputArray dst: 输出图像,尺寸大小和src相同
double alpha = 1: range normalization模式的最小值
double beta = 0: range normalization模式的最大值,不用于norm normalization(范数归一化)模式
int norm_type = NORM_L2: 归一化的类型,主要有
NORM_INF: 归一化数组的C-范数(绝对值的最大值)
NORM_L1: 归一化数组的L1-范数(绝对值的和)
NORM_L2: 归一化数组的L2-范数(欧几里得)
NORM_MINMAX: 数组的数值被平移或缩放到一个指定的范围,线性归一化,一般较常用。
int dtype = -1: 当该参数为负数时,输出数组的类型与输入数组的类型相同,否则输出数组与输入数组只是通道数相同,
               而depth = CV_MAT_DEPTH(dtype)
InputArray mask = noArray(): 操作掩膜版,用于指示函数是否仅仅对指定的元素进行操作。
     
2.使用convertTo()把小数映射到[0,255]内的整数。结果保存在一幅单通道图像内
     */
    normalize(magMat, magMat, 0, 1 ,CV_MINMAX);
    cv::Mat magImg(magMat.size(), CV_8UC1);
    magMat.convertTo(magImg,CV_8UC1,255,0);
基于傅立叶变换简单实现旋转校正_第2张图片
傅里叶谱图

这是经过了中心化之后显示的效果,低频部分位于四角,高频部分位于中间。习惯上会把图像做四等份,互相对调,使低频部分位于图像中心,也就是让频域原点位于中心。

/*
     0 | 3         2 | 1
    -------  ===> -------
     1 | 2         3 | 0
 */

下面以一张黑白矩形框为例:
基于傅立叶变换简单实现旋转校正_第3张图片

最后一张是经常看见的傅里叶谱,也叫功率图,越亮代表能量越大,幅度越高。垂直方向与水平方向都有白色的条纹,说明在垂直方向与水平方向低频部分很明显。

从傅里叶谱可以明显地看到一条经过中心点的倾斜直线。要想求出这个倾斜角,首先要在图像上找出这条直线。这里是采用霍夫(Hough)变换检测直线:

/*
Hough直线检测需要用到的函数:
C++:void HoughLines(InputArray image, OutputArray lines, double rho, double theta, 
     int threshold, double srn=0, double stn=0, double min_theta=0, 
     double max_theta=CV_PI )
参数解释:
InputArray image:输入图像,即源图像,需为8bit的单通道二值图像,
                  可以将任意的源图载入进来后由函数修改成此格式后,再填在这里。
InputArray lines:经过调用HoughLines函数后储存了霍夫线变换检测到线条的输出矢量。
                  每一条线由具有两个元素的矢量表示,其中,是离坐标原点((0,0)
                 (也就是图像的左上角)的距离。是弧度线条旋转角度(0~垂直线,π/2~水平线)。
double rho:以像素为单位的距离精度。另一种形容方式是直线搜索时的进步尺寸的单位半径。
            PS:Latex中/rho就表示 。
double theta:以弧度为单位的角度精度。另一种形容方式是直线搜索时的进步尺寸的单位角度。
int threshold:累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。
               大于阈值threshold的线段才可以被检测通过并返回到结果中。
double srn:有默认值0。对于多尺度的霍夫变换,这是第三个参数进步尺寸rho的除数距离。
            粗略的累加器进步尺寸直接是第三个参数rho,而精确的累加器进步尺寸为rho/srn。
double stn:有默认值0,对于多尺度霍夫变换,srn表示第四个参数进步尺寸的单位角度theta的除数距离。
            且如果srn和stn同时为0,就表示使用经典的霍夫变换。否则,这两个参数应该都为正数。
     
代码说明:
1.Hough变换要求输入图像是二值图,所以先进行二值化处理
2.定义存储直线容器:里面包含两个浮点值(rho,theta),每一对对应一条直线
3.弧度制中角度1度对应的值
4.绘制所有直线的画布
5.Hough直线检测
*/
threshold(magImg,magImg,120,255,CV_THRESH_BINARY);
std::vector lines;
float pi180 = (float)CV_PI/180;
cv::Mat linImg(magImg.size(),CV_8UC3);
HoughLines(magImg,lines,1,pi180,300,0,0);
unsigned long numLines = lines.size();

for (int i = 0; i < numLines; i++) {
        float rho = lines[i][0],theta = lines[i][1];
        CvPoint pt1,pt2;
        double a = cos(theta),b = sin(theta);
        double x0 = a*rho,y0 = b*rho;
        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(lineImg,pt1,pt2,cvScalar(0,0,255),1,8,0);
}
    
[self cv_MatToUIImageWithMat:lineImg];

通过以上检测,可以得到所有符合要求的直线。遍历lines,并绘制里面的所有直线到lineImg上,然后显示lineImg:
基于傅立叶变换简单实现旋转校正_第4张图片
9395条直线显示出来就是这鬼样

为什么有这么多直线,再次研究一番HoughLines函数才知道,里面有个int threshold参数:HoughLines 函数中threshold参数表示某条线段的投票数大于该阈值才可以被检测通过并返回到lines中。我们上面设定threshold为300,也就表示需要超过300的共线点来确定一条直线,也就才会被返回到lines中存储。

Threshold 直线数量 筛选时长
100 9395 0.000793
300 1852 0.000152
580 3 0.000055

下面设置threshold = 500:
基于傅立叶变换简单实现旋转校正_第5张图片
threshold = 500

这次就少多了,只有46条。所以经过几轮测试,当threshold = 580:
基于傅立叶变换简单实现旋转校正_第6张图片
threshold = 580
只有3条了,对应的可以得到三个角度:0度、90度、14度。去掉0和90度,14度就是我们所需要的倾斜角了。对于不同的threshold值对后续筛选最合直线的效率有一定影响:
Threshold 直线数量 筛选时长
100 9395 0.000793
300 1852 0.000152
580 3 0.000055

现在通过仿射变换函数warpAffine变换得到:
基于傅立叶变换简单实现旋转校正_第7张图片
What 怎么旋转角度不对!!!

如果旋转矫正为这种效果,明显是失败了,但是是哪里出问题了,有幸在一篇文章中看到了这段话:
如果这句不太明白,没关系,我也看不懂。再看看这句:因为DFT之前的原图像在x y方向上表示空间坐标,DFT是经过x y方向上的傅里叶变换来统计像素在这两个方向上不同频率的分布情况,所以DFT得到的图像在x y方向上不再表示空间上的长度,而是频率。opencv的DFT函数设计为输出(频谱)图像与输入(像素)图像尺寸一致。如果输入图像是正方形,那么输出图像在x y方向上的(频率)坐标间隔是相同的;如果输入图像不是正方形,就会造成输出图像在x或y方向上的缩放,即两轴的坐标间隔不一致。正方形被拉伸为长方形,其对角线的斜率也势必产生变化。所以在检测直线之后,还要根据图像的原始尺寸调整这个对角线的倾角。
DFT函数设计为输出图像与输入图像尺寸一致,所以傅立叶频谱图的尺寸取决于输入图。如果频谱图为正方形,那么我们上面所得到的角度就是需要旋转的角度,但是如果频谱图不为正方形,就会造成频谱图在x或y方向上的缩放,也就是两轴的坐标单元不一致,下面坐标的x,y轴对应不同的坐标单元得到的角度也不同。
基于傅立叶变换简单实现旋转校正_第8张图片
将就着看一下

分析出问题之所在,就是求出正确角度的问题了。另外这里还有个地方需要注意,虽然HoughLines()输出的倾斜角在[0,180)之间,但在[0,90]和(90,180)之间这个角的含义是不同的,当倾斜角大于90度时,(180-倾斜角)才是直线相对竖直方向的偏离角度。在OpenCV中,逆时针旋转,角度为正。要把图像转回去,这个角度就变成了(倾斜角-180)。

float ang=0;
float pi90 = (float)CV_PI/90;
float pi2 = CV_PI/2;
for(int l=0; l

关于霍夫变换检测直线的结果,这里是存储在vector lines结构体中。众所周知,在笛卡尔坐标系中,直线由斜率和截距表示,而在极坐标系中可由极径和极角表示。lines里面的两个浮点数就是这条直线的极径(r)和极角(θ):
基于傅立叶变换简单实现旋转校正_第9张图片

计算出旋转角度angel,最后才进行仿射变换:

/*
1.在变换之前先用getRotationMatrix2D()构造一个2*3的仿射变换矩阵,
  再把这个矩阵输入warpAffine()做一个单纯旋转的仿射变换
Mat getRotationMatrix2D(Point2f center, double angle, double scale)
参数解释:
Point2f center:表示旋转的中心点
double angle:表示旋转的角度
double scale:图像缩放因子
     
2.ones函数初始化矩阵,后面的CV_8UC3表示图像文件格式使用的是 Unsigned 8bits,
  最后的1、2、3表示通道数
3.仿射变换函数:void warpAffine(InputArray src, OutputArray dst, InputArray M, 
  Size dsize, int flags=INTER_LINEAR, int borderMode=BORDER_CONSTANT, 
  const Scalar& borderValue=Scalar())
参数解释:
InputArray src:输入图像
OutputArray dst:变换后图像,类型与src一致。
InputArray M:变换矩阵,需要通过其它函数获得,当然也可以手动输入。
Size dsize:输出图像的大小
int flags=INTER_LINEAR:插值算法
int borderMode=BORDER_CONSTANT:边界处理方式
const Scalar& borderValue=Scalar():设置由于旋转产生的空白的填充颜色
 */
    
cv::Mat rotMat = cv::getRotationMatrix2D(center,angel,1.0);
cv::Mat dstImg = cv::Mat::ones(cvImage.size(),CV_8UC3);
warpAffine(cvImage,dstImg,rotMat,cvImage.size(),1,0,cvScalar(255,255,255));
基于傅立叶变换简单实现旋转校正_第10张图片
旋转矫正效果图

旋转矫正到这告一段落了,接下来还是试试号码识别吧。继续上篇的步骤:已经灰度化了,直接二值化--腐蚀--轮廓检测--提取身份证号码区域,就是下面这种效果:
这不对啊

调试了半天还是没出来,轮廓Rect能拿到,提取身份证号码之前的图也是正常的.....一时还真不知道是哪里的问题。那就先说一个坑:旋转矫正完之后,可以拿到矫正之后的图像,最开始我是定义全局属性存储的,然后在号码区域提取时直接使用,但是才发现那已经完全不能用了。看了前面的代码:如果直接对全局矩阵类属性赋值,就是指针操作,即后面对原图像的一系列操作也会影响已经赋值的目标图像,所以在我取目标图像进行号码区域提取时已经是经过一系列二值化,腐蚀处理的图了。所以截取到的图就是什么都看不见的。后我尝试使用clone()函数进行操作,虽然拿到的目标图像已经是新的地址了,但截取的图像还是这样:

后面的解决办法是:
/*
     旋转操作的图像不再是经过灰度化的cvImage,而是使用UIImage加载的原始图
     旋转完成,经过灰度化,二值化,腐蚀,轮廓提取,图像截取成功
*/
UIImageToMat(image, sourceImg);
cv::Mat rotMat = cv::getRotationMatrix2D(center,angelD,1.0);
    cv::Mat dstImg = cv::Mat::ones(padImg.size(),CV_8UC3);
warpAffine(sourceImg,dstImg,rotMat,padImg.size(),1,0,cvScalar(255,255,255));
基于傅立叶变换简单实现旋转校正_第11张图片

上面旋转矫正原始图,然后进行图像截取是成功的,这里我继续测试了一下在旋转操作原彩图之前进行灰度化,然后旋转矫正,这么操作的话身份证号码区域截取是失败的。对于这个问题,我目前也不知道是什么原因。感觉是放射变换的时候对不同通道图像处理效果不同。具体原因就待后续补上了,如果有知道的也可以留言讨论,这篇基于傅立叶变换实现旋转矫正就整理完了。对于身份证,车牌这类带边框的图像要实现旋转矫正其实不需要傅立叶变换这么这么麻烦的处理。带有边框的直接使用一般处理步骤即可实现:图片灰度化 -- 阈值二值化 -- 检测轮廓 -- 寻找轮廓的包围矩阵,并且获取角度 -- 根据角度进行旋转矫正 -- 对旋转后的图像进行轮廓提取。

基于傅立叶变换简单实现旋转校正_第12张图片

对于纯文本图像进行旋转矫正的话,如果使用上述操作,矫正会失败,因为文本图像的背景是白色的,这里没有办法像身份证车牌那类有明显边界的矩形物体那样,提取出轮廓并旋转矫正。而使用傅立叶变换处理则可以实现。

文章原创,商业转载请联系作者获得授权,非商业转载请注明出处。

参考:
旋转文本矫正
基于傅立叶变换实现文本旋转矫正
霍夫线变换
放射变换
OpenCV实现基于傅里叶变换的旋转文本校正 推荐
图像的傅立叶变换
理解图像中的傅立叶变换

你可能感兴趣的:(基于傅立叶变换简单实现旋转校正)