
简介这份资源是一套基于深度学习自主训练开发的手写文字OCR识别系统面向需要处理非结构化手写信息的开发者与行业用户覆盖通用场景手写识别、银行支票与进账单识别并支持机打与手写文字混合识别提供文字检测、文字识别与结构化处理全流程能力。压缩包共46个文件约157.13MB以pyd编译模块、py源码、pb模型文件、jpg测试图片、md说明文档及xlsx配置表为主另含ttc字体与docx附赠资料目录按detect、rec、structure、service等模块划分便于按功能检索与二次开发。目前已有99人学习下载。读者可获取完整可运行的OCR工程结构、预训练模型与测试样例理解文字检测、识别到结构化输出的实现思路并参考银行支票、进账单等场景的字段分类与处理逻辑快速搭建或迁移到自身业务中。1. 从一张支票说起这套手写 OCR 系统到底能扛什么活上个月帮一个做票据自动化的朋友看现场他们最头疼的不是识别率而是同一张进账单上既有印刷体的金额栏又有手写出票人签名和日期传统 OCR 引擎一遇到手写就整行崩掉要么把「壹」认成「壶」要么直接把签名区域跳过。这套基于深度学习自主训练开发的手写文字 OCR 识别系统核心就是冲着这种混合场景去的——它不是调一个现成 API 就完事而是把文字检测、文字识别、结构化处理三段串成了一条可自己重训的链路。资源包里覆盖了通用场景手写识别、银行支票 OCR、银行进账单 OCR 三个方向支持机打和手写文字混排。适合两类人一是做金融票据、表单录入的工程师需要把非结构化图像转成字段级 JSON二是想拿 OCR 当深度学习落地练手的人因为检测、识别、后处理三个模块拆得比较清楚改起来不玄学。下面按「先跑通再调优」的顺序拆。2. 文字检测与识别链路从图像到文本框的完整拆解2.1 检测和识别为什么必须分开做很多人第一反应是端到端一个模型出结果但票据场景里字段位置固定、字体差异大端到端模型训练时梯度会被背景噪声带偏。这套系统采用检测 识别两阶段检测负责找出文字区域并给出四点坐标识别负责把每个区域转成字符序列。好处是检测模型可以单独用通用文本数据预训练识别模型再针对手写体微调两个模块的迭代互不干扰。检测部分常见做法是 DBDifferentiable Binarization类算法输出一张概率图加一张阈值图二值化后得到文本区域掩码再通过轮廓提取拿到包围框。识别部分用 CRNN 或 SVTR 结构CNN 提特征、序列建模、CTC 解码。这套资源没有在正文里写死具体 backbone但工程上我一般会先确认检测模型输出的是四点框还是水平矩形框——票据有倾斜时四点框更稳。2.2 环境配置与依赖安装先确认 Python 版本建议 3.8 到 3.10太新的版本某些推理库轮子还没跟上。# 创建独立环境避免和系统里的 opencv 冲突 conda create -n handwritten_ocr python3.9 -y conda activate handwritten_ocr # 安装推理和图像处理基础依赖 pip install opencv-python4.8.1.78 pip install numpy1.24.3 pip install pillow10.1.0 pip install shapely2.0.2 pip install pyclipper1.3.0.post5 # 深度学习框架按自己显卡选这里以 PyTorch 为例 pip install torch2.1.0 torchvision0.16.0 --index-url https://download.pytorch.org/whl/cu118逻辑说明opencv-python锁 4.8 是因为部分旧版cv2.dnn对 ONNX 算子支持不全pyclipper用于检测后处理里的多边形偏移缺了它文本框会贴得太紧导致识别丢边。参数上shapely版本别低于 2.01.x 的 API 在计算框重叠时有差异。2.3 检测推理把一张票据变成文本框列表import cv2 import numpy as np def detect_text_boxes(image_path, det_model, input_size960, thresh0.3, box_thresh0.6): det_model: 已加载的检测模型接受 NCHW 输入输出概率图 input_size: 长边缩放到该尺寸票据建议 960 以上太小会丢小字 thresh: 概率图二值化阈值手写淡墨可降到 0.2 box_thresh: 框置信度过滤低于该值的框丢弃 img cv2.imread(image_path) h, w img.shape[:2] scale input_size / max(h, w) resized cv2.resize(img, (int(w * scale), int(h * scale))) # 归一化和维度变换均值和方差按训练时配置来 blob resized.astype(np.float32) / 255.0 blob (blob - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225] blob blob.transpose(2, 0, 1)[np.newaxis, ...].astype(np.float32) prob_map det_model(blob)[0, 0] # 取概率图 binary (prob_map thresh).astype(np.uint8) * 255 contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) boxes [] for cnt in contours: # 还原到原图坐标 cnt cnt.astype(np.float32) / scale x, y, bw, bh cv2.boundingRect(cnt) if bw 3 or bh 3: continue # 过滤噪点框 boxes.append([x, y, x bw, y bh]) return boxes, img逻辑说明缩放比例scale必须同时作用于宽高否则框会错位thresh和box_thresh是两个不同阶段的阈值前者控制像素级二值化后者控制框级过滤调参时先动thresh。手写票据常见问题是笔画细导致概率图峰值低把thresh从 0.3 降到 0.2 通常能召回更多但会引入背景噪点需要靠box_thresh兜。2.4 识别推理文本框到字符串def recognize_text(img, boxes, rec_model, rec_image_shape(3, 48, 320)): rec_model: 识别模型输入固定高度 48宽度按比例缩放 返回: [(box, text, score), ...] results [] for box in boxes: x1, y1, x2, y2 box crop img[y1:y2, x1:x2] if crop.size 0: continue # 统一缩放到模型输入高度 ch, cw crop.shape[:2] ratio rec_image_shape[1] / ch new_w min(int(cw * ratio), rec_image_shape[2]) resized cv2.resize(crop, (new_w, rec_image_shape[1])) # 补白到固定宽度 pad np.zeros((rec_image_shape[1], rec_image_shape[2], 3), dtypenp.uint8) pad[:, :new_w] resized blob pad.astype(np.float32) / 255.0 blob (blob - 0.5) / 0.5 blob blob.transpose(2, 0, 1)[np.newaxis, ...].astype(np.float32) pred rec_model(blob) text, score decode_ctc(pred) # CTC 解码去掉 blank 和重复字符 results.append((box, text, score)) return results逻辑说明识别模型输入高度固定 48 是 CRNN 类结构的常见约定宽度 320 是最大宽度超过会截断所以检测框太宽时要先做行切分。decode_ctc需要自己实现或从工具库引入核心是取 argmax 后合并重复、去 blank。score是字符概率均值低于 0.5 的字段建议进人工复核队列。3. 银行支票与进账单的结构化处理字段怎么对齐3.1 结构化不是正则匹配那么简单拿到识别文本后通用场景可以直接输出文本行但支票和进账单必须落到字段出票日期、金额大小写、收款人、账号。常见做法是模板锚点 相对位置先检测固定印刷字段如「出票日期」四个字作为锚点再根据锚点坐标推算手写区域的位置范围落在范围内的识别结果归到对应字段。纯正则的问题在于手写数字「0」和「O」、「1」和「l」混淆正则一崩全崩。3.2 字段映射配置示例# 字段映射配置锚点文本 - 目标字段及偏移范围 FIELD_MAP { 出票日期: { field: issue_date, offset: [0, 0, 300, 60], # 相对锚点框的 x1,y1,x2,y2 偏移 post: date_normalize # 后处理函数名 }, 收款人: { field: payee, offset: [0, 0, 400, 60], post: None }, 金额: { field: amount, offset: [0, 0, 350, 60], post: amount_normalize } } def structure_fields(ocr_results, field_map): ocr_results: [(box, text, score), ...] structured {} anchors {} for box, text, score in ocr_results: for key in field_map: if key in text: anchors[key] box for key, anchor_box in anchors.items(): cfg field_map[key] ax1, ay1, ax2, ay2 anchor_box ox1, oy1, ox2, oy2 cfg[offset] target_zone [ax1 ox1, ay1 oy1, ax1 ox2, ay1 oy2] # 找落在目标区域内的识别框 for box, text, score in ocr_results: cx (box[0] box[2]) / 2 cy (box[1] box[3]) / 2 if target_zone[0] cx target_zone[2] and target_zone[1] cy target_zone[3]: structured[cfg[field]] {text: text, score: score} return structured逻辑说明offset的四个值是以锚点框左上角为原点的相对偏移不同银行票据版式不同这个配置需要按实际扫描件调。post指向后处理函数比如日期要把「二〇二四年」转成「2024」金额要把「壹佰贰拾万」转成数字。锚点匹配用in而不是因为识别结果可能带噪。3.3 机打手写混合时的置信度分流混合场景下机打文字识别置信度通常 0.95 以上手写体在 0.6 到 0.9 之间波动。我一般设两条线score 0.9直接入库0.5 score 0.9标记待复核 0.5直接丢弃并记录原图坐标。这样既不让低质手写污染数据也不至于把能用的结果扔掉。字段类型典型置信度区间处理策略机打印刷体0.95 - 0.99直接入库手写数字0.70 - 0.92高于 0.9 入库否则复核手写汉字0.55 - 0.88统一进复核队列签名区域0.30 - 0.60不做识别仅保留位置4. 避坑与排查训练和推理里最容易翻车的五件事4.1 检测框把相邻两行粘成一行现象识别结果里出现「日期2024年3月」和上一行「出票」连在一起。原因检测模型在文本行间距小的区域概率图连成一片轮廓提取时合并了。解决后处理里加一步基于高度的切分如果框高度超过平均行高 1.8 倍按水平投影找波谷切开或者训练时增加行间距小的样本。4.2 手写「0」和「O」识别混淆现象账号字段里数字和字母乱跳。原因识别模型训练集里手写数字样本不足且没有做字符集约束。解决在 CTC 解码后加一层字段级字符集过滤账号字段只允许数字遇到字母直接映射或标记低置信。更彻底的做法是微调时把该字段的字符集限制进 loss。4.3 图像预处理过度导致笔画断裂现象二值化后手写淡墨笔画断成几截检测不到完整框。原因用了全局固定阈值做二值化。解决改用自适应阈值或直接不二值化让检测模型吃原图归一化后的结果。这套链路里检测模型本身就是在灰度或 RGB 上训练的额外二值化反而丢信息。4.4 模型加载后推理结果全空现象检测输出概率图全接近 0。原因预处理均值和方差与训练时不一致常见于自己换了 backbone 但没改归一化参数。解决确认训练配置里的mean和stdRGB 和 BGR 顺序也要对齐OpenCV 读进来是 BGR很多训练脚本用 RGB。4.5 结构化字段错位到隔壁区域现象收款人识别成了账号。原因锚点检测到了但偏移量是按旧版票据调的换了一家银行的票就偏了。解决偏移量做成配置文件按票据类型加载别硬编码上线前用一批新票做回归统计字段命中率。5. 进阶技巧用置信度回传做半自动标注闭环跑通推理只是第一步真正让这套系统在业务里活下来的是识别结果反哺训练数据。我的习惯是每次推理后把低置信度样本连同原图裁剪、检测框坐标一起落盘攒到一定量后人工只做修正不做标注修正结果直接转成训练格式。这样标注成本能压到纯人工的三成左右。具体做法是在识别函数里加一个落盘分支import os, json, hashlib def dump_low_confidence(img, box, text, score, out_dirhard_samples, threshold0.75): 低置信样本落盘供后续人工修正和增量训练 if score threshold: return x1, y1, x2, y2 [int(v) for v in box] crop img[max(0, y1):y2, max(0, x1):x2] if crop.size 0: return uid hashlib.md5(f{x1}{y1}{x2}{y2}{text}.encode()).hexdigest()[:12] os.makedirs(out_dir, exist_okTrue) cv2.imwrite(os.path.join(out_dir, f{uid}.jpg), crop) with open(os.path.join(out_dir, f{uid}.json), w, encodingutf-8) as f: json.dump({box: [x1, y1, x2, y2], pred: text, score: float(score)}, f, ensure_asciiFalse)逻辑说明threshold设 0.75 是经验值太低会攒一堆废图太高起不到补充难例的作用。uid用坐标和预测文本做哈希避免同一位置重复落盘。人工修正时只需要打开 json 改pred字段后续写个转换脚本就能生成识别模型的训练标签。验证这套闭环有没有效果看两个指标一是低置信样本占比逐月下降二是复核队列里人工修正率下降。如果修正率一直不降说明模型没学到新东西要检查落盘样本和训练集是否分布一致。从那以后我每次部署 OCR 链路都强制先把低置信回传通道打通再谈调参否则模型永远在原地打转。希望帮到你。本文还有配套的精品资源点击获取