0%

CNN网络自动识别验证码(keras实现)

前言

这里的验证码为对于普通小写字母加数字构成的四字验证码,如果验证码中存在大写字母,同样是添加训练特征就。这里我们选用CNN神经网络来进行识别验证码。这里大概流程为先预处理收集来的验证码,对验证码进行处理之后,使用tensorflow深度学习框架来搭建CNN神经网络学习,最后用训练好的模型验证。

注意

此教程不适用tensorflow2.0。但理论上tensorflow1.1以上tensorflow2.0以下都可以适用。

博主环境为:tensorflow=1.12,keras=2.2.4

等待star的Github

本教程的源代码与原数据(训练图片、numpy数据、模型)均在我的git项目中,需要的朋友可以自取,觉得棒棒的可以给个star,感谢🙏

zhengfang-code

1 收集、处理、切分训练集

1.1 收集训练集

训练集如果从零获取就只有爬取所需识别网站的验证码,然后本地手动加标签,但是推荐先使用一个准确率不高的识别器对验证码识别打标签后,再手动修改错误标签。比如我这里就是,从新版方正教务系统爬取1000张验证码,然后用我从网络上下载的2260张的已经打上标签的旧版验证码进行训练模型识别,然后手动再修改错误标签。

训练集我之前从网络上(现已无从找到原贴链接,git内 提供下载)找到了2260张图片的已打标签的旧版验证码图片,还有自己爬取的新版验证码然后自己打上标签的1000张图片(git内提供下载),其中图片的名称就为验证码内容。

1.2 处理训练集

旧版验证码验证码如图:

0a13

而现在新版教务系统的验证码如下图:

0dej

可以观察到新版验证码的字体是没有变化的,但是周围有一圈的其他颜色的过渡层颜色,而旧版则是字符全为同一颜色,对于图片的预处理我们可以有两种处理的程度,第一种是使用Opencv图像处理库来对图片先灰度化处理再中值滤波去噪然后二值化处理,第二种是使用Opencv图像处理库直接对图片灰度化处理。

第一种方法实现的代码如下

1
2
3
4
5
6
import cv2

img = cv2.imread(path)
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 灰度化
mblur = cv2.medianBlur(gray_img, 3) # 中值滤波
ret, thresh = cv2.threshold(mblur, 127, 255, cv2.THRESH_BINARY) # 二值化

处理后的图像如下:

0a13-1

odej-1

可以看出以上代码对于新老两种验证码的字符提取效果都很好,都能去噪点,最后二值化提取出验证码字符。

第二种方法处理后的验证码图像会保留噪点,处理后的图像如下:

0a13-2

但是由于后面的CNN神经网络训练机制,自己学习生成的过滤器都能很好的过滤掉噪点,最后训练的结果其实很接近第一种方法处理后的图片的训练结果,所以第二种方法不仅处理图片的步骤少了,而且最后的识别效果和第一种没有什么区别,我们这里大道至简就选择第二种方法了。

1.3 切分训练集

对所有的图片进行处理后,就要对每张验证码图片中的四个字符进行切分,先分析四个字符的大致切分位置,这里分析的方法是将所有的验证码图片先使用上面第一种方法二值化,然后图片数据叠加到一起,这样叠加后的数据就相当于统计出了一张验证码图片上每个像素点出现的概数,然后通过纵向横向的投影,观察概数大小切分出四个字符的位置:

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
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt

height = 27
width = 72

old_code_path = 'train_pictures/old_code/'
new_code_path = 'train_pictures/new_code/'

# 图片二值化处理
def code2binary(path):
img = cv2.imread(path)
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 灰度化
mblur = cv2.medianBlur(gray_img, 3) # 中值滤波
ret, thresh = cv2.threshold(mblur, 127, 255, cv2.THRESH_BINARY_INV) # 二值化
return thresh

