【語義分割系列:四】Unet 論文閱讀翻譯筆記 醫學圖像 pytorch實現

UNet

2015 MICCAI
Olaf Ronneberger, Philipp Fischer, Thomas Brox


U-Net: Convolutional Networks for Biomedical Image Segmentation

5 Minute Teaser Presentation of the U-net 5分鐘預告片介紹

GitHub

時間順序: old ——> new

❤ orobix | U-net | 視網膜血管分割

ZijunDeng | PyTorch FCN、U-Net、SegNet、PSPNet 、GCN、DUC, HDC | 火車

zhixuhao | Unet, using Keras | ISBI 2012 神經元結構

milesial | Pytorch U-Net 圖像語義分割 | car

❤ LeeJunHyun | pytorch U-Net, R2U-Net, Attention U-Net, Attention R2U-Net | ISIC 2018 皮膚病變分析向黑色素瘤

qubvel | Unet Linknet FPN PSPNet | car

1、Introduce

基於FCN的一個語義分割網絡,適合用來做醫學圖像的分割。

  • contracting path :similar to an encoder
    用於獲取上下文信息(context)
  • expanding path :similar to a decoder
    擴張路徑用於精確的定位(localization)
  • data augmentation with elastic deformations

優勢:

  • 能夠從極少圖像端對端進行訓練,並且在ISBI競賽中,對於分割電子顯微鏡中的神經元結構的表現好於以前最好的方法(滑動窗口卷積網絡)。
  • 運行速度快

Architectural

  • VALID padding not SAME padding(因爲邊界用了鏡像處理)
  • matched lower and upper features after cropping lower feature(The cropping is necessary due to the loss of border pixels in every convolution:注意看圖1中的虛線框,就是與右分支對應的位置去crop左分支)
  • weighted cross entropy loss to separate instances(加權交叉熵損失)
  • elastic deformations (彈性變形)

Mirroring

邊界的鏡像處理

title

醫學圖像很大,所以要切成一張張小的patch,切成patch的時候因爲Unet網絡結構原因,適合切成Overlap-tile(重疊平鋪)的切圖。

Overlap-tile strategy for seamless segmentation of arbitrary large images
用於任意大圖像的無縫分割的重疊平鋪策略

觀察左圖:
白框是要分割區域,但是在切圖的時候要包含周圍區域(周圍overlap部分可以爲分割區域邊緣部分提供文理信息)
預測黃色區域中的分割,需要藍色區域內的圖像數據作爲輸入。

可以從右圖看出:
黃框區域分割結果沒有因爲切成小patch而造成分割情況不好。

通過鏡像推斷缺少輸入數據。

data augmentation

主要需要:移位、旋轉 不變性;變形、灰度值變化 魯棒性

  • 彈性變換

在3*3的網格上使用隨機位移矢量產生平滑形變,其中位移來自於10像素標準差的高斯分佈,且通過雙三次插值法計算得出。在收縮路徑的末尾的drop-out層進一步暗示了數據增強。

separation of touching objects of the same class

propose the use of a weighted loss, where the separating background labels between touching cells obtain a large weight in the loss function.

title

title

  • 預先計算每個ground truth segmentation的權值圖
  • 補償訓練數據集中某個類的不同像素出現的頻率
  • 使網絡學習我們在touch cells之間引入的小的分離邊界(如圖3c和d所示)

The separation border is computed using morphological operations. The
weight map is then computed as

title

Wc :the weight map to balance the class frequencies
d1 :表示到最近單元格邊界的距離
d2 :到第二個最近單元格邊界的距離
W0 = 10 pixels
σ = 5 pixels

Back propagation

反捲積可以進行反向傳播,所以整體Unet是可以反向傳播的。

2、Network

title

上圖爲U-net網絡結構圖(以最低分別率爲32*32爲例)。
每個藍色框對應一個多通道特徵圖(map),其中通道數在框頂標,x-y的大小位於框的左下角。
白色框表示複製的特徵圖。箭頭表示不同的操作。

U-net網絡由一個收縮路徑(左邊)和一個擴張路徑(右邊)組成。

