使用深度学习识别滑动验证码的缺口

上一节中我们使用深度学习完成了图形验证码的识别过程,正确率和使用 OCR 技术相比,高了非常多。这时可能有朋友会说,8.2 节不是还介绍了一种使用图像处理技术识别滑动验证码缺口的方法吗? 深度学习可以用在这种场景下吗?

当然可以,本节中我们就来了解使用深度学习识别滑动验证码缺口的方法。

准备工作

和 8.3 节一样,本节主要侧重于介绍利用深度学习模型识别滑动验证码缺口的过程,不会深入讲解深度学习模型的算法。另外由于整个模型的实现较为复杂,故不会从零开始编写代码,而是倾向于提前把代码下载下来进行实操练习。代码地址为 https://github.com/Python3WebSpider/DeepLearningSlideCaptcha2 ,还是先把它复制下来:

git clone https://github.com/Python3WebSpider/DeepLearningSlideCaptcha2.git

运行完毕后,本地就会出现一个 DeepLearningImageCaptcha2 文件夹,证明复制成功。之后,切换到 DeepLearningImageCaptcha2 文件夹,安装必要的依赖库:

pip3 install -r requirements.txt

运行完毕后,本项目所需的依赖库就全部安装好了。

目标检测

识别滑动验证码的自标缺口问题,其实可以归结为自标检测同题。什么叫自标检测?这里简单介绍一下。目标检测,顾名思义就是把想找的东西找出来,例如这里有一张包含狗的图片,我们想知道狗和狗的舌头在哪儿,如图 8-20 所示。

先找到图片中的狗和狗的舌头,再把它们框起来,就是目标检测。我们希望经过目标检测算法处理后得到的图片如图 8-21 所示。

图8-20 示例图片 图8-21 目标检测处理后得到的图片

现在比较流行的目标检测算法有 R-CNN、FastR-CNN、FasterR-CNN、SSD、YOLO 等,感兴趣的话可以了解一下,当然就算不太了解对学习本节也不会有影响。

目前目标检测算法主要有两种实现方法一一一阶段式和两阶段式,英文叫作 OneStage 和 TwoStage。

  • OneStage:不需要产生候选框,直接将目标的定位和分类问题转化为回归问题,俗称 “看一眼”,使用这种实现的算法有 YOLO 和 SSD,这种方法虽然正确率不及 Two Stage,但架构相对简单,检测速度更快。

  • TwoStage:算法首先生成一系列自标所在位置的候选框,再对这些框选出来的结果进行样本分类,即先找出来在哪儿,再分出来是啥,俗称 “看两眼”,使用这种实现的算法有 R-CNN、 FastR-CNN、FasterR-CNN 等,这种方法架构相对复杂,但正确率高。

这里我们选用 YOLO 算法实现对滑动验证码缺口的识别。YOLO 的英文全称是 You Only Look Once,目前的最新版本是 V5,应用比较广泛的版本是 V3,算法的具体流程我们就不过多介绍了,感兴趣的话可以搜一下相关资料。另外,可以了解一下 YOLO V1~V3 版本的不同和改进之处,这里列几个参考链接。

数据准备

和 8.3 节一样,训练深度学习模型需要准备训练数据。这里的数据也分两部分,一部分是验证码图片,另一部分是数据标注。和 8.3 节不一样的是,这次的数据标注不再是单纯的验证码文本,而是缺口位置,缺口对应一个矩形框,要表示矩形框,至少需要 4 个数据,如矩形左上角的点的横纵坐标 x、y 加上矩形的宽高 w、h。

明确了数据是什么,接下来就着手准备吧。第一步是收集验证码图片,第二步是标注图片中的缺口位置并转为化想要的 4 位数字。举个例子,打开网站 https://captchal.scrape.center/ ,点击 “登录” 按钮就会弹出一个滑动验证码,如图 8-22 所示。

单独将图 8-23 中框起来的区域保存下来,就收集了一张验证码图片。

图8-22 示例网站的滑动验证码 图8-23 要保存的区域

怎么保存那个区域呢?手工截图肯定不可靠,因为要收集的图片量非常大,这么做不仅费时费力,还不好准确地定位边界,会导致保存下来的图片有大有小。为了解决这个问题,可以简单写一个脚本实现自动化裁切和保存,就是之前下载的代码仓库中的 collect.py 文件,其内容如下:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import WebDriverException
import time
from loguru import logger

COUNT = 1000

