机器学习实现验证码识别

#AI #Case #Done

SVM实现

处理步骤

  1. 获取训练数据图片
  2. 图片处理
  3. 模型训练
  4. 模型测试

获取验证码图片

用的网上已有的验证码图片

Untitled.png

图片处理

图片二值化

  1. 将RGB彩图转为灰度图
  2. 将灰度图按照设定阈值转化为二值图

将RGB彩图转为灰度图二值化

#将图片灰度化、二值化
def binarizing(pic):
    im = Image.open(root_path+'/'+img_path+'/'+pic)
# 二值化图片 将RGB彩图转为灰度图
    im_grey = im.convert('L')
    im_binary = im_grey.convert('1') 
    table = get_bin_table(140)
    out = im_grey.point(table, '1')
    
    return out

图片切割

通过观察验证码图片的特征,可以查看
Untitled (1).png
可以得到如下参数:

则根据此特点利用代码切割字符串

def get_crop_imgs(img):
    """
    按照图片的特点,进行切割,这个要根据具体的验证码来进行工作
    :param img:
    :return:
    """
    child_img_list = []
    for i in range(4):
        x = 2 + i * (8 + 5)  
        y = 0
        child_img = img.crop((x, y, x + 8, y + 18))
        child_img_list.append(child_img)
    return child_img_list

Untitled (2).png

尺寸归一化

将分割好的字符串按照规定的大小进行存储

for image in images:
    o = image.resize((8, 8))
    o.save('./tmp1/'+str(n)+'.png','PNG')
    n += 1

模型训练

  1. 给图片打上标签
  2. 得到图片识别特征
  3. 模型训练

打标签

利用人工打标签,新建0-9十个文件夹,将肉眼可辨别的一部分分割字符串复制到对应的文件夹中

Untitled (3).png

提取图片特征

读取图片然后遍历每一个像素点提取非白色的像素点,然后加起来,将像素点的和作为图片特征

# 提取SVM用的特征值, 提取字母特征值
def getletter(fn):
    img = cv2.imread(fn)  # 读取图像
    alltz = []
    for now_h in range(0, 8):
        xtz = []
        for now_w in range(0, 18):
            b = img[now_h, now_w, 0]
            g = img[now_h, now_w, 1]
            r = img[now_h, now_w, 2]
            btz = 255 - b
            gtz = 255 - g
            rtz = 255 - r
            if btz > 0 or gtz > 0 or rtz > 0:
                nowtz = 1
            else:
                nowtz = 0
            xtz.append(nowtz)
        alltz += xtz
    return alltz

模型训练

使用SVM向量机进行学习训练,生训练结果

# 进行向量机的训练SVM
def trainSVM():
    array = extractLetters('tmp')
    # 使用向量机SVM进行机器学习
    letterSVM = SVC(kernel="linear", C=1).fit(array[0], array[1])
    # 生成训练结果
    joblib.dump(letterSVM, 'data/letter.pkl')

验证码识别

利用训练的模型,载入模型,然后载入识别的图片,将图片进行灰度化,切割处理,提取特征等操作后,进行识别

def ocrImg(fileName):
    clf = joblib.load('data/letter.pkl')
    p = Image.open('test_img/%s' % fileName)
    #p = cutImg(p)
    #b_img = binarizing(p, 170)
    b_img=p.convert('L')
    #v = vertical(b_img)
    imgs=get_crop_imgs(b_img)
    #imgs = getSplitImg(b_img, v)
    captcha = []
    for i, img in enumerate(imgs):
        path = 'test_img/letter_%s.png' % i
        img.save(path)
        data = getletter(path)
        data = np.array([data])
        # print(data)
        oneLetter = clf.predict(data)[0]
        # print(oneLetter)
        captcha.append(oneLetter)
    captcha = [str(i) for i in captcha]
    print("the captcha is :%s" % ("".join(captcha)))

Untitled (4).png
Untitled (5).png

参考链接

https://blog.csdn.net/weixin_43790276/article/details/108478270

CNN实现

实验环境

数据集来源:链接: https://pan.baidu.com/s/1-b8Z1NeH-r2-w30CTOZeug 提取码: 99kw

设计思路

方案选取

验证码形如下图,六位验证码并且存在干扰线,文件名形如1a1c63_d06b8c3fd606c4ca633c523ac408afc7.jpg,文件名前6位为我们标注好了图中的字符串。

Untitled (6).png
起初采取SVM支持向量机进行识别,在干扰线去除效果方面并不好,只能识别一些简单的验证码,因此经过调研之后采取基于CNN卷积神经网络的直接端到端的验证识别技术。

准备工作

