2018-05-03 17:00:02 baiboya 阅读数 998
  • Erdas遥感影像处理入门实战教程(GIS思维)

    《Erdas遥感影像处理入门实战教程》以Erdas2010版本经典界面进行实战教学,设计12章内容,正式教学内容总共45课时,15个小时时长。从软件界面开始,到后的应用,适合入门级、初级、中级的人员学习、工作、教师教学参考。课程根据作者实际工作经验,以及采访学员需求,开展课程设计,实用加实战,会是你学习路上的好帮手。

    4616 人正在学习 去看看 黄晓军
NiftyNet 详细介绍

NiftyNet 是一个基于 TensorFlow 的开源卷积神经网络平台,用来研究医疗影像分析和影像导向的治疗。NiftyNet 有着模块化的架构设计,能够共享网络架构和预训练模型。使用该模块架构,你可以:

  • 使用内建工具,从建立好的预训练网络开始;

  • 根据自己的图像数据改造已有的网络;

  • 根据自己的图像分析问题快速构建新的解决方案。

特征

NiftyNet 现在支持医疗影像分割和生成式对抗网络。该开源平台并非面向临床使用,其他的特征包括:

  • 易于定制的网络组件接口;

  • 共享网络和预训练模块;

  • 支持 2D、2.5D、3D、4D 输入;

  • 支持多 GPU 的高效训练;

  • 多种先进网络的实现(HighRes3DNet、3D U-net、V-net、DeepMedic);

  • 对医疗影像分割的综合评估指标。

网址:https://pypi.org/project/NiftyNet/

2019-07-21 16:41:38 weixin_40779727 阅读数 291
  • Erdas遥感影像处理入门实战教程(GIS思维)

    《Erdas遥感影像处理入门实战教程》以Erdas2010版本经典界面进行实战教学,设计12章内容,正式教学内容总共45课时,15个小时时长。从软件界面开始,到后的应用,适合入门级、初级、中级的人员学习、工作、教师教学参考。课程根据作者实际工作经验,以及采访学员需求,开展课程设计,实用加实战,会是你学习路上的好帮手。

    4616 人正在学习 去看看 黄晓军

[深度学习]胶质瘤病灶分割技术文档(不断更新)

参考:

1. 数据预处理

1.1 数据处理

原始数据集为nii格式,常用网络需要jpg格式的输入,故首先需要将数据转化为jpg。python用于处理医疗影像图片的库有两个

  • SimpleITK

  • nibabel

    SimpleITK 和 Nibabel 的区别:

    • SimpleITK 加载数据是通道在前,如(18,512,512);

    • Nibabel 是 通道在后,如(512,512,18),其中18是图像通道数,即18张图像,可以把nii看成二维图像,也可以看成三维。

    • nibabel加载出来的图像被旋转了90度,横过来了; SimpleITK加载的图片旋转了180°

​	   [外链图片转存失败(img-thMjD0nm-1563698300986)(C:\Users\eveadam\AppData\Roaming\Typora\typora-user-images\1563617733970.png)]

选用SimpleITK提取图片

1.2 遇到的问题

  • 问题1 scipy.misc.imsave()弃用

    解决方式,scipy.misc.imsave()在高版本被取代改用imageio.imwrite()

  • 问题2转化jpg出现ValueError: Max value == min value, ambiguous given dtype

    解决方式,针对此类异常进行修正

    if mi == ma:    
        DataSlice = np.ones_like(DataSlice, dtype=np.uint8) * mi
    
  • 问题3 报错 sitk::ERROR: Unable to determine ImageIO reader for “…/…/RAWDATA/NEW_MultiCenter_Lesion/MS\HS\HSMS003\2D-T2FLAIR_labelnii”

    解决方式:修改该文件后缀

  • 问题4 读取出来的图片的有8种分辨率不一致

    resolution (256,204) (256,228) (256,256) (320,270) (320,320) (512,408) (512,512) (640,640)
    number 20 48 1928 521 269 100 2609 80

    解决方式:将8种分辨率置于不同文件夹,在后期决定如何训练。

  • 问题5: 读取的label像素值为0-255,需要将有病灶的区域置于1,其它区域置为0.

    解决方式:分3种情况保存

        if sum(sum(ImgArray)) == 0:
            #全是背景,直接保存
            imageio.imwrite(SavePath, ImgArray)
        else:
            if ImgArray.max() == 1:
                # 如果图片本身已经是正确标注,直接保存
                imageio.imwrite(SavePath, ImgArray)
            else:
                # label标签为0-255时,设定阈值50
                ImgArray[ImgArray[:] < 50] = 0
                ImgArray[ImgArray[:] >= 50] = 1  #将label有病灶置为1,没有病灶置为0
                imageio.imwrite(SavePath, ImgArray)
    

    [外链图片转存失败(img-iqIOSskf-1563698300990)(C:\Users\eveadam\AppData\Roaming\Typora\typora-user-images\1563693311014.png)]

