图像分割之U2-Net介绍

图像分割之U2-Net介绍

论文:https://arxiv.org/pdf/2005.09007.pdf
代码:https://codeload.github.com/NathanUA/U-2-Net/zip/master

U2net是基于unet提出的一种新的网络结构,同样基于encode-decode,作者参考FPN,Unet,在此基础之上提出了一种新模块RSU(ReSidual U-blocks) 经过测试,对于分割物体前背景取得了惊人的效果。同样具有较好的实时性,经过测试在P100上前向时间仅为18ms(56fps)。
下面是对U2net的一个介绍:

首先还是贴一下网络结构:

在这里插入图片描述
其实这个网络结构以及把一整个u2net完整的表示出来了,作者提出了一种名为RSU的新模块。对于每个RSU本身就是一个小号的Unet,最后所有的RSU用一种类似FPN的结构连接在一起。类似down-top top-down。通过这种方式来增加多尺度能力。获得了极为优秀的分割结果。

下面是RSU结构:

在这里插入图片描述
从这个结构可以很容易的看出,所谓的RSU其实就是一个很简单的Unet

作者通过类似FPN的结构,将多个Unet输出结果进行组合。最后进行合并,得到mask,通过多个loss在不同层的表现来进行更新。取得了非常理想的效果。

下面是整个U2net代码细节的介绍:
由于这个代码写的相对比较。。嗯。写的比较清晰,所以和以往的介绍不一样,我以代码注释的形式介绍好了:

class U2NET(nn.Module):

    def __init__(self,in_ch=3,out_ch=1):
        super(U2NET,self).__init__()
        self.stage1 = RSU7(in_ch,32,64)
		#对于每一个RSU来说,本质其实就是一个Unet,多个下采样多个上采样

        self.pool12 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
        self.stage2 = RSU6(64,32,128)
        self.pool23 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
        self.stage3 = RSU5(128,64,256)
        self.pool34 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
        self.stage4 = RSU4(256,128,512)
        self.pool45 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
        self.stage5 = RSU4F(512,256,512)
        self.pool56 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
        self.stage6 = RSU4F(512,256,512)
        # decoder
        self.stage5d = RSU4F(1024,256,512)
        self.stage4d = RSU4(1024,128,256)
        self.stage3d = RSU5(512,64,128)
        self.stage2d = RSU6(256,32,64)
        self.stage1d = RSU7(128,16,64)
        self.side1 = nn.Conv2d(64,out_ch,3,padding=1)
        self.side2 = nn.Conv2d(64,out_ch,3,padding=1)
        self.side3 = nn.Conv2d(128,out_ch,3,padding=1)
        self.side4 = nn.Conv2d(256,out_ch,3,padding=1)
        self.side5 = nn.Conv2d(512,out_ch,3,padding=1)
        self.side6 = nn.Conv2d(512,out_ch,3,padding=1)
        self.outconv = nn.Conv2d(6,out_ch,1)

    def forward(self,x):

        hx = x
        #stage 1
        hx1 = self.stage1(hx)
        #通过一个个Unet得到相应的mask
        hx = self.pool12(hx1)
        #stage 2
        hx2 = self.stage2(hx)
        hx = self.pool23(hx2)
        #stage 3
        hx3 = self.stage3(hx)
        hx = self.pool34(hx3)
        #stage 4
        hx4 = self.stage4(hx)
        hx = self.pool45(hx4)
        #stage 5
        hx5 = self.stage5(hx)
        hx = self.pool56(hx5)
        #stage 6
        hx6 = self.stage6(hx)
        hx6up = _upsample_like(hx6,hx5)
        #-------------------- decoder --------------------
        hx5d = self.stage5d(torch.cat((hx6up,hx5),1))
        #这里类似FPN。每个block的输出结果和上一个(下一个block)结果做融合(cat),然后输出。
        hx5dup = _upsample_like(hx5d,hx4)
        #由于每个block做了下采样,为了resize到原图,需要做一个上采样,这里作者直接用的双线性插值做的上采样
        hx4d = self.stage4d(torch.cat((hx5dup,hx4),1))
        hx4dup = _upsample_like(hx4d,hx3)
        hx3d = self.stage3d(torch.cat((hx4dup,hx3),1))
        hx3dup = _upsample_like(hx3d,hx2)
        hx2d = self.stage2d(torch.cat((hx3dup,hx2),1))
        hx2dup = _upsample_like(hx2d,hx1)
        hx1d = self.stage1d(torch.cat((hx2dup,hx1),1))


        #side output
        d1 = self.side1(hx1d)
		#这里本质就是把每一个block输出结果,转换成WxHx1的mask最后过一个sigmod就可以得到每个block输出的概率图。
        d2 = self.side2(hx2d)
        d2 = _upsample_like(d2,d1)

        d3 = self.side3(hx3d)
        d3 = _upsample_like(d3,d1)

        d4 = self.side4(hx4d)
        d4 = _upsample_like(d4,d1)

        d5 = self.side5(hx5d)
        d5 = _upsample_like(d5,d1)

        d6 = self.side6(hx6)
        d6 = _upsample_like(d6,d1)

        d0 = self.outconv(torch.cat((d1,d2,d3,d4,d5,d6),1))
		#6个blovk cat一起之后做特征融合,然后再做输出,结果就是d0的结果,其他的输出都是为了计算loss
        return F.sigmoid(d0), F.sigmoid(d1), F.sigmoid(d2), F.sigmoid(d3), F.sigmoid(d4), F.sigmoid(d5), F.sigmoid(d6)