title


  • contracting path 收縮路徑遵循典型的卷積網絡結構,其由兩個重複的3*3卷積核(無填充卷積,unpadded convolution)組成,且均使用修正線性單元(rectified linear unit,ReLU)激活函數和一個用於下采樣(downsample)的步長爲2的2*2 max pooling 操作,以及在每一個下采樣的步驟中,特徵通道數量都加倍。

  • expanding path 擴張路徑中,每一步都包含對特徵圖進行上採樣(upsample);然後用2*2的卷積核進行卷積運算(上卷積,up-convolution),用於減少一半的特徵通道數量;接着級聯收縮路徑中相應的裁剪後的特徵圖;再用兩個3*3的卷積核進行卷積運算,且均使用ReLU激活函數。

  • 在最後一層,利用1*1的卷積核進行卷積運算,將每個64維的特徵向量映射網絡的輸出層。

  • 網絡有23個卷積層。

  • 權重初始化:高斯(0,sigma=sqrt(2/N))
    圖像增強採用仿射變換

3、Train

  • favor large input tiles

  • reduce batch size to a single image

  • a high momentum (0.99)

  • energy function

    • computed by a pixel-wise soft-max over the final feature map
    • combined with the cross entropy loss function

網絡分爲四個主要部分:預處理,向下卷積,向上卷積,輸出映射

Example 1

運行你的第一個U-net進行圖像分割 ISBI2012 神經元 keras

easy code

  • data ISBI 30train/30test
  • 數據集很小,只有30張,數據增強很必要

圖像扭曲論文

Example 2

bag分類,FCN改成Unet,調試中發現的問題

錯誤1:Sequential 裏面有逗號

class upsamping(nn.Module):
    def __init__(self,in_channels,out_channels):
        super(upsamping,self).__init__()
        self.up=nn.Sequential(
            nn.interpolate(scale_factor=2, mode='bilinear'),
            nn.Conv2d(in_channels,out_channels,kernel_size=3,stride=1,padding=1,bias=True),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
            )
    def forward(self,x):
        x=self.up(x)
        return x

錯誤二:nn.函數名寫錯

nn.Conv2d(in_channels,out_channels,kernel_size=3,stride=1,padding=1,bias=True),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True),
self.maxpool=nn.MaxPool2d(kernel_size=2,stride=2)  # 2×2

錯誤三:忘記寫super和init

def __init__(self,in_channels=3,n_class=2): 
        super(UNet,self).__init__()  
        self.maxpool=nn.MaxPool2d(kernel_size=2,stride=2)  # 2×2

錯誤四:UNet通道搞錯了

忘記了cat之後通道數變了

錯誤五:Unet forward中output忘記return了

TypeError: sigmoid(): argument ‘input’ (position 1) must be Tensor, not NoneType

完整代碼:

UNet.py

import torch
import torch.nn as nn
from torchvision import models
from torchvision.models.vgg import VGG
import torch.nn.functional as F
from torch.nn import init

class conv_block(nn.Module):
    def __init__(self,in_channels,out_channels):
        super(conv_block,self).__init__()
        self.conv=nn.Sequential(
            nn.Conv2d(in_channels,out_channels,kernel_size=3,stride=1,padding=1,bias=True),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_channels,out_channels,kernel_size=3,stride=1,padding=1,bias=True),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
            )
    def forward(self,x):
        x=self.conv(x)
        return x

'''

nn.ReLU(inplace=True)
inplace=True意味着它將直接修改輸入,而不分配任何額外的輸出。它有時可以略微減少內存使用量,但可能並不總是有效的操作.


'''



class upsamping(nn.Module):
    def __init__(self,in_channels,out_channels):
        super(upsamping,self).__init__()
        self.up=nn.Sequential(
            # nn.interpolate(scale_factor=2, mode='bilinear'),
            nn.Upsample(scale_factor=2, mode='bilinear'),
            nn.Conv2d(in_channels,out_channels,kernel_size=3,stride=1,padding=1,bias=True),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
            )
    def forward(self,x):
        x=self.up(x)
        return x


