2020/6/13 更新:看了评论区,发现一篇有价值的博客:https://blog.csdn.net/kerlomz/article/details/105974823
该博客主要提供了一种高效利用数据集的方法,鉴于作者写得过于晦涩,我这里再整理一下:
核心思想:训练一个只识别验证码中一种颜色文字的模型。
有效手段:将验证码通过颜色转换,就其余颜色都转化为训练所需的颜色。
举例:
如,训练一个只识别红色的字符的模型,这样我们将能得到原验证码、蓝转红、黄转红、黑转红四组数据集。如下图(从左到右依次为:原图、黄转红、黑转红和蓝转红):
这样一张图片就可以变为四张图片,一组数据集就可以变为四组数据集。在预测的时候,只需要获取需要输入的字符的颜色,然后将其转为红色,输入模型,即可得到待输入字符。
代码如下:
import cv2
# cv2 中默认读入图像是(B,G,R)
img = cv2.imread("20200306-180829.png")
cv2.imshow("raw", img)
# 蓝色转红色
# img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) # 蓝色R、G值较小,B值独大,B、G交换,接近红色分布
# 黑色转红色
# img[:, :, 2] = 255 - img[:, :, 2] # 黑色三个通道都比较小, 用255-R通道,则R通道独大,其余通道小,接近红色的分布
# 黄色转红色
# img = cv2.bitwise_not(img) # 黄色R、G值较大,B值较小,先取反,则R、G值较小, B值独大,接近蓝色分布
# img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) # 与蓝色转红色相同
cv2.imshow("convert", img)
cv2.waitKey(0)
更新:鉴于验证码数据标注困难,捣鼓出了一种自动生成方法。请移步:python 发票验证码自动生成
拿到一张发票,如何把上面的内容转化为计算机中结构化的数据呢?
直接拿到图片OCR,虽然目前的技术可以识别出内容,但很关键的问题就是,不知道哪条数据是什么。譬如“100”是数量还是单价。例如下面是一张增值税发票。
OCR识别结果如下:
这样的数据没啥意义。
发票左上角有一个二维码,通过扫描可以得到,发票的四要素(发票代码,发票号码,开票日期,校验码)。然后可以去全国增值税发票查验平台,输入四要素,查看发票详细信息。
二维码识别代码,放这里。要安装zxing包。参考:Python生成+识别二维码
import os
from PIL import Image
import zxing # 导入解析包
import random
# 在当前目录生成临时文件,规避java的路径问题
def ocr_qrcode_zxing(filename):
img = Image.open(filename)
ran = int(random.random() * 100000) # 设置随机数据的大小
img.save('%s%s.png' % (os.path.basename(filename).split('.')[0], ran))
zx = zxing.BarCodeReader() # 调用zxing二维码读取包
data = ''
zxdata = zx.decode('%s%s.png' % (os.path.basename(filename).split('.')[0], ran)) # 图片解码
print(zxdata)
# 删除临时文件
os.remove('%s%s.png' % (os.path.basename(filename).split('.')[0], ran))
return zxdata # 返回记录的内容
if __name__ == '__main__':
filename = '3.png' # 二维码图片
# zxing二维码识别
ltext = ocr_qrcode_zxing(filename) # 将图片文件里的信息转码放到ltext里面
官网查验需要验证码,回到本文主题,验证码识别。
1 数据集
全国增值税发票查验平台的验证码主要构成是:数字、字母、汉字的6位组合,以及红、黄、蓝、黑4种颜色,每次会随机要求输入其中某种颜色。没有颜色要求即是黑色。
基于selenium包进行验证码收集和标注。
参考:
selenium 安装与 chromedriver安装
自动化测试 selenium 模块 webdriver使用
通过对网站的分析发现,必须输入发票的四要素才能获得验证码的图片。
get_captcha.py
from selenium import webdriver #安装:pip install selenium
import time
import base64 #安装:pip install base64
def labelme(prex,filename):
src = browser.find_element_by_id('yzm_img').get_property('src')
filename = prex + str(filename)
label = "#"
while label.endswith("#"): #修改:要么从末尾backspace,要么输入"#"回车。注意从中间修改无效。看不清直接回车
label = input(filename+":")#输入:内容(字母一律小写)/颜色 颜色编码:除黑色为e,其余均为英文单词首字母
label = label.split('/')
if label == ['']:
return False
if len(label)!=2 or len(label[0])!=6 or len(label[1])!=6:
print("输入长度有误,内容颜色均6位,用/隔开")
return False
with open('labels.txt', 'a', encoding='utf-8') as f: # 标签保存在同目录下的labels.txt
f.write(filename + ":" + str(label) + "\n")
img = base64.b64decode(src.split(',')[-1])
with open('pic/'+filename + '.png', 'wb') as f:
f.write(img)
return True
if __name__ == "__main__":
prex = input('输入唯一标示(姓名首字母,例如:fsf):')
cnt = int(input('输入中断文件号,第一次为1:'))
browser = webdriver.Chrome()
browser.get("http://inv-veri.chinatax.gov.cn")
browser.find_element_by_css_selector('#fpdm').send_keys("发票代码")
browser.find_element_by_css_selector('#fphm').send_keys("发票号码")
browser.find_element_by_css_selector('#kprq').send_keys("开票日期")
browser.find_element_by_css_selector('#kjje').send_keys("校验码")
time.sleep(1)
print('修改:要么从末尾backspace,要么输入"#"回车。注意从中间修改无效。看不清直接回车')
print('输入:内容(字母一律小写)/颜色(颜色编码:除黑色为e,其余均为英文单词首字母)')
'''
获取需要输入的颜色,已注释
try:
color = browser.find_element_by_css_selector('#yzminfo font').text
except:
color = None
print(color)
'''
while True:
if labelme(prex,cnt):
cnt += 1
browser.find_element_by_css_selector('#imgarea a').click()
time.sleep(0.5)
2 模型
EndToEnd文本识别网络-CRNN(CNN+GRU/LSTM+CTC)
参考:语音识别:深入理解CTC Loss原理
输入是一张(35,90,3)的图片,分别是高、宽、通道数。(tensorflow里面高是在第一位)。通过一个CNN,这是一个类似VGG的三层卷积池化层。得到了(4,11,128)的特征向量。之后需要把高宽转置一下,让宽在第一位,reshape成(11,4*128),这个特征的含义,相当于把图片按宽度从左到右分成了11条,每一条是一个512维向量,代表这一条的特征。
之后这个(11,512)的特征向量作为第一个双向GRU的输入,一路从左到右输入到 GRU,一路从右到左输入到 GRU,然后将他们输出的结果加起来(sum)。然后输入第二个双向GRU,还是一路正方向,一路反方向,只不过这次直接将它们的输出连起来(contact)。这样得到了一个(11,128)维的向量。这个时候再分两路,一路全连接层输出11个(标签是6个,这时候就得靠ctc来对齐了)字符的概率,另一个全连接层输出11个颜色的概率。最后就是最小化ctc损失了。这里模型整体有两个ctc损失,一个来自颜色,一个来自字符。我这里按照颜色,字符3:7的权重计算最后总的loss(个人觉得颜色比较简单),这里大家可以自行调整。或者可以颜色训练一轮,字符训练两轮。好了,放代码吧。
先放个代码目录:
简单解释一下:
1.datasets,文件夹就是数据集咯,包括训练集train和测试集test。每个文件夹下又分别有输入inputs.npy、
字符标签labels_str.npy和颜色标签labels_color.npy。
2.get_captcha,数据集的获取和标注。由于本文只使用了少量数据集,想要提升识别效果,可以使用这个代码增加数据集。
其中pic存放验证码图片,labels存放文件名对应的标签。
3.model,一个是模型的代码,一个是模型的训练权重(只用了少量数据集7000,数字字母表现效果良好)。
4.captcha_predict.py, 验证码预测,sigin_in的中间文件。
5.color.txt, 所有颜色。
6.string.txt,所有字符(不全,只包含7000数据集中出现的字符)
7.config.py,配置文件。
8.pretreat.py, 图片预处理文件。
9.evaluate.py, 模型评估文件。
10.train.py, 模型训练文件。
11.sign_in.py,运行该文件,自动登录发票平台,查询发票信息。
ctc_model.py
from keras.layers import Conv2D, MaxPooling2D, Dropout, Flatten, Dense, GRU,\
Lambda,Permute,TimeDistributed,Bidirectional
from keras.models import Input,Model
from keras import backend as K
Height = 35
Width = 90
rnn_size = 64 # GRU的隐层大小
n_str = 1288 # 字符类别 + 1
n_color = 5 #
n_len = 6
conv_shape = (None, 11, 512)
def ctc_lambda_func(args):
y_pred, labels, input_length, label_length = args
return K.ctc_batch_cost(labels, y_pred, input_length, label_length)
def nn_base(input_tensor):
conv1_1 = Conv2D(32, 3, activation='relu', padding='same', kernel_initializer='he_normal')(input_tensor)
conv1_2 = Conv2D(32, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv1_1)
pool1 = MaxPooling2D((2, 2))(conv1_2)
conv2_1 = Conv2D(64, 3, activation='relu', padding='same', kernel_initializer='he_normal')(pool1)
conv2_2 = Conv2D(64, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv2_1)
pool2 = MaxPooling2D((2, 2))(conv2_2)
conv3_1 = Conv2D(128, 3, activation='relu', padding='same', kernel_initializer='he_normal')(pool2)
conv3_2 = Conv2D(128, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv3_1)
pool3 = MaxPooling2D((2, 2))(conv3_2)
m = Permute((2, 1, 3), name='permute')(pool3) # 高宽转置
flt = TimeDistributed(Flatten(), name='timedistrib')(m) #相当于flatten最后两个维度
des = Dense(32)(flt)
gru_1 = Bidirectional(GRU(rnn_size, return_sequences=True, kernel_initializer='he_normal'), merge_mode='sum')(des)
gru_2 = Bidirectional(GRU(rnn_size, return_sequences=True, kernel_initializer='he_normal'), merge_mode='concat')(
gru_1)
x = Dropout(0.25)(gru_2)
x1 = Dense(n_str, kernel_initializer='he_normal', activation='softmax',name='str_output')(x)
x2 = Dense(n_color, kernel_initializer='he_normal', activation='softmax', name='color_output')(x)
return x1,x2
def ctc_model(input_tensor, return_layer):
labels1 = Input(name='the_labels1', shape=[n_len], dtype='float32')
input_length = Input(name='input_length1', shape=[1], dtype='int64')
label_length = Input(name='label_length1', shape=[1], dtype='int64')
loss_out1 = Lambda(ctc_lambda_func, output_shape=(1,),
name='ctc1')([return_layer, labels1, input_length, label_length])
model = Model(inputs=[input_tensor, labels1, input_length, label_length], outputs=loss_out1)
model.compile(loss={'ctc1': lambda y_true, y_pred: y_pred}, optimizer='adadelta')
model.summary()
return model
input_tensor = Input(shape=(Height, Width, 3))
return_layers = nn_base(input_tensor)
model_str = ctc_model(input_tensor, return_layers[0])
model_color = ctc_model(input_tensor, return_layers[1])
model_all = Model(input_tensor, [return_layers[0], return_layers[1]])
model_all.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
train.py
from model.ctc_model import *
import os
import numpy as np
from keras.utils import generic_utils
from config import Config
def train():
train_x = np.load(os.path.join(Config.train_path,"inputs.npy"))
train_y_str = np.load(os.path.join(Config.train_path,"labels_str.npy"))
train_y_color = np.load(os.path.join(Config.train_path,"labels_color.npy"))
best_loss = np.inf
n_epoch = Config.n_epoch
batch_size = Config.batch_size
data_length = len(train_x)
steps = data_length // batch_size + 1
losses = np.zeros(shape=(steps, 2)) # 记录每个step的两个loss,一个epoch后又覆盖
for epoch in range(n_epoch):
bar = generic_utils.Progbar(steps) # keras进度条
print('Epoch {}/{}'.format(epoch + 1, n_epoch))
for step in range(steps):
start = batch_size * step
end = min(batch_size * (step + 1), data_length) # 获取批次首尾
X = train_x[start:end]
Y = train_y_str[start:end]
Y1 = train_y_color[start:end]
loss_str = model_str.train_on_batch([X, Y, np.array(np.ones(len(X)) * int(conv_shape[1])),
np.array(np.ones(len(X), ) * n_len)], Y)
loss_color = model_color.train_on_batch([X, Y1, np.array(np.ones(len(X)) * int(conv_shape[1])),
np.array(np.ones(len(X), ) * n_len)], Y1)
losses[step, 0] = loss_str
losses[step, 1] = loss_color
bar.update(step, [('str', np.mean(losses[:step + 1, 0])), ('color', np.mean(losses[:step + 1, 1]))])
mean_loss_str = np.mean(losses[:, 0])
mean_loss_color = np.mean(losses[:, 1])
mean_all_loss = 0.7 * mean_loss_str + 0.3 * mean_loss_color # 3:7 分配loss权重
print()
print("loss_str {} loss_color {} all_loss {}".format(mean_loss_str, mean_loss_color, mean_all_loss))
if mean_all_loss < best_loss: # 保存最佳val_loss的模型
best_loss = mean_all_loss
print("save weights")
model_all.save_weights(Config.model_path)
运行train.py就可以开始训练啦。
完了之后,模型用起来。
captcha_predict.py
from model.ctc_model import model_all,conv_shape
from pretreat import img_pretreat
from PIL import Image
import numpy as np
from keras import backend as K
from config import Config
def predict(filename):
'''
根据识别结果返回颜色和字符
:param filename: 验证码图片路径
:return:
'''
img = Image.open(filename)
arr = np.expand_dims(img_pretreat(img),0)
model_all.load_weights(Config.model_path)
result = model_all.predict(arr)
pred_str = K.get_value(
K.ctc_decode(result[0], input_length=np.ones(1, dtype=int) * int(conv_shape[1]), )[0][0])[:, :6] # ctc解码,[:,:6]只取前6位
pred_color = K.get_value(
K.ctc_decode(result[1], input_length=np.ones(1, dtype=int) * int(conv_shape[1]), )[0][0])[:, :6]
with open(Config.str_table_path,"r",encoding="utf-8") as f:
t_string = f.read()
with open(Config.color_table_path, "r", encoding="utf-8") as f:
t_color = f.read()
# print(pred_str,pred_color)
strings = [search_table(t_string,item)for item in pred_str[0]]
colors = [search_table(t_color,item)for item in pred_color[0]]
return strings,colors
def search_table(table,idx):
'''
排除掉小于0或者大于分类数目的索引
:param table: srting or color
:param idx: 索引
:return:
'''
if idx < 0:
return "0"
elif idx >= len(table):
return "1"
else:
return table[idx]
最后selenium脚本一键登录
sign_in.py
from selenium import webdriver
import time
import base64
from captcha_predict import predict
def captcha_img_ocr(input_color=None):
'''
:param input_color: 要求输入的颜色
:return:待输入字符串
'''
filename = time.strftime("%Y%m%d-%H%M%S")+'.png'
src = browser.find_element_by_id('yzm_img').get_property('src')
img = base64.b64decode(src.split(',')[-1])
with open(filename, 'wb') as f:
f.write(img)
strings, colors = predict(filename)
if input_color:
res = ""
for char,_color in zip(strings,colors):
if _color == input_color:
res += char
else:
res = "".join(strings)
return res
def set_text():
try:
color = browser.find_element_by_css_selector('#yzminfo font').text[0]
except:
color = None
print(color)
text = captcha_img_ocr(color)
print(text)
browser.find_element_by_css_selector('#yzm').clear()
browser.find_element_by_css_selector('#yzm').send_keys(text)
if __name__ == "__main__":
browser = webdriver.Chrome()
browser.get("http://inv-veri.chinatax.gov.cn")
browser.find_element_by_css_selector('#fpdm').send_keys("发票代码")
browser.find_element_by_css_selector('#fphm').send_keys("发票号码")
browser.find_element_by_css_selector('#kprq').send_keys("开票日期")
browser.find_element_by_css_selector('#kjje').send_keys("校验码")
time.sleep(1)
set_text()
check = browser.find_element_by_css_selector('#checkfp')
check.click()
time.sleep(1)
try:
popup = browser.find_element_by_css_selector('#popup_ok')
except:
popup = None
while popup: # 输错后弹窗,点击换验证码,反复输入
popup.click()
browser.find_element_by_css_selector('#imgarea a').click()
time.sleep(1)
set_text()
check.click()
time.sleep(1)
try:
popup = browser.find_element_by_css_selector('#popup_ok')
except:
popup = None