简介:这套基于Python实现的图像文字检测与识别工程,以Keras和TensorFlow搭建,整合EAST/AdvancedEAST文字检测模型与CRNN+CTC文字识别模型。适合希望学习OCR技术的小白及进阶学习者,也可用于毕业设计、课程设计、工程实训或初期项目立项,帮助理解自然场景下的文字定位与识别流程。压缩包共32个文件,包含19个Python源码、8张样例图片、3份说明文档和2个环境配置文件,整体仅957KB,模块划分清楚,便于按需查看。EAST模型可检测90°到-90°间任意角度的中英文、数字及符号,采用目标检测框形式返回坐标;CRNN模型联合CTC算法支持不定长文字识别,覆盖数据、网络、模型、测试等常用模块;环境配置文本可帮助快速准备运行依赖。已有164人学习浏览,适合作为参考资料进行调试、排错和二次开发。
1. 自然场景文字检测与识别:这份Keras资源到底能干什么
做了几年OCR相关的落地项目,我越来越认同一个判断:自然场景下的文字检测和识别,从来不是"调个库"就能解决的问题。光线、倾斜、遮挡、模糊,任何一样都能让传统的图像处理方案当场翻车。这份基于Python实现的资源,用Keras + TensorFlow组合实现了完整的"检测 + 识别"两段式方案——EAST/AdvancedEAST负责检测,CRNN+CTC负责识别,代码全是Python,依赖清晰,适合做毕设、课程设计、工程实训的起点。和那些"开箱即用"的黑盒OCR不同,它把模型结构、预测脚本、测试图片全部摊开放在你面前。你拿到的不是成品,是完整可改的实验框架,前提是你得会看代码、会调参。
2. 环境搭建与工程结构:Python 3.6 + TensorFlow 1.14 的兼容性细节
2.1 environment.txt 与版本组合的底层逻辑
这个资源的开发环境写得很明确:Python 3.6 + TensorFlow 1.14.0 + Keras 2.1.6。这三个版本号不是随手写的,它们之间存在强绑定关系。Keras 2.1.6发布于2018年左右,走的是"独立Keras + 后端TensorFlow"路线,和后来内置在TensorFlow 2.x里的tf.keras完全是两种生态。TF 1.x时代用的是静态图 + Session.run()模式,而很多从Python 3.8以后才入行的开发者,可能连tf.Session长什么样都没见过。
先看依赖安装,项目里带了environment.txt,按官方README的说法:
pip install -r environment.txt我的建议是不要直接在全局环境里执行,先建虚拟环境。常见做法是先用conda把Python版本锁死:
conda create -n ocr_env python=3.6 conda activate ocr_env pip install -r environment.txt这里有一个必须提前说明的坑:这个环境组合在Python 3.6上才能稳定跑通。TensorFlow 1.14官方支持的是Python 3.4到3.7,Python 3.8及以上版本直接装不上1.14.0轮子。你要是机器上只有Python 3.10,第一步就卡住了。我一般会先确认python --version,再决定是装3.6还是用Docker拉一份旧镜像。资源正文里出现的environment.txt在压缩包里是真实存在的,装完依赖后可以用pip list | grep tensorflow核对版本,不是1.14开头就说明装错环境了。
还有一点:如果你想在这个环境里用OpenCV读图,注意装的是opencv-python而不是opencv-contrib-python,两个包的API在基础用法上差别不大,但contrib版本体积更大、且会和某些预编译的TensorFlow轮子产生依赖冲突。我在复现时用的是opencv-python==4.5.4.60,这个版本在Python 3.6下还能正常装。
2.2 工程目录:east、crnn、asset各管什么
解压开ocr_chinese-master.zip之后,你能看到一个分得很清楚的结构。整个项目不复杂,但目录划分是典型的"检测/识别独立成包"模式。把文件列表整理成这张表:
| 目录/文件 | 职责 |
|---|---|
| east/ | 文字检测子模块,含data、net、model、test、predict_east.py |
| crnn/ | 文字识别子模块,含data、net、model、test、predict_crnn.py |
| predict.py | 主入口脚本,串联检测与识别 |
| environment.txt | pip依赖清单 |
| asset/ | 测试图片,如23_re.jpg、51_re.jpg、res.jpg |
| README.md | 项目说明 |
east目录下的net和model一般分别放网络结构定义和权重加载逻辑,test目录里是单元测试或调试脚本。crnn目录结构类似。asset里的23_re.jpg、51_re.jpg是原始测试图,51_0_.jpg、23_0_.jpg是检测后的裁剪图,res.jpg是最终可视化结果,我拿到后会先看这几张图——能看出项目作者预期的输出长什么样,比先读代码高效得多。
2.3 跑通predict.py:从测试图到标注框
环境装好后,第一步不是改代码,而是先用测试图跑一遍完整流程。一般来说,项目根目录下直接执行:
python predict.py --image_path asset/23_re.jpg这个命令的含义是:用默认权重对23_re.jpg执行完整的"检测→识别"流程。如果你在Windows环境,注意路径分隔符;如果报找不到模型文件,多半是权重路径写死了相对路径。这时打开predict.py看一眼,把模型加载部分改成os.path.join(os.path.dirname(__file__), 'east/model/xxx.h5')这种形式,避免从其他目录启动时报错。
跑通之后,你的屏幕上应该会打印检测框坐标和识别出的文字,或者在asset/下生成一张带框的结果图。这一步的意义在于验证环境,不代表项目没问题——很多人在这一步能跑通,但换成自己的图片就开始出各种幺蛾子,后面的章节会把典型的坑一个个拆开。
3. EAST/AdvancedEAST文字检测:四坐标框输出与角度回归
3.1 EAST网络结构:U型特征融合与RBOX输出
EAST(Efficient and Accurate Scene Text Detector)解决的核心问题是:在自然场景下,文字可能以任意角度出现,普通的目标检测矩形框装不下倾斜文本。EAST的思路很直接——不搞候选区域,不做文本框修正,直接做像素级预测,输出文本框的几何信息。
网络主干是一个类似U-Net的结构,通过卷积层逐级提取特征,再在多个尺度的特征图上做融合,输出的特征图最后分成两条分支:一条是分类分支(Score Map),判断每个像素是否属于文本框内部;另一条是几何分支(Geometry Map),预测文本框的位置和角度。在RBOX模式下,一个文本区域会输出4个距离值(上、右、下、左到文本框边界的距离)加上1个旋转角度,一共5个通道。这也是EAST的资源文件里预测结果经常是"文本框四个顶点坐标"的原因——从RBOX解码时,把中心点和距离、角度换算回四角坐标就行了。
实际在predict_east.py里,加载模型后输入一张图,中间会做一次resize。EAST默认的输入尺寸常见是320×320或512×512,代码里通常用cv2.resize把长边缩到固定值,保持宽高比。这里有个细节:如果你的测试图长宽比很极端(比如超宽横幅),直接resize会导致文字被压扁。我一般会改成按长边等比例缩放,做完检测后再把坐标映射回原图尺寸。
3.2 AdvancedEAST:角度回归的稳定性改进
AdvancedEAST在原始EAST基础上做了几个改进,最核心的一条是把角度回归的表示方式调整了。原始EAST用单个角度值来代表文本框旋转角,但在接近90°和接近-90°的边界区域,角度值会发生跳变,导致模型训练不稳定。AdvancedEAST改用两个正交距离或改用四个距离值配合更平滑的角度编码,来缓解这个问题。
这份资源标题里写的是"EAST/AdvancedEAST",说明两个版本都能在当前代码里切换。你可以在east/net目录下找网络定义文件里的advanced开关,或者在predict_east.py里看到两个分支。我的建议是直接默认用AdvancedEAST分支,它在倾斜文本上的鲁棒性更好,尤其是处理中文招牌、广告条幅这类"歪着写"的文字时。
一个直观的经验:用原始EAST分支识别45°左右倾斜文字,框往往贴不紧文本;切到AdvancedEAST后,框的四个顶点能更精准地贴合文字四角。损失函数的差别主要在几何分支的平滑项上。用这个项目做毕设时,"AdvancedEAST对倾斜文本的回归优化"本身就是一个很好的论文切入点。
3.3 predict_east.py:调用方式与坐标输出说明
east模块的推理入口在east/predict_east.py。看文件名就知道,它独立于识别模块,可以单独跑。调用方式通常是:
python east/predict_east.py --img asset/51_re.jpg如果没有传参逻辑,你也可以在脚本内部改成直接调用predict函数。我复现时的习惯是写一个小的封装:
# 参考结构,实际以代码为准 import cv2 from east.model import EASTModel model = EASTModel(weight_path='east/model/weights.h5') img = cv2.imread('asset/51_re.jpg') # 假设模型提供 predict 方法,返回文本框坐标和置信度 boxes, scores = model.predict(img, threshold=0.5) for box in boxes: # box 是4个顶点的 (x, y) 集合 print(box)threshold是置信度阈值,决定哪些候选框会保留下来。这个参数在自然场景下非常敏感:调太高(如0.9)容易漏检,调太低(如0.1)会多出一堆背景误检。我一般从0.5起步,根据测试图的效果上下浮动。坐标输出的格式,EAST系列通常是[左上, 右上, 右下, 左下]四个点的顺序,但不同实现可能不同,拿到输出后建议先打印第一个框确认顶点顺序,再画框验证。
画框验证这一步至关重要。用cv2.polylines就能画,但如果你直接用cv2.rectangle画外接正矩形,就把EAST的角度信息丢了,看不出效果。正确的画法是用cv2.polylines把四边形的四条边连起来。
4. CRNN+CTC文字识别:不定长文本序列解码的完整链路
4.1 CRNN三件套:CNN特征 → BiLSTM → CTC
EAST把文字区域框出来了,接下来要做的就是把框里的内容转成字符串。CRNN(Convolutional Recurrent Neural Network)是这里的选择,它的结构可以拆成三段理解。
第一段是卷积层,相当于视觉特征抽取器,把输入图片转成一系列特征列。图片被竖直切分成多个"片",每片对应一个时间步的特征。第二段是双向LSTM,对特征序列做上下文建模。第三段是CTC损失/解码层,负责解决"特征序列和文本序列长度不对齐"的问题。这套组合在自然场景OCR里是经典方案,字符识别只是它能力的子集。
由于这是不定长文本识别,没有预先定义文本长度,所以训练时用的是CTC Loss而不是普通的交叉熵。CTC的核心思想是引入一个"空白符"(blank token),允许模型在每帧上独立预测,然后在解码阶段用合并规则把重复字符和blank去掉,恢复出原始文本。
4.2 CTC解码:pred_idx到文本的映射过程
CTC解码是识别模型里最容易让人困惑的部分。模型输出的是一串帧级预测,每帧预测的是"某个字符的概率分布",但相邻帧可能预测同一个字符,CTC的处理规则是:
- 先把相邻帧中重复的字符合并
- 再把blank位置删除
举个例子,模型输出的字符序列是['空', '中', '中', '文', '空', '文', '个', '个'],其中"空"表示blank。先去重合并相邻项,得到['空', '中', '文', '空', '文', '个'],再删除blank,最终结果是"中文文个"。注意,如果序列里同一个字符被blank隔开,它们不能合并,这是一步关键逻辑。
predict_crnn.py里,解码部分一般长这样:
# 伪代码:CTC贪心解码 def ctc_decode(pred_seq): # pred_seq 形状为 [time_steps, num_classes] result = [] prev = None for frame in pred_seq: char_idx = frame.argmax(axis=-1) # 每一帧取概率最大的字符 if char_idx == blank_index: # blank 跳过 prev = None continue if char_idx != prev: # 相邻去重 result.append(char_idx) prev = char_idx return ''.join([index_to_char[i] for i in result])这段代码的argmax是贪心解码,简单直接,适合做流程验证。如果要更高精度,需要用beam search,但在这个资源里贪心够用。参数方面,blank_index一般设为0或最后一个类别索引,具体看字符集文件的定义顺序。
我用过一个更稳妥的调试方法:把模型输出的pred_seq维度打印出来,time_steps应该和输入图像的宽度正相关,如果time_steps太小(比如小于文本长度),那多半是输入图像在宽度方向被压得太狠,信息丢了。
4.3 检测+识别串联:predict.py的完整工作流
项目根目录的predict.py是真正的入口,它把east和crnn串了起来。流程不复杂:先加载EAST模型,读入原图,得到文本框坐标;再对每个框做透视变换,把倾斜框拉成水平矩形;然后把裁剪后的矩形区域送入CRNN识别,得到文本;最后把文本字符串和坐标画回原图。
这里有一个常见错误认知:很多人以为识别模型的输入是原始检测框的像素截图,模型负责"智能地"处理倾斜。实际上,CRNN的输入必须是水平方向排列的文本行,因为它的特征提取方式是按列切分的。所以倾斜的检测框一定要先做仿射变换转正,这一步通常在predict.py里完成。
串联时的关键参数有两个:一个是检测阶段的threshold,决定框的数量;另一个是识别阶段的max_text_length,决定识别序列最大长度。如果检测框漏了,识别再好也没用;如果max_text_length设太小,长句子会被截断。我在复现时一般把max_text_length设为32,遇到套餐名、地址这种一长串的中文,再往上调。
5. 避坑指南:环境、模型、图片预处理三个方向的踩坑记录
5.1 现象:TensorFlow 2.x环境直接跑代码,报AttributeError: module 'tensorflow' has no attribute 'Session'
原因:代码里大量使用tf.Session()、tf.placeholder等TF 1.x API,TensorFlow 2.0移除了这些接口。很多人在新环境里装了个tensorflow 2.x,跑项目秒挂。
解决:严格按resources里要求的版本来。创建Python 3.6环境,pip install tensorflow==1.14.0 keras==2.1.6。如果你必须在TF 2.x下运行,可以在程序开头加:
import tensorflow.compat.v1 as tf tf.disable_v2_behavior()但这只是"能跑"层面的应急方案,模型权重的兼容性、Keras后端绑定都可能出问题。我的建议是直接用旧环境,不要跟版本较劲。
5.2 现象:检测框角度明显不对,水平文字框成了斜框,或者斜体文字框成了正框
原因:EAST对角度回归的敏感度受输入图片resize方式影响大。直接cv2.resize改变了文本的长宽比,模型在训练时见过的是等比缩放后的特征分布,一旦比例失配,角度预测就偏。
解决:修改检测脚本,按长边等比缩放,记录缩放比例,检测完再把坐标还原。伪代码:
h, w = img.shape[:2] scale = 512.0 / max(h, w) resized = cv2.resize(img, (int(w * scale), int(h *scale))) boxes = model.predict(resized) boxes /= scale # 坐标映射回原图5.3 现象:CRNN识别结果全是空白,或者输出了一堆不相关的符号
原因:输入图像的高度不满足CRNN的卷积下采样要求。CRNN经过卷积层后,特征图高度会被压缩到一个很小的值(比如原本32像素高变成1像素高),如果输入图高度不足16像素,特征高度直接归零,模型输出全是无效帧。
解决:在送入CRNN之前,强制把文本行图片高度resize到32像素,宽度按比例缩放。此外,检查检测框是否把文字裁得太紧,留出的边距不够。我一般会在裁剪矩形上下各多切4个像素。
5.4 现象:中文字符识别准确率低,常见字都错
原因:模型权重是在特定字符集上训练的,如果项目里的字符集文件(通常是label.txt或char_dict.json)覆盖不全,生僻字、特殊符号就会映射到错误的索引位置。还有一个常见原因:图片没有做灰度预处理,CRNN的输入通道数不符合训练时的设定。
解决:先确认识别模型训练时的输入格式,是单通道灰度还是三通道RGB。然后查看字符集长度,和模型输出层的类别数是否匹配。字符集不足时,要么换权重,要么用现有权重在自己的数据上做微调。
5.5 现象:运行中内存持续增长,批量预测时直接OOM
原因:EAST和CRNN两个模型同时常驻内存,再加上TensorFlow 1.x的图模式会缓存中间结果,批量处理时图片叠堆,很容易把显存或内存吃满。
解决:限制TensorFlow的显存占用,按需增长而不是一次性占满:
import tensorflow as tf config = tf.ConfigProto() config.gpu_options.allow_growth = True sess = tf.Session(config=config)当然很多时候你根本不是在GPU上跑——CPU版本不会显存OOM,但会在RAM上爆。这时候把一次处理的图片数量降下来,每次只处理一张图,并且调用完及时del大数组。这是最朴素也最管用的方案。
6. 进阶使用:批量推理、自定义字符集与性能调优
最后说点能直接抄的进阶操作。我拿到项目后的第二个习惯,是把它的单张测试脚本改造成目录级批量推理,这样一整批测试图就能一次跑完。常见做法是加一段遍历逻辑:
import os import cv2 img_dir = 'asset' for name in os.listdir(img_dir): if not name.endswith(('.jpg', '.png')): continue path = os.path.join(img_dir, name) img = cv2.imread(path) boxes, texts = ocr_pipeline(img) print(name, texts)这里的ocr_pipeline是封装好的"检测+透视变换+识别"函数。如果原项目只提供了单张入口,你就需要自己写这个封装,正好把前面章节讲到的串联逻辑练一遍。
自定义字符集是很多人忽略的一个点。crnn/data下的字符集文件决定了模型能识别哪些字,如果你做的是中文项目,但默认字符集只有几千个常用字,地名、人名里的生僻字就会全部变成"口"或"?"。换字符集之后,模型输出层的类别数会改变,必须重新训练或微调才能生效,不要指望直接换文件就能用。
性能调优方面,如果纯CPU上跑觉得慢,优先检查是不是每次预测都在重复加载权重。正确做法是进程启动时加载一次模型,后续对所有图片复用同一个模型实例。另外,把图片缩放参数统一起来,避免每张图都触发重新计算scale逻辑。这些小改动合在一起,吞吐量能提升好几倍。
这套项目做完,我最大的一个习惯变化是:拿到OCR资源后,先看环境版本,再逐行读预测脚本,最后才跑测试图。顺序反过来的话,报错会让你误以为模型有问题,其实只是环境不匹配。血泪经验换来的教训。希望帮到你。
本文还有配套的精品资源,点击获取