前言

验证码技术作为一种反自动化技术,使得很多程序的自动化工作止步。今天作者采用一些数字图像处理和CNN方法来识别较为简单的数字验证码


实验步骤

实验步骤主要围绕以下展开

  1. 图像预处理即滤除噪声和字符分割
  2. CNN搭建和训练
  3. 验证码识别

一. 图像预处理

接下来,我将一张验证码0250为例,使用python语言和依赖opencv来开展预处理

可以看到在验证码中除了数字字符以外,夹杂着很多斑点噪声以及横线干扰。这些斑点噪声面积很小,在二值化图像里属于很小的连通域,在后续操作中我们可以通过限制连通域大小来滤除这些小斑点。

1.1 滤除噪声

(1)读取灰度图像

# 读取图片并保存为灰色img_gray = cv2.imread(r'./images/0250.jpg',flags=cv2.IMREAD_GRAYSCALE)

(2) 二值化

因为字符属于低灰度像素,因此采用逆向二值化,并为了照顾到颜色较淡的字符,我们采用较高的判别阈值,这里使用0.9

 # 二值化_,img_bin = cv2.threshold(img_gray,int(0.9*255),255,cv2.THRESH_BINARY_INV) #二值化阈值0.9

(3)滤除小连通域

由二值图像可以看到,图像存在着很多斑点(小连通域)干扰,这里我们采用连通域面积限制来滤除这些斑点,首先我们先定义滤除小连通域方法。

# 定义二值图像去除小连通域
def RemoveSmallCC(bin,small,connectivity=8):# (1)获取连通域标签ret, labels = cv2.connectedComponents(bin, connectivity=connectivity)# (2)去小连通域for n in range(ret + 1):# 0~maxnum = 0 # 清零for elem in labels.flat:if elem == n:num += 1if num < small:  # 去除小连通域bin[np.where(labels == n)] = 0return bin

connectivity 表示连通域构成方式为 8邻域 or 4邻域 。在初始阶段,为了缓解斑点和字符粘连的问题,我们希望采用4邻域来分解更多的连通域,我们限制连通域面积上限为200,接下来我们看看效果:

# 去除斑点img_bin = RemoveSmallCC(img_bin, 200,connectivity=4)

相比较原始图像,这里滤出掉了大多数斑点,保留了字符、横线等大连通域。

(4) 形态学开运算

在滤除斑点的过程中,依然可能存在少部分斑点粘连在字体上,这里我们采用形态学开运算,即腐蚀、消除、膨胀的方法来断开斑点和字符连通,去除斑点,再恢复初始字符形态。但是开运算的过程中也可能会导致字符结构断开并被消除,因此我们选择较小的结构元素(本文采用3X3),来去除那些较微弱的粘连问题

    # 定义结构元素size = 3kernel = np.ones((size, size), dtype=np.uint8)# 形态学腐蚀img_erosion = cv2.erode(img_bin, kernel, iterations=1)# 小连通域滤除img_erosion = RemoveSmallCC(img_erosion,30)# 形态学膨胀img_dila = cv2.dilate(img_erosion, kernel, iterations=1)

(5) 去除横线

此时,图像还有令人讨厌的横线干扰,其中横线穿梭在字符之间。为了保全字符的完整,我们不对字符上的横线进行滤除(粘有横线干扰的字符交给CNN吧。),仅滤除字符之间的横线。首先我们将二值图像在x轴上进行投影,为此不妨先定义一个投影方法。

# 二值图像一维投影
def BIN_PROJECT(bin):IMG = bin / 255.0 # 浮点型PROJ = np.zeros(shape=(IMG.shape[1]))for col in range(IMG.shape[1]):v = IMG[:, col]# 列向量PROJ[col] = np.sum(v)return PROJ

得到投影的统计图

其中红色圈圈标记了字符之间横线位置,其中该投影分量较小,因此我们可以通过设定阈值的方法去除。

    flaw_area = np.where(PROJ<5) # 字符间横线img_dila[:,flaw_area]=0 # 去除横线IMG = RemoveSmallCC(img_dila,200) # 去除小连通区域

这时验证码中的字符差不多显现出来了。

1.2 字符分割

