
做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 文本线构造算法与侧边精修模型输出的是一堆带得分的竖条后处理要把它们变成一个一个独立的文本行文本框。大致流程可以分成下面几步先按分类得分过滤只保留得分超过阈值常见0.7的竖条。对同一个位置附近的多个anchor做非极大值抑制把重复的、严重重叠的竖条合并掉。注意CTPN里的NMS和通用目标检测NMS略有区别它会优先考虑垂直方向上的重叠程度。把剩下的竖条按水平位置排序逐个检查两个相邻竖条是否满足连接条件。一般要求水平间距不要太大垂直范围的重叠比例至少达到一个阈值并且两者得分都符合标准。满足条件的竖条归入同一个文本行候选。对每个文本行候选把所有竖条的最小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保持原始宽高比优化器用SGDmomentum为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_thresh0.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的最小值作为文本框的topy2的最大值作为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设计和后处理细节的理解。