缩略语和关键术语定义
1、卷积神经网络(Convolutional Neural Networks, CNN)是一类包含卷积计算且具有深度结构的前馈神经网络(Feedforward Neural Networks),是深度学习(deep learning)的代表算法之一。
在二十一世纪后,随着深度学习理论的提出和数值计算设备的改进,卷积神经网络得到了快速发展,并被应用于计算机视觉、自然语言处理等领域。
2、本发明所指的训练模型是指:基于特定机器学习框架(如TensorFlow)通过特定算法训练生成的模型。
3、本发明中的发票类型模型,发票号码模型,发票代码模型,发票日期模型,不含税金额模型(专票),校验码模型(普票)均由卷积神经网络训练获得。
4、OCR技术是光学字符识别的缩写(Optical Character Recognition),是通过扫描等光学输入方式将各种票据、报刊、书籍、文稿及其它印刷品的文字转化为图像信息,再利用文字识别技术将图像信息转化为可以使用的计算机输入技术。
5、OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,可以运行在Linux、Windows、Android和Mac OS操作系统上。它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。
6、Selenium是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。支持的浏览器包括IE(7, 8, 9, 10, 11),Mozilla Firefox,Safari,Google Chrome,Opera等。这个工具的主要功能包括:测试与浏览器的兼容性——测试你的应用程序看是否能够很好得工作在不同浏览器和操作系统之上。测试系统功能——创建回归测试检验软件功能和用户需求。支持自动录制动作和自动生成 .Net、Java、Perl等不同语言的测试脚本。
7、Keras是一个由Python编写的开源人工神经网络库,可以作为Tensorflow、Microsoft-CNTK和Theano的高阶应用程序接口,进行深度学习模型的设计、调试、评估、应用和可视化。
1、与本发明最相近似的现有技术方案
1.1现有技术的技术方案
现有发票识别系统大多基于OCR技术(Optical Character Recognition,光学字符识别)技术实现;
1.2现有技术的缺点、本发明会解决的问题及达到的效果
用传统纸质发票记账需要手工输入发票信息,容易出错,处理需要消耗大量的人力和时间;
现有发票识别系统大多基于OCR技术(Optical Character Recognition,光学字符识别)技术实现,该方式要识别准确,对待识别的发票影像质量有较高的要求。(必须清晰,完整,无模糊污损等);
2、本发明技术方案的详细阐述(应结合流程图、时序图、原理图等附图进行说明)
上图展示了本发明的整体架构和主要流程:
构成本发明的主要部件:
1.财务机器人客户端:安装在用户PC上的一个客户端程序。主要的功能是:a.驱动或接入影像采集设备并提供给用户一个操作交互的界面,用于采集发票影像;b.上传发票影像到云端服务器;c.展示识别的结果等功能;
2.识别服务模块:部署在云端的服务程序。当客户端上传发票影像后,该模块负责识别发票上的要素字段并将发票要素信息发送给查验服务;
3.查验服务模板:部署在云端的服务程序。主要功能是通过获取到的发票要素信息到发票查验平台获取发票的详细信息;
本发明的详细技术方案要点:
1.用户使用扫描仪、高拍仪、相机、手机等采集发票影像;
2.财务机器人客户端将用户采集的发票影像传到云端服务器,并调用识别服务对发票影像进行识别;
3.识别服务的具体工作流程如下:
a.分析接收到的发票的影像,将其自动调整成横向(水平方向)正面(印章朝上)方向,并将影像压缩或扩展成统一的大小,以方便后续对该影像进行进一步的识别;自动调整发票方向的具体实现方式如下:
1).如果目标影像的高度大于宽度,说明影像是垂直方向展示的,这时将影像进行逆时针90度旋转,使其变为水平方向展示;否则,保持影像不旋转;
2).将影像按照图像的中轴线划分为4个象限;
3).截取所有增值税发票上都共有的特定特征(增值税发票上的“No”部分),然后通过opencv的目标匹配函数matchTemplate匹配到最合适的区域;
4).实践证明,即使是反向的发票,用目标匹配函数matchTemplate依然能够准确命中No图像所在的区域;
5).经观察,一个水平方向正面的增值税发票的发票号码标记应该在第二象限;
因此当匹配的坐标在第二象限时,说明该发票为水平方向正面;
6). 如果匹配的坐标在第三象限时,说明该发票在水平方向反向展示,这时将该影像旋转180度后,图像即可变为水平方向正向展示;
b. 选定一个特征区域截图,作为分类依据;该分类主要区分发票为增值税普通发票还是增值税专用发票;我们选定的特征区域可以定在发票顶部中轴线下方印章处,该区域正好包含了增值税发票中的普通和专用等汉字特征;选取特征区域的Python伪代码如下:
# 选定一个特征区域截图,作为分类依据
left = int(发票宽度/2 + 相对位置偏差)
top = 70
w = MODEL_WIDTH
h = MODEL_HEIGHT
image = imgsr[top:top + h, left:left + w] # 图像裁剪
c.利用事先训练并加载好的机器学习模型,预测选定区域的值,从而实现对发票类别的识别;
d.用发票上的“No”部分,通过opencv的目标匹配函数matchTemplate匹配到最合适的区域,由于发票号码,发票代码两个要素是印刷字,它们对于“No”符号的相对位置是固定的,因此通过定位“No”符号的位置,我们也就找到了发票号码和发票代码在发票上的大概区域;下面是截取发票代码区域部分的Python伪代码:
def getDaimaImage(image_file):
imgsr, (imgsrh1, imgsrw1) = conv_image_horizontal(image_file, HM_TEMPLATE)
# 与模版比对,找到匹配图像的相似度矩阵,imgtm即事先截取的“No”图片模板
res = cv2.matchTemplate(imgsr, imgtm, cv2.TM_CCOEFF_NORMED)
# 从相似度矩阵中抽取最大,最小值,其中最大值即为相似度最高部分的左上角坐标
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
# 获取发票代码的位置:相对于匹配位置,截取宽度为432像素的区域
left = max_loc[0] – 与发票代码第一个字符的相对位置
top = max_loc[1] + 发票代码顶部的相对偏差
w = 发票代码区域宽度
h = 发票代码区域高度
image = imgsr[top:top + h, left:left + w] # 图像裁剪
return image
e.利用事先训练并加载好的机器学习模型,预测识别发票号码和发票代码;
f.由于发票日期,校验码(普票),发票不含税金额是打印在发票上的,因此它们的位置相对于发票并不固定的,因此我们需要使用其它的模板来定位它们的相对位置:
用事先截取的“年”这个汉字作为模板,定位发票日期和发票不含税金额;
用事先截取的“校验码”这几个汉字作为模板,定位发票校验码;
g. 利用事先训练并加载好的机器学习模型,预测识别发票日期和发票校验码(普票)和发票不含税金额(专票);
4.发票的查验需要获取的发票4要素专票和普票有点差别,具体如下:
增值税普通发票:发票号码+发票代码+发票日期+校验码后6位;
增值税专用发票:发票号码+发票代码+发票日期+不含税金额;
5.用识别后的发票4要素调用查验服务获取发票的详细信息;具体流程如下:
a.用Selenium WebDriver打开发票查验平台,填入发票4要素;
b.下载查验平台验证码,并调用事先训练好的验证码识别模型识别验证码;
c.填写验证码;
d.驱动浏览器点击查验按钮后获取返回的发票详细信息;
6.返回发票详细信息给财务机器人;
下面详细介绍一下发票类别,发票号码,发票代码,发票日期,发票校验码,发票不含税金额如何通过卷积神经网络训练出符合识别率要求的模型:
上述的6个模型的训练实现方式大体相似,我们这里只以发票代码为例,描述整个模型训练产生的流程。
1.样本采集;收集大量的增值税发票影像,理论上讲,越多越好;
2.样本要素区域采集:将发票中的发票代码部分的图片从发票影像中裁剪出来,保存至一个样本文件夹Daima,下面的Python伪代码展示了如何将发票代码从发票影像中截取出来并进行保存;
# 加载原路径下的所有图片文件
image_files = glob.glob(os.path.join(发票样本所在文件夹, "*"))
# 遍历每一个原始图片进行处理
for (i, image_file) in enumerate(image_files):
# 获取文件名称和标识部分名称
print("当前处理图像进度: {}/{}".format(i + 1, len(image_files)))
image = getDaimaImage(image_file)
filename = os.path.basename(image_file)
save_path = os.path.join(保存发票代码区域图片的文件夹, filename)
try:
cv2.imwrite(保存发票代码区域图片的文件夹, image)
except:
print("处理图像时发生错误: {}".format(image_file))
其中的getDaiamaImage函数在前面的描述中也已经详细给出了伪代码这里不再赘述;
下图展示了保存发票代码部分图片的文件夹:
3.将发票代码拆分为0-9的单个数值的图片,每一个数值创建一个对应的文件夹来保存;由于组成发票代码每一个数值的字符字体和大小都相同,因此可以采用平均拆分的方式,将上述发票代码图片进行分拆保存,下面是将发票代码图片拆分成单个字符图片的Python伪代码:
# 将一个图片拆分成splitCount个大小相等的图片块
def split_avg_image(image_file,splitCount):
img = cv_imread(image_file)
imgBox = {}
h = int(img.shape[0])
w = int(img.shape[1] / splitCount)
for i in range(0, splitCount): # 遍历所有长度的点
image = img[0:h, w * i:w * i + w] # 图像裁剪
imgBox[i] = image
return imgBox
图片拆分后,我们人工根据拆分出来的每一个图片将其分别归类到0-9对应的文件中如下图,
4.构造神经网络,训练生成模型文件;
构造神经网络的步骤如下:
a. 本发明使用Keras作为构造神经网络的框架;
b. 将前面提到的按单个字符拆分的图片逐个读出,并将其转换为二值化的灰度图;
c. 创建一个与文件夹名称对应的标签数组,使其与二值化的数据对应;
d. 创建一个有三层的卷积神经网络,第一层网络映射上述二值化数列值,图像大小与拆分后的图像大小一致;
e. 创建一个具有最大化池的卷积层;
f. 创建一个含有500节点的隐藏层;
g. 创建一个含有指定数量的输出层;如发票代码,输出层只包含0-9这十个字符,因此输出层的数量为10;
h. 使用Keras 模型框架进行训练;
i. 保存模型;
具体实现的Python伪代码如下:
import cv2
import pickle
import os.path
import numpy as np
from imutils import paths
from sklearn.preprocessing import LabelBinarizer
from sklearn.model_selection import train_test_split
from keras.models import Sequential
from keras.layers.convolutional import Conv2D, MaxPooling2D
from keras.layers.core import Flatten, Dense
# 训练神经网络,并生成训练结果
LETTER_IMAGES_FOLDER = "extracted_letter_images"
MODEL_FILENAME = "model.hdf5"
MODEL_LABELS_FILENAME = "labels.dat"
OUTPUT_CNT = 10 # 输出层数量,即模型支持识别的字符数量
LETTER_WIDTH = 46 # 拆分后的单字符宽度
LETTER_HEIGHT = 66 # 拆分后的单字符高度
# 初始化标签
data = []
labels = []
# 遍历图片
for image_file in paths.list_images(LETTER_IMAGES_FOLDER):
# 将加载的图片转换为灰度图
image = cv2.imread(image_file)
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 创建 Keras 对象
image = np.expand_dims(image, axis=2)
# 按文件夹名称打标
label = image_file.split(os.path.sep)[-2]
# 图片文件数据与标签同下标
data.append(image)
labels.append(label)
# 二维化序列
data = np.array(data, dtype="float") / 255.0
labels = np.array(labels)
# 设置测试数据百分比
(X_train, X_test, Y_train, Y_test) = train_test_split(data, labels, test_size=0.25, random_state=0)
# 将标签矩阵二值化
lb = LabelBinarizer().fit(Y_train)
Y_train = lb.transform(Y_train)
Y_test = lb.transform(Y_test)
# 保存标签数据
with open(MODEL_LABELS_FILENAME, "wb") as f:
pickle.dump(lb, f)
# 创建神经网络
model = Sequential()
# 第一个具有最大池的卷积层
model.add(Conv2D(20, (5, 5), padding="same", input_shape=(LETTER_HEIGHT, LETTER_WIDTH, 1), activation="relu"))
model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))
# 第二个具有最大池的卷积层
model.add(Conv2D(50, (5, 5), padding="same", activation="relu"))
model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))
# 具有500节点的隐藏层
model.add(Flatten())
model.add(Dense(500, activation="relu"))
# 具有X个节点的输出层(每个可能的字母/数值一个)
model.add(Dense(OUTPUT_CNT, activation="softmax"))
model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])
# 训练神经网络
model.fit(X_train, Y_train, validation_data=(X_test, Y_test), batch_size=100, epochs=15, verbose=1)
# 保存模型
model.save(MODEL_FILENAME)
3、 本发明技术方案中的必要技术点及辅助技术点(关键点、创新点、需要保护的点)
3.1必要技术点