同多个字符识别相比,CNN对单个字符识别效率更高,而且多个字符所需的样本数据大,且交杂在一起的验证码字符更不易识别,因此有必要对字符串的几何特征分析来分割出单个字符。如上述验证码产生两个部分,02交杂在一起,50交杂在一起。

对于不同的字符交杂类型可以分为以下几种情况:

  • 4个部分:1+1+1+1 型,即字符之间不存在交杂,可直接分割
  • 3个部分:1+1+2型,我们仅对len=2的部分进行二分就可以了
  • 2个部分(1): 2+2型,对len =2 的部分均采用二分法
  • 2个部分(2):1+3型,对len=3的部分采用三分法
  • 1个部分: 4型,对len=4的部分采用4分法

这里我们用LOC表示每个部分首尾地址集合,COUNT表示每个部分的大小集合。观察0250验证码投影属于2+2型,因此每个部分均采用二分法即可

注意:对于2+2型和1+3型的区分,可以采用skew = len(max)/len(min)的比值来判定,通常较小的为2+2型,较大的为1+3型

首先提取出投影中各个部分的位置以及大小,定义方法:

# 提取数字部分
def Extract_Num(PROJ):num = 0COUNT = []LOC = []for i in range(len(PROJ)):if PROJ[i]:# 如果非零则累加num+=1if i == 0 or PROJ[i-1]==0:# 记录片段起始位置start = iif i == len(PROJ)-1 or PROJ[i+1]==0 :# 定义片段结束标志,并记录片段end = iif num > 10:# 提取有效片段COUNT.append(num)LOC.append((start,end))num = 0 #清0return LOC,COUNT

在对各个部分分析和分割成4个数字

# 分割数字
def Segment4_Num(COUNT,LOC):assert len(COUNT)<=4 and len(COUNT)>0# 数字部分分析if len(COUNT) ==4:#(1,1,1,1)return LOCif len(COUNT)==3:#(1,1,2)idx = np.argmax(np.array(COUNT))# 最大片段下标r = LOC[idx]# 最大片段位置start = r[0]end = r[1]m = (r[0]+r[1])//2 # 中间位置# 修改LOC[idx]LOC[idx] = (start,m)LOC.insert(idx+1,(m+1,end))return LOCif len(COUNT) ==2:#(2,2)or(1,3)skew = max(COUNT)/min(COUNT)# 计算偏移程度if skew<1.7:# 认为是(2,2)start1 = LOC[0][0]end1 = LOC[0][1]start2 = LOC[1][0]end2 = LOC[1][1]m1 = (start1+end1)//2m2 = (start2+end2)//2return [(start1,m1),(m1+1,end1),(start2,m2),(m2+1,end2)]else:       # 认为是(1,3)idx = np.argmax(np.array(COUNT))# 最大片段下标start = LOC[idx][0]end = LOC[idx][1]m1 = (end-start)//3+startm2 = (end-start)//3*2+start# 修改LOC[idx]LOC[idx] = (start, m1)LOC.insert(idx+1,(m1+1,m2))LOC.insert(idx+2,(m2+1,end))return LOCif len(COUNT) ==1:# (4)start = LOC[0][0]end = LOC[0][1]m1 = (end-start)//4+startm2 = (end - start) // 4*2 + startm3 = (end - start) // 4*3 + startreturn [(start,m1),(m1+1,m2),(m2+1,m3),(m3+1,end)]

调用方法

   # PROJ提取数字部分PROJ = BIN_PROJECT(IMG)LOC,COUNT = Extract_Num(PROJ)# 优化COUNTCOUNT = COUNT_OPTIMIZE(IMG, LOC, COUNT)# 分割数字LOC = Segment4_Num(COUNT,LOC)NUM0 = IMG[:,LOC[0][0]:LOC[0][1]]NUM0 = RemoveSmallCC(NUM0,50)NUM1 = IMG[:,LOC[1][0]:LOC[1][1]]NUM1 = RemoveSmallCC(NUM1,50)NUM2 = IMG[:,LOC[2][0]:LOC[2][1]]NUM2 = RemoveSmallCC(NUM2,50)NUM3 = IMG[:,LOC[3][0]:LOC[3][1]]NUM3 = RemoveSmallCC(NUM3,50)

分割结果:

