Python图像处理识别滑动验证码缺口

Python图像处理识别滑动验证码缺口

参考文章

OpenCV_Python模板匹配

网易易盾

模板匹配

近期在破解网易易盾的滑动验证码,涉及到缺口识别的问题时,由于没有经验,上网查了一些资料,发现一个比较火的方式是利用opencv的模板匹配。

模板匹配是一种在较大图像中搜索和查找模板图像位置的方法。OpenCV提供了一个函数cv2.matchTemplate()。它只是在输入图像上滑动模板图像(如在2D卷积中),并比较模板图像下的输入图像的模板和补丁。在OpenCV中实现了几种比较方法。它返回一个灰度图像,其中每个像素表示该像素的邻域与模板匹配的程度。

假设输入图像的大小(W*H)且模板图像的大小(w*h),则输出图像的大小为(W-w + 1,H-h + 1)。获得结果后,可以使用cv2.minMaxLoc()函数查找最大/最小值的位置。将其作为矩形的左上角,并将(w,h)作为矩形的宽度和高度。那个矩形是你的模板区域匹配后得到的区域。

  • 函数使用方式:
1
2
3
4
5
6
import cv2
cv2.matchTemplate(img_big,img_temp,cv2.method)

img_big:在该图上查找图像
img_temp:待查找的图像,模板图像
method: 模板匹配的方法
  • 参数method
method introduce
cv2.TM_SQDIFF 平方差匹配法 该方法采用平方差来进行匹配;最好的匹配值为0;匹配越差,匹配值越大
cv2.TM_CCORR 相关匹配法 该方法采用乘法操作;数值越大表明匹配程度越好。
cv2.TM_CCOEFF 相关系数匹配法 1表示完美的匹配;-1表示最差的匹配。
cv2.TM_SQDIFF_NORMED 归一化平方差匹配法
cv2.TM_CCORR_NORMED 归一化相关匹配法
cv2.TM_CCOEFF_NORMED 归一化相关系数匹配法

TM_SQDIFF,TM_SQDIFF_NORMED匹配数值越低表示匹配效果越好,其它四种反之。

假设我们现有易盾验证码的背景图片和滑块图片,尝试去调用此方法来解决问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import cv2
import numpy as np
from PIL import Image

def template_match():
# 待检测的目标(也叫模板)
target = cv2.imread('target.png', 0) # 1-彩色, 0-黑白
# 读取背景图像
background = cv2.imread('background.jpg', 0) # 彩色

# 得到目标图片的高和宽(如果是彩色,shape返回3个值)
h, w = target.shape
print(h, w)

# 模板匹配操作
res = cv2.matchTemplate(background, target, cv2.TM_CCOEFF_NORMED)
# 这里可以打印看一下模板匹配返回的结果
print(res)

# 得到最大值和最小值的位置
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
top_left = max_loc # 左上角的位置
print(top_left)
bottom_right = (top_left[0]+w, top_left[1]+h) # 右下角的位置
cv2.rectangle(background, top_left, bottom_right, 255, 2)
cv2.imshow('img', background)
cv2.waitKey(0)

这里最后是在背景图上做了矩形然后显示出来,这样我们一下就可以看出找的对不对。

这里因为用的是cv2.TM_CCOEFF_NORMED,匹配的越好,匹配值越大,所以我们以最大值的位置作为矩阵的左上角,如果用cv2.TM_SQDIFF就要选最小值的位置作为矩阵的左上角。

具体在检测的时候,method参数到底使用哪一种效果更好,要实际测试,在我遇到过的一些滑动验证码,貌似都是TM_CCOEFF_NORMED参数效果好一些。

这里是直接使用黑白图片进行检测,效果还可以,但是针对具体不同的图片,有可能彩色的效果更好一些。

后来有遇到过另一个网站的图片,发现用上面的方法无法正常检测到滑块的位置,检测结果是这样的:

后来从大佬那里学习到,在模板匹配之前,将图片进行一个边缘检测处理,能够提高检测的效果

edges = cv2.Canny( image, threshold1, threshold2[, apertureSize[, L2gradient]])

edges 为计算得到的边缘图像。

  • image 为 8 位输入图像。
  • threshold1 表示处理过程中的第一个阈值。
  • threshold2 表示处理过程中的第二个阈值。
  • apertureSize 表示 Sobel 算子的孔径大小。
  • L2gradient 为计算图像梯度幅度(gradient magnitude)的标识。其默认值为 False。如果为 True,则使用更精确的 L2 范数进行计算(即两个方向的导数的平方和再开方),否则使用 L1 范数(直接将两个方向导数的绝对值相加)。