# 处理一个目录的图片,并返回处理后的图片的numpy数据
def process(path):
data = []
for file_name in os.listdir(path):
if file_name[-3:] != 'png':
continue
img = code2binary(path + file_name)
data = np.concatenate((data, img), axis=0) if data != [] else img
return data

data = np.concatenate((process(old_code_path), process(new_code_path)), axis=0) # 新老验证码合并
data = data.reshape(int(data.shape[0] / height), height, width) # data shape = (2265, 27, 72)
data = data / 255 # 255 转为 1

sum_data = np.sum(data, axis=0) # 每张图片的数据叠加

# 横向投影数据
split_lines = [4, 16, 28, 40, 52]
vlines = [plt.axvline(i, color='r') for i in split_lines]
plt.figure(1)
plt.plot(np.sum(sum_data, axis=0))
plt.show()

# 纵向投影数据
split_lines = [0, 22]
plt.figure(2)
vlines = [plt.axvline(i, color='r') for i in split_lines]
plt.plot(np.sum(sum_data, axis=1))
plt.show()

横向投影数据折线图:

row

纵向投影数据折线图:

column

通过对所有图片叠加后的数据的横纵投影折线图的分析,我们可以得出切分验证码的最佳切分位置为纵向[0, 22],横向[4, 16, 28, 40, 52]。在图中红线就是这几条线的位置,可以看出能很好的匹配字符的位置。

1.4 保存为本地numpy数据

有了切分位置我们现在就可以对每张图片进行切分为四个字符了,并且为每个字符图片加上相应的标签,只需要简单的修改上面press函数内的内容:

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
import os
import numpy as np
import cv2
import string

def process(path):
# 处理一个目录的图片,并返回处理后的图片数据和相应标签数据
cdata = []
clabel = []
for file_name in os.listdir(path):
if file_name[-3:] != 'png':
continue
img = cv2.imread(path + file_name)
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 灰度化
ims = [img[y_min:y_max, u:v+1] for u, v in zip(split_lines[:-1], split_lines[1:])] # 切分验证码
cdata = np.concatenate((cdata, ims), axis=0) if cdata != [] else ims # shape = (x, 22, 13)
for i in file_name[:4]:
clabel.append(CHRS.index(i))
return cdata, clabel

if __name__ == '__main__':
split_lines = [4, 16, 28, 40, 52] # 验证码纵向切分的位置
y_min, y_max = 0, 22 # 验证码横向切分的位置

height = 27
width = 72

old_code_path = 'train_pictures/old_code/'
new_code_path = 'train_pictures/new_code/'

CHRS = string.ascii_lowercase + string.digits # 小写字母+数字

data_1, label_1 = process(old_code_path)
data_2, label_2 = process(new_code_path)

data = np.concatenate((data_1, data_2), axis=0) # shape = (9060, 22, 13)
data = data / 255 # [0, 255] 转为 [0, 1]
label = np.array(label_1 + label_2) # shape = (9060, )

最后返回的data形状为(9060, 22, 13)内容为每个单独的字符的图片矩阵、label形状为(9060, )内容为CHRS转化后的对应的数字, data与label的索引值是对应的,例如:

1
2
3
4
5
num = 142

plt.imshow(data[num], 'gray')
plt.show()
print('字符为:', CHRS[label[num]])

其输出为:

n

可以将保存有处理和切分后的字符图片和相应的标签的numpy数据保存到本地,方便后面神经网络训练使用,代码如下:

1
np.savez('train_pictures/data_label.npz', data=data, label=label)  # 保存

需要加载时,可以分别加载data和lable,加载方式如下:

1
2
3
a = np.load('train_pictures/data_label.npz')  # 加载
data = a['data']
label = a['label']

保存本地的文件大概20.8MB大小,可以看出numpy数据:简直不能太爽👍,深度学习中,有时候你保存了训练集,验证集,测试集,还包括他们的标签,用这个方式存储起来,要啥加载啥,文件数量大大减少,也不会到处改文件名去。

2. CNN神经网络的搭建与训练

2.1 网络搭建