分割结束后我们需要将每张数字图片resize到32x32格式黑白图像保存起来,提供给后面CNN进行训练和识别


二. CNN搭建和训练

2.1 训练集和测试集

这里我们从数据集中随机采样70%作为训练集,30%作为测试集

#--------------------------- 随机选取70%作为训练集,%30作为测试集 ---------------------------------------------data_total = len(img_list) # 数据集总数
train_total = int(0.7*data_total) #训练集总量
test_total = data_total - train_total # 测试集总量#随机采样训练样本
import random
train_list = random.sample(img_list,train_total) #训练样本列表
test_list = [item for item in img_list if item not in train_list] # 测试样本列表

样本的保存和读入采用TFRecord格式,其中依赖TFRtools脚本下载链接为:https://download.csdn.net/download/ephemeroptera/11139598

2.2 CNN搭建和训练

CNN采用两个卷积层和两个全连层,输出使用softmax函数,损失函数采用交叉熵,使用ADAM优化器,学习率为0.0001,迭代5000次,加入dropout,架构如下

"""
输入:x:[-1,32,32,1] 输入图像y_:[-1,10]     标签
CNN:CONV1: w(3x3),stride = 1,pad = 'same',fmaps = 32 ([-1,16,16,32])POOL1: p(2x2),stride = 2,pad = 'same'RELUCONV2: w(3x3),stride = 1,pad = 'same,fmaps = 64   ([-1,8,8,64])POOL2: p(2x2),stride = 2,pad = 'same'RELU,Reshape                                      ([-1,8*8*64])DENSE1: fmaps = 1024                               ([-1,1024])RELUDropoutDENSE2: fmaps = 10                                   ([-1,10])Softmax
"""

完整代码如下:

import tensorflow as tf
import  numpy as np
import TFRtools
import pickle# 生成相关目录保存生成信息
def GEN_DIR():import osif not os.path.isdir('ckpt'):print('文件夹ckpt未创建,现在在当前目录下创建..')os.mkdir('ckpt')if not os.path.isdir('trainLog'):print('文件夹ckpt未创建,现在在当前目录下创建..')os.mkdir('trainLog')# 定义输入
x = tf.placeholder(tf.float32,[None,32,32,1],'x')
y_ = tf.placeholder(name="y_", shape=[None, 10],dtype=tf.float32)# 第一层卷积层 32x32 to 16x16 , fmaps = 32
CONV1 = tf.layers.conv2d(x,32,5,padding='same',activation=tf.nn.relu,kernel_initializer=tf.random_normal_initializer(0,0.1),name='CONV1')
POOL1 = tf.layers.max_pooling2d(CONV1,2,2,padding='same',name='POOL1')# 第二层卷积层 16x16 to 8x8, fmaps =64
CONV2 = tf.layers.conv2d(POOL1,64,5,padding='same',activation=tf.nn.relu,kernel_initializer=tf.random_normal_initializer(0,0.1),name='CONV2')
POOL2 = tf.layers.max_pooling2d(CONV2,2,2,padding='same',name='POOL2')# 第三层全连接层 8x8x64 to 1024  , fmaps = 1024
flat = tf.reshape(POOL2,[-1,8*8*64]) # 平铺特征图
DENSE1 = tf.layers.dense(flat,1024,activation=tf.nn.relu,kernel_initializer=tf.random_normal_initializer(0,0.1),name='DENSE1')
# dropout
drop_rate = tf.placeholder(dtype=tf.float32,name='drop_rate')
DP = tf.layers.dropout(DENSE1,rate=drop_rate,name='DROPOUT')# softmax 1024 to 10 , fmaps = 10
DENSE2 = tf.layers.dense(DP,10,activation=tf.nn.softmax,kernel_initializer=tf.random_normal_initializer(0,0.1),name='DENSE2')
# 定义损失函数
cross_entropy = -tf.reduce_sum(y_ * tf.log(DENSE2)) #计算交叉熵# 梯度下降
train_step = tf.train.AdamOptimizer(0.0001).minimize(cross_entropy) #使用adam优化器来以0.0001的学习率来进行微调# 准确率测试
correct_prediction = tf.equal(tf.argmax(DENSE2,1), tf.argmax(y_,1)) #判断预测标签和实际标签是否匹配
accuracy = tf.reduce_mean(tf.cast(correct_prediction,"float"))# 保存模型
saver = tf.train.Saver(var_list=[var for var in tf.trainable_variables()])# 数据集读取
# 读取TFR,不打乱文件顺序,指定数据类型
[data,label,num] = TFRtools.ReadFromTFRecord(sameName= r'.\TFR\CODE-*',isShuffle= False,datatype= tf.float64,labeltype= tf.uint8,)
# 批量处理,送入队列数据,指定数据大小,不打乱数据项,设置批次大小32
[data_batch,label_batch,num_batch] = TFRtools.DataBatch(data,label,num,dataSize= 32*32,labelSize= 10,isShuffle= False,batchSize= 32)
# 修改格式
data_batch = tf.cast(tf.reshape(data_batch,[-1,32,32,1]),tf.float32)
label_batch = tf.cast(label_batch,tf.float32)ACC = [] #记录准确率# 建立会话
with tf.Session() as sess:# 创建相关目录GEN_DIR()# 初始化变量init = (tf.global_variables_initializer(), tf.local_variables_initializer())sess.run(init)# 开启协调器coord = tf.train.Coordinator()# 启动线程threads = tf.train.start_queue_runners(sess=sess, coord=coord)for step in range(10000):# 获取批数据TDB = sess.run([data_batch,label_batch,num_batch])# 训练sess.run(train_step,feed_dict={x:TDB[0],y_:TDB[1],drop_rate:0.5})# 测试if step%5 == 1:train_acc = sess.run(accuracy,feed_dict={x:TDB[0],y_:TDB[1],drop_rate:0.0})ACC.append(train_acc)print('迭代次数:%d..准确率:%.3f'%(step,train_acc))# 保存模型if step % 1000 == 0 and step>0:saver.save(sess, './ckpt/CNN.ckpt', global_step=step)# 关闭线程coord.request_stop()coord.join(threads)
#  保存loss记录with open('./trainLog/ACC.acc', 'wb') as a:losses = np.array(ACC)pickle.dump(losses,a)print('保存accuracy信息..')

