☰
CTPN文本检测算法详解:从原理到实践
2026/10/17 22:17:48 网站建设 项目流程

做OCR这一块,只要涉及自然场景,文本检测就像是一道绕不开的门槛。CTPN这个名字,我最早是在一篇2016年的论文里看到的,当时觉得它不过是一个“旧模型”,直到真正上手做票据识别、街景文字抽取这类项目,才发现它背后的设计思路比很多花哨方案都扎实。这篇文章我想从“为什么选它”“它的结构在做什么”“怎么跑起来”“坏了怎么查”几个角度,把CTPN完整讲透。如果你正准备入门文本检测,或者已经跑过不少深度学习模型但总被长文本搞到头大,这篇内容应该能帮你省不少时间。

CTPN的全称是Connectionist Text Proposal Network,核心任务是从图片里找出文字所在的区域,并且给出一个尽量紧贴文字的矩形框。它当年最大的贡献,是把“文本行检测”这件事重新定义成“检测成串的小文本块”,再用序列模型把这些小块连成整行。这个思路后来影响了很多检测算法,直到现在依然值得仔细研究。

1. CTPN到底解决什么问题

1.1 自然场景文本检测的难点在哪里

如果你只做过文档扫描件的OCR,可能觉得文本检测很容易,白底黑字,二值化之后用投影法就能切开。但自然场景完全不是这样。路边招牌、商品包装、聊天截图、视频字幕,背景里什么都有,文字可能压在复杂纹理上,字体五花八门,光照还不均匀。传统图像处理很难在这种条件下稳定地把“文字区域”和“背景区域”分开。

另外,文本行的形态也很特殊。它不像行人、汽车那样有一个明显的二维轮廓,而是一条窄长的带子,长度可能比高度大出十倍以上。普通目标检测器如果直接对整个文本行画一个检测框,需要学习回归四个坐标,水平方向上的误差会非常大,特别是当文本很长、拍摄角度又有点歪的时候。坐标系里一点抖动,框就偏出去很多。

还有一个麻烦是文本行里的字符间距不稳定。一个单词里的字母可能紧挨着,也可能被空格隔开,中文那就更复杂了,标点、左右结构、上下结构都会让“一个字符等于一个框”的假设失效。CTPN的聪明之处在于,它不预测整个文本行,而是预测很多个小竖条,每个竖条宽度固定,高度自适应文本字符的高度。然后根据这些小竖条的相对位置,把它们按顺序串起来。这样就把“检测一条长边”变成了“检测一系列小部件”,难度和稳定性都改善了很多。

1.2 CTPN方案的基本盘:把文本切成竖条

CTPN把文本行的检测拆成两个阶段:局部定位和全局连接。局部定位阶段,在特征图的每个位置生成一组“锚点”,每个锚点负责一个宽度固定为16像素的候选框,模型预测这个候选框里是文字还是背景,同时回归它的垂直中心位置和高度。第二阶段,模型按水平顺序读取这些局部预测,利用上下文信息把属于同一行的竖条连接起来。

这种拆法有一个直接好处:不管文本有多长,局部单元始终是同样尺度的竖条,检测器不需要处理“长宽比极端”的框。你只需要让模型学会判断“这个位置是不是文字的一部分”以及“这块文字占多高”。举个不恰当但容易理解的例子,就像你在墙上贴了一排瓷砖,不需要一下子判断整面墙有多大,只需要判断每块瓷砖是不是贴对了位置,再沿着水平方向把相邻的瓷砖拼起来。

连接的工作则交给两条线索:一是双向LSTM提供的序列上下文,让模型知道附近还有没有文字;二是一个后处理算法,把高分的竖条按水平近邻关系组成完整的文本行。从工程角度看,这个方案对长文本特别友好,因为即便某个小段被漏检了,只要前后段还在,后处理也有机会把它们连起来。

2. 网络结构与原理拆解

2.1 Backbone与特征提取:VGG16的取舍