class UNet(nn.Module):

    def __init__(self,in_channels=3,n_class=2): 
        super(UNet,self).__init__()  
        self.maxpool=nn.MaxPool2d(kernel_size=2,stride=2)  # 2×2
        self.conv1=conv_block(in_channels,64)
        self.conv2=conv_block(64,128)
        self.conv3=conv_block(128,256)
        self.conv4=conv_block(256,512)
        self.conv5=conv_block(512,1024)
        self.upsamping5=upsamping(1024,512)
        self.upconv5=conv_block(1024,512)
        self.upsamping4=upsamping(512,256)
        self.upconv4=conv_block(512,256)
        self.upsamping3=upsamping(256,128)
        self.upconv3=conv_block(256,128)
        self.upsamping2=upsamping(128,64)
        self.upconv2=conv_block(128,64)
        self.upconv1=nn.Conv2d(64,n_class,kernel_size=1,stride=1,padding=0)   #和上面conv比沒有 bias=True

    def forward(self,x):
        # contracting path 

        x1=self.conv1(x)  # [4, 64, 160, 160]

        x2=self.maxpool(x1)
        x2=self.conv2(x2)  # [4, 128, 80, 80]

        x3=self.maxpool(x2)
        x3=self.conv3(x3)  # [4, 256, 40, 40]

        x4=self.maxpool(x3)
        x4=self.conv4(x4)  # [4, 512, 20, 20]

        x5=self.maxpool(x4)
        x5=self.conv5(x5)  # [4, 1024, 10, 10]

        # expanding path 

        d5=self.upsamping5(x5)
        d5=torch.cat((x4,d5),dim=1)
        d5=self.upconv5(d5)  # [4, 512, 20, 20]

        d4=self.upsamping4(d5)
        d4=torch.cat((x3,d4),dim=1)
        d4=self.upconv4(d4)  # [4, 256, 40, 40]

        d3=self.upsamping3(d4)
        d3=torch.cat((x2,d3),dim=1)
        d3=self.upconv3(d3)  # [4, 128, 80, 80]

        d2=self.upsamping2(d3)
        d2=torch.cat((x1,d2),dim=1)
        d2=self.upconv2(d2)  # [4, 64, 160, 160]

        d1=self.upconv1(d2)   # [4, 2, 160, 160]
        return d1

train.py

# !/usr/bin/python
# -*- coding: utf-8 -*
'''
Train Bag with PyTorch.
FCN
U-Net
'''
from datetime import datetime

import matplotlib.pyplot as plt
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import visdom

from BagData import test_dataloader, train_dataloader
from FCN import FCN8s, FCN16s, FCN32s, FCNs, VGGNet
from UNET import UNet

# 命令行解析的庫文件
import sys
import argparse  


epo_num=50


# vis = visdom.Visdom()   # 可視化控件

device = torch.device('cuda:3' if torch.cuda.is_available() else 'cpu')
print('==> Building model..')

'''------------UNet--------------'''
unet_model = UNet()
unet_model = unet_model.to(device)

criterion = nn.BCELoss().to(device)
'''
BCELoss:二分類用的交叉熵,用的時候需要在該層前面加上 Sigmoid 函數。
CrossEntropyLoss:多分類用的交叉熵損失函數,用這個 loss 前面不需要加 Softmax 層。
'''
optimizer = optim.SGD(unet_model.parameters(), lr=1e-2, momentum=0.7)

all_train_iter_loss = []
all_test_iter_loss = []