1
2
3
# 边缘检测
target = cv2.Canny(target, 100, 200)
background = cv2.Canny(background, 100, 200)

两个阈值的设置要根据情况而定。

检测效果:

此外,因为网易易盾的滑块图片是有一个纯白色或者纯黑色底色的背景的,我有尝试过转换过背景底色再去匹配,但是没啥没效果,底色转换代码:

1
2
3
4
# 白色底变为黑色底
img_template[np.where((img_template == [255, 255,255]).all(axis=2))] = [0, 0, 0]
# 黑色底变为白色底
img_template[np.where((img_template == [0, 0, 0]).all(axis=2))] = [255, 255, 255]
传统算法

因为网易易盾的验证码图片的内部色彩变化较大,而不像极验绿色、蓝色灯底色,图一张易盾的背景的色彩变化就很大,很难直接利用像素变化定位到缺口位置,我也写了一个尝试性的算法,但是并没有成功,边界如果定的宽松些,就会返回多个满足条件的位置,如果将边界定的严格些,又会无返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def judge_pixel(gray: Image, col: int, row: int, value: int):
# 不可能在第一列
if not col:
return False
start = 0 if col < 30 else (col-30)
# 直接拿一整块区域来对比不太合适,考虑到图的像素会随着行或列进行变化,尝试计算满足的比例
count_i = 0
count_j = 0
for i in range(start, col):
for j in range(row, row + 30):
# 相差较大,不满足
if abs(gray.getpixel((i, j)) - value) < 15:
count_i += 1
if count_i/((col-start) * 30) > 0.5:
return True
return False


# 根据像素变化的传统算法
def conventional_algorithm():
image = Image.open(r'j_127.jpg')
gray = image.convert("L")
# gray.show()
width, height = gray.size
distance_list = list()
# 滑块的大小大概是40*40的
for row in range(height-40):
for col in range(width-40):
pre_value = gray.getpixel((col, row))
next_value = gray.getpixel((col+1, row))
# 由黑变白并判断pre_value这一点
# 与其左下方一块区域的像素值相差是否较大
if next_value - pre_value > 15 and judge_pixel(gray, col, row, pre_value):
count_i = 0
for i in range(row+1, row+40):
for j in range(col + 1, col + 40):
if gray.getpixel((j, i)) - pre_value > 10:
count_i += 1
if count_i > 1000:
distance_list.append((col, row))
# 由白变黑
if pre_value - next_value > 15 and judge_pixel(gray, col, row, pre_value):
count_i = 0
for i in range(row + 1, row + 40):
for j in range(col + 1, col + 40):
if pre_value - gray.getpixel((j, i)) > 10:
count_i += 1
if count_i > 1000:
distance_list.append((col, row))
return distance_list

因为这里直接利用图像处理的方法还无法破解极验滑块的缺口位置,我后面打算利用深度学习的目标检测方法来实现,感兴趣的可以关注后面的文章。

极验

图片还原

对于极验需要先介绍下极验滑动验证码的图片还原的方式。

直接从极验服务器获取的背景图是乱的,根本看不出来缺口在哪里,大概是这样的:

想要将其还原为原来的样子,首先我们要知道完整的验证码图片由52个10*58像素的小图片组成,共2行26列,其次只要获取使用极验服务的相关网站上的css坐标,类似这样的:

然后按照这里的坐标顺序,从乱序的图中将一个个的小图片扣出来,粘贴到一个空白的图片中重组就能复原,下面的我实现的Python代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 从css中获取
offset_list = [
[157, 58], [145, 58], [265, 58], [277, 58], [181, 58], [169, 58], [241, 58], [253, 58], [109, 58], [97, 58], [289, 58], [301, 58], [85, 58], [73, 58], [25, 58],
[37, 58], [13, 58], [1, 58], [121, 58], [133, 58],
[61, 58], [49, 58], [217, 58], [229, 58],
[205, 58], [193, 58], [145, 0], [157, 0], [277, 0], [265, 0], [169, 0], [181, 0], [253, 0], [241, 0],
[97, 0], [109, 0], [301, 0], [289, 0], [73, 0], [85, 0],
[37, 0], [25, 0], [1, 0], [13, 0], [133, 0], [121, 0], [49, 0], [61, 0],[229, 0], [217, 0], [193, 0], [205, 0]
]

