OpenCV基于傅里叶变换进行文本的旋转校正
本文描述一种利用OpenCV及傅里叶变换识别图片中文本旋转角度并自动校正的方法,由于对C#比较熟,因此本文将使用OpenCVSharp。 文章参考了http://johnhany.net/2013/11/dft-based-text-rotation-correction,对原作者表示感谢。我基于OpenCVSharp用C#进行了重写,希望能帮到同样用OpenCVSharp的同学。
================= 正文开始 =================
手里有一张图片如下,是经过旋转的,如何通过程序自动对它进行旋转校正? (旋转校正是行分割、字符识别等后续工作的基础)
傅里叶变换可以用于将图像从时域转换到频域,对于分行的文本,其频率谱上一定会有一定的特征,当图像旋转时,其频谱也会同步旋转,因此找出这个特征的倾角,就可以将图像旋转校正回去。
先来对原始图像进行一下傅里叶变换,需要这么几步:
1、以灰度方式读入原文件
stringfilename="source.jpg";varsrc=IplImage.FromFile(filename,LoadMode.GrayScale);
2、将图像扩展到合适的尺寸以方便快速变换
OpenCV中的DFT对图像尺寸有一定要求,需要用GetOptimalDFTSize方法来找到合适的大小,根据这个大小建立新的图像,把原图像拷贝过去,多出来的部分直接填充0。
intwidth=Cv.GetOptimalDFTSize(src.Width);intheight=Cv.GetOptimalDFTSize(src.Height);varpadded=newIplImage(width,height,BitDepth.U8,1);//扩展后的图像,单通道Cv.CopyMakeBorder(src,padded,newCvPoint(0,0),BorderType.Constant,CvScalar.ScalarAll(0));
3、进行DFT运算
DFT要分别计算实部和虚部,这里准备2个单通道的图像,实部从原图像中拷贝数据,虚部清零,然后把它们Merge为一个双通道图像再进行DFT计算,完成后再Split开。
//实部、虚部(单通道)varreal=newIplImage(padded.Size,BitDepth.F32,1);varimaginary=newIplImage(padded.Size,BitDepth.F32,1);//合成(双通道)varfourier=newIplImage(padded.Size,BitDepth.F32,2);//图像复制到实部,虚部清零Cv.ConvertScale(padded,real);Cv.Zero(imaginary);//合并、变换、再分解Cv.Merge(real,imaginary,null,null,fourier);Cv.DFT(fourier,fourier,DFTFlag.Forward);Cv.Split(fourier,real,imaginary,null,null);
4、对数据进行适当调整
上一步中得到的实部保留下来作为变换结果,并计算幅度:magnitude = sqrt(real^2 + imaginary^2)。
考虑到幅度变化范围很大,还要用log函数把数值范围缩小。
最后经过归一化,就会得到图像的特征谱了。
//计算sqrt(re^2+im^2),再存回reCv.Pow(real,real,2.0);Cv.Pow(imaginary,imaginary,2.0);Cv.Add(real,imaginary,real);Cv.Pow(real,real,0.5);//计算log(1+re),存回reCv.AddS(real,CvScalar.ScalarAll(1),real);Cv.Log(real,real);//归一化Cv.Normalize(real,real,0,1,NormType.MinMax);
此时图像是这样的:
5、移动中心
DFT操作的结果低频部分位于四角,高频部分在中心,习惯上会把频域原点调整到中心去,也就是把低频部分移动到中心。
///<summary>///将低频部分移动到图像中心///</summary>///<paramname="p_w_picpath"></param>///<remarks>///0|32|1///-------===>-------///1|23|0///</remarks>privatestaticvoidShiftDFT(IplImagep_w_picpath){introw=p_w_picpath.Height;intcol=p_w_picpath.Width;intcy=row/2;intcx=col/2;varq0=p_w_picpath.Clone(newCvRect(0,0,cx,cy));//左上varq1=p_w_picpath.Clone(newCvRect(0,cy,cx,cy));//左下varq2=p_w_picpath.Clone(newCvRect(cx,cy,cx,cy));//右下varq3=p_w_picpath.Clone(newCvRect(cx,0,cx,cy));//右上Cv.SetImageROI(p_w_picpath,newCvRect(0,0,cx,cy));q2.Copy(p_w_picpath);Cv.ResetImageROI(p_w_picpath);Cv.SetImageROI(p_w_picpath,newCvRect(0,cy,cx,cy));q3.Copy(p_w_picpath);Cv.ResetImageROI(p_w_picpath);Cv.SetImageROI(p_w_picpath,newCvRect(cx,cy,cx,cy));q0.Copy(p_w_picpath);Cv.ResetImageROI(p_w_picpath);Cv.SetImageROI(p_w_picpath,newCvRect(cx,0,cx,cy));q1.Copy(p_w_picpath);Cv.ResetImageROI(p_w_picpath);}
最终得到图像如下:
可以明显的看到过中心有一条倾斜的直线,可以用霍夫变换把它检测出来,然后计算角度。 需要以下几步:
1、二值化
把刚才得到的傅里叶谱放到0-255的范围,然后进行二值化,此处以150作为分界点。
Cv.Normalize(real,real,0,255,NormType.MinMax);Cv.Threshold(real,real,150,255,ThresholdType.Binary);
得到图像如下:
2、Houge直线检测
由于HoughLine2方法只接受8UC1格式的图片,因此要先进行转换再调用HoughLine2方法,这里的threshold参数取的100,能够检测出3条直线来。
//构造8UC1格式图像vargray=newIplImage(real.Size,BitDepth.U8,1);Cv.ConvertScale(real,gray);//找直线varstorage=Cv.CreateMemStorage();varlines=Cv.HoughLines2(gray,storage,HoughLinesMethod.Standard,1,Cv.PI/180,100);
3、找到符合条件的那条斜线,获取角度
floatangel=0f;floatpiThresh=(float)Cv.PI/90;floatpi2=(float)Cv.PI/2;for(inti=0;i<lines.Total;++i){//极坐标下的点,X是极径,Y是夹角,我们只关心夹角varp=lines.GetSeqElem<CvPoint2D32f>(i);floattheta=p.Value.Y;if(Math.Abs(theta)>=piThresh&&Math.Abs(theta-pi2)>=piThresh){angel=theta;break;}}angel=angel<pi2?angel:(angel-(float)Cv.PI);
4、角度转换
由于DFT的特点,只有输入图像是正方形时,检测到的角度才是真正文本的旋转角度,但原图像明显不是,因此还要根据长宽比进行变换,最后得到的angelD就是真正的旋转角度了。
if(angel!=pi2){floatangelT=(float)(src.Height*Math.Tan(angel)/src.Width);angel=(float)Math.Atan(angelT);}floatangelD=angel*180/(float)Cv.PI;
5、旋转校正
这一步比较简单了,构建一个仿射变换矩阵,然后调用WarpAffine进行变换,就得到校正后的图像了。最后显示到界面上。
varcenter=newCvPoint2D32f(src.Width/2.0,src.Height/2.0);//图像中心varrotMat=Cv.GetRotationMatrix2D(center,angelD,1.0);//构造仿射变换矩阵vardst=newIplImage(src.Size,BitDepth.U8,1);//执行变换,产生的空白部分用255填充,即纯白Cv.WarpAffine(src,dst,rotMat,Interpolation.Cubic|Interpolation.FillOutliers,CvScalar.ScalarAll(255));//展示using(varwin=newCvWindow("Rotation")){win.Image=dst;Cv.WaitKey();}
最终结果如下,效果还不错:
最后放完整代码:
usingSystem;usingSystem.Collections.Generic;usingSystem.IO;usingSystem.Text;usingOpenCvSharp;usingOpenCvSharp.Extensions;usingOpenCvSharp.Utilities;namespaceOpenCvTest{classProgram{staticvoidMain(string[]args){//以灰度方式读入原文件stringfilename="source.jpg";varsrc=IplImage.FromFile(filename,LoadMode.GrayScale);//转换到合适的大小,以适应快速变换intwidth=Cv.GetOptimalDFTSize(src.Width);intheight=Cv.GetOptimalDFTSize(src.Height);varpadded=newIplImage(width,height,BitDepth.U8,1);Cv.CopyMakeBorder(src,padded,newCvPoint(0,0),BorderType.Constant,CvScalar.ScalarAll(0));//实部、虚部(单通道)varreal=newIplImage(padded.Size,BitDepth.F32,1);varimaginary=newIplImage(padded.Size,BitDepth.F32,1);//合并(双通道)varfourier=newIplImage(padded.Size,BitDepth.F32,2);//图像复制到实部,虚部清零Cv.ConvertScale(padded,real);Cv.Zero(imaginary);//合并、变换、再分解Cv.Merge(real,imaginary,null,null,fourier);Cv.DFT(fourier,fourier,DFTFlag.Forward);Cv.Split(fourier,real,imaginary,null,null);//计算sqrt(re^2+im^2),再存回reCv.Pow(real,real,2.0);Cv.Pow(imaginary,imaginary,2.0);Cv.Add(real,imaginary,real);Cv.Pow(real,real,0.5);//计算log(1+re),存回reCv.AddS(real,CvScalar.ScalarAll(1),real);Cv.Log(real,real);//归一化,落入0-255范围Cv.Normalize(real,real,0,255,NormType.MinMax);//把低频移动到中心ShiftDFT(real);//二值化,以150作为分界点,经验值,需要根据实际情况调整Cv.Threshold(real,real,150,255,ThresholdType.Binary);//由于HoughLines2方法只接受8UC1格式的图片,因此进行转换vargray=newIplImage(real.Size,BitDepth.U8,1);Cv.ConvertScale(real,gray);//找直线,threshold参数取100,经验值,需要根据实际情况调整varstorage=Cv.CreateMemStorage();varlines=Cv.HoughLines2(gray,storage,HoughLinesMethod.Standard,1,Cv.PI/180,100);//找到符合条件的那条斜线floatangel=0f;floatpiThresh=(float)Cv.PI/90;floatpi2=(float)Cv.PI/2;for(inti=0;i<lines.Total;++i){//极坐标下的点,X是极径,Y是夹角,我们只关心夹角varp=lines.GetSeqElem<CvPoint2D32f>(i);floattheta=p.Value.Y;if(Math.Abs(theta)>=piThresh&&Math.Abs(theta-pi2)>=piThresh){angel=theta;break;}}angel=angel<pi2?angel:(angel-(float)Cv.PI);Cv.ReleaseMemStorage(storage);//转换角度if(angel!=pi2){floatangelT=(float)(src.Height*Math.Tan(angel)/src.Width);angel=(float)Math.Atan(angelT);}floatangelD=angel*180/(float)Cv.PI;Console.WriteLine("angtlD={0}",angelD);//旋转varcenter=newCvPoint2D32f(src.Width/2.0,src.Height/2.0);varrotMat=Cv.GetRotationMatrix2D(center,angelD,1.0);vardst=newIplImage(src.Size,BitDepth.U8,1);Cv.WarpAffine(src,dst,rotMat,Interpolation.Cubic|Interpolation.FillOutliers,CvScalar.ScalarAll(255));//显示using(varwindow=newCvWindow("Image")){window.Image=src;using(varwin2=newCvWindow("Dest")){win2.Image=dst;Cv.WaitKey();}}}///<summary>///将低频部分移动到图像中心///</summary>///<paramname="p_w_picpath"></param>///<remarks>///0|32|1///-------===>-------///1|23|0///</remarks>privatestaticvoidShiftDFT(IplImagep_w_picpath){introw=p_w_picpath.Height;intcol=p_w_picpath.Width;intcy=row/2;intcx=col/2;varq0=p_w_picpath.Clone(newCvRect(0,0,cx,cy));//左上varq1=p_w_picpath.Clone(newCvRect(0,cy,cx,cy));//左下varq2=p_w_picpath.Clone(newCvRect(cx,cy,cx,cy));//右下varq3=p_w_picpath.Clone(newCvRect(cx,0,cx,cy));//右上Cv.SetImageROI(p_w_picpath,newCvRect(0,0,cx,cy));q2.Copy(p_w_picpath);Cv.ResetImageROI(p_w_picpath);Cv.SetImageROI(p_w_picpath,newCvRect(0,cy,cx,cy));q3.Copy(p_w_picpath);Cv.ResetImageROI(p_w_picpath);Cv.SetImageROI(p_w_picpath,newCvRect(cx,cy,cx,cy));q0.Copy(p_w_picpath);Cv.ResetImageROI(p_w_picpath);Cv.SetImageROI(p_w_picpath,newCvRect(cx,0,cx,cy));q1.Copy(p_w_picpath);Cv.ResetImageROI(p_w_picpath);}}}
最后吐槽一下51cto的编译器,总是把代码的换行和缩进弄没,还要手工再处理一遍,真是受够了,难道是我打开的方式不对?
PS:最近增加了源码,因为加了opencv的dll,比较大,下载链接
http://down.51cto.com/data/2329576
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。