☰
深度学习手写文字OCR识别系统:银行支票与进账单结构化处理实战
2026/9/29 7:36:35 网站建设 项目流程

简介:本资源是一套基于深度学习自主训练开发的手写文字OCR识别系统,面向金融、教育、医疗等需要处理非结构化手写信息的行业开发者与算法学习者。系统覆盖通用场景手写文字识别、银行支票OCR、银行进账单OCR,并支持机打与手写文字混合识别,提供文字检测、文字识别与结构化处理完整链路。压缩包共46个文件,约157.13MB,以pyd编译模块、py源码、pb模型文件、jpg测试图片、md说明文档及xlsx配置表为主,另含ttc字体与docx附赠资料,目录按detect、rec、structure、service等模块划分,便于按功能检索。已有99人学习下载。读者可获取从模型推理到字段结构化落地的完整工程代码,理解支票、进账单等场景的字段定位与分类逻辑,并借助测试样例与配置表快速复现和二次开发。

1. 手写文字OCR识别系统:从通用场景到银行票据,一套模型怎么吃下三种活

银行柜面每天要处理大量进账单和支票,手写金额、日期、账号混着机打文字,传统OCR模板匹配一遇到手写体就崩。这个标题讲的就是用深度学习自主训练一套手写文字OCR识别系统,覆盖通用场景手写识别、银行支票OCR、银行进账单OCR,支持机打和手写混合识别,并且把文字检测、文字识别、结构化处理串成完整链路。适合谁?做金融票据自动化的后端工程师、想用OCR识别python方案落地毕设的学生、以及需要本地化部署OCR识别软件的技术团队。核心痛点不是“能不能识别”,而是“手写体+机打混排+固定模板票据”三件事同时发生时,检测框和识别结果怎么对齐、结构化字段怎么稳定抽取。下面按我实际做过的路径,从数据构造到模型训练再到结构化输出,把可复现的步骤和参数讲清楚。

2. 文字检测与识别模型选型:为什么我最终选了DBNet+CRNN而不是端到端

2.1 检测和识别拆开做的三个现实理由

常见做法是直接上端到端模型,比如TrOCR或者Donut,输入图像直接输出文本。但在银行票据场景里,我踩过的坑是:端到端模型对长文本行和密集小字的手写体召回率不够稳定,而且一旦识别错,你没法定位是检测框偏了还是识别模型认错了。拆成检测+识别两阶段,检测用DBNet,识别用CRNN+CTC,好处是每个阶段可以独立调参、独立换模型、独立做数据增强。

DBNet的核心是可微分二值化,它把分割概率图经过一个可学习的阈值图,得到二值化结果。相比EAST和PSENet,DBNet在弯曲文本和手写连笔上的鲁棒性更好,推理速度也够用。CRNN是CNN+RNN+CTC的经典结构,对不定长文本序列友好,训练数据不需要字符级标注,只需要文本行图像和对应字符串。

选型时还要考虑部署环境。如果目标机器是NPU电脑部署深度学习环境,DBNet和CRNN都有成熟的ONNX导出路径,量化到INT8后单张票据推理能压到200ms以内。如果直接用PyTorch服务端跑,batch size设4到8,GPU利用率比较均衡。

2.2 用PaddleOCR快速搭出检测+识别基线

我一般会先用PaddleOCR跑一个基线,确认数据质量和标注格式没问题,再决定要不要自己从头训。下面这段代码是在本地装好PaddleOCR后,对一张银行进账单做检测+识别的完整调用。

from paddleocr import PaddleOCR # 初始化:det_model_dir和rec_model_dir可以换成自己训练的模型 ocr = PaddleOCR( use_angle_cls=True, # 方向分类器,票据可能倒置 lang='ch', # 中英文混合 det_model_dir='./models/det_db', rec_model_dir='./models/rec_crnn', cls_model_dir='./models/cls', use_gpu=True, drop_score=0.5 # 低于0.5的识别结果丢弃 ) img_path = 'bank_statement_001.jpg' result = ocr.ocr(img_path, cls=True) # result结构:[[[box, (text, score)], ...]] for line in result[0]: box = line[0] # 四点坐标 text, score = line[1] print(f'坐标:{box} 文本:{text} 置信度:{score:.3f}')

逻辑说明:use_angle_cls=True对银行票据很重要,柜面扫描件经常有180度倒置。drop_score=0.5是经验值,低于这个分数的结果大概率是手写连笔被误检,宁可漏也不要错。det_model_dir和rec_model_dir如果不指定,会用PaddleOCR自带的通用模型,但通用模型对银行票据的手写金额识别率大概只有60%到70%,必须用自己的数据微调。

参数怎么改:如果票据图像分辨率高,比如300dpi扫描件,把det_limit_side_len设到960或1280,默认是960。如果显存不够,降到640,但小字检测会掉点。rec_batch_num控制识别阶段的batch,GPU上设6到10,CPU上设1。

2.3 自己训练检测模型时的数据标注格式