for i in range(1, COUNT + 1):
    try:
        browser = webdriver.Chrome()
        wait = WebDriverWait(browser, 10)
        browser.get('https://captcha1.scrape.center/')
        button = wait.until(EC.element_to_be_clickable(
            (By.CSS_SELECTOR, '.el-button')))
        button.click()
        captcha = wait.until(
            EC.presence_of_element_located((By.CSS_SELECTOR, '.geetest_slicebg.geetest_absolute')))
        time.sleep(5)
        captcha.screenshot(f'data/captcha/images/captcha_{i}.png')
    except WebDriverException as e:
        logger.error(f'webdriver error occurred {e.msg}')
    finally:
        browser.close()

代码中先是一个 for 循环,循环次数为 COUNT,每次循环都使用 Selenium 启动一个浏览器,然后打开目标网站,模拟点击 “登录” 按钮的操作触发验证码弹出,然后截取验证码对应的节点,再调用 screenshot 方法保存下来。

运行 collect.py 文件:

python3 collect.py

运行完后,data/captcha/images/ 目录下就会出现很多验证码图片,例如图 8-24 所示的这样。

图8-24 收集的验证码图片

第一步完成,接下来完成第二步一一准备数据标注,这里推荐使用的工具是 labelImg,下载地址为 https://github.com/tzutalin/labellmg 。使用 pip3 工具安装它:

pip3 install labelImg

安装完后可以直接在命令行运行:

labelImg

这样就成功启动了 labelImg 工具,如图 8-25 所示。

图8-25 启动了 labelImg 工具

点击左侧的 Open Dir 按钮打开 data/captcha/images/ 目录,然后点击左下角的 Create RectBox 创建一个标注框,可以将缺口所在的矩形框框起来,框完后 labelImg 会弹出填写保存名称的提示框,填写 target,然后点击 0K 按钮,如图 8-26 所示。

图8-26 将缺口所在的矩形框保存为 target

这时会发现本地保存 xml 文件。内容如下:

<annotation>
    <folder>images</folder>
    <filename>captcha_0.png</filename>
    <path>data/captcha/images/captcha_0.png</path>
    <source>
        <database>Unknown</database>
    </source>
    <size>
        <width>520</width>
        <height>320</height>
        <depth>3</depth>
    </size>
    <segmented>0</segmented>
    <object>
        <name>target</name>
        <pose>Unspecified</pose>
        <truncated>0</truncated>
        <difficult>0</difficult>
        <bndbox>
            <xmin>321</xmin>
            <ymin>87</ymin>
            <xmax>407</xmax>
            <ymax>167</ymax>
        </bndbox>
    </object>
</annotation>

从中可以看到,size 节点里有三个节点,分别是 widthheightdepth, 代表原始验证码图片的宽度、高度和通道数。object 节点下的 bndbox 节点包含标注的缺口位置,通过观察对比可知 xminymin 指的是左上角的坐标,xmaxymax 指的是右下角的坐标。

我们可以使用下面的方法简单做一下数据处理:

以下是图片中识别出的文本:

文件 1 (image_c1091b.jpg):

从中可以看到,size 节点里有三个节点,分别是 width、height 和 depth, 代表原始验证码图片的
宽度、高度和通道数。object 节点下的 bndbox 节点包含标注的缺口位置,通过观察对比可知 xmin、
ymin 指的是左上角的坐标,xmax、ymax 指的是右下角的坐标。

我们可以使用下面的方法简单做一下数据处理:

import xmltodict
import json

def parse_xml(file):
    xml_str = open(file, encoding='utf-8').read()
    data = xmltodict.parse(xml_str)
    data = json.loads(json.dumps(data))
    annotation = data.get('annotation')
    width = int(annotation.get('size').get('width'))
    height = int(annotation.get('size').get('height'))
    bndbox = annotation.get('object').get('bndbox')
    box_xmin = int(bndbox.get('xmin'))
    box_xmax = int(bndbox.get('xmax'))
    box_ymin = int(bndbox.get('ymin'))
    box_ymax = int(bndbox.get('ymax'))
    box_width = (box_xmax - box_xmin) / width
    box_height = (box_ymax - box_ymin) / height
    return box_xmin / width, box_ymin / height, box_width / width, box_height / height

这里定义了一个 parse_xml 方法,这个方法首先读取 xml 文件,然后调用 xmltodict 库里的 parse 方法将 XML 字符串转换为 JSON 字符,之后依次读取验证码的宽高信息和缺口的位置信息,最后以元组的形式返回我们想要的数据——缺口左上角的点的坐标和宽高的相对值。都标注好后,对每个 xml 文件都调用一次此方法便可以生成想要的标注结果了。

我已经将对应的标注结果都处理好了,大家可以直接使用,结果的保存路径为 data/captcha/labels,如图 8-27 所示。

图8-27 对所有 xml 文件的标注结果

