圖像分割必備知識點 | Unet詳解 理論+ 代碼

文章轉自:微信公衆號【機器學習煉丹術】。文章轉載或者交流聯繫作者微信:cyx645016617
喜歡的話可以參與文中的討論、在文章末尾點贊、在看點一下唄。

0 概述

語義分割(Semantic Segmentation)是圖像處理和機器視覺一個重要分支。與分類任務不同,語義分割需要判斷圖像每個像素點的類別,進行精確分割。語義分割目前在自動駕駛、自動摳圖、醫療影像等領域有着比較廣泛的應用。

上圖爲自動駕駛中的移動分割任務的分割結果,可以從一張圖片中有效的識別出汽車(深藍色),行人(紅色),紅綠燈(黃色),道路(淺紫色)等

Unet可以說是最常用、最簡單的一種分割模型了,它簡單、高效、易懂、容易構建、可以從小數據集中訓練。

Unet已經是非常老的分割模型了,是2015年《U-Net: Convolutional Networks for Biomedical Image Segmentation》提出的模型

論文連接:https://arxiv.org/abs/1505.04597

在Unet之前,則是更老的FCN網絡,FCN是Fully Convolutional Netowkrs的碎屑,不過這個基本上是一個框架,到現在的分割網絡,誰敢說用不到卷積層呢。 不過FCN網絡的準確度較低,不比Unet好用。現在還有Segnet,Mask RCNN,DeepLabv3+等網絡,不過今天我先介紹Unet,畢竟一口喫不成胖子。

1 Unet

Unet其實挺簡單的,所以今天的文章並不會很長。

1.1 提出初衷(不重要)

  1. Unet提出的初衷是爲了解決醫學圖像分割的問題;
  2. 一種U型的網絡結構來獲取上下文的信息和位置信息;
  3. 在2015年的ISBI cell tracking比賽中獲得了多個第一,一開始這是爲了解決細胞層面的分割的任務的

1.2 網絡結構

這個結構就是先對圖片進行卷積和池化,在Unet論文中是池化4次,比方說一開始的圖片是224x224的,那麼就會變成112x112,56x56,28x28,14x14四個不同尺寸的特徵。然後我們對14x14的特徵圖做上採樣或者反捲積,得到28x28的特徵圖,這個28x28的特徵圖與之前的28x28的特徵圖進行通道傷的拼接concat,然後再對拼接之後的特徵圖做卷積和上採樣,得到56x56的特徵圖,再與之前的56x56的特徵拼接,卷積,再上採樣,經過四次上採樣可以得到一個與輸入圖像尺寸相同的224x224的預測結果。

其實整體來看,這個也是一個Encoder-Decoder的結構:

Unet網絡非常的簡單,前半部分就是特徵提取,後半部分是上採樣。在一些文獻中把這種結構叫做編碼器-解碼器結構,由於網絡的整體結構是一個大些的英文字母U,所以叫做U-net。

  • Encoder:左半部分,由兩個3x3的卷積層(RELU)再加上一個2x2的maxpooling層組成一個下采樣的模塊(後面代碼可以看出);
  • Decoder:有半部分,由一個上採樣的卷積層(去卷積層)+特徵拼接concat+兩個3x3的卷積層(ReLU)反覆構成(代碼中可以看出來);

在當時,Unet相比更早提出的FCN網絡,使用拼接來作爲特徵圖的融合方式。

  • FCN是通過特徵圖對應像素值的相加來融合特徵的;
  • U-net通過通道數的拼接,這樣可以形成更厚的特徵,當然這樣會更佳消耗顯存;

Unet的好處我感覺是:網絡層越深得到的特徵圖,有着更大的視野域,淺層卷積關注紋理特徵,深層網絡關注本質的那種特徵,所以深層淺層特徵都是有格子的意義的;另外一點是通過反捲積得到的更大的尺寸的特徵圖的邊緣,是缺少信息的,畢竟每一次下采樣提煉特徵的同時,也必然會損失一些邊緣特徵,而失去的特徵並不能從上採樣中找回,因此通過特徵的拼接,來實現邊緣特徵的一個找回。

2 爲什麼Unet在醫療圖像分割種表現好

這是一個開放性的問題,大家如果有什麼看法歡迎回復討論。

大多數醫療影像語義分割任務都會首先用Unet作爲baseline,當然上一章節講解的Unet的優點肯定是可以當作這個問題的答案,這裏談一談醫療影像的特點