def convert_css_to_offset(off):
# 返回图片左上角和右下角的点坐标
# (left, upper)o ------ o
# | |
# o ------ o (right, lower)
return off[0], off[1], off[0]+10, off[1]+58

def convert_index_to_offset(index):
if index < 26:
return index*10, 0
else:
i = index - 26
return i*10, 58

def recombine_captcha(self):
# 完整的验证码图片由52个10*58像素的小图片组成,共2行26列
captcha = Image.new('RGB', (10*26, 58*2)) # 空白图片
img = Image.open('full_bg.jpg')
for i, off in enumerate(offset_list):
# 根据css background-position
# 按顺序获取每张小图在错乱的验证码图片中的坐标
box = convert_css_to_offset(off)
region = img.crop(box) # 抠图
offset = self.convert_index_to_offset(i)
# 根据当前坐标将小图粘贴到空白图片
captcha.paste(region, offset)
captcha.save('full_bg_fact.jpg')

这里的css左边一般各个使用极验服务的网站不一样,不是通用的,根据你要破解的网站寻找其css坐标即可,而且据我观察一般是固定不变的,若动态变化,只要每次将获取到的html代码中的这部分解析一下就可以了。

模板匹配

按照我实际实践的顺序,我是先破解的极验验证码,用的是传统算法(下面会介绍),没有用过模板匹配,然后又去破解的网易易盾的,但是利用模板匹配和传统算法都没有解决。后来我想了下是不是因为网易易盾的背景图片只有带缺口的,没有完整的原图,所以匹配不上,正好极验的滑动验证码是可以获取原图的,所以我对极验的也尝试了一下,但是发现依然不行,这里就不详细展示叙述了。

传统算法
  • 方法一:仅根据带缺口图片的像素变化

    因为之前做过一个很简单的滑动验证码的识别,当时那个网站的图片色彩风格很单一,仅根据像素变化就找出来了缺口位置,观察极验这里感觉应该也可以用类似的方式实现,所以就尝试了一下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    def get_picture_gap(name, path):
    image = Image.open(path+name)
    gray = image.convert('L')
    # gray.save('gray.jpg')
    width, height = gray.size
    for col in range(width - 55):
    for row in range(height - 55):
    if gray.getpixel((col, row)) < 80:
    count_i = 0
    count_j = 0
    for i in range(row + 1, row + 50):
    if gray.getpixel((col, i)) < 80:
    count_i += 1
    for j in range(col + 1, col + 50):
    if gray.getpixel((j, row)) < 80:
    count_j += 1
    if count_i > 20 and count_j>20:
    return col

    算法的大概思路在之前的海关进出口破解的那里已经讲过了,不再详细说了,然后我随机测试了五张图片(我自己利用上面的方式还原后的图片):

    只有第三张图片定位错误,其他四张都准确定位了;因为我这里没有实际破解需求,只是兴趣使然,所以没有再去优化算法,应该还有进一步优化的空间。

  • 方法二:对比带缺口的和不带缺口的图片

    对于极验的滑动验证码比较特殊的一点是可以获取完整地图片和待缺口的图片,由此就产生一种大家都很容易想到的方法,逐列扫描对比这两个图片,发现连续较多行都有一定的像素差,一般就是缺口的位置,我这里简单写了一个算法,同样是测试了上面的五张图片,更巧的是,也是第三张图片没有获取到缺口位置,因为这个算法是我凭感觉直接写的,没有去认真观察图片像素变化,应该也是存在优化空间的,我这里仅仅判断这一列若干行,其实可以再继续判断后续的列和行,只不过会增加时间复杂度。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    def get_picture_gap1(self, name1, name2, path1, path2):
    image1 = Image.open(path1 + name1)
    image2 = Image.open(path2 + name2)
    gray1 = image1.convert('L')
    gray2 = image2.convert('L')
    # gray.save('gray.jpg')
    width, height = gray1.size
    for col in range(width - 55):
    count_i = 0
    for row in range(height - 55):
    if abs(gray1.getpixel((col, row)) - gray2.getpixel((col, row))) > 20:
    count_i += 1
    if count_i > 25:
    return col

至于这里识别出来的缺口位置是不是实际滑动的距离,他们之间的关系,不属于这里的研究范围……做出来这一步,再去计算实际滑动距离基本就没什么困难了。