最近工作中经常遇到验证码识别的问题,大部分的验证码都比较简单,代码的结构也比较类似,于是抽空总结下,便于以后参考。
简单的验证码拥有几个明显的特征:
- 容易去除背景色
- 小字符集,比如:字母和数字
- 字符间隔比较大,没有字符粘连
- 字符比较规范,没有旋转、拉伸
注:本文只针对简单验证码
先来张图看看效果~
一、灰度化
灰度化
就是把多通道的彩色图像,转为单通道的灰度图像,其目的是为了识别的方便,当前,前提是不影响识别的准确性。这里,采用r * 0.299 + g * 0.587 + b * 0.114
公式进行灰度化。
为了方便的在C#中处理图像数据,我们定义一个ImageData
类,用于存储Image
中各个像素的值(这里直接存储灰度化后的值)。代码如下:
1 | /// <summary> |
二、二值化
通常,灰度化后图像要进行降噪、增强等处理,由于验证码比较简单,这里就直接跳过了这些步骤。二值化
就是根据阈值参数,判断当前像素点是否是有效像素点,其目的是为了过滤背景色,这里简单粗暴的选择128。
二值化代码如下:
1 | /// <summary> |
三、文本分割
文本分割是为了找到目标图像中文本所在的区域,是文本识别的基础。
这里采用种子填充法进行连通域分析,代码如下:
1 | public delegate bool ColorComparator(byte cr); |
二值化后的图像中可能存在间断点,一个字符可能被分割成多个连通域,因此,我们把距离比较近的连通域合并起来(本文验证码是4个字符)。
1 | /*连通域合并*/ |
之后,我们需要把每个识别区域进行归一化,即调整为20*20
的标准结构,并将图像居中。代码如下: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//调整图像的大小为 20*20
private List<ImageData> imageDataList = new List<ImageData>();
private void 图像居中()
{
if (regionList == null || regionList.Count != 4) return;
imageDataList.Clear();
foreach (var r in regionList)
{
var imageData = new ImageData(20, 20);
for (var i = 0; i < imageData.Data.Length; i++) imageData[i] = 255;
var tmp = r.ToImageData();
var sx = (imageData.Width - tmp.Width) / 2;
var sy = (imageData.Height - tmp.Height) / 2;
for (var x = 0; x < tmp.Width; x++)
{
for (var y = 0; y < tmp.Height; y++)
{
imageData[sx + x, sy + y] = tmp[x, y];
}
}
imageDataList.Add(imageData);
}
}
四、文本识别
这里,我们采用模板匹配法进行文本识别。为了提高识别准确度,我们采用k邻近法(k选择10)。
1.收集标准样本
模板特征有很多种,最简单的方法就是把灰度化->二值化->文本分割->居中
后的图像作为特征,直接保存起来,之后也是根据这个特征进行匹配。
特征收录的代码,如下:
1 | public void 收录() |
2.文本识别
文本识别就是根据上一步得到的特征,在标准模板库中进行匹配,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13public string 识别()
{
var sb = new StringBuilder();
for (var i = 0; i < imageDataList.Count; i++)
{
var imageData = imageDataList[i];
var c = SampleHelper.Recognize(imageData);
sb.Append(c);
}
return sb.ToString();
}
模本匹配的代码如下: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/// <summary>
/// 样本工具类
/// </summary>
public static class SampleHelper
{
/// <summary>
/// 样本列表
/// </summary>
internal static List<Sample> SampleList { get; } = new List<Sample>();
/// <summary>
/// 收录标准样本
/// </summary>
public static void Include(char label, ImageData feature)
{
var ls = new List<string>();
for (var l = 0; l < feature.Data.Length; l++) ls.Add((255 - feature[l]).ToString());
var line = label + " " + string.Join(",", ls.ToArray()) + Environment.NewLine;
File.AppendAllText("sample.txt", line);
SampleList.Add(Sample.Parse(line));
}
/// <summary>
/// 识别(K临近法)
/// </summary>
public static char Recognize(ImageData feature)
{
/*1.模板匹配*/
var ls = new List<RecognizeResult>();
foreach (var sample in SampleList)
{
var error = sample.Compare(feature);
if (ls.Count == 0) {
ls.Add(new RecognizeResult { Label = sample.Label, Error = error });
}
else
{
for (var i = 0; i < ls.Count; i++)
{
var item = ls[i];
if (error < item.Error)
{
ls.Insert(i, new RecognizeResult { Label = sample.Label, Error = error });
break;
}
}
}
}
/*2.距离最近的10个元素进行投票*/
var dic = new Dictionary<char, int>();
for (var i = 0; i < Math.Min(10, ls.Count); i++)
{
var v = ls[i].Label;
if (dic.ContainsKey(v)) dic[v]++;
else dic[v] = 1;
}
/*3.返回投票最多的*/
var max = 0;
var label = '\0';
foreach (var item in dic)
{
if (max < item.Value)
{
max = item.Value;
label = item.Key;
}
}
return label;
}
/// <summary>
/// 加载标准模板
/// </summary>
public static void LoadSample()
{
if (!File.Exists("sample.txt")) return;
SampleList.Clear();
var lines = File.ReadAllLines("sample.txt");
foreach (var line in lines)
{
if (string.IsNullOrEmpty(line)) continue;
SampleList.Add(Sample.Parse(line));
}
}
internal class Sample
{
public char Label { get; set; }
public ImageData Feature { get; set; }
public Sample(char label, ImageData feature)
{
Label = label;
Feature = feature;
}
public double Compare(ImageData input)
{
if (input.Width != Feature.Width || input.Height != Feature.Height)
throw new Exception("数据格式不正确.");
double sum = 0;
for (var i = 0; i < Feature.Data.Length; i++)
{
sum += Math.Abs(input[i] - Feature[i]);
}
return sum;
}
public static Sample Parse(string line)
{
var label = line[0];
var featureTxt = line.Substring(2);
var feature = new ImageData(20, 20);
var ss = featureTxt.Split(',');
for (var i = 0; i < ss.Length; i++)
{
var v = int.Parse(ss[i]);
feature[i] = (byte)(255 - v);
}
return new Sample(label, feature);
}
}
internal class RecognizeResult : IComparable
{
public char Label { get; set; }
public double Error { get; set; }
public int CompareTo(object obj)
{
var v = obj as RecognizeResult;
if (v == null) return -1;
return (int)(Error - v.Error);
}
}
}
五、参考资料
本文源代码(包含数据): http://pan.baidu.com/s/1pLjvqgN 密码:muak
- 本文作者: killf
- 本文链接: http://www.killf.info/机器学习/简单验证码识别/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!