OpenCV实现基于傅里叶变换的旋转文本校正

代码

        先给出代码,再详细解释一下过程:


过程
读取图片

        srcImg.empty()用来判断是否成功读进图像,如果srcImg中没有数据,在后面的步骤会产生内存错误。
        由于处理的是文本,彩色信息不会提供额外帮助,所以要用CV_LOAD_IMAGE_GRAYSCALE表明以灰度形式读进图像。
        假定读取的图像如下:

english-rotation

旋转原图像(可选)

        如果手头没有这样的倾斜图像,可以选择一张正放的文本图像,再把第12行#define DEGREE那行前的注释符号去掉。然后这部分代码就会把所给的图像旋转你规定的角度,再交给后面处理。

图像延扩

        OpenCV中的DFT采用的是快速算法,这种算法要求图像的尺寸是2、3和5的倍数时处理速度最快。所以需要用getOptimalDFTSize()找到最适合的尺寸,然后用copyMakeBorder()填充多余的部分。这里是让原图像和扩大的图像左上角对齐。填充的颜色如果是纯色对变换结果的影响不会很大,后面寻找倾斜线的过程又会完全忽略这一点影响。

DFT

        DFT要分别计算实部和虚部,把要处理的图像作为输入的实部、一个全零的图像作为输入的虚部。dft()输入和输出应该分别为单张图像,所以要先用merge()把实虚部图像合并,分别处于图像comImg的两个通道内。计算得到的实虚部仍然保存在comImg的两个通道内。

获得DFT图像

        一般都会用幅度图像来表示图像傅里叶的变换结果(傅里叶谱)。
        幅度的计算公式:magnitude = sqrt(Re(DFT)^2 + Im(DFT)^2)。
        由于幅度的变化范围很大,而一般图像亮度范围只有[0,255],容易造成一大片漆黑,只有几个点很亮。所以要用log函数把数值的范围缩小。

 

        dft()直接获得的结果中,低频部分位于四角,高频部分位于中间。习惯上会把图像做四等份,互相对调,使低频部分位于图像中心,也就是让频域原点位于中心。

 

fft-mag

        虽然用log()缩小了数据范围,但仍然不能保证数值都落在[0,255]之内,所以要先用normalize()规范化到[0,1]内,再用convertTo()把小数映射到[0,255]内的整数。结果保存在一幅单通道图像内:

 

english-mag

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

        Hough变换要求输入图像是二值的,所以要用threshold()把图像二值化。
        二值化的一种结果:

english-binary

        这一部分用HoughLines()检测图像中可能存在的直线,并把直线参数保存在向量组lines中,然后绘制出找到的直线。
        两个参数GRAY_THRESH和HOUGH_VOTE需要手动指定,不同的图像需要设置不同的参数,同一段文本旋转不同的角度也需要不同的参数。GRAY_THRESH越大,二值化的阈值就越高;HOUGH_VOTE越大,霍夫检测的投票数就越高(需要更多的共线点来确定一条直线)。说白了,如果发现二值化图像中直线附近有很多散点,就要适当提高GRAY_THRESH;如果发现从二值图像的一条直线上检测到了几条角度相差很小的直线,就需要适当提高HOUGH_VOTE。我们希望得到的结果时刚好检测到三条直线(有时只能检测到一条直线,后面会给出一个例子)。
        检测到的直线:

english-line

计算倾斜角
        上面得到了三个角度,一个是0度,一个是90度,另一个就是我们所需要的倾斜角。要把这个角找出来,而且要考虑误差。

        由于DFT的特点,只有输入图像是正方形时,检测到的角才是文本真正旋转的角度。但我们的输入图像不一定是正方形的,所以要根据图像的长宽比改变这个角度。
        还有一个需要注意的细节,虽然HoughLines()输出的倾斜角在[0,180)之间,但在[0,90]和(90,180)之间这个角的含义是不同的。请看图示:

 

hough-transformation

        当倾斜角大于90度时,(180-倾斜角)才是直线相对竖直方向的偏离角度。在OpenCV中,逆时针旋转,角度为正。要把图像转回去,这个角度就变成了(倾斜角-180)。
校正图像
        最后一步,当然是把图像转回去~

 

        先用getRotationMatrix2D()获得一个2*3的仿射变换矩阵,再把这个矩阵输入warpAffine(),做一个单纯旋转的仿射变换。warpAffine()的最后一个参数Scalar(255,255,255)是把由于旋转产生的空白用白色填充。
        校正的结果:

english-correction


一个检测单条直线的例子
原始图像:

single-english-rotation

傅里叶谱:

single-english-mag

        只有一条明显的直线。还好仅有的这条直线正是我们所需要的。
检测直线:

single-english-line

校正结果:

single-english-correction


对中文的效果
        我们来试试看这段程序对中文的校正效果。
输入图像:

chinese-rotation

傅里叶谱:

chinese-mag

        可以发现有许多条平行的亮线,其中过频域原点的那条长度最长,最容易检测出来。
检测直线:

chinese-line

校正结果:

chinese-correction

        虽然中文和英文在文字上有很大的不同,但字母(或者文字)的高度比较一致,使得行与行之间的分隔很明显。所以它们的频域特征是相似的。


对其他语言文字的效果
        我从IMDB.com摘取影片《教父》的英文介绍,然后用谷歌翻译成其他文字进行测试。
阿拉伯语

arabic-rotation

arabic-mag

arabic-line

一枚反例
老挝语:

lao-rotation

傅里叶谱:

