今天为大家带来一篇CVPR2019,解决边缘提取问题,应该是目前最好的方法了。
论文地址:here
官方代码: 看博客最下面。官方在github上提供了pytorch 0.2 版的代码,我自己修改成了1.0版本,并且调试通过,有需要的朋友直接看最下面。
背景
作者发现,轮廓这个东西,尺度不同一的问题非常严重,比如说,一个人的边缘,手部的轮廓好头部的轮廓尺度就很不一样,手弯弯曲曲很小,这样的边缘很难学习;但头部或者身体,就很宽很大,对于网络来说很好学习。那么如何解决多尺度问题就是边缘提取的重点。作者就提出了一种双向的(bi-directional)级联网络结构,每个层的输出都被一个特定尺度的边缘label监督,这个特定尺度是网络自身学习到的(创新点,后文会详细讲)。为了增强每个层输出的特征,使用类似ASPP结构的SEM模块产生多尺度特征。最后的输出是许多多尺度特征的融合。
亮点
- 一般的多尺度特征,要么利用图像金字塔,最后做融合多个特征,这样的方式带来了重复的运算;要么利用非常深的网络提取不同stage的特征,这样带来了大量参数和推理时间。作者基于VGG的卷积部分,设计了轻量型的网络结构做边缘检测,取得了很好的效果。
- 作者认为每个层对应一个特征的尺度,比如说,浅层因为感受野的关系,只能聚焦在局部图案而高层就能注意到目标级别的信息,那么对最后的层以及中间的层使用相同的label做监督是否合理呢?当然不是了,作者希望每个层能学习自身能捕捉到的尺度,即浅层对手指的边缘提取较好,深层对目标级的边缘提取较好,最后对所有层的输出做融合。
- 提出了一种类似ASPP的模块,形式简单,不多介绍
- 使用很新颖的双向loss监督方式,让每个中间层学习自身合适的尺度。
方法
假设
Y
Y
Y是边缘的label,label中很小部分的边缘,也有大部位的边缘,意思是label中存在很大的尺度变化,那么我们将Y分解为不同尺度的叠加
Y
=
∑
Y
s
Y = \sum Y_s
Y=∑Ys
Y
s
Y_s
Ys是某一个特征的尺度的label。
很容易知道,人为的去分解label为s个尺度是不现实的,作者希望能用网络自动学习特定层的特定尺度。规定一些符号:
N
s
N_s
Ns是第S个卷积层的输出特征图,
D
s
D_s
Ds是专门用
N
s
N_s
Ns提取边缘的。那么第s层的监督方式为:
L
s
=
∑
∣
P
s
−
Y
s
∣
L_s = \sum |P_s-Y_s|
Ls=∑∣Ps−Ys∣
其中
P
s
=
D
s
(
N
s
)
P_s=D_s(N_s)
Ps=Ds(Ns)。如何能得到
Y
s
Y_s
Ys呢?
一种可能的方案是,用Y减去其他层的输出置信图
Y
i
(
i
∈
[
1
,
2
,
j
−
1
,
j
+
1
,
.
.
s
)
Y_i (i \in [1,2,j-1,j+1,..s)
Yi(i∈[1,2,j−1,j+1,..s)。这样就得到了
Y
j
Y_j
Yj的label,用它来做第J层的监督label。
Y
s
≈
∑
i
≠
s
P
i
Y_s \approx \sum \limits_{i \neq s}P_i
Ys≈i̸=s∑Pi
不过作者也证明了这样对于自动让中间层学习适合他们的尺度是没有用的,因为根据链式法则,
Y
^
对
任
何
一
个
P
i
\widehat Y 对任何一个P_i
Y
对任何一个Pi的导数都为1。那么对于任意的
D
i
D_i
Di都接受相同的梯度,意味着还是用相同的label去监督每一个层,这就犯了前面提到的,这不是更优的监督方式。
为了解决这个问题,作者设计了很新颖的方式。将
Y
s
Y_s
Ys再分解为两个互补的监督。其中一个忽略比s尺度更小监督
,另一个忽略尺度比s大的监督。这里的监督其实是每个中间层预测的结果,之所以称其为监督,因为是面向每个
D
s
D_s
Ds的输出对D本身参数而言,
P
s
P_s
Ps是D的监督。至于
P
s
P_s
Ps的究竟更倾向于捕捉什么尺度,由后面的部分自行决定。
公式可能不够直白。看下面的图
这是网络的部分结构,前三层是VGG的前三个阶段,SEM模块和1x1卷积的结合。每个ID block对应了一个VGG的stage和若干SEM,同时有多少SEM就有多少1x1的conv。
重点在最下面。先看block1,
P
1
s
2
d
P_1^{s2d}
P1s2d是第一个stage的其中一个输出,此时的预测图分辨率和原图一致,故无需上采样。它这个会送到之后的所有阶段,和其他stage经过上采样的输出相加去计算当前scale的loss。注意高stage是不沿着
s
2
d
s2d
s2d这条线往更低的尺度送的。高尺度会沿着
d
2
s
d2s
d2s线路往更低的尺度送,只是图上没有画出来,和
s
2
d
s2d
s2d的情况是类似的。
那么对于VGG作为backbone来说,loss将会有10个,同时网络最后结合所有的
P
s
P_s
Ps,concat一起在用1x1的conv融合,得到的结果也去计算loss、一共有11个loss。
值得一提的是,对一个中间层的监督是混杂了其他层的信息的,当然希望对某一层的监督不要去影响其他层,所以源码用了detach来断开梯度向其他层传播。
损失函数
这里还是简单说一下,对11个loss,分为两类。一类是side loss,就是前10个层的输出计算的loss,最后的loss就是融合前10个输出得到的预测图,用它计算得到的loss,称为fuse loss。
每个loss使用WBCE,带调整正负样本权重的loss。在两类loss之间使用两个超参数权衡两类loss,side loss的权重为0.5,fuse loss的权重是1.1。
有趣的实验
我们可以看出,每个BLOCK确实捕捉到了他们自身适合的边缘尺度。前几层的局部信息很强,后几层,输出的是目标级别的边缘。
代码实现注意的
使用detach来解决中间层s对其他层的影响。
最终的输出是fuse作为最终预测,但训练阶段需要用所有中间层数据和fuse,一共11个预测结果。
最终预测还需要加sigmioid激活函数,然后乘上255。,化成uint8型就能显示了。
我的一些实验结果
先付我自己修改的代码地址:使用方法请见文件描述