在2000次迭代后CNN差不多收敛了。

2.3 验证测试集

这里我们将训练好的CNN来验证下测试集,因为CNN是对单个字符识别,识别一个验证码需要识别4个字符,这里我们分别给出数字识别率和验证码识别率

"""
该脚本用于验证测试集的准确率
"""import tensorflow as tf
import numpy as np#*************************** CNN识别 *******************************# 读取测试集
import TFRtools
[data,label,num] = TFRtools.ReadFromTFRecord(sameName= r'.\TFR\CODE_TEST-*',isShuffle= False,datatype= tf.float64,labeltype= tf.uint8,)
# 单个验证不采用批处理
# [data_batch,label_batch,num_batch] = TFRtools.DataBatch(data,label,num,dataSize= 32*32,labelSize= 10,isShuffle= False,batchSize= 32)# 数据格式修正
data = tf.cast(tf.reshape(data,[-1,32,32,1]),tf.float32)
label = tf.cast(label,tf.float32)with tf.Session() as sess:# 初始化变量init = (tf.global_variables_initializer(), tf.local_variables_initializer())sess.run(init)# 开启协调器coord = tf.train.Coordinator()# 启动线程threads = tf.train.start_queue_runners(sess=sess, coord=coord)# 恢复模型meta_graph = tf.train.import_meta_graph('./ckpt/CNN.ckpt-4000.meta')  # 加载模型meta_graph.restore(sess, tf.train.latest_checkpoint('./ckpt'))  # 加载数据# 获取输入输出graph = tf.get_default_graph()x = graph.get_tensor_by_name("x:0")  # 获取输入占位符xdrop_rate = graph.get_tensor_by_name("drop_rate:0") # 获取输入占位符drop_rateDENSE2 = graph.get_tensor_by_name("DENSE2/Softmax:0")# 获取输出DENSE2# 验证码识别test_total = 2583 # 测试验证码数量NUM_SC = [] # 数字识别情况ID_SC = [] # 测试集验证码识别情况for N in range(test_total):score = 0 # 每组验证码的验证成绩,满分4分for idx in range(4):# 获取数据集TD =sess.run([data,label,num])# 识别y = sess.run(DENSE2,feed_dict={x:TD[0],drop_rate:0.0})# 比较comp = np.equal(np.argmax(y),np.argmax(TD[1]))NUM_SC.append(comp)# 计分if comp:score +=1if idx ==3 :# 完成识别一组验证码ID_SC.append(score)score = 0 # 识别后清零# 打印print('第%d张验证码的第%d位识别情况:%s'%(N,idx,comp))# 统计识别率# (1)数字识别率print('数字识别率为:%.3f'%(sum(NUM_SC)/len(NUM_SC)))#  (2) 验证码识别率SC = 0 # 测试集识别总分for sc in ID_SC:if sc == 4:SC +=1print('验证码识别率为:%.3f' % (SC/len(ID_SC)))# 关闭线程coord.request_stop()coord.join(threads)

