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


    wKiom1Wx7WaTkCKUAAP8FR_1gxY697.jpg


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


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


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

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


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

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

    1
    2
    3
    4
    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开。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //实部、虚部(单通道)
    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, nullnull, fourier);
    Cv.DFT(fourier, fourier, DFTFlag.Forward);
    Cv.Split(fourier, real, imaginary, nullnull);


    4、对数据进行适当调整

      上一步中得到的实部保留下来作为变换结果,并计算幅度:magnitude = sqrt(real^2 + imaginary^2)。

      考虑到幅度变化范围很大,还要用log函数把数值范围缩小。

      最后经过归一化,就会得到图像的特征谱了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //计算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);


    此时图像是这样的:

    wKioL1Wx8Hrw4_DKAASfCPJ9KK4456.jpg


    5、移动中心

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    /// <summary>
    /// 将低频部分移动到图像中心
    /// </summary>
    /// <param name="image"></param>
    /// <remarks>
    ///  0 | 3         2 | 1
    /// -------  ===> -------
    ///  1 | 2         3 | 0
    /// </remarks>
    private static void ShiftDFT(IplImage image)
    {
        int row = image.Height;
        int col = image.Width;
        int cy = row / 2;
        int cx = col / 2;
         
        var q0 = image.Clone(new CvRect(0, 0, cx, cy));   //左上
        var q1 = image.Clone(new CvRect(0, cy, cx, cy));  //左下
        var q2 = image.Clone(new CvRect(cx, cy, cx, cy)); //右下
        var q3 = image.Clone(new CvRect(cx, 0, cx, cy));  //右上
         
        Cv.SetImageROI(image, new CvRect(0, 0, cx, cy));
        q2.Copy(image);
        Cv.ResetImageROI(image);
         
        Cv.SetImageROI(image, new CvRect(0, cy, cx, cy));
        q3.Copy(image);
        Cv.ResetImageROI(image);
         
        Cv.SetImageROI(image, new CvRect(cx, cy, cx, cy));
        q0.Copy(image);
        Cv.ResetImageROI(image);
         
        Cv.SetImageROI(image, new CvRect(cx, 0, cx, cy));
        q1.Copy(image);
        Cv.ResetImageROI(image);
    }

    最终得到图像如下:

    wKioL1Wx8Jah8u2hAASfEauPYhA310.jpg


    可以明显的看到过中心有一条倾斜的直线,可以用霍夫变换把它检测出来,然后计算角度。 需要以下几步:


    1、二值化

      把刚才得到的傅里叶谱放到0-255的范围,然后进行二值化,此处以150作为分界点。

    1
    2
    Cv.Normalize(real, real, 0, 255, NormType.MinMax);
    Cv.Threshold(real, real, 150, 255, ThresholdType.Binary);

     得到图像如下:

    wKioL1Wx8NPRQYgUAACELmMFysQ406.jpg


    2、Houge直线检测

      由于HoughLine2方法只接受8UC1格式的图片,因此要先进行转换再调用HoughLine2方法,这里的threshold参数取的100,能够检测出3条直线来。

    1
    2
    3
    4
    5
    6
    7
    //构造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、找到符合条件的那条斜线,获取角度

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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就是真正的旋转角度了。

    1
    2
    3
    4
    5
    6
    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进行变换,就得到校正后的图像了。最后显示到界面上。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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();
    }


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

    wKiom1Wx8QOjPEd8AAL5za5_XCA781.jpg


    最后放完整代码:


    1. using System;
    2. using System.Collections.Generic;
    3. using System.IO;
    4. using System.Text;
    5. using OpenCvSharp;
    6. using OpenCvSharp.Extensions;
    7. using OpenCvSharp.Utilities;
    8. namespace OpenCvTest
    9. {
    10. class Program
    11. {
    12. static void Main(string[] args)
    13. {
    14. //以灰度方式读入原文件
    15. string filename = "source.jpg";
    16. var src = IplImage.FromFile(filename, LoadMode.GrayScale);
    17. //转换到合适的大小,以适应快速变换
    18. int width = Cv.GetOptimalDFTSize(src.Width);
    19. int height = Cv.GetOptimalDFTSize(src.Height);
    20. var padded = new IplImage(width, height, BitDepth.U8, 1);
    21. Cv.CopyMakeBorder(src, padded, new CvPoint(0, 0), BorderType.Constant, CvScalar.ScalarAll(0));
    22. //实部、虚部(单通道)
    23. var real = new IplImage(padded.Size, BitDepth.F32, 1);
    24. var imaginary = new IplImage(padded.Size, BitDepth.F32, 1);
    25. //合并(双通道)
    26. var fourier = new IplImage(padded.Size, BitDepth.F32, 2);
    27. //图像复制到实部,虚部清零
    28. Cv.ConvertScale(padded, real);
    29. Cv.Zero(imaginary);
    30. //合并、变换、再分解
    31. Cv.Merge(real, imaginary, null, null, fourier);
    32. Cv.DFT(fourier, fourier, DFTFlag.Forward);
    33. Cv.Split(fourier, real, imaginary, null, null);
    34. //计算sqrt(re^2+im^2),再存回re
    35. Cv.Pow(real, real, 2.0);
    36. Cv.Pow(imaginary, imaginary, 2.0);
    37. Cv.Add(real, imaginary, real);
    38. Cv.Pow(real, real, 0.5);
    39. //计算log(1+re),存回re
    40. Cv.AddS(real, CvScalar.ScalarAll(1), real);
    41. Cv.Log(real, real);
    42. //归一化,落入0-255范围
    43. Cv.Normalize(real, real, 0, 255, NormType.MinMax);
    44. //把低频移动到中心
    45. ShiftDFT(real);
    46. //二值化,以150作为分界点,经验值,需要根据实际情况调整
    47. Cv.Threshold(real, real, 150, 255, ThresholdType.Binary);
    48. //由于HoughLines2方法只接受8UC1格式的图片,因此进行转换
    49. var gray = new IplImage(real.Size, BitDepth.U8, 1);
    50. Cv.ConvertScale(real, gray);
    51. //找直线,threshold参数取100,经验值,需要根据实际情况调整
    52. var storage = Cv.CreateMemStorage();
    53. var lines = Cv.HoughLines2(gray, storage, HoughLinesMethod.Standard, 1, Cv.PI / 180, 100);
    54. //找到符合条件的那条斜线
    55. float angel = 0f;
    56. float piThresh = (float)Cv.PI / 90;
    57. float pi2 = (float)Cv.PI / 2;
    58. for (int i = 0; i < lines.Total; ++i)
    59. {
    60. //极坐标下的点,X是极径,Y是夹角,我们只关心夹角
    61. var p = lines.GetSeqElem<CvPoint2D32f>(i);
    62. float theta = p.Value.Y;
    63. if (Math.Abs(theta) >= piThresh && Math.Abs(theta - pi2) >= piThresh)
    64. {
    65. angel = theta;
    66. break;
    67. }
    68. }
    69. angel = angel < pi2 ? angel : (angel - (float)Cv.PI);
    70. Cv.ReleaseMemStorage(storage);
    71. //转换角度
    72. if (angel != pi2)
    73. {
    74. float angelT = (float)(src.Height * Math.Tan(angel) / src.Width);
    75. angel = (float)Math.Atan(angelT);
    76. }
    77. float angelD = angel * 180 / (float)Cv.PI;
    78. Console.WriteLine("angtlD = {0}", angelD);
    79. //旋转
    80. var center = new CvPoint2D32f(src.Width / 2.0, src.Height / 2.0);
    81. var rotMat = Cv.GetRotationMatrix2D(center, angelD, 1.0);
    82. var dst = new IplImage(src.Size, BitDepth.U8, 1);
    83. Cv.WarpAffine(src, dst, rotMat, Interpolation.Cubic | Interpolation.FillOutliers, CvScalar.ScalarAll(255));
    84. //显示
    85. using (var window = new CvWindow("Image"))
    86. {
    87. window.Image = src;
    88. using (var win2 = new CvWindow("Dest"))
    89. {
    90. win2.Image = dst;
    91. Cv.WaitKey();
    92. }
    93. }
    94. }
    95. /// <summary>
    96. /// 将低频部分移动到图像中心
    97. /// </summary>
    98. /// <param name="image"></param>
    99. /// <remarks>
    100. /// 0 | 3 2 | 1
    101. /// ------- ===> -------
    102. /// 1 | 2 3 | 0
    103. /// </remarks>
    104. private static void ShiftDFT(IplImage image)
    105. {
    106. int row = image.Height;
    107. int col = image.Width;
    108. int cy = row / 2;
    109. int cx = col / 2;
    110. var q0 = image.Clone(new CvRect(0, 0, cx, cy));//左上
    111. var q1 = image.Clone(new CvRect(0, cy, cx, cy));//左下
    112. var q2 = image.Clone(new CvRect(cx, cy, cx, cy));//右下
    113. var q3 = image.Clone(new CvRect(cx, 0, cx, cy));//右上
    114. Cv.SetImageROI(image, new CvRect(0, 0, cx, cy));
    115. q2.Copy(image);
    116. Cv.ResetImageROI(image);
    117. Cv.SetImageROI(image, new CvRect(0, cy, cx, cy));
    118. q3.Copy(image);
    119. Cv.ResetImageROI(image);
    120. Cv.SetImageROI(image, new CvRect(cx, cy, cx, cy));
    121. q0.Copy(image);
    122. Cv.ResetImageROI(image);
    123. Cv.SetImageROI(image, new CvRect(cx, 0, cx, cy));
    124. q1.Copy(image);
    125. Cv.ResetImageROI(image);
    126. }
    127. }
    128. }






    附件列表

    • 相关阅读:
      JS的数据类型
      JS瀑布流布局模式(2)
      Morris Traversal方法遍历二叉树(非递归,不用栈,O(1)空间)——无非是在传统遍历过程中修改叶子结点加入后继结点信息(传统是stack记录),然后再删除恢复
      leetcode 538. Convert BST to Greater Tree
      python 闭包变量不允许write,要使用nonlocal
      机器学习中,有哪些特征选择的工程方法?
      python利用决策树进行特征选择
      机器学习 不均衡数据的处理方法
      python dns server开源列表 TODO
      python dig trace 功能实现——通过Querying name server IP来判定是否为dns tunnel
    • 原文地址:https://www.cnblogs.com/jqyp/p/920db7e473c6796423e2f523e42e2e49.html
    Copyright © 2011-2022 走看看