zoukankan      html  css  js  c++  java
  • OpenCV.Net基于傅里叶变换进行文本的旋转校正

    本文描述一种利用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),再存回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);
    
    //归一化
    Cv.Normalize(real, real, 0, 1, NormType.MinMax);

    此时图像是这样的:

    5、移动中心

      DFT操作的结果低频部分位于四角,高频部分在中心,习惯上会把频域原点调整到中心去,也就是把低频部分移动到中心。

    /// <summary>
    /// 将低频部分移动到图像中心
    /// </summary>
    /// <param name="p_w_picpath"></param>
    /// <remarks>
    ///  0 | 3         2 | 1
    /// -------  ===> -------
    ///  1 | 2         3 | 0
    /// </remarks>
    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参数取的90,能够检测出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, 90);

    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<CvPoint2D32f>(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参数取90,经验值,需要根据实际情况调整
                var storage = Cv.CreateMemStorage();
                var lines = Cv.HoughLines2(gray, storage, HoughLinesMethod.Standard, 1, Cv.PI / 180, 90);
                
                //找到符合条件的那条斜线
                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<CvPoint2D32f>(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();
                    }
                }
            }
            
            /// <summary>
            /// 将低频部分移动到图像中心
            /// </summary>
            /// <param name="p_w_picpath"></param>
            /// <remarks>
            ///  0 | 3         2 | 1
            /// -------  ===> -------
            ///  1 | 2         3 | 0
            /// </remarks>
            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);
            }
        }
    }
  • 相关阅读:
    POJ-1035 Spell checker---字符串处理
    hdu-3572 Task Schedule---最大流判断满流+dinic算法
    BZOJ4826: [Hnoi2017]影魔
    BZOJ4825: [Hnoi2017]单旋
    BZOJ3504: [Cqoi2014]危桥
    BZOJ4407: 于神之怒加强版
    BZOJ2818: Gcd
    BZOJ4542: [Hnoi2016]大数
    BZOJ4540: [Hnoi2016]序列
    BZOJ4537: [Hnoi2016]最小公倍数
  • 原文地址:https://www.cnblogs.com/BoyTNT/p/11887247.html
Copyright © 2011-2022 走看看