测试结果:

看来对于横线干扰的字符CNN依然能够较好地识别,因为只要一个字符错误,该验证码算作错误识别,所以验证码识别率相对较低


三.在线识别

这里我们使用先前的去噪,字符分割,CNN 识别方法来随机识别一张测试集中的图片

"""
该脚本用于在线识别一张验证码
"""
import tensorflow as tf
from ImageProcess import *
from PreProcess import Segment4_NUMBERdef CNN_Identify(numbers):with tf.Session() as sess:# 初始化变量init = (tf.global_variables_initializer(), tf.local_variables_initializer())sess.run(init)# 恢复模型meta_graph = tf.train.import_meta_graph('./ckpt/CNN.ckpt-4000.meta')  # 加载模型meta_graph.restore(sess, tf.train.latest_checkpoint('./ckpt'))  # 加载数据# 获取输入输出graph = tf.get_default_graph()x = graph.get_tensor_by_name("x:0")  # 获取输入占位符xdrop_rate = graph.get_tensor_by_name("drop_rate:0")  # 获取输入占位符drop_rateDENSE2 = graph.get_tensor_by_name("DENSE2/Softmax:0")  # 获取输出DENSE2id = []for number in numbers:# 格式修正number = np.reshape(number,[-1,32,32,1]).astype(np.float32)# 识别y = sess.run(DENSE2, feed_dict={x: number, drop_rate: 0.0})# 记录识别结果id.append(np.argmax(y))return idcode_name = r'./TEST/0243.jpg'##(1)分割数字
img_gray = cv2.imread(code_name, flags=cv2.IMREAD_GRAYSCALE) # 读取灰度图像
SHOW('code',img_gray) # 显示
_, img_bin = cv2.threshold(img_gray, int(0.9 * 255), 255, cv2.THRESH_BINARY_INV) # 二值化(0.9)
numbers = Segment4_NUMBER(img_bin)# 分割数字##(2)识别
ID = CNN_Identify(numbers)print('验证码%s的识别结果为%s'%(code_name,ID))
cv2.waitKey(0)

识别效果:


四.总结

本次实验并没有直接将验证码送入CNN识别,而是先采用去噪,字符分割的预处理,再送入CNN进行训练。这里能否分割一个完整的字符很重要,如果分割不准确造成字符错位,截断等问题势必影响字符的识别,因此设计一个良好的字符分割算法尤为重要。本文中基于字符的几何特征考虑到了字符之间交杂错位的各种情况分别制定了对应的分割策略,总体上分割效果良好,如下图(字符0)

但是验证码字符情况千奇百怪,本算法也有一定的局限性,存在着错分情况(尤其是2+2型结构分割),因此在保持CNN不变的前提下,可以通过优化分割算法来进一步提高识别率.

最后,这里有完整的代码提供给需要学习的同学参考:https://download.csdn.net/download/ephemeroptera/11141212