# start timing
prev_time = datetime.now()
for epo in range(epo_num):  #  for each training  epoch
    print("\nEpoch: {}".format(epo))
    train_loss = 0
    unet_model.train()
    for index, (bag, bag_msk) in enumerate(train_dataloader): # 不停的 循環 這個 DataLoader 對象
        # if(index>0):
        #     sys.exit()

        # bag.shape is torch.Size([4, 3, 160, 160])
        # bag_msk.shape is torch.Size([4, 2, 160, 160])
        bag = bag.to(device)    # 
        bag_msk = bag_msk.to(device)
        optimizer.zero_grad()
        output = unet_model(bag)   # [4, 2, 160, 160]
        output = torch.sigmoid(output) #  ([4, 2, 160, 160])

        loss = criterion(output, bag_msk)
        loss.backward()
        optimizer.step()

        iter_loss = loss.item()  # 將tensor轉換成python的scalars
        all_train_iter_loss.append(iter_loss)
        train_loss += iter_loss  # 計算總train loss


        '''
        .cpu() : Some operations on tensors cannot be performed on cuda tensors so you need to move them to cpu first.
        detach() : 截斷反向傳播的梯度流。
        '''
        output_np = output.cpu().detach().numpy().copy() # output_np.shape = (4, 2, 160, 160)
        #  轉成熱度圖heatmap
        output_np = np.argmin(output_np, axis=1)    # 返回沿軸的最小值的索引   (4, 160, 160)

        '''
        min/max與np.argmin/np.argmax函數的功能不同:
        min/max  返回值,適合處理list等可迭代對象;
        np.argmin/np.argmax  返回最值所在的索引(下標)前者,適合處理numpy裏的核心數據結構ndarray(多維數組)
        Returns:   index_array : 下標組成的數組。shape與輸入數組a去掉axis的維度相同。
        '''
        bag_msk_np = bag_msk.cpu().detach().numpy().copy() # bag_msk_np.shape = (4, 2, 160, 160) 
        bag_msk_np = np.argmin(bag_msk_np, axis=1)   # (4, 160, 160)
        
        if np.mod(index, 15) == 0: # mod 返回兩個元素相除後的餘數
            print('epoch {}, {}/{},train loss is {}'.format(epo, index, len(train_dataloader), iter_loss))
        #     # vis.close()
        #     vis.images(output_np[:, None, :, :], win='train_pred', opts=dict(title='train prediction')) 
        #     vis.images(bag_msk_np[:, None, :, :], win='train_label', opts=dict(title='label'))
        #     vis.line(all_train_iter_loss, win='train_iter_loss',opts=dict(title='train iter loss'))
        # plt.subplot(1, 2, 1) 
        # plt.imshow(np.squeeze(bag_msk_np[0, ...]), 'gray')
        # plt.subplot(1, 2, 2) 
        # plt.imshow(np.squeeze(output_np[0, ...]), 'gray')
        # plt.pause(0.5)

    test_loss = 0
    unet_model.eval()
    with torch.no_grad():
        for index, (bag, bag_msk) in enumerate(test_dataloader):
            # if(index>0):
            #     sys.exit()
            bag = bag.to(device)
            bag_msk = bag_msk.to(device)

            optimizer.zero_grad()
            output = unet_model(bag)
            output = torch.sigmoid(output) # output.shape is torch.Size([4, 2, 160, 160])
            loss = criterion(output, bag_msk)
            iter_loss = loss.item()
            all_test_iter_loss.append(iter_loss)
            test_loss += iter_loss

            output_np = output.cpu().detach().numpy().copy() # output_np.shape = (4, 2, 160, 160)  
            output_np = np.argmin(output_np, axis=1)
            bag_msk_np = bag_msk.cpu().detach().numpy().copy() # bag_msk_np.shape = (4, 2, 160, 160) 
            bag_msk_np = np.argmin(bag_msk_np, axis=1)
    
            # if np.mod(index, 15) == 0:
                # print(r'Testing... Open http://localhost:8097/ to see test result.')
            #     # vis.close()
            #     vis.images(output_np[:, None, :, :], win='test_pred', opts=dict(title='test prediction')) 
            #     vis.images(bag_msk_np[:, None, :, :], win='test_label', opts=dict(title='label'))
            #     vis.line(all_test_iter_loss, win='test_iter_loss', opts=dict(title='test iter loss'))
            
            plt.subplot(1, 2, 1) 
            plt.imshow(np.squeeze(bag_msk_np[0, ...]), 'gray')
            plt.subplot(1, 2, 2) 
            plt.imshow(np.squeeze(output_np[0, ...]), 'gray')
            plt.pause(0.5)   # 暫停半秒鐘

    # len(train_dataloader)=135  540÷4  train_dataset total / batch size
    # len(test_dataloader) = 15   60÷4   test_dataset total / batch size

    cur_time = datetime.now()
    h, remainder = divmod((cur_time - prev_time).seconds, 3600)
    # python divmod() 函數把除數和餘數運算結果結合起來,返回一個包含商和餘數的元組(a // b, a % b)。
    m, s = divmod(remainder, 60)
    time_str = "Time %02d:%02d:%02d" % (h, m, s)
    prev_time = cur_time

    print('epoch train loss = %f, epoch test loss = %f, %s'
            %(train_loss/len(train_dataloader), test_loss/len(test_dataloader), time_str))
    

    if np.mod(epo, 5) == 0:
        torch.save(unet_model, 'checkpoints/unet_model_{}.pt'.format(epo))
        print('saveing checkpoints/unet_model{}.pt'.format(epo)) 

