简介:本资源提供一套基于 Python 与 Keras 的自然场景图像文字检测与识别完整实现,面向希望入门或进阶 OCR 技术的学习者,可作为毕业设计、课程设计、大作业或工程实训的参考项目。方案采用 EAST 模型完成文字检测,支持 90° 至 -90° 任意角度方向,覆盖中英文、数字与符号,以目标检测方式输出文本框四角坐标;识别环节使用 CRNN 模型配合 CTC 算法,实现不定长文字序列的端到端识别,整体基于 Keras 与 TensorFlow 后端,便于生产环境部署与维护。压缩包共 32 个文件,以 19 个 py 脚本为核心,辅以 8 张 jpg 示例图、3 个 md 说明文档与 2 个 txt 环境配置,整体约 957KB,目录按 east、crnn、data、net、model、test 等模块划分,结构清晰。目前已有 164 人学习浏览。读者可从中获取检测与识别两阶段的模型代码、预测脚本及环境依赖清单,理解 EAST 与 CRNN+CTC 的工程落地思路,并在此基础上自行调试、修改与扩展功能。
1. 从一张发票照片到结构化文本:这套 Keras OCR 双模型能解决什么
手里有一批扫描件、发票照片、街景截图,想把里面的文字抠出来变成可编辑、可检索的文本,这是很多做文档处理、票据识别、工业质检的工程师绕不开的需求。开源方案里,PaddleOCR 精度高但依赖重,Tesseract 对中文和倾斜文字又经常翻车。这套ocr_chinese-master走的是另一条路:用 Keras + TensorFlow 后端,把文字检测和文字识别拆成两个独立模型——EAST/AdvancedEAST 负责在整图里框出文字区域,CRNN+CTC 负责把框里的内容转成不定长字符串。检测和识别解耦,意味着你可以单独替换其中一环,比如检测用 EAST、识别换成别的 CRNN 变体,调试和迭代都更灵活。
它适合谁?做毕设、课程设计、工程实训的学生,能直接拿到一套跑得通的端到端流程;做初期项目立项的工程师,可以拿它当 baseline,先验证业务可行性再决定要不要上更重的方案。代码全部基于 Keras,生产环境部署时模型文件小、推理链路清晰,不像某些框架需要额外装一堆算子库。需要说明的是,这份资源定位是参考资料,不是开箱即用的产品,你得能看懂代码、会自己调报错、能按业务改后处理逻辑。下面从环境搭建一路讲到两个模型的推理细节和踩坑记录。
2. 环境搭建与目录结构:把 TensorFlow 1.14 + Keras 2.1.6 跑起来
2.1 为什么锁定 tensorflow 1.14.0 + keras 2.1.6
这套代码用的是 Keras 独立包而不是tf.keras,这是关键。TensorFlow 1.14 是最后一个支持 Python 3.6 且与独立 Keras 2.1.6 配合稳定的版本组合。如果你图省事装了 TF 2.x,from keras.layers import ...会直接报模块找不到,或者虽然能 import 但Model.predict的行为和 1.x 不一致,检测框坐标会整体偏移。常见做法是单独建一个虚拟环境,不要和系统里其他项目的 TF 混用。
# 建议用 conda 建独立环境,避免污染全局 conda create -n ocr_east python=3.6 -y conda activate ocr_east # 按 environment.txt 安装,注意 tensorflow 和 keras 的版本必须锁死 pip install tensorflow==1.14.0 pip install keras==2.1.6 pip install numpy==1.16.4 opencv-python==4.1.0.25 pillow==6.1.0这里numpy锁 1.16.4 是因为 1.17 以后部分 API 和 TF 1.14 编译时的 ABI 不兼容,会报numpy.dtype size changed这类玄学错误。opencv-python用 4.1.0.25 是因为 EAST 的预处理里用到了cv2.dnn相关接口,版本太新反而会提示某些 blob 函数签名变化。装完之后跑一句python -c "import tensorflow, keras; print(tensorflow.__version__, keras.__version__)",确认输出是1.14.0 2.1.6再往下走。
2.2 目录结构与两个模型的入口文件
解压ocr_chinese-master.zip后,顶层是ocr_chinese-master,里面分east和crnn两个大目录,各自带net、model、data、test子目录。east下的predict_east.py是检测入口,crnn下的predict_crnn.py是识别入口,根目录的predict.py是把两者串起来的端到端脚本。asset目录里放了几张示例图,23_re.jpg、51_re.jpg是检测结果可视化,23_0_.jpg、51_0_.jpg是原图,res.jpg是识别输出示意。
| 路径 | 作用 | 是否需改动 |
|---|---|---|
east/predict_east.py | 加载 EAST 权重,输出文本框四坐标 | 改图片路径和权重路径 |
crnn/predict_crnn.py | 加载 CRNN 权重,对裁剪图做 CTC 解码 | 改输入图和字符集路径 |
predict.py | 检测+识别串联 | 改输入图路径即可 |
environment.txt | 依赖清单 | 一般不改 |
asset/ | 示例图与结果图 | 可替换为自己的图 |
net目录下是模型结构定义,model目录下是权重文件。注意权重文件通常较大,解压后先确认model目录里确实有.h5文件,有些压缩包为了体积会把权重单独放,缺权重的话load_weights会直接报文件不存在。
3. EAST 文字检测:从任意角度文本框到四坐标输出
3.1 EAST 的像素级预测原理与 AdvancedEAST 的改进点
EAST 的核心思路不是传统目标检测那样先出候选框再回归,而是对每个像素预测它是否属于文字区域,同时回归该像素到文本框四条边的距离。这样做的直接好处是能处理 90° 到 -90° 之间的任意倾斜文字,中英文、数字、符号混排也不受影响。原版 EAST 输出一个 score map 加四个距离图(上、下、左、右),后处理时按阈值筛出文字像素,再把这些像素按几何关系合并成完整文本框。
AdvancedEAST 的改进主要在两点:一是把四边距离回归改成对顶点坐标的回归,减少长文本在合并时的断裂;二是对 score map 做了更细的监督,小文字和密集文字区域的召回率更高。这套代码里east/net下的模型结构就是按 AdvancedEAST 搭的,输出层通道数和原版有区别,所以你不能拿原版 EAST 的权重直接加载,必须用配套的.h5。
3.2 检测推理的完整步骤与参数含义
# east/predict_east.py 核心逻辑(简化后) import cv2 import numpy as np from east.net.model import east_model # 模型结构 # 1. 加载权重,compile=False 因为只做推理 model = east_model(input_shape=(None, None, 3)) model.load_weights('east/model/east_weights.h5', by_name=True) # 2. 读图并做尺寸对齐,EAST 要求边长是 32 的倍数 img = cv2.imread('asset/23_0_.jpg') h, w = img.shape[:2] new_h = (h // 32) * 32 new_w = (w // 32) * 32 img_resized = cv2.resize(img, (new_w, new_h)) # 3. 归一化,注意这里减的是 ImageNet 均值,不是 0.5 mean = np.array([123.68, 116.78, 103.94]) img_norm = (img_resized - mean) / 255.0 img_input = np.expand_dims(img_norm, axis=0) # 4. 前向推理,得到 score map 和 geometry map score, geometry = model.predict(img_input, batch_size=1) # 5. 阈值筛选 + 文本框合并(代码里封装在 detect 函数) boxes = detect(score[0], geometry[0], threshold=0.9, min_area=10)threshold=0.9是 score map 的二值化阈值,调低会召回更多文字但误检增加,调高则漏检小字。min_area=10是过滤掉面积过小的噪点框,实际业务里如果文字特别小,这个值要往下调。by_name=True在加载权重时按层名匹配,避免因为层顺序微调导致权重错位。推理完boxes里每个元素是四个顶点坐标,顺序是左上、右上、右下、左下,后面裁剪时按这个顺序做透视变换。
3.3 文本框后处理:NMS 与坐标还原
模型输出的框是在缩放后图像上的坐标,必须按原图比例还原。常见做法是记录scale_h = h / new_h、scale_w = w / new_w,把四个顶点分别乘回去。还原之后还要做一次 NMS,因为相邻文字区域的 score map 可能连在一起,合并时会产出重叠框。
def nms_boxes(boxes, scores, iou_thresh=0.3): # boxes: N x 4 x 2 的顶点坐标 # 按 score 降序,逐个保留并抑制 IoU 超阈值的框 order = scores.argsort()[::-1] keep = [] while order.size > 0: i = order[0] keep.append(i) ious = [poly_iou(boxes[i], boxes[j]) for j in order[1:]] inds = np.where(np.array(ious) <= iou_thresh)[0] order = order[inds + 1] return keepiou_thresh=0.3比通用目标检测的 0.5 要低,因为文字框通常细长,稍微重叠就可能是同一个词被拆成两个框。这个值在密集排版场景下可以再降到 0.2。poly_iou计算的是多边形交并比,不是矩形 IoU,因为 EAST 输出的是任意四边形,直接用矩形 IoU 会算错。
4. CRNN+CTC 文字识别:不定长序列怎么解码
4.1 CRNN 的 CNN+RNN+CTC 三段式结构
CRNN 把识别拆成三步:CNN 提特征,RNN 建模序列依赖,CTC 解决对齐问题。CNN 部分通常是几层卷积加池化,把输入图压成高度为 1 的特征图,宽度方向保留序列信息。RNN 部分用双向 LSTM,每个时间步输出一个字符概率分布。CTC 的作用是让网络在不知道每个字符具体位置的情况下也能训练,解码时把重复字符和空白符合并,得到最终字符串。
这套代码里crnn/net下的结构,CNN 用的是 VGG 风格的堆叠,RNN 是两层双向 LSTM,输出维度等于字符集大小加 1(那个 1 是 CTC 的 blank)。字符集文件通常在crnn/data下,里面按行列出所有支持的字符,顺序不能乱,因为模型输出的索引直接对应这个顺序。
4.2 识别推理与 CTC 解码的两种方式
# crnn/predict_crnn.py 核心逻辑(简化后) from crnn.net.model import crnn_model import numpy as np # 1. 加载模型和字符集 model = crnn_model(img_h=32, img_w=100, nclass=len(char_set) + 1) model.load_weights('crnn/model/crnn_weights.h5') # 2. 输入图统一缩放到 32 高,宽度按比例,但不超过 100 img = cv2.imread('crop.jpg', cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, (100, 32)) img = img.astype(np.float32) / 255.0 img = np.expand_dims(img, axis=(0, -1)) # 3. 前向得到每个时间步的 softmax preds = model.predict(img) # shape: (1, T, nclass) # 4. 贪心解码:每个时间步取 argmax,去重去 blank def greedy_decode(preds, char_set): indices = np.argmax(preds[0], axis=1) result = [] prev = -1 for idx in indices: if idx != prev and idx != len(char_set): # blank 索引 result.append(char_set[idx]) prev = idx return ''.join(result)贪心解码够用但精度有上限,因为每个时间步独立取最大,没有考虑序列整体最优。如果识别结果经常出现相邻字符混淆,可以换成 beam search 解码,保留 top-k 候选路径再选总分最高的。img_w=100是训练时的固定宽度,推理时如果文字很长,直接缩到 100 会丢细节,常见做法是把长文本按比例切成多段分别识别再拼接。
4.3 检测与识别串联:裁剪、透视变换与批量推理
端到端脚本predict.py做的事就是先跑 EAST 拿到框,再对每个框做透视变换裁出水平文字图,送进 CRNN。透视变换这步不能省,因为 EAST 输出的框是倾斜的,直接按外接矩形裁会把背景带进去,识别精度下降明显。
def crop_by_box(img, box): # box: 四个顶点,顺序左上、右上、右下、左下 w = int(max(np.linalg.norm(box[0]-box[1]), np.linalg.norm(box[2]-box[3]))) h = int(max(np.linalg.norm(box[0]-box[3]), np.linalg.norm(box[1]-box[2]))) dst = np.array([[0,0],[w,0],[w,h],[0,h]], dtype=np.float32) M = cv2.getPerspectiveTransform(box.astype(np.float32), dst) return cv2.warpPerspective(img, M, (w, h))裁完之后统一转灰度、缩放到 32 高,再逐个送 CRNN。如果框很多,可以攒成 batch 一起推理,但要注意不同框的宽度不一样,得先 pad 到同一宽度,否则model.predict会因为 shape 不一致报错。
5. 避坑与排查:这套代码最容易翻车的五个地方
5.1 报No module named 'keras'或 import 后行为异常
现象是明明pip list里有 keras,运行却提示找不到,或者能 import 但predict结果和预期差很远。原因通常是环境里同时装了tensorflow自带的tf.keras和独立keras,Python 导入时优先命中了错误的那个。解决方式是确认keras.__file__指向的是 site-packages 下的独立包,而不是 tensorflow 内部路径。最稳妥的做法是虚拟环境里只装tensorflow==1.14.0和keras==2.1.6,不要装 TF 2.x。
5.2 检测框坐标整体偏移或框到背景上
现象是 EAST 输出的框位置对不上原图文字,整体往一个方向偏。原因是预处理时 resize 的尺寸不是 32 的倍数,或者归一化均值用错了。EAST 要求输入边长是 32 的倍数,因为网络里有多次下采样,尺寸不对齐会导致特征图和原图坐标映射错位。解决方式是 resize 前先算new_h = (h // 32) * 32,并且用 ImageNet 均值[123.68, 116.78, 103.94]而不是 0.5。
5.3 CRNN 识别结果全是重复字符或空
现象是识别输出像啊啊啊啊或者直接空字符串。原因一般是 CTC 解码时 blank 索引判断错了。字符集大小是 N,模型输出维度是 N+1,blank 的索引是 N 而不是 N-1 或 0。如果你的字符集文件最后一行有空行,len(char_set)会多算一个,导致 blank 索引偏移。解决方式是读字符集时过滤掉空行,并打印len(char_set)和模型输出维度核对。
5.4 长文本识别被截断
现象是超过一定长度的文字只识别出前半段。原因是 CRNN 训练时固定了输入宽度img_w=100,推理时把长图硬缩到 100 宽,时间步不够覆盖所有字符。解决方式是把长文本框按宽度切成多段,每段单独识别再按顺序拼接,或者改用支持变长输入的 CRNN 变体。切分时注意在字符间隙处切,不要在字符中间切。
5.5 权重加载报 shape mismatch
现象是load_weights时提示某层权重形状不匹配。原因通常是模型结构定义和权重文件不是同一套,比如你改了nclass但权重还是旧的。解决方式是确认crnn_model的nclass参数和字符集大小一致,EAST 那边确认输出通道数和权重匹配。如果只是微调了层名,可以用by_name=True跳过不匹配的层,但前提是核心层名没变。
6. 进阶技巧:把检测和识别拆开调优的实操习惯
这套代码最大的价值在于两个模型解耦,你可以单独评估每一环的瓶颈。我一般会先固定 CRNN 不动,拿一批已经裁好的文字图跑识别,统计准确率;如果识别没问题但端到端效果差,那瓶颈就在 EAST 的检测框质量上。反过来,如果检测框很准但识别错,就去查字符集覆盖和 CTC 解码参数。
一个具体技巧是给 EAST 的 score map 做可视化。把score[0]用cv2.applyColorMap转成热力图叠在原图上,能直观看到哪些区域被判定为文字、哪些漏了。如果发现某些字体或颜色漏检,可以在预处理阶段做自适应直方图均衡,提升对比度后再送模型。另一个技巧是 CRNN 的输入不要直接转灰度,有些彩色文字转灰度后和背景混在一起,保留三通道反而识别更稳,代价是推理稍慢。
验证方法上,建议自己标一小批测试集,检测用 IoU 算召回和精确率,识别用编辑距离算字符准确率。不要只看示例图res.jpg的效果就下结论,示例图通常是挑过的。我踩过的坑是拿示例图调参数,上线后遇到低分辨率扫描件直接崩,后来养成习惯:每次改完参数,都拿业务里最差的那几张图先跑一遍,过了再跑全量。从那以后我每次调 OCR 参数都强制走一遍「最差样本优先」的流程,省了很多返工。希望帮到你。
本文还有配套的精品资源,点击获取