
简介这是一份面向计算机相关专业学生与深度学习初学者的EAST自然文本检测完整项目基于Pytorch与TensorFlow实现提供了从模型训练、损失计算到评估调优的全流程代码。压缩包内共15个文件以9个Python脚本为核心涵盖网络结构定义、数据预处理、损失函数、评估指标如hmean及推理脚本另含shell启动脚本、说明文档和License并附带原始数据压缩包整体仅1.99MB轻量易部署。项目源码已通过导师审核并在真实环境测试运行成功适合作为毕业设计、课程设计或竞赛演示的基线工程尤其便于在此基础上进行迁移与二次开发。目前已有193人学习下载作为一份经实践验证的高分资源能帮助使用者快速理解EAST检测原理并搭建可用系统。1. 自然文本检测里EAST在Pipeline中的位置以及为什么要同时学PyTorch和TensorFlow自然文本检测的目标是定位图片中“哪里有文字”并不关心文字内容内容识别交给后面的OCR。很多团队的检测精度上不去问题往往出在文本框预测不完整或歪斜而不是识别模型不够强。EAST用全卷积网络一次性输出score map和几何偏移没有候选框、没有RNN训练和推理都比CTPN之类更轻对任意角度的文本更友好。这个标题把PyTorch和TensorFlow放在一起并不是简单罗列框架而是一个典型的工程分工实验迭代在PyTorch里跑线上用TensorFlow Serving提供gRPC服务。想系统掌握文本检测落地的人最好同时保留两条技术栈。2. EAST自然文本检测的算法拆解与PyTorch核心实现2.1 从候选框到回归四边形EAST改变了文本检测的什么在深度学习的目标检测方法介绍里EAST通常被归为one-stage家族。它对标的是两阶段的CTPNCTPN先按列切分锚点再用BiLSTM聚合成行最后得到水平的文本框遇到倾斜文本时CTPN需要额外加回归分支或者做透视变换整体链路长调参点散。EAST去掉候选区域和循环网络直接用FCN输出两层信息一层是文本区域概率图一层是每个像素到所在文本框四条边的距离以及一个旋转角度。推理时只要把得到的RBOX结果做一次NMS就能得到文本框或最小外接四边形。这种设计让它很适合作“文本前的定位任务”。在街景、票据、工业质检场景文本方向往往不固定EAST对倾斜、近水平、长文本都有稳定的检出率。速度上在同样输入尺寸下用ResNet-50做主干单张640×640的图在GPU上的推理时间能控制在几十毫秒量级换成PVANet还可以更快。它不擅长的地方是极端透视形变和中文笔画密集的小字后者在后续章节里会用后处理技巧尽量补救。我用一个表快速对比三个常见候选帮助决定在什么任务里选EAST模型输出结构是否依赖RNN倾斜文本工程复杂度EAST像素级score map RBOX/QUAD否适合中CTPN水平proposal BiLSTM是弱高DB可微分二值化否适合中高DB在准确率上限上略优于EAST但EAST更常出现在课程和工程包里因为结构清晰容易改造成自定义数据集的训练框架。这也是标题里选择EAST的原因之一源码可读性好出现问题容易定位。2.2 用PyTorch实现EAST Head输出score map和RBOXEAST的主干负责取特征常见的做法是去掉ResNet-50最后的全连接层保留四个stage的特征图再用上采样和拼接做尺度融合。最后的融合特征图直接接一个“头”输出两个张量。一个不分通道概率图另一个几何图。RBOX模式下几何图是5个通道QUAD模式下是8个通道工程上多数用RBOX。PyTorch框架里这个头可以只用一个卷积层完成完整训练工程里还会加初始化。下面这段代码是从训练源码中抽出来的关键部分import torch.nn as nn class EASTHead(nn.Module): def __init__(self, in_channels: int, num_classes: int 1, geometry: str RBOX): super().__init__() # 文本区域概率图每个像素是文字区域的置信度 self.score_conv nn.Conv2d(in_channels, num_classes, 1) # 几何输出RBOX为5个通道QUAD为8个通道 geo_channels 5 if geometry RBOX else 8 self.geo_conv nn.Conv2d(in_channels, geo_channels, 1) def forward(self, x): score self.score_conv(x) geo self.geo_conv(x) return score, geo这里in_channels是特征融合层输出的通道数一般在256到512之间取决于主干选择。score_conv和geo_conv都使用1×1卷积作用是把融合特征映射到预测空间不改变特征图尺寸。训练时输入为640×640输出就是1×1×W×H和5×W×H。后面的损失函数只在正样本像素上计算几何损失所以还要结合score map做一个mask避免背景区域参与了距离回归。推理时对score map做阈值过滤然后从每个像素的五个值中解析出该像素所在的旋转框。常用方法是用cv2.minAreaRect把连通的像素聚成外接矩形再按原始图的坐标体系还原。这一部分虽然不参与PyTorch反传但后处理的精度直接决定最终检测框长什么样。2.3 损失函数为什么不能省掉角度项EAST训练损失包括三部分区域分类损失、几何回归损失和角度损失。实际工程里最容易忽略的是角度。如果只回归四条边的距离旋转角度信息丢失预测框会变成水平矩形对小角度倾斜文本的召回率下降严重。RBOX的几何回归使用IoU损失而不是简单的L1或MSE核心原因是对矩形宽高变化不敏感模型更关注框的重叠度。下面这段是典型的RBOX损失计算伪代码真实源码里会把旋转框的IoU展开成某个近似计算import torch def east_rbox_loss(score_pred, geo_pred, score_true, geo_true): # score 损失全图二值交叉熵负样本占比较多时可调整 pos_weight score_loss torch.nn.functional.binary_cross_entropy_with_logits( score_pred, score_true, pos_weighttorch.tensor(2.0) ) # 只计算文本区域内的几何损失背景像素直接置0 mask score_true 0.5 geo_pred geo_pred * mask.float() geo_true geo_true * mask.float() # approximated box IoU loss这里的box_iou是简化写法 iou rotated_box_iou(geo_pred.permute(0, 2, 3, 1), geo_true.permute(0, 2, 3, 1)) geo_loss 1 - iou.mean() return score_loss 20.0 * geo_loss说明几个参数pos_weight2.0是在负样本远多于正样本时把正样本损失放大避免模型学会了输出全零mask保证只有文字区域像素参与几何回归20.0是几何损失的系数原论文里这个权重用来平衡分类与几何损失的量级数值太小时框会松散太大时训练初期会出现loss震荡。实际调试时我会先固定geo_loss系数再用pos_weight微调类别失衡。这一层的选型直接影响后续TensorFlow部署时的兼容性PyTorch训练权重里的卷积结构和ONNX导出都能一一对应但损失函数并不会被导出所以部署时只需要网络前向输出score和geo。3. Anaconda配置PyTorch环境、标注格式与训练命令3.1 Anaconda配置PyTorch环境的两个细节工程包解压后第一件事不是直接跑训练而是核对依赖。常见的依赖文件里有torch、torchvision、opencv-python、shapely、lanms和tensorboard。其中lanms用于局部感知NMS原仓库在Linux上安装最顺利Windows下建议直接用cv2.dnn.NMSBoxes替代。Anaconda配置PyTorch环境时最容易踩坑的是CUDA与PyTorch版本错位。我一般会先用nvidia-smi确认驱动支持的最高CUDA版本而不是直接装最新版。下面的命令创建一个Python 3.10环境并安装CUDA 12.1对应的PyTorchconda create -n east python3.10 -y conda activate east pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 pip install opencv-python shapely lanms tensorboard--index-url指定的是PyTorch官方包源只用于安装torch相关的轮子其它依赖不要混用这个源。在GPU版本装好后执行python -c import torch;print(torch.cuda.is_available())一定要能输出True。如果输出False问题出在libcudnn或显卡驱动先重装CUDA工具包再重装PyTorch不要反复改conda环境。如果目标机器没有GPU可以安装CPU版PyTorchEAST训练依然能跑只是每次迭代时间会拉长到GPU的5到10倍。调试模型逻辑阶段用CPU反而更容易发现内存和显存相关的问题。3.2 ICDAR标注格式转RBOX标签的数据预处理自然文本检测最常用的公开数据是ICDAR系列格式也最直白一张图对应一个同名txttxt里每行是四个点的x、y坐标共8个数字后面跟着转录内容。四个点要求按顺时针排列否则计算出的最小外接矩形会错位。拿到原始数据后不是直接把8个点作为回归目标而是先转成RBOX的5维表示。转法用OpenCV的最小外接矩形即可。这里是一个可以直接跑通的预处理函数import numpy as np import cv2 def quad_to_rbox(quad): # quad: 一行标注里的8个坐标值 pts np.array(quad, dtypenp.float32).reshape(4, 2) rect cv2.minAreaRect(pts) (cx, cy), (w, h), angle rect # 统一为长边角度在[-45, 45]避免旋转方向歧义 if w h: w, h h, w angle 90 # EAST的RBOX几何需要的是像素到四边的距离这里先把矩形中心保存下来 # 实际训练时仍需要按每个像素位置计算 top/right/bottom/left return [cx, cy, w, h, angle]先解释一下cv2.minAreaRect返回旋转矩形的中心、宽高和旋转角角度范围在-90到0之间。代码里把宽高统一成w大于等于h再把角度做90度偏移可避免同一矩形因为宽高交换产生不同标注。正式训练时EAST的标签不是整图一个框而是对score map上每个正样本像素计算它到旋转框四条边的距离这一步需要把多边形坐标和每个像素位置同时传入不能用整图的最小外接矩形替代。数据资料里如果已经带了icdar目录建议保留原始文件名只生成后缀为_rbox.npy的标签缓存文件。这样每次训练读到同名numpy文件省去在DataLoader里反复调用cv2.minAreaRect整体数据读取速度能提升20%以上。3.3 训练命令与核心超参数表拿到训练脚本后常见做法是直接在项目根目录执行下面的命令。如果脚本支持命令行参数通常会有--img_size、--batch_size、--max_epoch、--lr和--resumeconda activate east python train.py \ --root ./data/ICDAR2015 \ --img_size 640 \ --batch_size 8 \ --max_epoch 200 \ --lr 1e-4 \ --gpu 0 \ --resume ./checkpoints/latest.pth--img_size控制输入尺寸640是训练GPU显存与检测精度的平衡点显存不足就降到512检测长文本可以提到736。--batch_size要和--lr配合batch从8减半时lr也应同步减半否则训练初期的loss会冲高。--resume可以从上次权重继续断点续训比重新开始效率高很多。下面是我实际操作中比较稳的一组超参数前150轮建议不做学习率衰减最后50轮再用余弦退火参数推荐值说明img_size640训练图像短边推理时可以更大batch_size8单卡A100/409016G以下显存用到4lr1e-4主干用ImageNet预训练时不要超过2e-4max_epoch200数据量小且mAP稳定后可提前停score_thresh0.8影响召回训练阶段不影响nms_thresh0.2IOU阈值越小去重越狠训练过程中用TensorBoard看三个指标score loss、geo loss和验证集的F1。前20轮score loss会从0.7降到0.2附近geo loss缓慢下降这是正常的。如果geo loss在30轮仍不降优先检查标签是否注错不要动学习率。4. PyTorch模型转TensorFlow Serving做推理部署4.1 为什么线上推理更愿意交给TensorFlow ServingPyTorch的TorchServe也能做生产部署但很多老牌运维体系里TensorFlow Serving已经占了位置它天然支持gRPC、REST、模型多版本、自动加载新权重压测和监控都能直接复用。标题里同时提到TensorFlow真实工程里未必是重写训练代码而是把PyTorch训练好的模型导出成TensorFlow格式。这种训练和部署框架分离的做法在2024年依然常见。PyTorch负责论文复现和快速训练TensorFlow Serving负责稳定上线。转换路径固定为PyTorch权重 → ONNX中间表示 → TensorFlow SavedModel。在动手之前先确认PyTorch的torch.onnx.export导出的动态尺寸参数否则转出来的模型只能接受固定尺寸输入部署端想多batch推理就很被动。4.2 用ONNX导出并转出TensorFlow SavedModel导出脚本放在独立的export_onnx.py中不要塞在执行训练的代码里避免eval和train状态切换出错。导出时使用torch.randn(1, 3, 640, 640)作为示例输入网络输入与训练预处理一致BGR转RGB、resize到固定尺寸、归一化到0到1之间。import torch from models.east import EAST net EAST(pretrainedFalse).eval() net.load_state_dict(torch.load(checkpoints/best.pth, map_locationcpu)) example torch.randn(1, 3, 640, 640) torch.onnx.export( net, example, east.onnx, input_names[input], output_names[score, geo], dynamic_axes{ input: {0: batch}, score: {0: batch}, geo: {0: batch} }, opset_version11, )dynamic_axes让batch维可变如果只做单图推理可以去掉这个字段并提高转换成功率。opset_version11是为了兼容大多数转换工具新版本PyTorch默认opset可能到17直接转ONNX时算子兼容性没问题但转到TensorFlow时会遇到Resize和Pad算子的歧义。实际转换如果报错第一步就是改成11。拿到east.onnx后用onnx-tf转换。onnx-tf已经不太活跃新项目更推荐onnx2tf同样是命令行工具pip install onnx-tf # 或 pip install onnx2tf python -m onnx_tf.backend.convert east.onnx -o east_saved_model转换成功后查看east_saved_model里面必须有saved_model.pb和variables/目录输入输出名和torch.onnx.export里保持一致。缺少variables/说明还在SavedModel的父目录把子目录拷贝到部署目录即可。转换工具的选择可以参考下面这个表工具命令示例适用模型常见坑onnx-tfpython -m onnx_tf.backend.convert旧模型、opset 11部分算子无实现onnx2tfonnx2tf -i east.onnx新模型需要额外确认动态尺寸无论用哪个工具转换完都先检查输入名称。很多onnx模型转成SavedModel后输入名会从自定义名变成input_1如果不改TensorFlow Serving的请求名gRPC调用会直接报错。4.3 TensorFlow Serving启动参数与gRPC调用服务端部署时使用Docker是最稳妥的。把SavedModel挂载到/models/east/1下注意“1”是版本号后面更新权重就换成2、3TensorFlow Serving会自动把流量切到最新版本。容器运行时最少的参数是下面这些docker run -p 8500:8500 -p 8501:8501 \ --mount typebind,source$(pwd)/east_saved_model,target/models/east/1 \ -e MODEL_NAMEeast \ tensorflow/serving两个端口分别对应gRPC和HTTP/REST。GPU环境需要额外加--gpus all并在启动命令中增加--per_process_gpu_memory_fraction0.5避免显存占满。MODEL_NAME必须和挂载目标路径中的“east”一致否则Serving会提示找不到模型。调用端可以写一个纯Python的推理客户端把图像resize后构造成TensorProto发送import cv2 import numpy as np import tensorflow as tf import grpc from tensorflow_serving.apis import predict_pb2, prediction_service_pb2_grpc channel grpc.insecure_channel(localhost:8500) stub prediction_service_pb2_grpc.PredictionServiceStub(channel) img cv2.imread(sample.jpg) img cv2.resize(img, (640, 640)) img img[:, :, ::-1].astype(np.float32) / 255.0 img np.transpose(img, (2, 0, 1))[None] request predict_pb2.PredictRequest() request.model_spec.name east request.model_spec.signature_name serving_default request.inputs[input].CopyFrom(tf.make_tensor_proto(img)) result stub.Predict(request) score tf.make_ndarray(result.outputs[score])[0] geo tf.make_ndarray(result.outputs[geo])[0]这里必须强调预处理一致性img[:, :, ::-1]完成BGR转RGB归一化操作和训练代码完全一致如果训练脚本减均值或除以标准差客户端也要同步。request.inputs[input]这个名字来自ONNX导出的input_names改成别的名字会导致Not found。score和geo拿到手后先套上score阈值过滤再调用cv2.minAreaRect生成候选框之后做NMS。TensorFlow Serving部署还有一个容易忽略的细节signature_name在多数SavedModel里是serving_default如果转换工具生成的是tensorflow/serving/predict要先用saved_model_cli show --dir east_saved_model --all查看不能照抄。5. 上线前最值得做的三个优化阈值调整、NMS策略与端到端验证5.1 用score阈值控制误检而不是先调NMS很多从源码直接跑起来的人会把模型检出框太多的责任推给NMS实际上在EAST这里score map的阈值比NMS阈值影响更大。我一般会把score阈值从0.5开始逐步抬到0.9观察每张图的目标数量变化。文本区域置信度本身很高如果0.9下依然有大量低置信度框说明训练数据里正样本标签给宽了优先检查标注框是否包含过多背景。下面的函数在服务端推理后执行替换固定的0.5阈值def filter_boxes(score, geo, score_thresh0.9, nms_thresh0.2): mask score score_thresh ys, xs np.where(mask) if len(xs) 0: return [] # 根据geo五个通道还原旋转候选框之后做NMS boxes decode_geo(geo, xs, ys) return nms(boxes, nms_thresh)这里的score_thresh设成0.9是高召回与低误检的折中如果下游OCR明确要求不能漏就降回0.7如果OCR结果错别字多就继续上调。NMS阈值0.2代表两个候选框IoU超过0.2就合并比常规目标检测更严格因为文本检测框本身重叠度高。5.2 验证时不要只盯着Loss要看框的几何偏移学完整个流程后还有一个容易踩的坑PyTorch训练时的数据增强如果包含随机旋转和裁剪最好在推理时不启用。而在服务端对比时训练代码里用的cv2.resize和客户端的tf.image.resize实现有差异会造成输出框整体偏移。我建议在部署目录保留一批固定原图用服务端脚本跑出检测框并可视化和训练脚本跑出的图逐张对比。验证脚本我习惯这样写用一个人工标注的JSON文件记录每张图的预期框数量服务端返回的框数量即使相同也要计算两个框中心点之间的像素差超过5个像素就要检查resize或padding逻辑。自然文本检测的成品率指标是“框位置偏差小于原图宽度的1%且文本内容能被下游完整识别”这个阈值比单纯的F1更贴近生产。最后留一个经验在线上OCR链路里检测误报比漏报更致命。漏报了最多OCR少解析一段误报会把不在识别区域的背景裁出来产生大量脏数据。所以EAST调用端的score阈值尽量往上调NMS阈值保持0.2左右宁可漏两个小字也不要让下游识别器整天处理空框。本文还有配套的精品资源点击获取