EAST文本检测项目拆解:从倾斜文字到旋转文本框的实现 简介这是一份面向自然场景文本检测的EAST实现资源包适合对OCR、场景文字定位感兴趣的开发者和学生动手实践。内容围绕EAST模型整理包含可加载的h5模型权重、Python脚本、数据样本及相关标签说明按模型训练和推理需求组织便于对照博客说明把检测流程完整跑通。压缩包共2000余个文件以jpg图片、npy数组和txt文本为主另有py脚本、pyc、h5模型与xml等辅助文件整体约461.7MB可覆盖数据、训练权重和运行代码等多方面素材。已有352人学习下载对刚接触EAST算法的读者来说可直接通过样本和脚本观察数据组织方式理解模型输入输出与训练细节缩短环境搭建和代码调试时间。1. 一个开源文本检测项目的完整拆解从倾斜文字到旋转文本框的最后一公里做自然场景OCR最头疼的从来不是识别而是“先找到文字在哪”。尤其在街景、票据、工单照片这些场景里文字行往往带着明显倾斜横平竖直的目标检测框根本框不住内容。这份“旷世east文本检测.zip”资源本质上是把EAST算法从论文变成了一份能直接跑的工程实现。你给它一张自然场景图片它直接输出旋转后的四边形文本框坐标刚好把文字行“包住”。对于要做营业执照识别、门店招牌文字采集、视频截图字幕检测的开发者这个包解决的是OCR流水线里最容易被卡住的一环——倾斜文本定位。老手可以直接拿源码改训练集新手能照着流程跑通推理下面按我拆项目的顺序从目录到代码逐一讲清楚。2. 项目结构与技术选型从目录看EAST的实现骨架EAST这个名字指 Efficient and Accurate Scene Text Detector核心思路是“端到端预测文本行几何形状”。拿到这份压缩包我建议先不要看代码先走一遍目录弄清楚哪些文件管训练、哪些管推理再深入到具体实现就不会迷路。2.1 目录结构训练、推理、后处理三块怎么分工解压后第一层结构通常是这样的east-text-detection/ ├── src/ │ ├── model.py # 网络结构定义特征提取与输出层 │ ├── loss.py # 训练损失函数 │ ├── data_generator.py # 从标注文件生成score map和geometry │ ├── decode.py # 推理后处理把网络输出转成文本框 │ └── eval.py # 在验证集上计算检测精度 ├── config.py # 全局配置输入尺寸、阈值、训练参数 ├── train.py # 训练入口 ├── run_inference.py # 单张图片推理入口 └── models/ # 预训练权重存放目录你看完这份目录其实已经把EAST的工程套路看明白了model.py负责网络“算”decode.py负责把网络的输出“翻译”成坐标data_generator.py负责把人的标注“翻译”成网络能学的标签。整个项目最重的逻辑都压在这三个文件上其余入口文件只是把这三块串起来。在实际源码里model.py的后半段往往包含特征融合层Feature Merging和输出层decode.py里实现了连通域分析和NMS。我拆这类项目时习惯先跑通run_inference.py再回头看训练因为推理路径短、反馈快能先确认模型“活着”。2.2 数据表示训练标签的四边形与score mapEAST训练时每个文本实例不是用一个矩形框表示而是用一个带旋转角度的四边形表示。对应到输入图片网络要学两个东西一个是像素级的文本区域概率图score map另一个是每个像素到最近文本边界的几何距离geometry几何距离由四边距离加上旋转角度组成。生成训练标签时常见做法是把标注的四个顶点坐标转换成旋转矩形再对文本区域做收缩shrink这样能避免相邻文本之间产生粘连的检测结果。我在data_generator.py里看到的关键代码是这样import numpy as np import cv2 def generate_rbox(quad, shrink_ratio0.25): 根据四个顶点生成旋转矩形的score map填充区域 quad: [[x1,y1],[x2,y2],[x3,y3],[x4,y4]] # 先算原始多边形的面积和周长 area cv2.contourArea(quad) perimeter cv2.arcLength(quad, True) # 计算向内收缩的距离 shrink_dist int(area * (1 - shrink_ratio * shrink_ratio) / perimeter) # 用approxPolyDP做多边形缩放实现文本区域收缩 shrunk cv2.approxPolyDP(quad, shrink_dist, True) return shrunk注意这里有个参数容易被忽略shrink_ratio。数值越小收缩越少数值越大文本区域的边缘离真实边界越远。我一般会保留论文推荐的0.25但训练数据里文字密集时会把收缩调到0.4以减少相近文本框互相干扰。cv2.approxPolyDP在这里没在干“近似多边形”的常规活而是利用收缩距离对多边形顶点做简化达到整体向内收缩的效果。对于每个收缩后的多边形再取最小外接旋转矩形cv2.minAreaRect得到中心点、宽高和角度一起写入geometry张量。这就是EAST里RBOX表示法的来源。2.3 技术选型为什么是VGG16骨干加特征融合EAST的网络主体不是自己发明的结构而是迁移了PVANet的思路用VGG16的前几个卷积阶段提取多尺度特征再通过自顶向下的特征融合逐层合并。这样做的直接好处是浅层特征保留了小文本的细节深层特征包含了语义信息融合后能同时照顾到大招牌和小发票字。项目源码里通常会在model.py里加载在ImageNet上预训练的VGG16权重然后截掉全连接层只保留卷积层。后面接的特征融合结构是一个简单的“反卷积加拼接”的循环# 特征融合层 def feature_merge(self, feature_list): # feature_list 是VGG16不同stage的输出 f feature_list[-1] # 最深的特征例如 1x1 通道数 512 for feature in reversed(feature_list[:-1]): # 先反卷积放大两倍 f tf.keras.layers.Conv2DTranspose(128, (2, 2), strides2, paddingsame)(f) # 再按通道拼接浅层特征 f tf.keras.layers.concatenate([f, feature], axis-1) # 加一个1x1卷积和3x3卷积降低通道数 f tf.keras.layers.Conv2D(128, 1, paddingsame)(f) f tf.keras.layers.Conv2D(128, 3, paddingsame)(f) return f这段代码看起来简单但决定了整个模型的感受野。如果你后续要改模型结构改的就是这一层把反卷积换成上采样或者把3x3卷积换成可变形卷积都是在这里动手。模型最后的输出层有四个分支score map、rbox的四个距离通道和角度通道。在源码里这几个输出通常被合并成一个多输出的Tensor后面接不同损失函数。纯从“工程上够不够用”的角度看VGG16已经算是偏重的骨架但它的优点是权重好找、训练稳定。想提速的话可以换成MobileNetV3做骨干只是精度会有一点损失这个话题留给最后一章二次开发讲。3. 核心模块拆解一个请求从进入模型到文本框返回的完整链路这一章我们把“黑匣子”打开。假设你有一张图片现在要从run_inference.py进去看它经过哪些环节最终返回几个带角度的坐标框。3.1 推理入口图像预处理把输入图片转成网络需要的张量EAST对输入尺寸没有硬性要求但训练时通常固定为[256, 256]、[384, 384]或者[512, 512]。推理时常见做法是保持原图宽高比先把短边缩放到目标尺寸然后做padding补成正方形避免文字被拉伸变形。def preprocess(image, target_size384): 将输入图片等比缩放并补边到 target_size x target_size image: BGR格式来自cv2.imread h, w image.shape[:2] # 统一转成 RGB避免通道顺序对结果造成玄学影响 image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 等比缩放 scale target_size / max(h, w) new_h, new_w int(h * scale), int(w * scale) resized cv2.resize(image, (new_w, new_h)) # 补边到正方形 canvas np.zeros((target_size, target_size, 3), dtypenp.float32) canvas[:new_h, :new_w, :] resized # 归一化到 [0, 1] canvas canvas / 255.0 # 扩展成 batch 维度shape: (1, target_size, target_size, 3) return np.expand_dims(canvas, axis0), scale这段代码有三个值得注意的细节。第一补边用的canvas一般用0填充也就是黑色区域这会让边界处没有文字但有一定响应实际操作中如果不想要边界影响可以用整个图片的均值像素填充。第二归一化除以255之后网络内部一般不会再减均值很多复现版本会把归一化写在网络第一层里如果你在预处理里做了就不要再用tf.keras.applications.vgg16.preprocess_input否则相当于做了两遍。第三返回的scale是原图到输入图的缩放比例后面恢复坐标要原样乘回去。3.2 前向推理拿到score map和geometry输出把预处理后的张量喂给模型EAST的输出分两部分一是score map形状是(1, H, W, 1)每个像素的值表示该位置属于文本区域的概率二是geometry形状是(1, H, W, 5)前四通道分别是该像素到文本区域上、右、下、左四条边的距离第五通道是文本行的旋转角度。def inference(model, image_tensor): # 模型返回两个输出score_map 和 geometry score_map, geometry model.predict(image_tensor) # 去掉 batch 维度只保留单张图的结果 score_map score_map[0, :, :, 0] geometry geometry[0, :, :, :] return score_map, geometry这里有一个容易误解的地方网络输出的角度是弧度不是角度制范围约在[-π/4, π/4]。实际调试时我们发现很多“边框转出来是歪的”不是模型问题而是直接用np.degrees转角度后画框却忘了后续构造旋转矩形的逻辑需要弧度值。这一层是纯计算没太多参数可调但输出之后一定要把score_map可视化保存一眼如果一片黑说明模型权重没有正确加载后面再怎么调整后处理都白搭。3.3 后处理从概率图到旋转矩形框后处理是EAST最核心的工程部分论文里称为“Locality-Aware NMS”的前置解码。网络输出的score map是一个逐像素的概率图不能直接用需要先用阈值把它变成二值图再找连通域。def decode_boxes(score_map, geometry, score_thresh0.8): 从 score_map 和 geometry 解码出旋转矩形框 # 1. 二值化 binary (score_map score_thresh).astype(np.uint8) # 2. 找连通域 num_labels, labels cv2.connectedComponents(binary) boxes [] for i in range(1, num_labels): # 3. 取当前连通域内的全部像素坐标 y, x np.where(labels i) if len(x) 10: continue # 4. 对几何图按连通域取平均得到稳定的四边距离和角度 d_top np.mean(geometry[y, x, 0]) d_right np.mean(geometry[y, x, 1]) d_bottom np.mean(geometry[y, x, 2]) d_left np.mean(geometry[y, x, 3]) angle np.mean(geometry[y, x, 4]) # 5. 根据中心点和四边距离构造旋转矩形的四个顶点 cx np.mean(x) cy np.mean(y) boxes.append((cx, cy, d_left, d_right, d_top, d_bottom, angle)) return boxes这段代码是“像素级检测”和“文本框级输出”之间的桥梁。第3步过滤少于10个像素的连通域是为了去掉背景噪点如果图片上有很多零散误检可以把这个数字往上调。第4步取平均是一种工程上的近似处理论文里推荐的做法是在连通域内部做回归但平均法在大多数场景下都够稳。第5步要注意cx和cy是输入图像坐标系里的坐标此时还没有还原到原图尺寸。如果你直接拿这些坐标画原图框会画在小图上位置对不上。必须用前面算的scale把每个坐标和距离都除以scale才能映射回原图。真实项目中后处理往往还会对连通域做一次cv2.minAreaRect拟合用来替代直接平均的偏转角度这样对长文本行的角度更鲁棒。这也是很多人调“框总是不够贴边”时最先改的地方。3.4 文本行合并与NMSLocality-Aware NMS的落地写法文本检测的难点不只是“找文字”还有“找完文字后去重”。EAST原版论文提出了Locality-Aware NMS实现思路是先把方向大致相同的框按位置排序合并再做标准NMS。因为在EAST输出里如果直接对所有候选框做NMS会对斜着的长文本框产生大量重复抑制。def locality_aware_nms(boxes, iou_threshold0.3): 简化版 Locality-Aware NMS 先按角度分桶再对每个桶内的框做加权合并 boxes: 每个元素是 (cx, cy, w, h, angle) if not boxes: return [] # 按角度分桶每10度一个桶 buckets {} for idx, b in enumerate(boxes): angle int(np.degrees(b[4]) // 10) buckets.setdefault(angle, []).append(b) merged [] for bucket in buckets.values(): # 对桶内框做加权平均权重取score if not bucket: continue bucket np.array(bucket) merged.append(bucket.mean(axis0)) # 最后再做一次标准NMS keep [] for b in merged: if not keep: keep.append(b) continue # 简单检查是否与已有框重叠 iou compute_iou(b[:4], keep[-1][:4]) if iou iou_threshold: keep.append(b) return keep这段代码做了一次很取巧的工程简化先把角度接近的框合并成一个大框再按顺序做IoU判断。因为EAST输出的框本身已经很接近真实文本行同一个文本行上的像素会被连通域合并成一个框所以实际候选框数量不多标准NMS足以应付。compute_iou需要处理旋转矩形如果你用普通矩形的IoU计算会漏掉很多重合区域。常规做法是把旋转矩形的四个顶点转成多边形再用cv2.intersectConvexConvex求交面积。源码里如果没有这个函数二次开发时要补上否则检测长文本时会明显感觉“框到一半就没了”。4. 跑通训练与推理常见问题排查与避坑记录推理能跑通是第一步真正要落地还得自己训练。EAST训练流程不算复杂但很多开发者第一次跑时会翻车这一章我把从数据准备到训练参数的路走一遍再把踩过的坑集中列出。4.1 训练数据准备从ICDAR格式转成EAST标签网上下载的文本检测数据大多是ICDAR风格标注每行表示一个文本行四个顶点坐标用逗号分隔。EAST训练不能直接用这个格式需要先转成score map和geometry。常见的数据生成流程是把原图缩放到训练尺寸同时标注也按同样比例缩放然后在data_generator.py里调用生成RBOX的函数。下面是一段数据加载的伪代码def load_annotation(annotation_path): 解析 ICDAR 格式标注 每行格式: x1,y1,x2,y2,x3,y3,x4,y4,text quads [] with open(annotation_path, r, encodingutf-8) as f: for line in f: parts line.strip().split(,) if len(parts) 8: continue pts np.array(parts[:8], dtypenp.float32).reshape(4, 2) quads.append(pts) return quads这里要特别提醒很多标注文件里文本内容可能包含逗号比如“Hello,world”。如果按,直接split会把后面的文本内容也切碎。常见做法是先解析前8个数字然后把剩余部分当成文本内容不要用整行做split。这是训练数据预处理里最隐蔽的bug之一。生成标签时每个文本多边形都要独立生成一个RBOX通道区域多个文本重叠时score map取最大值geometry只对当前文本区域覆盖。如果两个文本挨得很近收缩可以避免它们重叠。4.2 训练脚本和关键超参数让训练不翻车训练入口train.py一般负责加载预训练VGG16权重、配置优化器和损失函数。EAST的损失由三部分组成score map用类平衡交叉熵因为背景像素远多于文本像素geometry用IoU损失角度用余弦损失。常见实现里总损失是把这三项加权相加。参数推荐值说明input_size384越大精度越高显存占用也越高batch_size8显存不足时先降到4learning_rate1e-4用Adam优化器时建议从1e-4起步max_epochs200模型在100轮后开始收敛明显lambda_geo1.0geometry损失的权重lambda_angle10.0角度损失权重调大能让框更贴边score_thresh0.8推理时的score阈值nms_thresh0.3NMS的IoU阈值我在实际训练中习惯把lambda_angle拉高到10因为这个值在原版论文里是10但很多复现版本里被错写成1。角度损失权重不够时训练出来的框“转得不够”倾斜长文本行的检测框会明显往里收看起来像是框小了其实是角度没学准。训练时如果你发现loss下降很慢把输入尺寸从256调到384大多数情况下都能改善。代价是训练时间大约变成原来的2倍显存占用升高但文本检测精度提升更明显。4.3 具体踩坑记录现象、原因、解决这一节是根据我自己在这类项目上的血泪经验整理出来的每条都是真实能复现的坑。现象一训练过程中loss突然变成NaN原因通常有两个一个是学习率太高导致梯度爆炸另一个是geometry里出现距离为0的情况导致IoU损失分母为0。最开始我以为是网络结构写错了排查了三天最后定位到是数据生成时文本区域收缩过度某些短文本被收缩成空区域标签里出现空白mask。解决方法是给IoU损失的分母加一个极小值1e-6同时把初始学习率降到1e-4。对于数据端在生成标签时检查收缩后的多边形面积如果小于5个像素直接丢弃该样本。现象二推理时一个框都检测不到这个问题出现在第一次跑通模型时score map输出全黑或者全是低置信度值。原因很可能不是模型坏了而是推理图片被放到了完全不匹配的尺寸。有些实现训练用的是[256, 256]推理时如果你传一张[512, 512]的照片网络未必能泛化到这么大的尺度。另一个常见原因是score阈值设得过高。原作者给的阈值是0.8但实际模型输出概率偏低时0.5是更稳妥的起点。我建议先用0.3跑一张图确认能看到大量候选框再逐步抬高阈值。现象三检测框能框住文本但位置整体偏移这种“差点意思”的表现最常见原因是后处理坐标没有按缩放比例还原。前面预处理把原图缩放并补边后处理里如果只对cx和cy乘了scale却忘了把四边距离d_left等也乘上scale就会出现框中心对、大小不对的诡异效果。解决方法是把所有几何量放在同一个坐标空间里处理先在网络输出坐标系里构造完整的旋转矩形顶点再用一个统一的矩阵变换映射回原图坐标。现象四长文本行被截成好几段EAST的score map对单个文本行应该是一个完整的连通域但实际训练不足时行内可能有断裂。常见原因是训练标签的收缩比例不对文本行中心区域过窄网络学到的是“只有文字中心是正样本”。增加训练轮数、调小收缩比例都能改善。如果不想重新训练可以在后处理里做一个合并步骤检查相邻两个旋转框的距离如果中心点距离小于某个阈值把两个框合并成一个大的旋转矩形。这个操作在工程里叫“文本线合并”很多OCR项目都会加这一层。现象五正弦波动的角度导致NMS把好框误删EAST输出的角度范围有限但当文本本身接近水平时模型的输出角度会在0附近抖动。NMS里如果直接按IoU比较两个抖动角度不同的框可能只因为旋转角差异大就变成“看起来不重叠”导致同一个文本行留下两个框。解决方式是在计算IoU之前先把候选框按角度排序角度相邻的框先合并再做IoU过滤。这也是Locality-Aware NMS的核心动机之一不要为了省事跳过它。5. 二次开发把EAST输出接到自己的OCR流水线里检测模型一般只负责“找到文字”真正做识别还需要把检测到的旋转矩形区域送给OCR识别引擎。从EAST拿到的是带角度的四边形顶点不能直接cv2.crop因为你想要的是一个正立的长方形文本块。这里的关键一步是用仿射变换把旋转矩形“掰正”。def crop_rotated_box(image, quad): 根据四个顶点裁剪出正立的文本区域 quad: [[x1,y1],[x2,y2],[x3,y3],[x4,y4]] # 计算四条边的长度确定宽高 width max(np.linalg.norm(quad[0] - quad[1]), np.linalg.norm(quad[2] - quad[3])) height max(np.linalg.norm(quad[1] - quad[2]), np.linalg.norm(quad[3] - quad[0])) # 目标矩形的四个顶点长边在上 dst np.array([[0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]], dtypenp.float32) matrix cv2.getPerspectiveTransform(quad.astype(np.float32), dst) crop cv2.warpPerspective(image, matrix, (int(width), int(height))) return crop这段代码有三个工程细节。第一宽高的计算用到了四个顶点的距离而不是简单取横纵坐标差值因为旋转后横纵坐标差值和真实长度并不一致。第二目标矩形的顶点顺序必须和源四边形顶点顺序一致否则会把文本块倒过来我见过很多次“识别全乱”都是顶点顺序没统一。第三调用warpPerspective比getAffineTransform更稳因为四边形本身可能不是严格的矩形透视变换能矫正更多形变。拿这份资源做完改造后我的个人习惯是每次把检测器接入新业务时先把中间结果可视化保存出来观察旋转框是否正确贴合文本再进行下一步识别。这样做看起来像是多了一道无用功但好过在识别结果里莫名其妙地乱猜。从那以后我每次都会强制跑一遍“检测框可视化再进识别服务”的流程宁可多花一分钟看图也不愿意上线后花一小时看错的识别日志。希望帮到你。本文还有配套的精品资源点击获取