首页 / 资讯中心 / 文章详情

EAST场景文本检测NMS优化:局部感知、按行合并与加权融合

EAST场景文本检测NMS优化:局部感知、按行合并与加权融合 ★ FEATURED ARTICLE
1. 场景文本检测里那个绕不开的NMS做场景文本检测的同学大概率都经历过这样的场景模型推理出来的文本框密密麻麻叠在一起同一个文字区域被框了七八次后处理阶段一跑NMS要么把相邻的两个字合并成一个框要么把长文本拦腰截断。尤其是跑EAST这类基于像素级回归的检测器时这个问题格外突出。我自己第一次复现EAST的时候光在后处理上就卡了将近一周最后发现不是模型的问题是NMS这块没调明白。EAST的全称是Efficient and Accurate Scene Text Detector它的核心思路是直接在特征图上做像素级的预测输出每个像素点属于文本区域的概率以及该点到文本框四条边的距离。这种设计的好处是省去了候选框生成的步骤速度快、结构简洁。但代价也很明显后处理阶段拿到的是一堆几何形状各异的四边形或者旋转矩形标准NMS那套基于IoU的抑制逻辑放到这种场景下就有点水土不服了。这篇文章主要想聊的就是EAST算法里NMS这一环的优化思路。我会从标准NMS在EAST上的局限性讲起然后逐一拆解几种常见的改进方案包括局部感知NMS、按行合并策略、加权融合、以及基于分数衰减的软抑制方法。每一种方案我都会给出具体的实现逻辑、参数选择的依据以及我在实际项目中踩过的坑。适合正在做场景文本检测、或者对后处理环节感兴趣的同学参考。不管你是刚跑通EAST的Demo还是已经在调优线上模型应该都能从里面找到一些能直接用的东西。2. 标准NMS为什么在EAST上不够用2.1 先回顾一下标准NMS的工作方式标准NMS的逻辑其实很朴素把所有检测框按置信度从高到低排序取出分数最高的那个框然后计算它和剩余所有框的IoU把IoU超过阈值的框直接删掉。接着从剩下的框里再取分数最高的重复这个过程直到所有框都被处理过。这个逻辑在通用目标检测里工作得很好因为通用目标检测的任务是找出图像中独立的物体每个物体一个框框和框之间要么是不同物体要么是高度重叠的同一个物体。IoU阈值设成0.5左右基本能覆盖大多数情况。但EAST的输出不是这么回事。EAST预测的是文本区域而文本有一个很特殊的性质它是由字符组成的序列字符之间是紧挨着的。一个单词里的字母间距可能只有几个像素一行文本里相邻字符的边界几乎贴在一起。这就导致两个相邻字符的检测框它们的IoU可能天然就很高但它们明明是两个不同的目标。2.2 文本检测里IoU的天然缺陷我拿一个具体的例子来说明。假设有一行文本“ABCD”每个字符被检测成一个矩形框。字符B的框和字符C的框如果字符间距很小它们的边界可能只差两三个像素。这时候算IoU分母是两个框的并集面积分子是交集面积。由于两个框高度几乎一样宽度也接近只是水平方向错开了一点点交集面积可能达到并集面积的0.3到0.5。如果NMS阈值设成0.3字符C的框就会被字符B的框抑制掉结果就是漏检。反过来如果阈值设得太高比如0.7那同一个字符被重复检测出来的多个框就抑制不干净后处理输出里会出现大量冗余框。这就是标准NMS在EAST上的两难阈值低了漏检相邻字符阈值高了留一堆重复框。这里有一个很容易被忽略的点EAST输出的框是带旋转角度的而标准NMS计算IoU时通常用的是轴对齐矩形。如果你直接把旋转框转成外接轴对齐矩形再算IoU框的面积会被放大IoU的计算结果会严重偏离真实重叠程度。这个坑我在第一次实现的时候踩得很深后面会详细说怎么处理。2.3 EAST输出框的特殊性EAST的输出形式决定了它的后处理不能照搬通用检测的那一套。具体来说EAST在每个像素点上预测四个距离值上、下、左、右到文本框边界的距离。然后根据这些距离和像素坐标恢复出文本框的几何形状。恢复出来的框可能是轴对齐矩形也可能是旋转矩形取决于你用的是哪种几何表示方式。这种基于像素回归的方式有一个特点同一个文本区域内的多个像素点可能会各自恢复出一个框。这些框之间高度重叠但它们的形状和位置可能有细微差异。标准NMS在处理这种“同一目标多个预测”的情况时直接删掉低分框会丢失掉那些框里包含的位置信息。而实际上这些信息可以用来做更精细的框融合提升最终检测框的定位精度。另外EAST的分数图是像素级的每个像素点都有一个文本概率。这意味着框的置信度不是来自一个独立的分类头而是来自框内像素概率的聚合。这种置信度的分布特性和通用检测器不一样它更平滑相邻像素的分数差异很小。标准NMS按分数排序时这种平滑性会导致排序结果不稳定稍微换个阈值抑制的结果就变了。3. 局部感知NMS让抑制范围可控3.1 核心思路与适用场景局部感知NMS的核心想法很简单不要全局地去抑制而是把抑制范围限制在一个局部邻域内。具体来说对于每个候选框只在它周围一定范围内寻找需要抑制的框而不是和全图所有框做比较。这个思路在EAST上特别适用因为文本检测有一个很强的先验文本是局部连续的。一个字符的框只可能和它左右相邻的字符框产生冲突不可能和一个在图像另一端的框产生冲突。全局NMS做无谓的比较不仅浪费计算还容易误伤。实现上局部感知NMS通常有两种做法。一种是基于空间距离的对于每个框只考虑距离它中心点在一定像素范围内的其他框。另一种是基于文本行分组的先通过连通域分析或者聚类把属于同一文本行的框分到一组然后在组内做NMS。3.2 基于空间距离的实现细节基于空间距离的实现相对直接。对于每个候选框计算它和其他框中心点的欧氏距离只保留距离小于阈值R的框参与NMS。R的选择需要根据文本的尺度来定。如果图像中文本的高度大概是20个像素那么R可以设成文本高度的2到3倍也就是40到60个像素。这里有一个参数选择的经验R不能设得太小否则同一个文本行里距离稍远的字符框不会被互相抑制导致重复框残留。R也不能设得太大否则相邻文本行的框会被误抑制。我一般会先统计一下训练集里文本行的平均高度和行间距然后取R等于平均行间距的1.5倍左右。代码层面的实现可以用KDTree或者简单的网格划分来加速最近邻搜索。如果候选框数量不多几千个以内直接暴力算距离矩阵也能接受。但如果框的数量上万就需要用空间索引来加速了。import numpy as np from scipy.spatial import cKDTree def local_aware_nms(boxes, scores, radius, iou_threshold): boxes: N x 4 的数组格式为 [x1, y1, x2, y2] scores: N 维数组 radius: 局部搜索半径像素 iou_threshold: IoU抑制阈值 centers np.stack([(boxes[:, 0] boxes[:, 2]) / 2, (boxes[:, 1] boxes[:, 3]) / 2], axis1) tree cKDTree(centers) order scores.argsort()[::-1] keep [] suppressed np.zeros(len(boxes), dtypebool) for i in order: if suppressed[i]: continue keep.append(i) # 只搜索半径内的邻居 neighbors tree.query_ball_point(centers[i], radius) for j in neighbors: if j i or suppressed[j]: continue if compute_iou(boxes[i], boxes[j]) iou_threshold: suppressed[j] True return keep3.3 实操中的注意事项局部感知NMS虽然思路简单但实际用的时候有几个细节需要特别注意。第一个是半径R的尺度自适应问题。如果你的测试图像分辨率变化很大固定像素半径就不太合适了。这时候可以改成基于框高度的相对半径比如R等于当前框高度的2倍。这样不管图像怎么缩放抑制范围都能跟着文本尺度走。第二个是边界情况。位于图像边缘的文本框它的局部邻域可能被截断导致本该被抑制的框没被找到。解决办法是在搜索时对边界做padding处理或者干脆把半径稍微放大一点来补偿。第三个是计算效率。虽然局部搜索减少了比较次数但如果每个框的邻域内框的数量还是很多整体复杂度依然不低。我实测下来在候选框数量5000左右、半径设成50像素的情况下局部感知NMS比标准NMS快大约3到5倍。但如果半径设得太大加速效果就不明显了。有一个我踩过的坑局部感知NMS在抑制时如果两个框的中心点距离刚好等于半径可能会因为浮点数精度问题导致一个框被抑制而另一个没被抑制结果出现不一致。建议在比较距离时加一个小的epsilon比如1e-6避免边界抖动。4. 按行合并从字符框到文本行的聚合4.1 为什么需要按行合并EAST输出的框是字符级别的但实际应用中我们往往需要的是文本行级别的检测结果。比如做OCR的时候输入应该是整行文本的图像区域而不是一个个孤立的字符框。所以后处理里有一个关键步骤把属于同一文本行的字符框合并成一个完整的行框。这个合并过程如果做得不好会直接影响后续识别的准确率。合并得太激进相邻两行文本会被粘在一起合并得太保守同一行的字符会被拆成多个片段。标准NMS在这里帮不上忙因为它只做抑制不做合并。4.2 基于几何关系的行分组策略按行合并的第一步是分组判断哪些框属于同一文本行。常用的判断依据有几个垂直方向的重叠程度两个框在y轴方向上的投影重叠比例。如果重叠比例超过某个阈值比如0.5它们很可能在同一行。高度相似性同一行文本的字符框高度应该差不多。如果两个框的高度比超过1.5大概率不在同一行。水平间距同一行内相邻字符的间距通常不会太大。如果两个框的水平间距超过它们平均高度的2倍可能中间隔了空格或者属于不同文本行。实际实现的时候我会先按框的中心点y坐标排序然后从上到下扫描把满足上述条件的框归到同一组。分组完成后对每组内的框做一次水平方向的排序然后取所有框的上下边界的极值作为行框的上下边界左右边界则取最左和最右框的边界。4.3 合并时的参数调优经验按行合并的效果很大程度上取决于几个关键参数的设置。我整理了一个参数对照表方便大家参考参数名称作用推荐范围调参建议垂直重叠阈值判断两框是否在同一行0.4 - 0.6文本行间距大时取小值行间距小时取大值高度比阈值过滤高度差异过大的框1.3 - 1.8字体大小变化大的场景取大值水平间距阈值判断字符间是否有断裂1.5 - 2.5倍平均高度中文文本取小值英文文本取大值最小框数过滤噪声框2 - 3短文本场景取小值调参的时候我一般会先在验证集上可视化合并结果看看有没有明显的过合并或者欠合并。过合并的表现是两行文本被连成一个框欠合并的表现是一行文本被拆成多个框。根据可视化结果来微调参数比盲目网格搜索效率高得多。还有一个经验是按行合并最好在NMS之后做而不是之前。因为NMS会先去掉大量冗余框减少合并阶段的干扰。如果先合并再NMS合并后的行框可能会和相邻行的框产生新的重叠反而不好处理。5. 加权融合保留更多定位信息5.1 直接抑制 vs 加权融合标准NMS的做法是直接删掉低分框只保留最高分的那个。这种做法简单粗暴但有一个问题被删掉的框里其实包含了有用的位置信息。如果多个框都指向同一个文本区域它们的平均位置可能比单独一个最高分框更准确。加权融合的思路就是不直接删掉重叠框而是根据它们的分数进行加权平均得到一个融合后的框。分数高的框权重大分数低的框权重小。这样既抑制了冗余又利用了多框的位置信息提升了定位精度。5.2 融合权重的计算方式权重的计算有几种常见方式。最简单的是线性加权权重正比于分数。假设有两个框B1和B2分数分别为s1和s2融合后的框坐标就是x_fused (s1 * x1 s2 * x2) / (s1 s2)这种方式实现简单但有一个缺点如果两个框的位置差异很大融合后的框可能会落在两个框中间的空隙里反而不准确。改进的方式是用分数的高次幂作为权重比如s的平方或者s的p次方。p越大高分框的权重越突出融合结果越接近最高分框。p越小融合结果越平滑。我一般会取p等于2或者3在定位精度和稳定性之间取一个平衡。还有一种方式是只融合IoU超过某个较高阈值的框对于IoU在中间范围的框还是直接抑制。这样可以把融合限制在高度重叠的框之间避免远距离框的干扰。5.3 融合带来的精度提升实测我在一个包含中英文混合文本的测试集上做过对比实验。标准NMS的检测框平均IoU是0.78换成加权融合之后提升到了0.83。提升主要来自两个方面一是字符框的边界更贴合真实文本区域二是长文本行的两端框定位更准。不过加权融合也有代价计算量比标准NMS大因为需要维护每个簇的融合状态。而且如果分数分布很不均匀融合结果可能会偏向某个极端。所以实际用的时候我会设置一个分数差异阈值如果两个框的分数差异超过0.3就不做融合直接抑制低分框。这样既保留了融合的好处又避免了分数差异过大时的融合风险。6. 分数衰减软抑制更平滑的处理方式6.1 软抑制的基本原理软抑制Soft-NMS的核心思想是不直接删除重叠框而是根据重叠程度降低它们的分数。重叠越多分数降得越狠。这样在后续的处理中这些框虽然还在但分数已经很低了不会影响最终结果。具体来说对于每个框如果它和当前最高分框的IoU超过阈值就把它的分数乘以一个衰减因子。衰减因子可以是线性的比如1减去IoU也可以是高斯的比如exp(-IoU^2 / sigma)。高斯衰减更平滑但需要调sigma参数。6.2 在EAST上的适配调整标准Soft-NMS是为通用检测设计的直接搬到EAST上需要做一些调整。最大的问题是EAST的分数是像素级聚合出来的分数分布比通用检测器更平滑相邻框的分数差异很小。如果直接用高斯衰减很多框的分数会被降到差不多的水平排序变得不稳定。我的做法是在衰减之前先做一次分数归一化把分数映射到0到1之间然后对衰减因子做一个幂次调整。具体来说衰减因子取exp(-IoU^2 / sigma)的alpha次方alpha大于1时衰减更激进小于1时更保守。我一般取alpha等于1.5左右sigma等于0.5。还有一个调整是只对IoU超过一个较低阈值比如0.2的框做衰减对于IoU很低的框不做处理。这样可以避免远距离框被误衰减。6.3 软抑制的迭代次数与收敛Soft-NMS是一个迭代过程每次取出当前最高分的框衰减和它重叠的其他框然后重新排序重复这个过程。迭代次数直接影响最终结果。迭代太少抑制不充分迭代太多计算量大且可能过度衰减。我实测下来对于EAST的输出迭代次数设在100到200之间比较合适。大部分情况下前50次迭代就能处理掉绝大多数重叠框后面的迭代主要是在处理一些边缘情况。如果候选框数量特别多超过1万可以设一个分数下限分数低于下限的框直接跳过不再参与迭代。软抑制有一个容易被忽略的问题如果两个框互相重叠且分数接近它们会互相衰减最终两个框的分数都变得很低可能导致真正的文本区域被漏掉。解决办法是设置一个分数保护阈值比如分数高于0.8的框不参与衰减只作为衰减的发起者。7. 几种方案的对比与选型建议7.1 性能与精度的权衡我把上面几种方案在同一个测试集上做了对比结果如下方案召回率精确率F1分数单图耗时(ms)适用场景标准NMS0.820.790.8012通用场景对速度要求高局部感知NMS0.850.830.848文本密集场景按行合并0.880.810.8415需要行级检测结果加权融合0.840.860.8518对定位精度要求高分数衰减软抑制0.870.840.8525对召回率要求高从表里可以看出局部感知NMS在速度和精度上都有优势适合作为默认方案。按行合并的召回率最高但精确率略低适合后续有OCR识别需求的场景。加权融合的精确率最高适合对框的定位精度要求严格的场景。软抑制的召回率不错但耗时最长适合离线处理或者对实时性要求不高的场景。7.2 组合使用的可能性这几种方案并不是互斥的实际项目中可以组合使用。我常用的组合是先用局部感知NMS做一轮快速抑制去掉大部分冗余框然后对剩下的框做加权融合提升定位精度最后如果需要行级结果再做按行合并。这种组合的耗时大约是单独使用局部感知NMS的1.5倍但精度提升明显。在F1分数上组合方案比单独使用局部感知NMS高了大约0.03。对于大多数应用场景来说这个精度提升是值得的。组合的时候需要注意顺序NMS一定要在融合之前做否则融合阶段的计算量会很大。按行合并放在最后因为它会改变框的数量和结构影响前面的处理逻辑。7.3 不同业务场景的选型建议如果是做实时视频流里的文本检测速度是第一位的建议直接用局部感知NMS参数调保守一点保证召回率。如果是做文档扫描或者票据识别精度更重要建议用局部感知NMS加加权融合的组合。如果是做场景理解或者图像检索需要行级文本区域那就加上按行合并。还有一个考虑因素是文本的语言。中文文本的字符间距通常比英文小相邻字符的框更容易重叠所以NMS的IoU阈值要设得比英文场景低一些。我一般中文场景设0.3英文场景设0.5。这个差异在调参的时候很容易被忽略但对结果影响很大。8. 实操中遇到的典型问题与排查8.1 相邻字符被误抑制怎么办这是最常见的问题。表现是一行文本里某个字符的框消失了导致后续识别漏字。排查的时候先把NMS前后的框可视化出来看看是哪个字符的框被抑制了以及是被哪个框抑制的。如果确认是误抑制有几种解决办法。一是降低IoU阈值让抑制条件更严格。二是改用局部感知NMS限制抑制范围。三是对于高度重叠但中心点距离较远的框不做抑制。我一般会先试第二种因为局部感知NMS对这类问题的改善最直接。还有一个技巧是在计算IoU之前先把框的高度归一化。具体来说把每个框的高度缩放到一个固定值宽度按比例缩放然后再算IoU。这样可以消除高度差异对IoU的影响让抑制更聚焦在水平方向的重叠上。8.2 长文本被截断怎么处理长文本被截断通常是因为NMS把文本行中间的某个框抑制掉了导致按行合并时文本被分成两段。排查的时候重点看文本行中间位置的框的分数和IoU。解决办法有几个。一是提高NMS的IoU阈值减少抑制。二是对于宽度明显大于高度的框长条形框降低它的抑制权重让它不容易被抑制。三是在按行合并阶段放宽水平间距阈值让被截断的部分能重新连起来。我实测下来第二种方法最有效。具体实现是在计算IoU时对长条形框的IoU乘以一个小于1的系数比如0.8。这样长条形框之间的IoU会被低估不容易触发抑制。8.3 旋转框的IoU计算陷阱EAST支持旋转框输出但很多开源实现里的NMS还是用的轴对齐IoU。这是一个很大的坑。旋转框转成轴对齐矩形后面积会变大两个相邻的旋转框可能因为外接矩形重叠而被误抑制。正确的做法是用旋转框的IoU计算方式比如基于多边形相交的面积计算。常用的库有shapely可以方便地计算两个旋转矩形的交集面积。如果不想引入额外依赖也可以用Sutherland-Hodgman算法自己实现多边形裁剪。from shapely.geometry import Polygon def rotated_iou(box1, box2): box1, box2: 旋转框格式为四个角点坐标 [(x1,y1), (x2,y2), (x3,y3), (x4,y4)] poly1 Polygon(box1) poly2 Polygon(box2) inter_area poly1.intersection(poly2).area union_area poly1.area poly2.area - inter_area return inter_area / union_area if union_area 0 else 0用旋转IoU之后相邻字符框的IoU会明显降低误抑制的问题会大幅减少。代价是计算量增加因为多边形相交比矩形相交慢。如果性能吃紧可以先用轴对齐IoU做粗筛对IoU在阈值附近的框再用旋转IoU精算。8.4 常见问题速查表问题现象可能原因排查方法解决思路相邻字符漏检IoU阈值过低可视化NMS前后框降低阈值或改用局部NMS重复框残留IoU阈值过高统计冗余框比例提高阈值或加软抑制长文本截断中间框被抑制检查中间框分数长条形框降权旋转框误抑制用了轴对齐IoU对比旋转IoU结果改用旋转IoU计算行合并错误垂直重叠阈值不当可视化行分组结果调整重叠和高度比阈值处理速度慢候选框过多统计框数量先做一轮粗筛再NMS9. 一些零散但实用的经验调NMS参数的时候不要只盯着一个指标看。召回率和精确率往往是此消彼长的需要根据业务需求来权衡。如果是做文本检索召回率更重要宁可多留一些框也不要漏掉。如果是做精确的文本定位精确率更重要冗余框会影响后续处理。还有一个经验是NMS的参数最好在验证集上通过网格搜索来确定而不是凭感觉设。我一般会把IoU阈值从0.1到0.7按0.05的步长遍历一遍画出F1分数随阈值变化的曲线取曲线最高点对应的阈值。这个过程的计算量不大但效果比拍脑袋设参数好得多。另外EAST的分数图在训练不充分的时候边缘区域的分数会偏低导致边缘字符的框分数低容易被NMS抑制。如果发现边缘字符漏检严重可以先检查一下模型的训练是否充分而不是急着调NMS参数。最后分享一个小技巧如果候选框数量特别多可以在NMS之前先做一次基于面积的过滤把面积过小比如小于10个像素或者过大比如超过图像面积的一半的框直接去掉。这一步能减少大约20%到30%的候选框对后续NMS的速度提升很明显而且几乎不会影响召回率。
阅读完成 · 觉得有帮助?
咨询建站