写在前面数据特点获取数据识别思路
按顺序抠出F4定位f4计算相似度
打标签(~~偷懒~~ )训练孪生网络使用模型 识别结果改进点 写在前面
近日某众打码平台被跑路的消息一出,脚本圈中一片哗然(我并不是脚本圈的,只是喜欢看群里人吹逼而已 ),仿佛再也听不到那句熟悉的广告语了。这也预示着,第三方打码平台不靠谱了。但打码功能有时候又必不可少,这时候怎么办呢?当然是自己自己动手丰衣足食啦!最近工作不是很忙,准备撸一个用Python识别验证码的系列文章,该系列计划囊括各种时下比较流行的验证码形式,如滑块、四则运算、点选、手势、空间推理、谷歌等。已经跑通了的所有代码都放在了我的知识星球上,需要的话请自取。话不多说,开撸!
数美的图标点选和其他的图标点选差不多,要按顺序点击。
正常人都知道这些数据肯定是要写爬虫来抓的(如果你单身至今,当我没说 )。数美对于反爬这块还算良心,稍微分析下请求就会发现有些参数看似加密实则写死,所以构造下请求头和参数就能轻松获取到验证码图片的url。
一个验证码是由两张图组成的,一个是后缀是_bg.jpg的背景图,一个是后缀是_fg.png的图标图。
首先想想看,要解决哪些问题,才能实现按顺序点击:
从图标图中按顺序(按顺序点击的依据)抠出4个图标,我愿称之为F4在背景图中定位已经被旋转缩放后的4个图标,并抠出来,我愿称之为f4计算出F4们与f4们之间的相似度(搞基配对 ) 按顺序抠出F4
稍微懂点CV的老铁应该知道,这种图好扣的很。转成灰度图,OTSU阈值分割,膨胀一下就能得到比较好的F4们的连通区域了。
# image是图标图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了。
# F4roi_image = []i = 0while 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
抠图效果
如果有老铁看过我之前写那篇识别数美拼图滑块的流水账,估计会想着继续用模板匹配去背景图上找图标的位置。但如果你头铁试了一下的话,会发现,找的位置一点都!不!准!因为模板匹配没有旋转不变性和缩放不变性。所谓不变性就是变了等于没变。也就是说,模板匹配不适于去匹配已经被旋转和缩放后的目标,即使它们从人眼看来是一个东西。
本来想用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是个列表,是用来抠f4们直接能用的坐标pos = yolo_detector.detect(bg_img)# 存放f4的列表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 cv2import numpy as npimport osimport timedef 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 resdef 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 imgdef 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 roibgs = 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)# F4们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# 把F4们贴到图的最下方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.构造一个网络结构做到端到端识别。