one-hot编码

验证码识别也还是分类问题,一共abcdefghijklmnopqrstuvwxyz加上0123456789一共36个类别,因此我们将验证码用one-hot编码代替

captcha_list = list('0123456789abcdefghijklmnopqrstuvwxyz')
captcha_length = 6

验证码文本使用向量表示,采用的是ONE-HOT的形式

# 验证码文本转为向量
def text2vec(text):
    vector = torch.zeros((captcha_length, len(captcha_list)))
    text_len = len(text)
    for i in range(text_len):
        vector[i,captcha_list.index(text[i])] = 1    
    return vector

向量转成验证码文本

# 验证码向量转为文本
def vec2text(vec):
    label = torch.nn.functional.softmax(vec, dim =1)
    vec = torch.argmax(label, dim=1)
    for v in vec:
        text_list = [captcha_list[v] for v in vec]
    return ''.join(text_list)

如上面的1a1c63向量化结果如下图,1的位置对应着相应的字符

print(text2vec('1a1c63'))

tensor([[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., 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., 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., 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., 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., 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.]])

加载数据

使用torch.utils.data.Dataset类,重新实现类的两个魔法方法:len(self),getitem(self)

  1. len(self): 返回容器元素的个数
  2. getitem(self) 定义获取容器中指定元素的行为,在获取数据时向量化,统一大小

其中读取图像默认图像为4通道RGBA,A为透明通道,所以需要img.convert('RGB')方便后续训练

# 加载所有图片,并将验证码向量化
def make_dataset(data_path):
    img_names = os.listdir(data_path)
    samples = []
    for img_name in img_names:
        img_path = data_path+img_name
        target_str = img_name.split('_')[0].lower()
        samples.append((img_path, target_str))
    return samples

class CaptchaData(Dataset):
    def __init__(self, data_path, transform=None):
        super(Dataset, self).__init__()
        self.transform = transform
        self.samples = make_dataset(data_path)

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

    def __getitem__(self, index):
        img_path, target = self.samples[index]
        target = text2vec(target)
        target = target.view(1, -1)[0]
        img = Image.open(img_path)
        img = img.resize((140,44))
        img = img.convert('RGB') # img转成向量
        if self.transform is not None:
            img = self.transform(img)
        return img, target

构建神经网络

构建3个卷积层池化层,两个全连接层

定义损失函数用交叉熵

优化器采用Adam

class Net(nn.Module):

    def __init__(self):
        super(Net, self).__init__()
        # 第一层神经网络
        # nn.Sequential: 将里面的模块依次加入到神经网络中
        self.layer1 = nn.Sequential(
            nn.Conv2d(3, 16, kernel_size=3, padding=1), # 3通道变成16通道,图片:44*140
            nn.BatchNorm2d(16),
            nn.ReLU(),
            nn.MaxPool2d(2)  # 图片:22*70
        )
        # 第2层神经网络
        self.layer2 = nn.Sequential(
            nn.Conv2d(16, 64, kernel_size=3), # 16通道变成64通道,图片:20*68
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.MaxPool2d(2)  # 图片:10*34
        )
        # 第3层神经网络
        self.layer3 = nn.Sequential(
            nn.Conv2d(64, 128, kernel_size=3), # 16通道变成64通道,图片:8*32
            nn.BatchNorm2d(128),
            nn.ReLU(),
            nn.MaxPool2d(2)  # 图片:4*16
        )
        # 第4层神经网络
        self.fc1 = nn.Sequential(
            nn.Linear(4*16*128, 1024),
            nn.Dropout(0.2),  # drop 20% of the neuron
            nn.ReLU()
        )
        # 第5层神经网络
        self.fc2 = nn.Linear(1024, 6*36) # 6:验证码的长度, 36: 字母列表的长度

    #前向传播
    def forward(self, x):
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = x.view(x.size(0), -1)
        x = self.fc1(x)
        x = self.fc2(x)
        return x

训练模型

优先采取GPU进行训练,每2000次做一次输出

