深度学习应用于图像处理领域应该说有很长一段时间了,相关的研究成果也有很多的积累了,从项目和实践入手是我觉得的最好最快速有效的学习手段,当下很多主流的验证码识别系统大都是基于神经网络设计开发而来的,在处理图像数据方面,神经网络有着无与伦比的优势,本人最开始接触到卷积神经网络也是从验证码是被项目开始入手的。本项目从零开始介绍整体的实践思路,以我们熟知的12306网站验证码为例进行分析实战,从数据采集、图像处理、模型搭建、预测识别、界面开发几个节点进行针对性的设计开发,实现了验证码识别的完整流程,达到了很高的精度要求和速度要求,能够满足基本的调用需求。在模型方面,对比使用了多种经典的神经网络模型来进行实验,选取最优的实验结果来用于界面开发和调用识别。
整个项目基于python3.6开发实现,项目中包含整体项目所使用到的数据、代码脚本、模型文件和界面文件的所有数据文件。项目文件截图如下所示:
上述文件解释说明如下表所示:
文件名称 | 文件说明 |
---|---|
screenshot/ | 软件截图目录 |
getData.py | 图像验证码数据采集模块 |
imageCut.py | 图像验证码数据切分处理模块 |
dataHelper.py | 模型数据加载预处理模块 |
myModel.py | 模型训练模块 |
resnetModel.py | Resnet模块 |
predict.py | 离线模块预测识别模块 |
guiDemo.py | 界面可视化模块 |
texts.txt | 文本标签集合 |
valid_ip_all.json | IP代理池数据,避免爬虫被封禁 |
imageModel.h5 | 训练好的图像识别模型文件 |
textModel.h5 | 训练好的文本识别模块文件 |
接下来针对整个建模流程的各个节点进行详细说明。
一、数据采集
做深度学习的,基础条件就是:数据+算力,想要建模实践,首先就需要把所需的数据给准备好了,下面我们开始本文的第一步工作:源站验证码数据采集,这就是一个单纯地数据爬虫工作,详细的实现原理本来也就没什么,我也就不多讲解了,核心代码部分如下:
#!usr/bin/env python
#encoding:utf-8
from __future__ import division
'''
__Author__:沂水寒城
功能: 网络验证码数据采集模块
'''
def buildProxy():
'''
构建代理信息
'''
header_list=generateRandomUA(num=500)
header={'User-Agent':random.choice(header_list)}
ip_proxy=random.choice(ip_list)
one_type,one_ip,one_port=ip_proxy[0],ip_proxy[1],ip_proxy[2]
proxy={one_type:one_type+'://'+one_ip+':'+one_port}
return header,proxy
def getPageHtml(url,header,proxy,num_retries=3):
'''
多代理形式、超时重试机制,获取数据
'''
try:
response=requests.get(url,headers=header,proxies=proxy,timeout=5)
return response
except Exception as e:
time.sleep(random.randint(3,8))
while num_retries:
num_retries-=1
print('Left tring number is: ', num_retries)
return getPageHtml(url,header,proxy,num_retries)
def getVCPics(img_url,start,end,saveDir):
'''
下载验证码数据
'''
if not os.path.exists(saveDir):
os.makedirs(saveDir)
for i in range(start,end):
print("Downloading",i+1,"......")
header,proxy=buildProxy()
try:
img=getPageHtml(img_url,header,proxy,num_retries=3)
pic_name=saveDir+str(i+1)+'.jpg'
file_pic=open(pic_name,'ab')
file_pic.write(img.content)
file_pic.close()
time.sleep(random.randint(0.1,1))
except:
pass
if __name__ == '__main__':
print('captchaDataCollection!!!')
url="要爬取的验证码链接"
#验证码数据采集
getVCPics(url,0,500,'originalData/play/')
为了避免被源站检测到我们固定的IP地址在频繁地发送数据请求导致的被拉黑的情况出现,这里我采用了随机UA伪装+动态IP代理的方式来进行了规避处理,buildProxy函数是整个具体的实现,任何场合里面,这个方法都可以直接拿去使用的,尽可能地提升爬虫的工作效率。
二、图像切分处理
原始采集到的验证码数据是没有办法去直接使用的,需要进行切分处理,这样就转化为了单个子图识别的问题了,相对于原始大图来说识别的难度就小了很多了,当然了这里也不是说所有的图像验证码数据都是可以这样去操作的,12306的验证码数据是很规整的类型,可以基于一定的阈值对其进行划分切割处理,很方便就可以得到单个的子图数据了,下面是一些原始验证码数据样例:
可以看到:每幅图片里面均包含了8张子图,在第一行上面是文本标签数据,也就是说12306一张验证码数据我们要完成两种类型图像数据的识别工作。分析到这一步,接下来的工作就比较清晰了,我们首先要编写图像切割方法,完成对原始图像数据的切割处理,下面是切割部分的核心代码:
def singleCut(img_path,row,col,cutDir):
'''
单张验证码图像数据切割处理
'''
img=Image.open(img_path)
print('image_shape: ', img.size)
name=img_path.split('/')[-1].strip().split('.')[0].strip()
if not os.path.exists(cutDir):
os.makedirs(cutDir)
w,h=img.size
if row<=h and col<=w:
print('Original image info: %sx%s, %s, %s' % (w,h,img.format,img.mode))
rowheight=h//row
colwidth=w//col
for r in range(row):
for c in range(col):
box=(c*colwidth,r*rowheight,(c+1)*colwidth,(r+1)*rowheight)
if not os.path.exists(cutDir+name+'/'):
os.makedirs(cutDir+name+'/')
num=len(os.listdir(cutDir+name+'/'))
img.crop(box).save(cutDir+name+'/'+str(num)+'.png')
print('Total: ',num)
else:
print('Wrong Parameters!!!')
下面是实际切割的样例数据,原图如下:
上面的代码能做的是对输入的下半部分【也就是主体图像数据】部分进行切割,在调用上面的代码之前,需要的操作如下:
#截取文字
box=(120,3,177,22) #(左上角坐标,右下角坐标)
res=img.crop(box)
res.save('text.jpg')
res.show()
#截取主体图像数据
box=(0,33,293,190) #(左上角坐标,右下角坐标)
res=img.crop(box)
res.save('image.jpg')
res.show()
其中里面的切割参数是经过我的尝试之后得出来的,不是绝对固定的,坐标偏差1、2都是可以的,可以根据自己的喜好进行实际的调整,经过上面的切割结果如下。
文本数据部分:
图像数据部分:
之后,我们就可以针对image.jpg也就是主体的图像数据内容进行切割处理,结果如下所示:
之后就可以对自己爬取得到的所有数据按照上面的切割处理方式进行同样的处理操作了。完成上述工作之后,我们就可以着手创建自己的数据集了,验证码的识别其实本质上只是一个图像的分类任务,既然是分类,我们就需要知道一开始一共有多少个类别,这里我们搜集出来了一共有 80 个类别,具体类别名称如下所示:
打字机
调色板
跑步机
毛线
老虎
安全帽
沙包
盘子
本子
药片
双面胶
龙舟
红酒
拖把
卷尺
海苔
红豆
黑板
热水袋
烛台
钟表
路灯
沙拉
海报
公交卡
樱桃
创可贴
牌坊
苍蝇拍
高压锅
电线
网球拍
海鸥
风铃
订书机
冰箱
话梅
排风机
锅铲
绿豆
航母
电子秤
红枣
金字塔
鞭炮
菠萝
开瓶器
电饭煲
仪表盘
棉棒
篮球
狮子
蚂蚁
蜡烛
茶盅
印章
茶几
啤酒
档案袋
挂钟
刺绣
铃铛
护腕
手掌印
锦旗
文具盒
辣椒酱
耳塞
中国结
蜥蜴
剪纸
漏斗
锣
蒸笼
珊瑚
雨靴
薯条
蜜蜂
日历
口哨
整理创建好的数据集如下所示:
一共有80个子文件夹,每个子文件夹里面存放的是一个类别的数据,为了方便模型的计算,这里使用数字【文本类别对应的索引值】来标识不同的类别,下面简单看几个类别的数据。
0类:
5类:
52类:
等。。。。。。
三、模型搭建
完成了基础数据集的准备和处理工作后就可以进行神经网络模型的搭建工作了,这里我们主要使用了lenet、vgg、resnet几种模型,其他的模型也可以按照同样的形式进行组织,代码设计的很灵活,可以自主地进行替换处理。下面是建模部分核心代码:
def resnetModel(num_classes,deep=18,h=16,w=10,way=1):
'''
resnet 模型
'''
if deep==18:
model=ResnetBuilder.build_resnet_18((way, h, w), num_classes)
elif deep==34:
model=ResnetBuilder.build_resnet_34((way, h, w), num_classes)
elif deep==50:
model=ResnetBuilder.build_resnet_50((way, h, w), num_classes)
model.compile(optimizer='sgd',loss='categorical_crossentropy',metrics=['accuracy'])
print(model.summary())
return model
def vggModel(num_classes,epochs,x_train,h=16,w=10,way=1):
'''
VGG-16模型
'''
print('x_train.shape: ',x_train.shape)
input_shape=(h,w,way)
model=Sequential()
model.add(Conv2D(64,(3,3),strides=(1,1),input_shape=input_shape,padding='same',activation='relu',kernel_initializer='uniform'))
model.add(Conv2D(64,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Conv2D(128,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(Conv2D(128,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Conv2D(256,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform')) #512
model.add(Conv2D(256,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(Conv2D(256,(3,3),strides=(1,1),padding='same',activation='relu',kernel_initializer='uniform'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Flatten())
model.add(Dense(1024,activation='relu')) #4096
model.add(Dropout(0.5))
model.add(Dense(1024,activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes,activation='softmax'))
model.compile(loss='categorical_crossentropy',optimizer='sgd',metrics=['accuracy'])
print(model.summary())
return model
def selfModel(num_classes,epochs,x_train,h=16,w=10,way=1):
'''
自定义基础模型 sparse_categorical_crossentropy
'''
print('x_train.shape: ',x_train.shape)
input_shape=(h,w,way)
model = models.Sequential([
layers.Conv2D(64, (3, 3), padding='same', activation='relu', input_shape=input_shape),
layers.MaxPooling2D(), # 19 -> 9
layers.Conv2D(64, (3, 3), padding='same', activation='relu'),
layers.MaxPooling2D(), # 9 -> 4
layers.Conv2D(64, (3, 3), padding='same', activation='relu'),
layers.MaxPooling2D(), # 4 -> 2
layers.GlobalAveragePooling2D(),
layers.Dropout(0.25),
layers.Dense(256, activation='relu'),
layers.Dense(num_classes, activation='softmax'),
])
model.compile(optimizer='rmsprop',loss='categorical_crossentropy',metrics=['accuracy'])
print(model.summary())
return model
def main(dataDir='dataset/',nepochs=100,h=66,w=66,way=3,deep=18,flag='self',resDir='result/self/'):
'''
主函数
'''
if not os.path.exists(resDir):
os.makedirs(resDir)
X,y=dataHelper.loadDataset(dataDir=dataDir,h=h,w=w)
X=preProcess(X)
y,num_classes=oneHotEncode(y)
#数据集分割
X_train,X_test,y_train,y_test=train_test_split(X,y,test_size=0.30,random_state=7)
#模型定义初始化
if flag=='self':
model=selfModel(num_classes,nepochs,X_train,h=h,w=w,way=way)
elif flag=='vgg':
model=vggModel(num_classes,nepochs,X_train,h=h,w=w,way=way)
else:
model=resnetModel(num_classes,deep=deep,h=h,w=w,way=way)
# 当标准评估停止提升时,降低学习速率
reduce_lr = ReduceLROnPlateau(verbose=1)
history = model.fit(X_train,y_train,epochs=nepochs,validation_data=(X_test, y_test),callbacks=[reduce_lr])
resHandle(history,resDir,model,X_test,y_test)
print('Finished.....................................................')
我们的模型训练工作在GPU上进行,下面是部分训练输出截图:
基于GPU的训练速度还是非常快的,我尝试在CPU环境里面跑一下,发现真的是很慢,而且电脑卡到了爆炸,我们接下来看一下对应模型的实验结果。每个模型对应的结果文件示例图如下所示:
以resnet-34为例,展示其训练过程可视化曲线:
我们原始采集的数据量并不大,对于模型的训练有一定的影响,这个主要是考虑到不想对12306网站造成过大的压力,这里可以根据自己的需要尽心调整。
四、模型识别预测与可视化调用
完成了模型的训练工作后,我们就得到了可以使用的离线模型文件,之后想要对图像进行识别就可以直接加载离线模型文件进行识别预测了,为了使得我们的使用更加直观方便,这里编写了简单的界面工具来辅助进行预测分析,核心代码如下:
def upload_image():
'''
上传图像
'''
try:
file_path=filedialog.askopenfilename()
uploaded=Image.open(file_path)
uploaded.thumbnail(((top.winfo_width()/2.25),(top.winfo_height()/2.25)))
im=ImageTk.PhotoImage(uploaded)
sign_image.configure(image=im)
sign_image.image=im
label.configure(text='')
executeButton(file_path)
except:
pass
upload=Button(top,text=u"点击上传图像",command=upload_image,padx=10,pady=5)
upload.configure(background='#63B8FF', foreground='#364156',font=('arial',20,'bold'))
upload.pack(side=BOTTOM,pady=50)
sign_image.pack(side=BOTTOM,expand=True)
label.pack(side=BOTTOM,expand=True)
heading = Label(top, text=u"12306验证码识别机器人",pady=20, font=('arial',30,'bold'))
heading.configure(background='#40E0D0',foreground='#364156')
heading.pack()
top.mainloop()
使用样例如下:
后台输出如下:
自己本地测试了几百张图片,识别的精度还是很高的,能够满足自动识别的需求。
作者:沂水寒城,CSDN博客专家,个人研究方向:机器学习、深度学习、NLP、CV
Blog: http://yishuihancheng.blog.csdn.net
赞 赏 作 者
更多阅读
2020 年最佳流行 Python 库 Top 10
2020 Python中文社区热门文章 Top 10
5分钟快速掌握 Python 定时任务框架
特别推荐
点击下方阅读原文加入社区会员