1.3 数据预处理模块代码

数据预处理模块由4部分构成

[外链图片转存失败(img-afIB3vbC-1563698300991)(C:\Users\eveadam\AppData\Roaming\Typora\typora-user-images\1563698087536.png)]

  1. 数据读取

    • 将nii文件转化为jpg格式数据并储存

    • 数据命名格式:CQMS005_0000.jpg 最后4位为切片的维度。data和label名称一致,但放在不同的文件夹

      # coding=utf-8
      import nibabel as nib
      import SimpleITK as sitk
      import os
      import numpy as np
      import matplotlib.pyplot as plt
      
      
      DataRootPath = r'../../RAWDATA/NEW_MultiCenter_Lesion/MS'
      SaveRootPath = r'../../RAWDATA/RAWDATA_jpg'
      # 将MS所有图像转化为jpg
      
      
      def read_img(path):
          img = sitk.ReadImage(path)
          data = sitk.GetArrayFromImage(img)
          return data
      
      # 显示一个系列图
      def show_img(data):
          for i in range(data.shape[0]):
              plt.imshow(data[i, :, :],  cmap='gray')
              print(i)
              plt.show()
      
      
      def get_shape(DataPath, SavePath, SaveName, FileName):
          import SimpleITK as sitk
          img = sitk.ReadImage(DataPath)
          DataVolumn = sitk.GetArrayFromImage(img)
      
          for channel in range(DataVolumn.shape[0]):
              SaveNameOutput = SaveName + '_%04d.jpg' % channel
              OutputPath = os.path.join(SavePath, SaveNameOutput)
              DataSlice = DataVolumn[channel, :, :]
              ImageShape = (DataSlice.shape[0], DataSlice.shape[1])
              with open('%s.txt'%FileName, 'a') as f:
                  f.write(OutputPath +  '    ' +  str(ImageShape) + '\n')
      
      
      
      
      
      def nii2jpg(DataPath, SavePath, SaveName):
          import SimpleITK as sitk
          import imageio
          import numpy as np
          img = sitk.ReadImage(DataPath)
          DataVolumn = sitk.GetArrayFromImage(img)
      
          for channel in range(DataVolumn.shape[0]):
              SaveNameOutput = SaveName + '_%04d.jpg'%channel
              OutputPath = os.path.join(SavePath, SaveNameOutput)
              print(OutputPath)
      
              DataSlice = DataVolumn[channel,:,:]
              mi = np.nanmin(DataSlice)
              ma = np.nanmax(DataSlice)
              if mi == ma:
                  DataSlice = np.ones_like(DataSlice, dtype=np.uint8) * mi
              imageio.imsave(OutputPath, DataSlice)
      
      
      if __name__ == '__main__':
          PersonNames = os.listdir(DataRootPath)  # iteration_one
          for PersonName in PersonNames:  # how many sicker
              if len(PersonName) >= 8:  # 有一个乱码长度大于8
                  continue
              PersonNamePath = os.path.join(DataRootPath, PersonName)
      
              PersonPhotoTimes = os.listdir(PersonNamePath)  # iteration_two
              for PersonPhotoTime in PersonPhotoTimes:
                  if len(PersonPhotoTime) >= 10:  # 避免乱码
                      continue
                  PersonPerTimeImagePath = os.path.join(PersonNamePath, PersonPhotoTime)
                  FileNames = os.listdir(PersonPerTimeImagePath)
      
                  for FileName in FileNames:
                      if 'nii' not in FileName:
                          # 跳过非数据文件
                          continue
                      # print(FileName)
                      DataPath = os.path.join(PersonPerTimeImagePath, FileName)
                      SaveRootName = PersonPhotoTime
                      if 'label' not in FileName:
                          SavePath = os.path.join(SaveRootPath, 'data')
      
                          nii2jpg(DataPath=DataPath, SavePath=SavePath, SaveName=SaveRootName)
                          get_shape(DataPath=DataPath, SavePath=SavePath, SaveName=SaveRootName, FileName='ImageShape_data')
      
                      else:
                          SavePath = os.path.join(SaveRootPath, 'label')
                          nii2jpg(DataPath=DataPath, SavePath=SavePath, SaveName=SaveRootName)
                          get_shape(DataPath=DataPath, SavePath=SavePath, SaveName=SaveRootName, FileName='ImageShape_label')
      
      
  2. 检查异常项(主要检查同一data和label维度是否匹配的情况)

    import shutil
    import os
    
    AbnormalDataRootPath = r'../../RAWDATA/AbnormalData'
    
    with open('ImageShape_data.txt', 'r') as f1:
        with open('ImageShape_label.txt', 'r') as f2:
            i = 0
            for DataLine, LabelLine in zip(f1.readlines(), f2.readlines()):
                i += 1
                DataLineSplit = DataLine.split('    ')
                LabelLineSplit = LabelLine.split('    ')
    
                DataLineShape =DataLineSplit[1][:-1]
                LabelLineShape = LabelLineSplit[1][:-1]
                print(DataLineShape, LabelLineShape)
                if DataLineShape != LabelLineShape:
                    shutil.move(DataLineSplit[0], os.path.join(AbnormalDataRootPath, 'data'))
                    shutil.move(LabelLineSplit[0], os.path.join(AbnormalDataRootPath, 'label'))
    
    
  3. resolution classify 将jpg图片按照不同分辨率置于不同的文件夹

    # import pandas as pd
    import os
    import shutil
    
    def main():
        ImageShapeClass = []
        with open('ImageShape_data.txt', 'r') as f1:
            with open('ImageShape_label.txt', 'r') as f2:
                i = 0
                for DataLine, LabelLine in zip(f1.readlines(), f2.readlines()):
                    i += 1
                    DataLineSplit = DataLine.split('    ')
                    LabelLineSplit = LabelLine.split('    ')
    
                    DataLineShape = DataLineSplit[1][:-1]
                    LabelLineShape = LabelLineSplit[1][:-1]
    
                    DataLinePath = os.path.split(DataLineSplit[0])[0]
                    LabelLinePath = os.path.split(LabelLineSplit[0])[0]
    
                    FileName = os.path.split(DataLineSplit[0])[1]
                    if LabelLineShape != DataLineShape:  # if data's shape != label's shape skip
                        continue
                    else:
                        # create file path for different resolution image
    
                        if LabelLineShape not in ImageShapeClass:
                            ImageShapeClass.append(LabelLineShape)
                            NewPathFile = 'Resolution_' + str(LabelLineShape)
                            DataNewPath = os.path.join(DataLinePath, NewPathFile)
                            LabelNewPath = os.path.join(LabelLinePath, NewPathFile)
    
                            DataIsExists = os.path.exists(DataNewPath)
                            LabelIsExists = os.path.exists(LabelNewPath)
    
                            if not DataIsExists:
                                os.mkdir(DataNewPath)
                            if not LabelIsExists:
                                os.mkdir(LabelNewPath)
    
                        DataMovePath = os.path.join(DataLinePath, 'Resolution_' + str(LabelLineShape))
                        LabelMovePath = os.path.join(LabelLinePath, 'Resolution_' + str(LabelLineShape))
                        DataRawPath = DataLineSplit[0]
                        LabelRawPath = LabelLineSplit[0]
    
                        shutil.move(DataRawPath, DataMovePath)
                        shutil.move(LabelRawPath, LabelMovePath)
    
    if __name__ == '__main__':
        main()
    
    
    
  4. 将jpg格式的label转化为baseline的png格式,并对label的像素值进行处理,有病灶的区域置为1,其他区域置为0。

    import imageio
    import os
    
    ImageRootPath = r'..\..\RAWDATA\RAWDATA_jpg\label\Resolution_(640, 640)'
    SaveRootPath = r'..\..\RAWDATA\RAWDATA_jpg\label\Resolution_(640, 640)\label_png'
    
    
    def jpg2png(ImagePath, SavePath):
    
        ImgArray = imageio.imread(ImagePath)
        if sum(sum(ImgArray)) == 0:
            #全是背景,直接保存
            imageio.imwrite(SavePath, ImgArray)
        else:
            if ImgArray.max() == 1:
                # 如果图片本身已经是正确标注,直接保存
                imageio.imwrite(SavePath, ImgArray)
            else:
                # label标签为0-255时,设定阈值50
                ImgArray[ImgArray[:] < 50] = 0 #将label有病灶置为1,没有病灶置为0
                ImgArray[ImgArray[:] >= 50] = 1
    
                imageio.imwrite(SavePath, ImgArray)
    
    
    
    if __name__ == '__main__':
        ImageNames = os.listdir(ImageRootPath)
        print('{:_^50}'.format('start transform'))
        for ImageName in ImageNames:
            if 'jpg' not in ImageName:
                continue
    
            ImagePath = os.path.join(ImageRootPath, ImageName)
            ImageNameNew = ImageName.split('.')[0] + '.png' # png格式命名
            SavePath = os.path.join(SaveRootPath, ImageNameNew)
    
            jpg2png(ImagePath=ImagePath, SavePath=SavePath)
    
        print('{:_^4}'.format('Successful!'))
    
    

