全国增值税发票查验平台验证码识别

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
源码:https://download.csdn.net/download/okfu_dl/11193913
评论 56
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值