CTPN早期版本使用VGG16作为特征提取网络,取到conv5层输出。输入图片通常先缩放到高度600像素,宽度等比缩放。经过VGG16的四个pooling层之后,特征图的空间分辨率是输入尺寸的1/16。也就是说,原图上每隔16个像素,在特征图上对应一个位置。

为什么是VGG16而不是更深的ResNet?当年一个重要原因是VGG16结构简单,中间特征图比较规整,方便做“保持空间位置对应”的密集预测。CTPN不是像分类网络那样最后接全局池化和全连接,它需要在每个位置上做预测,所以希望特征图保留足够的空间分辨率和感受野。VGG16的conv5感受野大约有两百多个像素,对判断一个文字竖条是否成立足够用,而且结构容易加载预训练权重。后来有人用ResNet替换主干,效果也确实更好,但VGG16作为教学和理解用的基线,依然很合适。

从conv5特征图开始,网络先做一个3x3的卷积,目的是把局部信息进一步融合。这一步的输出通道数通常设计成512。你可以把3x3卷积理解为在特征图每个位置上综合周围8个像素的信息,让局部特征更平滑。之后再接一个1x3的卷积,因为CTPN关心的是水平方向的连续模式,这个窄卷积能特意聚合左右邻居的信息,为后面进入LSTM做准备。

2.2 Anchor设计:固定宽度,高度自适应

Anchor这个概念从Faster R-CNN来,但CTPN的anchor设计很不一样。Faster R-CNN的anchor有不同长宽比,覆盖各种形状的目标;CTPN的anchor宽度固定为16个像素,和特征图一个位置对应的原图感受野跨度一致。高度则预设了一组从小到大排列的值,目的是覆盖不同字号、不同行高的文字。

常见的一组anchor高度是:11、16、23、33、46、69、96、128、171、231,单位是原图像素。每一个特征图位置都会生成这10个anchor,每个anchor需要输出两类信息:一是这个竖条区域是文本还是背景的得分,二是这个竖条在垂直方向上的精修偏移,具体包含“垂直中心位置”和“高度”两个值。为什么不需要回归水平左边界和右边界?因为水平位置靠固定宽度决定,后续文本线构造算法会把一串竖条拼成行,再整体做水平方向的微调。这样设计减少了回归自由度,模型更容易稳定训练。

训练时的正负样本匹配也很关键。把所有anchor和标注文本行做IoU计算,IoU大于0.7的当成正样本,小于0.3的当成负样本,落在中间的部分在损失计算时忽略。因为anchor宽度固定为16,而文本行的宽度远大于16,所以一个标准文本行会被很多个anchor覆盖,正样本数量非常充足。这对训练很有帮助,尤其是长文本,天然提供了大量上下文样本。

2.3 双向LSTM与序列上下文

如果只是对每个竖条独立分类和回归,模型很难区分“字符的一部分”和“背景里的竖条纹理”。比如一个路灯杆,它在图片里也是窄长的,单看局部它和文字竖条很像。这时候就需要利用上下文来判断:如果竖条两边紧挨着还有类似的竖条,而且它们的垂直范围大致对齐,那很可能是文字;如果两边空空荡荡,那就更像背景物体。

CTPN在3x3卷积和1x3卷积之后,把特征图按照宽度方向展开成序列,送入一个双向LSTM。双向LSTM的每个时间步对应特征图的一个水平位置,前向和后向两个方向分别编码“左侧上下文”和“右侧上下文”。最后把两个方向的隐状态拼接起来,再接回一个卷积层,把每个位置的特征恢复成可以继续做预测的形式。LSTM的隐层节点数常见设置是128,整个结构并不复杂,但效果非常明显。

我自己的理解是,这一步让模型学会了“文字连续性”这个概念。它不再孤立地看待每个16像素宽的竖条,而是能感知“我正在预测的这段内容,左边是字符的一部分,右边也是字符的一部分,所以我大概确实处于一个文本行中间”。到了文本尾部,上下文信息又会告诉它“右边没有文字了”,从而降低把远处无关区域误判为文本的概率。这种序列建模的思路,是CTPN区别于一众纯卷积检测器最核心的地方。