Example 3

LeeJunHyun | pytorch U-Net, R2U-Net, Attention U-Net, Attention R2U-Net | ISIC 2018 皮膚病變分析向黑色素瘤

黑色素瘤檢測的皮膚病變分析 論文 2018

title

先 下載 數據集,根據 dataset.py 中路徑放到響應位置 ISIC/dataset/ …
先 運行 dataset.py

source activate pytorch1.0
python dataset.py

title

數據準備完成

 python main.py

lr =0.01 (每50步乘0.1)
batchsize=1
epoch=250

在這裏插入圖片描述

4、Other

Why U-Net?

  • 多模態

醫療影像是具有多種模態的。以ISLES腦梗競賽爲例,其官方提供了CBF,MTT,CBV,TMAX,CTP等多種模態的數據。

設計網絡去提取不同模態的特徵feature。

參考論文:

Joint Sequence Learning and Cross-Modality Convolution for 3D Biomedical Segmentation(CVPR 2017)

Dense Multi-path U-Net for Ischemic Stroke Lesion Segmentation in Multiple Image Modalities.

  • 可解釋性

醫療影像最終是輔助醫生的臨牀診斷,所以網絡告訴醫生有沒有病是遠遠不夠的,醫生還要進一步的想知道,病竈在哪一層?哪個位置?分割了嗎?體積?結果是爲什麼?
比較常用的就是畫activation map。看網絡的哪些區域被激活了

參考論文:

Learning Deep Features for Discriminative Localization(CVPR2016)

Deep Learning for Identifying Metastatic Breast Cancer 2016

  • 醫學圖像語義較爲簡單、結構較爲固定,底層的特徵其實很重要

U-net利用了底層的特徵(同分辨率級聯)改善上採樣的信息不足。底層信息有助於提高精度,高層信息用來提取複雜特徵。

  • 和FCN區別

    • 多尺度
      基於FCNs做改進。U-Net特徵提取部分,每經過一個池化層就一個尺度,包括原圖尺度一共有5個尺度。

    • 上採樣部分
      每上採樣一次,就和特徵提取部分對應的通道數相同尺度融合,但是融合之前要將其crop。這裏的融合是concat而不是FCN的element-wise。

    • 適合超大 圖像分割,適合醫學圖像分割

design choices

沒發現這個有什麼用( ╯□╰ )

“DeepLab的四個對齊規則”:
(1)在所有卷積和pooling中使用奇數大小的內核。
(2)在所有卷積和pooling中使用SAME邊界條件。
(3)使用雙線性插值對特徵映射進行上採樣時,使用align_corners = True。
(4)使用height/width等於output_stride的倍數的輸入加1(for example, when the CNN output stride is 8, use height or width equal to 8 * n + 1, for some n, e.g., image HxW set to 321x513)

R2U-Net

2018 CVPR
Md Zahangir Alom, Mahmudul Hasan, Chris Yakopcic, Tarek M. Taha, Vijayan K. Asari

Recurrent Residual Convolutional Neural Network based on U-Net (R2U-Net) for Medical Image Segmentation

UNet++

2018 CVPR
Zongwei Zhou, Md Mahfuzur Rahman Siddiquee, Nima Tajbakhsh, Jianming Liang

UNet++: A Nested U-Net Architecture for Medical Image Segmentation

研習U-Net++

Attention U-Net

2018 CVPR
Ozan Oktay, Jo Schlemper, Loic Le Folgoc, Matthew Lee

Attention U-Net: Learning Where to Look for the Pancreas

nnU-Net

2019 CVPR
Fabian Isensee, Jens Petersen, Simon A. A. Kohl, Paul F. Jäger, Klaus H. Maier-Hein

nnU-Net: Breaking the Spell on Successful Medical Image Segmentation

github上暫時是空的,作者還沒更新代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章