使用OpenCV识别滑动验证码的缺口

随着互联网技术的发展,各种新型验证码层出不穷,最具有代表性的便是滑动验证码。本节中我们首先了解滑动验证码的验证流程,然后介绍一个简易的利用图像处理技术识别滑动验证码缺口的方法。

滑动验证码

说起滑动验证码,比较有代表性的服务商有极验、网易易盾等,验证码效果如图 8-7 和图 8-8 所示。

图8-7 极验的滑动验证码

图8-8 网易易盾的滑动验证码

滑动验证码的下方通常有一个滑轨,上面带有类似 “拖动滑块完成拼图” 字样的文字提示,我们需要向右拖曳滑轨上的滑块,这时正上方的滑块会随它一起向右移动。验证码的右侧有一个滑块缺口,我们将滑块恰好拖到这个缺口处,就算验证成功了,图 8-7 验证成功的效果如图 8-9 所示。

如果我们想用爬虫自动化完成这一流程,关键步骤有两个:

  1. 识别目标缺口的位置;

  2. 将滑块拖到缺口位置。

其中第(2)步的实现方式有很多,例如可以用 Selenium 等自动化工具模拟这个流程,验证并登录成功后获取对应的 Cookie 或 Token 等信息,再进行后续操作,但这种方法的运行效率比较低。也可以直接逆向验证码背后的 JavaScript 逻辑,将缺口信息直接传给 JavaScript 代码,执行获取类似 “密钥” 信息的操作,再利用获取的 “密钥” 进行下一步操作。

出于某些安全方面的考虑,本书不会介绍第(2)步的具体操作,只会讲解第(1)步的技术问题。

本节的目标明确了一一识别目标缺口的位置,即给定一张滑动验证码的图片,使用图像处理技术识别出缺口的位置。

基本原理

本节中我们会介绍使用 OpenCV 技术识别缺口的方法,输入一张带有缺口的验证码图片,输出标明缺口位置(一般是缺口左侧的横坐标)的图片。这里输入的图片如图 8-10 所示。最后输出的图片如图 8-11 所示。

图8-10 输入的带有缺口的验证码图片 图8-11 输出的标明缺口位置的图片

具体的步骤为:

  1. 对验证码图片进行高斯模糊滤波处理,消除部分噪声干扰;

  2. 利用边缘检测算法,通过调整相应阈值识别出验证码图片中滑块的边缘;

  3. 基于上一步得到的各个边缘轮廓信息,对比面积、位置、周长等特征,筛选出最可能的轮廓得到缺口位置。

准备工作

请确保已经安装好了 python-opencv 库,安装方式如下:

pip3 install python-opencv

如果安装出现问题,可以参考 https://setup.scrape.center/python-opencv。

另外,建议提前准备一张滑动验证码图片,样例图片的下载地址是 https://github.com/Python3WebSpider/CrackSlideCaptcha/blob/cv/captcha.png ,当然也可以从 https://captchal.scrape.center/ 上自行截取,得到的图片如图 8-10 所示。

基础知识

先来了解一些 OpenCV 的基础方法,以便我们更好地搞懂整个原理。

高斯滤波

高斯滤波用来去除图片中的一些噪声,减少噪声干扰,其实就是把一张图片模糊化,为下一步的边缘检测做好铺垫。

OpenCV 提供了一个用于实现高斯模糊的方法,叫作 GaussianBlur,其声明如下:

defGaussianBlur(src,ksizesigmax,dst=None,sigmaY=None,borderType=None)

其中比较重要的参数如下。

  • src:需要处理的图片。

  • ksize:高斯滤波处理所用的高斯内核大小,需要传入一个元组,包含 x 和 y 两个元素。

  • sigmaX:高斯内核函数在 X 方向上的标准偏差。

  • sigmaY:高斯内核函数在 Y 方向上的标准偏差。若 sigmaY 为 O,就将它设为 sigmaX;若 sigmaXsigmaY 都是 O,就通过 ksize 计算出 sigmaXsigmaY

这里 ksizesigmaX 是必传参数,对于图 8-10, ksize 可以取作(5,5),sigmax 可以取作 0。经过高斯滤波处理后,图片会变模糊,效果如图 8-12 所示。

边缘检测

由于验证码图片里的目标缺口通常具有比较明显的边缘,所以借助一些边缘检测算法,再加上调整值是可以找出缺口位置的。自前应用比较广泛的边缘检测算法是 Canny,这是 JohnF.Canny 于 1986 年开发出来的一个多级边缘检测算法,效果很不错。OpenCV 也实现了算法,方法名就叫 Canny,其声明如下:

