热门IT资讯网

OpenCV基于傅里叶变换进行文本的旋转校正

发表于:2024-11-25 作者:热门IT资讯网编辑
编辑最后更新 2024年11月25日,本文描述一种利用OpenCV及傅里叶变换识别图片中文本旋转角度并自动校正的方法,由于对C#比较熟,因此本文将使用OpenCVSharp。 文章参考了http://johnhany.net/2013/1

本文描述一种利用OpenCV及傅里叶变换识别图片中文本旋转角度并自动校正的方法,由于对C#比较熟,因此本文将使用OpenCVSharp。 文章参考了http://johnhany.net/2013/11/dft-based-text-rotation-correction,对原作者表示感谢。我基于OpenCVSharp用C#进行了重写,希望能帮到同样用OpenCVSharp的同学。


================= 正文开始 =================


手里有一张图片如下,是经过旋转的,如何通过程序自动对它进行旋转校正? (旋转校正是行分割、字符识别等后续工作的基础)


傅里叶变换可以用于将图像从时域转换到频域,对于分行的文本,其频率谱上一定会有一定的特征,当图像旋转时,其频谱也会同步旋转,因此找出这个特征的倾角,就可以将图像旋转校正回去。


先来对原始图像进行一下傅里叶变换,需要这么几步:


1、以灰度方式读入原文件

string filename = "source.jpg";var src = IplImage.FromFile(filename, LoadMode.GrayScale);


2、将图像扩展到合适的尺寸以方便快速变换

OpenCV中的DFT对图像尺寸有一定要求,需要用GetOptimalDFTSize方法来找到合适的大小,根据这个大小建立新的图像,把原图像拷贝过去,多出来的部分直接填充0。

int width = Cv.GetOptimalDFTSize(src.Width);int height = Cv.GetOptimalDFTSize(src.Height);var padded = new IplImage(width, height, BitDepth.U8, 1);//扩展后的图像,单通道Cv.CopyMakeBorder(src, padded, new CvPoint(0, 0), BorderType.Constant, CvScalar.ScalarAll(0));


3、进行DFT运算

DFT要分别计算实部和虚部,这里准备2个单通道的图像,实部从原图像中拷贝数据,虚部清零,然后把它们Merge为一个双通道图像再进行DFT计算,完成后再Split开。

//实部、虚部(单通道)var real = new IplImage(padded.Size, BitDepth.F32, 1);var imaginary = new IplImage(padded.Size, BitDepth.F32, 1);//合成(双通道)var fourier = new IplImage(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操作的结果低频部分位于四角,高频部分在中心,习惯上会把频域原点调整到中心去,也就是把低频部分移动到中心。

/// /// 将低频部分移动到图像中心/// /// /// ///  0 | 3         2 | 1/// -------  ===> -------///  1 | 2         3 | 0/// private static void ShiftDFT(IplImage p_w_picpath){    int row = p_w_picpath.Height;    int col = p_w_picpath.Width;    int cy = row / 2;    int cx = col / 2;        var q0 = p_w_picpath.Clone(new CvRect(0, 0, cx, cy));   //左上    var q1 = p_w_picpath.Clone(new CvRect(0, cy, cx, cy));  //左下    var q2 = p_w_picpath.Clone(new CvRect(cx, cy, cx, cy)); //右下    var q3 = p_w_picpath.Clone(new CvRect(cx, 0, cx, cy));  //右上        Cv.SetImageROI(p_w_picpath, new CvRect(0, 0, cx, cy));    q2.Copy(p_w_picpath);    Cv.ResetImageROI(p_w_picpath);        Cv.SetImageROI(p_w_picpath, new CvRect(0, cy, cx, cy));    q3.Copy(p_w_picpath);    Cv.ResetImageROI(p_w_picpath);        Cv.SetImageROI(p_w_picpath, new CvRect(cx, cy, cx, cy));    q0.Copy(p_w_picpath);    Cv.ResetImageROI(p_w_picpath);        Cv.SetImageROI(p_w_picpath, new CvRect(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格式图像var gray = new IplImage(real.Size, BitDepth.U8, 1);Cv.ConvertScale(real, gray);//找直线var storage = Cv.CreateMemStorage();var lines = Cv.HoughLines2(gray, storage, HoughLinesMethod.Standard, 1, Cv.PI / 180, 100);


3、找到符合条件的那条斜线,获取角度

