查看原文
其他

【强基固本】手把手教你学DBNet



“强基固本,行稳致远”,科学研究离不开理论基础,人工智能学科更是需要数学、物理、神经科学等基础学科提供有力支撑,为了紧扣时代脉搏,我们推出“强基固本”专栏,讲解AI领域的基础知识,为你的科研学习提供助力,夯实理论基础,提升原始创新能力,敬请关注。

来源:知乎—痴迷计算机视觉
地址:https://zhuanlan.zhihu.com/p/368035566

编辑:人工智能前沿讲习

论文:https://arxiv.org/pdf/1911.08947.pdf
本文参考代码:https://github.com/MhLiao/DB
目前文字检测普遍的做法有两种:
  • 基于回归:Textboxes++、R2CNN,FEN等
  • 基于分割:Pixel-Link,PSENet,PMTD,LOMO,DBNet等
自然场景下,文本类型有水平文本、倾斜文本和曲形文本,基于分割的方法能够在像素水平进行预测,能更好的描述自然场景下中不同形状的文字。

01

传统分割方法 VS DBNet
图1
基于分割的方法最关键的步骤是对二值化map的后处理过程,即将分割方法产生的概率图转化为文本框的过程。

1)传统分割方法的后处理

图一蓝色箭头所示:首先,设定一个固定的阈值,将分割网络生成的概率图转换为二值图像;然后,采用像素聚类等启发式技术将像素分组为文本实例。
缺点:由于是在pixel层面操作,比较复杂且时间消耗较大。

2)DBNet后处理

DBNet的pipeline(如图1中红色箭头所示)目的是将二值化操作插入到分割网络中进行联合优化,这样网络可以自适应的预测图像中每一个像素点的阈值(区别去传统方法的固定阈值),从而可完全区分前景和背景的像素。
二值化阈值由网络学习得到,彻底将二值化这一步骤加入到网络里一起训练,这样最终的输出图对于阈值就会具有非常强的鲁棒性,在简化了后处理的同时提高了文本检测的效果。

02

DBNet网络结构
图2
整个流程如下
  1. 图像经过FPN网络结构,得到四个特征图,分别为1/4,1/8,1/16,1/32大小;
  2. 将四个特征图分别上采样为1/4大小,再concat,得到特征图F
  3. 由F得到 probability map (P) 和 threshold map (T)
  4. 通过P、T计算(通过可微分二值化DB,下文介绍) approximate binary map( 近似binary map    )
对于每个网络,一定要区分训练和推理阶段的不同:
  • 训练阶段:对P、T、B进行监督训练,P和B是用的相同的监督信号(label);
  • 推理阶段:通过P或B就可以得到文本框。
以上就是DBNet的大致流程,接下来,结合原理和代码,对每一个步骤展开进行阐述。首先是主干网络。

03

DBNet主干网络
主干网络是ResNet,在stage2-4中使用Deformable convolution来更好地检测长文本。然后增加了一个FPN结构,想必各位大佬都懂。经过FPN后,得到了四张大小分别为原图的1/4,1/8,1/16,1/32的特征图,如下图,
图3
再将四个特征图分别上采样至原图的1/4大小,然后将四个1/4大小的特征图concat,得到F。代码中上采样插值采用最近邻方法:
nn.Upsample(scale_factor=2, mode='nearest'))
网络代码:
https://github.com/MhLiao/DB/blob/master/decoders/seg_detector.py

04

由F得到P和T
先给出这部分的代码如下,fuse为特征图F:
binary = self.binarize(fuse) #由F得到Pif self.training: result = OrderedDict(binary=binary)else: return binaryif self.adaptive and self.training: if self.serial: fuse = torch.cat( (fuse, nn.functional.interpolate(          binary, fuse.shape[2:])), 1) thresh = self.thresh(fuse)
在得到F后,我们需要生成 probability map (P) 和 threshold map (T)。具体的网络架构师什么样的?通过代码进行分析。

4.1 probability map

