字符验证码识别之数据处理

验证码识别之数据处理

当我们想要自己通过深度学习网络训练识别网上的各种验证码时,首先需要下载一定量的图片,而且还需要给这些验证码图片标注好正确的答案。所以,一开始我们要借助一些打码平台来帮助我们完成这部分工作,当然如果不嫌麻烦的话自己一张张图标注也是可以的……

当我们完成标注工作后,还需要对这些验证码图片进行处理,使之适合输入到我们的网络中。

对图像处理时我们需要用到OpenCV-python这个库,先使用pip install OpenCV-python安装一下。

在windows系统中,若出现导入cv2模块后,使用pycharm调用cv2的方法全部标黄,并且不提示方法名的情况,可以进入cv2__init__.py中,注释全部代码,然后在最后加上以下代码:

1
2
3
4
5
6
import sys
import os
import importlib
os.environ["PATH"] += os.pathsep + os.path.dirname(os.path.realpath(__file__))
from .cv2 import *
globals().update(importlib.import_module('cv2.cv2').__dict__)

之后重启Pycharm应该可以解决。

图像模糊(滤波)

模糊操作时图像处理中最简单和常用的操作之一,主要是为了给图像预处理时降低噪声。

模糊的过程与卷积神经网络的原理类似,我们都知道图像其实是由一个个的像素点构成,我们可以选取特定大小的一个模板,然后扫描整幅图像,每一次扫描对整个模板内的所有像素点的值进行运算(不同的模糊操作采用的运算不一样),将得到的值赋值给中间的像素点。

主要有以下几种常见的模糊操作:

均值模糊

均值滤波是典型的线性滤波算法,它是指在图像上对目标像素给一个模板,该模板包括了其周围的临近像素(如果模板大小为3*3,则是目标像素为中心的周围8个像素,构成一个滤波模板),再用模板中的全体像素的平均值来代替原来像素值。

1
cv2.blur(src, ksize, dst=None, anchor=None, borderType=None)

中值模糊

中值滤波与均值滤波的区别仅在于,对模板内的所有像素值进行排序,取中值来代替中心点的值。

1
cv2.medianBlur(src, ksize, dst=None)

高斯模糊

而高斯滤波就是计算加权平均值。

1
cv2.GaussianBlur(src, ksize, sigmaX, dst=None, sigmaY=None, borderType=None)

针对本次处理的验证码图片:

我们可以使用中值滤波,图中的噪声叫做椒盐噪声,是幅值近似相等但是随机的分布在不同位置,图中既有污染的点,也有干净的点。

模糊处理不一定是必须的,具体的实践过程中,需要针对图片的具体情况而定,若需去噪,选择采用哪种滤波方式也要视情况而定,不好选择的情况下可以逐个尝试。

图像灰度化

1
2
3
4
5
6
import cv2

# 读取图片
img = cv2.imread(img_path)
# 灰度化
img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)

灰度化处理也不是必须的,但是针对大部分验证码图片,颜色对其识别几乎没有影响,可以考虑将其灰度化后再训练,这样可以减少数据处理量(由三通道转变为单通道)。若灰度化后背景会影响到验证码字符就不要灰度化。

cv2常用操作

