OpenCV实现基于傅里叶变换的旋转文本校正
代码
先给出代码,再详细解释一下过程:
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream> using namespacecv;
using namespacestd; #define GRAY_THRESH 150
#define HOUGH_VOTE 100 //#define DEGREE 27 intmain(intargc,char**argv)
{
//Read a single-channel image
constchar*filename="imageText.jpg";
Mat srcImg=imread(filename,CV_LOAD_IMAGE_GRAYSCALE);
if(srcImg.empty())
return-;
imshow("source",srcImg); Point center(srcImg.cols/,srcImg.rows/); #ifdef DEGREE
//Rotate source image
Mat rotMatS=getRotationMatrix2D(center,DEGREE,1.0);
warpAffine(srcImg,srcImg,rotMatS,srcImg.size(),,,Scalar(,,));
imshow("RotatedSrc",srcImg);
//imwrite("imageText_R.jpg",srcImg);
#endif //Expand image to an optimal size, for faster processing speed
//Set widths of borders in four directions
//If borderType==BORDER_CONSTANT, fill the borders with (0,0,0)
Mat padded;
intopWidth=getOptimalDFTSize(srcImg.rows);
intopHeight=getOptimalDFTSize(srcImg.cols);
copyMakeBorder(srcImg,padded,,opWidth-srcImg.rows,,opHeight-srcImg.cols,BORDER_CONSTANT,Scalar::all()); Mat planes[]={Mat_<float>(padded),Mat::zeros(padded.size(),CV_32F)};
Mat comImg;
//Merge into a double-channel image
merge(planes,,comImg); //Use the same image as input and output,
//so that the results can fit in Mat well
dft(comImg,comImg); //Compute the magnitude
//planes[0]=Re(DFT(I)), planes[1]=Im(DFT(I))
//magnitude=sqrt(Re^2+Im^2)
split(comImg,planes);
magnitude(planes[],planes[],planes[]); //Switch to logarithmic scale, for better visual results
//M2=log(1+M1)
Mat magMat=planes[];
magMat+=Scalar::all();
log(magMat,magMat); //Crop the spectrum
//Width and height of magMat should be even, so that they can be divided by 2
//-2 is 11111110 in binary system, operator & make sure width and height are always even
magMat=magMat(Rect(,,magMat.cols&-,magMat.rows&-)); //Rearrange the quadrants of Fourier image,
//so that the origin is at the center of image,
//and move the high frequency to the corners
intcx=magMat.cols/;
intcy=magMat.rows/; Mat q0(magMat,Rect(,,cx,cy));
Mat q1(magMat,Rect(,cy,cx,cy));
Mat q2(magMat,Rect(cx,cy,cx,cy));
Mat q3(magMat,Rect(cx,,cx,cy)); Mat tmp;
q0.copyTo(tmp);
q2.copyTo(q0);
tmp.copyTo(q2); q1.copyTo(tmp);
q3.copyTo(q1);
tmp.copyTo(q3); //Normalize the magnitude to [0,1], then to[0,255]
normalize(magMat,magMat,,,CV_MINMAX);
Mat magImg(magMat.size(),CV_8UC1);
magMat.convertTo(magImg,CV_8UC1,,);
imshow("magnitude",magImg);
//imwrite("imageText_mag.jpg",magImg); //Turn into binary image
threshold(magImg,magImg,GRAY_THRESH,,CV_THRESH_BINARY);
imshow("mag_binary",magImg);
//imwrite("imageText_bin.jpg",magImg); //Find lines with Hough Transformation
vector<Vec2f>lines;
floatpi180=(float)CV_PI/;
Mat linImg(magImg.size(),CV_8UC3);
HoughLines(magImg,lines,,pi180,HOUGH_VOTE,,);
intnumLines=lines.size();
for(intl=;l<numLines;l++)
{
floatrho=lines[l][],theta=lines[l][];
Point pt1,pt2;
doublea=cos(theta),b=sin(theta);
doublex0=a*rho,y0=b*rho;
pt1.x=cvRound(x0+*(-b));
pt1.y=cvRound(y0+*(a));
pt2.x=cvRound(x0-*(-b));
pt2.y=cvRound(y0-*(a));
line(linImg,pt1,pt2,Scalar(,,),,,);
}
imshow("lines",linImg);
//imwrite("imageText_line.jpg",linImg);
if(lines.size()==){
cout<<"found three angels:"<<endl;
cout<<lines[][]*/CV_PI<<endl<<lines[][]*/CV_PI<<endl<<lines[][]*/CV_PI<<endl<<endl;
} //Find the proper angel from the three found angels
floatangel=;
floatpiThresh=(float)CV_PI/;
floatpi2=CV_PI/;
for(intl=;l<numLines;l++)
{
floattheta=lines[l][];
if(abs(theta)<piThresh||abs(theta-pi2)<piThresh)
continue;
else{
angel=theta;
break;
}
} //Calculate the rotation angel
//The image has to be square,
//so that the rotation angel can be calculate right
angel=angel<pi2?angel:angel-CV_PI;
if(angel!=pi2){
floatangelT=srcImg.rows*tan(angel)/srcImg.cols;
angel=atan(angelT);
}
floatangelD=angel*/(float)CV_PI;
cout<<"the rotation angel to be applied:"<<endl<<angelD<<endl<<endl; //Rotate the image to recover
Mat rotMat=getRotationMatrix2D(center,angelD,1.0);
Mat dstImg=Mat::ones(srcImg.size(),CV_8UC3);
warpAffine(srcImg,dstImg,rotMat,srcImg.size(),,,Scalar(,,));
imshow("result",dstImg);
//imwrite("imageText_D.jpg",dstImg); waitKey(); return0;
}
过程
读取图片
Mat srcImg=imread(filename,CV_LOAD_IMAGE_GRAYSCALE);
if(srcImg.empty())
return-;
srcImg.empty()用来判断是否成功读进图像,如果srcImg中没有数据,在后面的步骤会产生内存错误。
由于处理的是文本,彩色信息不会提供额外帮助,所以要用CV_LOAD_IMAGE_GRAYSCALE表明以灰度形式读进图像。
假定读取的图像如下:
旋转原图像(可选)
Point center(srcImg.cols/, srcImg.rows/); #ifdef DEGREE
//Rotate source image
Mat rotMatS = getRotationMatrix2D(center, DEGREE, 1.0);
warpAffine(srcImg, srcImg, rotMatS, srcImg.size(), , , Scalar(,,));
imshow("RotatedSrc", srcImg);
//imwrite("H:\\imageText_02_R.jpg",srcImg);
#endif
如果手头没有这样的倾斜图像,可以选择一张正放的文本图像,再把第12行#define DEGREE那行前的注释符号去掉。然后这部分代码就会把所给的图像旋转你规定的角度,再交给后面处理。
图像延扩
Mat padded;
int opWidth = getOptimalDFTSize(srcImg.rows);
int opHeight = getOptimalDFTSize(srcImg.cols);
copyMakeBorder(srcImg, padded, , opWidth-srcImg.rows, , opHeight-srcImg.cols, BORDER_CONSTANT, Scalar::all());
OpenCV中的DFT采用的是快速算法,这种算法要求图像的尺寸是2、3和5的倍数时处理速度最快。所以需要用getOptimalDFTSize()找到最适合的尺寸,然后用copyMakeBorder()填充多余的部分。这里是让原图像和扩大的图像左上角对齐。填充的颜色如果是纯色对变换结果的影响不会很大,后面寻找倾斜线的过程又会完全忽略这一点影响。
DFT
Mat planes[]={Mat_<float>(padded),Mat::zeros(padded.size(),CV_32F)};
Mat comImg; merge(planes,,comImg); dft(comImg,comImg);
DFT要分别计算实部和虚部,把要处理的图像作为输入的实部、一个全零的图像作为输入的虚部。dft()输入和输出应该分别为单张图像,所以要先用merge()把实虚部图像合并,分别处于图像comImg的两个通道内。计算得到的实虚部仍然保存在comImg的两个通道内。
获得DFT图像
split(comImg, planes);
magnitude(planes[], planes[], planes[]); Mat magMat = planes[];
magMat += Scalar::all();
log(magMat, magMat);
一般都会用幅度图像来表示图像傅里叶的变换结果(傅里叶谱)。
幅度的计算公式:magnitude = sqrt(Re(DFT)^2 + Im(DFT)^2)。
由于幅度的变化范围很大,而一般图像亮度范围只有[0,255],容易造成一大片漆黑,只有几个点很亮。所以要用log函数把数值的范围缩小。
magMat = magMat(Rect(, , magMat.cols & -, magMat.rows & -)); int cx = magMat.cols/;
int cy = magMat.rows/; Mat q0(magMat, Rect(, , cx, cy));
Mat q1(magMat, Rect(, cy, cx, cy));
Mat q2(magMat, Rect(cx, cy, cx, cy));
Mat q3(magMat, Rect(cx, , cx, cy)); Mat tmp;
q0.copyTo(tmp);
q2.copyTo(q0);
tmp.copyTo(q2); q1.copyTo(tmp);
q3.copyTo(q1);
tmp.copyTo(q3); normalize(magMat, magMat, , , CV_MINMAX);
Mat magImg(magMat.size(), CV_8UC1);
magMat.convertTo(magImg,CV_8UC1,,);
dft()直接获得的结果中,低频部分位于四角,高频部分位于中间。习惯上会把图像做四等份,互相对调,使低频部分位于图像中心,也就是让频域原点位于中心。
虽然用log()缩小了数据范围,但仍然不能保证数值都落在[0,255]之内,所以要先用normalize()规范化到[0,1]内,再用convertTo()把小数映射到[0,255]内的整数。结果保存在一幅单通道图像内:
Hough直线检测
从傅里叶谱可以明显地看到一条过中心点的倾斜直线。要想求出这个倾斜角,首先要在图像上找出这条直线。
一个很方便的方法是采用霍夫(Hough)变换检测直线。
threshold(magImg,magImg,GRAY_THRESH,,CV_THRESH_BINARY);
Hough变换要求输入图像是二值的,所以要用threshold()把图像二值化。
二值化的一种结果:
vector<Vec2f> lines;
float pi180 = (float)CV_PI/;
Mat linImg(magImg.size(),CV_8UC3);
HoughLines(magImg,lines,,pi180,HOUGH_VOTE,,);
int numLines = lines.size();
for(int l=; l<numLines; l++)
{
float rho = lines[l][], theta = lines[l][];
Point pt1, pt2;
double a = cos(theta), b = sin(theta);
double x0 = a*rho, y0 = b*rho;
pt1.x = cvRound(x0 + *(-b));
pt1.y = cvRound(y0 + *(a));
pt2.x = cvRound(x0 - *(-b));
pt2.y = cvRound(y0 - *(a));
line(linImg,pt1,pt2,Scalar(,,),,,);
}
这一部分用HoughLines()检测图像中可能存在的直线,并把直线参数保存在向量组lines中,然后绘制出找到的直线。
两个参数GRAY_THRESH和HOUGH_VOTE需要手动指定,不同的图像需要设置不同的参数,同一段文本旋转不同的角度也需要不同的参数。GRAY_THRESH越大,二值化的阈值就越高;HOUGH_VOTE越大,霍夫检测的投票数就越高(需要更多的共线点来确定一条直线)。说白了,如果发现二值化图像中直线附近有很多散点,就要适当提高GRAY_THRESH;如果发现从二值图像的一条直线上检测到了几条角度相差很小的直线,就需要适当提高HOUGH_VOTE。我们希望得到的结果时刚好检测到三条直线(有时只能检测到一条直线,后面会给出一个例子)。
检测到的直线:
计算倾斜角
上面得到了三个角度,一个是0度,一个是90度,另一个就是我们所需要的倾斜角。要把这个角找出来,而且要考虑误差。
float angel=;
float piThresh = (float)CV_PI/;
float pi2 = CV_PI/;
for(int l=; l<numLines; l++)
{
float theta = lines[l][];
if(abs(theta) < piThresh || abs(theta-pi2) < piThresh)
continue;
else{
angel = theta;
break;
}
} angel = angel<pi2 ? angel : angel-CV_PI;
if(angel != pi2){
float angelT = srcImg.rows*tan(angel)/srcImg.cols;
angel = atan(angelT);
}
float angelD = angel*/(float)CV_PI;
由于DFT的特点,只有输入图像是正方形时,检测到的角才是文本真正旋转的角度。但我们的输入图像不一定是正方形的,所以要根据图像的长宽比改变这个角度。
还有一个需要注意的细节,虽然HoughLines()输出的倾斜角在[0,180)之间,但在[0,90]和(90,180)之间这个角的含义是不同的。请看图示:
当倾斜角大于90度时,(180-倾斜角)才是直线相对竖直方向的偏离角度。在OpenCV中,逆时针旋转,角度为正。要把图像转回去,这个角度就变成了(倾斜角-180)。
校正图像
最后一步,当然是把图像转回去~
Mat rotMat = getRotationMatrix2D(center,angelD,1.0);
Mat dstImg = Mat::ones(srcImg.size(),CV_8UC3);
warpAffine(srcImg,dstImg,rotMat,srcImg.size(),,,Scalar(,,));
先用getRotationMatrix2D()获得一个2*3的仿射变换矩阵,再把这个矩阵输入warpAffine(),做一个单纯旋转的仿射变换。warpAffine()的最后一个参数Scalar(255,255,255)是把由于旋转产生的空白用白色填充。
校正的结果:
一个检测单条直线的例子
原始图像:
傅里叶谱:
只有一条明显的直线。还好仅有的这条直线正是我们所需要的。
检测直线:
校正结果:
对中文的效果
我们来试试看这段程序对中文的校正效果。
输入图像:
傅里叶谱:
可以发现有许多条平行的亮线,其中过频域原点的那条长度最长,最容易检测出来。
检测直线:
校正结果:
虽然中文和英文在文字上有很大的不同,但字母(或者文字)的高度比较一致,使得行与行之间的分隔很明显。所以它们的频域特征是相似的。
对其他语言文字的效果
我从IMDB.com摘取影片《教父》的英文介绍,然后用谷歌翻译成其他文字进行测试。
阿拉伯语
一枚反例
老挝语:
傅里叶谱:
一种二值化的结果:
直线检测:
这种文字的很多字母的上下方多了很多“笔画”(我不知道该怎么称呼那些小曲线),让行与行之间的分离变得不明显,使得频域特征变得不明显。
虽然用肉眼可以看出傅里叶谱中存在一条倾斜的直线,但它的亮度太低,二值化过程很难排除噪声,导致直线检测会首先检出噪声产生的直线。这也是我的程序目前受限之处。需要增加一个过滤散点噪声的步骤以增加程序的适用范围。
参考:Discrete Fourier Transform — OpenCV 2.4.7.0 documentation
代码还可以在这里下载:https://github.com/johnhany/textRotCorrect
2014.1.3更新:
由于文章内的图片右下角存在水印,若直接使用文章内的图片进行处理会使频域原点附近增加一团亮点,妨碍直线的检出。而且为了节省空间,图片是经过缩小的,使得字母的边缘变得模糊,频域特征也减弱。为此我提供了十幅没有水印的图片,供想要亲手实验的朋友使用。下载链接
OpenCV实现基于傅里叶变换的旋转文本校正的更多相关文章
- OpenCV.Net基于傅里叶变换进行文本的旋转校正
本文描述一种利用OpenCV及傅里叶变换识别图片中文本旋转角度并自动校正的方法,由于对C#比较熟,因此本文将使用OpenCVSharp. 文章参考了http://johnhany.net/2013/1 ...
- Arctext.js - 基于 CSS3 & jQuery 的文本弯曲效果
Arctext.js 是基于 Lettering.js 的文本旋转插件,根据设置的旋转半径准确计算每个字母的旋转弧度并均匀分布.虽然 CSS3 也能够实现字符旋转效果,但是要让安排每个字母都沿着弯曲路 ...
- 推荐20款基于 jQuery & CSS 的文本效果插件
jQuery 和 CSS 可以说是设计和开发行业的一次革命.这一切如此简单,快捷的一站式服务.jQuery 允许你在你的网页中添加一些真正令人惊叹的东西而不用付出很大的努力,要感谢那些优秀的 jQue ...
- 基于jQuery右下角旋转环状菜单代码
基于jQuery右下角旋转环状菜单代码.这是一款固定在页面的右下角位置,当用户点击了主菜单按钮后,子菜单项会以环状旋转进入页面,并使用animate.css来制作动画效果.效果图如下: 在线预览 ...
- MiniGUI文档参考手册 基于v1.6.10文本
MiniGUI各种功能都分布在预先定义宏对每个文档标题.特别不方便查找,这是不利于初学者学习. 有一天,我发现doxygen,因此,使用该工具可以生成一个minigui参考文献 .基于v1.6.10文 ...
- 基于jQuery 3D旋转明星人物展示特效
分享一款基于jQuery 3D旋转明星人物展示特效.这是一款来自百度换肤活动的明星旋转展示效果.效果图如下: 在线预览 源码下载 实现的代码. html代码: <div class=&quo ...
- 基于Spark Mllib的文本分类
基于Spark Mllib的文本分类 文本分类是一个典型的机器学习问题,其主要目标是通过对已有语料库文本数据训练得到分类模型,进而对新文本进行类别标签的预测.这在很多领域都有现实的应用场景,如新闻网站 ...
- canvas旋转文本
canvas旋转文本 <!DOCTYPE html> <html lang="en"> <head> <meta charset=&quo ...
- 基于傅里叶变换的音频重采样算法 (附完整c代码)
前面有提到音频采样算法: WebRTC 音频采样算法 附完整C++示例代码 简洁明了的插值音频重采样算法例子 (附完整C代码) 近段时间有不少朋友给我写过邮件,说了一些他们使用的情况和问题. 坦白讲, ...
随机推荐
- 完美解决Bootstrap4 导航栏 fixed-top 后,锚点定位时遮挡问题
利用锚点改变事件\(onhashchange\),使用jQuery的\(scrollTop\)向前滚回导航栏的高度(比如我的100个像素) HTML: <body onhashchange=&q ...
- SpringMVC之RequestMappingHandlerMapping
<mvc:annotation-driven content-negotiation-manager="" enable-matrix-variables="tru ...
- Mac 10.12安装Windows远程桌面工具Microsoft Remote Desktop
说明:之前Office自带的Windows远程桌面工具虽然简便,但是保存的服务器列表有限.而这个微软推出的自家工具可以完美解决这些问题. 下载: (链接:https://pan.baidu.com/s ...
- Javascript之in操作符的用法
in操作符是js里面常用的一个操作符,下面是其几个常用的功能: 1.配合for语句循环遍历/迭代数组中的元素 2.配合for语句循环遍历/迭代集合中的属性 3.判断对象是否是数组的元素 4.判断对象是 ...
- jsf和facelets的生命周期
一.JSF生命周期 JSF是基于事件驱动.JSF生命周期分为两个主要阶段:执行阶段和渲染阶段. 1.执行阶段 分为六个阶段: 恢复视图阶段 当客户端请求一个JavaServer Faces页面时,Ja ...
- web开发之缓存
以数据为驱动的web站点,当访问量增大后,由于频繁的从DB中读取数据,使得DB服务器的压力大增,从而影响系统的性能.为了缓解这种来自于大访问量的频繁读取DB的压力,我们可以把一些数据缓存起来,当请求过 ...
- 设置tomcat字符编码
Tomcat的默认编码是ISO-8859-1,如果有是get请求时,会出现乱码,这种情况可以修改Tomcat的编码解决,当然也可以写个过滤器来解决. 在tomcat的conf目录下,编辑server. ...
- HTML5--(2)属性选择器+结构性伪类+伪类
一.属性选择器 [att] 匹配所有具有att属性的 [att=val] 匹配所有att属性等于“val”的 [att~=val] 匹配所有att属性包含“val”或者等于“val”的(val必须是一 ...
- RabbitMQ---1、安装与部署
一.下载资源 Rabbit MQ 是建立在强大的Erlang OTP平台上,因此安装Rabbit MQ的前提是安装Erlang.(在官网自行选择版本) 1.otp_win64_20.2.exe 下载地 ...
- 多边形游戏(DP)
Description 多边形游戏是一个单人玩的游戏,开始时有一个由n个顶点构成的多边形.每个顶点被赋予一个整数值,每条边被赋予一个运算符 "+" 或 "*". ...