2 模型训练

2.1 baseline

采用MIT开源全卷积神经网络(FCN)进行baseline训练。程序已跑通,下周使用公司GPU完整运行。

MIT开源全卷积神经网络(FCN)源码地址:

[外链图片转存失败(img-yprT6Xr3-1563698300993)(C:\Users\eveadam\AppData\Roaming\Typora\typora-user-images\1563697418017.png)]

未完待续。。。

2018-12-29 10:42:16 m0_37477175 阅读数 945
  • Erdas遥感影像处理入门实战教程(GIS思维)

    《Erdas遥感影像处理入门实战教程》以Erdas2010版本经典界面进行实战教学,设计12章内容,正式教学内容总共45课时,15个小时时长。从软件界面开始,到后的应用,适合入门级、初级、中级的人员学习、工作、教师教学参考。课程根据作者实际工作经验,以及采访学员需求,开展课程设计,实用加实战,会是你学习路上的好帮手。

    4616 人正在学习 去看看 黄晓军

MICCAI 2018的论文
在这里插入图片描述

前言

深度学习的迅速发展,使得在医疗影像分割上也有很多深度学习模型。但是论文提出,大部分的网络只能处理数量较少的类别(<10),并且在3D影像分割中,很难处理小目标,解决数据极度不均衡的问题。本论文提出了新的3D网络网络结构和新的loss function。这个loss function是收到了focal loss的启发,称为指数对数损失函数(Exponential Logarithmic loss),可以通过不同标签的样本数量的相对大小,以及他们的分割难度来平衡各个标签。论文使用20个标签的脑分割图像,达到了DICE 82%,其中最大最小标签之间的大小比例是0.14%。训练不需要100个epoch便可以达到这个精度。
区域大小和Dice分数之间的相关性:
在使用DICE loss时,对小目标是十分不利的,因为小目标一旦有部分像素预测错误,那么就会导致DICE大幅度的下降。
先看一下GDL(the generalized Dice loss),公式如下(标签数量为2):
GDL=12l=12wlnrlnplnl=12wlnrln+pln GDL = 1 - 2\frac{\sum_{l=1}^{2}w_l\sum_nr_{ln}p_{ln}}{\sum_{l=1}^{2}w_l\sum_nr_{ln} + p_{ln}}
其中rlnr_{ln}为类别l在第n个像素的标准值(GT),而plnp_{ln}为相应的预测概率值。此处最关键的是wlw_l,为每个类别的权重。其中wl=1(n=1Nrln)2w_l = \frac{1}{(\sum_{n=1}^{N}r_{ln})^2},直观的感觉就是将得到的DICE值除以每个label的所有的真值,说白了就是进行了均衡化的操作,将大物体和小物体放到同一水平上再进行对比。
打个比方吧,一立方米棉花和一立方分米的铁,分别切掉相同大小的东西,要比较对那个物体的影响最大,质量丢失的最多。如果直接比较切下来的部分那么:
&lt;&lt;切下的棉花 &lt;&lt; 切下的铁
这对棉花不公平,那么改进为:
\frac{切下的棉花} {所有的棉花} 与 \frac{切下的铁} {所有的铁}
相比较,归一化到同一水平下,就可以同时兼顾大物体和小物体。
但是此篇论文觉得而且这种情况跟不同标签之间的相对尺寸无关,但是可以通过标签频率来进行平衡。
值得实验探究~~~

