写在前面
近日某众打码平台被跑路的消息一出,脚本圈中一片哗然(我并不是脚本圈的,只是喜欢看群里人吹逼而已 ),仿佛再也听不到那句熟悉的广告语了。这也预示着,第三方打码平台不靠谱了。但打码功能有时候又必不可少,这时候怎么办呢?当然是自己自己动手丰衣足食啦!最近工作不是很忙,准备撸一个用Python识别验证码的系列文章,该系列计划囊括各种时下比较流行的验证码形式,如滑块、四则运算、点选、手势、空间推理、谷歌等。已经跑通了的所有代码都放在了我的知识星球上,需要的话请自取。话不多说,开撸!
数据特点
数美的图标点选和其他的图标点选差不多,要按顺序点击。
获取数据
正常人都知道这些数据肯定是要写爬虫来抓的(如果你单身至今,当我没说 )。数美对于反爬这块还算良心,稍微分析下请求就会发现有些参数看似加密实则写死,所以构造下请求头和参数就能轻松获取到验证码图片的url。
识别思路
一个验证码是由两张图组成的,一个是后缀是_bg.jpg的背景图,一个是后缀是_fg.png的图标图。
首先想想看,要解决哪些问题,才能实现按顺序点击:
- 从图标图中按顺序(按顺序点击的依据)抠出4个图标,我愿称之为F4
- 在背景图中定位已经被旋转缩放后的4个图标,并抠出来,我愿称之为f4
- 计算出F4们与f4们之间的相似度(
搞基配对 )
按顺序抠出F4
稍微懂点CV的老铁应该知道,这种图好扣的很。转成灰度图,OTSU阈值分割,膨胀一下就能得到比较好的F4们的连通区域了。
gray_img = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, threshold_img = cv2.threshold(gray_img, 100, 255, cv2.THRESH_OTSU)
kernel = np.ones([3, 3], np.uint8)
dialte_img = cv2.dilate(threshold_img, kernel, 2)
结果就是酱紫。 有了上面的结果后,按顺序抠图就简单了。从左到右遍历下康康纵向的像素和是不是0就OJBK了。
roi_image = []
i = 0
while i < image.shape[1]:
if(np.sum(dialte_img[:,i]) > 0):
start_col = i
while np.sum(dialte_img[:,i]) > 0:
i += 1
end_col = i
roi_image.append(image[:,start_col:end_col])
else:
i += 1
抠图效果
定位f4
如果有老铁看过我之前写那篇识别数美拼图滑块的流水账,估计会想着继续用模板匹配去背景图上找图标的位置。但如果你头铁试了一下的话,会发现,找的位置一点都!不!准!因为模板匹配没有旋转不变性和缩放不变性。所谓不变性就是变了等于没变。也就是说,模板匹配不适于去匹配已经被旋转和缩放后的目标,即使它们从人眼看来是一个东西。
本来想用SIFT来做定位的,但SIFT申请了专利,我opencv降版本都白嫖不了…
后来想想,算了,上YoloV5吧,反正YoloV5n对显卡要求不高,我这4G小霸王训练个定位图标的模型还是可以的。说干就干。
经过半个小时的标注,标注了90多张图。然后用YoloV5n pytorch版训练了一个13.9M的模型,mAP高达98!!!不得不说,Yolo牛逼!(如果各位看官对yolov5不熟,可以参考官方github)
有了模型之后,就需要改写一下官方提供的predict.py,因为predict.py太臃肿,而且所有的预测结果是画在图上的,就像酱紫。 但我们想要的结果是f4们的位置。所以像可视化啊、dump日志啊什么杂七杂八的全可以删掉。需要注意的是:predict.py里面有两种坐标表示方式,一种是xywh ,还有一种是xyxy 。xywh 是目标矩形框的左上角的归一化后的坐标和矩形的归一化后的宽高。xyxy 是目标矩形框的左上角坐标和右下角坐标,并且坐标没有被归一化。
至于用哪种方式表示位置的话,见仁见智了。反正都能互相转换。
我这边为了opencv好抠图,就用的xyxy ,然后稍微封装了一下。
pos = yolo_detector.detect(bg_img)
bg_roi_imgs = []
for p in pos:
bg_roi_imgs.append(bg_img[p[0]:p[1], p[2]:p[3]])
计算相似度
F4,f4都有了,那就差他们搞基配对了。搞基的逻辑其实也挺简单,大概酱紫。
rects = []
for i in range(len(puzzle_images)):
best_score = 0
best_rect = None
for j in range(len(bg_roi_imgs)):
score = 计算相似度(puzzle_images[i], bg_roi_imgs[j])
if dis > best_score:
best_score = dis
best_rect = [pos[j][2], pos[j][0], pos[j][3], pos[j][1]]
rects.append(best_rect)
那么相似度怎么算?一开始我想这要不算个感知哈希?结果发现不太行。要不算个HOG特征然后算余弦距离?结果他喵的比感知哈希还拉跨…
算了,用孪生网络一把梭,不就是打标签嘛,我打还不行吗…
打标签(偷懒 )
至于打标签嘛,学过ML或DL的都知道,数据用业务场景的真实数据肯定是最好的,因为数据分布最为相似。但我很懒…我就投机取巧的做了图像增强。思路就是随便找了几张网图,然后把种子随机旋转,缩放得到贴图。再把贴图随机找个网图贴上去然后抠出来。
import cv2
import numpy as np
import os
import time
def random_rotate(img):
rows, cols, channels = img.shape
angle = [0, 20, 45, 60, -20, -45]
aa = np.random.randint(len(angle))
rotate = cv2.getRotationMatrix2D((rows * 0.5, cols * 0.5), angle[aa], 1)
res = cv2.warpAffine(img, rotate, (cols, rows))
return res
def random_resize(img):
scale = [1.0, 1.2, 1.3,1.5, 1.7 ,2, 2.5]
x = np.random.randint(len(scale))
img = cv2.resize(img, (0, 0), fx=scale[x], fy=scale[x])
return img
def gen_random_img(bg, fg):
fg_ = fg.copy()
fg_ = random_rotate(fg_)
fg_ = random_resize(fg_)
fg_r, fg_c = fg_.shape[0], fg_.shape[1]
x = np.random.randint(bg.shape[1]-fg_c)
y = np.random.randint(bg.shape[0]-fg_r)
roi = bg[y:y+fg_r, x:x+fg_c].copy()
for i in range(roi.shape[0]):
for j in range(roi.shape[1]):
if np.sum(fg_[i,j,:]) > 30:
roi[i, j, 0] = fg_[i, j, 0]
roi[i, j, 1] = fg_[i, j, 1]
roi[i, j, 2] = fg_[i, j, 2]
return roi
bgs = os.listdir('random_bg')
for fg_path in os.listdir('./images_background/'):
filename = os.listdir(os.path.join('./images_background/', fg_path))[0]
for i in range(100):
bg_i = np.random.randint(len(bgs))
bg = cv2.imread('random_bg/'+bgs[bg_i])
fg = cv2.imread(os.path.join(os.path.join('./images_background/',fg_path),filename))
roi = gen_random_img(bg, fg)
cv2.imwrite(os.path.join(os.path.join('./images_background/',fg_path),str(round(time.time()*1000)))+'.jpg', roi)
然后就有了大概酱紫的数据集
训练孪生网络
pytorch版本的孪生网络github上有很多,选一个看得最顺眼的就行。我选的是backbone是VGG16,损失函数是三元组损失的。大概训练了7个epoch后精度就还行了(后来实验证明有点过拟合了 )。
使用模型
有了模型之后,直接调用模型预测就好,反正给的结果是个概率值。概率值越高,说明越像。
rects = []
for i in range(len(puzzle_images)):
best_score = 0
best_rect = None
for j in range(len(bg_roi_imgs)):
score = siamese_model.detect_image(puzzle_images[i], bg_roi_imgs[j])
if dis > best_score:
best_score = dis
best_rect = [pos[j][2], pos[j][0], pos[j][3], pos[j][1]]
rects.append(best_rect)
这个时候rects 里面就会有F4们在背景图中的位置了。
识别结果
为了方便查看识别结果,我把F4和背景图都贴到了一张图上,然后框框上的数字就是依次点击的顺序。
bg_img = cv2.imread(bg_path)
fg_img = cv2.imread(fg_path)
back = np.zeros([340, 600, 3], dtype=np.uint8)
rects = get_result(bg_img, fg_img, model, detector)
for i, rect in enumerate(rects):
bg_img = cv2.rectangle(bg_img, (rect[0], rect[1]), (rect[2], rect[3]), (0, 255, 255), 3)
bg_img = cv2.putText(bg_img, str(i + 1), (rect[0], rect[1]), cv2.FONT_HERSHEY_SIMPLEX, 1.1, (0, 255, 255), 2)
back[:bg_img.shape[0], :bg_img.shape[1], :] = bg_img
for i in range(fg_img.shape[0]):
for j in range(fg_img.shape[1]):
if fg_img[i, j, 0] != 0 and fg_img[i, j, 1] != 0 and fg_img[i, j, 2] != 0:
back[i+bg_img.shape[0], j, 0] = fg_img[i, j, 0]
back[i+bg_img.shape[0], j, 1] = fg_img[i, j, 1]
back[i+bg_img.shape[0], j, 2] = fg_img[i, j, 2]
测试了下,依次点击的正确率大概65%的样子。
改进点
1.抠F4的时候我没考虑那种隔得很开的图标,比如下图中的AI 会抠成A 和I 。可以考虑直接用定位f4的yolo模型来抠图,效果肯定比这个好。 2.因为懒,标注的数据太少,孪生网络的数据我只标注了50多种图标,实际上测试时图标种类远不止50多种,导致模型容易过拟合。比如下图中1 和2 的图标中都有类似s 形的缝隙。模型就算错了。如果不懒,效果不会差。
3.构造一个网络结构做到端到端识别。
|