lao-mag

一种二值化的结果:

lao-binary

直线检测:

lao-line

        这种文字的很多字母的上下方多了很多“笔画”(我不知道该怎么称呼那些小曲线),让行与行之间的分离变得不明显,使得频域特征变得不明显。
        虽然用肉眼可以看出傅里叶谱中存在一条倾斜的直线,但它的亮度太低,二值化过程很难排除噪声,导致直线检测会首先检出噪声产生的直线。这也是我的程序目前受限之处。需要增加一个过滤散点噪声的步骤以增加程序的适用范围。


参考:Discrete Fourier Transform — OpenCV 2.4.7.0 documentation

代码还可以在这里下载:https://github.com/johnhany/textRotCorrect


2014.1.3更新:

        由于文章内的图片右下角存在水印,若直接使用文章内的图片进行处理会使频域原点附近增加一团亮点,妨碍直线的检出。而且为了节省空间,图片是经过缩小的,使得字母的边缘变得模糊,频域特征也减弱。为此我提供了十幅没有水印的图片,供想要亲手实验的朋友使用。下载链接

36

avatar
17 Comment threads
19 Thread replies
0 Followers
 
Most reacted comment
Hottest comment thread
18 Comment authors
lmmybobo~Neilcondor青雲 Recent comment authors
  Subscribe  
最新 最旧
订阅评论
lmmy
lmmy

博主,你好,我是用lena图,按你说的设置了旋转角度为27,但是通过Hough变换检测直线计算出的角度是63,校正回去就表现为更远离正图了。hough变换那里我检测出的有四条线,我尝试把HOUGH_VOTE增大,就完全检测不出来直线了

lmmy
lmmy

很想试验一下opencv中离散傅里叶变换进行图像倾斜校正,正苦于找不到倾斜的图像,没想到博主还提供图片,在此先感谢博主,我先去试试,有问题再来请教!

bobo~
bobo~

博主,好像这个是基于大段文字的检测哦,对于就只有两个字符,比如:4m,这样的短字符检测有什么方法改进的么?不然识别不了啊!

Neil
Neil

博主有90度和180度图片旋转的解决方案吗?

condor
condor

有java版本的实现么?

青雲
青雲

你好,博主,这个地方不理解,可以解释一下吗?

由于DFT的特点,只有输入图像是正方形时,检测到的角才是文本真正旋转的角度。但我们的输入图像不一定是正方形的,所以要根据图像的长宽比改变这个角度。

huishao
huishao

楼主,你好。我尝试用你的代码做倾斜矫正,但是对于同一张不同倾斜角度的图片,需要调整灰度化参数值和霍夫变换投票率才能达到比较好的效果。想请教一下有没有什么好的自适应方法。

胡

你好,我想问问,为什么频谱图象进行中心转移之后,计算得到的倾角还是原来的那个角呢?不会变嘛?

Jersey
Jersey

Android下使用傅里叶变换检测出linenum=0是怎么回事,谢谢解惑!

ma
ma

您好,请问一下您使用的这个方法是有没有发过相应的文章,因为我的实验里用到了您的方法,论文的时候需要写这个方法的来源,所以想问一下您这个方法来自您自己发过的文章里的还是其他书本或期刊上的。谢谢您。

ma
ma

您好,看了您的方法后,我用C语言试用了一下,但是不知道为什么在HoughLines后,呈现出来的图象是全黑的。为了方便,我贴出这部分的代码,不知道哪里出了问题,想向您请教一下。谢谢。

 

CvMemStorage *storage  = cvCreateMemStorage(0);
 CvSeq* lines = NULL;
 float pi180 = (float)CV_PI/180;
 lines = cvHoughLines2(Image,storage,CV_HOUGH_STANDARD,1,pi180,HOUGH_VOTE,0,0); //Image是之前的二值化图象
IplImage *lineImg;
 lineImg = cvCreateImage(cvGetSize(src),IPL_DEPTH_8U,1);
 int numLines = lines->total;
 int i=0;
 for(i=0;i<numLines;i++)
   {
     float *line = (float*) cvGetSeqElem(lines,i);
     float rho = line[0];
     float theta = line[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));
     cvLine(lineImg,pt1,pt2,CV_RGB(255,0,0),3,8,0);
}
    cvNamedWindow("line",0);
    cvShowImage("line",lineImg);
    cvWaitKey(10000);

席大军
席大军

很好用的代码,十分感谢!

元冲
元冲

作者您好,我想做的是一幅图像经过傅里叶变换之后,分别得到实部和虚部的数据,看了您的文章,深受启发,但是遇到个问题,下面三步算是对原有图像填充的尺寸,但是填充值后傅里叶变换得到的虚部和实部尺寸也已改变,怎么让实部虚部数据都还原到填充前的尺寸呢?非常感谢!

int opWidth = getOptimalDFTSize(srcImg.rows);

int opHeight = getOptimalDFTSize(srcImg.cols);

copyMakeBorder(srcImg, padded, 0, opWidth-srcImg.rows, 0, opHeight-srcImg.cols, BORDER_CONST

Jason
Jason

是把高频部分移到中心位置吧?

wahaha893611361
wahaha893611361

试了一下 英文很好用,中文就不行了,葡萄牙语也不行,西班牙语还能凑合着用。总之,非常感谢博主分享。另有什么改善的方法么?

ET民工
ET民工

非常感谢如此详细的文章!

马东兴
马东兴

能不能判断出一个图片中的三角形倾斜的角度?