其中每个 txt 文件各对应一张验证码图片的标注结果,文件内容类似这样:

0 0.6153846153846154 0.275 0.16596774 0.24170968

第一位 0 代表标注目标的索引,由于我们只需要检测一个缺口,所以索引就是 0;第二位和第三位代表缺口左上角的点在验证码图片中所处的位置,例如 0.6153846153846154 代表从横向看,缺口左上角的点大约位于验证码的 61.5% 处,这个值乘上验证码的宽度 520 得到 320,表示左上角的点的偏移量是 320 像素;第四位和第五位代表缺口的宽高和验证码图片的宽高的比,例如用第五位的 0.24170968 乘以验证码图片的高度 320 得到大约 77,表示缺口的高度大约为 77 像素。

至此,数据准备阶段完成。

训练

为了达到更好的训练效果,还需要下载一些预训练模型,预训练的意思是已经有一个提前训练好的基础模型了。直接使用预训练模型中的权重文件,就不用从零开始训练模型了,只需要基于之前的模型进行微调即可,这样既节省训练时间,又能达到比较好的效果。

先下载预训练模型,YOLO V3 模型才能有不错的训练效果。下载预训练模型的命令如下:

bash prepare.sh

在 Windows 环境下,请使用 Bash 命令行工具(如 Git Bash)运行此命令。

之后,就能下载 YOLO V3 模型的一些权重文件,包括 yolov3.weightsdarknet.weights。在正式训练模型之前,需要使用这些权重文件初始化 YOLO V3 模型。

接下来就是训练了,还是推荐使用 GPU 来训练,运行如下命令:

bash train.sh

同样,在 Windows 环境下请使用 Bash 命令行工具(如 Git Bash)运行此命令。

在训练过程中,我们可以使用 TensorBoard 观察 lossmAP 的变化,运行如下命令:

tensorboard --logdir='logs' --port=6006 --host 0.0.0.0

请确保已经正确安装了本项目的所有依赖库,其中就包括 TensorBoard,安装成功之后便可以使用 tensorboard 命令。

运行后,打开 http://localhost:6006 观察,loss 的变化和图 8-28 类似。

mAP 的变化和图 8-29 类似。

图8-28 loss 的变化 图8-29 mAP 的变化

可以看到,loss 最初非常高,之后下降到很低,正确率也逐渐接近 100%。

以下是模型训练过程中命令行中的一些输出结果:

这里显示了训练过程中各个指标的变化情况,如 lossrecallprecisionconf_obj,分别代表损失(越小越好)、召回率(能识别出的结果占应该识别出的结果的比例,越高越好)、精确率(识别结果中识别正确的概率,越高越好)和置信度(模型有把握识别对的概率,越高越好),可以作为参考。

测试

模型训练完毕后,会在 checkpoints 文件夹下生成一些 pth 文件,这些就是模型文件,和 8.3 节的 best_model.pkl 文件原理一样,只是表示形式略有不同,我们可以直接使用这些模型做测试,生成标注结果。

先在测试文件夹 data/captcha/test 下放人一些验证码图片,样例如图 8-30 所示。 运行如下命令做测试:

bash detect.sh

该命令会读取测试文件夹下的所有图片,并将处理后的结果输出到 data/captcha/result 文件夹,控制台会输出一些验证码的识别结果。同时,在 data/captcha/result 文件夹下会生成标注结果,样例如图 8-31 所示。

图8-30 样例图片 图8-31 带有标注结果的样例图片

可以看到,缺口被准确识别出来了。

实际上,detect.sh 命令的作用是运行 detect.py 文件,这个文件中的关键代码如下:

bbox = patches.Rectangle((x1 + box_w/2, y1+box_h/2), box_w, box_h, linewidth=2, edgecolor=color, facecolor="none")
print('bbox', (x1, y1, box_w, box_h), 'offset', x1)

这里的 bbox 就是指最终缺口的轮廓位置,x1指的是轮廓最左侧距离整个验证码最左侧的横向偏移量,即 offset。通过这两个信息,就能得到缺口的位置了。

得到了目标缺口的位置,便可以进行一些模拟滑动的操作从而通过验证码的检测。

总结

本节主要介绍使用深度学习识别滑动验证码缺口的整体流程,最终我们成功训练出了模型,并得到了一个深度学习模型文件。

往这个模型中输入一张滑动验证码图片,模型便会输出缺口的相关信息,包括偏移量、宽度等,通过这些信息可以确定缺口所处的位置。

和 8.3 节一样,本节介绍的内容也可以做进一步优化,即把预测过程对接 API 服务器,例如对接 Flask、Django、FastAPI等,把预测过程实现为一个支持 POST 请求的 API。