代码第一行通过fuse得到了binary,binary为P,具体实现在self.binarize函数:
self.binarize = nn.Sequential(   nn.Conv2d(inner_channels, inner_channels // 4, 3, padding=1, bias=bias),   #shape:(batch,256,1/4W,1/4H) BatchNorm2d(inner_channels//4), nn.ReLU(inplace=True), nn.ConvTranspose2d(inner_channels//4, inner_channels//4, 2, 2), #shape:(batch,256,1/2W,1/2H) BatchNorm2d(inner_channels//4), nn.ReLU(inplace=True), nn.ConvTranspose2d(inner_channels//4, 1, 2, 2), #shape:(batch, W, H) nn.Sigmoid())
通过上述代码,F得到P的流程可以总结为:
1)F(shape:(batch,256,1/4W,1/4H))先经过卷积层,将通道压缩为输入的1/4,然后经过BN和relu,得到的特征图shape为(batch,64,1/4W,1/4H);(代码2至5行)
2)将得到的特征图进行反卷积操作,卷积核为(2,2),得到的特征图shape为(batch,256,1/2W,1/2H),此时为原图的1/2大小;(代码第6行)
3)再进行反卷积操作,同第二步,不同的是输出的特征图通道为1,得到的特征图shape为(batch,W,H),此时为原图大小。(代码第9行)
4)最后经过sigmoid函数,输出概率图,probability map。(代码第10行)
生成概率图后,上文提到传统的分割方法是设定一个固定的阈值,将分割网络生成的概率图转换为二值图像。而DBNet提出了自适应阈值,为计算approximate binary map,还需要threshold map。

4.2 threshold map

binary = self.binarize(fuse) #由F得到Pif self.training: result = OrderedDict(binary=binary) else: return binary if self.adaptive and self.training: if self.serial: fuse = torch.cat( (fuse, nn.functional.interpolate(         binary, fuse.shape[2:])), 1) thresh = self.thresh(fuse)   #由F得到T
代码第4行,如果在推理阶段,直接用P得到文本框。
代码第6行,如果在训练阶段且自适应阈值,则计算threshold map。具体实现在self.thresh函数,该函数与self.binarize函数实现一样,训练得到的参数不一样。
至此,我们就得到了probability map和threshold map,通过P和T,我们就可以计算出approximate binary map。


05

由P和T得到approximate binary map
代码很简单:
thresh_binary = self.step_function(binary, thresh)
其中,binary和thresh分别为P和T,在上文已经计算出来了。thresh_binary 就是approximate binary map,具体实现在self.step_function函数:
def step_function(self, x, y):        return torch.reciprocal(1 + torch.exp(-self.k * (x - y)))
通过这个函数可以看出,作者将P和T经过一个公式(可微分二值化),计算得到了approximate binary map(  )。这里也就验证了作者所说的将二值化操作插入到分割网络中进行联合优化。那这个公式是怎么得来的?下面讲一下可微分二值化的原理部分。


06

可微分二值化

1)标准二值化(Standard binarization)

作者之所以提出可微分二值化,是因为标准的二值化存在一个问题,从以下标准二值化公式看出,标准二值化操作会给训练带来梯度不可微的情况,如果将二值化操作加入到分割网络进行联合优化,是没法直接用SGD来做的。
其中:
t为预先设定的固定阈值

2)可微分二值化(Differentiable binarization)

为解决不可微的问题,作者引入了Differentiable Binarization,公式如下,
其中:
  为生成的approximate binary map,近似二值图;
   是probability map,    是threshold map;
k为放大倍数,根据实验取值为50。
由上文可以知道,    和    是输入,所以可以将其看为输入,即    ,这样来看的话,这个函数其实就是一个带系数k的sigmoid函数。

3)为什么可微分二值化会带来性能提升?

接下来,对比一下标准二值化和可微分二值化的的曲线图:
图4
从左图可以看出,DB曲线与标准二值化曲线具有很高的相似度,并且DB曲线是可微分的,从而达到了二值化的目的,也可加入分割网络联合优化。
DBNet性能提升的原因可以用梯度的反向传播来解释。首先对于该任务的分割网络,每个像素点都是二分类,即文字区域(正样本为1)和非文字区域(负样本为0),可以使用CELoss:
  
   ,那么DB函数可以表示为    ,所以正样本的Loss    和负样本的Loss    分别为:
根据链式法则求得偏导数为:
图4右边两幅图是DB函数正负标签的导数曲线,结合上述偏导数公式,可以得出以下结论:
1)梯度被放大因子k增大;
2)对比曲线中的黄线和蓝线,在错误预测的区域(
  的x<0区域,  的x>0区域)这一放大效果更为显著,也就是loss更大,进而可以促进模型的优化过程并产生更为清晰的预测结果。被放大区域为下图红框区域。
