简介:基于EAST算法的文本检测实战压缩包,面向计算机视觉、OCR方向的开发者和研究者,解决场景文本检测中模型训练与推理跑通的常见问题。压缩包体积约461.7MB,共2000个文件,主要包含jpg图像样本、npy特征数据、txt标注文件、Python脚本以及h5模型权重:jpg用于输入测试,npy保存中间特征与标签,txt存放文本标注,h5提供不同输入尺寸的预训练权重(如3T256、3T736),py脚本涵盖训练和预测入口。资源目录划分清楚,拿到后可直接加载权重对图片执行文本检测,也能结合样本与标注自行微调;对于希望快速复现EAST、理解文本框回归与损失函数计算细节的初中级学习者,具备较高的参考价值。目前已有352人学习下载,适合作为基于深度学习的文字检测入门和进阶材料。
1. 先看清EAST是什么:一包能直接复用的场景文本检测代码
手头要做驾驶证信息结构化,发现通用OCR对歪斜文本框总是漏检:招牌、身份证边缘、发票表格,只要文字倾斜超过15度就碎成一片。这时候最省事的解法不是去调识别模型,而是先上一个专门的文本检测模型。EAST就是这类模型里最经典的一个——它把检测目标定义为"直接在图上回归出文本框的四个顶点",不需要预先生成候选区域,也不需要网格锚点,网络结构干净,推理也快。这套名为旷世east文本检测的资源,刚好把模型权重、推理脚本和训练配置装在一个包里,适合刚接触场景文本检测的开发者快速跑通流程,也适合想拿一套稳定结果塞进OCR管线的人当检测前端用。
2. 环境与推理:让模型把第一张图里的文字位置吐出来
2.1 先搞清楚结构再动手:没有候选框,直接回归顶点
很多第一次接触EAST的人会拿它和Faster R-CNN对比,然后被搞晕。EAST的思路其实更接近分割模型:整张图进网络,输出一个和原图等比缩小的特征图,特征图的每个位置都预测"这里有没有文字"和"如果有,文本框四个顶点在哪"。整个过程是纯卷积,没有ROI Pooling,也没有Anchor生成,所以训练和推理都要轻量不少。
资源里带的模型结构大致是两条腿:一条是骨干网络负责提特征,另一条是特征融合模块把不同尺度的特征图拼起来。融合之后网络分出两个头,一个头输出score map,也就是每个像素属于文本框的概率;另一个头输出geometry,也就是文本框顶点的坐标信息。这种设计的好处是文本尺度变化大时依然能检测出长句和单个字符,代价是后处理里必须自己拼四边形和做NMS,不像目标检测框架那样自带标准输出。
跑推理之前我建议你先确认手里的权重是什么框架导出的。这套资源里同时给了模型定义和权重文件。如果是从原版TensorFlow迁移来的,输入尺寸多数默认按短边到768的写法;如果是PyTorch重构版,通常配置文件里会写明resize逻辑。我当时拿到包以后第一步就是翻preprocess函数,看它有没有做等比缩放和填充,这一步直接决定后面坐标能不能还原回原图。
2.2 跑通自带推理脚本:预处理和坐标还原是两套坐标系
我自己习惯先跑通最小可用的推理路径再谈调参。下面这段脚本是从资源里的demo脚本精简出来的,逻辑是读图、缩放、推理、拿输出,没有加花哨的可视化。
import cv2 import numpy as np import torch def load_and_preprocess(image_path, input_size=768): img = cv2.imread(image_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w = img.shape[:2] # 按长边缩放到 input_size,短边不足处用零填充 # 注意:这里使用的是等比缩放,不会把文字拉伸变形 scale = input_size / max(h, w) new_h, new_w = int(h * scale), int(w * scale) resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LINEAR) canvas = np.zeros((input_size, input_size, 3), dtype=np.float32) canvas[:new_h, :new_w, :] = resized # 转成 CHW 并归一化;具体均值方差看模型训练时的配置 blob = canvas.transpose(2, 0, 1).astype(np.float32) blob = (blob / 255.0 - 0.5) / 0.5 blob = torch.from_numpy(blob).unsqueeze(0) return blob, scale, new_h, new_w def run_inference(model, blob, device="cpu"): model.eval() with torch.no_grad(): blob = blob.to(device) outputs = model(blob) # 这里默认输出是 score_map 和 geo_map,形状都是 [1, C, H/4, W/4] score_map = outputs[0][0, :, :, 0].cpu().numpy() geo_map = outputs[0][0, :, :, :].cpu().numpy() return score_map, geo_map这段脚本里最关键的是scale和new_h, new_w。因为网络输入是正方形,原图长宽比不同时,短边被填充成0,这部分区域模型不会预测任何有效文本框。拿到score_map以后,如果你直接把像素坐标乘4当原图坐标,会漏掉填充带来的平移量。正确做法有两种:一种是在解码前把score_map裁剪回new_h/4, new_w/4范围,再乘4;另一种是解码所有点,最后用原始填充偏移去纠正。资源里的demo取的是前者,我建议你保持一致。
2.3 输出尺寸的数学关系:为什么特征图要除以4
模型输出头的stride是4,也就是说原图768x768,输出的score_map是192x192。这个"除以4"直接决定了坐标还原的精度。每4个像素共享一个预测点,所以预测框的边界天然有最多4个像素的误差,对一般文字识别来说可以接受,但对小字号文本会明显。
我自己曾经在这个地方翻过一次车:跑了20张测试图,发现所有框都整体往右下偏移了十几个像素,查了半天发现是解码时先乘4再补填充偏移,而实际应该先在1/4尺度下把偏移加上再去乘4。这种细节在资源里已经写进decode函数,但如果你是从零手写,很容易犯同样的事故。经验是跑通后先拿一张标好真值的图叠一下预测框,肉眼确认不是整体平移后,再开始调分数阈值。
3. 从热力图到文本框:后处理与NMS参数怎么设
3.1 score map 与 geometry 到底是什么
模型不会直接画框,它输出的是两张"图"。score_map上每个点的值代表该位置中心存在文字的概率,值越大越可能是文本框内部;geometry则携带框的几何信息。
资源里支持两种几何输出,一种是RBOX,用5个通道表达一个带旋转角度的矩形:4个通道分别是中心点到上、右、下、左边界的距离,第5个通道是旋转角。另一种是QUAD,用8个通道直接表达四边形的四个顶点相对该网格点的偏移。RBOX计算量小,适合检测横平竖直的印刷体;QUAD更自由,能贴合任意四边形,比如曲面屏幕上的倾斜文字。
| 输出分支 | 通道数 | 含义 | 适用场景 |
|---|---|---|---|
| score_map | 1 | 每个网格点属于文本框的概率 | 用于过滤背景 |
| RBOX | 5 | 中心点到四边距离 + 旋转角 | 横排文本、车牌 |
| QUAD | 8 | 四个顶点相对网格点的偏移 | 倾斜文本、四边形文本 |
选择哪种几何体并不是看心情。资源里的训练配置默认开的是QUAD,因为它在检测倾斜招牌和手写文本时抗噪能力更强。RBOX后处理更快,但遇到斜长条文本时,旋转矩形的表达效率不如四边形。
3.2 从热力图到角度框:解码与NMS
拿到score_map和geo_map以后,不能直接阈值化就算完,因为文本行内部的每个点都会预测出一个相似的框,最后需要合并同类项。EAST采用的是一种叫作Locality-Aware NMS的变体,它按行合并互相重叠的候选框,比普通NMS处理大量重叠框时更稳。
decode的流程我一般这样拆:先按score阈值筛出候选点,然后按每个候选点从geo_map里读出四边形顶点,最后把所有候选框按空间关系合并。
def decode_boxes(score_map, geo_map, score_thresh=0.8): h, w = score_map.shape[:2] boxes = [] # 找出所有高置信度的网格点 ys, xs = np.where(score_map >= score_thresh) for y, x in zip(ys, xs): # geo_map 的 QUAD 模式:每两通道对应一个顶点的偏移 offsets = geo_map[y, x] pts = np.array([ [x * 4 + offsets[0], y * 4 + offsets[1]], # 左上 [x * 4 + offsets[2], y * 4 + offsets[3]], # 右上 [x * 4 + offsets[4], y * 4 + offsets[5]], # 右下 [x * 4 + offsets[6], y * 4 + offsets[7]], # 左下 ], dtype=np.float32) # 简单过滤掉面积过小的框,避免噪声点进入 NMS area = cv2.contourArea(pts.reshape(-1, 1, 2)) if area < 20: continue boxes.append(pts) if len(boxes) == 0: return np.zeros((0, 4, 2), dtype=np.float32) # 这里用资源里带的 LANMS 实现替换普通 NMS # 普通 NMS 在文本框重叠场景下会把同一行文字拆成多段 boxes = lanms.merge_quadrangle_n9(boxes.astype(np.float32), nms_thresh=0.2) return boxes解码时有个隐藏参数容易忽略:score_thresh。设太大会把一些模糊字体漏掉,设太小又会把背景纹理识别成文字。资源默认是0.8,我实际测试下来0.6到0.8之间更均衡。这个值和训练时的正负样本定义有关,不是随便拍的,调整时最好配合可视化一块看。
3.3 参数调整心得:先看线,再看框,最后看角
后处理参数在网上被很多人说成"玄学",其实没那么玄。问题主要出在互相耦合的变量太多:输入尺寸、score阈值、NMS阈值、以及是否做多尺度融合。我的排查顺序是先固定输入尺寸,调节NMS阈值到文本框不粘连也不碎裂,然后再慢慢往下压score阈值提高召回。
| 参数 | 常见取值 | 调高后果 | 调低后果 |
|---|---|---|---|
| input_size | 768 | 小字更清晰但更慢 | 整体速度提升但漏检 |
| score_thresh | 0.8 | 大量误检消失但漏检 | 召回变高但背景噪声多 |
| nms_thresh | 0.2 | 框更独立但易碎 | 粘连框增多 |
| 矩形边长阈值 | 20 | 丢小字符 | 噪声框混入 |
一个小技巧:如果只是做身份证、单据这类结构化文档,可以优先把输入尺寸压到640,速度能快30%以上;如果是街景招牌检测,必须上1024甚至双尺度。那段时间我把这套流程在几十张图上反复跑,最后得出的结论是调参前先确定应用场景的文本尺度上限,比盲目跟参数表更省时间。
4. 复现中容易踩的坑:推理与训练排查记录
4.1 长文本被拆成好几段
现象:一句话"请保管好您的证件"被检测成三四个独立的框,每个框里只有一两个字。
原因:输入图像缩放后长边被压得太小,长文本在特征图上占据的跨度变大,相邻网格点之间的预测四分五裂。还有一个原因是NMS合并时阈值设太低,导致同一文本行内的小框被当成不同目标。
解决:把输入尺寸从608提到768或1024,让长文本在特征图上更连贯;同时把nms_thresh从0.2提到0.3,允许更多重叠框合并。我一般会在资源自带的推理脚本里加一个多尺度选项,比如跑两个尺寸然后取并集,代价是慢一点,但长段落检测稳定很多。
4.2 相邻文字行粘连成一个框
现象:两行文字距离很近时,检测框把两行一起包进去,识别阶段根本没法按行切分。
原因:相邻文本框间隙的score_map没有低到阈值以下,间隙区域也被判定为文本内部。常见诱因是训练数据里缺少小行距样本,或者后处理时先膨胀了score_map。
解决:把关口往前移,检查score_thresh是否太低,以及模型的输入图上是否做过归一化导致亮度反差变小。我习惯在decode之后加一个按行高度的过滤,把高度超过正常文本行2倍的框丢回候选池重新分割。如果资源里带了训练脚本,还应该检查loss里的分类权值,正负样本比例失衡也会让概率图整体偏高。
4.3 四边形顶点顺序错乱导致识别模型读乱序
现象:检测框画出来没问题,但送进识别模型后结果完全错乱,比如一行文字被识别成从右往左读。
原因:EAST输出的是无序四边形顶点,解码后如果没有做点排序,某些框的顶点顺序和训练标签不一致。识别模型一般按左上、右上、右下、左下取点做透视变换,顺序错一个角,矫正出来的图就翻转了。
解决:解码后补一个order_points函数,按"左上角x和y最小"的经验法重新排序。更稳妥的是用外接矩形的四个角作为参考,把点按对角关系分组后再定向。血泪经验是不要相信模型输出顺序,无论训练还是推理统一做一次排序。
4.4 训练时Loss不降或直接震荡
现象:用自己的数据继续训练,前几个epoch loss纹丝不动,再训练几天直接飘成nan。
原因:最常见是学习率设置错误,另一个是输入图像标注格式不对,导致正样本区域全是0。EAST对文本框标注格式很敏感,quad四个点的顺序必须按顺时针排列,否则geometry的计算全错。
解决:先冻结backbone只训练head,学习率设到1e-4,跑通后再放开整体。同时把第一批训练数据的score_map可视化出来,如果整个掩膜都是黑的,先回去查标注解析代码。我记得当时卡了整整一个下午,最后发现是转标注时把顶点坐标除以了2,这个坑看配置文件很难看出来,只看score map一眼就能定位。
4.5 输出坐标整体偏移
现象:所有框的形状大小都对,但整体往右下偏移4到8个像素。
原因:解码时坐标还原用的是sigmoid(score)*4,但实际应该用网格坐标乘4再加偏移。很多复现版本会把这个混淆掉,导致一个全局平移。
解决:在decode函数里打印第一个候选点的坐标和对应输出点的网格坐标,手算一次坐标应该落在哪。如果所有结果差一个常量,别去调NMS,先检查坐标还原公式。我把这个问题写进排查清单以后,后续换任何模型都会第一时间验证这一点,省了很多无意义的调参。
5. 把EAST接进OCR流水线:矫正框与后续识别
5.1 按检测框做透视变换
文本检测的终点是识别,而识别模型大都需要水平或近似水平的文本行图片。拿到四边形顶点后,先算最小外接矩形,再按矩形做透视矫正,把倾斜的文字"摆正"再送进识别模型,准确率通常能提升10个点以上。
def crop_text_region(img, quad): rect = cv2.minAreaRect(quad.astype(np.float32)) box = cv2.boxPoints(rect) width = int(rect[1][0]) height = int(rect[1][1]) if width < height: box = np.roll(box, 1, axis=0) width, height = height, width dst = np.array([[0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]], dtype=np.float32) M = cv2.getPerspectiveTransform(box, dst) result = cv2.warpPerspective(img, M, (width, height)) return result注意:取最小外接矩形时,长边不一定是水平方向,所以代码里做了宽高互换,保证输出是横向文本。如果你接的是竖排文字识别模型,这一步的宽高逻辑要反过来。
5.2 给识别模型留点后悔药
框矫正完以后,我习惯保留一份未矫正的原始裁剪图,放进流水线的debug目录。原因是当识别结果和预期不符时,可以直接对比原始文本和多边形裁剪图,快速判断问题出在检测还是识别环节。
一张图里的多个文本框,我按从上到下、从左到右排序后再批量识别,顺序基本符合人类阅读习惯。资源里的检测脚本来得及的话,我会加上输出JSON的函数,把顶点坐标存下来,这样后续做版面分析时可以直接复用。
这套流程跑了两个月以后我养成了一个习惯:每次接到新的文本检测需求,先不做任何参数改动,直接拿一张有代表性的图跑一遍资源里的推理脚本,观察score阈值下哪些框会碎、哪些框会粘,再针对性调参。从那以后我几乎没有再被检测结果坑过。希望帮到你。
本文还有配套的精品资源,点击获取