我佛了!用KNN实现验证码识别,又 Get 到一招!
作者| 李秋键
责编| Carol
出品| AI科技大本营(ID:rgznai100)
头图 | CSDN付费下载自视觉中国
验证码使我们生活中最为常见的防治爬虫和机器人登录攻击的手段,一般的验证码主要由数字和字母组成,故我们可以设想:我们是否可以根据文本识别训练模型进行识别验证码呢?当然可以,今天我们就将利用 KNN 实现验证码的识别。
关于 KNN 基本常识如下:
KNN 算法我们主要要考虑三个重要的要素,对于固定的训练集,只要这三点确定了,算法的预测方式也就决定了。 这三个最终的要素是 k 值的选取,距离度量的方式和分类决策规则。
对于 k 值的选择,没有一个固定的经验,一般根据样本的分布,选择一个较小的值,可以通过交叉验证选择一个合适的 k 值。
选择较小的 k 值,就相当于用较小的领域中的训练实例进行预测,训练误差会减小,只有与输入实例较近或相似的训练实例才会对预测结果起作用,与此同时带来的问题是泛化误差会增大,换句话说, K 值的减小就意味着整体模型变得复杂,容易发生过拟合;
选择较大的 k 值,就相当于用较大领域中的训练实例进行预测,其优点是可以减少泛化误差,但缺点是训练误差会增大。 这时候,与输入实例较远(不相似的)训练实例也会对预测器作用,使预测发生错误,且 K 值的增大就意味着整体的模型变得简单。
一个极端是 k 等于样本数 m ,则完全没有分类,此时无论输入实例是什么,都只是简单的预测它属于在训练实例中最多的类,模型过于简单。
效果图如下:
实验前的准备
首先我们使用的 python 版本是 3 .6.5 所用到的库有 cv2 库用来图像处理;
N umpy 库用来矩阵运算;
训练的数据集如下所示:
训练模型的搭建
1 、获取切割字符轮廓:
我们定义ws和valid_contours数组,用来存放图片宽度和训练数据集中的图片。如果分割错误的话需要重新分割。主要根据字符数量判断是否切割错误,如果切割出有4个字符。说明没啥问题:
代码如下:
#定义函数get_rect_box,目的在于获得切割图片字符位置和宽度 def get_rect_box(contours): print("获取字符轮廓。。。") #定义ws和valid_contours数组,用来存放图片宽度和训练数据集中的图片。如果分割错误的话需要重新分割 ws = [] valid_contours = [] for contour in contours: #画矩形用来框住单个字符,x,y,w,h四个参数分别是该框子的x,y坐标和长宽。因 x, y, w, h = cv2.boundingRect(contour) if w = w_min * 2: box_left = np.int0([[x,y], [x+w/3,y], [x+w/3,y+h], [x,y+h]]) box_mid = np.int0([[x+w/3,y], [x+w*2/3,y], [x+w*2/3,y+h], [x+w/3,y+h]]) box_right = np.int0([[x+w*2/3,y], [x+w,y], [x+w,y+h], [x+w*2/3,y+h]]) result.append(box_left) result.append(box_mid) result.append(box_right) elif w_max 4: for contour in valid_contours: x, y, w, h = cv2.boundingRect(contour) box = np.int0([[x,y], [x+w,y], [x+w,y+h], [x,y+h]]) result.append(box) result = sorted(result, key=lambda x: x[0][0]) return result
2、数据集图像处理:
在读取数据集后,我们需要对图片数据集进行二值化和降噪处理,以获得更为合适的训练数据。
其中代码如下 :
def process_im(im): rows, cols, ch = im.shape #转为灰度图 im_gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY) #二值化,就是黑白图。字符变成白色的,背景为黑色 ret, im_inv = cv2.threshold(im_gray,127,255,cv2.THRESH_BINARY_INV) #应用高斯模糊对图片进行降噪。高斯模糊的本质是用高斯核和图像做卷积。就是去除一些斑斑点点的。因为二值化难免不够完美,去燥使得二值化结果更好 kernel = 1/16*np.array([[1,2,1], [2,4,2], [1,2,1]]) im_blur = cv2.filter2D(im_inv,-1,kernel) #再进行一次二值化。 ret, im_res = cv2.threshold(im_blur,127,255,cv2.THRESH_BINARY) return im_res
3 、切割字符:
在得到字符位置后,我们对图片进行切割和保存
部分代码如下:
#借助第一个函数获得待切割位置和长宽后就可以切割了 def split_code(filepath): #获取图片名 filename = filepath.split("/")[-1] #图片名即为标签 filename_ts = filename.split(".")[0] im = cv2.imread(filepath) im_res = process_im(im) im2, contours, hierarchy = cv2.findContours(im_res, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) #这里就是用的第一个函数,获得待切割位置和长宽 boxes = get_rect_box(contours) #如果没有区分出四个字符,就不切割这个图片 if len(boxes) != 4: print(filepath) # 如果区分出了四个字符,说明切割正确,就可以切割这个图片。将切割后的图片保存在char文件夹下 for box in boxes: cv2.drawContours(im, [box], 0, (0,0,255),2) roi = im_res[box[0][1]:box[3][1], box[0][0]:box[1][0]] roistd = cv2.resize(roi, (30, 30)) timestamp = int(time.time() * 1e6) filename = "{}.jpg".format(timestamp) filepath = os.path.join("char", filename) cv2.imwrite(filepath, roistd) #cv2.imshow("image", im) #cv2.waitKey(0) #cv2.destroyAllWindows() # split all captacha codes in training set #调用上面的split_code进行切割即可。 def split_all(): files = os.listdir(TRAIN_DIR) for filename in files: filename_ts = filename.split(".")[0] patt = "label/{}_*".format(filename_ts) saved_chars = glob.glob(patt) if len(saved_chars) == 4: print("{} done".format(filepath)) continue filepath = os.path.join(TRAIN_DIR, filename) split_code(filepath)
4 、标注字符:
通过已经标注好的数据集字符读取标签,然后存储标签,以方便和图片达到对应。字符数据集如下:
代码如下:
#用来标注单个字符图片,在label文件夹下,很明显可以看到_后面的就是标签。比如图片里是数字6,_后面就是6 def label_data(): files = os.listdir("char") for filename in files: filename_ts = filename.split(".")[0] patt = "label/{}_*".format(filename_ts) saved_num = len(glob.glob(patt)) if saved_num == 1: print("{} done".format(patt)) continue filepath = os.path.join("char", filename) im = cv2.imread(filepath) cv2.imshow("image", im) key = cv2.waitKey(0) if key == 27: sys.exit() if key == 13: continue char = chr(key) filename_ts = filename.split(".")[0] outfile = "{}_{}.jpg".format(filename_ts, char) outpath = os.path.join("label", outfile) cv2.imwrite(outpath, im) #和标注字符图反过来,我们需要让电脑知道这个字符叫啥名字,即让电脑知道_后面的就是他字符的名字 def analyze_label(): print("识别数据标签中。。。") files = os.listdir("label") label_count = {} for filename in files: label = filename.split(".")[0].split("_")[1] label_count.setdefault(label, 0) label_count[label] += 1 print(label_count)
5 、KNN模型训练:
KNN算法我们直接使用 OpenCV 自带的 KNN 函数即可。通过读取数据集和标签,加载模型训练即可。代码如下:
#训练模型,用的是k相邻算法 def get_code(im): #将读取图片和标签 print("读取数据集和标签中。。。。") [samples, label_ids, id_label_map] = load_data() #k相邻算法 print("初始化中...") model = cv2.ml.KNearest_create() #开始训练 print("训练模型中,请等待!") model.train(samples, cv2.ml.ROW_SAMPLE, label_ids) #处理图片。即二值化和降噪 im_res = process_im(im) #提取轮廓 im2, contours, hierarchy = cv2.findContours(im_res, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) #获取各切割区域位置和长宽 boxes = get_rect_box(contours) #判断有没有识别出4个字符,如果没有识别出来,就不往下运行,直接结束了 if len(boxes) != 4: print("cannot get code") result = [] #如果正确分割出了4个字符,下面调用训练好的模型进行识别。 for box in boxes: #获取字符长宽 roi = im_res[box[0][1]:box[3][1], box[0][0]:box[1][0]] #重新设长宽。 roistd = cv2.resize(roi, (30, 30)) #将图片转成像素矩阵 sample = roistd.reshape((1, 900)).astype(np.float32) #调用训练好的模型识别 ret, results, neighbours, distances = model.findNearest(sample, k = 3) #获取对应标签id label_id = int(results[0,0]) #根据id得到识别出的结果 label = id_label_map[label_id] #存放识别结果 result.append(label) return result
模型调用
if __name__ == "__main__": file=os.listdir("test") filepath="test/"+file[4] im = cv2.imread(filepath) preds = get_code(im) preds="识别结果为:"+preds[0]+preds[1]+preds[2]+preds[3] print(preds) canny0 = im img_PIL = Image.fromarray(cv2.cvtColor(canny0, cv2.COLOR_BGR2RGB)) myfont = ImageFont.truetype(r'simfang.ttf', 18) draw = ImageDraw.Draw(img_PIL) draw.text((20, 5), str(preds), font=myfont, fill=(255, 23, 140)) img_OpenCV = cv2.cvtColor(np.asarray(img_PIL), cv2.COLOR_RGB2BGR) cv2.imshow("frame", img_OpenCV) key = cv2.waitKey(0) print(filepath)
到这里,我们整体的程序就搭建完成,下面为我们程序的运行结果:
源码地址:
链接: https://pan.baidu.com/s/1Ir5QNjUZaeTW26T8Gb3txQ
提取码:9eqa
作者简介:
李秋键,CSDN博客专家,CSDN达人课作者。硕士在读于中国矿业大学,开发有taptap竞赛获奖等等。