使用机器学习建模进行图片验证码识别

什么是机器学习?

听到机器学习的名头时,人们往往会一脸错愕的联想到类似《终结者》、《我,机器人》里的场景,但实际上的机器学习却与之相差甚远。

机器学习实际上更像是统计模型训练或者算法模型训练,通过训练模型和拟合数据,让模型去猜出结果。也就是说,机器学习的目标便是让算法模拟智能。

案例概述

验证码识别实际上难点并不在算法模型上,毕竟验证码千奇百怪,极难找到分布规律。这些分布规律也就是算法模型的维度,当一个模型的维度出现各式各样的偏差时,模型本身一定是会受到影响的。所以说本文的难点问题便是验证码的字符的清洗与分割。

依赖库

import os
import time
import random
import threading
import joblib
import numpy as np
from PIL import Image, ImageFilter
from concurrent.futures import ThreadPoolExecutor
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split
from captcha.image import ImageCaptcha
  • os模块主要用于文件夹操作
  • time模块用于计算时间戳等操作
  • random随机数模块
  • threading线程模块
  • joblib模块用于打包模型
  • numpy主要用于数值化图片
  • PTL图像处理模块
  • ThreadPoolExecutor线程池
  • KNeighborsClassifier即knn算法模型
  • train_test_split用于分割数据集
  • ImageCaptcha用于生成验证码图片

清洗图片

关于验证码的生成部分,由于并非本文核心,在此处不做多余赘述,详情请看下方的代码链接:

https://github.com/macxin123/verification_code_identification/blob/master/make_code.py

关于图片的请洗,一般都要经历这么几个过程:灰度图处理、黑白图像处理、中值滤波处理。

  • 灰度图处理

    原图

    由于验证码大部分都是颜色不统一的,所有说第1步便是将其颜色归一。

    from PIL import Image
    
    # 读取图片
    img = Image.open('./img.jpg')
    # 将图片转换为灰度图
    img_l = img.convert('L')
    

    convert方法即可将图像调整为灰度图。

    可调用img_l.show()查看。

    灰度图
  • 黑白图像处理

    当图片被转换成数组时,数组中的每个数值都是一个像素,当这个数值越大时,代表这个像素颜色越深。

    # 将灰度图转换为数组形式
    img_arr = np.array(img_l)
    # 求出该数组的平均值
    means = img_arr.mean() 
    # 调整平均值,作为清洗参数
    if means > 227:
        means = means + 30
    # 清洗灰度图中的“雀斑”
    img_clean = img_l.point(lambda i: i>means-40, mode='1')
    

    将图像数组化后,取数组的平均值作为黑白化的阈值,通过point方法进行调整,这个阈值最好是根据实际情况进行调整,不必死磕。

    调整后的图片
  • 中值滤波处理

    MedianFilter为PIL库的中值滤波器,该方法会以size为像素点中心,选择中值像素作为新值。

    # 使用中值滤波再次清洗
    img_end = img_clean.filter(ImageFilter.MedianFilter(size=3))
    

    因为要照顾到所有样本图片,导致中值滤波对部分图片只能起到“雀斑”优化的作用,而不能完全剔除。

    中值滤波

    也可选用最大值滤波器MaxFilter,效果如下:

    最大值滤波

    关于使用何种滤波器,需要根据实际情况权衡使用。

分割图片

完成了图像请洗的阶段后,我们需要将图片中的字符分割出来,即后续的识别是进行单个字符的识别。如果使用整张验证码进行训练,那么可能需要十分庞大的数据集,这其中的算法复杂度也会大幅度提升。

本案例的数据集最麻烦的一点在于,验证码直接的间隔基本上是随机的,甚至还有撞在一起的,这就令人十分头痛,甚至劝退,毕竟,就是用来恶心爬虫工程师的。不过办法总比困难多,毕竟Google、stackoverflow上的大神们还是蛮多的。

# 是否为开始位置
inletter = False
# 是否为结束位置
foundletter = False
# 开始位置的x坐标
start = 0
# 结束位置的x坐标
end = 0
# 存储分割位置的列表
letters = list()
# 遍历图片中的像素点,进行切割位置识别
for x in range(img.size[0]):
    for y in range(img.size[1]):
        pix = img.getpixel((x, y))
        if pix != 1:
            inletter = True
    if foundletter == False and inletter == True:
        foundletter = True
        start = x
    if foundletter == True and inletter == False:
        foundletter = False
        end = x
        letters.append((start, end))
    inletter = False

上述代码中,内层的for循环相当于是遍历图片中的列像素,如果此列存在不为1的像素,也就是非白色,则判定为起点切割位置,直到遇见整列为白色像素的列,判定为终点位置。

# 存储筛选后分割位置的列表
real_letters = list()
# 切割后的图片宽度小于15,大概率为没有清洗好的像素点,所以直接舍弃
for n in letters:
    if abs(n[0] - n[1]) > 15:
        real_letters.append(n)

由于图片中只有4个字符,但因为“雀斑”的存在,可能会多划分出好几块,所有需要上述代码进行判定舍弃。

不过心细的小伙伴会发现,此处还存在一个bug:如果两个字符连在一起,那是不是就区分不出来了?

确实,上述算法无法判定出已连接的两个字符,所有还需要下列代码去进行判定。