2.4 文本线构造算法与侧边精修

模型输出的是一堆带得分的竖条,后处理要把它们变成一个一个独立的文本行文本框。大致流程可以分成下面几步:

  1. 先按分类得分过滤,只保留得分超过阈值(常见0.7)的竖条。
  2. 对同一个位置附近的多个anchor做非极大值抑制,把重复的、严重重叠的竖条合并掉。注意CTPN里的NMS和通用目标检测NMS略有区别,它会优先考虑垂直方向上的重叠程度。
  3. 把剩下的竖条按水平位置排序,逐个检查两个相邻竖条是否满足连接条件。一般要求水平间距不要太大,垂直范围的重叠比例至少达到一个阈值,并且两者得分都符合标准。满足条件的竖条归入同一个文本行候选。
  4. 对每个文本行候选,把所有竖条的最小y和最大y作为文本框的上下边界,x方向先以固定宽度为基础,再用侧边精修模块对左右边界做一次微调,让框更贴合首尾字符。

侧边精修(side refinement)是CTPN论文里的一个细节。它会对文本行的最左边和最右边竖条的x坐标做额外回归,修正水平边界,相当于在固定宽度anchor基础上再补一次精确定位。如果你在复现时省略了这一步,短文本可能看不出太大差别,但长文本的两端容易多出一截空白或者切掉最后一个字符。

3. 实操指南:从数据准备到推理

3.1 数据标注与训练样本生成

想自己训练一个CTPN模型,第一步不是写网络,而是准备好能够生成训练样本的脚本。原始标注通常是每张图片对应若干个文本框,每个框用四个顶点坐标表示。比如一个文本行框是[x1, y1, x2, y2, x3, y3, x4, y4],按顺时针顺序标。CTPN训练时不能直接拿整个文本框去匹配anchor,因为anchor宽度固定是16,而文本框可能几百个像素宽。

因此离线阶段可以先把标注框按宽度方向切成长度不超过一定阈值的小块。一种常见的做法是,从文本框左上角开始,以16像素步长向右移动,每次取一个宽度为16的竖直矩形,记录它的中心y和高度作为该位置的回归目标。所有小块的集合就是这一行文字产生正样本的参照。

这里我想提醒一句:切分成16像素宽的小块不代表每个小块内部刚好包含一个完整字符。文本行里字符间距不均匀,所以有些竖条可能切在字符边上,但这对训练影响不大,因为模型只需要学会判断“该区域是否属于文本”,并不需要知道字符边界。真正重要的是垂直方向回归目标要准确,上下边界尽量贴合文字的实际高度。

3.2 网络搭建与训练策略

如果按PyTorch实现,CTPN的主干部分可以直接加载VGG16预训练权重。建议把VGG16前两组的卷积参数冻结或者设一个更小的学习率,重点微调后面几层,让模型先学会适应目标数据的纹理特点。分类和回归两个分支从共享特征之后分成两路,分类分支用SoftmaxLoss,回归分支用SmoothL1Loss,整体损失是两者按比例相加。

训练参数方面,我的习惯是这样:输入图片高度缩放到600,宽度不超过1000,保持原始宽高比;优化器用SGD,momentum为0.9,权重衰减设0.0005;初始学习率1e-3,训练到loss平台后降到1e-4。Batch size要看显存大小,一般8到16之间。如果只想快速验证效果,可以用一小批开源合成文本图片先训练几千步,然后再用真实数据微调。

训练过程中最难的是正负样本不均衡。文本区域在原图里占比往往很小,背景区域却非常大。如果直接拿所有anchor参与Loss计算,负样本会淹没那么几个正样本。一般处理方式是限制每张图参与训练的anchor总数,比如256个,其中正负样本比例接近1:1。如果正样本不足,就把IoU阈值稍微放宽,或者在一张图里复制一些难样本。这个细节直接决定模型能不能顺利收敛。