这里插入一些cv2的基本操作,方便我们对图片的处理,以及在处理过程中观察效果。

  • 读取图片

    1
    2
    3
    import cv2

    img = cv2.imread('1.jpg',cv2.IMREAD_GRAYSCALE)

    参数:

    filepath:要读入图片的完整路径;
    flags:读入图片的标志,flags的可选值:cv2.IMREAD_COLOR:默认参数,读入一副彩色图片,忽略alpha通道;cv2.IMREAD_GRAYSCALE:读入灰度图片;cv2.IMREAD_UNCHANGED:顾名思义,读入完整图片,包括alpha通道。

    这里的图片路径一般不能包含中文字符,否则有可能读取失败

  • 显示图片

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import cv2

    # 这里定义一个窗口,后面可以拖动窗口,否则无法拖动
    cv2.namedWindow('window_name', cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO)
    cv2.imshow('window_name',img)
    # 等待键盘输入,单位为毫秒;参数为0表示无限等待;不调用waitKey的话,窗口会一闪而逝,看不到显示的图片
    cv2.waitKey(0)
    # 删除指定窗口
    cv2.destroyWindow('window_name')
    # 销毁所有窗口
    cv2.destroyAllWindows()
  • 保存图片

    1
    2
    cv2.imwrite('1.png', img, [int(cv2.IMWRITE_JPEG_QUALITY),95])
    cv2.imwrite('1.png',img,[int(cv2.IMWRITE_PNG_COMPRESSION),9])

    第一个参数是要保存的文件名,第二个参数是要保存的图像的名字。

    可选的第三个参数,它针对特定的格式:

    对于JPEG,其表示的是图像的质量,用0 - 100的整数表示,默认95;

    对于png ,第三个参数表示的是压缩级别,默认为3。

    注意:cv2.IMWRITE_JPEG_QUALITY类型为 long ,必须转换成 int;

    cv2.IMWRITE_PNG_COMPRESSION, 从0到9 压缩级别越高图像越小。

  • 图片复制

    1
    imgcopy=img.copy()
  • 颜色空间转换

    1
    2
    3
    4
    5
    # RGB彩色图片转为灰度图片
    img2 = cv2.cvtColor(img,cv2.COLOR_RGB2GRAY)

    # 灰度图片转为RGB彩色图片
    img3 = cv2.cvtColor(img,cv2.COLOR_GRAY2RGB)
  • 模糊处理(上面已经有了)

  • 二值化

    二值化就是让图像的像素点矩阵中的每个像素点的灰度值为0(黑色)或者255(白色),也就是让整个图像呈现只有黑和白的效果。在灰度化的图像中灰度值的范围为0~255,在二值化后的图像中的灰度值范围是0或者255。

    全局阈值

    1
    cv2.threshold(src, thresh, maxval, type, dst=None)

    参数

    src,输入数组; thresh,阈值;maxval,最大值;type,阈值类型。

    参数type有以下几种类型:

    THRESH_BINARY 像素值大于阈值时,取Maxval,否则取0

    THRESH_BINARY_INV 像素值大于阈值时,设置为0,否则取Maxval

    THRESH_TRUNC 像素值大于阈值时,设置为阈值,否则不变

    THRESH_TOZERO 像素值大于阈值时,不变,否则取0

    THRESH_TOZERO_INV 像素值大于阈值时,设置为0,否则不改变

    返回值

    threshold函数有两个返回值,其中第二个返回值是二值化后的灰度图。当我们指定了阈值参数thresh,第一个返回值ret就是我们指定的thresh。换句话说,我们可以不指定阈值参数thresh。

    通常情况,我们一般不知道设定怎样的阈值thresh才能得到比较好的二值化效果,只能去试。如对于一幅双峰图像(理解为图像直方图中存在两个峰),我们指定的阈值应尽量在两个峰之间的峰谷。这时,就可以用第四个参数THRESH_OTSU,它对一幅双峰图像自动根据其直方图计算出合适的阈值(对于非双峰图,这种方法得到的结果可能不理想)。

    1
    cv2.threshold(img, 0, 255, cv2.THRESH_OTSU)

    此外还有另一种自适应阈值的参数:

    1
    cv2.threshold(img, 0, 255, cv2.THRESH_TRIANGLE)

    以上自适应阈值都是将像素值大于阈值的取Maxval,否则取0。

    局部阈值

    根据图片一小块区域的值来计算对应区域的阈值,从而得到也许更为合适的图片。

    1
    dst = cv2.adaptiveThreshold(src, maxval, thresh_type, type, Block Size, C)

    返回值dst: 二值化后的灰度图

    参数

    rc: 输入图,只能输入单通道图像,通常来说为灰度图

    maxval: 当像素值超过了阈值(或者小于阈值,根据type来决定),所赋予的值
    thresh_type: 阈值的计算方法,包含以下2种类型:

    (1)、cv2.ADAPTIVE_THRESH_MEAN_C(局部邻域块的平均值,该算法是先求出块中的均值,再减去常数C);

    (2)、 cv2.ADAPTIVE_THRESH_GAUSSIAN_C(局部邻域块的高斯加权和。该算法是在区域中(x, y)周围的像素根据高斯函数按照他们离中心点的距离进行加权计算,再减去常数C。)
    type:二值化操作的类型,与固定阈值函数相同,包含以下5种类型:

    cv2.THRESH_BINARY; cv2.THRESH_BINARY_INV; cv2.THRESH_TRUNC; cv2.THRESH_TOZERO;cv2.THRESH_TOZERO_INV
    Block Size: 图片中分块的大小,一般选择为3、5、7……等
    C :阈值计算方法中的常数项

