
简介这份资源面向图像文字检测与识别方向的学习者提供一套基于 Python 的完整实现方案适合作为课程设计、毕业设计或工程实训的参考项目。代码采用 Keras 搭配 TensorFlow 后端编写便于生产环境部署与维护。其中 EAST 模型负责文字检测支持 90° 至 -90° 任意角度方向可覆盖中文、英文、数字及符号以目标检测方式画框定位并返回文本框四角坐标CRNN 模型结合 CTC 算法模块实现不定长文字识别形成检测加识别的完整链路。资源包共 32 个文件以 19 个 py 脚本为核心辅以 8 张 jpg 示例图、3 个 md 说明文档和 2 个 txt 环境配置压缩包约 957KB目录按 east、crnn 等模块划分结构清晰便于按需查阅。目前已有 164 人学习读者可借此理解检测与识别模型的搭建思路、训练流程与推理接口并在此基础上自行调试、修改与扩展功能。1. 从一张发票照片说起EAST 检测加 CRNN 识别到底能跑出什么手里有一张拍歪了的增值税发票或者一张街道路牌的照片你想让程序自动把上面的文字读出来。这件事拆开就是两步先找到文字在哪再把找到的文字认出来。基于 python 实现的图像文字检测模型EAST/AdvancedEAST及包含文字识别模型CRNNCTC讲的正是这条完整链路——EAST 系列负责检测文本框CRNN 加 CTC 负责把不定长的文本序列解码成字符串。这套组合在 OCR 工程里属于经典且容易复现的方案。EAST 全称 Efficient and Accurate Scene Text Detector核心思路是用一个全卷积网络直接回归文本框的几何参数省掉了传统方法里繁琐的候选框生成和后处理。AdvancedEAST 是在 EAST 基础上针对长文本做的改进把回归目标从单点扩展成多点缓解长文本检测断裂的问题。CRNN 则是卷积加循环加 CTC 的结构专门处理不定长序列识别。适合谁适合想自己搭一套 OCR 流水线、又不想一上来就啃大模型推理框架的工程师也适合需要把检测和识别拆开调优的场景。python 环境配好显卡有一块就能往下走。2. EAST 检测头怎么回归文本框从特征图到四边形的完整链路2.1 为什么选 EAST 而不是先做候选框再分类传统文本检测走的是通用目标检测那套先生成大量候选框再分类回归。文本和普通物体不一样长宽比极端一行字可能横跨半张图候选框机制在这种形状上效率很低。EAST 的做法是直接在全卷积特征图上做像素级预测每个像素预测它到文本框四条边的距离外加一个旋转角度。这样一次前向就能得到所有文本框后处理只剩阈值筛选和 NMS。具体来说EAST 的输出通道分三块score map 表示每个像素是不是文本geometry map 表示该像素到上右下左四条边的距离angle map 表示文本框的旋转角。训练时 score map 用交叉熵geometry 用 IoU 或平滑 L1角度用余弦损失。推理时先对 score map 二值化再对每个文本区域内的像素做几何回归最后用局部感知 NMS 合并。AdvancedEAST 的改动在于几何回归的目标。原版 EAST 每个像素只回归到四条边的距离长文本中间像素的回归目标会非常大网络学起来吃力。AdvancedEAST 把长文本拆成多个头尾区域每个区域回归到最近边界的距离相当于把一个大回归问题拆成若干小回归问题。这个改动对长文本检测的召回提升明显代价是后处理合并逻辑复杂一些。2.2 用 python 跑通 EAST 推理的最小命令假设你已经拿到 EAST 的权重文件常见格式是 .h5 或 .pb下面这段代码用 OpenCV 的 dnn 模块加载并推理。选 OpenCV 是因为它不依赖深度学习框架部署时省事。import cv2 import numpy as np # 加载 EAST 模型权重文件路径按实际改 net cv2.dnn.readNet(east_text_detection.pb) # 读图并缩放到 32 的倍数EAST 对输入尺寸有要求 image cv2.imread(invoice.jpg) h, w image.shape[:2] resized cv2.resize(image, (320, 320)) blob cv2.dnn.blobFromImage(resized, 1.0, (320, 320), (123.68, 116.78, 103.94), swapRBTrue, cropFalse) net.setInput(blob) # 输出两层score map 和 geometry map scores, geometry net.forward([feature_fusion/Conv_7/Sigmoid, feature_fusion/concat_3]) # score map 阈值筛选0.5 是常用起点 score_map scores[0, 0] _, score_map cv2.threshold(score_map, 0.5, 255, cv2.THRESH_BINARY) score_map score_map.astype(np.uint8) # 从 geometry map 还原文本框这里省略逐像素回归细节 # 实际工程中会遍历 score_map 非零点取对应 geometry 值算四边形 print(score map shape:, score_map.shape) print(geometry shape:, geometry.shape)这段代码的关键参数有三个。输入尺寸 320 是 EAST 常用的推理分辨率太小会丢小文字太大显存吃紧实际项目里 640 或 960 也常见。均值 (123.68, 116.78, 103.94) 是 ImageNet 的通道均值EAST 训练时用的就是这个推理必须对齐。score 阈值 0.5 是起点发票这类背景干净的图可以调到 0.6 减少误检街景图文字和背景对比弱可能要降到 0.3 保召回。geometry map 的还原逻辑是 EAST 后处理的核心。每个 score 大于阈值的像素取它对应的四个距离值加上当前像素坐标就能算出文本框四个顶点。多个像素会算出重叠的框最后用 NMS 合并。OpenCV 官方示例里有完整的decode函数可以直接拿来用。2.3 AdvancedEAST 的长文本处理差异如果你用的是 AdvancedEAST 权重输出层名字和几何含义会变。它的 geometry map 不是回归到四条边而是回归到文本区域的头尾分割线。后处理时先根据头尾分割线把长文本切成段再把段拼回完整文本框。这个改动让它在长文本上的检测框更贴合不会出现中间断掉的情况。实际选型时短文本为主比如车牌、单行标签用原版 EAST 就够长文本为主发票、合同、路牌优先 AdvancedEAST。两者推理代码结构相似主要差别在后处理合并逻辑。我一般会先用原版跑一遍看效果如果长文本召回明显偏低再换 AdvancedEAST 权重。3. CRNN 加 CTC 怎么把文本框变成字符串序列识别的工程细节3.1 CRNN 的三段结构和 CTC 为什么能省掉对齐标注CRNN 分三段。第一段是卷积层把输入文本图像压成特征序列高度压到 1宽度保留这样每一列对应原图的一个水平片段。第二段是循环层常用双向 LSTM对特征序列做上下文建模让每个时间步的输出包含前后文信息。第三段是转录层把循环层的输出序列映射成字符序列。难点在于文本图像里字符没有逐像素对齐标注你不知道哪个时间步对应哪个字符。CTC 就是解决这个的。它引入一个空白符允许网络在任意时间步输出空白或重复字符解码时把重复字符和空白去掉就得到最终字符串。这样训练时只需要整张图的文本标签不需要每个字符的位置标注成本大幅降低。CTC 解码有两种常用方式。贪心解码每个时间步取概率最大的字符快但可能不是全局最优。束搜索解码保留多个候选路径效果更好但慢。工程里如果对速度敏感贪心解码够用如果字符集大或者图像质量差束搜索值得上。3.2 用 python 加载 CRNN 并做一次识别下面这段代码用 PyTorch 加载 CRNN 权重并推理。字符集按你的训练数据改常见中文字符集有几千类。import torch import cv2 import numpy as np # 字符集索引 0 留给 CTC 空白符 chars 0123456789abcdefghijklmnopqrstuvwxyz char2idx {c: i 1 for i, c in enumerate(chars)} idx2char {i 1: c for i, c in enumerate(chars)} # 加载 CRNN 模型这里假设模型定义已导入 model CRNN(num_classeslen(chars) 1) model.load_state_dict(torch.load(crnn.pth, map_locationcpu)) model.eval() # 文本图像预处理灰度、缩放到固定高度 32宽度按比例 img cv2.imread(text_crop.jpg, cv2.IMREAD_GRAYSCALE) h, w img.shape new_w int(w * 32 / h) img cv2.resize(img, (new_w, 32)) img img.astype(np.float32) / 255.0 img (img - 0.5) / 0.5 tensor torch.from_numpy(img).unsqueeze(0).unsqueeze(0) # 推理并做 CTC 贪心解码 with torch.no_grad(): logits model(tensor) # shape: [T, batch, num_classes] probs logits.softmax(2) preds probs.argmax(2).squeeze(1).numpy() # 去重去空白 result [] prev 0 for p in preds: if p ! 0 and p ! prev: result.append(idx2char.get(p, )) prev p print(识别结果:, .join(result))参数说明几个关键点。输入高度固定 32 是 CRNN 的常见设定宽度按比例缩放太宽的图要分段。归一化用 0.5 均值和 0.5 标准差这是 CRNN 原论文的设定换别的值识别率会掉。CTC 解码时prev记录上一个时间步的输出相同字符只保留一个空白符直接跳过。如果识别结果出现大量重复字符检查 CTC 解码的去重逻辑是不是写错了。如果结果全是空白检查字符集索引有没有对齐空白符必须占一个固定索引。如果识别率整体偏低先确认文本图像有没有做透视矫正歪斜的文本 CRNN 很难认。3.3 检测和识别怎么串起来检测出来的文本框是四边形识别前要做透视变换把它拉正。用 OpenCV 的getPerspectiveTransform和warpPerspective把四边形映射成矩形再送进 CRNN。这一步不做的话倾斜文本的识别率会明显下降。串接顺序是EAST 输出四边形列表按从上到下、从左到右排序逐个做透视变换逐个送 CRNN最后把结果按顺序拼成整图文本。排序逻辑对多行文本很重要不然识别结果顺序会乱。4. 训练自己的数据标注格式、损失函数和几个必调参数4.1 检测和识别的标注格式不一样EAST 训练需要文本框标注常见格式是每张图一个 txt每行是四个顶点坐标加文本内容。AdvancedEAST 的标注多一步要把长文本框按头尾分割线拆成多个短框。CRNN 训练需要文本图像加对应字符串图像从检测框裁剪出来字符串就是框里的文字。标注工具用 labelImg 或 PPOCRLabel 都行导出格式按训练脚本要求改。我一般会写个脚本把标注统一成训练脚本能读的格式避免手工改。4.2 损失函数和参数设置EAST 的损失是三部分加权和score map 用交叉熵geometry 用 IoU 损失或平滑 L1angle 用余弦损失。权重一般设 1:1:1 起步如果检测框位置不准加大 geometry 权重如果漏检多加大 score 权重。CRNN 的损失就是 CTC 损失PyTorch 里用nn.CTCLoss。注意输入长度和标签长度要对齐CTCLoss的input_lengths是时间步数target_lengths是字符串长度。这两个参数搞错会直接报错或训练不收敛。学习率检测用 1e-4 起步识别用 1e-3 起步都用 Adam 优化器。batch size 看显存检测 8 到 16识别 32 到 64。训练轮数检测一般 100 到 200 轮识别 50 到 100 轮早停看验证集损失。4.3 数据增强对识别率的实际影响文本检测和识别都吃数据增强。检测常用随机裁剪、旋转、颜色抖动识别常用随机模糊、亮度对比度调整、轻微透视变换。增强强度别太大文本图像本来就小过度增强会把字符糊掉。我做过对比加增强后 CRNN 在模糊文本上的识别率能提十几个点。但增强参数要调模糊核大小超过 3 就可能把细笔画糊没反而掉点。5. 避坑与排查这套链路最容易翻车的五个地方5.1 检测框坐标还原错位现象画出来的框整体偏移或者框的大小对不上文字。原因EAST 推理时输入图像做了 resizegeometry map 还原出的坐标是 resize 后的坐标系没映射回原图尺寸。解决记录 resize 的缩放比例把四边形顶点坐标除以缩放比例再画。如果做了 padding还要减去 padding 偏移。5.2 CRNN 识别结果全是重复字符现象输出像 aaaaabbbbb 这种去重逻辑没生效。原因CTC 解码时只做了 argmax没做去重和去空白。解决解码循环里加prev判断相同字符跳过空白符索引跳过。检查空白符索引是不是 0有些实现把空白符放在最后一类索引要对齐。5.3 长文本检测断裂现象一行长文字被检测成好几个短框。原因用的是原版 EAST 权重长文本中间像素回归目标太大网络学不好。解决换 AdvancedEAST 权重或者在后处理里加框合并逻辑把水平相邻且高度重叠的框拼起来。5.4 训练损失不下降现象CRNN 训练几个 epoch 后损失卡住不动。原因CTC 的input_lengths设错或者字符集索引和标签不对齐。解决打印一个 batch 的输入长度和标签长度确认时间步数大于标签长度。检查标签里有没有字符集外的字符有的话会被当成未知类。5.5 推理速度慢现象单张图检测加识别要好几秒。原因EAST 输入分辨率设太大或者 CRNN 没做 batch 推理。解决检测分辨率降到 640 或 480识别把多个文本框拼成一个 batch 送网络。如果还慢考虑把 CRNN 的 LSTM 换成 GRU 或者用卷积替代循环层。6. 把检测和识别压进一个可复用的推理脚本前面拆开讲了检测和识别实际用的时候肯定要串成一个脚本。我一般会写一个OCRPipeline类初始化时加载两个模型__call__里先检测再识别返回文本框和文本内容。这样换模型或者调参数只改一个地方。class OCRPipeline: def __init__(self, east_path, crnn_path, chars): self.detector cv2.dnn.readNet(east_path) self.recognizer CRNN(num_classeslen(chars) 1) self.recognizer.load_state_dict(torch.load(crnn_path, map_locationcpu)) self.recognizer.eval() self.chars chars def detect(self, image): # EAST 推理返回四边形列表 # 省略具体实现参考第 2 章代码 return boxes def recognize(self, image, boxes): results [] for box in boxes: # 透视变换拉正 warped self.warp(image, box) text self.crnn_infer(warped) results.append(text) return results def __call__(self, image): boxes self.detect(image) texts self.recognize(image, boxes) return list(zip(boxes, texts))这个类的好处是检测和识别解耦你可以单独换检测模型或者识别模型。比如检测换成 DBNet识别换成 Transformer 解码器只要接口对齐上层调用不用改。验证方法上我习惯准备一个小测试集二十张左右覆盖清晰文本、模糊文本、倾斜文本、长文本四种情况。每次改完参数跑一遍看四类各自的召回和识别率。只看总体指标容易被某一类拖偏。一个具体技巧CRNN 推理时把多个文本框按宽度排序后拼 batch宽度接近的放一起padding 浪费少速度能提一截。这个改动不大但实际部署时省的时间很可观。这套方案我前后调了几个月最大的教训是别一上来就追求端到端。检测和识别分开调每步的指标都能单独看出问题好定位。等两边都稳了再串起来返工次数少很多。希望帮到你。本文还有配套的精品资源点击获取