图像分割之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)。
在这里插入图片描述

  • 32
    点赞
  • 170
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
下面是一个简单的PyQt5界面实现U2Net图像分割的例子,使用PyTorch实现。 ``` import sys import os import numpy as np from PIL import Image from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QFileDialog from PyQt5.QtGui import QPixmap import torch import torchvision.transforms as transforms from model.u2net import U2NET class MainWindow(QMainWindow): def __init__(self): super().__init__() # 创建UI界面 self.initUI() # 加载模型 self.model = U2NET() self.model.load_state_dict(torch.load("u2net.pth", map_location=torch.device('cpu'))) self.model.eval() def initUI(self): # 设置窗口标题和大小 self.setWindowTitle("U2Net Image Segmentation") self.setGeometry(100, 100, 800, 600) # 创建标签和按钮 self.label = QLabel(self) self.label.setGeometry(25, 50, 750, 450) self.label.setStyleSheet("border: 1px solid black;") self.button = QPushButton("Select Image", self) self.button.setGeometry(25, 525, 150, 50) self.button.clicked.connect(self.selectImage) self.button2 = QPushButton("Segment Image", self) self.button2.setGeometry(200, 525, 150, 50) self.button2.clicked.connect(self.segmentImage) def selectImage(self): # 打开文件对话框,选择要处理的图像 options = QFileDialog.Options() options |= QFileDialog.DontUseNativeDialog fileName, _ = QFileDialog.getOpenFileName(self,"QFileDialog.getOpenFileName()", "","All Files (*);;Images (*.png *.jpg *.jpeg)", options=options) if fileName: # 加载图像并显示在标签上 pixmap = QPixmap(fileName) pixmap = pixmap.scaled(750, 450) self.label.setPixmap(pixmap) # 将图像转换为PyTorch tensor格式 self.input_image = Image.open(fileName).convert("RGB") self.transform = transforms.Compose([transforms.Resize((320, 320)), transforms.ToTensor(), transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))]) self.input_tensor = self.transform(self.input_image).unsqueeze(0) def segmentImage(self): # 对选择的图像进行分割 with torch.no_grad(): output_tensor = self.model(self.input_tensor) # 将输出转换为PIL Image格式 output_tensor = output_tensor.squeeze().numpy() output_tensor = np.where(output_tensor > 0.5, 1.0, 0.0) output_image = Image.fromarray((output_tensor * 255).astype(np.uint8)).convert("L") # 显示分割结果 output_pixmap = QPixmap.fromImage(ImageQt(output_image)) output_pixmap = output_pixmap.scaled(750, 450) self.label.setPixmap(output_pixmap) if __name__ == "__main__": # 创建应用程序和主窗口 app = QApplication(sys.argv) mainWindow = MainWindow() mainWindow.show() sys.exit(app.exec_()) ``` 在上面的代码中,我们首先创建了一个`MainWindow`类,它继承自`QMainWindow`类,并重写了`initUI`方法来创建UI界面。在`initUI`方法中,我们创建了一个标签和两个按钮,其中一个用于选择图像,另一个用于对图像进行分割。 在选择图像按钮的回调函数`selectImage`中,我们使用`QFileDialog`打开一个文件对话框,让用户选择要处理的图像。然后,我们使用`PIL`库来加载图像,并将其转换为PyTorch tensor格式。在转换过程中,我们使用了`transforms`模块来对图像进行缩放、标准化等预处理操作。 在对图像进行分割的按钮回调函数`segmentImage`中,我们将输入张量传递给已加载的U2Net模型,并得到输出张量。然后,我们将输出张量转换为PIL Image格式,并将其显示在标签上。在转换过程中,我们使用了NumPy来将输出张量转换为二值图像,使用`PIL`库将其转换为灰度图像,并使用`QPixmap`将其转换为Qt图像格式。 最后,我们在`__main__`函数中创建了应用程序和主窗口,并调用`show`方法来显示窗口。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值