def train(epoch_nums):
    # 数据准备
    transform = transforms.Compose([transforms.ToTensor()]) 
    train_dataset = CaptchaData('I:\\\\Coding\\\\mssb\\\\trains\\\\', transform=transform)
    train_data_loader = DataLoader(train_dataset, batch_size=32, num_workers=0, shuffle=True, drop_last=True)

    test_data = CaptchaData('I:\\\\Coding\\\\mssb\\\\tests\\\\', transform=transform)
    test_data_loader = DataLoader(test_data, batch_size=128, num_workers=0, shuffle=True, drop_last=True)
    # 更换设备
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    #device=torch.device("cpu")
    print('当前设备是:',device)
    net.to(device)

    criterion = nn.MultiLabelSoftMarginLoss() # 损失函数
    optimizer = torch.optim.Adam(net.parameters(), lr=0.001) # 优化器

    # 加载模型
    model_path = 'I:\\\\Coding\\\\mssb\\\\model.pth'
    if os.path.exists(model_path):
        print('开始加载模型')
        checkpoint = torch.load(model_path)
        net.load_state_dict(checkpoint['model_state_dict'])
        optimizer.load_state_dict(checkpoint['optimizer_state_dict'])

    # 开始训练
    i = 1
    for epoch in range(epoch_nums):
        running_loss = 0.0
        net.train() # 神经网络开启训练模式
        for data in train_data_loader:
            inputs, labels = data
            inputs, labels = inputs.to(device), labels.to(device) #数据发送到指定设备
            #每次迭代都要把梯度置零
            optimizer.zero_grad()
            # 关键步骤
            # 前向传播
            outputs = net(inputs)
            # 计算误差
            loss = criterion(outputs, labels)
            # 后向传播
            loss.backward()
            # 优化参数
            optimizer.step()

            running_loss += loss.item()
            if i % 2000 == 0:
                acc = calculat_acc(outputs, labels)
                print('第%s次训练正确率: %.3f %%, loss: %.3f' % (i,acc,running_loss/2000))
                running_loss = 0
                # 保存模型
                torch.save({
                            'model_state_dict':net.state_dict(),
                            'optimizer_state_dict':optimizer.state_dict(),
                            },model_path)
            i += 1

运行

训练截图

Untitled (7).png

(python38) λ python main.py
当前设备是: cuda:0
第2000次训练正确率: 0.000 %, loss: 0.089
第4000次训练正确率: 18.750 %, loss: 0.055
测试集正确率: 39.062 %
第6000次训练正确率: 31.250 %, loss: 0.011
第8000次训练正确率: 31.250 %, loss: 0.034
第10000次训练正确率: 56.250 %, loss: 0.028
测试集正确率: 57.812 %
第12000次训练正确率: 65.625 %, loss: 0.013
第14000次训练正确率: 62.500 %, loss: 0.019
第16000次训练正确率: 75.000 %, loss: 0.017
测试集正确率: 71.875 %
第18000次训练正确率: 78.125 %, loss: 0.012
第20000次训练正确率: 75.000 %, loss: 0.013
测试集正确率: 74.219 %
第22000次训练正确率: 87.500 %, loss: 0.002
第24000次训练正确率: 87.500 %, loss: 0.010
第26000次训练正确率: 87.500 %, loss: 0.009
测试集正确率: 83.594 %
第28000次训练正确率: 93.750 %, loss: 0.003
第30000次训练正确率: 81.250 %, loss: 0.007
第32000次训练正确率: 90.625 %, loss: 0.007
测试集正确率: 84.375 %
第34000次训练正确率: 84.375 %, loss: 0.004
第36000次训练正确率: 87.500 %, loss: 0.006
测试集正确率: 85.938 %
第38000次训练正确率: 87.500 %, loss: 0.000
第40000次训练正确率: 90.625 %, loss: 0.005
第42000次训练正确率: 96.875 %, loss: 0.005
测试集正确率: 89.062 %
第44000次训练正确率: 96.875 %, loss: 0.002
第46000次训练正确率: 96.875 %, loss: 0.005
第48000次训练正确率: 93.750 %, loss: 0.005
测试集正确率: 88.281 %
第50000次训练正确率: 90.625 %, loss: 0.003
第52000次训练正确率: 96.875 %, loss: 0.004
第54000次训练正确率: 100.000 %, loss: 0.004
测试集正确率: 86.719 %
第56000次训练正确率: 96.875 %, loss: 0.003
第58000次训练正确率: 100.000 %, loss: 0.004
测试集正确率: 94.531 %
第60000次训练正确率: 93.750 %, loss: 0.001
第62000次训练正确率: 93.750 %, loss: 0.003
第64000次训练正确率: 100.000 %, loss: 0.003
测试集正确率: 92.969 %
第66000次训练正确率: 96.875 %, loss: 0.001
第68000次训练正确率: 100.000 %, loss: 0.003
第70000次训练正确率: 96.875 %, loss: 0.003
测试集正确率: 89.844 %
第72000次训练正确率: 96.875 %, loss: 0.002
第74000次训练正确率: 96.875 %, loss: 0.003
测试集正确率: 89.844 %
第76000次训练正确率: 100.000 %, loss: 0.000
第78000次训练正确率: 100.000 %, loss: 0.003
第80000次训练正确率: 100.000 %, loss: 0.003
测试集正确率: 91.406 %
第82000次训练正确率: 93.750 %, loss: 0.001
第84000次训练正确率: 96.875 %, loss: 0.002
第86000次训练正确率: 100.000 %, loss: 0.002
测试集正确率: 94.531 %
第88000次训练正确率: 100.000 %, loss: 0.001
第90000次训练正确率: 100.000 %, loss: 0.002
第92000次训练正确率: 96.875 %, loss: 0.002
测试集正确率: 84.375 %
第94000次训练正确率: 96.875 %, loss: 0.002
第96000次训练正确率: 100.000 %, loss: 0.002
测试集正确率: 91.406 %
第98000次训练正确率: 96.875 %, loss: 0.001
第100000次训练正确率: 87.500 %, loss: 0.002
第102000次训练正确率: 96.875 %, loss: 0.002
测试集正确率: 94.531 %
第104000次训练正确率: 100.000 %, loss: 0.001
第106000次训练正确率: 100.000 %, loss: 0.002
第108000次训练正确率: 96.875 %, loss: 0.002
测试集正确率: 88.281 %