Exponential Logarithmic loss

结合了focal loss以及Dice loss。此loss的公式如下:
LEXP=wdiceLDice+wCrossLCrossL_{EXP} = w_{dice}*L_{Dice} + w_{Cross}*L_{Cross},此时新增添了两个参数权重分别是wDicew_{Dice}wCrossw_{Cross},而LDiceL_{Dice}为 指数log Dice损失(the exponential logarithmic Dice loss),LCrossL_{Cross}为指数交叉熵损失。
LDice=E[(ln(Dicei))γDice]L_{Dice} = E[(-ln({Dice}_i))^{\gamma_{Dice}}],其中Dicei=(xδil(x)pi(x))+ϵ(xδil(x)+pi(x))+ϵDice_i = \frac{2(\sum_x\delta_{il}(x)p_i(x)) + \epsilon}{(\sum_x\delta_{il}(x) + p_i(x) ) + \epsilon}LCross=E[wl(ln(pl(x)))rCross]L_{Cross} = E[w_l(-ln(p_l(x)))^{r_{Cross}}],x为体素的位置,i为label,l为在位置x的ground-truth。pi(x)p_i(x)为softmax之后的概率值。其中wl=(kfkfl)0.5w_l = (\frac{\sum_kf_k}{f_l})^{0.5}fkf_k为标签k的出现频率,这个参数可以减小出现频率较高的类别权重。γDice\gamma^{Dice}γCross\gamma^{Cross},提升非线性的作用,如下图显示的是不同的指数log非线性表现:
在这里插入图片描述