具体的对于每个RSU来说:

class RSU7(nn.Module):#UNet07DRES(nn.Module):

    def __init__(self, in_ch=3, mid_ch=12, out_ch=3):
        super(RSU7,self).__init__()

        self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1)

        self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1)
        self.pool1 = nn.MaxPool2d(2,stride=2,ceil_mode=True)

        self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=1)
        self.pool2 = nn.MaxPool2d(2,stride=2,ceil_mode=True)

        self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=1)
        self.pool3 = nn.MaxPool2d(2,stride=2,ceil_mode=True)

        self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=1)
        self.pool4 = nn.MaxPool2d(2,stride=2,ceil_mode=True)

        self.rebnconv5 = REBNCONV(mid_ch,mid_ch,dirate=1)
        self.pool5 = nn.MaxPool2d(2,stride=2,ceil_mode=True)

        self.rebnconv6 = REBNCONV(mid_ch,mid_ch,dirate=1)

        self.rebnconv7 = REBNCONV(mid_ch,mid_ch,dirate=2)

        self.rebnconv6d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
        self.rebnconv5d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
        self.rebnconv4d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
        self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
        self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
        self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1)

    def forward(self,x):

        hx = x
        hxin = self.rebnconvin(hx)

        hx1 = self.rebnconv1(hxin)
        hx = self.pool1(hx1)

        hx2 = self.rebnconv2(hx)
        hx = self.pool2(hx2)

        hx3 = self.rebnconv3(hx)
        hx = self.pool3(hx3)

        hx4 = self.rebnconv4(hx)
        hx = self.pool4(hx4)

        hx5 = self.rebnconv5(hx)
        hx = self.pool5(hx5)

        hx6 = self.rebnconv6(hx)

        hx7 = self.rebnconv7(hx6)

        hx6d =  self.rebnconv6d(torch.cat((hx7,hx6),1))
        hx6dup = _upsample_like(hx6d,hx5)
		#双线性差值做的上采样
        hx5d =  self.rebnconv5d(torch.cat((hx6dup,hx5),1))
        hx5dup = _upsample_like(hx5d,hx4)

        hx4d = self.rebnconv4d(torch.cat((hx5dup,hx4),1))
        hx4dup = _upsample_like(hx4d,hx3)

        hx3d = self.rebnconv3d(torch.cat((hx4dup,hx3),1))
        hx3dup = _upsample_like(hx3d,hx2)

        hx2d = self.rebnconv2d(torch.cat((hx3dup,hx2),1))
        hx2dup = _upsample_like(hx2d,hx1)

        hx1d = self.rebnconv1d(torch.cat((hx2dup,hx1),1))

        return hx1d + hxin

对应结构图本质其实是这样的:
在这里插入图片描述
RSU和ResNet的残差结构其实非常的相似。只不过将权重层换成了Unet而已。

最后我们来看下loss:

由于作者把U2net分成了多个block,每个block输出一个loss,那么最后整个模型的loss自然就是这样的

在这里插入图片描述
然后带了一堆的解释,本质其实就是7个loss相加(6个block输出结果加1个特征融合后的结果):