DBNet训练需要标注文件,每行格式是图像路径\t[{"transcription": "文本", "points": [[x1,y1],[x2,y2],[x3,y3],[x4,y4]]}]。手写文字识别场景下,标注框要贴紧文字边缘,不要留太多空白,否则检测框会偏大,影响后续CRNN的输入裁剪。

我一般用LabelImg或者PPOCRLabel做标注,导出后写一个转换脚本统一成上述格式。注意:银行支票OCR识别里,金额栏的手写数字经常有连笔和粘连,标注时要把每个数字单独框出来还是整行框?我的经验是整行框,让CRNN去学序列关系,单独框反而丢失上下文。

import json def convert_to_dbnet_format(label_lines, output_path): with open(output_path, 'w', encoding='utf-8') as f: for img_path, annotations in label_lines: # annotations: list of dict with transcription and points line = f'{img_path}\t{json.dumps(annotations, ensure_ascii=False)}' f.write(line + '\n') # 示例:单张图两个文本框 sample = [ ('imgs/check_001.jpg', [ {'transcription': '20240115', 'points': [[100,200],[300,200],[300,240],[100,240]]}, {'transcription': '壹万贰仟元整', 'points': [[100,300],[500,300],[500,350],[100,350]]} ]) ] convert_to_dbnet_format(sample, 'train_label.txt')

这段脚本把标注转成DBNet可读的格式。points是四个点,顺序是左上、右上、右下、左下。transcription是文本内容,机打和手写混排时,同一个框里可能既有印刷体又有手写体,标注时按实际内容写,不要分开。

3. 银行支票与进账单的结构化处理:从文本行到字段的映射

3.1 固定模板票据的字段定位策略

银行支票OCR识别和银行进账单OCR识别都属于固定模板票据,但“固定”是相对的。不同银行的进账单布局有差异,同一银行不同年份的版本也可能微调。我一般用两种策略结合:先做模板匹配定位大区域,再用文字检测框的相对位置做字段归类。

具体做法:对票据图像做透视校正后,用预先定义的ROI区域裁剪出“日期”、“金额”、“账号”、“收款人”等区域。每个区域内的检测框按x坐标排序,拼接成该字段的文本。如果某个区域检测不到框,回退到全图检测,再用最近邻规则匹配到字段。

import cv2 import numpy as np def perspective_correct(img, src_points): # src_points: 票据四个角点,顺序左上、右上、右下、左下 dst_points = np.float32([[0,0],[800,0],[800,400],[0,400]]) M = cv2.getPerspectiveTransform(np.float32(src_points), dst_points) return cv2.warpPerspective(img, M, (800, 400)) # 定义字段ROI(基于校正后800x400画布) field_rois = { 'date': (50, 30, 250, 70), 'amount': (300, 30, 750, 70), 'payee': (50, 100, 400, 140), 'account': (450, 100, 750, 140), } def extract_fields(ocr_result, rois): fields = {} for name, (x1, y1, x2, y2) in rois.items(): texts = [] for line in ocr_result[0]: box = line[0] cx = sum(p[0] for p in box) / 4 cy = sum(p[1] for p in box) / 4 if x1 <= cx <= x2 and y1 <= cy <= y2: texts.append(line[1][0]) fields[name] = ''.join(texts) return fields

逻辑说明:perspective_correct把票据拉正,避免扫描倾斜导致ROI偏移。extract_fields用检测框中心点判断属于哪个字段,比用框的左上角更稳,因为手写文字框可能大小不一。field_rois的坐标需要根据实际票据调整,我一般会拿20张不同来源的票据做统计,取字段区域的最小外接矩形再留10%余量。

参数注意:如果票据是机打和手写混合识别,同一个字段里可能有机打前缀和手写内容,比如“金额:壹万”,拼接时不要加空格,后续用正则清洗。

3.2 手写金额的中文大写转换与校验

银行支票OCR识别里,手写金额经常是中文大写,比如“壹万贰仟叁佰元整”。识别出来之后要做两件事:转成数字金额,和进账单上的小写金额做交叉校验。如果两者不一致,标记为异常件转人工。

import re CN_NUM = {'零':0,'壹':1,'贰':2,'叁':3,'肆':4,'伍':5,'陆':6,'柒':7,'捌':8,'玖':9} CN_UNIT = {'拾':10,'佰':100,'仟':1000,'万':10000,'亿':100000000} def cn_amount_to_number(cn_str): cn_str = cn_str.replace('元整','').replace('元','').replace('整','') total = 0 section = 0 number = 0 for ch in cn_str: if ch in CN_NUM: number = CN_NUM[ch] elif ch in CN_UNIT: unit = CN_UNIT[ch] if unit >= 10000: section = (section + number) * unit total += section section = 0 else: section += number * unit number = 0 return total + section + number # 测试 print(cn_amount_to_number('壹万贰仟叁佰元整')) # 12300

这段代码处理了“万”和“亿”的节权位。注意:手写体识别结果可能有错字,比如“贰”识别成“貳”,需要在预处理里做异体字归一化。我一般维护一个映射表,把常见异体字转成标准字。