最后选取了5000个样本进行测试,测试结果如下

验证码是:3p6hys, 预测为:3p6hys,结果正确                                    
验证码是:3p6k32, 预测为:3p6k32,结果正确                                    
验证码是:3p6m66, 预测为:3p6m66,结果正确                                    
验证码是:3p6p2f, 预测为:3p6p2f,结果正确                                    
验证码是:3p6p2r, 预测为:3p6p2r,结果正确                                    
验证码是:3p6p37, 预测为:3p6p37,结果正确                                    
验证码是:3p6p54, 预测为:3p6p54,结果正确                                    
验证码是:3p6p58, 预测为:3p6p58,结果正确                                    
验证码是:3p6p76, 预测为:3p6p76,结果正确                                    
验证码是:3p6p8j, 预测为:3p6p8j,结果正确                                    
验证码是:3p6pdc, 预测为:3p6pdc,结果正确                                    
验证码是:3p6r2c, 预测为:3p6r2c,结果正确                                    
验证码是:3p6r2y, 预测为:3p6r2y,结果正确                                    
验证码是:3p6r4y, 预测为:3p6r4y,结果正确                                    
验证码是:3p6t8a, 预测为:3p6t8a,结果正确                                    
验证码是:3p6ve5, 预测为:3p6ve5,结果正确                                    
验证码是:3p6vt2, 预测为:3p6vt2,结果正确                                    
验证码是:3p6y34, 预测为:3p6y34,结果正确                                    
验证码是:3p6ymr, 预测为:3p6ymr,结果正确                                    
验证码是:3p7ap3, 预测为:3p7ap3,结果正确                                    
验证码是:3p7bs4, 预测为:3p7bs4,结果正确                                    
验证码是:3p7bsx, 预测为:3p7bsx,结果正确                                    
...
...                                 
验证码是:3p8c7e, 预测为:3p8c7e,结果正确                                    
验证码是:3p8dh7, 预测为:3p8dh7,结果正确                                    
验证码是:3p8ff1, 预测为:3p8ff1,结果正确                                    
验证码是:3p8fs2, 预测为:3p8fs2,结果正确                                    
验证码是:3p8htx, 预测为:3p8htx,结果正确                                    
验证码是:3p8v44, 预测为:3p8v44,结果正确                                    
验证码是:3p8xf5, 预测为:3p8xf5,结果正确                                    
验证码是:3p8y83, 预测为:3p8y83,结果正确                                    
验证码是:3p8y8k, 预测为:3p8y8k,结果正确                                    
最终正确率:0.9789285714285714

最后利用argsparse模块方便终端调用识别,最后运行结果

parser = argparse.ArgumentParser(description='valite the captcha.')
parser.add_argument('-d', dest='dir',help='input the dir you need to valite')
args = parser.parse_args()

if args.dir is not None:
    model_path = 'I:\\\\Coding\\\\mssb\\\\model.pth'
    if os.path.exists(model_path):
        print('开始加载模型')
        checkpoint = torch.load(model_path)
        net.load_state_dict(checkpoint['model_state_dict'])
    transform = transforms.Compose([transforms.ToTensor()]) 
    vil_data = CaptchaData(args.dir, transform=transform)
    vil_data_loader = DataLoader(vil_data, batch_size=1, num_workers=0, shuffle=False)
    #开始预测
    acc = 0
    for img, target in vil_data_loader:
        pre = predict(img)
        print(f'预测结果{pre}')

Untitled (8).png