下面给一个推理阶段解码文本框的简化示例,用Python加NumPy示意,方便理解核心流程:

import numpy as np def decode_proposals(scores, verticals, anchors, score_thresh=0.7): """ scores: [num_anchors, 2],背景和文本得分 verticals: [num_anchors, 2],垂直中心偏移和高度偏移 anchors: [num_anchors, 4],anchor的x1,y1,x2,y2 """ text_scores = scores[:, 1] keep = text_scores > score_thresh anchors = anchors[keep] verticals = verticals[keep] text_scores = text_scores[keep] boxes = [] for anchor, v, s in zip(anchors, verticals, text_scores): x1 = anchor[0] x2 = anchor[2] # anchor 高 h = anchor[3] - anchor[1] # 回归值转成真实高度和中心 pred_h = h * np.exp(v[1]) pred_cy = anchor[1] + h * v[0] y1 = pred_cy - pred_h / 2 y2 = pred_cy + pred_h / 2 boxes.append([x1, y1, x2, y2, s]) return np.array(boxes)

这只是解码anchor回归结果,真正做文本线连接还需要按得分排序、执行NMS、排序后串联。后处理逻辑不复杂,但非常依赖阈值,建议在一个单独的验证集上多试几组参数。

3.3 推理后处理:如何把竖条串成文本行

前一步得到了很多过滤之后的竖条,接下来要把它们连成行。我这里分享一个比较稳的处理顺序:

  • 先把竖条按照中心y坐标和高度做一次“同区域合并”,因为同一块文本的多个anchor可能位置非常接近,不合并的话后面连接时会出现大量重复框。
  • 将合并后的竖条按x中心排序,然后依次遍历。如果后一个竖条与前一个竖条的水平间距小于某个阈值,比如不超过50像素,并且垂直方向的重叠度超过0.7,就认为是同一行。
  • 对已经连接的文本行,取所有竖条中y1的最小值作为文本框的top,y2的最大值作为bottom。x1取第一个竖条的左边界,x2取最后一个竖条的右边界。如果训练时包含侧边精修,还需要把精修偏移加到左右边界上。

我把这个流程写成代码时踩过很多坑,最大的坑是“两个竖条属于同一行但垂直范围没有重叠”。比如中文文本行里的标点符号位置偏高或者偏低,会让竖条之间垂直方向重叠度不够,导致中间断开。解决方案是把重叠度判断从“必须相交”放宽成“中心高度差小于一定范围”,或者先把竖条做一次基于垂直投影的聚类,再在聚类内连接。

另外,如果图片里有弯曲文本、倾斜超过15度的文本,CTPN后处理很容易把同一行文字拆成几段。原因很好理解,竖条与竖条之间的水平间距在倾斜时变大,垂直重叠也变小。遇到这类场景,要么提前做图像旋转校正,要么干脆换一个能回归旋转框的模型,硬调CTPN参数不太划算。

4. 常见问题与排查技巧实录

4.1 长文本检测断裂怎么办

这是CTPN使用中最常见的问题。一排很长的文字,中间某个位置没有检测出竖条,后处理就会把文本分成两段。排查时先看断裂处到底是因为分类得分低,还是因为两个竖条之间连接条件不满足。前者说明特征没学好,后者说明后处理阈值太苛刻。

如果是分类得分低,可以考虑在训练时增加负难样本挖掘,比如专门把一些和文字很像的护栏、条纹、密集纹理图片加入训练。如果是连接条件太苛刻,可以把水平距离阈值调大一点,或者把垂直重叠度要求从0.7降到0.6。有些开源实现里还有一个“最大文本行长度”的限制,如果设得太短,长文本也会被强行截断,记得检查这一项。

4.2 小文本检测不到

CTPN的anchor高度最小值是11像素。如果图片里的文字高度只有五六个像素,那是很难检测出来的。一个办法是修改anchor列表,把最小值改成5或者7,重新生成训练样本。另一个办法是提高输入图片分辨率,把原图短边从600扩大到800甚至1000,这样小字在图上会更大,检测难度直线下降。