收敛快,计算效率高的网络结构(结合skip connections和deep supervision)

在这里插入图片描述

2020-02-10 21:32:58 weixin_35811044 阅读数 561
  • Erdas遥感影像处理入门实战教程(GIS思维)

    《Erdas遥感影像处理入门实战教程》以Erdas2010版本经典界面进行实战教学,设计12章内容,正式教学内容总共45课时,15个小时时长。从软件界面开始,到后的应用,适合入门级、初级、中级的人员学习、工作、教师教学参考。课程根据作者实际工作经验,以及采访学员需求,开展课程设计,实用加实战,会是你学习路上的好帮手。

    4616 人正在学习 去看看 黄晓军

Unet 和 Unet++


自从2015年,全卷积网络(FCN)诞生,图像分割在深度学习领域掀起旋风,同年稍晚Unet诞生,号称可用极少数据获取优质的结果,在数据可贵的医疗影像领域称王称霸。
2018年对Unet的改进Unet++诞生。
此外还有DeepLap、PSPNet等其他优质的图像分割网络,但它们的本质基本都是编码器-解码器(encode-decode)网络。编码器通常是一个预训练的分类网络,像 VGG、ResNet等,然后是一个解码器网络。这些架构不同的地方主要在于解码器网络。

Unet

  • 网络结构
    Unet网络结构如其名呈现一个U字形,即由卷积和池化单元构成,左半边为编码器即如传统的分类网络是“下采样阶段”,右半边为解码器是“上采样阶段”,中间的灰色箭头为跳跃连接,将浅层的特征与深层的特征拼接,因为浅层通常可以抓取图像的一些简单的特征,比如边界,颜色。深层经过的卷积操作多抓取到图像的一些说不清道不明的抽象特征,将浅深同时利用起来为上上策,同时允许解码器学习在编码器池化下采样中丢失的相关特征。纵观整个网络Unet其实有点带有残差(residual)结构的思想。结构如下两幅图:
    在这里插入图片描述
    在这里插入图片描述
  • Keras实现unet
    论文“上采样”采用的线性插值方式,keras的api即UpSampling2D,这里采用转置卷积的方式keras的api即Conv2DTranspose。论文中并没有采用padding,因此输入和输出大小并不一样,这里采用padding保持输入与输出大小一致。