def Canny(image, threshold1, threshold2, edges=None, apertureSize=None, L2gradient=None)

其中比较重要的参数如下。

  • image:需要处理的图片。

  • threshold1threshold2:两个值,分别是最小判定临界点和最大判定临界点。

  • apertureSize:用于查找图片渐变的索贝尔内核的大小。

  • L2gradient:用于查找梯度幅度的等式。

通常来说,只需要设置 threshold1threshold2 的值即可,其数值大小需要视具体图片而定,这里可以分别取为 200 和 450。经过边缘检测算法的处理后,会保留下一些比较明显的边缘信息,如图 8-13 所示。

轮廓提取

进行边缘检测处理后,可以看到图片中会保留比较明显的边缘信息,下一步可以利用 OpenCV 技术提取出这些边缘的轮廓,这需要用到 findContours 方法,其声明如下:

def findContours(image,mode,method,contours=None,hierarchy=None,offset=None)

其中比较重要的参数如下。

  • image:需要处理的图片。

  • mode:用于定义轮廓的检索模式,详情见 OpenCV 官方文档中对 RetrievalModes 的介绍。

  • method:用于定义轮廓的近似方法,详情见 OpenCV 官方文档中对 ContourApproximationModes 的介绍。

这里,我们将 mode 设置为 RETR_CCOMP,将 method 设置为 CHAIN_APPROX_SIMPLE,具体的选择标准可以参考 OpenCV 官方文档的介绍,这里不再展开讲解。

外接矩形

提取到边缘轮廓后,可以计算出轮廓的外接矩形,以便我们根据面积和周长等参数判断提取到的轮廓是不是目标缺口的轮廓。计算外接矩形使用的方法是 boundingRect,其声明如下:

def boundingRect(array)

这个方法只有一个参数,就是 array,它可以是一个灰度图或者 2D 点集,这单传入轮廓信息经过对轮廓信息和外接矩形做判断,可以得到类似如图 8-14 所示的效果

轮廓面积

从图 8-14 可以看到,我们已经成功获取了各个轮廓的外接矩形,很明显有些不是我们想要的,我们可以根据面积和周长等筛选缺口所在的位置,于是需要用到计算面积的方法 contourArea,其定义如下:

def contourArea(contour, oriented=None)

其中各参数的介绍如下。

  • contour:轮廓信息。

  • oriented:方向标识符,默认值为 False。若取 True,则该方法会返回一个带符号的面积值,正负取决于轮廓的方向(顺时针还是逆时针)。若取 False,则面积值以绝对值形式返回。

返回值就是轮廓的面积。

轮廓周长

同理,周长也有对应的计算方法,叫作 arcLength,其定义如下:

def arcLength(curve,closed)

其中各参数的介绍如下。

  • curve:轮廓信息。

  • closed:轮廓是否封闭。

返回值就是轮廓的周长。

至此,我们介绍了一些 OpenCV 的内置方法,了解这些方法怎么用可以让我们更透彻地理解之后的具体实现。

缺口识别

现在,我们开始真正地实现缺口识别算法。

首先,定义实现高斯滤波、边缘检测和轮廓提取的 3 个方法:

import cv2

GAUSSIAN_BLUR_KERNEL_SIZE = (5, 5)
GAUSSIAN_BLUR_SIGMA_X = O
CANNY_THRESHOLD1 = 200
CANNY_THRESHOLD2 = 450

def get_gaussian_blur_image(image):
    return cv2.GaussianBlur(image, GAUSSIAN_BLUR_KERNEL_SIZE, GAUSSIAN_BLUR_SIGMA_X)

def get_canny_image(image):
    return cv2.Canny(image, CANNY_THRESHOLD1, CANNY_THRESHOLD2)