这里使用tensorflow神经网络框架搭建CNN神经网络进行验证码字符的识别,搭建的网络具体参数如下图:

model_sum

代码实现如下:

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
from tensorflow.keras import models, layers, utils, losses, optimizers
import numpy as np

def load_data(cpath):
a = np.load(cpath)
cdata = a['data']
clabel = a['label']
return cdata, clabel

def network():
# 模型建立
# 两层3x3窗口的卷积(卷积核数为32和64),一层最大池化(MaxPooling2D)
# 再Dropout(随机屏蔽部分神经元)并一维化(Flatten)到128个单元的全连接层(Dense),最后Dropout输出到36个单元的全连接层(全部字符为36个)
model = models.Sequential()
model.add(layers.Conv2D(32,
kernel_size=(3, 3),
activation='relu',
input_shape=input_shape))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPool2D(pool_size=(2, 2)))
model.add(layers.Dropout(0.25)) # 随机屏蔽部分神经元
model.add(layers.Flatten()) # 一维化
model.add(layers.Dense(128, activation='relu'))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(num_classes, activation='softmax'))

# 模型编译
model.compile(loss=losses.categorical_crossentropy,
optimizer=optimizers.Adadelta(),
metrics=['accuracy'])

return model

2.2 网络训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import string

path = 'train_pictures/data_label.npz'
CHRS = string.ascii_lowercase + string.digits # 小写字母+数字
num_classes = len(CHRS) # 分类数
input_shape = (22, 13, 1)
batch_size = 128
epochs = 12

X, Y = load_data(path)
Y = utils.to_categorical(Y, num_classes) # 对Y进行one-hot编码
X = X.reshape(X.shape[0], *input_shape)
# 简单的切分出训练集和测试集
split_point = len(Y) - 500
x_train, y_train, x_test, y_test = X[:split_point], Y[:split_point], X[split_point:], Y[split_point:]

model = network()
history = model.fit(x_train, y_train,
batch_size=batch_size,
epochs=epochs,
verbose=1,
validation_data=(x_test, y_test))

最后训练出来的结果如下图:

model_result

loss:

loss

accuracy:

acc

可以看出有在大概第四轮左右到达了精度上限,但是看损失的情况还是没有过拟合的情况产生,所以也就无所谓了,最后的验证精度可以达到大概0.99左右算是非常高了,所以这个模型也是可以很好的用在后面实际的验证码识别上了。

2.3 网络保存

使用以下代码保存已训练完成的模型

1
2
# 保存模型
model.save('verification_code_model.h5')

3. 识别展示

模型训练完后就可以进行调用识别了,这里是一个简单的验证,代码如下:

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
# 预测展示
from tensorflow.keras.models import load_model
import string
import numpy as np
import cv2
import matplotlib.pyplot as plt

CHRS = string.ascii_lowercase + string.digits # 小写字母+数字
split_lines = [4, 16, 28, 40, 52] # 验证码纵向切分的位置
y_min, y_max = 0, 22 # 验证码横向切分的位置
input_shape = (22, 13, 1)

model_path = 'verification_code_model.h5'
img_path = '28vh.png'

cnn_model = load_model(model_path)

image = cv2.imread(img_path)
gray_img = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 灰度化
ims = [gray_img[y_min:y_max, u:v + 1] for u, v in zip(split_lines[:-1], split_lines[1:])] # 切分验证码

name = ''
for i in range(len(ims)):
test_input = 1.0 * np.array(ims[i]) # 图片转化为矩阵
test_input = test_input.reshape(1, *input_shape) # reshape多出来一个1因为预测的时候只有一个样本
y_probs = cnn_model.predict(test_input) # 模型预测, y_probs的形状为(1, 36)
name += CHRS[y_probs[0].argmax(axis=0)]

plt.imshow(gray_img, 'gray')
plt.show()
print('result: ', name)

结果为:

predict_result

可以看出成功预测出结果。

如果您觉得还不错,可以请我喝杯咖啡。