def max_pro(x):
    """
    返回两点间的绝对值距离
    """
    return abs(x[0] - x[1])
# 如果real_letters为3,代码有2个字符被分割到了一起
if len(real_letters) == 3:
    res = max(real_letters, key=max_pro)
    res_index = real_letters.index(res)
    le = list()
    for i in range(4):
        if len(le) == 4:
            break
        if i == res_index:
            # 再次分割图片
            ca = round(abs(real_letters[i][1] - real_letters[i][0]) / 2)
            le.append((real_letters[i][0], real_letters[i][0] + ca))
            le.append((real_letters[i][0] + ca +1, real_letters[i][1]))
        else:
            le.append(real_letters[i])     
    real_letters = le
# 如果real_letters为2,代码有3个字符被分割到了一起
elif len(real_letters) == 2:
    res = max(real_letters, key=max_pro)
    res_index = real_letters.index(res)
    le = list()
    for i in range(4):
        if len(le) == 4:
            break
        if i == res_index:
            # 再次分割图片
            ca = round(abs(real_letters[i][1] - real_letters[i][0]) / 3)
            le.append((real_letters[i][0], real_letters[i][0] + ca))
            le.append((real_letters[i][0] + ca + 1, real_letters[i][0] + ca + ca))
            le.append((real_letters[i][0] + ca + ca +1, real_letters[i][1]))
        else:
            le.append(real_letters[i])     
    real_letters = le
# 4为正确分割,此时什么都不用做
elif len(real_letters) == 4:
    pass
# 出现其他情况,则放弃该验证码
else:
    return None

上述代码的核心即,根据列表中的数量的不同,选择最宽的那段进行平均切割。(缺几块,分几份)

有了分割位置的列表,就可以开始切图了。

# 文件名(验证码内容)
filename = img.filename.rsplit('.')[1].rsplit('/')[-1]

for i, v in enumerate(real_letters):
    # 切割的起始横坐标,起始纵坐标,切割的宽度,切割的高度
    img_split = img.crop((v[0], 0, v[1], img.size[1]))
    # 将图片size进行统一
    i_m_g = img_split.resize((40, 60), Image.ANTIALIAS)
    # windows文件夹名不区分大小写,所以文件夹名需要更改
    if filename[i].isupper():
        dir_name = filename[i] + '_Upper'
        i_m_g.save(f'./chars/{dir_name}/{filename[i] + str(random.randint(1, 999)) + str(time.time())[:8]}.jpg')
    else:
        i_m_g.save(f'./chars/{filename[i]}/{filename[i] + str(random.randint(1, 999)) + str(time.time())[-6:]}.jpg')

保存图片的时候有一个坑需要注意一下,那就是关于文件夹的命名问题,a文件夹和A文件夹是一个文件夹
,即windows文件夹名称是不区分大小写的,这点需要特别注意一下。

还有就是图片大小的问题,需要通过resize方法进行size的统一,否则会导致数据集的偏差过大。

分割好的图片数据

KNN分类算法

KNN最近邻算法是一种非常简单易懂的算法,使用“距离”进行度量,通过“多数表决”进行分类。可以解决有监督学习的分类问题、回归问题、以及无监督学习等多个领域的问题,适用范围较广。本文的算法模型将使用KNN进行验证码识别模型训练。

训练模型

训练模型前,我们需要先将图片数值化,以便进行运算,之后再划分训练集和测试集。

# 数据
data = list()
# 标签
label = list()
# 图片路径
path = './chars/'
dir_list = os.listdir(path)

for p in dir_list:
    dir_path = path + p + '/'
    lst = os.listdir(dir_path)
    for jpg in lst:
        img = Image.open(dir_path + jpg)
        filename = img.filename.rsplit('/')[-1].split('.')[0][0]
        label.append(list(filename))
        img_arr = np.array(img).tolist()
        xx = list()
        for arr in img_arr:
            for a in arr:
                xx.append(a)
        data.append(xx)

使用sklearn自带的train_test_split方法自动划分训练集和测试集。

# 测试集占比30%
x_train, x_test, y_train, y_test = train_test_split(x_list, y_list, test_size=0.3, random_state=12)

接下来,便是直接开始训练模型了。

# 实例化knn算法模型
knn = KNeighborsClassifier()
# 训练数据
knn.fit(x_train, y_train)
# 测试准确率
score = knn.score(x_test, y_test)

此时,我们最好通过更换K值测试最优的算法模型,当然,此处使用KNN也并不是最优解,作者测试准确率时,仅有70%左右的准确率。

最后一步,便是保存我们的算法模型了。

joblib.dump(knn, './knn_model.pkl')

结语

写到最后的一刻我是有些崩溃的,因为处理了很多意料之外的麻烦,再加上最后的准确率只有7成,实在让人劝退。

一直到最后我都在问自己,难道是CNN不香了嘛???即便是CNN不香,那打码平台也不香嘛???

没办法,自己挖的坑还是得自己填。不过呢,在实际工作中,尤其是爬虫程序中遇见图片验证码时,还是建议使用打码平台或者使用深度学习算法去建模。

关于这个验证码识别脚本的全部代码,请参考下面的GitHub链接:

https://github.com/macxin123/verification_code_identification

你可能感兴趣的:(使用机器学习建模进行图片验证码识别)