Block Size和C经常分别选择11和2;实际应用中通过测试取较合适的值

以下是两种二值化结果展示:

全局阈值cv2.THRESH_OTSU:

局部阈值cv2.ADAPTIVE_THRESH_MEAN_C

看上去区别不大。。。

保存数据

当我们对图片做完相应的处理后需要将图片数据保存为3D张量(图像数据一般为3D张量),保存时需要用到Python的numpy库。

1
2
3
4
5
6
7
8
import numpy as np

train_img = list()
img_h = 40 # 图像的高
img_w = 120 # 图像的宽
channels = 1 # 图像的通道数(灰度图像为1,彩色图像为3)
# img是处理过是图像
train_imgs.append(np.reshape(img, (img_h, img_w, channels)))

我们对所有训练图片全部执行上述操作后,train_imgs就变成了一个4D张量,形状为(samples, height, width, channels),之后我们再讲数据类型设置为float32再保存即可。

1
2
train_imgs = np.array(train_imgs, dtype='float32')
np.save('x_train', train_imgs)

不过还需要指出的是,回忆我们在cifar10简单图像识别的学习中,处理训练数据时会将像素值(0~255 范围内)缩放到 [0, 1] 区间,其中《python深度学习》书中说“神经网络喜欢处理较小的输入值”。

一般来说,将取值相对较大的数据(比如多位整数,比网络权重的初始值大很多)或异质数据(heterogeneous data,比如数据的一个特征在0~1 范围内,另一个特征在100~200 范围内) 输入到神经网络中是不安全的。这么做可能导致较大的梯度更新,进而导致网络无法收敛。为了让网络的学习变得更容易,输入数据应该具有以下特征:

1、 取值较小:大部分值都应该在 0~1 范围内;

2、 同质性(homogenous):所有特征的取值都应该在大致相同的范围内。

所以我们这里二值化时可以采用一种比较巧妙的方式,直接将像素值控制在[0, 1]区间:

1
2
3
cv2.threshold(img, 0, 1, cv2.THRESH_OTSU)
or
img = cv2.adaptiveThreshold(img, 1, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 2)

标签处理

不仅要处理每张验证码图片,每张验证码图片对应的验证码字符也是要处理的,处理方式如下:

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
from string import ascii_lowercase, digits

labels_len = 4 # 验证码位数
num_classes = 36 # 验证码包含的字符集数量(这里是所有数字和字母)
alphanumeric = ascii_lowercase + digits # 小写字母和数字


def process_label(label):
result = np.zeros((labels_len, num_classes), dtype='float32')
x = label.lower()
for i, c in enumerate(x):
result[i][alphanumeric.index(c)] = 1

return result

data = process_label('d6jp')
print(data)

[[0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]

与处理图像一样,我们对所有训练图片对应的验证码字符全部执行上述操作后,会生成一个3D张量,形状为(samples, labels_len, num_classes),之后我们再讲数据类型设置为float32再保存即可。

注意:这里是按照36个字符进行分类,所有的小写字母和数字,针对的情况是一般需要输入验证码的网站,英文字符不区分大小写,所以我们就全部当做小写字母来进行训练,如果该网站的验证码字符区分大小写,就要分成26x2+10=62个分类进行训练。

数据分类

一般我们利用神经网络训练验证码时,需要将数据集分为三类,即训练集、验证集和测试集,如果图片存储在windows系统下,并且文件名是诸如验证码字符+一串随机字符的形式,那么这些文件是会被按照文件名顺序存放的,这个时候分类一定不能直接按顺序分类,这样会导致每个数据集的数据特征具有局部性,这样会导致训练效果下降。

可以采用以下代码,随机将数据分类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import os
import random
from os import remove


img_path = 'D:/captcha/credit_gx/'
imgs_list = os.listdir(img_path)
for i in range(3000):
t = int(random.random() * len(imgs_list))
i_path = imgs_list[t]
with open(img_path + i_path, 'rb') as f1:
with open('D:/captcha/credit_gx_train/{}'.format(i_path), 'wb') as f2:
f2.write(f1.read())
print('copy {} success'.format(i_path))
remove(img_path + i_path)
del imgs_list[t]

加载数据

numpy保存的数据形式为x_train.npy,我们在进行训练之前同样使用numpy加载数据:

1
2
3
import numpy as np

np.load('x_train.npy')