C# runtime平台实现的局限性验证码识别

如果看了之前的一篇文章,就知道我之前在做的校园应用中有一个校园卡的功能,实现了余额查询,消费记录查询,银行卡转账等功能。
但是其实在这些操作中,都是会有验证码的过程,那我们就有两种解决的方案,一种是在页面中画一个PopUp显示验证码,然后让用户自己识别之后再按确定,另外一种就是软件可以自动识别验证码,帮助用户省去这个过程。
那这两种方案比起来,肯定更倾向第二种方法了,你要是在登陆过程中突然输入一个东西,那也会非常影响用户体验,而且查询的时候也要输入,转账的时候也要输入。多不好呀。于是我们就动手开始做验证码的识别了~

基础知识

基本结构

一幅图片的基本构成是什么呢?当然是像素(Pixel),在我们接下来的所有操作中,我们也是通过对像素的操作、处理和识别来进行处理。平时说的一幅图片200100像素,就代表着是一个200100的矩阵。

操作形式

那每一个像素是怎样存储的呢~ 每一个像素都是一个内存块,在每个内存块中都有三(openCV)或者四(C sharp runtime)个字节,按着顺序分别代表这个这个像素的 b、g、r、(a)值,前面三个大家都挺熟悉,最后一个是我在处理这一次的gif格式的验证码中遇到的,代表的应该是这个像素点的透明通道。

像素存储描述

大致的结构是这样子:第一个像素( b(0,0) g(0,0) r(0,0) a(0,0)) || 第二个像素b(0,1) g(0,1)……

那么如果我们用代码来描述这些点,就可以用这种方式:

假设imag->imageData指向的是保存像素的内存块,widthStep表示的是每一行的像素个数,那么图像第i行的开始位置在:img->imageData + i * img->WidthStep

列j就位于这个位置之后的第j个像素处:img->imageData + i img->WidthStep + j 4

nChannels表示每个像素占用的字节数的话
B: (img->imageData + iimg->WidthStep)[jimg->nChannels + 0]
G: (img->imageData + iimg->WidthStep)[jimg->nChannels + 1]
R: (img->imageData + iimg->WidthStep)[jimg->nChannels + 2]
A: (img->imageData + iimg->WidthStep)[jimg->nChannels + 3]

### 颜色
黑色(0,0,0,255)

白色(255,255,255,255)

实施过程

这里,我们首先放来一张待处理的照片:
测试用例

基本思想

首先我们要明白要怎么去识别验证码,最基本的步骤肯定是,先以机器能理解的方式得到图片的数据,那当然就是像素,然后再把这个数据和我们的训练库进行匹配,找出符合程度最高的这一个。围绕这个过程,我们加入了这些步骤:

  • 二值化。为了更好的去除杂色的干扰,并便于匹配之后的样本。
  • 切割。为了方便的进行匹配。

然后要说明,每一种验证码几乎都需要用不同的代码来进行识别,虽然说思想路线是一样的,但是很多细节处理上都需要找到很好的方式。比如我这一个验证码,每一个数字块的长宽是固定的,这也就方便了很多。而且也没有倾斜等问题,因此只需要最简单的01匹配就可以了。若是更复杂的,需要通过向量等方式来比对特征。

图像二值化

什么叫二值化,这个是CV中常用的一个名词,意思是指将图像上的像素点的灰度值设置为0或255,也就是将整个图像呈现出明显的只有黑和白的视觉效果。在这个demo中,我们将构成数字的像素变成黑色,其余的变成白色。这个时候也是需要找规律的,因为背景色和数字的forecolor肯定有一些规律可循,在我的这个demo中,backgroud pixel的RGB中有一个必为255,我用PS找到了这个规律。因此有这样的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void GrayByPixels(byte[] _byte)
{

//因为是20*60的像素,所以为1200
for (int i = 0; i < 1200; i++)
{
int index = (4 * i);
byte _b = _byte[index];
byte _g = _byte[index + 1];
byte _r = _byte[index + 2];
//通过PS的分析,在这种验证码中,构成数字的像素的rgb中总含有255
if ((_b == 255) || (_g == 255) || (_r == 255))
{
_byte[index] = 0;
_byte[index + 1] = 0;
_byte[index + 2] = 0;
}
else
{
_byte[index] = 255;
_byte[index + 1] = 255;
_byte[index + 2] = 255;
}
}
}

