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

简介:

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


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


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

wKioL1mARezA1tGsAATgzli4464874.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,  null null , fourier);
Cv.DFT(fourier, fourier, DFTFlag.Forward);
Cv.Split(fourier, real, imaginary,  null null );


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
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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<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="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);
         }
     }
}



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


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

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






     本文转自 BoyTNT 51CTO博客,原文链接http://blog.51cto.com/boytnt/1678090:,如需转载请自行联系原作者

相关文章
|
1月前
|
存储 Cloud Native Linux
OpenCV图像翻转和旋转
OpenCV图像翻转和旋转
|
1月前
|
存储 编解码 API
【图像文本化】Base64编解码OpenCV4中 Mat 对象
【图像文本化】Base64编解码OpenCV4中 Mat 对象
26 0
|
2月前
|
存储 计算机视觉
【OpenCV】—离散傅里叶变换
【OpenCV】—离散傅里叶变换
|
2月前
|
数据采集 数据挖掘 计算机视觉
最全OpenCV-Python实战(3)——OpenCV中绘制图形与文本,面试官必问问题及答案
最全OpenCV-Python实战(3)——OpenCV中绘制图形与文本,面试官必问问题及答案
|
2月前
|
监控 API 计算机视觉
OpenCV这么简单为啥不学——1.6、图像旋转与翻转(rotate函数、imutils环境安装、imutils任意角度旋转)
OpenCV这么简单为啥不学——1.6、图像旋转与翻转(rotate函数、imutils环境安装、imutils任意角度旋转)
58 0
|
2月前
|
计算机视觉 Python
OpenCV中图像的平移、旋转、倾斜、透视的讲解与实战(附Python源码)
OpenCV中图像的平移、旋转、倾斜、透视的讲解与实战(附Python源码)
218 0
|
2月前
|
计算机视觉 开发者 Python
OpenCV中图像的缩放与旋转讲解及实战演示(附Python源码)
OpenCV中图像的缩放与旋转讲解及实战演示(附Python源码)
100 0
|
9月前
|
计算机视觉
OpenCV-最小包围旋转矩形边框cv::minAreaRect
OpenCV-最小包围旋转矩形边框cv::minAreaRect
|
9月前
|
计算机视觉
OpenCV-绘制旋转矩形
OpenCV-绘制旋转矩形
111 0
|
9月前
|
计算机视觉
OpenCV-离散傅里叶变换cv::dft&cv::idft
OpenCV-离散傅里叶变换cv::dft&cv::idft