python生成验证码→处理验证码→建立CNN模型训练→测试模型准确率→识别验证码

python生成验证码→处理验证码→建立CNN模型训练→测试模型准确率→识别验证码

前言

  • 本文使用了pillow库来生成自己想要的验证码,直接运行pip install pillow即可完成安装
  • 使用pytorch来建立和训练模型,本文使用的pytorchcuda8.0版本的1.0.0,可以在pytorch官网选择适合自己电脑的版本
  • 使用opencv来对验证码进行处理,直接运行pip install opencv-python即可安装

一、生成验证码

本文要生成的验证码是由大写字母或数字来组成典型4个字符的验证码,字母和数字是黑色的,背景是灰色的,然后加了两条颜色随机的干扰线以及1000个随机颜色随机坐标的点。如下所示:


更多细节请看代码注释,具体实现代码如下:

from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
import shutil
import string
import os
import random

def random_color():
    '''获取一个随机颜色(r,g,b)格式的'''
    c1 = random.randint(0, 255)
    c2 = random.randint(0, 255)
    c3 = random.randint(0, 255)
    return c1, c2, c3

def random_xy():
    '''获取一个随机的坐标,用来添加噪声'''
    x=random.randint(0,132) # width
    y=random.randint(0,40)  # height
    return x,y

def random_str():
    '''从26个大写英文字母+10个阿拉伯数字中获取一个随机的字符'''
    all_str=list(string.digits+string.ascii_uppercase)
    random_char = random.choice(all_str)
    return random_char

class CreatImage:
    '''生成宽度132,高度40的验证码数据集'''
    def __init__(self):
        self.width=132 # 宽度
        self.height=40 # 高度
        self.train_num=5000  # 训练数据量
        self.test_num=1000   # 测试数据量
        self.font_file='C:/Windows/Fonts/simhei.ttf'  # 字体文件
        self.base_path='images/'
        self.train_path=os.path.join(self.base_path,'train')
        self.test_path=os.path.join(self.base_path,'test')
        if not os.path.exists(self.train_path):
            os.makedirs(self.train_path)
        if not os.path.exists(self.test_path):
            os.makedirs(self.test_path)
                      
    def divice(self):
        '''划分测试集'''
        for _ in range(self.test_num):
            img=random.choice(os.listdir(self.train_path))
            shutil.move(f'{self.train_path}/{img}',f'{self.test_path}/{img}')
    
    def creat_img(self,howmany):
        '''生成验证码'''
        for _ in range(howmany):
            image=Image.new('RGB', (self.width, self.height), (250,250,250))
            draw = ImageDraw.Draw(image)
            font = ImageFont.truetype(self.font_file, size=30)
            # 画线
            for i in range(2):
                y1 = random.randint(0, self.height)
                y2 = random.randint(0, self.height)
                draw.line((0, y1, self.width, y2), fill=random_color(),width=3)
            # 画点
            for i in range(1000):
                draw.point(random_xy(),fill=random_color())
            # 写字
            temp = []
            for i in range(4):
                random_char = random_str()
                y=random.randint(0,6)
                draw.text((15+i*30, y), random_char, (0,0,0), font=font)
                temp.append(random_char)
            valid_str = "".join(temp)
            image.save(f'{self.train_path}/{valid_str}.png')
    
    def main(self):
        '''主函数。考虑到文件名重复的情况,直到生成5000张用来训练的验证码和1000张用来测试的验证码为止'''
        while True:
            howmany=(self.train_num+self.test_num)-len(os.listdir(self.train_path))
            if howmany == 0:
                break
            self.creat_img(howmany)
        self.divice()

if __name__ == '__main__':
    creator=CreatImage()
    creator.main()

二、处理验证码

针对本文生成的验证码,主要用到的一个图片处理方法是二值化,即将图片的像素点数值大于预先设置的阈值的像素点等于255,而将小于阈值的像素点等于0,255即白色,0即黑色。将图片二值化的方法有很多,其中cv2中就有一个封装好的函数可以直接调用,即cv2.threshold()

处理后的验证码效果如下:



具体实现的代码如下:

import matplotlib.pyplot as plt
from PIL import Image
import cv2