校验逻辑:小写金额从进账单的机打区域提取,用正则\d+\.?\d*匹配。如果大写转数字和小写金额差值超过0.01,标记异常。这个规则在银行进账单OCR识别里能拦下大部分手写识别错误。

3.3 结构化输出的JSON Schema设计

结构化处理最后要输出统一JSON,方便下游系统消费。我一般定义这样的schema:

{ "doc_type": "bank_statement", "image_id": "stmt_001.jpg", "fields": { "date": {"value": "2024-01-15", "confidence": 0.98}, "amount_cn": {"value": "壹万贰仟叁佰元整", "confidence": 0.95}, "amount_num": {"value": 12300.00, "confidence": 0.99}, "payee": {"value": "张三", "confidence": 0.92}, "account": {"value": "6222020200112233445", "confidence": 0.97} }, "raw_lines": [ {"text": "日期 2024年1月15日", "box": [[50,30],[250,30],[250,70],[50,70]], "score": 0.98} ], "anomalies": [] }

confidence取该字段所有检测框识别置信度的最小值,这样保守一些。anomalies记录校验失败的字段,比如大小写金额不一致。raw_lines保留原始检测结果,方便排查。

4. 避坑与排查:手写OCR训练和部署中翻车的五个场景

4.1 损失不下降,检测框全糊在一起

现象:DBNet训练几个epoch后loss在0.5左右震荡,推理时检测框把整行文字框成一个大框。原因:学习率太大,或者标注框之间重叠严重。解决:把初始学习率从0.01降到0.001,用warmup前500步。检查标注文件,如果两个框的IoU超过0.3,合并或重新标注。

4.2 手写数字“1”和“7”混淆,识别结果不稳定

现象:CRNN对银行支票上的手写数字识别,1和7经常互换,0和6也偶尔错。原因:训练数据里这两个数字的样本不均衡,或者图像分辨率太低。解决:对1和7做针对性数据增强,加随机旋转±5度、随机缩放0.9到1.1。把输入高度从32提到48,宽度保持320。如果还不行,在CTC解码后加一个规则后处理:金额字段里如果出现“7”但上下文是日期,优先判为“1”。

4.3 机打和手写混排时,识别结果串行

现象:一行里前半段是机打“金额:”,后半段是手写“壹万”,识别出来变成“金额壹万”或者“金额:壹万”丢字。原因:检测阶段把机打和手写分成两个框,但识别阶段按框裁剪后,CRNN对短文本的上下文建模不足。解决:检测后处理时,如果两个框的垂直重叠超过70%且水平间距小于20像素,合并成一个框再送识别。合并后的文本行让CRNN一次识别,CTC能学到“:”和手写数字的边界。

4.4 部署到NPU电脑后推理速度慢十倍

现象:PyTorch GPU上单张200ms,转到NPU电脑部署深度学习环境后变成2秒。原因:模型没有量化,或者NPU只支持特定算子。解决:用ONNX导出后做INT8量化,校准集用500张票据图像。检查DBNet的可变形卷积是否被NPU支持,如果不支持,换成普通卷积,精度掉1%到2%但速度能回来。CRNN的LSTM层如果NPU不支持,换成GRU或者用CNN替代。

4.5 结构化字段错位,日期跑到金额栏

现象:进账单OCR识别结果里,日期字段提取到了金额数字。原因:ROI区域定义时没有考虑票据版本差异,或者透视校正的角点检测偏了。解决:不要硬编码ROI,改成相对坐标。先检测票据的表格线,用表格线交点做锚点,再按比例定位字段。如果表格线检测不到,用文字检测框的聚类结果做动态ROI:把所有框按y坐标聚类,取最上面一行做日期,第二行做金额。

5. 进阶技巧:用少量标注数据微调出可用的手写识别模型

如果你手头只有几百张标注票据,从头训DBNet+CRNN不现实。我一般用预训练模型做微调,检测用PaddleOCR的ch_PP-OCRv4_det预训练权重,识别用ch_PP-OCRv4_rec,冻结骨干网络,只训neck和head。学习率设0.0001,batch size设8,训20到30个epoch。数据增强用RandAugment,但不要用Cutout,会把手写连笔切断。

验证方法:留出50张票据做测试集,算字段级准确率,不是字符级。字段级准确率=完全正确的字段数/总字段数。我做过的一个进账单项目,微调前字段准确率62%,微调后到89%。提升主要来自手写金额和日期字段。

还有一个技巧:用LLM做后处理校验。把OCR识别出的文本行和字段值拼成prompt,让LLM判断“金额大写和小写是否一致”、“日期格式是否合法”。但注意LLM是否属于深度学习这个讨论不影响落地,它就是个规则增强。我一般用本地部署的小模型做校验,延迟控制在50ms以内。

最后说个血泪经验:不要指望一个模型吃下所有银行票据。我一般按银行分模型,每个银行训一个检测+识别模型,共享骨干网络,只微调head。这样单银行数据量要求降到200张,准确率还能到92%以上。部署时用模型路由,先分类票据来源,再走对应模型。这个方案比追求一个通用大模型现实得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询