基于CNN的四位数字验证码识别相关推荐

  1. 基于Python的KNN数字验证码识别

    一.主要内容 本项目基于Python爬虫爬取验证码图片,对图片进行去噪.分割,通过KNN算法训练模型,实现验证其准确率. 二.系统流程 首先从指定的网页中爬取验证码图片数据,然后对数据进行一个去噪和分 ...

  2. 基于CNN 对车牌数字进行识别,(二)

    文章目录 序言 识别方案 方案1 方案2 选择方案: 准备数据 数据的准备 读取数据集 构建神经网络 进行训练 查看模型的准确度 优化 测试效果 进行车牌字符预测 实测 总结 下一章 完整代码: 数据 ...

  3. 【Matlab验证码识别】遗传算法和最大熵优化+大津法(OTSU)+自定义阈值数字验证码识别【含GUI源码 1694期】

    一.代码运行视频(哔哩哔哩) [Matlab验证码识别]遗传算法和最大熵优化+大津法(OTSU)+自定义阈值数字验证码识别[含GUI源码 1694期] 二.matlab版本及参考文献 1 matlab ...

  4. 【转】CNN+BLSTM+CTC的验证码识别从训练到部署

    [转]CNN+BLSTM+CTC的验证码识别从训练到部署 转载地址:https://www.jianshu.com/p/80ef04b16efc 项目地址:https://github.com/ker ...

  5. 【python OpenCV3.3 图像处理教程:直线检测、圆检测、对象测量、腐蚀、膨胀等形态学操作、数字验证码识别、人脸检测

    1. 直线检测 Hough Line Transform:前提:边缘检测已经完成,基于霍夫变换 1.1 原理 可以通过(theta,r)唯一表示一个点. 把过三个点的全部直线以某一角度全部计算出来,如 ...

  6. 【Python5】图像操作,数字验证码识别,图像拼接/保存器

    文章目录 1.安装 2.画图 3.几何变换 3.1 位计算 3.2 遮挡 3.3 通道切分合并 3.4 金字塔 3.5 缩放 3.6 平移 3.7 旋转 3.8 仿射变换 3.9 透视变换 4.形态学 ...

  7. 基于CNN的性别、年龄识别及Demo实现

    一.相关理论 本篇博文主要讲解2015年一篇paper<Age and Gender Classification using Convolutional Neural Networks> ...

  8. 按键精灵+大漠插件简单数字验证码识别实践笔记

    因为资源短缺,公司用了一个很老的系统分配资源,每个项目每天都要经历上演一次像抢火车票一样的经历,而往往又空手而归,搞得大家疲惫不堪.而其中的关键在于几个简单的数字验证码的识别,于是在业余时间看了一些验 ...

  9. 简单的四位数字验证码

    1.Body <body οnlοad="loadCodeNumber()"><form name="form1" οnsubmit=&quo ...

最新文章

  1. 前端日报-20160527-underscore 源码解读
  2. win10系统启动服务器不可用,解决win10专业版windows installer服务不可用不能启动更不能访问...
  3. win7查看某个端口被占用的解决方法
  4. python树状节点 可拖拽_Python 的 heapq 模块源码分析
  5. [译]ASP.NET Core 2.0 带初始参数的中间件
  6. tdbgrid 数据类型输入错误 vb_VB语言基础(上)
  7. JAVA xml转dom_如何在Java中将String转换为DOMSource?
  8. 【华为云技术分享】Batch Normalization (BN) 介绍
  9. 使用Mali Graphics Debugger调优Unity程序(Killer示例)
  10. python线程暂停_在python中暂停一个线程和另一个线程
  11. 一本通1594涂抹果酱
  12. Zabbix Server端配置文件说明
  13. Redis3集群安装
  14. VScode环境配置C/C++
  15. 基于python的-使用正则表达式验证手机号
  16. VMware ThinApp应用程序虚拟化工具
  17. git和SVN的区别
  18. umount提示target is busy无法卸载
  19. 有趣的表情包购物网站
  20. 教你怎样查询快递查询单号并保存物流信息

热门文章

  1. 2022-11-15面试题
  2. springboot中使用mybatis只打印sql和总条数不打印查询结果的yml配置
  3. C#使用微软的TTS引擎发音
  4. django的信号signals详解
  5. Flink电商实时数仓项目03-DWM层
  6. axios封装nodejs前后端实现getpost文件上传
  7. 不规则图形数格子的方法_常见的估算不规则图形的面积.ppt
  8. Go学习笔记 -- 程序实体之变量
  9. 2021.11.9MySQL基本语句以及数据库于Unity的连接
  10. html引入第三方语音合成,HTML5语音合成功能的实现