img = cv2.imread('LOVE.png') # 读取图片
img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) # 转为灰度图
img = cv2.threshold(img,20,255,cv2.THRESH_BINARY)[1] # 阈值设置为20
img = Image.fromarray(cv2.cvtColor(img,cv2.COLOR_BGR2RGB)) # cv2转为PIL的格式
plt.imshow(img)

三、建立CNN模型训练

本文使用主流开源框架pytorch建立的CNN模型,由3个卷积层、3个池化层和两个全连接层构成,中间采用ReLU函数进行映射,更多细节请看下面的代码:

from torch.nn import Module
from torch.nn import Sequential
from torch.nn import Conv2d
from torch.nn import BatchNorm2d
from torch.nn import Dropout
from torch.nn import ReLU
from torch.nn import MaxPool2d
from torch.nn import Linear
import string
import os

path_image = 'images/'
path_train = os.path.join(path_image, 'train')
path_test = os.path.join(path_image, 'test')
captcha_number = 4
image_height = 40
image_width = 132
all_str = {v: k for k, v in enumerate(list(string.digits + string.ascii_uppercase))}

class CNNModel(Module):

    def __init__(self):
        super(CNNModel, self).__init__()

        # 设定参数
        self.pool = 2  # 最大池化
        self.padding = 1  # 矩形边的补充层数
        self.dropout = 0.2  # 随机抛弃概率
        self.kernel_size = 3  # 卷积核大小 3x3

        # 卷积池化
        self.layer1 = Sequential(
            # 时序容器Sequential,参数按顺序传入
            # 2维卷积层,卷积核大小为self.kernel_size,边的补充层数为self.padding
            Conv2d(1, 32, kernel_size=self.kernel_size, padding=self.padding),
            # 对小批量3d数据组成的4d输入进行批标准化操作
            BatchNorm2d(32),
            # 随机将输入张量中部分元素设置为0,随机概率为self.dropout。
            Dropout(self.dropout),
            # 对输入数据运用修正线性单元函数
            ReLU(),
            # 最大池化
            MaxPool2d(self.pool))

        # 卷积池化
        self.layer2 = Sequential(
            Conv2d(32, 64, kernel_size=self.kernel_size, padding=self.padding),
            BatchNorm2d(64),
            Dropout(self.dropout),
            ReLU(),
            MaxPool2d(self.pool))

        # 卷积池化
        self.layer3 = Sequential(
            Conv2d(64, 128, kernel_size=self.kernel_size, padding=self.padding),
            BatchNorm2d(128),
            Dropout(self.dropout),
            ReLU(),
            MaxPool2d(self.pool))

        # 全连接
        self.fc = Sequential(
            Linear((image_width // 8) * (image_height // 8) * 128, 1024),
            Dropout(self.dropout),
            ReLU())
        self.rfc = Sequential(Linear(1024, captcha_number * len(all_str)))

    def forward(self, x):
        out = self.layer1(x)
        out = self.layer2(out)
        out = self.layer3(out)
        out = out.view(out.size(0), -1)
        out = self.fc(out)
        out = self.rfc(out)
        return out

在训练神经网络模型之前,要先将标签数据给数值化矩阵化,这样才能给模型去计算。这里我们的标签就是:“0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ”这36个字符,怎么将它们数值化呢,通常可以使用类似的独热编码来将它们数值化,就比如“A”这个字符,变为独热编码就是array([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.]),即对应位置上为1,其余均为0。但我们生成的验证码图片中有四个字符,所以我们的独热编码的shape要扩大四倍,具体实现的代码如下:

import numpy as np
import string

captcha_number=4  # 验证码字符数量
# 26个大写字母+10个数字
all_str = {v: k for k, v in enumerate(list(string.digits + string.ascii_uppercase))}

def one_hot_encode(value: list) -> tuple:
    '''编码:将字符转为独热码,vector为独热码,order用于解码'''
    order = []
    shape = captcha_number * len(all_str)
    vector = np.zeros(shape, dtype=float)
    for k, v in enumerate(value):
        index = k * len(all_str) + all_str.get(v)
        vector[index] = 1.0
        order.append(index)
    return vector, order

def one_hot_decode(value: list) -> str:
    '''解码:将独热码转为字符'''
    res = []
    for ik, iv in enumerate(value):
        val = iv - ik * len(all_str)
        for k, v in all_str.items():
            if val == int(v):
                res.append(k)
                break
    return ''.join(res)
    
if __name__ == '__main__':
    vector,order=one_hot_encode('LOVE')
    print(f'独热码:{vector}')
    print(f'用于解码的列表:{order}')
    print(f'解码结果:{one_hot_decode(order)}')

到了这里,准备工作还差最后一步就可以开始训练模型了,那就是将图片数据打包起来,然后设定好一些超参数,分批往神经网络里输入数据即可,具体实现的代码如下:

from torch.utils.data import Dataset
from torch.utils.data import DataLoader
from torchvision import transforms
from PIL import Image
import torch
import cv2
import os

class ImageDataSet(Dataset):
    '''图片加载和处理'''
    
    def __init__(self, folder):
        self.transform = transforms.Compose([
            transforms.Lambda(lambda x:process_img(x)),
            transforms.ToTensor()
        ])
        self.images = [os.path.join(folder,i) for i in os.listdir(folder)]

    def __len__(self):
        return len(self.images)

    def __getitem__(self, idx):
        image_path = self.images[idx]
        image = self.transform(image_path)
        # 获取独热码和字符位置列表
        vector, order = one_hot_encode(image_path[-8:-4])
        label = torch.from_numpy(vector)
        return image, label, order

def process_img(img_path: str) -> object:
    img = cv2.imread(img_path)
    img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) # 灰度
    img = cv2.threshold(img,20,255,cv2.THRESH_BINARY)[1] # 二值化
    img = Image.fromarray(cv2.cvtColor(img,cv2.COLOR_BGR2RGB)).convert('L') # 转为PIL并将通道数转为1
    return img

def loaders(folder: str, size: int) -> object:
    # 包装数据和目标张量的数据集
    objects = ImageDataSet(folder)
    return DataLoader(objects, batch_size=size, shuffle=True)

下面开始训练,具体代码如下:(注意:如果你的电脑没有GPU,或者安装的pytorch不是GPU版本,则直接去掉cuda()这个方法即可)

from torch.nn import MultiLabelSoftMarginLoss
from torch.autograd import Variable
from torch.optim import Adam
import logging
logging.basicConfig(level=logging.INFO)

# 数字与大写字母混合
all_str = {v: k for k, v in enumerate(list(string.digits + string.ascii_uppercase))}

# 图片路径
path_image = 'images/'
path_train = os.path.join(path_image, 'train')
path_test = os.path.join(path_image, 'test')

# 图片规格
captcha_number = 4
image_height = 40
image_width = 132

# 训练参数
epochs = 5
batch_size = 25
rate = 0.001
model_name = 'result.pkl'

def train_model():
    model = CNNModel().cuda()
    model.train()  # 训练模式
    logging.info('Train start')
    # 损失函数
    criterion = MultiLabelSoftMarginLoss()
    # Adam算法
    optimizer = Adam(model.parameters(), lr=rate)
    ids = loaders(path_train, batch_size)
    logging.info('Iteration is %s' % len(ids))
    for epoch in range(epochs):
        for i, (image, label, order) in enumerate(ids):
            # 包装Tensor对象并记录其operations
            images = Variable(image).cuda()
            labels = Variable(label.float()).cuda()
            predict_labels = model(images)
            loss = criterion(predict_labels, labels)
            # 保持当前参数状态并基于计算得到的梯度进行参数更新。
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            i += 1
            if i % 100 == 0:
                logging.info("epoch:%s, step:%s, loss:%s" % (epoch+1, i, loss.item()))
                # 保存训练结果
                torch.save(model.state_dict(), model_name)
    # 保存训练结果
    torch.save(model.state_dict(), model_name)
    logging.info('Train done')
if __name__ == '__main__':
    train_model()

大概过个几分钟,模型训练完成!

四、测试CNN模型的准确率

训练完模型后,就需要对其进行测试,测算出准确率,来评估模型的好坏。具体实现代码如下:

def test_model():
    model = CNNModel().cuda()
    model.eval()  # 预测模式
    # 载入模型
    model.load_state_dict(torch.load(model_name))
    logging.info('load cnn model')
    verifies = loaders(path_test, 1)
    correct, total, cha_len,  = 0, 0, len(all_str)
    for i, (image, label, order) in enumerate(verifies):
        captcha = one_hot_decode(order)  # 正确的验证码
        images = Variable(image).cuda()
        predict_label = model(images)
        predicts = []
        for k in range(captcha_number):
            # 根据预测结果取值
            code = one_hot_decode([(np.argmax(predict_label[0, k * cha_len: (k + 1) * cha_len].data.cpu().numpy()))])
            predicts.append(code)
        predict = ''.join(predicts)  # 预测结果
        total += 1
        if predict == captcha:
            correct += 1
        else:
            logging.info('Fail, captcha:%s->%s' % (captcha, predict))
    logging.info(f'完成。总预测图片数为{total}张,准确率为{int(100 * correct / total)}%')

if __name__ == '__main__':
    test_model()

毫无意外,准确率达99%+

五、识别验证码

看到模型准确率如此之高,当然已经是可以用来做预测,去识别验证码了,不过仅限本文所生成的验证码,因为模型对其拟合的很好了,可是一旦换了另外的类型的验证码,如字体、字号、字体颜色等不一样的验证码,识别率恐怕就不那么乐观了,那就需要基于那种验证码重新训练神经网络模型了。

其中,模型训练完后用于识别验证码的具体实现代码如下:

def predict_model(img_path: str) -> str:
    model = CNNModel().cuda()
    model.eval()  # 预测模式
    model.load_state_dict(torch.load(model_name)) # 载入模型
    transform = transforms.Compose([transforms.Lambda(lambda x:process_img(x)),transforms.ToTensor()])
    image=Image.open(img_path)
    img = transform(img_path).reshape((-1,1,image.height,image.width))
    predict_label = model(Variable(img).cuda())
    predicts=[]
    for k in range(captcha_number):
        code = one_hot_decode([(np.argmax(predict_label[0, k * len(all_str): (k + 1) * len(all_str)].data.cpu().numpy()))])
        predicts.append(code)
    predict = ''.join(predicts)
    return predict
if __name__ == '__main__':
    pred=predict_model('LOVE.png')
    print(f'识别结果为:{pred}')

以上即本文所有内容,代码也全都在上面了,我也把代码给整理了一遍,如果你不想复制粘贴的话,那就关注一下《Python王者之路》公众号,回复关键词:20201226,即可获取源代码。


PS:CNN模型建立参考了《Python3反爬虫原理与绕过实战》一书,在此特别感谢这本书的作者所提供的知识,才得以完成这篇文章。


写在最后

本文所识别的验证码模型,其泛化能力是很低的,因为训练集只是我们生成的验证码这一种类型,换成别的验证码,则会发现识别不出来。还有本文生成的验证码已经限定了字符颜色是黑色的,所以一个二值化就可以将图片处理的很好,可是如果将字符颜色变成随机的话,本文的处理方法就不适用了。

还有一个点,就是本文的CNN网络中用到了20%抛弃概率的Dropout,就是说有百分之二十的概率抛弃已经学习到的神经网络参数,这就会导致收敛速度变慢,而如果将概率设置为0或者直接不要Dropout,你会发现CNN很快就收敛了,并且准确率会倍增,毫无疑问直接100%。然而这样的模型泛化能力是极其低的,就是说它只会认它所训练的验证码,会变得死板。所以这就涉及到一个问题:是要准确率呢还是泛化能力呢?

个人认为主要还是看业务需求和数据量,或者使用其他神经网络模型。如果数据量比较少,完全可以不要Dropout来保证准确率;但如果想解决一个类型内所有的验证码,那就要先保证数据量,然后适当调整Dropout的概率,并且还需要不断地进行训练来获取一个准确率高的模型。

终于是一整套流程从头开始完成了很久以前就想实现的验证码识别了,从中学习到了很多,但我觉得还是很皮毛的东西,但这并不影响我继续研究下去,并且我也会分享我学习到的东西,大家一起交流互相学习互相切磋,一起进步~

最后,提前祝大家2021年元旦快乐吧!!

  • 8
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值