多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

自然场景文字检测与端到端OCR:TensorFlow、Keras、PyTorch实战毕业设计

自然场景文字检测与端到端OCR:TensorFlow、Keras、PyTorch实战毕业设计 简介这份毕业设计资源面向计算机视觉方向的学生与开发者聚焦自然场景下的中文文字检测与端到端OCR识别提供基于TensorFlow、Keras与PyTorch的完整实现方案。包内共237个文件以91个Python源码、50个编译缓存、38张JPG与19张PNG测试图片为主另含Shell部署脚本、Cython与CUDA加速代码及模型权重文件压缩包约62.71MB。项目包含三个核心网络VGG16文本方向分类、CTPN文本区域检测以及CRNN端到端识别支持不定长中文识别并提供Keras与PyTorch双版本训练代码PyTorch版更稳定。资源附带Ubuntu环境构建脚本与依赖安装说明可一键部署CPU或GPU环境运行demo即可查看检测框与识别结果。目前已有697人学习下载适合作为毕业设计参考或OCR入门实践素材。1. 自然场景文字检测加端到端 OCR这套毕业设计到底在做什么自然场景下的文字检测与端到端 OCR 中文识别说白了就是让模型在一张随手拍的街景、菜单、路牌、票据照片里先框出文字在哪再把框里的字认出来。它和扫描件 OCR 最大的区别在于背景杂乱、光照不均、透视倾斜、字体千奇百怪检测框稍微偏一点识别端就会把「未」认成「末」。这套毕业设计标题里同时出现了 tensorflow、keras、pytorch 三个框架名很多人第一反应是「到底用哪个」。实际落地时常见做法是检测端用 TensorFlow/Keras 生态里的轻量检测网络识别端用 PyTorch 训练 CRNN 或类似序列模型两边通过一个统一的推理脚本串起来。它适合正在做毕设、想跑通「检测识别」完整链路的学生也适合想从零搭一套中文 OCR 流水线的工程师。下面我按自己复现这类方案的习惯把选型、数据、训练、串联和踩坑讲清楚。2. 检测与识别为什么要拆成两段选型理由与最小可跑链路2.1 检测端和识别端各自解决什么问题文字检测负责回答「字在哪」输出一串四边形或矩形框识别负责回答「框里是什么字」输出中文字符串。把两件事塞进一个网络做端到端训练当然存在比如一些基于注意力的一阶段方法但对毕设来说两段式更稳检测模型可以单独调识别模型可以单独换中间用裁剪图对接出问题时能快速定位是框歪了还是认错了。检测端常见选择有两类一类是通用目标检测改文字框比如把锚框比例调成细长型另一类是专门为文字设计的像素级方法直接预测每个像素是不是文字中心再聚类成框。后者对密集中文更友好因为中文没有明显词间空格通用检测的锚框容易把相邻字粘成一个框。识别端主流是 CRNN 结构卷积层提特征循环层建模字符序列最后接 CTC 损失做对齐。中文类别多常用做法是先训一个包含几千个常用汉字的字典输出层维度就是字典大小加一空白符。这里有个容易忽略的点检测框的高度要统一缩放到固定值宽度按比例缩放否则循环层看到的序列长度忽长忽短训练很难收敛。2.2 用一份合成数据先把链路跑通真实标注的中文场景文字数据集获取成本高毕设阶段我一般先用合成数据把流程跑顺。下面这段脚本用 PIL 把随机汉字渲染到随机背景上同时生成检测框标注和识别标签格式对齐常见训练输入。import os import random from PIL import Image, ImageDraw, ImageFont # 常用汉字池实际项目可从语料统计高频字 CHARS 的一是了我不人在他有这个上们来到时大地为子中你说生国年着就那和要她出也得里后自以会家可下而过天去能对小多然于心学么之都好看起发当没成只如事把还用第样道想作种开美总从无情己面最女但现前些所同日手又行意动方期它头经长儿回位分爱老因很给名法间斯知世什两次使身者被高已亲其进此话常与活正感 FONT_PATH /usr/share/fonts/truetype/wqy/wqy-zenhei.ttc # 换成你机器上存在的中文字体 def random_bg(w, h): # 生成带噪点和渐变的背景模拟自然场景底色 img Image.new(RGB, (w, h), (random.randint(80, 200), random.randint(80, 200), random.randint(80, 200))) draw ImageDraw.Draw(img) for _ in range(random.randint(20, 60)): x, y random.randint(0, w), random.randint(0, h) r random.randint(1, 4) draw.ellipse([x, y, x r, y r], fill(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))) return img def render_line(text, font_size32): font ImageFont.truetype(FONT_PATH, font_size) # 先量文字尺寸再建画布避免裁切 tmp Image.new(RGB, (10, 10)) d ImageDraw.Draw(tmp) bbox d.textbbox((0, 0), text, fontfont) w, h bbox[2] - bbox[0] 20, bbox[3] - bbox[1] 20 img random_bg(w, h) draw ImageDraw.Draw(img) color (random.randint(0, 80), random.randint(0, 80), random.randint(0, 80)) draw.text((10 - bbox[0], 10 - bbox[1]), text, fontfont, fillcolor) return img, (0, 0, w, h) os.makedirs(synth/images, exist_okTrue) with open(synth/labels.txt, w, encodingutf-8) as f: for i in range(2000): n random.randint(3, 12) text .join(random.choice(CHARS) for _ in range(n)) img, box render_line(text) name f{i:06d}.jpg img.save(os.path.join(synth/images, name)) # 每行文件名\t四点坐标\t文本 f.write(f{name}\t{box[0]},{box[1]},{box[2]},{box[1]},{box[2]},{box[3]},{box[0]},{box[3]}\t{text}\n)这段代码的关键参数有三个font_size决定文字在图中占多大太小会让检测网络学不到笔画特征一般不低于 24random_bg里的噪点数量控制背景复杂度太少模型过拟合纯色背景太多文字被淹没n控制每行字数中文识别序列长度建议在 3 到 15 之间太长 CTC 收敛慢。生成完先肉眼抽查几十张确认没有全黑或全白的废图再进训练。2.3 检测模型训练时的输入尺寸与锚框设置如果检测端用基于锚框的方法锚框宽高比要覆盖中文的细长形态。常见做法是设置三档宽高比比如 1:1、1:3、3:1再配合多尺度特征图。输入尺寸我一般缩放到 32 的倍数比如 640×640这样下采样对齐不会出现半像素偏移。训练时正样本判定用 IoU 阈值 0.5负样本低于 0.3中间忽略。中文密集场景下如果发现相邻字被合并优先调小锚框尺度而不是加大网络深度。3. 识别端 CRNN 训练字典、CTC 与三个必调参数3.1 字典构建与标签编码中文识别第一步是定字典。字典太大输出层参数爆炸太小遇到生僻字就输出空白。毕设常用做法是取前 3000 到 5000 个高频汉字加上数字、字母和标点。下面这段代码把标签文本转成索引序列注意 CTC 要求标签里不能有重复字符直接相邻遇到「林」这种叠字要在中间插一个空白占位或者依赖 CTC 自身的折叠机制——后者更常见但训练初期容易把叠字认成一个。import json # 字典索引0留给CTC空白符 chars 0123456789abcdefghijklmnopqrstuvwxyz 的一是了我不人在他有这个上们来到时大地为子中你说生国年着就那和要她出也得里后自以会家可下而过天去能对小多然于心学么之都好看起发当没成只如事把还用第样道想作种开美总从无情己面最女但现前些所同日手又行意动方期它头经长儿回位分爱老因很给名法间斯知世什两次使身者被高已亲其进此话常与活正感 char2idx {c: i 1 for i, c in enumerate(chars)} # 0 是空白 idx2char {i: c for c, i in char2idx.items()} with open(char_dict.json, w, encodingutf-8) as f: json.dump({char2idx: char2idx, idx2char: {str(k): v for k, v in idx2char.items()}}, f, ensure_asciiFalse) def encode(text): # 未知字符直接跳过避免训练时报 KeyError return [char2idx[c] for c in text if c in char2idx] print(encode(自然场景))字典文件要固定下来训练和推理必须用同一份否则索引错位会让识别结果变成乱码。这里char2idx从 1 开始0 专门留给 CTC 空白这是硬约束不能改。3.2 CTC 损失下的序列长度对齐CRNN 的循环层输出时间步数由输入图像宽度决定。假设输入高度固定 32宽度缩放到 320经过下采样后时间步大约是 80。如果标签长度超过时间步数CTC 直接报错。所以训练前要过滤掉超长标签或者把图像宽度按标签长度动态调整。我一般设一个上限比如标签长度不超过时间步数的三分之二留出空白折叠的余量。下面是一个简单的过滤逻辑。def filter_samples(samples, max_ratio0.66): # samples: [(img_path, text), ...] kept [] for path, text in samples: # 假设图像宽度已缩放到 320下采样 4 倍得 80 步 time_steps 80 if len(text) time_steps * max_ratio: kept.append((path, text)) return keptmax_ratio这个参数很关键。设太大CTC 没有足够空白分隔叠字和相邻字容易混淆设太小长文本被大量丢弃模型学不到长序列。经验值在 0.6 到 0.7 之间中文场景偏 0.6 更稳。3.3 学习率、批大小与早停的配合CRNN 训练最玄学的地方是学习率。用 Adam 的话初始 1e-3 往往在几个 epoch 后损失突然爆炸因为 CTC 在早期梯度方差大。我一般用 1e-4 起步配合 ReduceLROnPlateau耐心值设 3衰减系数 0.5。批大小受显存限制中文识别输入 32×320批大小 32 在 8G 显存上基本能跑。早停看验证集准确率连续 8 个 epoch 不升就停别硬撑过拟合后识别结果会开始「编字」——明明图里没有的字也冒出来。4. 把检测和识别串成端到端流水线裁剪、批处理与后处理4.1 检测框裁剪与透视矫正检测输出的框往往是倾斜四边形直接按外接矩形裁剪会把背景带进来影响识别。常见做法是做透视变换把四边形拉正成矩形。下面这段代码用 OpenCV 实现四点透视矫正注意点的顺序要统一否则图像会翻转。import cv2 import numpy as np def order_points(pts): # 按左上、右上、右下、左下排序 rect np.zeros((4, 2), dtypefloat32) s pts.sum(axis1) rect[0] pts[np.argmin(s)] rect[2] pts[np.argmax(s)] d np.diff(pts, axis1) rect[1] pts[np.argmin(d)] rect[3] pts[np.argmax(d)] return rect def crop_and_warp(img, box): pts np.array(box, dtypefloat32).reshape(4, 2) rect order_points(pts) tl, tr, br, bl rect w int(max(np.linalg.norm(br - bl), np.linalg.norm(tr - tl))) h int(max(np.linalg.norm(tr - br), np.linalg.norm(tl - bl))) dst np.array([[0, 0], [w - 1, 0], [w - 1, h - 1], [0, h - 1]], dtypefloat32) M cv2.getPerspectiveTransform(rect, dst) return cv2.warpPerspective(img, M, (w, h))order_points里用坐标和与坐标差来定位四个角是处理倾斜框的常用技巧。裁剪出来的图再缩放到识别模型要求的 32 高度宽度按比例不足补灰边。补边颜色建议用 127 灰比纯黑更接近自然场景的平均亮度。4.2 批处理推理与结果拼接一张图里可能有几十个文本框逐个送识别模型太慢。把裁剪图按高度统一后堆成 batch一次前向。但宽度不一致需要 padding 到当前 batch 的最大宽度。下面是一个简单的批处理封装。import torch import numpy as np def batch_infer(model, crops, char2idx, devicecuda, max_w320): idx2char {i: c for c, i in char2idx.items()} tensors [] for img in crops: h, w img.shape[:2] scale 32.0 / h nw min(int(w * scale), max_w) img cv2.resize(img, (nw, 32)) if nw max_w: # 右侧补灰边保证 batch 内宽度一致 img cv2.copyMakeBorder(img, 0, 0, 0, max_w - nw, cv2.BORDER_CONSTANT, value(127, 127, 127)) tensors.append(img.astype(np.float32) / 255.0) batch torch.from_numpy(np.stack(tensors)).permute(0, 3, 1, 2).to(device) with torch.no_grad(): logits model(batch) # CTC 贪心解码 preds logits.argmax(dim2).cpu().numpy() results [] for seq in preds: out, prev [], 0 for idx in seq: if idx ! 0 and idx ! prev: out.append(idx2char.get(idx, )) prev idx results.append(.join(out)) return results解码时idx ! prev是 CTC 折叠重复字符的关键idx ! 0跳过空白。注意max_w要和训练时一致训练用 320 推理也用 320否则时间步对不上识别会整体偏移。4.3 置信度过滤与空结果处理检测框有置信度识别结果也有平均概率。实际输出时检测置信度低于 0.5 的框直接丢识别结果为空或平均概率低于 0.3 的也丢。中文场景里模型容易在纹理丰富的背景上误检比如树叶、砖墙这些框识别出来往往是乱码靠置信度过滤能挡掉大半。如果发现漏检严重先别急着换模型检查一下输入图像有没有做归一化很多翻车都是因为推理时忘了除以 255。5. 避坑与排查这套 OCR 流水线最常见的五个翻车点5.1 检测框正常但识别全是空白现象检测能框出文字识别结果为空字符串。原因通常是字典不匹配训练用的字典和推理加载的字典索引错位或者输入图像没有做和训练一致的归一化。解决打印前几个字符的索引和字典文件比对确认推理时img / 255.0这一步没漏。另一个隐蔽原因是裁剪图高度缩放后宽度为 0比如框的宽高比极端resize 后宽度取整变成 0模型输入空张量。加一个max(nw, 1)兜底。5.2 中文叠字被识别成一个字现象「林」「品」「森」这类叠字只输出一个字符。原因是 CTC 折叠机制把相邻相同索引合并了。解决训练数据里叠字要足够多让模型学会在叠字之间插入空白或者在标签编码时对叠字做特殊处理但后者会改变字典结构不推荐。更实际的做法是检查时间步是否太短时间步不足时空白符没有空间插入叠字必然合并。把输入宽度从 320 加到 480 试试。5.3 训练损失下降但验证准确率不动现象训练集损失一路降验证集准确率卡在某个值。原因多半是过拟合合成数据背景太单一模型记住了背景而不是文字。解决增加背景多样性加模糊、加透视变换、加亮度抖动或者引入真实场景数据做微调。另一个可能是验证集标签编码有误比如验证集用了不同的字典这种错误很隐蔽建议训练前把验证集的前几条样本解码出来肉眼核对。5.4 推理速度慢到无法接受现象一张图要跑好几秒。原因通常是检测和识别没有批处理或者模型跑在 CPU 上。解决确认device是 cuda识别端把裁剪图攒成 batch别一张一张送检测端输入尺寸别盲目加大640 够用就别上 1280。如果显存不够用半精度推理model.half()加输入batch.half()速度能快近一倍精度损失在 OCR 任务上通常可接受。5.5 换一台机器就报字体或路径错误现象合成数据脚本在别人机器上跑不起来。原因是字体路径写死了。解决把字体文件放到项目目录下用相对路径加载或者启动时遍历系统字体目录找一个可用的中文字体。路径问题在毕设答辩换电脑演示时特别致命提前把依赖和资源打包成相对路径能省很多后悔药。6. 进阶技巧用真实数据微调与量化部署的取舍合成数据能把链路跑通但真实场景的准确率往往差一截。我一般会留出 10% 到 20% 的真实标注数据做微调学习率调到 1e-5只训识别端检测端冻结。微调时有个细节真实数据的标注框往往没有合成数据那么规整裁剪出来的图会带更多背景这时候可以在识别模型前加一个轻量的空间变换模块让网络自己学矫正但会增加参数量毕设阶段量力而行。部署方面如果目标是演示直接 PyTorch 推理最省事如果要求实时考虑把识别端导出成 ONNX 再用推理引擎加速。导出时注意 CTC 解码不要放进计算图否则很多引擎不支持。下面是一个导出 ONNX 的最小示例。import torch model.eval() dummy torch.randn(1, 3, 32, 320).to(cuda) torch.onnx.export( model, dummy, crnn.onnx, input_names[input], output_names[logits], dynamic_axes{input: {0: batch, 3: width}, logits: {0: batch, 1: time}}, opset_version11 )dynamic_axes把 batch 和宽度设为动态这样推理时不用固定尺寸。opset_version用 11 兼容性最好别追新。导出后拿几张测试图对比 ONNX 和 PyTorch 的输出误差在 1e-3 以内算正常差太多说明有算子没对齐。最后说个我自己的习惯每次改完字典或输入尺寸一定先跑一张固定测试图把检测框、裁剪图、识别结果三样都打印出来看一遍。这个习惯帮我挡掉了至少一半的玄学问题——很多时候不是模型不行是中间某一步的尺寸或索引悄悄变了。希望帮到你。本文还有配套的精品资源点击获取
返回列表