说到场景文本检测,文本检测网络CTPN是绕不开的一座桥。做OCR的人对这个名字都不陌生,2016年提出的连接文本提案网络(Connectionist Text Proposal Network),几乎定义了一代场景文本检测的技术路线。哪怕现在基于分割、基于Transformer的检测器铺天盖地,CTPN里"把文本行当作序列来检测"的底层思想,依然值得反复琢磨。这篇文章,我会从设计动机、网络结构、训练细节、后处理算法几个层面把CTPN彻底拆开,再结合我在复现和项目移植过程中踩过的坑,把每个关键决策背后的为什么说清楚。不管你是准备复现这篇论文的新手,还是在调优老检测模型的老手,应该都能从中翻到点有用的东西。
先说它解决什么问题:给定一张自然场景图,需要框出其中所有文字的位置。CTPN用细粒度锚点预测文本片段,再用双向LSTM连接上下文,最后在推理阶段把片段拼成完整的文本行。适合的场景包括街景文字定位、票据识别、车牌识别、视频字幕检测等等。它最可贵的地方在于"兼顾了深度学习的表征能力和传统算法的后处理逻辑",这也让它在落地项目中特别好用。
1. 从OCR链路理解CTPN:文本检测到底难在哪
1.1 检测在OCR全流程中的位置
常规OCR链路是"检测+识别+后处理"三段式:检测负责回答"文字在哪",识别负责回答"文字是什么"。这两个问题难度完全不同。识别面对的是裁剪好的图片,无非是字符分类或者序列建模;而检测要在一整张复杂场景图里找出文字区域,既要处理尺度变化,又要排除背景干扰,本质上是一个更开放的视觉搜索任务。
打个比方:识别是让人读一句话,检测是让人在乱糟糟的房间里先找到印着那句话的纸片。CTPN负责的正是"找纸片"这一步。它输出的不是字符内容,而是一串坐标框,把候选文字区域准确圈出来交给下游识别模型。
很多人刚接触OCR时有个误区:觉得检测模型越花哨越好,把精力全放在换先进网络结构上,结果识别模型迟迟喂不上干净的输入。我在实际做某票据识别项目时就体会很深,检测框哪怕歪几个像素,下游识别精度都会明显抖动。所以CTPN这种把"定位问题"做得扎实的传统管线,在工程里反而非常耐打。
1.2 场景文本为什么这么难检测
印刷文档里的文本检测相对容易,白纸黑字、行整齐、背景干净。但自然场景完全不同:招牌上的字可能是艺术字体,车身广告的字是弯曲的,路牌上的字在逆光下模糊不清,还有Logo、按钮、横幅上的文本跟背景纹理纠缠在一起。传统方法用MSER(最大稳定极值区域)或SWT(笔画宽度变换)做连通域分析,在简单背景上还能凑合,一旦遇到纹理复杂、光照不均的画面,误检率就直线上升。
文本本身还有一个天然属性让检测器头疼:长宽比极端。单个字符接近正方形,但一整行文字的长宽比可以轻松到10:1、20:1。这对目标检测里的边界框回归是很大的挑战,因为回归大长框的坐标偏移时,误差会被放大。CTPN看到了这个矛盾,所以它偏偏不去直接回归整行,而是把文本行切碎成小段来做,后面会详细说。
1.3 CTPN给行业带来的范式转变
在CTPN之前,场景文本检测的主流思路是从图像底层特征里找字符组件,再通过规则合并。CTPN出现后,行业猛然发现:可以用深度网络直接预测文本片段,再用序列模型去理解文本行的上下文。它没有像后来的检测器那样一步到位端到端,而是把"深度学习特征提取"和"结构化后处理"巧妙地结合在了一起。
这个结合点的选择非常工程化。如果完全交给网络去回归整行,长文本稳定性差;如果完全用规则去连字符,又绕回了传统方法的脆弱。CTPN选择了中间路线:神经网络负责局部感知和上下文建模,后处理负责用明确规则拼接文本行。这种"网络做粗定位、规则做精整合"的思路,对我后来的项目设计影响很大。
2. 核心设计动机:为什么要分片检测文本行
2.1 整行检测的直觉困境
假设输入一张图,里面有一行20个字的标语,如果让检测头直接回归出覆盖整行的大矩形框,网络需要同时精确预测左上角和右下角的位置。问题在于,文本行跨度大,前景特征在空间上非常分散,一个全局框的内部同时包含大量背景,这让特征和回归目标的对齐变得很困难。训练时稍微有点偏移,IoU就跌得厉害,梯度信号也变得不稳定。
我见过直接拿通用目标检测框架做文本检测的项目,在小文本上表现还行,一旦遇到撑满画面的大横幅,检测框经常只框住一半,或者把旁边无关区域也带进来。这不是网络能力不够,而是任务形式和回归目标没匹配好。
2.2 把文本行当作序列来建模
CTPN的切入点非常聪明:文本本身就是一个字符序列,天然具有顺序性和局部连贯性。既然如此,检测器不必一次看到整行,只需要识别文本行的"片段",然后利用序列上下文把片段串起来。每个片段覆盖一小段文字,比如1024像素宽的区域,高度自适应,这样就避开了大长框回归问题。
原论文把anchor的宽度固定为16像素,这个数字不是随便定的。VGG16经过4次池化后,特征图相对于原图缩小了16倍,所以特征图上一个像素点恰好对应原图16x16的感受野窗口。固定宽度为16,本质上是在特征图粒度上做逐列预测,每一列锚点负责判断"这一竖条位置是否有文本片段"。高度则是可变的,因为文字高矮差异很大。
2.3 垂直锚点设计细节
CTPN沿用了Faster R-CNN的锚点机制,但做了改造:每个锚点不是常见的正方形或等比缩放,而是一个宽度固定、高度可变的细长框。在特征图的每个位置,同时放置10个不同高度的锚点,高度从小到大覆盖常见字体大小。这样每个位置都能匹配不同字号的文字。
注意:锚点的高度设计强烈影响小字和大字的召回效果。如果锚点最大高度只有128像素,检测大标题就会失效;如果最小高度太大,小票小字又会漏检。我在做票据项目时,专门统计了业务图片里的字符高度分布,再微调锚点区间,效果比直接套默认参数好很多。
锚点输出的回归目标是两个值:竖直方向的中心坐标偏移和高度偏移。原理类似Faster R-CNN的边框回归,但只在y方向做精细调整,x方向因为宽度固定,不回归。这里有个关键细节:文本片段之间是有重叠的,相邻锚点覆盖的区域会互相重叠一部分,这是为了保证文本行能被连续覆盖,避免出现空隙。
2.4 为什么固定宽度反而成了优势
表面上看,固定宽度好像限制了检测能力,比如遇到波浪形文本或竖直文本就抓瞎。但换个角度看,这个小尺度检测粒度反而让网络的任务变得极其简单:每个像素列只需要回答"这里有没有文字竖条",不需要考虑全局布局。大量重复的局部判断叠加起来,对文本行的覆盖率反而很高。
对水平文本这种最常见的形态来说,这种设计非常稳健。原论文在ICDAR 2013和ICDAR 2015基准上都拿到了当时最优的结果,而且推理速度很快,一张图在当时的GPU上只要一百多毫秒。这个性能在当年是很有说服力的。
3. 网络结构逐层拆解:VGG特征、BLSTM序列建模、全连接预测
3.1 骨干网络与特征提取
CTPN的骨干网络选用VGG16,这是当时图像分类任务里表现稳定的特征提取器。图片输入网络后,经过前5个卷积阶段,取conv5_3的输出作为共享特征图。为什么停在conv5而不是继续往上到全连接层?因为文本检测需要保留空间位置信息,全连接层会破坏空间结构,而conv5_3特征图尺寸适中,既保留了高层语义,又有足够的空间分辨率用于密集预测。
在conv5_3特征图上,再接一个3x3的滑动卷积层,把每个空间位置周围3x3邻域的上下文信息融合起来。这个操作一方面扩大了感受野,让每个位置能"看到"周围像素,另一方面相当于在共享特征上做了一个轻量的空间编码。生成的特征每个通道都代表某种局部模式,后面接循环网络时输入质量会更高。
3.2 双向LSTM的引入:文本上下文的关键一招
CTPN最标志性的创新是在检测网络里引入了双向LSTM(BLSTM)。每一行特征图上有W个位置(特征图宽度),每个位置携带一个特征向量。如果只看单个位置,很难判断这里到底是文字还是背景纹理;但如果把左右两边的信息串起来看,文字段落的连续性和语法特征就会显现出来。BLSTM就是干这个的:它从左到右和从右到左分别扫一遍特征序列,让每个位置的输出既包含左边信息又包含右边信息。
我在理解这一步时,喜欢把特征图想象成一段被逐列切片的时间序列。普通卷积网络处理的是空间网格,循环网络处理的是时间序列,CTPN把空间的一行当作时间维来建模,本质上是在用序列模型去捕捉文本行的历史依赖。它对标的是文本中"字与字彼此关联"这个内在属性,这也是它在文字检测上比纯卷积网络更稳的原因。
提示:BLSTM的隐藏层维度不是越大越好。我试过把隐层从128提到256,检测效果没有明显提升,反而显存占用和推理耗时都上去了。对这种局部序列建模任务,128维已经能有效建模上下文,过度加参只会让训练更慢、更容易过拟合。
3.3 输出分支:分类分、坐标分、边界精修分
BLSTM输出后接一个全连接层,在每个位置产生三组输出。第一组是分类分数,判断该位置的每个锚点是文本还是背景;第二组是y方向回归值,包含竖直中心偏移和高度偏移;第三组是side-refinement的边界精修值,用于之后微调文本框的左右边界。这三组输出共享特征,却在各自的任务空间里独立优化,属于非常典型的多任务学习结构。
side-refinement这个细节很多人会忽略,但它对最终框精度影响很大。文本提案的宽度固定为16像素,即使连接成线,整行文本的左边起点和右边终点往往不够精确,可能差了十几个像素。side-refinement在这时候预测每个文本提案相对于真实边界的水平偏移量,拿到文本线的首尾提案后做一次校正,就能把边界误差压到很小。
3.4 感受野与步长的匹配关系
理解CTPN的结构,必须把握住"步长16"这条线索。VGG16做了4次max pooling,每次缩小一半,所以特征图是原图的1/16。一个特征点对应原图16x16区域,锚点宽度设为16,就是让每个锚点在原图尺度上刚好覆盖一个特征点宽度。这个设计保证了锚点和特征位置的一一对应,也让"逐列扫描"有了物理意义。
如果你换用ResNet这类骨干,需要特别注意步长的变化。比如ResNet的conv特征图可能只有原图的1/32,这时如果还沿用16像素宽的锚点设计,锚点对应的特征粒度就变了,检测小字会明显吃力。我在复现时将特征图上采样到原图1/16再接入CTPN头部,才恢复了原来的检测精度。
4. 训练细节与损失函数设计:让网络学会判断片段
4.1 多任务损失定义
CTPN的训练是标准的端到端多任务优化。总损失由两部分组成:分类损失和回归损失。分类部分用softmax交叉熵,判断每个锚点是文本还是背景;回归部分用平滑L1损失,约束预测的竖直中心和高度的偏移量。原文还强调,只有被标记为正样本的锚点才参与回归损失的计算,背景锚点不产生回归梯度,这一点和Faster R-CNN保持一致。
平滑L1损失的好处在于梯度平稳,不像L2那样对大误差反应过激,也不像L1那样在零点附近不可导。在坐标回归这种数值范围变化较大的任务里,平滑L1能避免个别难例把训练带偏。我后来在做其他检测项目时,也一直沿用这个损失配方,几乎成了默认选项。
4.2 正负样本分配与难例挖掘
锚点的正负样本分配是整个训练的胜负手。对于每个文本区域的标注框,凡是IoU超过0.7的锚点标记为正样本,低于0.5的标记为负样本,两者之间的忽略掉。这个阈值和通用检测器保持一致,但场景文本有个特殊情况:小字目标的面积小,锚点高度分布稍微不合适,正样本数量可能剧烈不足。
负样本数量远大于正样本,如果不加处理,网络会被大量背景锚点带偏。常规做法是hard negative mining,即每次前向计算时,从负样本中按分数排序选取最难的一批参与反向传播。我做实验时发现,抽样比例和挖掘数量都需要根据数据集调节,过于激进会丢失背景多样性,过于保守则正样本梯度被淹没。
4.3 训练策略与数据组织
CTPN通常在ImageNet预训练的VGG16基础上微调,初始学习率设得很低,SGD优化器配合权重衰减。训练数据用公开的场景文本数据集,比如ICDAR系列标注数据。为了增强鲁棒性,训练时会做随机裁剪、缩放、颜色扰动等数据增强手段,让网络见过更多样的场景。
这里有一个在复现时很关键的细节:文本标注框的形态。ICDAR数据集里的标注是整行文本的四点框,而CTPN需要的是逐字符级别的片段目标。所以在训练前要做一步预处理,把整行标注切分成若干宽度约16像素的小片段,并为小片段分配对应锚点。很多复现失败都出在这一步,标签都没转对,模型当然学不出来。
注意:竖直文本的处理要单独考虑。CTPN原文主要针对水平文本,对竖直文本效果不稳定。我在中文场景里遇到过大量竖排文字,做法是把图片旋转90度后再检测,或者训练时把竖直文本的标注也旋转成水平方向,这样网络学起来更稳定。
5. 文本线构造:从散落片段到完整文本行的后处理
5.1 得分过滤:先砍掉低置信度的噪声
网络前向推理之后,会得到一大堆锚点提案,其中夹杂着大量低置信度的背景误判。文本线构造的第一步就是过滤:只保留文本得分高于阈值(常见设为0.7,也可以调底到0.6)的提案。这个阈值非常影响精度和召回率的平衡,阈值太高会漏掉边缘模糊的文本片段,阈值太低会把招牌纹理、窗户格栅等背景当成文本。
实际操作中,我会根据业务指标调这个值。做OCR识别时,稍微多一点片段还能靠后续连接或识别环节纠正,所以我会偏向低阈值保召回;做纯检测评估时,则会把阈值抬高保精度。没有万能值,只有动态调参。
5.2 贪心连接算法:片段如何串成线
过滤后的提案还要经过文本线构造算法才能形成完整文本行。算法按得分从高到低排序,对每个提案向右寻找可以连接的候选提案,连接条件大致有三条:两个提案在垂直方向上的重叠IoU要大于一个阈值(常见0.7),水平距离不能太远,候选提案本身得分够高。满足条件就合并成线,然后继续向右延伸。
这一步本质上是贪心搜索,复杂度很低,但效果出奇地好。因为垂直方向重叠保证了两个片段属于同一行文字,水平距离限制保证了连续性,得分保证了置信度。三个条件把"同一行"的本质约束都刻画出来了,所以很少出现跨越文本行乱串联的情况。
5.3 边界精修与最终输出
得到每条文本线后,再遍历线上的所有提案:y方向的精确定位由所有提案的预测结果做加权平均,x方向的左右边界用side-refinement输出的偏移量来校正。这一步能显著提高最终检测框的贴合度,尤其是文本行的首尾边缘。
整个后处理流程用代码实现不过几十行,但它决定了网络输出能不能变成可用的检测结果。我在评估一个检测模型时,从来不会只看网络输出就下结论,而是把后处理也当作模型的一部分来调优——文本线构造的连接距离、垂直IoU阈值、得分阈值,每个参数都值得单独做一轮实验。
6. 复现踩坑与排查实录:那些没人写进论文里的细节
6.1 训练不收敛或者loss乱跳:多半是标签没转对
我一个做毕设的朋友复现CTPN时,模型训了两天loss还是居高不下,检测框一片空白。我帮他一查,问题出在标签生成:他把整行标注直接当成了anchor的回归目标,没有切成16像素宽的小片段,也没有算锚点和切片之间的IoU。结果大部分锚点的回归目标都是错乱的,网络根本无法学习。
正确做法是先把文本行标注切碎,再逐锚点计算IoU并分配标签。切碎时要注意切片之间要有重叠,不能刚好无缝拼接,否则连接时容易出现断点。建议切片的水平间距设置为锚点步长的一半,这样每个字都能被多个片段覆盖。
6.2 检测框断成好几段:后处理连接太苛刻
长文本行是CTPN最典型的弱点之一。如果一行字特别长,BLSTM的上下文在远端会减弱,中间某个片段的得分偏低,过滤阶段就被砍掉了,文本线也就从中断裂。排查时可以先看过滤阈值是不是太高,把得分阈值从0.7降到0.6,断线现象通常立刻缓解。
如果阈值调低还断,就要检查文本线构造里的最大水平距离设置。有些实现默认只连接距离不超过几十像素的提案,遇到字距较大的文本就断了。把最大连接距离放宽一些,或者改为动态计算(根据相邻片段高度估算字间距),长文本的完整性会明显改善。
6.3 小字完全检不到:锚点和输入分辨率的问题
场景图里手机拍的小票、路牌上的小字,都容易漏检。这不是网络笨,而是锚点列表里小高度锚点太少,或者输入图片被压缩到很小,小字的特征细节全丢了。我做的项目里,把最小锚点高度从默认值往下降几个档位,同时把输入图片的短边保持在600像素以上,小字召回率立刻上来了。
代价是锚点数量变多、推理变慢。如果对速度有硬要求,可以只保留小尺寸锚点,不对大图做全局推断,而是用滑动窗口扫描可能包含小字的区域。这种"分区检测"策略在合发票项目里验证过,速度和召回都能兼顾。
6.4 密集文本互相粘连合并
当一行紧挨着一行,或者店铺招牌上的字非常密集时,文本线构造可能把两行文字串成一条线。原因通常是垂直IoU阈值设得太低,导致不同行的片段也能被连接。排查时优先提高垂直重叠阈值,从0.7往0.8调,同时收紧水平方向的最大距离限制。
如果多行文本属于不同朝向或者不同大小,还可以在连接前按提案高度做聚类,只允许高度相近的片段相连。这个改进实现成本很低,但在广告牌、标语这类场景里效果非常明显。
6.5 一个值得养的工程习惯:从规律到参数的实验备案
做CTPN这类模型的调优,最忌讳的是拍脑袋改参数。我自己的习惯是维护一个参数备案表,每次实验记录下输入分辨率、锚点高度区间、后处理阈值、检测指标变化,一个月下来就能摸清当前业务数据下参数和效果之间的规律。这套方法后来被我沿用到其他检测模型的调优上,省下大量重复试错时间。
7. 文本检测算法的后浪:与EAST、DBNet的对比与取舍
7.1 EAST给CTPN上了一课:全卷积直接回归四边形
EAST提出了一种更简洁的思路:用全卷积网络直接预测文本区域的得分图和几何属性,可以回归旋转矩形和任意四边形。相比CTPN,它不需要BLSTM,也不需要复杂的文本线构造后处理,推理速度更快。在ICDAR数据集上,EAST的精度和速度都表现抢眼。
但EAST也有自己的短板:对超长文本行和稠密文本的稳定性不如CTPN,因为它回归的是整个文本区域,极端长宽比问题依然存在。我当时在路牌识别项目里对比过两者,CTPN在水平长文本上更稳,EAST在近水平但略微倾斜的牌匾上更灵活。
7.2 DBNet把文本检测变成了分割问题
DBNet引入了可微分二值化机制,把文本检测建模成图像分割任务:网络输出每个像素属于文本的概率,再用可学习的二值化阈值把概率图转成二值图,最后通过轮廓提取得到文本框。这个思路能天然处理弯曲文本和任意形状文本,后处理也相对简单,是后来文本检测的主流方向之一。
不过要对齐的是,CTPN的"序列上下文建模"能力是分割方法没有的。DBNet虽然能分割出弯曲文本形状,但对文本行内部语义连续性的利用比较弱。在实际项目中,如果文本大多是规则的横排文字,CTPN的管线依然很能打;如果场景里弯曲、倾斜、艺术字占多数,DBNet更合适。
7.3 算法选型建议:不要盲目追新
模型选型的核心是看业务数据长什么样。我在某公司做文字识别中台时,总结了一套粗筛选逻辑:先统计业务图片里文本行的角度分布、长宽比、密度、字体大小。如果九成以上都是水平文本,直接选CTPN最省事;如果倾斜和旋转场景占比高,优先考虑EAST或DBNet;如果图像里弯曲文本特别多,再上分割类模型。
CTPN在今天已经不是各项指标的天花板,但它的设计思路和工程经验仍然大量渗透在后来的模型里。用垂直锚点缓解长宽比问题、用序列模型建模文本上下文、用结构化后处理保证输出质量,这三个思想在任何文本检测项目里都不过时。
从我个人经验来说,做完一遍CTPN的完整复现,你对文本检测任务的理解会比直接用现成模型深刻得多。它就像一本教科书,每层结构都对应一个明确的设计动机,每个参数都有可解释的作用。以后再遇到新的检测模型,你也能举一反三,快速抓住它的设计要害。
最后再分享一个实用小技巧:做CTPN调优时,先把后处理参数固定在中等水平,只调网络输入和锚点;等检测召回基本达标后,再回头精调后处理阈值拉精度。把两个环节同时调,变量太多,你很难分清楚效果变化到底来自哪里。一次只动一个旋钮,是我在所有模型调优里最朴素也最有效的经验。