def unet(input_size = (512,512,3),base_filter_num=64):
    inputs = Input(input_size)
    conv1 = Conv2D(base_filter_num, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(inputs)
    conv1 = Conv2D(base_filter_num, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv1)
    pool1 = MaxPooling2D(pool_size=(2, 2))(conv1)

    conv2 = Conv2D(base_filter_num*2, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool1)
    conv2 = Conv2D(base_filter_num*2, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv2)
    pool2 = MaxPooling2D(pool_size=(2, 2))(conv2)

    conv3 = Conv2D(base_filter_num*4, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool2)
    conv3 = Conv2D(base_filter_num*4, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv3)
    pool3 = MaxPooling2D(pool_size=(2, 2))(conv3)

    conv4 = Conv2D(base_filter_num*8, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool3)
    conv4 = Conv2D(base_filter_num*8, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv4)
    pool4 = MaxPooling2D(pool_size=(2, 2))(conv4)

    conv5 = Conv2D(base_filter_num*16, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool4)
    conv5 = Conv2D(base_filter_num*16, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv5)

    up6 = Conv2DTranspose(base_filter_num*8, (2, 2), strides=(2, 2), padding='same')(conv5)
    # up6 = Conv2D(base_filter_num*8, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(conv5))
    merge6 = concatenate([conv4,up6], axis = -1)
    conv6 = Conv2D(base_filter_num*8, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge6)
    conv6 = Conv2D(base_filter_num*8, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv6)

    up7 = Conv2DTranspose(base_filter_num*4, (2, 2), strides=(2, 2), padding='same')(conv6)
    # up7 = Conv2D(base_filter_num*4, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(conv6))
    merge7 = concatenate([conv3,up7], axis = -1)
    conv7 = Conv2D(base_filter_num*4, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge7)
    conv7 = Conv2D(base_filter_num*4, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv7)

    up8 = Conv2DTranspose(base_filter_num*2, (2, 2), strides=(2, 2), padding='same')(conv7)
    # up8 = Conv2D(base_filter_num*2, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(conv7))
    merge8 = concatenate([conv2,up8], axis = -1)
    conv8 = Conv2D(base_filter_num*2, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge8)
    conv8 = Conv2D(base_filter_num*2, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv8)

    up9 = Conv2DTranspose(base_filter_num, (2, 2), strides=(2, 2), padding='same')(conv8)
    # up9 = Conv2D(base_filter_num, 2, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(UpSampling2D(size = (2,2))(conv8))
    merge9 = concatenate([conv1,up9], axis = -1)
    conv9 = Conv2D(base_filter_num, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge9)
    conv9 = Conv2D(base_filter_num, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv9)
    # 二分类任务
    conv10 = Conv2D(1, 1, activation = 'sigmoid')(conv9)
    
    model = Model(input = inputs, output = conv10)
	model.compile(optimizer=Adam(lr = 1e-4), loss='binary_crossentropy', metrics=['acc'])
	model.summary()
    return model

Unet++

  • 网络结构
    研习U-Net
    是Unet++的作者周纵苇亲自阐述是如何创造Unet++,包括起因经过结果,不可错过!
    我这里简单复述一遍核心~
    首先,作者对Unet下采用4层深度提出了疑问,为什么一定要4层呢?其他深度不行吗?于是对不同深度的Unet做实验,结果如下图(来自研习U-Net):
    在这里插入图片描述
    先不看白色的Unet++,前4个橙色对应不同深度Unet,从两个数据集的结果可看出并不是越深效果就越好,也可以推出不同层次特征的重要性对于不同的数据集是不一样的,浅有浅的侧重,深有深的优势,因此如何将不同深度的Unet融合在一起就成了重点。
    在这里插入图片描述
    于是,思绪迸发而出,直观的将不同深度的子Unet拼成下图不就连接在一起了:
    在这里插入图片描述
    但上面的结构是存在一些问题的,中间的子模型并没有与输出连接,这样中间部分是无法通过反向传播跟新权重的,在UC Berkeley团队发表在CVPR上的论文"Deep Layer Aggregation",出现了一个结构(与Unet++几乎同时发表,英雄所见略同):
    在这里插入图片描述
    Unet++与上面结构不同之处就在于,上图去掉了Unet的长连接全部使用短连接,而UNet++的作者认为长连接是有必要的,它联系了输入图像的很多信息,有助于还原降采样所带来的信息损失,在一定程度上和残差的操作非常类似,因此最终结构诞生:
    在这里插入图片描述
  • keras实现Unet++
def unet_plus_plus(input_size = (512,512,3),base_filter_num=64):
    inputs = Input(input_size)
    conv0_0 = Conv2D(base_filter_num, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(inputs)
    conv0_0 = Conv2D(base_filter_num, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv0_0)
    pool1 = MaxPooling2D(pool_size=(2, 2))(conv0_0)

    conv1_0 = Conv2D(base_filter_num*2, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool1)
    conv1_0 = Conv2D(base_filter_num*2, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv1_0)
    pool2 = MaxPooling2D(pool_size=(2, 2))(conv1_0)

    up1_0 = Conv2DTranspose(base_filter_num, (2, 2), strides=(2, 2), padding='same')(conv1_0)
    merge00_10 = concatenate([conv0_0,up1_0], axis=-1)
    conv0_1 = Conv2D(base_filter_num, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge00_10)
    conv0_1 = Conv2D(base_filter_num, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv0_1)

    conv2_0 = Conv2D(base_filter_num*4, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool2)
    conv2_0 = Conv2D(base_filter_num*4, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv2_0)
    pool3 = MaxPooling2D(pool_size=(2, 2))(conv2_0)

    up2_0 = Conv2DTranspose(base_filter_num*2, (2, 2), strides=(2, 2), padding='same')(conv2_0)
    merge10_20 = concatenate([conv1_0,up2_0], axis=-1)
    conv1_1 = Conv2D(base_filter_num*2, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge10_20)
    conv1_1 = Conv2D(base_filter_num*2, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv1_1)

    up1_1 = Conv2DTranspose(base_filter_num, (2, 2), strides=(2, 2), padding='same')(conv1_1)
    merge01_11 = concatenate([conv0_0,conv0_1,up1_1], axis=-1)
    conv0_2 = Conv2D(base_filter_num, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge01_11)
    conv0_2 = Conv2D(base_filter_num, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv0_2)

    conv3_0 = Conv2D(base_filter_num*8, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool3)
    conv3_0 = Conv2D(base_filter_num*8, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv3_0)
    pool4 = MaxPooling2D(pool_size=(2, 2))(conv3_0)

    up3_0 = Conv2DTranspose(base_filter_num*4, (2, 2), strides=(2, 2), padding='same')(conv3_0)
    merge20_30 = concatenate([conv2_0,up3_0], axis=-1)
    conv2_1 = Conv2D(base_filter_num*4, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge20_30)
    conv2_1 = Conv2D(base_filter_num*4, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv2_1)

    up2_1 = Conv2DTranspose(base_filter_num*2, (2, 2), strides=(2, 2), padding='same')(conv2_1)
    merge11_21 = concatenate([conv1_0,conv1_1,up2_1], axis=-1)
    conv1_2 = Conv2D(base_filter_num*2, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge11_21)
    conv1_2 = Conv2D(base_filter_num*2, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv1_2)

    up1_2 = Conv2DTranspose(base_filter_num, (2, 2), strides=(2, 2), padding='same')(conv1_2)
    merge02_12 = concatenate([conv0_0,conv0_1,conv0_2,up1_2], axis=-1)
    conv0_3 = Conv2D(base_filter_num, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge02_12)
    conv0_3 = Conv2D(base_filter_num, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv0_3)

    conv4_0 = Conv2D(base_filter_num*16, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(pool4)
    conv4_0 = Conv2D(base_filter_num*16, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv4_0)

    up4_0 = Conv2DTranspose(base_filter_num*8, (2, 2), strides=(2, 2), padding='same')(conv4_0)
    merge30_40 = concatenate([conv3_0,up4_0], axis = -1)
    conv3_1 = Conv2D(base_filter_num*8, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge30_40)
    conv3_1 = Conv2D(base_filter_num*8, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv3_1)

    up3_1 = Conv2DTranspose(base_filter_num*4, (2, 2), strides=(2, 2), padding='same')(conv3_1)
    merge21_31 = concatenate([conv2_0,conv2_1,up3_1], axis = -1)
    conv2_2 = Conv2D(base_filter_num*4, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge21_31)
    conv2_2 = Conv2D(base_filter_num*4, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv2_2)

    up2_2 = Conv2DTranspose(base_filter_num*2, (2, 2), strides=(2, 2), padding='same')(conv2_2)
    merge12_22 = concatenate([conv1_0,conv1_1,conv1_2,up2_2], axis = -1)
    conv1_3 = Conv2D(base_filter_num*2, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge12_22)
    conv1_3 = Conv2D(base_filter_num*2, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv1_3)

    up1_3 = Conv2DTranspose(base_filter_num, (2, 2), strides=(2, 2), padding='same')(conv1_3)
    merge03_13 = concatenate([conv0_0,conv0_1,conv0_2,conv0_3,up1_3], axis = -1)
    conv0_4 = Conv2D(base_filter_num, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(merge03_13)
    conv0_4 = Conv2D(base_filter_num, 3, activation = 'relu', padding = 'same', kernel_initializer = 'he_normal')(conv0_4)
    # 二分类任务
    conv0_4 = Conv2D(1, 1, activation = 'sigmoid')(conv0_4)

    model = Model(input = inputs, output = conv0_4)
    model.compile(optimizer=Adam(lr = 1e-4), loss='binary_crossentropy', metrics=['acc'])
	model.summary()
    return model
2019-12-21 17:11:06 weixin_44259900 阅读数 66
  • Erdas遥感影像处理入门实战教程(GIS思维)

    《Erdas遥感影像处理入门实战教程》以Erdas2010版本经典界面进行实战教学,设计12章内容,正式教学内容总共45课时,15个小时时长。从软件界面开始,到后的应用,适合入门级、初级、中级的人员学习、工作、教师教学参考。课程根据作者实际工作经验,以及采访学员需求,开展课程设计,实用加实战,会是你学习路上的好帮手。

    4616 人正在学习 去看看 黄晓军

随着深度学习技术的快速发展,很多需要大量分析图像的领域都引入了相关算法,如医疗影像分类和分割,任务性能也得到了大幅度提升。然而在将深度学习网络应用到某个特殊领域时,常常面临着需要大量标注数据的窘境,这大大增加了研发的时间和经济成本。近期,自监督学习逐渐得到重视,这是一种监督式学习方法,但是训练目标不引入任何标注信息,通过设计精巧的损失函数,刻画目标任务,如图像分割,并融合相应约束条件,实现模型的自监督训练。本文对CVPR2019一篇将自监督学习引入图像分割的开创性工作进行介绍。

1.简介

论文地址:https://arxiv.org/abs/1905.01298

该工作尝试通过自监督学习的方式,解决物体部位分割的任务,比如模型的输入为一张人脸图像,输出为人脸各部位的分割结果,如头发区域,眼睛区域,鼻子区域和嘴巴区域。该方法没有使用任何监督信息,如分割任务常用的mask标注,而是从对模型预测的分割热图进行一系列的约束,最终实现模型的自监督学习。

图1.论文提出的模型示意图。其中分割网络使用的是经典的Deeplab框架。

如图1所示,SCOPS的分割模型使用的是经典的Deeplab架构,针对部位分割任务,文章设计了三种自监督约束条件:

  1. 几何一致性 (Geometric conentration loss)目的是将属于同一个局部区域的像素在几何空间上集中在一起,也就是在图像中属于同一部位的像素尽量坐标尽量靠近,公式表示为:
    Lcon=ku,vu,vcuk,vvk2R(k,u,v)/zk\mathcal{L}_{con}=\sum_k\sum_{u,v}||\langle u,v \rangle-\langle c_u^k,v_v^k \rangle||^2 \cdot R(k,u,v)/z_k
    R(k,u,v)R(k,u,v)是分割热图中第kk个部位(即第kk个通道)在(u,v)(u,v)处的值,zk=u,vR(k,u,v)z_k=\sum_{u,v}R(k,u,v)为归一化项,cuk=u,vuR(k,u,v)/zkc_u^k=\sum_{u,v}u\cdot R(k,u,v)/z_k表示第kk个部位在uu方向的中心点,可以想象,在分割热图中部位kk响应强烈的区域对这个部位在图像中对中心点位置(坐标)的确定贡献更多。如果一个区域离这个中心区域较远,即使响应比较强烈,在该约束下也以很大的概率被分割为非第kk个部位。

  2. 同变性约束(Equivariance loss)旨在增加模型对表观和姿态变化的鲁棒性,如图像颜色空间的变化、头部旋转等因素。

图2.同变性约束示意图。

如图2所示,在训练过程中,使用随机的旋转Ts()T_s(\cdot)和随机颜色干扰Ta()T_a(\cdot)对每个训练图像进行变换I=Ts(Ta(I))I'=T_s(T_a(I)),相应的分割热图是RRRR',并使用1中的方法得到部位中心坐标cuk,vvk\langle c_u^k,v_v^k \ranglecuk,vvk\langle c_u^{k'},v_v^{k'} \rangle,损失函数设计为:
Leqv=λeqvsDKL(RTs(R))+λeqvckcuk,vvkTs(cuk,vvk)2\mathcal{L}_eqv=\lambda_{eqv}^sD_{KL}(R'||T_s(R))+\lambda_{eqv}^c\sum_k||\langle c_u^{k'},v_v^{k'} \rangle-T_s(\langle c_u^k,v_v^k \rangle)||……2
上述公式显式的加强已知变换的图像与原图间分割结果的一致性,即中心位置的一致性(通过二范数来实现)和表观一致性(通过KL散度来衡量)。

3.语义一致性(Semantic consistency loss)借鉴了迁移学习的思想,强调分为同一个部位的区域在某一个特征空间应该距离非常接近,本文使用一个预训练的VGG-19来提取特征,表示为VRC×H×W\mathbf{V} \in \mathcal{R}^{C\times H \times W}。考虑到分割部位的数量KK远小于特征空间的维度CC,本文设计了KK个可学习的特征中心{wk},k=[1,2,...,K]\{\mathbf{w}_k\},k=[1,2,...,K],最后损失函数设计为:
Lsc=u,vV(u,v)kR(k,u,v)wk2\mathcal{L}_{sc}=\sum_{u,v}||\mathbf{V}(u,v)-\sum_kR(k,u,v)\mathbf{w}_k||^2
该公式旨在使不同部位在隐特征空间(语义空间)更加接近。另外,为了解决KK过大时,学习到的分割热图可能聚于同一个特征中心,因此设计了正交约束和显著性约束,具体信息可以参考原文。

2.实验结果

图3.在CelebA上进行的对比实验和Ablation Study。

1. 图3第2-第4行显示了SCOPS与其他方法的对比结果,第5-第9行显示了不同约束条件对分割结果的影响。需要注意的是,其中有一项显著性约束(saliency constraint)使用显著性标注使模型只关注感兴趣目标区域,因此这一项显著提高了各部分的分割效果。

图4.不同K值,即不同部位数量的设定对分割结果的影响。

  1. 由于SCOPS是一种非监督式学习方式,对于分割部位的数量不敏感,因此可以尝试调整不同的K值,将目标分割为实际应用中需要的分割区域个数。但是如果K值过高,分割结果的一致性可能会有所影响,如K=10时,人脸头发区域与额头区域发生一定程度的混叠;当K=4时,模型成功将人脸区域划分为头发,眼部,鼻子+嘴巴,下颌区域。

  2. 其他量化结果可以查看原文。

3. 小结

本文提出了一种自监督式学习方法,基本的网络架构采用的是经典的分割网络,主要通过设计新的监督信号来驱动网络参数的调整。该方法首次将自监督学习与分割相关任务结合起来,解决标注困难和数量不足的难题,而且对基础网络要求不高,可以替换为任意当前流行的架构,虽然设计的损失函数有待改进,但是作为该领域的初次尝试,相信近期应该会受到大量关注,对不同领域的相关应用产生较大的影响,并衍生更多类似的工作。

没有更多推荐了,返回首页