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. }






附件列表

     

    posted on 2016-10-27 15:16  ①块腹肌  阅读(3456)  评论(1编辑  收藏  举报

    导航