这样就变成了一个只有黑白的图片。

图像的切割

啊那之前说了要切割的原因,就是找到刚好能和样本匹配的那些有效块,切掉那些没用的背景。这个就是一个小小的算法问题了,你需要从左往右,从上往下进行一个扫描,得到每一个数字方块左上角的那个点,然后利用固定的长度和宽度来截取出来。根据每个不同的验证码,算法当然也会有一些差别,不过我还是放出我的代码给大家做一些参考。

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
/// <summary>
/// 根据最上边和最左边,以及数字的固定长宽,画出边界
/// </summary>
/// <param name="_byte"></param>
/// <returns></returns>
public static void CutByPixels(byte[] _byte)
{

BinaryImage(_origeByte);
int[] left = new int[4];
left[0] = 4; left[1] = 17; left[2] = 30; left[3] = 43;
int[] top = new int[4];

//逐行扫描找出最上方的点
int count = 0;
for (int row = 0; row < 20; row++)
{
for (int col = 0; col < 60; col++)
{
if (_binaryByte[row, col] == 1)
{
if (col >= 4 && col <= 9)
{
if (top[0] == 0)
{
top[0] = row;
count++;
}
if (count == 4)
{
col = 60;
row = 20;
}
}
if (col >= 17 && col <= 22)
{
if (top[1] == 0)
{
top[1] = row;
count++;
}
if (count == 4)
{
col = 60;
row = 20;
}
}
if (col >= 30 && col <= 35)
{
if (top[2] == 0)
{
top[2] = row;
count++;
}
if (count == 4)
{
col = 60;
row = 20;
}
}
if (col >= 43 && col <= 48)
{
if (top[3] == 0)
{
top[3] = row;
count++;
}
if (count == 4)
{
col = 60;
row = 20;
}
}
}
}
}
_imageMutrix = new int[4][,];
for (int i = 0; i < 4; i++)
{
_imageMutrix[i] = new int[11, 7];
}
for (int i = 0; i < 4; i++)
{
//Debug.WriteLine("imageMutrix[" + i + "]:");
for (int row = 0; row < 11; row++)
{
string line = string.Empty;
for (int col = 0; col < 6; col++)
{
_imageMutrix[i][row, col] = _binaryByte[top[i] + row, left[i] + col];
line += (_imageMutrix[i][row, col] + " ");
}
//Debug.WriteLine(line);
}
}
}

获得特征样本

到这一步,每一种验证码的处理方式就都不一样了,根据我现在的能力,我可以给出这样几个范例。

  1. 数字不倾斜,数字块长宽统一。
    这种是最简单,也是挺常见的一种了,这样直接把黑白不同的点用0、1来取代,然后和样本库进行匹配,就很简单了。
  2. 其他。
    这种你就需要通过数据块来算出特征向量了,这个稍微麻烦一些。

样本匹配

这里给出一个数字的样本:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static int[,] _0 ={
{0,0,1,1,1,0,0},
{0,1,1,0,1,1,0},
{0,1,0,0,0,1,0},
{1,1,0,0,0,1,1},
{1,1,0,0,0,1,1},
{1,1,0,0,0,1,1},
{1,1,0,0,0,1,1},
{1,1,0,0,0,1,1},
{0,1,0,0,0,1,0},
{0,1,1,0,1,1,0},
{0,0,1,1,1,0,0}
};

匹配的过程就是简单的比对,找出匹配度最大的这个。只要验证码不坑爹,基本上95%的正确率也是很好的。

总而言之,验证码识别在明白原理之后其实是一件很简单的事,只不过随着程序员之间的互相博弈,变得越来越麻烦。

函数库

WriteableBitmap to byte[]:

1
byte[] _byte = _bitmap.PixelBuffer.ToArray();

byte[] to WriteableBitmap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
WriteableBitmap _wb = new WriteableBitmap(_weight, _height);
int _area = _weight * _height * 4;
using (Stream stream = _wb.PixelBuffer.AsStream())
{
if (stream.CanWrite)
{
byte[] pixelArray = new byte[_area];
for (int i = 0; i < _area; i++)
{
pixelArray[i] = _byte[i];
}
await stream.WriteAsync(pixelArray, 0, pixelArray.Length);
stream.Flush();
}
}
return _wb;


这样我们就实现了验证码的识别,是不是很简单 0.0

动手尝试吧~