不过提高分辨率也会带来显存占用增大和推理变慢的问题。我通常先做一个尺度敏感性测试:把同一张图缩放成不同大小分别跑一遍,看文字高度在多少像素以上时模型表现稳定。有了这个基线,再决定是走“扩anchor”还是“提分辨率”的路线。项目驱动的话,优先保证最常出现的字号范围,不必让模型覆盖所有极端情况。

4.3 倾斜文本能不能用

CTPN本质上是为水平文本设计的,虽然它通过竖条连接可以容忍轻微倾斜,但只要文本倾斜角度超过15度,检测框就会明显不准确。倾斜文本的竖条之间水平间距被拉大,垂直范围错位,文本线构造算法几乎必然把它们切割成多段。

如果业务里倾斜文本占比很高,我建议不要继续在CTPN上耗时间,可以直接选支持旋转框的检测模型,比如EAST、DBNet的旋转框版本。当然,也可以先对输入图像做基于传统图像处理的角度校正,比如用霍夫变换找到文本行的主方向,旋转到水平再送进CTPN。这样能救一部分场景,但流程变长,实时性会下降。

4.4 重复框和漏框并存

有时候模型会同时出现两个问题:同一个文本行被检出多个框,某些短单词反而漏检。先别急着调NMS,我一般会先分别看分类得分和回归质量。如果重复框置信度都很高,说明是NMS合并阈值太低,把IoU阈值从0.5调到0.7就能压下去。如果短单词漏检,可能是正样本数量不足,模型对短文本特征不敏感,需要在训练时多保留高度较矮的anchor样本。

还有一个小坑:有些实现里的NMS只考虑竖条之间的重叠,忽略得分,容易把相邻两个文本行合并成一个。这时候建议改成带得分的加权NMS,或者先按得分排序再逐个合并。另外,不同文本行距离很近时,后处理里的“水平连接”也容易把它们串到一起,可以增加一个判断条件:两个竖条之间必须没有明显的间隙,或者间隙区域得分为低。

5. 我的实操体会与后续扩展

如果你已经跑通了CTPN,你会发现它的代码量和工程复杂度比后来的检测模型低很多,但它里面几乎囊括了文本检测的几大核心问题:多尺度、长序列依赖、后处理连接。我现在看一个OCR项目,会先问检测模块用的是哪类思路,如果项目里的文本大多是水平长行、背景不算太杂乱,那CTPN依然是一个性价比很高的选择。

我自己在复现CTPN时比较推荐先以“竖条预测+后处理连接”为主线做起来,暂时不做侧边精修。等主流程通了,再逐步加入双向LSTM的复杂变体和侧边精修。这样每一步的效果变化都看得见,出了问题也好定位。数据方面建议准备一份混了真实场景和合成文本的训练集,合成文本用来保证基本召回,真实场景用来磨难度。训练时记录一下每类anchor高度上的召回率,你会发现高度分布拍脑袋设计就会导致某些字号一直召不回,这不是网络的问题,是anchor先验没覆盖好。

CTPN之后出现了很多新方法,有直接预测旋转框的,也有用语义分割思路做文本区域检测的。新方法在弯曲文本、多方向文本上确实更强,但理解CTPN依然有价值。它的“先用固定宽度锚点做局部预测,再用序列模型做上下文融合”这两板斧,在很多其他序列检测任务里也能迁移。比如表格线检测、化学分子式结构检测,本质上都是“一长串结构候选项需要按顺序连接”的问题,完全可以借鉴CTPN的连接思路。

如果你正准备拿一个项目练手,我建议找一个票据识别或者试卷识别的数据,文字基本都是水平排列,标注相对规范。先用这个项目把CTPN完整跑通,再逐步去碰街景、视频字幕这类更难的数据。踩过几次坑之后你就会发现,文本检测的模型虽然一个比一个新,但真正决定项目能不能落地的,往往是对数据、anchor设计和后处理细节的理解。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询