根據網友的討論,得到的結果:

  1. 醫療影像語義較爲簡單、結構固定。因此語義信息相比自動駕駛等較爲單一,因此並不需要去篩選過濾無用的信息。醫療影像的所有特徵都很重要,因此低級特徵和高級語義特徵都很重要,所以U型結構的skip connection結構(特徵拼接)更好派上用場

  2. 醫學影像的數據較少,獲取難度大,數據量可能只有幾百甚至不到100,因此如果使用大型的網絡例如DeepLabv3+等模型,很容易過擬合。大型網絡的優點是更強的圖像表述能力,而較爲簡單、數量少的醫學影像並沒有那麼多的內容需要表述,因此也有人發現在小數量級中,分割的SOTA模型與輕量的Unet並沒有神惡魔優勢

  3. 醫學影像往往是多模態的。比方說ISLES腦梗競賽中,官方提供了CBF,MTT,CBV等多中模態的數據(這一點聽不懂也無妨)。因此醫學影像任務中,往往需要自己設計網絡去提取不同的模態特徵,因此輕量結構簡單的Unet可以有更大的操作空間。

3 Pytorch模型代碼

這個是我自己寫的代碼,所以並不是很精簡,但是應該很好理解,和我之前講解的完全一致,(有任何問題都可以和我交流:cyx645016617):

import torch
import torch.nn as nn
import torch.nn.functional as F

class double_conv2d_bn(nn.Module):
    def __init__(self,in_channels,out_channels,kernel_size=3,strides=1,padding=1):
        super(double_conv2d_bn,self).__init__()
        self.conv1 = nn.Conv2d(in_channels,out_channels,
                               kernel_size=kernel_size,
                              stride = strides,padding=padding,bias=True)
        self.conv2 = nn.Conv2d(out_channels,out_channels,
                              kernel_size = kernel_size,
                              stride = strides,padding=padding,bias=True)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.bn2 = nn.BatchNorm2d(out_channels)
    
    def forward(self,x):
        out = F.relu(self.bn1(self.conv1(x)))
        out = F.relu(self.bn2(self.conv2(out)))
        return out
    
class deconv2d_bn(nn.Module):
    def __init__(self,in_channels,out_channels,kernel_size=2,strides=2):
        super(deconv2d_bn,self).__init__()
        self.conv1 = nn.ConvTranspose2d(in_channels,out_channels,
                                        kernel_size = kernel_size,
                                       stride = strides,bias=True)
        self.bn1 = nn.BatchNorm2d(out_channels)
        
    def forward(self,x):
        out = F.relu(self.bn1(self.conv1(x)))
        return out
    
class Unet(nn.Module):
    def __init__(self):
        super(Unet,self).__init__()
        self.layer1_conv = double_conv2d_bn(1,8)
        self.layer2_conv = double_conv2d_bn(8,16)
        self.layer3_conv = double_conv2d_bn(16,32)
        self.layer4_conv = double_conv2d_bn(32,64)
        self.layer5_conv = double_conv2d_bn(64,128)
        self.layer6_conv = double_conv2d_bn(128,64)
        self.layer7_conv = double_conv2d_bn(64,32)
        self.layer8_conv = double_conv2d_bn(32,16)
        self.layer9_conv = double_conv2d_bn(16,8)
        self.layer10_conv = nn.Conv2d(8,1,kernel_size=3,
                                     stride=1,padding=1,bias=True)
        
        self.deconv1 = deconv2d_bn(128,64)
        self.deconv2 = deconv2d_bn(64,32)
        self.deconv3 = deconv2d_bn(32,16)
        self.deconv4 = deconv2d_bn(16,8)
        
        self.sigmoid = nn.Sigmoid()
        
    def forward(self,x):
        conv1 = self.layer1_conv(x)
        pool1 = F.max_pool2d(conv1,2)
        
        conv2 = self.layer2_conv(pool1)
        pool2 = F.max_pool2d(conv2,2)
        
        conv3 = self.layer3_conv(pool2)
        pool3 = F.max_pool2d(conv3,2)
        
        conv4 = self.layer4_conv(pool3)
        pool4 = F.max_pool2d(conv4,2)
        
        conv5 = self.layer5_conv(pool4)
        
        convt1 = self.deconv1(conv5)
        concat1 = torch.cat([convt1,conv4],dim=1)
        conv6 = self.layer6_conv(concat1)
        
        convt2 = self.deconv2(conv6)
        concat2 = torch.cat([convt2,conv3],dim=1)
        conv7 = self.layer7_conv(concat2)
        
        convt3 = self.deconv3(conv7)
        concat3 = torch.cat([convt3,conv2],dim=1)
        conv8 = self.layer8_conv(concat3)
        
        convt4 = self.deconv4(conv8)
        concat4 = torch.cat([convt4,conv1],dim=1)
        conv9 = self.layer9_conv(concat4)
        outp = self.layer10_conv(conv9)
        outp = self.sigmoid(outp)
        return outp
    

model = Unet()
inp = torch.rand(10,1,224,224)
outp = model(inp)
print(outp.shape)
==> torch.Size([10, 1, 224, 224])

先把上採樣和兩個卷積層分別構建好,供Unet模型構建中重複使用。然後模型的輸出和輸入是相同的尺寸,說明模型可以運行。

參考博客:

  1. https://blog.csdn.net/wangdongwei0/article/details/82393275
  2. https://www.zhihu.com/question/269914775?sort=created
  3. https://zhuanlan.zhihu.com/p/90418337
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章