本项目基于Python爬虫爬取验证码图片,对图片进行去噪、分割,通过KNN算法训练模型,实现验证其准确率。
首先从指定的网页中爬取验证码图片数据,然后对数据进行一个去噪和分割的处理,再将数据集分割为训练集和测试集,运用训练集去拟合并保存模型,最后将保存的模型应用于测试集,对模型进行一个评估。
本项目包括4个模块:数据爬取、去噪与分割、模型训练及保存、准确率验证,下面分别介绍各模块的功能及相关代码。
本部分用resquest库爬取抓取验证码100张,并做好标注。相关代码如下:
from __future__ import unicode_literals
import requests
import time
if __name__ == "__main__":
#获取图片总数设置
number = 100
for num in range(number):
img_url = 'http://run.hbut.edu.cn/Account/GetValidateCode?time=1644928431690'
data = {'timestamp':unicode(long(time.time()*1000))} #获取时间,并且转换成long类型,再进行解码
#print(img_url)
res = requests.get(img_url,params = data)
#一个获取图片资源请求
with open("./download_image/%d.jpg" % num,"wb") as f:
#获取图片资源位置并将图片保存在本地
f.write(res.content)
print("%d.jpg" % num + "获取成功")
其中这部分的代码需要Python2.7的配置,因为long()函数只有在Python2.7中才有,在Python3.8中不存在这个函数。这里将获取的图片总数设置为100张,书中是1200张,由于数据量太大故先用100张进行一个测试。
爬取图片示例如下图所示:
由于后续需要进行分割所以预先对得到的图片名进行一个标注,即对应将图片名标注为其对应的验证码,标注完成如下图所示:
转换成灰度图后对图片进行分割,去掉边框和部分噪声,分成4张图,统计每张图的灰度直方图(自己设置bins),找到第二大对应的像素范围(即某一像素范围内像素数第二多,对应的像素范围,像素最多的应该是白色空白处),取像素范围中位数模式,然后保留(mode±biases)的像素,去除大部分噪声。
import math
import os
import cv2
#去除背景噪声
def del_noise(im_cut):
bins = 16
# 函数返回大于或等于一个给定数字的最小整数
num_gray = math.ceil(256/bins)
# [im_cut]为输入的图像,[0]表示计算直方图的通道,None表示处理整幅图像,[bins]表示这个直方图分成多少份(这里分为了16个直方柱),[0, 256]表示直方图能表示像素值从0到256的像素
hist = cv2.calcHist([im_cut],[0],None,[bins],[0,256])
lists = []
for i in range(len(hist)): #len(hist)都为16
#print hist[i][0]
lists.append(hist[i][0]) #lists里面有16个数
second_max = sorted(lists)[-2] #先从小到大排序再取倒数第二个数
#查找第2多像素(对应的是验证码),最多的是空白处
bins_second_max = lists.index(second_max)
#取像素范围中位数模式,保留(mode±biases)的像素
mode = (bins_second_max + 0.5) * num_gray
for i in range(len(im_cut)): #len(im_cut)都为23,对应图片的宽
for j in range(len(im_cut[0])): #len(im_cut[0])为48,对应图片的长
#print im_cut[i][j]
if im_cut[i][j] < mode-15 or im_cut[i][j] > mode+15:
#不在中位数附近的设为白(255)
im_cut[i][j] = 255
return im_cut #得到去噪后的图片
以数据集中的验证码图片0290.jpg为例:
im_cut = cv2.imread('./img/0290.jpg')
hist = cv2.calcHist([im_cut], [0], None, [16], [0, 256])
# plt.plot(hist)
# plt.show()
print(len(hist))
print(hist)
得到hist的长度是16,hist是一个16行1列的数组,所以hist[i][0]分别对应数组里1列的每个数
分割100张已标注好的图片,得到400张子图片(因为每一张验证码中有4个数字)。相关代码如下:
def cut_image(image,num,img_name):
#image = cv2.imread('./img/8.jpg')
#将BGR格式图片转换成灰度图片
im = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
im_cut_0 = im[4:18, 7:16] #每个数字在图片中占的宽和长
im_cut_1 = im[4:18, 16:25]
im_cut_2 = im[4:18, 25:34]
im_cut_3 = im[4:18, 34:43]
im_cut = [im_cut_0,im_cut_1,im_cut_2,im_cut_3]
for i in range(4):
im_temp = del_noise(im_cut[i]) #对分割后的每个数字图片进行去噪处理
#将图片分割为4个
cv2.imwrite('./img_train_cut/'+str(num)+'_'+str(i)+'_'+img_name[i]+'.jpg',im_temp)
if __name__ == '__main__':
img_dir = './img'
img_name = os.listdir(img_dir) #列出文件夹下所有的目录与文件
#print(img_name[1])
for i in range(len(img_name)): #[0,100)
path = os.path.join(img_dir,img_name[i]) #合成得到每张图片的路径
path = path.replace('\\', '/') #将path中的'\'替换成'/'
image = cv2.imread(path) #根据路径读取图片
name_list = list(img_name[i])[:4] #获取该图片名的前四个字符(即图片对应的验证码)
#name = ''.join(name_list)
cut_image(image,i,name_list) #进行图片分割
print("图片%s分割完成" %i)
print(u'*****图片分割预处理完成!*****')
分割图片成功示例如下图所示,其中图片名称中的3个数字分别表示第几+1张图的第几个数是什么,例如0_3_4表示第1张图的第3个数是4。
处理数据后拆分训练集和测试集,训练并保存,模型被保存后可以被重新使用,也可以移植到其他环境中使用。相关代码如下:
import numpy as np
from sklearn import neighbors
import os
from sklearn.preprocessing import LabelBinarizer
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
import joblib
import cv2
if __name__ == '__main__':
#读入数据
data = []
labels = []
img_dir = './img_train_cut'
img_name = os.listdir(img_dir) #列出文件夹下所有目录与文件
# number = ['0','1','2','3','4','5','6','7','8','9']
for i in range(len(img_name)): #[0,400)
path = os.path.join(img_dir,img_name[i])
path = path.replace('\\', '/')
#cv2读入的图片是RGB三维的,转换成灰度图,将图片转换成一维
image = cv2.imread(path)
im = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
image = im.reshape(-1)
data.append(image) #添加到data列表中
y_temp = img_name[i][-5] #获取图片名中倒数第5个字符,对应图片中的数
labels.append(y_temp) #添加到labels列表中
#标签标准化
y = LabelBinarizer().fit_transform(labels)
x = np.array(data) #将列表转化为数组
y = np.array(y)
# print(x.shape,y.shape)
#拆分训练数据与测试数据,其中训练集320个数据,测试集80个数据
x_train, x_test, y_train, y_test = train_test_split(x,y,test_size=0.2)
#训练KNN分类器
clf = neighbors.KNeighborsClassifier()
clf.fit(x_train,y_train)
#保存分类器模型
joblib.dump(clf,'./knn.pkl')
#测试结果打印
pre_y_train = clf.predict(x_train)
pre_y_test = clf.predict(x_test)
class_name = ['class0','class1','class2','class3','class4','class5','class6','class7','class8','class9']
print(classification_report(y_train,pre_y_train,target_names=class_name))
print(classification_report(y_test, pre_y_test, target_names=class_name))
训练集的结果如下图所示:
测试集的结果如下图所示:
可以看到识别的准确度还是挺高的。
将所有代码整合,用验证码原图(4个数字)来测试准确率,相关代码如下:
from __future__ import division
import cv2
import math
import numpy as np
import os
import joblib
def del_noise(im_cut):
bins = 16
num_gray = math.ceil(256/bins)
#函数返回大于或等于一个给定数字的最小整数
hist = cv2.calcHist([im_cut],[0],None,[bins],[0,256])
lists = []
for i in range(len(hist)): #len(hist)都为16
#print hist[i][0]
lists.append(hist[i][0]) #lists里面有16个数
second_max = sorted(lists)[-2]
#查找第二多像素,最多的是验证码空白
bins_second_max = lists.index(second_max)
#取像素范围中位数模式,保留(mode±biases)的像素
mode = (bins_second_max + 0.5) * num_gray
for i in range(len(im_cut)): #len(im_cut)都为23(宽)
for j in range(len(im_cut[0])): #len(im_cut[0])为48(长)
#print im_cut[i][j]
if im_cut[i][j] < mode-15 or im_cut[i][j] > mode+15:
#不在中位数附近的设为白(255)
im_cut[i][j] = 255
return im_cut
def predict(image,img_name):
# image = cv2.imread('./img/8.jpg')
# 将BGR格式图片转换成灰度图片
im = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
im_cut_0 = im[4:18, 7:16] # 每个数字在图片中占的宽和长
im_cut_1 = im[4:18, 16:25]
im_cut_2 = im[4:18, 25:34]
im_cut_3 = im[4:18, 34:43]
im_cut = [im_cut_0, im_cut_1, im_cut_2, im_cut_3]
pre_text = []
for i in range(4):
#图片转换成一维后,再转换成二维的输入变量x
im_temp = del_noise(im_cut[i])
# print(type(im_temp))
image = im_temp.reshape(-1) #转化成1行
# print(image.shape)
tmp = []
tmp.append(list(image))
x = np.array(tmp)
pre_y = clf.predict(x) #使用训练的模型进行预测
pre_y = np.argmax(pre_y[0]) #得到预测出的验证码数
pre_text.append(str(pre_y))
# print(pre_text)
pre_text = ''.join(pre_text) #将预测得到的4个验证码数连起来与验证码图片名对比
if pre_text != img_name:
print('lable: %s' %(img_name),'predict: %s' %(pre_text),'\t','false')
return 0
else:
print('lable: %s' %(img_name),'predict: %s' %(pre_text))
return 1
if __name__ == '__main__':
img_dir = './img_test'
img_name = os.listdir(img_dir)
right = 0
global clf
clf = joblib.load('knn.pkl') #获取训练的KNN模型
for i in range(len(img_name)):
path = os.path.join(img_dir,img_name[i])
path = path.replace('\\', '/')
image = cv2.imread(path)
name_list = list(img_name[i])[:4] #将图片名的前4个数组成一个列表,例['0','2','9','0']
name = ''.join(name_list) #再将列表中的4个数组成一个字符串,例如'0290'
pre = predict(image,name) #进行验证,验证正确返回1,验证错误返回0
right += pre
accuracy = (right/len(img_name)) * 100 #计算准确率,(验证成功的图片数/所有图片数)*100
print(u'准确率为:%s%%,一共%s张验证码,正确:%s,错误:%s'%(accuracy,len(img_name),right,len(img_name)-right))
这里选择了20张验证码图片进行验证,输出结果如下图所示:
可以看到,可能是验证的图片基数太小,得到的准确率并不理想,只有50%,若选择更多的验证码图片进行验证,得到的结果可能会更加令我们满意。
本次实验项目是基于李永华老师编著的《Al源码解读——机器学习案例》进行学习编写的,书中有一些错误的地方也在此进行了修改,收获良多,其他项目我也会仔细学习研究,若有想法也会继续分享给大家~