本文描述一种利用OpenCV及傅里叶变换识别图片中文本旋转角度并自动校正的方法,由于对C#比较熟,因此本文将使用OpenCVSharp。 文章参考了http://johnhany.net/2013/11/dft-based-text-rotation-correction,对原作者表示感谢。我基于OpenCVSharp用C#进行了重写,希望能帮到同样用OpenCVSharp的同学。
================= 正文开始 =================
手里有一张图片如下,是经过旋转的,如何通过程序自动对它进行旋转校正? (旋转校正是行分割、字符识别等后续工作的基础)
傅里叶变换可以用于将图像从时域转换到频域,对于分行的文本,其频率谱上一定会有一定的特征,当图像旋转时,其频谱也会同步旋转,因此找出这个特征的倾角,就可以将图像旋转校正回去。
先来对原始图像进行一下傅里叶变换,需要这么几步:
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);
|
此时图像是这样的:
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);
}
|
最终得到图像如下:
可以明显的看到过中心有一条倾斜的直线,可以用霍夫变换把它检测出来,然后计算角度。 需要以下几步:
1、二值化
把刚才得到的傅里叶谱放到0-255的范围,然后进行二值化,此处以150作为分界点。
1
2
|
Cv.Normalize(real, real, 0, 255, NormType.MinMax);
Cv.Threshold(real, real, 150, 255, ThresholdType.Binary);
|
得到图像如下:
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();
}
|
最终结果如下,效果还不错:
最后放完整代码:
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