float angel = 0f;float piThresh = (float)Cv.PI / 90;float pi2 = (float)Cv.PI / 2;for (int i = 0; i < lines.Total; ++i){    //极坐标下的点,X是极径,Y是夹角,我们只关心夹角    var p = lines.GetSeqElem(i);    float theta = 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){    float angelT = (float)(src.Height * Math.Tan(angel) / src.Width);    angel = (float)Math.Atan(angelT);}float angelD = angel * 180 / (float)Cv.PI;


5、旋转校正

这一步比较简单了,构建一个仿射变换矩阵,然后调用WarpAffine进行变换,就得到校正后的图像了。最后显示到界面上。

var center = new CvPoint2D32f(src.Width / 2.0, src.Height / 2.0);//图像中心var rotMat = Cv.GetRotationMatrix2D(center, angelD, 1.0);//构造仿射变换矩阵var dst = new IplImage(src.Size, BitDepth.U8, 1);//执行变换,产生的空白部分用255填充,即纯白Cv.WarpAffine(src, dst, rotMat, Interpolation.Cubic | Interpolation.FillOutliers, CvScalar.ScalarAll(255));//展示using (var win = new CvWindow("Rotation")){    win.Image = dst;    Cv.WaitKey();}


最终结果如下,效果还不错:


最后放完整代码:

using System;using System.Collections.Generic;using System.IO;using System.Text;using OpenCvSharp;using OpenCvSharp.Extensions;using OpenCvSharp.Utilities;namespace OpenCvTest{    class Program    {        static void Main(string[] args)        {            //以灰度方式读入原文件            string filename = "source.jpg";            var src = IplImage.FromFile(filename, LoadMode.GrayScale);            //转换到合适的大小,以适应快速变换            int width = Cv.GetOptimalDFTSize(src.Width);            int height = Cv.GetOptimalDFTSize(src.Height);            var padded = new IplImage(width, height, BitDepth.U8, 1);            Cv.CopyMakeBorder(src, padded, new CvPoint(0, 0), BorderType.Constant, CvScalar.ScalarAll(0));                        //实部、虚部(单通道)            var real = new IplImage(padded.Size, BitDepth.F32, 1);            var imaginary = new IplImage(padded.Size, BitDepth.F32, 1);            //合并(双通道)            var fourier = new IplImage(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),再存回re            Cv.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),存回re            Cv.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格式的图片,因此进行转换            var gray = new IplImage(real.Size, BitDepth.U8, 1);            Cv.ConvertScale(real, gray);                        //找直线,threshold参数取100,经验值,需要根据实际情况调整            var storage = Cv.CreateMemStorage();            var lines = Cv.HoughLines2(gray, storage, HoughLinesMethod.Standard, 1, Cv.PI / 180, 100);                        //找到符合条件的那条斜线            float angel = 0f;            float piThresh = (float)Cv.PI / 90;            float pi2 = (float)Cv.PI / 2;            for (int i = 0; i < lines.Total; ++i)            {                //极坐标下的点,X是极径,Y是夹角,我们只关心夹角                var p = lines.GetSeqElem(i);                float theta = 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)            {                float angelT = (float)(src.Height * Math.Tan(angel) / src.Width);                angel = (float)Math.Atan(angelT);            }            float angelD = angel * 180 / (float)Cv.PI;            Console.WriteLine("angtlD = {0}", angelD);            //旋转            var center = new CvPoint2D32f(src.Width / 2.0, src.Height / 2.0);            var rotMat = Cv.GetRotationMatrix2D(center, angelD, 1.0);            var dst = new IplImage(src.Size, BitDepth.U8, 1);            Cv.WarpAffine(src, dst, rotMat, Interpolation.Cubic | Interpolation.FillOutliers, CvScalar.ScalarAll(255));                        //显示            using (var window = new CvWindow("Image"))            {                window.Image = src;                using (var win2 = new CvWindow("Dest"))                {                    win2.Image = dst;                    Cv.WaitKey();                }            }        }                ///         /// 将低频部分移动到图像中心        ///         ///         ///         ///  0 | 3         2 | 1        /// -------  ===> -------        ///  1 | 2         3 | 0        ///         private static void ShiftDFT(IplImage p_w_picpath)        {            int row = p_w_picpath.Height;            int col = p_w_picpath.Width;            int cy = row / 2;            int cx = col / 2;                        var q0 = p_w_picpath.Clone(new CvRect(0, 0, cx, cy));//左上            var q1 = p_w_picpath.Clone(new CvRect(0, cy, cx, cy));//左下            var q2 = p_w_picpath.Clone(new CvRect(cx, cy, cx, cy));//右下            var q3 = p_w_picpath.Clone(new CvRect(cx, 0, cx, cy));//右上                        Cv.SetImageROI(p_w_picpath, new CvRect(0, 0, cx, cy));            q2.Copy(p_w_picpath);            Cv.ResetImageROI(p_w_picpath);                        Cv.SetImageROI(p_w_picpath, new CvRect(0, cy, cx, cy));            q3.Copy(p_w_picpath);            Cv.ResetImageROI(p_w_picpath);                        Cv.SetImageROI(p_w_picpath, new CvRect(cx, cy, cx, cy));            q0.Copy(p_w_picpath);            Cv.ResetImageROI(p_w_picpath);                        Cv.SetImageROI(p_w_picpath, new CvRect(cx, 0, cx, cy));            q1.Copy(p_w_picpath);            Cv.ResetImageROI(p_w_picpath);        }    }}



最后吐槽一下51cto的编译器,总是把代码的换行和缩进弄没,还要手工再处理一遍,真是受够了,难道是我打开的方式不对?


PS:最近增加了源码,因为加了opencv的dll,比较大,下载链接

http://down.51cto.com/data/2329576



0