图5
重新回到DB整个流程,至此我们就得到了approximate binary map,接下来该计算损失函数,从而反向传播进行参数优化。要计算损失就需要标签,上文说到对P、T、B进行监督训练,P和B是用的相同的监督信号,为什么要对T进行监督?T可是作者加入的自适应阈值哦。


07

自适应阈值
图6
上图中的图c为没有监督的阈值图,虽然没有进行监督,但阈值图也会突出显示文本边界区域,这表明类似边界的阈值图有利于最终结果。因此,我们在阈值图上应用了类似边界的监督,以提供更好的指导。图d为带监督的阈值图,显然效果更好了。图b为概率图。
接下来看一下标签是怎么生成的。


08

标签生成

概率图P和近似二值图  使用同一个监督信号,加上阈值图T的标签,所以需要生成两个标签。
label generation

8.1 概率图标签

参考PSE网络,使用Vatti clipping算法,将原始的多边形文字区域G(红线区域)收缩到Gs(蓝线区域),收缩的偏移量D从原始多边形的周长和面积按照如下公式计算:
其中:
A是原始区域的面积,L是原始区域的周长,r是收缩系数,依经验设置为r=0.4。
标签为:蓝线区域内为文字区域,蓝线区域外为非文字区域。

8.2 阈值图标签

通过类似的过程,为阈值图生成标签。首先将原始的多边形文字区域G扩张到Gd(绿线区域),收缩偏移量D同概率图中的D。将收缩框Gs(蓝线)和扩张框Gd(绿线)之间的间隙视为文本区域的边界,计算这个间隙里每个像素点到原始图像边界G(红线)的归一化距离(最近线段的距离)。
计算完之后可以发现,扩张框Gd上的像素点和收缩框Gs上的像素点的归一化距离的值是最大的,并且文字红线上的像素点的值最小,为0。呈现出以红线为基准,向Gs和Gd方向的值逐渐变大。
所以再对计算完的这些值进行归一化,也就是除以偏移量D,此时Gs和Gd上的值变为1,再用1减去这些值。最后得到,红线上的值为1,Gs和Gd线上的值为0。呈现出以红线为基准,向Gs和Gd方向的值逐渐变小。此时Gs和Gd区域内的值取值范围为[0,1]。我们再次进行归一化,取值为(0, 1),比如[0.2, 0.8]等等,也就是最终的标签。
有了标签,就可以计算损失啦!!

09

损失函数
损失函数为概率图的损失    、二值化图的损失    和阈值图的损失    的和
α和β分别设置为1和10,因为  的结果会影响到  的结果,所以β设置为10.
  和  都采用BCE Loss(二元交叉熵),为平衡正负样本的比例,使用OHEM进行困难样本挖掘,正样本:负样本=1:3:
其中:
   表示使用OHEM进行采样,正负样本比例为1:3
  使用Gd中预测值和标签间的L1距离:
中:
   是Gd区域内的像素,注意是Gd区域内的所有像素点都要算,不仅仅是Gd和Gs区域内的;
   是阈值图的标签。
损失函数定义代码:
https://github.com/MhLiao/DB/blob/master/decoders/seg_detector_loss.py


10

推理阶段
在推理时,采用概率图或近似二值图便可计算出文本框,为了方便,作者选择了概率图,这样在推理时便可删掉阈值分支。文本框的形成可分为三个步骤:
1)使用固定阈值(0.2)对概率图(或近似二值图)进行二值化,得到二值图;
2)从二值图中得到连通区域(收缩文字区域);
3)将收缩文字区域按Vatti clipping算法的偏移系数D'进行扩张得到最终文本框,D'的计算公式如下:
其中:
A'、L'是收缩区域的面积、周长,r‘依经验设置为1.5(对应收缩比例r=0.4)。
代码如下:
def unclip(self, box, unclip_ratio=1.5): poly = Polygon(box) distance = poly.area * unclip_ratio / poly.length offset = pyclipper.PyclipperOffset() offset.AddPath(box, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) expanded = np.array(offset.Execute(distance)) return expanded

本文目的在于学术交流,并不代表本公众号赞同其观点或对其内容真实性负责,版权归原作者所有,如有侵权请告知删除。

“强基固本”历史文章


更多强基固本专栏文章,

请点击文章底部“阅读原文”查看



分享、点赞、在看,给个三连击呗!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存