def get_contours(image):
    contourS, _ = cv2.findContours(image, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
    return contours

对 3 个方法的介绍如下。

  • get_gaussian_blur_image:传入待处理图片的信息,返回高斯滤波处理后的图片信息,ksize 参数定义为(5,5),sigmaX 参数定义为 0。

  • get_canny_image:传入待处理图片的信息,返回边缘检测处理后的图片信息,threshold1 参数和 threshold2 参数分别定义为 200 和 450。

  • get_contours:传入待处理图片的信息,返回提取得到的轮廓信息,mode 定义为 RETR_CCOMP, method 定义为 CHAIN_APPROX_SIMPLE。

将原始的待识别的验证码图片命名为 captcha.png,接下来分别调用以上方法对此图片做处理:

image_raw = cv2.imread('captcha.png')
image_height,image_width,_ = image_raw.shape
image_gaussian_blur = get_gaussian_blur_image(image_raw)
image_canny = get_canny_image(image_gaussian_blur)
contours = get_contours(image_canny)

我们先读取原始图片,赋值为 image_raw,然后获取其宽高信息。接着调用 get_gaussian_blur_image 方法进行高斯滤波处理,将返回值赋值为 image_gaussian_blur。再将 image_gaussian_blur 传入 get_canny_image 方法进行边缘检测处理,并将返回值赋给 image_canny。最后将 image_canny 传入 get_contours 方法得到各个边缘的轮廓信息,将返回值赋值为 contours

得到各个轮廓信息后,便需要根据这些轮廓的外接矩形的面积和周长筛选我们想要的结果了。第一步需要确定怎么筛选,例如我们可以给面积设定一个范围,给周长设定一个范围,另外给缺口位置也设定一个范围,经过实际测量可以得出目标缺口的外接矩形的高度大约是验证码高度的 0.25 倍,宽度大约是验证码宽度的 0.15 倍。所以在允许误差是 20% 的情况下,可以根据验证码的宽高信息大约计算出外接矩形的面积和周长的取值范围。同时,缺口位置(缺口左侧)有一个最小偏移量和一个最大偏移量,这里的最小偏移量是验证码宽度的 0.2 倍,最大偏移量是验证码宽度的 0.85 倍。将这些内容综合起来,我们可以定义 3 个值方法:

def get_contour_area_threshold(image_width,image_height):
    contour_area_min = (image_width * 0.15) * (image_height * 0.25) * 0.8
    contour_area_max = (image_width * 0.15) * (image_height * 0.25) * 1.2
    return contour_area_min, contour_area_max

def get_arc_length_threshold(image_width, image_height):
    arc_length_min = ((image_width * 0.15) + (image_height * 0.25)) * 2 * 0.8
    arc_length_max = ((image_width * 0.15) + (image_height * 0.25)) * 2 * 1.2
    return arc_length_min,arc_length_max

def get_offset_threshold(image_width):
    offset_min = 0.2 * image_width
    offset_max = 0.85 * image_width
    return offset_min, offset_max

对这 3 个方法的介绍如下。

  • get_contour_area_threshold:定义目标轮廓的面积下限和面积上限,分别为 contour_area_mincontour_area_max

  • get_arc_length_threshold:定义目标轮廓的周长下限和周长上限,分别为 arc_length_minarc_length_max

  • get_offset_threshold:定义缺口位置的偏移量下限和偏移量上限,分别为 offset_minoffset_max

定义完方法,只需要遍历各个轮廓信息,根据限定条件进行筛选,即可得出目标轮廓的信息,实现如下:

contour_area_min,contour_area_max = get_contour_area_threshold(image_width, image_height)
arc_length_min, arc_length_max = get_arc_length_threshold(image_width, image_height)
offset_min,offset_max = get_offset_threshold(image_width)
offset=None
for contour in contours:
    x, y, w, h = cv2.boundingRect(contour)
    if contour_area_min < cv2.contourArea(contour) < contour_area_max and arc_length_min < cv2.arcLength(contour, True) < arc_length_max and offset_min < x < offset_max:
        cv2.rectangle(image_raw, (x, y), (x+w, y+h), (0,0,255),2) offset = x
cv2.imwrite('image_label.png', image_raw)
print('offset', offset)

这里我们首先调用 get_contour_area_thresholdget_arc_length_thresholdget_offset_threshold 方法获取3个判断阈值,然后遍历 contours 并根据这些阈值进行筛选,最终得到的 x 值就是目标缺口位置的偏移量,将其赋给 offset 变量并打印出来。与此同时,我们调用 rectangle 方法对目标缺口的外接矩形做了标注,将其保存为 image_label.png 图片。

代码的运行结果如下: offset 163

同时输出的 image_label.png 文件如图 8-15 所示。

图8-15 输出的 image_label.png 文件

这样我们就成功提取出目标缺口的位置了,本节的问题得以解决。

总结

本节中我们介绍了利用 OpenCV 技术识别滑动验证码缺口的方法,其中涉及一些关键的图像处理和识别技术——高斯滤波、边缘检测、轮廓提取等算法。我们也可以举一反三,将这些基本的技术应用到其它类型的工作中,也会很有帮助。

本节代码见 https://github.com/Python3WebSpide/CrackSlideCaptcha/tree/cv 注意这里是 cv 分支。