def muti_bce_loss_fusion(d0, d1, d2, d3, d4, d5, d6, labels_v):

	loss0 = bce_loss(d0,labels_v)
	loss1 = bce_loss(d1,labels_v)
	loss2 = bce_loss(d2,labels_v)
	loss3 = bce_loss(d3,labels_v)
	loss4 = bce_loss(d4,labels_v)
	loss5 = bce_loss(d5,labels_v)
	loss6 = bce_loss(d6,labels_v)

	loss = loss0 + loss1 + loss2 + loss3 + loss4 + loss5 + loss6
	print("l0: %3f, l1: %3f, l2: %3f, l3: %3f, l4: %3f, l5: %3f, l6: %3f\n"%(loss0.data[0],loss1.data[0],loss2.data[0],loss3.data[0],loss4.data[0],loss5.data[0],loss6.data[0]))

	return loss0, loss

#每个mask计算二值交叉熵最后相加
d0, d1, d2, d3, d4, d5, d6 = net(inputs_v)
        loss2, loss = muti_bce_loss_fusion(d0, d1, d2, d3, d4, d5, d6, labels_v)

        loss.backward()
        optimizer.step()



可以说整个代码清晰简单好懂。但是效果确实好,我觉得好的分割检测模型就该这样,大道至简。

分割结果如下图所示,在我的实际使用中发现,如果只是前背景分割不涉及语义分割,在几个项目中有极为优秀的表现,即使背景很复杂,同时实时性非常优秀(p100前向能做到18ms)。
在这里插入图片描述

### U2-Net在缺陷分割任务中的效果 U2-Net作为一种改进型的U-Net架构,特别适用于资源受限环境下的高效处理。该模型不仅保持了U-Net原有的优点——即通过编码器-解码器结构有效捕捉图像中的多尺度上下文信息[^1],还引入了多个嵌套式的跳跃连接机制,进一步增强了细节保留能力。 实验表明,在Classic VOC, Blender VOC, Cityscapes等多种数据集测试环境下,即使面对复杂的场景变化以及类别不平衡等问题时,U2-Net依然能展现出优秀的鲁棒性和适应性[^3]。特别是在有标签样本数量有限的情况下,其性能优势更为明显。 然而值得注意的是,尽管U2-Net可以在一定程度上缓解因训练集中某些类别的实例数远少于其他类别而导致的表现不佳现象,但对于极端情况下的长尾分布问题仍然存在挑战。因此当应用于特定领域如工业产品表面瑕疵检测等实际应用场景时,可能还需要针对性地调整优化策略以获得更佳的结果。 ### 实现方法概述 为了实现高效的缺陷分割功能,U2-Net采用了独特的设计思路: - **多级监督学习**:不同于传统单阶段输出的方式,U2-Net利用中间层产生的特征映射作为辅助损失函数输入,使得整个网络能够在不同层次上接受反馈指导,进而提高最终预测的质量。 - **强化版残差模块**:在网络内部构建了一系列带有门控单元的记忆细胞,允许信息沿深层路径传递的同时减少梯度消失的风险;此外,这些记忆细胞还可以帮助过滤掉不必要的噪声干扰项,专注于重要区域的学习过程。 - **轻量化设计原则**:考虑到计算成本与部署灵活性的要求,开发者们精心裁剪每一部分参数规模,并采用分组卷积技术代替标准形式的操作符,既降低了内存占用又加快了推理速度。 ```python import torch.nn as nn class RSU7(nn.Module): # Residual U-block with seven convolutions def __init__(self, in_ch=3, mid_ch=12, out_ch=3): super(RSU7,self).__init__() self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1) self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1) ... ``` 此代码片段展示了U2-Net中使用的加强版本Residual U-block之一RSU7的具体定义方式,其中包含了七个连续执行的标准或膨胀卷积操作序列。 ### 常见问题分析 虽然U2-Net具备诸多先进特性,但在具体实施过程中仍会遇到一些典型难题: - 对于非常规形状或者微小目标物体难以准确定位; - 当背景复杂程度较高时容易误判正常部位为异常区; - 需要大量高质量标注图片支持才能达到理想识别率; - 训练初期可能会因为权重初始化不当而陷入局部最优解陷阱。 针对上述状况,建议采取如下措施加以改善: - 利用形态学运算预处理原始影像资料,突出潜在病变位置轮廓边界; - 结合先验知识设定合理的阈值范围区分疑似病灶与其他成分; - 积极收集更多种类丰富的正负样本扩充数据库容量; - 尝试不同的随机种子组合多次重复试验寻找稳定收敛模式。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值