简介:一套基于Python与Keras/TensorFlow实现的自然场景图像文字检测与识别方案,使用EAST/AdvancedEAST完成任意角度文字检测,CRNN+CTC实现不定长文字识别,适合深度学习初学者、毕业设计及工程实训。压缩包共32个文件,含19个Python脚本、8张JPG测试图、3个Markdown文档和2个TXT环境配置,整体仅957KB。检测端支持90度至负90度范围内中英文、数字及符号的文本框定位,识别端采用CTC算法输出文字序列,需Python3.6、TensorFlow1.14与Keras2.1.6环境。资源附有预测脚本和测试样例,结构清晰,便于对照调试和二次开发。目前已有164人学习下载。
1. 从一张带字图片到可编辑文本:两阶段 OCR 方案在解决什么问题
拍一张营业执照、扫描一份合同、对着黑板拍张照,想要里面那几行字变成能复制粘贴的文本,落地通常不是靠一个大模型一把梭,而是“检测 + 识别”两段接力:先用 EAST / AdvancedEAST 圈出哪里有文字,再用 CRNN + CTC 读出文字本身。这套组合在 Python 里跑起来不依赖云端接口,数据能留在本地,适合票据、卡证、横幅等排版相对固定的离线 OCR 场景。下面会先讲两个模型各自的原理和参数,再给一条能直接改参数的推理脚本,最后把检测框太紧、CTC 解码重复这类必经的坑逐个拆开。适合用 OpenCV 和 PyTorch 搭过基础图像程序的读者,新手按步骤也能跑通。
2. 文字检测层选型:EAST 与 AdvancedEAST 的差异和最小推理实现
先明确一个边界:检测层解决的是“字在哪”,不是“字是什么”。它输出的是若干文本框的坐标,可能是水平的,也可能是带旋转角度的四边形。下面按模型原理、最小推理实现和选型三个小节来写。环境方面,先装好 Python 3.8 以上版本、PyTorch 和 Opencv-python,很多新手卡在 numpy 装不上,其实直接pip install numpy就能解决,折腾环境变量比装库本身更费时间,这个放到后面排查章节再提。
2.1 为什么检测层先于识别层:EAST 的模型结构和输出约定
EAST 全称 Efficient and Accurate Scene Text Detector,核心是一套全卷积网络(FCN)。和两阶段检测器先提候选框再分类不同,它把文本检测当成逐像素的回归任务:输入一张图,输出两个分支——score map 和 geometry。score map 的每个像素表示该点处于文本区域内部的置信度;geometry 在 RBOX 模式下是五个值,分别是该像素到文本区域上、右、下、左边界的距离 d1~d4,以及文本框相对水平方向的旋转角度 angle。QUAD 模式下则直接回归四边形四个顶点的坐标。
这种设计的收益是不需要预设锚框,检测形状更贴合自由排版的场景文字,速度也快,在 CPU 上也能做到接近实时的推理。缺点是当文本区域很长、倾斜角度很大的时候,d1~d4 的回归值方差变大,模型容易把一条长文本拆成几段。AdvancedEAST 的主要改动就在这一层:它把角度的回归改造成 sin/cos 两个分量,并在损失上对短边和长边做了不同权重的处理,长文本和倾斜文本的稳定性明显更好。如果你的图片以水平印刷体为主,EAST 完全够用;一旦出现大量倾斜、竖排、透视变形的文本,我会优先选 AdvancedEAST。
2.2 跑通检测的最小 Python 代码:加载、前向与后处理
这一节给出一个可直接改路径运行的检测脚本。常见做法是:图片先按高度等比缩放,避免模型输入尺寸跨度太大导致 score map 上的文本区域比例失真;前向得到 score 和 geometry 之后,先按阈值筛出候选点,再由这些点还原旋转矩形,最后做 NMS 合并。
import cv2 import numpy as np import torch from shapely.geometry import Polygon def east_inference(img_path, model, input_h=320, score_thres=0.4, nms_thres=0.3): # 读取图像,统一转成 RGB,避免 OpenCV 的 BGR 顺序影响可视化 img = cv2.imread(img_path) img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h0, w0 = img_rgb.shape[:2] scale = input_h / h0 resized = cv2.resize(img_rgb, (int(w0 * scale), input_h)) # 归一化到 [0,1],按 NCHW 排布送入模型 tensor = torch.from_numpy(resized).permute(2, 0, 1).unsqueeze(0).float() / 255.0 model.eval() with torch.no_grad(): score, geometry = model(tensor) # score 经过 sigmoid 变成置信度;geometry 里有 5 个通道 score = score.sigmoid().squeeze().numpy() # (input_h, W) geo = geometry.squeeze().numpy() # (5, input_h, W) boxes, scores = [], [] h, w = score.shape for y in range(h): for x in range(w): if score[y, x] < score_thres: continue d1, d2, d3, d4 = geo[0, y, x], geo[1, y, x], geo[2, y, x], geo[3, y, x] angle = geo[4, y, x] cos_a, sin_a = np.cos(angle), np.sin(angle) # 先构造相对中心点的四个角点,再做旋转和平移 pts = np.array([[-d4, -d1], [d2, -d1], [d2, d3], [-d4, d3]]) rot = np.array([[cos_a, -sin_a], [sin_a, cos_a]]) pts = pts @ rot.T + np.array([x, y]) boxes.append(pts) scores.append(score[y, x]) if not boxes: return [] boxes = np.array(boxes, dtype=np.float32) / scale # 缩放回原图坐标 scores = np.array(scores) # 用四边形 IoU 做 NMS:先按得分排序,再合并重叠框 keep = [] idx = np.argsort(-scores) while len(idx) > 0: cur = idx[0] keep.append(cur) poly_cur = Polygon(boxes[cur]) ious = [] for j in idx[1:]: poly_j = Polygon(boxes[j]) inter = poly_cur.intersection(poly_j).area union = poly_cur.union(poly_j).area ious.append(inter / max(union, 1e-6)) idx = np.array([j for j, iou in zip(idx[1:], ious) if iou < nms_thres]) return boxes[keep].tolist()代码里几个参数要解释一下。input_h 控制输入图像的缩放高度,我一般设 320,模型小的可以降到 224,速度更快但小字召回率会下降。score_thres 决定置信度高于多少的像素才被采纳,0.4 是相对稳的起点,文字模糊时调到 0.3。nms_thres 是 NMS 的 IoU 阈值,0.3 表示两个框重叠面积超过 30% 就只保留得分高的那个;这个值调大,框会更多更细碎,调小则可能把相邻两行文本误合并。PyTorch 前向时关掉梯度计算(torch.no_grad())不只是省显存,对推理提速也很明显。
这里旋转矩阵恢复文本框的数学原理是:d1~d4 给的是当前像素相对文本区域四条边界的距离,先把它们组合成一个未旋转的矩形,再乘上由 angle 构造的旋转矩阵,最后加上中心坐标。注意 angle 的单位是弧度,模型输出一般也是弧度。恢复后的坐标要除以 scale 还原到原图尺寸,否则画框位置会整体偏移。
2.3 EAST 与 AdvancedEAST 的选型对照:旋转文本和长文本怎么选
直接给一张选型表,按实际数据分布对照着选即可。
| 场景 | 推荐模型 | 关键原因 |
|---|---|---|
| 印刷体、水平排版(扫描件、PDF 截图) | EAST | 结构简单、后处理快,CPU 推理友好 |
| 手机拍摄、略微倾斜(发票、工牌) | EAST | 轻微倾斜下 EAST 足够,调整输入尺寸即可 |
| 大幅倾斜、竖排文字(门店招牌) | AdvancedEAST | 角度回归用 sin/cos 表示,旋转框更稳定 |
| 超长文本行(一整段话、长标语) | AdvancedEAST | 长边回归的损失权重处理更好,断框率低 |
| 密集小字(名片、表格单元格) | EAST + 更大输入尺寸 | 此类问题更多受分辨率限制,两模型差距不大 |
EAST 的公开权重和实现相对常见,跑通成本低;AdvancedEAST 的实现稍复杂,但对倾斜和长文本更稳,适合业务图片来源复杂的场景。我的习惯是先拿同一批真实样本在两个模型上各跑一遍,统计“完整框出文本行”的比例,用数据选型而不是凭印象。如果新场景里文本基本水平,直接选 EAST,省下的调试时间足够做别的。
3. CRNN+CTC 识别模型:把检测框变成字符串的路径
检测层把文字区域框住之后,第二步是把裁剪出来的小块图像识别成字符串。CRNN 负责把图像编码成序列特征,CTC 负责在序列和字符之间做对齐,两者组合在训练和推理上都有成熟套路。这一章讲结构和参数,再给完整解码代码。
3.1 CRNN 的“卷积提特征 + 双向 LSTM 做序列”结构
CRNN 全称 Convolutional Recurrent Neural Network,处理顺序是先把整张图喂给一个卷积栈(常由 VGG 或 ResNet 变体构成),浅层提取边缘、纹理,深层提取笔画和字符部件的语义。关键一步在最后:把特征图的高度压缩成 1,只保留宽度方向上的维度,然后按列切分,每一列看作一个时间步的输入,送进双向 LSTM。
为什么这样做有效?文字本身就是一维序列,从左到右排列,CNN 的输出天然携带局部视觉信息,但缺少上下文;BiLSTM 恰好能建模字符间的依赖关系,比如“花”后面出现“生”的概率更高。实践中还要注意,输入到 CRNN 的图像高度会先归一化到一个固定值(常见 32),宽度保持原比例,这样输入进来的图片不会因为被拉伸而破坏字形。这一点和后面流水线的矫正环节是配套的。
训练时 CRNN 的损失用 CTC loss,而不是普通的交叉熵。原因是图像中每个字符的宽度不是等分的,模型不知道“这一列对应哪几个字符”,CTC 通过在输出序列中插入 blank 来对齐字符,让模型自己学习分配关系。模型的输出是 T 个时刻上的字符概率分布,T 是特征图在宽度方向的列数,类别数由字典决定。
3.2 CTC 解码不是简单 argmax:blank 机制与贪心解码
推理时,常见入坑是直接把模型的输出 argmax 然后认为每个时刻都对应一个字符。实际上 CTC 输出里每个时刻的 argmax 结果是带有 blank 的序列。CTC 中 blank 表示“当前时刻没有字符”,贪心解码的规则是:先取每个时刻概率最大的类别,得到一串索引;然后从左到右扫描,先把相同的相邻索引合并成一个,再把 blank 索引删掉。注意顺序不能反——先删 blank 再合并重复会把原本连续的两个相同字符错误地合并成一个。这一点在后面的排查章节里我会写一个具体翻车记录。
贪心解码只是 CTC 解码里最朴素的一种,速度快但在某些排列下不是全局最优。要求更高的场景会用 beam search,保留前 K 个概率最大的候选路径,最后从候选里挑出最大概率的一个。Beam size 越大结果越稳,耗时也越高。在小批量离线识别场景,我通常先跑贪心解码看整体准确率,准确率不够再换 beam search。
OCR 领域的识别评估不只看字准不准,还要看编辑距离(Levenshtein Distance)和执行耗时。一堆文字里只错一个字的识别结果,和错一半的结果在业务体感上是天壤之别,所以建议评估时按整句算字错误率(CER),而不要按整图算是否完全正确。
3.3 识别模型的推理代码与字典映射参数
这里给出识别侧的最小推理脚本,包含一个 CTC 标签转换器。模型结构部分用占位类代替,你可以换成自己复现的 CRNN 网络。
import cv2 import numpy as np import torch class CTCLabelConverter: """CTC 解码器:负责把模型输出的类别索引还原成可读文本""" def __init__(self, chars, blank_index=0): # 注意 blank 的索引必须是训练时约定的索引,常见约定是 0 self.blank_index = blank_index self.int2char = {i: c for i, c in enumerate(chars, start=1)} # 0 留给 blank def decode(self, pred_ids): out_chars = [] prev = self.blank_index for p in pred_ids: if p == self.blank_index: prev = p continue if p != prev: out_chars.append(self.int2char[p]) prev = p return ''.join(out_chars) def crnn_inference(crop_img, model, converter, input_h=32): # 统一转灰度,CRNN 常见输入是单通道 gray = cv2.cvtColor(crop_img, cv2.COLOR_BGR2GRAY) h, w = gray.shape[:2] scale = input_h / h resized = cv2.resize(gray, (int(w * scale), input_h)) # 归一化到 [0,1],增加 batch 维度和通道维度 tensor = torch.from_numpy(resized).unsqueeze(0).unsqueeze(0).float() / 255.0 model.eval() with torch.no_grad(): logits = model(tensor) # (1, T, num_classes) pred_ids = logits.squeeze(0).argmax(dim=1).tolist() return converter.decode(pred_ids)代码里的 converter 建议在模型加载后立即打印前 20 个映射关系,当作一次“握手验证”。如果模型训练时用的字典是0123456789abcdef,而你推理时传入的字典多了一个字符,那么从那个位置开始的映射就会错位,输出的文字会变成乱码,这是后面排查章节的一条重点。
参数方面:input_h 必须和训练时一致,常见值是 32,有些中文模型用 48 或 64;不一致会让 LSTM 拿到的序列长度和训练分布差异过大。模型输出的 T 是特征图宽度,W 越大 T 越大,所以输入图像宽度不能被压得太窄——单字符宽度最好不低于 8 像素,否则小字会被识别成缺笔画。
4. 检测与识别串联:从检测框裁剪到完整 OCR 的流水线
检测和识别两个部分各自跑通只能算完成一半。真把两者接起来,最常见的翻车是直接在原图上按检测框的矩形范围裁剪,然后丢给识别模型。如果文本框带旋转角度,这种裁剪会把周围无关像素也切进去,同时文字方向没有拉正,识别模型的输入和它见过的训练分布完全不一致,准确率掉得厉害。所以串联的关键是先讲坐标变换,再讲矫正。
4.1 检测框的坐标转换与透视矫正参数
EAST 系列输出的框是任意四边形的四个顺时针角点,但 CRNN 期望的输入是水平方向的图像。常见做法是使用透视变换:把检测到的四边形映射到一个水平矩形上,长边对应目标宽,短边对应目标高。这样处理后,文字是水平方向,背景是白色或近似色,识别模型的输入分布接近训练时的样本。
目标尺寸很有讲究:CRNN 对高度有强约束,所以目标矩形的高度应该和识别模型的 input_h 保持一致,宽度按检测框的宽高比计算。这样既不会拉伸变形,又满足固定的输入高度。目标宽度如果超过模型可接受的范围,通常的做法是拆分成多段,比如每段控制在 512 像素以内,分别识别后再拼接。这种切分方案在长文本场景中比强行压缩整行更可靠,因为压缩过小会导致字符模糊,模型无法分辨相近字符。
接下来给一个透视矫正函数,输入检测框四点,输出矫正后的裁剪图:
def order_points(pts): """把四边形四点整理成 左上、右上、右下、左下 的顺序""" pts = np.array(pts, dtype=np.float32) s = pts.sum(axis=1) # 左上角 x+y 最小,右下角 x+y 最大 diff = np.diff(pts, axis=1).reshape(-1) # np.diff 得到 y - x,右上角 y-x 最小 return np.array([ pts[np.argmin(s)], pts[np.argmin(diff)], pts[np.argmax(s)], pts[np.argmax(diff)] ], dtype=np.float32) def rectify_crop(img, box, input_h=32, max_width=512): tl, tr, br, bl = order_points(box) width_top = np.linalg.norm(tr - tl) width_bottom = np.linalg.norm(br - bl) height_left = np.linalg.norm(bl - tl) height_right = np.linalg.norm(br - tr) # 取平均,避免单侧噪声 W = int(max(width_top, width_bottom)) H = int((height_left + height_right) / 2) # 如果宽高比过大,就限制最长宽度并等比缩高 scale = 1.0 if W > max_width: scale = max_width / W dst = np.array([[0, 0], [int(W * scale) - 1, 0], [int(W * scale) - 1, int(H * scale) - 1], [0, int(H * scale) - 1]], dtype=np.float32) M = cv2.getPerspectiveTransform(np.array([tl, tr, br, bl]), dst) warped = cv2.warpPerspective(img, M, (int(W * scale), int(H * scale))) return warped这段代码有几个容易被忽略的参数。max_width 是防止超长文本行被压缩到一屏内,512 是一个比较保守的上限;如果你的数据里长文本多,在识别模型支持的情况下调到 768。scale 是等比缩小时的倍数,不能单边暴力压缩,否则字形横向被压扁,字符间粘连,识别模型很容易把m识别成n这类形状敏感的字。透视变换的映射关系是dst = M * src,传入的 pts 顺序必须一一对应,排序函数一旦出错,整个图会被左右颠倒或上下颠倒。
4.2 一条龙推理脚本:检测 → 矫正 → 识别
把前两章的代码拼起来,加上对单张图上多个文本框的循环处理,就是一条完整推理链路。我一般会设定一个最小可运行脚本,能对一张图输出所有检测框坐标和对应文本,先拿到基线效果再逐步调参。下面这个脚本把检测、矫正、识别串起来了:
def ocr_pipeline(img_path, det_model, rec_model, converter): img = cv2.imread(img_path) raw = img.copy() boxes = east_inference(img_path, det_model) # 复用第 2 章的函数 texts = [] for i, pts in enumerate(boxes): # 先做透视矫正,再做识别 crop = rectify_crop(raw, pts, input_h=32) text = crnn_inference(crop, rec_model, converter) texts.append(text) # 在原图上画出四边形框,方便肉眼判断框位置 cv2.polylines(raw, [pts.astype(np.int32)], True, (0, 255, 0), 2) return boxes, texts, raw脚本核心就三步:检测拿框、矫正、识别。注意把 raw 原图单独存一份,避免 cv2 的裁剪操作影响原图坐标。输出时把绿色框画回原图,对排查“检测框位置不对但识别结果还行”这类错误很有效,因为很多时候模型没看错,是框偏了或框小了,直接看可视化比看坐标数字直观得多。实际项目里我会把east_inference改造成接收图片数组,省掉一次重复读图。
4.3 用 PIL / OpenCV 组织输入的细节:通道顺序、缩放、归一化
这一节处理的是“怎么把图像喂给模型才不出偏差”。OpenCV 读进来的是 BGR,而 PyTorch 训练时通常按 RGB 做归一化,所以cv2.imread后要对通道做转换。更隐蔽的是缩放方式:检测模型建议用cv2.INTER_LINEAR或INTER_AREA,识别模型由于字符边缘对形变敏感,我一般用INTER_CUBIC,它在放大时对边缘的还原更好。
归一化的均值方差要和训练时保持一致。很多公开训练代码用的是mean=[0.485,0.456,0.406]、std=[0.229,0.224,0.225],如果推理代码直接用除 255,会把亮度和对比度改变,推理效果会明显变差。一个简单的检查技巧:在模型加载后随机找一张训练图做一次前向,对比训练时的损失;或者干脆在推理前把输入图像用cv2.imwrite导出一份,肉眼确认预处理后的样子是否正常。这招虽然土,但能快速排除很多黑匣子问题。
5. 常见问题排查:这 5 个坑我踩了一遍又一遍
两阶段 OCR 的好处是每个阶段都能单独测试,坏处是问题经常发生在两个阶段的连接处。下面按检测、识别、串联三个环节列几条高频踩坑记录,每一条都是实际项目里反复遇到过的,每条按“现象 → 原因 → 解决”展开。
5.1 检测端:检测框贴着文字边缘,识别反而丢字
现象:检测框肉眼看起来很准,紧贴文字边缘,但把裁剪图送进 CRNN 后,句子两端的字总是识别错或直接丢字。原因很反直觉:识别模型在训练时,样本区域四周通常会留白,模型实际上学到了字符周围的上下文;检测框太过贴合等于把这个上下文裁掉了,CRNN 看到的字符排列和训练分布不一致,两端字符尤其容易受影响。
解决:在矫正后的 crop 四周填充白边,或者对检测框坐标做膨胀,常见是向外扩 2~4 像素。具体做法是在后处理循环里把框坐标按中心放大后再矫正,注意扩展别超过 8 像素,否则相邻文本行会被带进来,反而引入噪声。这个“把框故意放宽一点”的操作,是检测和识别之间最值得先试的调参点。
5.2 识别端:CTC 解码输出连续重复字符
现象:识别结果出现“好好好好好”这类连续重复,或者“你好”变成“你好好好”。原因基本在解码逻辑写错,最常见的是先删 blank 再合并相邻重复,或者干脆忘了合并重复,只做了 argmax 加删 blank。CTC 的约束是:同一个字符连续出现时,中间必须插入一个 blank 才能区分“两个相同字符”和“一个字符重复两次”,所以合并重复和删除 blank 的顺序非常关键。
解决:严格按照第 3.3 节的解码顺序,用一个人为构造的序列验证解码器。比如给pred_ids=[0, 5, 5, 0, 6, 6, 0],期望输出是[5, 6]对应的两个字符;如果输出变成了[5, 6]之外的组合,说明合并逻辑有 bug。先验证解码器,再怀疑模型,这是定位问题的最快路径。
5.3 识别端:字典映射错位导致识别乱码
现象:识别结果里前几个字符是对的,后面全错,或者所有字符都变成另一个相近字的重复。例如数字识别结果里前几位对,从某一位开始全乱。原因几乎可以肯定是推理时的字典和训练时的字典不一致:模型训练时类别索引从 1 排到 N,推理时你在chars列表里多传或少传了一个字符,导致从那个位置开始全部错位。
解决:打印converter.int2char的前 20 项,用训练时导出的 charset.txt 逐项比对。我在一个项目里遇到过把英文字符集abcdefghijklmnopqrstuvwxyz误写成少了i,结果第 9 个字符之后全部错位,打印映射表后一分钟就定位了。这个验证动作应该写进加载模型的公共函数里,每次加载权重都自动打印一次,花不了几毫秒但效果极好。
5.4 串联端:透视矫正把文字转成了九十度
现象:检测框已经框对位置,但矫正出来的 crop 图被旋转了 90 度或左右镜像,识别结果变成一堆无意义字符。原因基本都在角点排序函数上。第 4.1 节写的order_points依赖“文本框是凸四边形”的假设,但实际检测框可能严重倾斜甚至呈梯形,简单的 sum/diff 排序在部分几何形状下会把左上和右上搞混。
解决:先对四点计算凸包,再按边的长度比值判断长边方向,最后确定左右顺序。最直观的验证方法,是把矫正后的 crop 保存下来人眼看一轮,比任何打印坐标都快。我现在的习惯是每次矫正后都顺手cv2.imwrite('debug_crop.jpg', crop),在调试阶段这种“土办法”远比日志可靠。
5.5 串联端:BGR 与 RGB 混用导致的颜色和识别双重翻车
现象:可视化时框的位置很准,但矫正后的裁剪图颜色偏怪,识别结果也不堪入目。原因通常是 OpenCV 读进来是 BGR,检测模型或者识别模型在训练时用的是 RGB,通道没有转就送进去了。对于检测模型,这会影响 score map 的质量;对于三通道输入的识别模型,颜色通道错乱等于把训练分布彻底打乱。
解决:在入口统一转 RGB,后面所有环节都用 RGB,避免在串联链路里出现颜色双重转换。如果识别模型是单通道灰度输入,则只转灰度,不受 BGR/RGB 影响。我的习惯是写一个read_image(path, mode='rgb')的公共函数,把通道转换收口到一个地方,后续所有模块都从这个函数取图,从根本上杜绝混用。
6. 进阶:用自己的数据微调检测和识别模型时,先动这几个参数
到这一步,你已经能用现成权重跑通整条 OCR 流水线。接下来如果要投入实际业务,比如识别自家票据上的印刷体、门店招牌上的繁体字,几乎都要做微调。下面不展开完整训练流程,只讲最值得先动的几个参数和验证技巧。
先放结论:检测模型微调时,不要一上来就全量训练。常见做法是冻结 backbone 前几层,只训练特征融合层和输出层,学习率从 1e-4 起步。冻结前几层可以让模型保留通用的边缘、纹理特征,避免因为新的数据分布导致底层特征被冲掉。如果新数据和公开数据集差异很大,比如全是二值化的黑白文档图,再逐步解冻更多层,每次解冻后学习率减半。AdvancedEAST 的损失里长短边权重,在长文本多的业务里要把长边项的权重从默认的 1.0 提到 1.5~2.0,否则断框问题会伴随整个训练周期。
识别模型微调的关键在“字典”和“输入高度”这两个参数。新业务里如果有生僻字或特殊符号,先把这些字符加进字典,重新生成映射文件;字典一变,模型输出层的类别数就变了,原来预训练权重里最后一层不能加载,需要随机初始化再训练。此时学习率要降得更低,在 1e-5 到 5e-5 之间,否则输出层还没收敛就震荡。输入高度建议与预训练保持一致,不要从 32 改成 48,除非你同时做了足够多的重采样增强,否则特征提取结构需要调整,工作量和收益不成比例。
验证阶段最该做的不是只看准确率,而是拉一张“失败图集”出来。把推理失败的样本分类:是检测框偏了、矫正后文字倾斜,还是识别字典外字符。这项分类工作能很快定位到瓶颈在检测还是识别。比如误识别的字集中在相近字形(王/玉、己/已)里,说明识别精度不够,回去调解码或数据增强;如果整块区域根本没被框出来,改检测阈值比调整识别网络有效得多。这也是我反复和同事说的一句话:两阶段流水线的优化,先分模块评估再做整体调参,你才能拿到确定的收益。
这套方案能不能投入生产,最终取决于三个指标:单张耗时、字错误率、失败样本占比。用你自己的一批真实图去测这三项,比任何公开数据集上的数字都可信。我早期的教训是只盯着准确率不放,结果耗时和失败样本拖垮了上线计划;现在的习惯是先定好评估脚本再动模型参数。希望帮到你。
本文还有配套的精品资源,点击获取