FCL-Net: Towards accurate edge detection via Fine-scale Corrective Learning
本次阅读分析的论文来自于期刊Neural Networks(影响因子9.66,中科院计算机科学1区top)2022年发表的论文,研究内容为边缘检测。主要是翻译和一些自己的理解,如有写的不对的请在评论区指正,谢谢。
论文地址:https://www.sciencedirect.com/science/article/abs/pii/S0893608021004135
开源代码:https://github.com/DREAMXFAR/FCL-Net
如对该方向感兴趣,可以看我的关于EDTER的论文理解。
文章目录
一、引言
边缘检测任务旨在提取突出物体边缘,是计算机视觉中的一项长期任务。这项任务最近引起了越来越多的关注,因为它在许多高级视觉任务中发挥着基础作用,例如语义分割、目标检测等
目前,边缘检测任务主要是通过深度学习的方法,这些方法主要聚焦于基于分块patch的方式和基于多尺度特征融合的方式。 HED是第一个聚焦于多尺度特征提取和融合进行边缘提取的方式。这些方法取得了令人印象深刻的性能,但它们通常忽略了精细级别(fine-level)分支的学习能力不足,这对于整体融合性能至关重要,尤其是在像素级检测任务中
由于神经网络的分层性质,来自较低层的精细尺度预测通常会受到容易受噪声影响的过于详细的检测。如图1所示,精细尺度特征可以精确定位边缘像素,但会产生大量的假阳性预测。相比之下,具有强语义线索的高级特征在消除假阳性预测方面表现出优势。因此,精细尺度特征学习的能力较差可归因于缺乏去除无关信息的高级语义知识。限制精细尺度特征学习的另一个因素是融合阶段常用的全局加权(GW)策略(Xie&Tu,2015),它没有充分利用尺度敏感信息。GW融合策略将每个像素级预测视为在每个阶段同等重要。这种融合策略忽略了这样一个事实,即预测的边缘通常包含与尺度相对应的各种厚度,这不能充分利用精细尺度特征来准确检测特定的边缘
本文提出一种新的方法:FCL-Net。该方法包括两个关键模块:TAG和PW。简而言之,TAG是设计了一个自上而下的LSTM用于将粗尺度语义编码到隐藏状态中以用于细尺度特征学习的附加横向连接(即将高层语义信息用于指导底层精细的边缘预测)。PW: 在融合不同尺度的边缘预测时,PW独立地处理每个空间位置的贡献。因此,它可以同时考虑尺度和空间方差,这有助于预测最有把握的边缘。它还提供了一种更灵活的方式来利用所有刻度的侧面输出,并提高最终边缘检测结果。
总之,本文的主要贡献如下:(1)我们提出了一个自上而下的注意力引导(TAG)模块,该模块利用可学习的自上而下的LSTM从粗尺度预测中获得的语义线索。TAG显著提高了精细级别分支的学习能力,并增强了详细的边缘预测;(2) 我们设计了一个像素级加权(PW)模块,该模块生成像素级、尺度和空间变量权重,用于更精细地融合不同尺度预测。消融研究证明了PW模块的有效性;(3) 我们在三个公共基准数据集(即BSDS500、Multicue和BIPED)上评估我们的FCL Net。我们的方法显著优于基线,在BSDS500基准上实现了0.826的竞争ODS。实验结果证明了该方法的有效性。我们还报告了多尺度测试结果以供比较。
二、相关工作
主要就是介绍一些以前主流的方法,这里值得注意的是作者提到了有一篇20年的边缘检测模型BAN,与作者提出的想法类似,可以进一步学习。
三、方法
3.1 总体结构
总体结构如图2所示
该网络最主要的创新模块在于TAG模块和PW模块。接下来简单我将简单介绍FCL-Net的backbone模块、TAG模块和PW模块。
3.2 Backbone
作者的Backbone模块使用的是vgg模块,与BDCN网络的Backbone做了相同的处理,即将所有的全连接层以及最后一个用于图像-图像预测的池化层删除。并将第四个阶段的最大池化的strides修改为1去提高其特征图的分辨率,和第五阶段的特征图分辨率相同。代码如下:
### vgg16
backbone_mode = self.cfg.MODEL.backbone
pretrained = self.cfg.MODEL.pretrained
vgg16 = models.vgg16(pretrained=False)
if backbone_mode == 'vgg16':
vgg16 = models.vgg16(pretrained=pretrained).cuda()
if pretrained:
pre = torch.load('../../models/vgg16_bn-6c64b313.pth')
vgg16.load_state_dict(pre)
elif self.cfg.MODEL.backbone == 'vgg16_bn':
vgg16 = models.vgg16_bn(pretrained=False).cuda()
if pretrained:
pre = torch.load('../../models/vgg16_bn-6c64b313.pth')
vgg16.load_state_dict(pre)
# extract conv layers from vgg
self.conv1_1 = self.extract_layer(vgg16, backbone_mode, 1)
self.conv1_2 = self.extract_layer(vgg16, backbone_mode, 2)
self.conv2_1 = self.extract_layer(vgg16, backbone_mode, 3)
self.conv2_2 = self.extract_layer(vgg16, backbone_mode, 4)
self.conv3_1 = self.extract_layer(vgg16, backbone_mode, 5)
self.conv3_2 = self.extract_layer(vgg16, backbone_mode, 6)
self.conv3_3 = self.extract_layer(vgg16, backbone_mode, 7)
self.conv4_1 = self.extract_layer(vgg16, backbone_mode, 8)
self.conv4_2 = self.extract_layer(vgg16, backbone_mode, 9)
self.conv4_3 = self.extract_layer(vgg16, backbone_mode, 10)
self.conv5_1 = self.extract_layer(vgg16, backbone_mode, 11)
self.conv5_2 = self.extract_layer(vgg16, backbone_mode, 12)
self.conv5_3 = self.extract_layer(vgg16, backbone_mode, 13)
修改池化层的步长为1:
# check for the different between RCF and BDCN
if self.cfg.MODEL.change_conv5_dsn5:
for m in self.conv5_1:
if isinstance(m, nn.MaxPool2d):
m.stride = 1
m.padding = 1
self.dsn5_up = nn.ConvTranspose2d(1, 1, 16, stride=8)
VGG16的13个卷积层被手动分为五个阶段,如图2所示。注意,我们分别预测每个阶段的边缘图作为侧输出,以尽可能充分利用各尺度相关的特征和信息。所有的输出都进行深度监督(与gt进行loss计算)。为了方便起见,我们将第i级表示为i=1,2,Ⅰ, 其中I是阶段总数。鉴于更丰富的多尺度特征在边缘检测中至关重要(He et al.,2020;Liu et al.,2017),我们利用每个阶段所有层的丰富卷积特征,并使用SEM(He等人,2020)来丰富多尺度表示,如图3所示。接下来,由同一阶段的多个SEM提取的特征图被融合,并通过去卷积将其上采样到原始图像大小。此外,遵循两个独立的1×1卷积层来生成两个输出:特征图Fi,i=0,1,…Ⅰ,用于注意力引导和侧预测,以及相应的得分图Si,i=0,1,…,Ⅰ,表示像素级加权。
其中的SEM代码如下:
class MSBlock(nn.Module):
def __init__(self, c_in, rate=4):
super(MSBlock, self).__init__()
c_out = c_in
self.rate = rate
self.conv = nn.Conv2d(c_in, 32, 3, stride=1, padding=1)
self.relu = nn.ReLU(inplace=True)
dilation = self.rate*1 if self.rate >= 1 else 1
self.conv1 = nn.Conv2d(32, 32, 3, stride=1, dilation=dilation, padding=dilation)
self.relu1 = nn.ReLU(inplace=True)
dilation = self.rate*2 if self.rate >= 1 else 1
self.conv2 = nn.Conv2d(32, 32, 3, stride=1, dilation=dilation, padding=dilation)
self.relu2 = nn.ReLU(inplace=True)
dilation = self.rate*3 if self.rate >= 1 else 1
self.conv3 = nn.Conv2d(32, 32, 3, stride=1, dilation=dilation, padding=dilation)
self.relu3 = nn.ReLU(inplace=True)
self.conv_final = nn.Conv2d(32, 21, (1, 1), stride=1)
self._initialize_weights()
def forward(self, x):
o = self.relu(self.conv(x))
o1 = self.relu1(self.conv1(o))
o2 = self.relu2(self.conv2(o))
o3 = self.relu3(self.conv3(o))
out = o + o1 + o2 + o3
out_final = self.conv_final(out)
return out_final
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
m.weight.data.normal_(0, 0.01)
if m.bias is not None:
m.bias.data.zero_()
可以理解为使用不同dilation的空洞卷积对特征图进行处理,然后相加在使用一个1×1大小的卷积进行融合。
3.3 TAG模块
我们提出的自上而下注意力引导模块由四个独立的LSTM单元组成,以捕获不同阶段的尺度敏感依赖性。来自编码高语义信息的顶层的特征图分辨率低且粗糙,但具有较大的感受野。相比之下,浅层精细尺度特征包含丰富的细节和精确的位置信息。为了增强精细级别分支的特征学习能力并生成具有高语义的详细边缘图,我们通过自顶向下的LSTM构造了一系列边输出和特征图。TAG在后期阶段从粗的、语义高的预测开始,自适应地学习在来自先前预测的语义线索的指导下融合多尺度特征,并逐步生成具有语义的详细边缘图。因此,整个网络也可以被视为规模特定检测器的集成。图2所示的所有尺度的可视化边缘预测与我们的初衷一致。
这一模块的核心内容在图二中LSTM单元,该单元,作者借鉴的是ConvLSTMCell,该模块可以参考下图来理解。
主要是借助LSTM的三个门控结构来进行高层次的语义信息指导低层次的边缘学习。LSTM单元接受的输入是三个:1、前一个侧预测边缘特征图 C t − 1 C_{t-1} Ct−1,上一个单元传递下来的隐藏状态向量 H t − 1 H_{t-1} Ht−1,当前阶段的特征图 x t x_{t} xt。三个门执行特征细化的整个过程:忘记门 f t f_{t} ft根据当前的特征图来决定应该从前一个的预测中删除多少信息。它有助于去除粗糙的边缘并消除多余的细节和隐藏状态向量。输入们 i t i_{t} it通过筛选当前特征图和先前预测的边缘图来控制保留的内容。在这种情况下,通过修改隐藏的状态变量,诸如语义线索之类的有用信息将被自适应地保持。由于当前阶段的特征图具有更高的分辨率,输出门 o t o_{t} ot在补偿边缘细节和在先前粗尺度预测语义线索的指导下细化边缘像素的位置方面发挥着重要作用。三个门的数学定义参考原论文。其代码如下:
输入代码:
hs4, dsn4_up = self.lstmcell_4(dsn4_add_up, hs5, dsn5_up)
dsn4_final = self.crop_layer(dsn4_up, h, w)
ConvLSTMCell代码块:
class ConvLSTMCell(nn.Module):
def __init__(self, input_channels=21, hidden_channels=1, kernel_size=3):
super(ConvLSTMCell, self).__init__()
self.input_channels = input_channels
self.hidden_channels = hidden_channels
self.kernel_size = kernel_size
self.padding = int((kernel_size - 1) / 2)
# forget gate
self.Wxf = nn.Conv2d(self.input_channels, self.hidden_channels, self.kernel_size, 1, self.padding, bias=True)
self.Whf = nn.Conv2d(self.hidden_channels, self.hidden_channels, self.kernel_size, 1, self.padding, bias=False)
# input gate
self.Wxi = nn.Conv2d(self.input_channels, self.hidden_channels, self.kernel_size, 1, self.padding, bias=True)
self.Whi = nn.Conv2d(self.hidden_channels, self.hidden_channels, self.kernel_size, 1, self.padding, bias=False)
self.Wxc = nn.Conv2d(self.input_channels, self.hidden_channels, self.kernel_size, 1, self.padding, bias=True)
self.Whc = nn.Conv2d(self.hidden_channels, self.hidden_channels, self.kernel_size, 1, self.padding, bias=False)
# output gate
self.Wxo = nn.Conv2d(self.input_channels, self.hidden_channels, self.kernel_size, 1, self.padding, bias=True)
self.Who = nn.Conv2d(self.hidden_channels, self.hidden_channels, self.kernel_size, 1, self.padding, bias=False)
# initialize model
self.init_hidden()
def forward(self, x, h, c):
"""
:param x: (1,21,h,w)
:param h: (1,1,h,w)
:param c: (1,1,h,w)
:return: c, h
"""
# initialize if c,h is none
if h is None:
h = torch.zeros(1, self.hidden_channels, x.shape[2], x.shape[3]).cuda()
if c is None:
c = torch.zeros(1, self.hidden_channels, x.shape[2], x.shape[3]).cuda()
cf = torch.sigmoid(self.Wxf(x) + self.Whf(h))
ci = torch.sigmoid(self.Wxi(x) + self.Whi(h))
cc = cf * c + ci * torch.tanh(self.Wxc(x) + self.Whc(h))
co = torch.sigmoid(self.Wxo(x) + self.Who(h))
ch = co * torch.tanh(cc)
return ch, cc
def init_hidden(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
m.weight.data.normal_(0, 0.01)
if m.bias is not None:
m.bias.data.zero_()
print('initialized successfully! default normal')
print('-'*30)
3.4 pw模块
给定所有边输出,先前的工作通常使用额外的卷积层来直接学习合适的权重,并通过全局加权平均生成综合边缘图。然而,这种全局融合策略不能很好地处理细节。不同阶段的侧面输出,同一阶段的边缘图具有相似的thickness(此处不是很理解),这使得全局权重有点刚性,并由于融合权重的不平衡而限制了整体融合性能。比较不同的边输出,我们发现在后期感知到的边缘可能在早期检测到,而后期的预测更粗糙,语义更高。同时,由于接受野有限,不同阶段可能会产生矛盾的结果。具体地,在第一侧输出中被预测为边缘的一个像素可以被认为是粗尺度级中的非边缘或噪声。以图1中的侧面输出为例,风车的边缘(红色框中)在早期阶段被精确捕捉,但随着规模的增加而变得更粗糙。相反,如图1中的墙(蓝色框中)等大型物体的边缘显示出相反的性能,其中,随着尺度的增加,较粗尺度的边缘映射消除了更多的假阳性预测。
这里我的理解是之前的操作一般是将所有的侧边预测concat,然后使用1×1的卷积进行融合,这样每张侧边预测图在融合的时候会有一个权值进行融合。但是这样只是在图像层面进行融合,没有在像素层面继续融合。作者参考语义边缘提取DFF文章提出PW模块,在融合的构成中,给侧边预测图中每个像素都分配一个权值,即做到了像素级别的融合。
整个过程分为两个步骤:分数生成和像素级融合。首先,将从每个阶段中的多尺度特征提取的分数图连接起来,这些分数图表示每个阶段中边缘像素的置信度。然后我们使用1×1卷积来调整与其他阶段相关的分数,并将通道减少到与侧输出相同的数量。这些综合得分图表示每个像素在不同阶段的贡献。如果一个像素在特定阶段具有较高的分数,则我们更确信该像素是某个对象的边缘像素。我们还将max函数与softmax函数进行了比较,而softmax函数产生了更稳健的结果。因此,我们应用softmax函数来创建像素权重图W。其结构图如下:
代码如下:
if self.cfg.MODEL.ClsHead:
# set BDCN supervision mode
o1, o2, o3, o4, o5 = dsn1_final.detach(), dsn2_final.detach(), dsn3_final.detach(), dsn4_final.detach(), dsn5_final.detach()
# self.cfg.MODEL.supervision # d2s change weight
if self.cfg.MODEL.supervision == 's2d':
p1_1 = dsn1_final
p2_1 = dsn2_final + o1
p3_1 = dsn3_final + o2 + o1
p4_1 = dsn4_final + o3 + o2 + o1
p5_1 = dsn5_final + o4 + o3 + o2 + o1
elif self.cfg.MODEL.supervision == 'd2s': # add d2s 20200430
p1_1 = dsn1_final + o2 + o3 + o4 + o5
p2_1 = dsn2_final + o3 + o4 + o5
p3_1 = dsn3_final + o4 + o5
p4_1 = dsn4_final + o5
p5_1 = dsn5_final
elif self.cfg.MODEL.supervision == 'normal':
p1_1 = dsn1_final
p2_1 = dsn2_final
p3_1 = dsn3_final
p4_1 = dsn4_final
p5_1 = dsn5_final
concat = torch.cat((p1_1, p2_1, p3_1, p4_1, p5_1), 1)
dsn1_cls_score_up = self.dsn1_cls(dsn1_1 + dsn1_2)
dsn2_cls_score = self.dsn2_cls(dsn2_1 + dsn2_2)
dsn2_cls_score_up = self.dsn2_up_cls(dsn2_cls_score)
dsn3_cls_score = self.dsn3_cls(dsn3_1 + dsn3_2 + dsn3_3)
dsn3_cls_score_up = self.dsn3_up_cls(dsn3_cls_score)
dsn4_cls_score = self.dsn4_cls(dsn4_1 + dsn4_2 + dsn4_3)
dsn4_cls_score_up = self.dsn4_up_cls(dsn4_cls_score)
dsn5_cls_score = self.dsn5_cls(dsn5_1 + dsn5_2 + dsn5_3)
dsn5_cls_score_up = self.dsn5_up_cls(dsn5_cls_score)
score = [dsn1_cls_score_up, dsn2_cls_score_up, dsn3_cls_score_up, dsn4_cls_score_up, dsn5_cls_score_up]
min_h = min([i.shape[2] for i in score])
min_w = min([i.shape[3] for i in score])
for i in range(len(score)):
score[i] = self.crop_layer(score[i], min_h, min_w)
concat_score = torch.cat(score, 1)
score_final = self.cls_head(concat_score)
score_final = self.crop_layer(score_final, h, w)
# { 2020-09-22
dsn6_final = torch.sum(concat * score_final, axis=1).unsqueeze(0)
concat = torch.cat((concat, dsn6_final), 1) # 0924
dsn7_final = self.new_score_weighting(concat)
# end }
return p1_1, p2_1, p3_1, p4_1, p5_1, dsn6_final, dsn7_final # 0902 add dsn7_final
三、实验结果
四、总结
作者在三个基准数据集上评估了我们方法的有效性,包括BSDS500、Multicue和BIPED。实验结果表明,作者提出的FCL网络可以显著增强fine-scale特征学习,并生成更准确、更清晰的边缘图。与现有最先进的方法相比,作者的FCL-Net实现了具有竞争力的性能,在BSDS500的ODS指标为0.826。
总的来说,该篇论文所提出的方法给我带来了很多启发思考,值得精读。
本文仅对FCL的模型设计进行解读,有关该模型的实验结果以及更多的实现细节请大家参考文章前面的原作者论文和代码。