ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Keras OCR双模型实战:EAST文字检测与CRNN识别全流程解析

Keras OCR双模型实战:EAST文字检测与CRNN识别全流程解析 简介本资源提供一套基于 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 与 CRNNCTC 的工程落地思路并在此基础上自行调试、修改与扩展功能。1. 从一张发票照片到结构化文本这套 Keras OCR 双模型能解决什么手里有一批扫描件、发票照片、街景截图想把里面的文字抠出来变成可编辑、可检索的文本这是很多做文档处理、票据识别、工业质检的工程师绕不开的需求。开源方案里PaddleOCR 精度高但依赖重Tesseract 对中文和倾斜文字又经常翻车。这套ocr_chinese-master走的是另一条路用 Keras TensorFlow 后端把文字检测和文字识别拆成两个独立模型——EAST/AdvancedEAST 负责在整图里框出文字区域CRNNCTC 负责把框里的内容转成不定长字符串。检测和识别解耦意味着你可以单独替换其中一环比如检测用 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.xfrom keras.layers import ...会直接报模块找不到或者虽然能 import 但Model.predict的行为和 1.x 不一致检测框坐标会整体偏移。常见做法是单独建一个虚拟环境不要和系统里其他项目的 TF 混用。# 建议用 conda 建独立环境避免污染全局 conda create -n ocr_east python3.6 -y conda activate ocr_east # 按 environment.txt 安装注意 tensorflow 和 keras 的版本必须锁死 pip install tensorflow1.14.0 pip install keras2.1.6 pip install numpy1.16.4 opencv-python4.1.0.25 pillow6.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. 加载权重compileFalse 因为只做推理 model east_model(input_shape(None, None, 3)) model.load_weights(east/model/east_weights.h5, by_nameTrue) # 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, axis0) # 4. 前向推理得到 score map 和 geometry map score, geometry model.predict(img_input, batch_size1) # 5. 阈值筛选 文本框合并代码里封装在 detect 函数 boxes detect(score[0], geometry[0], threshold0.9, min_area10)threshold0.9是 score map 的二值化阈值调低会召回更多文字但误检增加调高则漏检小字。min_area10是过滤掉面积过小的噪点框实际业务里如果文字特别小这个值要往下调。by_nameTrue在加载权重时按层名匹配避免因为层顺序微调导致权重错位。推理完boxes里每个元素是四个顶点坐标顺序是左上、右上、右下、左下后面裁剪时按这个顺序做透视变换。3.3 文本框后处理NMS 与坐标还原模型输出的框是在缩放后图像上的坐标必须按原图比例还原。常见做法是记录scale_h h / new_h、scale_w w / new_w把四个顶点分别乘回去。还原之后还要做一次 NMS因为相邻文字区域的 score map 可能连在一起合并时会产出重叠框。def nms_boxes(boxes, scores, iou_thresh0.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_thresh0.3比通用目标检测的 0.5 要低因为文字框通常细长稍微重叠就可能是同一个词被拆成两个框。这个值在密集排版场景下可以再降到 0.2。poly_iou计算的是多边形交并比不是矩形 IoU因为 EAST 输出的是任意四边形直接用矩形 IoU 会算错。4. CRNNCTC 文字识别不定长序列怎么解码4.1 CRNN 的 CNNRNNCTC 三段式结构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_h32, img_w100, nclasslen(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], axis1) 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_w100是训练时的固定宽度推理时如果文字很长直接缩到 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]], dtypenp.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和独立kerasPython 导入时优先命中了错误的那个。解决方式是确认keras.__file__指向的是 site-packages 下的独立包而不是 tensorflow 内部路径。最稳妥的做法是虚拟环境里只装tensorflow1.14.0和keras2.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模型输出维度是 N1blank 的索引是 N 而不是 N-1 或 0。如果你的字符集文件最后一行有空行len(char_set)会多算一个导致 blank 索引偏移。解决方式是读字符集时过滤掉空行并打印len(char_set)和模型输出维度核对。5.4 长文本识别被截断现象是超过一定长度的文字只识别出前半段。原因是 CRNN 训练时固定了输入宽度img_w100推理时把长图硬缩到 100 宽时间步不够覆盖所有字符。解决方式是把长文本框按宽度切成多段每段单独识别再按顺序拼接或者改用支持变长输入的 CRNN 变体。切分时注意在字符间隙处切不要在字符中间切。5.5 权重加载报 shape mismatch现象是load_weights时提示某层权重形状不匹配。原因通常是模型结构定义和权重文件不是同一套比如你改了nclass但权重还是旧的。解决方式是确认crnn_model的nclass参数和字符集大小一致EAST 那边确认输出通道数和权重匹配。如果只是微调了层名可以用by_nameTrue跳过不匹配的层但前提是核心层名没变。6. 进阶技巧把检测和识别拆开调优的实操习惯这套代码最大的价值在于两个模型解耦你可以单独评估每一环的瓶颈。我一般会先固定 CRNN 不动拿一批已经裁好的文字图跑识别统计准确率如果识别没问题但端到端效果差那瓶颈就在 EAST 的检测框质量上。反过来如果检测框很准但识别错就去查字符集覆盖和 CTC 解码参数。一个具体技巧是给 EAST 的 score map 做可视化。把score[0]用cv2.applyColorMap转成热力图叠在原图上能直观看到哪些区域被判定为文字、哪些漏了。如果发现某些字体或颜色漏检可以在预处理阶段做自适应直方图均衡提升对比度后再送模型。另一个技巧是 CRNN 的输入不要直接转灰度有些彩色文字转灰度后和背景混在一起保留三通道反而识别更稳代价是推理稍慢。验证方法上建议自己标一小批测试集检测用 IoU 算召回和精确率识别用编辑距离算字符准确率。不要只看示例图res.jpg的效果就下结论示例图通常是挑过的。我踩过的坑是拿示例图调参数上线后遇到低分辨率扫描件直接崩后来养成习惯每次改完参数都拿业务里最差的那几张图先跑一遍过了再跑全量。从那以后我每次调 OCR 参数都强制走一遍「最差样本优先」的流程省了很多返工。希望帮到你。本文还有配套的精品资源点击获取
返回列表