简介:一套使用Keras与TensorFlow构建的自然场景图像文字检测与识别工程,核心包含EAST/AdvancedEAST文字检测模型和CRNN加CTC不定长文字识别模型。EAST采用目标检测画框方式返回文本框四个坐标,能支持中文、英文、数字、符号混排以及负90度至正90度任意角度文字方向;CRNN负责不定长文字序列识别,通过CTC算法完成对齐输出。项目整体压缩包共32个文件,其中有19个Python脚本、8张测试样例图片、3份Markdown说明文档和2个TXT配置文件,代码按east与crnn两个模块清晰拆分,并附带环境依赖清单,总大小仅957KB。该资源已有164人学习,适合作为毕业设计、课程设计、大作业或工程实训的参考项目;检测与识别脚本能够直接结合样例图运行调试,方便快速理解模型调用流程。对于希望从工程层面掌握文字检测识别落地方法的学习者,可借助模块化代码逐步拆解EAST的文本框回归策略与CRNN的CTC序列解码思路,并在此基础上进行二次开发与功能扩展。
1. 图像文字识别,第一步是让模型知道“字在哪”
拿到一张包含营业执照、收银小票或者路牌的照片,直接让模型去识别“字”,结果往往惨不忍睹。因为识别模型处理的是“裁剪后的文字区域”,它天生不负责搜索文字在图像中的哪一块。所以完整的OCR流水线必须先有一个文本检测模型,把“字所在的多边形或矩形”找出来,再把裁剪后的图像交给识别模型。这个任务就是标题里的EAST和AdvancedEAST的职责,而CRNN+CTC则负责把裁剪区域变成字符串。
这套方案的现实价值在于:EAST推理速度快,能处理任意四边形文本;AdvancedEAST在它基础上解决了弧度文本和长文本的召回问题;CRNN+CTC则是工业界落地最广泛的中文识别方案。Python让这三个模型可以快速串联成一条可调试的管线。如果你已经在接触目标检测或OCR,这篇文章会给你一套从原理到参数都能直接上手的路径。
2. EAST与AdvancedEAST的检测原理和Python环境搭建
2.1 EAST网络结构:从全卷积到Locality-Aware NMS
EAST(Efficient and Accurate Scene Text Detector)的核心思路是用一个类似U-Net的全卷积网络直接回归文本行的得分图和几何信息。它不依赖候选框,没有RPN这类重计算,所以单张图像推理时延很低。常见的实现基于PVANet或ResNet作为backbone,后续通过FPN逐级融合特征,最后输出两个分支:一个分支是每个像素是否为文本区域的score map,另一个分支预测每个像素到文本矩形四条边的距离(RBOX)或到四边形的四个角点的相对偏移(QUAD)。
在Python工程里使用预先训练好的模型,不需要从零训练也能验证效果。但如果你需要理解模型输出尺寸,记住这样一个事实:EAST的输出feature map通常只有输入图像的1/4大小。假设输入图像是batch为1的[1, 3, 512, 512],输出score map是[1, 1, 128, 128],几何图是[1, 5, 128, 128](RBOX)。对每一个像素,如果其score高于阈值,就根据预测的偏移重建出一个旋转矩形。重建完毕后再执行一个叫做Locality-Aware NMS的合并过程。它与传统NMS的区别是:最终生成的文本框数量通常远小于目标检测,所以底层通过按行排序和相邻框传播的方式合并,避免了平方级的IoU计算。
2.2 AdvancedEAST在EAST上的改进:类别平衡与边界回归
AdvancedEAST解决了EAST在长文本和密集排列文本上的几个问题。首先是边界回归形式不同:EAST直接回归距离,而AdvancedEAST回归的是像素到顶边、底边、左边、右边以及角度偏移,然后重建四边形。其次它引入了两个头,一个负责文本区域分割,一个负责边界回归,并且在loss处理上加入了dice loss与boundary loss的权重配比。
更重要的是,AdvancedEAST融合了VGG和U-Net结构,它的输出层会对每个像素预测7个值:1个文本得分、2个顶底边相对位置、4个左右及角度参数。如果你要在Python里复现,需要注意它训练时对正负样本的处理:文本中心区域为正样本,边缘像素会计算额外权重。这样做的直接好处是在弯曲文本和倾斜文本上,召回率比原始EAST高。实际项目中,如果遇到文本行本身是倾斜但排列规整的情况,AdvancedEAST比EAST更容易得到一个可用的四边形坐标。
2.3 Python依赖安装与模型权重准备
在开始写代码前,先把环境准备好。深度学习框架我一般会选PyTorch,因为EAST和AdvancedEAST的开源实现多数基于它,调试也更直观。需要安装的包至少包括torch、torchvision、opencv-python、numpy、shapely。shapely用于计算四边形和矩形的交集面积,这在NMS和评估阶段都会用到。
pip install torch torchvision opencv-python numpy shapely如果是纯CPU环境,请从PyTorch官网选择对应的CPU版本,否则默认安装的CUDA版会在没有GPU的机器上报错或回退到极慢的CPU运算。验证安装是否成功,用下面一段脚本:
import torch import cv2 import shapely print(torch.__version__) print(cv2.__version__) print(shapely.__version__)输出能显示三个版本号就说明环境就绪。模型权重方面,你可以从常见的开源项目里下载预训练文件,或者使用论文作者公开的Google Drive备份。注意不同的后端对应不同的输入尺度和输出定义,下载前务必看清楚源码里的图像归一化方式,否则推理结果会偏差巨大。我自己通常的做法是把权重放在项目的weights/目录下,并同时保存一份对应的配置文件,记录模型输入尺寸、归一化均值和标准差。
3. 用Python实现文本检测的完整流程
3.1 图像预处理与缩放策略
文本检测的输入尺度没有唯一正确答案,但有一个经验性的选择范围。如果图像中文字占比较大,直接缩放到[512, 512]即可。如果图像是长宽比很悬殊的截图,比如网页长截图,直接等比缩放到固定尺寸会让小字完全消失。我一般会先限制最长边为1024,短边相应缩放,不足的部分零填充。这样做既不会丢失太多细节,也保证了batch内张量形状一致。
预处理代码大致如下:
import cv2 import numpy as np def resize_with_pad(image, target_size=1024): h, w = image.shape[:2] scale = target_size / max(h, w) new_w, new_h = int(w * scale), int(h * scale) resized = cv2.resize(image, (new_w, new_h)) padded = np.zeros((target_size, target_size, 3), dtype=np.uint8) padded[:new_h, :new_w] = resized return padded, scale这段代码返回两个值:一个是零填充后的图像,另一个是缩放比例。零填充的方向固定放在右侧和底部,后续坐标映射时需要把偏移量减掉。这里有一个容易踩的坑:cv2.resize默认使用双线性插值,在缩小文本时会出现锯齿。检测模型对锯齿不敏感,但后续识别阶段对清晰度有要求,所以如果同时把预处理结果用于识别,建议缩小高质量场景时改用INTER_AREA:
resized = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_AREA)3.2 模型推理和文本框解码
加载模型后的推理流程可以封装成一个函数。下面以AdvancedEAST风格的输出为例,模型输出一个[batch, 7, H, W]张量,索引0是文本得分,索引1和2是顶边和底边的相对预测,索引3和4是左右边的水平坐标预测,索引5和6是角度预测。
def detect_text(model, image, device, score_threshold=0.8): # 图像已缩放到训练尺寸并归一化 tensor = torch.from_numpy(image).permute(2, 0, 1).unsqueeze(0).float().to(device) with torch.no_grad(): out = model(tensor)[0].cpu().numpy() score_map = out[0] geometry = out[1:] # 根据score_map的像素位置和geometry重建四边形 boxes = [] for y in range(score_map.shape[0]): for x in range(score_map.shape[1]): if score_map[y, x] < score_threshold: continue # 从geometry恢复四边形的四个角点 top = geometry[0, y, x] bottom = geometry[1, y, x] left = geometry[2, y, x] right = geometry[3, y, x] angle = geometry[4, y, x] # 这里省略角度计算细节,实际代码会用cos/sin计算偏移量 boxes.append(restore_box(x, y, top, bottom, left, right, angle)) # 合并重叠框 return merge_boxes(boxes)上面的代码省去了restore_box的内部计算,但它清楚了表达了解码流程:开一个循环遍历每个像素,对大于阈值的像素恢复出一个矩形。针对这类密集循环,我建议先做阈值二值化,再用np.nonzero提取候选像素数组,否则纯Python循环在128×128的feature map上会慢到让人怀疑人生。改写方式:
ys, xs = np.where(score_map >= score_threshold) for x, y in zip(xs, ys): # 恢复四边形 pass这样做以后,推理耗时可以从几百毫秒降低到几十毫秒。注意最终得到的文本框坐标是feature map尺度的,需要先乘以4(或对应的采样倍数)补回原图尺度,再除以之前传来的缩放比例scale,才能映射回原始图像坐标。
3.3 可视化检测结果与坐标输出
检测结果不能只存在于内存里,通常要保存为可视化的图片和JSON供调试。可视化时,我会用OpenCV直接画多边形,注意四边形角点顺序必须一致,否则形状会扭曲。输出JSON时把四个角点的x、y都转为整数,因为后续传给识别模型需要矩形裁剪,而整数像素坐标才能被cv2.getPerspectiveTransform正确处理。
def draw_polygons(image, boxes): result = image.copy() for box in boxes: pts = np.array(box, dtype=np.int32).reshape(-1, 2) cv2.polylines(result, [pts], isClosed=True, color=(0, 0, 255), thickness=2) return result存储到JSON时,需要定义一套可追溯的格式。我通常用:
{ "image_name": "sample.jpg", "text_polygons": [ {"points": [[x1, y1], [x2, y2], [x3, y3], [x4, y4]], "score": 0.95} ] }这里的score就是模型输出的文本得分,它在后续过滤低置信度结果时非常有用。如果你看到某些文本被漏检,处理思路是降低score_threshold到0.3再做一次可视化对比,观察漏检框的得分区间,这种情况往往是模型本身对这个样本的置信度偏低,而不是后处理逻辑出错。
4. CRNN+CTC文字识别模型的训练与推理配置
4.1 CRNN骨干网络:CNN+RNN+CTC的串联
CRNN的核心思想是先用卷积网络提取图像特征,再把特征图按宽度展开成序列,送入双向LSTM学习上下文关系,最后通过CTC损失计算序列和标签的对齐。它的名字已经说明了结构:好像是CNN提取特征,然后经过RNN(双向LSTM),最后用CTC解码。
在Python中实现时,你可以把CRNN看作三部分。第一部分是一个类似VGG的CNN,对输入图像进行下采样,得到[batch, channel, height, width]的特征图,其中height通常压缩到很小水平,比如1或2。第二部分是双向LSTM,输入是按宽度排列的序列,每个时刻对应图像的一部分列。第三部分是线性分类头,将LSTM的输出映射到字符类别数上。整个模型最终的输出是一个[batch, seq_len, num_classes]的张量,其中每个时刻的预测代表当前列可能属于哪个字符。
训练时输入图像的宽度不是固定的,但同一batch内必须相同。所以数据加载器里要按宽高比分组,把宽度接近的图片放在一个batch里。我一般用collate_fn动态找到batch内的最大宽度,其余图像填充随机噪声或零。要特别注意,填充对CTC损失会产生影响:如果填充区域对应的目标是空白符,模型会学到输出blank;如果填充目标设置为其他字符,会导致错误惩罚。正确做法是把填充位置的目标设为-1,表示忽略该位置的loss。
4.2 CTC损失函数的对齐逻辑与超参数
CTC解决的核心问题是:没有逐帧对齐标注,只有字符串级别的转录。假设识别“hello”这几个字符,LSTM输出了20个时间步,CTC允许每个字符重复合并,同时允许插入blank分隔相同字符。比如h h - e e - l - l - o o会先合并重复字符得到h e l l o,再删除blank得到“hello”,因此一条真实标签对应了很多条不同的路径。
PyTorch里的torch.nn.CTCLoss要求输入是[seq_len, batch, num_classes]的log softmax输出,目标序列的长度不能超过输入序列长度。关键超参数有两个:blank索引和零无穷大。blank索引默认为0,所以字符映射表应该把blank放在索引0,之后的索引对应实际字符。zero_infinity参数设为True可以避免某些样本只有空白时产生NaN损失,这对嘈杂场景的鲁棒性很有帮助。
另一个容易出错的点是log_softmax。CTC损失内部需要概率的对数,但torch.nn.CTCLoss并不自己做log_softmax,所以你在网络输出后必须手动加这个操作:
import torch import torch.nn.functional as F log_probs = F.log_softmax(model_out, dim=-1) # [seq_len, batch, num_classes] loss = ctcloss(log_probs, targets, input_lengths, target_lengths)4.3 字符集构建与数据集标注格式
字符集是识别模型的输出维度基础。常见中文识别项目会构建一个包含常用汉字、数字、标点、大写英文字母的字典,字符总数在5000到8000之间。字符集过大会增加模型参数量和训练难度,过小则导致识别结果中频繁出现未知字符。我的做法是先从语料库中统计字频,截取出现次数排前5000的汉字,再加上完整ASCII可见字符,最终形成dict.txt,每行一个字符,行号就是该字符的索引。
对应地,数据集标注格式通常是一行一个样本,路径和标签用制表符分隔:
images/001.png 银行回执单 images/002.png NO. 000234训练时,首先读取标签并把每个字符映射为索引列表。然后把索引列表转成torch.LongTensor。一个需要注意的问题是,某些标注里含有空格。如果字符集里没有空格字符,训练时这一帧的预测永远得不到正确标签,会导致loss居高不下。所以我强烈建议在字符集中包含一个空格或全角空格符号,这能显著改善中文文本中自然空隙带来的序列对齐问题。
4.4 训练脚本关键参数与学习率调整
训练CRNN的batch size受限于GPU显存。对于[1, 32, 320]的图像,在12GB显存下batch size可以设为64,但这是指PyTorch默认的固定尺寸情况。序列长度越长,LSTM中间状态占用的显存也越多,因此动态batch里对最大宽度应有上限。我一般会限制图像宽高比在10:1以内,超过该比例的样本直接缩放变形,或者用无缝拼接的合成数据替代。
下面给出训练循环的核心片段:
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=[10, 20], gamma=0.1) for epoch in range(total_epochs): for batch in train_loader: inputs, labels, input_lengths, target_lengths = batch logits = model(inputs) # [seq_len, batch, num_classes] log_probs = F.log_softmax(logits, dim=-1) loss = ctcloss(log_probs, labels, input_lengths, target_lengths) loss.backward() optimizer.step() scheduler.step()这里的learning_rate初始值设为1e-3,在训练到10个和20个epoch时分别除以10。如果发现loss曲线在刚开始就震荡,很可能是学习率太大,回退到3e-4。另一个常见问题是训练到后期loss不再下降,但精度也上不去。这时查看一下模型对训练集的预测结果,如果训练集准确率已经很高,而验证集低,这就是过拟合,应该增加数据增强或降低模型容量。如果训练集准确率不高,那问题出在序列对齐上,需要检查字符映射表和宽度归一化。
5. 检测与识别串联的工程优化技巧
5.1 检测框到识别区域的裁剪与透视校正
检测模型输出的是四边形,识别模型需要的是矩形或高度归一化的图像。直接把四边形的外接矩形裁出来会让倾斜文本中包含大量背景,干扰识别。更好的是做透视校正:用四边形的四个角点作为目标矩形的四个角点,然后计算变换矩阵,把区域拉正。
def crop_and_warp(image, poly, dst_size=(320, 32)): src = np.array(poly, dtype=np.float32) dst = np.array([[0, 0], [dst_size[0], 0], [dst_size[0], dst_size[1]], [0, dst_size[1]]], dtype=np.float32) matrix = cv2.getPerspectiveTransform(src, dst) return cv2.warpPerspective(image, matrix, dst_size)dst_size的宽度和高度不一定是320和32,但高度建议恒定,宽度按文本串长度比例缩放。如果文本长度超过宽度,字符会被压扁。解决办法是按文本框原始宽高比设定输出宽度,其中高度固定为32,宽度等于32 * (poly_width / poly_height)左右,但还要限制最小宽度和最大宽度。
5.2 提升长文本识别速度的批处理技巧
OCR服务往往要处理一批图片,而每张图片会有多个检测框。简单的做法是循环逐个识别,但这样没有利用GPU并行能力。更快的方式是把所有检测框裁剪、缩放到统一尺寸后作为一个batch输入识别模型。由于广义文本长度不相同,统一尺寸会导致很多空白填充。我通常按文本的宽度分组,把宽度相近的裁剪图像放进同一个batch,每组使用独立的预测宽度。这样的动态batch可以让GPU利用率保持在60%以上。
在识别阶段,解码使用贪心搜索或前缀束搜索。贪心直接取每个时刻最大概率字符,速度最快。束搜索保持多个候选序列,最后选择概率最高的。对于中文文本,束宽度在10左右就能明显减少单字错误率,继续增加作用不大,反而拖慢速度。
5.3 模型量化与导出到ONNX的验证方法
生产环境里如果不想依赖PyTorch运行时,可以把CRNN模型导出到ONNX。导出时有一个条件:模型的输入尺寸必须固定或使用动态轴。推荐使用动态轴,这样同一份模型可以适配不同宽度的文本图像。
import torch.onnx dummy_input = torch.randn(1, 3, 32, 320) torch.onnx.export( model, dummy_input, "crnn.onnx", input_names=["input"], output_names=["logits"], dynamic_axes={"input": {0: "batch", 3: "width"}, "logits": {0: "batch", 1: "seq_len"}} )导出后,用ONNX Runtime加载并比对输出误差。误差通常来自于torch.onnx.export对LSTM内部的循环解析,如果误差超过1e-4,优先检查是否使用了torch.nn.utils.rnn.pack_padded_sequence,该操作在ONNX导出时会发生图结构变化。替代方案是直接把输入padding到固定长度并手动输入sequence length列表,避免使用pack操作。验证完成后可以尝试量化:对于CRNN这类模型,权重从FP32降到INT8后,精度损失通常小于1%,但推理速度能提升2倍以上。量化适应度测试需要拿真实文本图片跑一遍,不能只看loss曲线。
以上从检测到识别再到串联合并的路径,覆盖了工程落地的多数关键点。在你自己的项目里,优先把握检测框质量和识别宽度这两个核心因素,剩下的参数可以按实际数据微调。
本文还有配套的精品资源,点击获取