本文将介绍如何使用tf2.0的keras框架实现典型的U-net网络,并以CT影像的肿瘤分割为案例进行讲解。本文将详细地介绍UNet肿瘤分割的实现过程,这将有助于读者快速掌握UNet这一网络,并熟悉深度学习的基本使用方法。

预备知识点:

  • 掌握Python中函数、模块和类的使用方法;
  • 掌握基本的图像处理方法(数字图像处理);
  • 掌握基本的深度学习知识(深度学习的基本概念);
  • 能配置tf2.0的运行环境;

数据集和源代码可以从GitHub中下载:https://github.com/LittleMount/UNet-for-tumor-segmentation/tree/master
或者从百度网盘分享链接中下载:https://pan.baidu.com/s/1oqOAJfQZKeprUC0pkgM0Vg 提取码:0pi9

目录

  • 一、问题的描述
  • 二、基本流程
  • 三、数据预处理
  • 四、构建网络
  • 五、编译网络
  • 六、训练网络
  • 七、测试评价

一、问题的描述

    在医学诊断中,通过CT影像来确定肿瘤的位置、大小和判断是否发生转移具有重要的意义。本案例的目标就是从已有的CT影像中分割出肿瘤的区域,肿瘤的区域是由专业医生描绘出来的,可以当做label(PS: 数据集来源于第七届“泰迪杯”数据挖掘挑战赛[1])。数据情况如下图所示,

图1 数据示意图

如图,左图是CT图像的原始图像,右图是医生所绘制的肿瘤区域。

分割要求:

  • 尽可能准确地分割出肿瘤区域;
  • 无肿瘤的CT影像,网络分割结果应为全黑;
  • 评价指标为准确率、Dice系数;

源文件及文件夹说明:

train: 放置了108位患者的CT影像文件夹;
label: 放置了与train对应的肿瘤掩膜;
evaluation: 放置了网络测试的结果;
weights: 保存网络权重;
u_net.py: 本文的执行文件;

本文达到的效果:

图2 网络分割效果示意图

二、基本流程

    利用深度学习解决问题具有较为固定的流程,具体可以概括为:
——1.数据预处理 -> 2.构建网络 -> 3.编译网络 -> 4.训练网络 -> 5.测试和评价。

图3 UNet肿瘤分割基本流程图

  • 数据预处理,包括数据集准备、数据归一化、数据裁剪等等操作;
  • 构建网络,根据具体问题的需要构建合适的网络,本文构建的网络是UNet;
  • 编译网络,选择训练网络的优化器、学习率、损失函数和评价函数等等;
  • 训练网络,设置batch_size,epoch,是否乱序训练,验证集分割比例等等;
  • 测试网络,编写测试函数,查看网络分割效果并计算相关评价指标等等。

    下面将具体介绍每一步的实现方法。深度学习是一门工具,只有亲自实践并且多加练习才能熟练掌握它,所以建议读者(入门级)能自己复现出每一行命令。

三、数据预处理

    UNet肿瘤分割问题可以拆解为多个模块,同样地,在每个模块下面也可以再细分具体的工作。数据预处理模块的工作包括:图片读取 -> 图片有效区域裁剪 -> 图像归一化 -> 分离训练集和测试集 -> 保存数据。

    监督学习的深度学习需要大量数据,所以数据预处理的工作非常重要,后期熟练使用深度学习后,数据集的制备也会成为主要工作。

1. 数据集介绍

    train文件夹中包含108位患者的CT影像文件夹,每位患者大概有二三十张CT影像,原本影像是DCM格式的,为了读者操作方便,笔者已经将它读取后保存为".png"格式;label文件夹保存了相对应mask图片。

2. 图片读取

    由于train中每个文件夹的CT影像数量不一样,所以需要使用Python中glob函数自动读取文件夹和文件名;之后再使用Image.open读取图片;

for file in glob('./train/*'):  # 获取文件夹名称for filename in glob(file + '/*'):  # 获取文件夹中的文件img = np.array(Image.open(filename), dtype='float32') / 255

PS:注意glob函数在使用之前要进行导入(可参考程序开头的模块导入),这部分代码以及后面的代码都只是函数中的片段,作解释用,不能直接运行。

3. 图片裁剪

    直肠肿瘤只发生在直肠,所以实际需要处理的区域并不需要整张图片,适当的裁剪可以提高网络训练速度;

图4 图片裁剪示意图

实现方法:

x_train.append(img[256:, 128:384])

    每读取一张图片,就将其扩展到x_train列表之后,当遍历完所有图片,列表x_train就保存了所有图片数据;

4. 分离训练集和测试

    因为肿瘤分布是连续的,所以为了避免局部特征过于集中,这里对数据集进行乱序,之后再按9:1分配训练集和测试集。

np.random.seed(116) # 设置相同的随机种子,确保数据匹配
np.random.shuffle(x_train) # 对第一维度进行乱序
np.random.seed(116)
np.random.shuffle(x_label)
# 图片有三千张左右,按9:1进行分配
return x_train[:2700, :, :], x_label[:2700, :, :], x_train[2700:, :, :], x_label[2700:, :, :]

PS:数据预处理完之后可以专门保存到一个.npy数据中,这样不用每次都执行一遍预处理的操作。

四、构建网络

1. UNet网络介绍

    UNet网络是一种可以实现端到端映射的网络,非常适用于实现图像分割、恢复、增强和超分辨等目标。其具体网络结构如下图所示,

图5 UNet网络结构示意图[2]

UNet网络有几个主要的特点:
① 网络可以从中间分为左右两部分,左边是收缩路径,利用降采样和卷积模块提取不同尺度的特征;右边是扩展路径,利用上采样和卷积模块恢复尺度并融合先前特征,逐渐恢复图像;
② 卷积模块由两层连续的卷积层组成,它可以实现更大尺寸和更高深度的特征提取;降采样实现图像尺度的缩小;上采样(或反卷积层)实现图像尺度的变大;
③ 图中灰色的箭头线表示跳连,作用是在同一尺度下,将收缩路径上的特征合并到扩展路径上的特征,这样可以在扩展路径上提供更好的约束,网络也更容易输出我们期望的结果。

2. tf2.0的keras框架编写UNet网络

    为了创建UNet网络,笔者专门编写了一个函数,下面的代码将按照顺序拆分讲解。根据创建UNet网络的需要,定义函数:

def build_unet(self, n_filters=16, dropout=0, batchnorm=True, padding='same'):

函数的输入中有self,是因为笔者将函数放在了class(类)中,可以实现函数的灵活调用。
n_filters是卷积核数量,dropout是Dropout层的参数,batchnorm控制是否进行标准化,padding控制卷积前后图片尺寸是否变化。这些参数在后面的程序中会使用到。

(1) 首先定义重复使用的卷积模块(函数)

    这个函数放在了build_unet函数内部,是一个需要重复使用的模块(函数)。卷积模块的组成:
——卷积层 -> 标准化层 -> 激活函数 -> 卷积层 -> 标准化层 -> 激活函数 -> 返回结果。

定义的函数输入参数说明:

  • input_tensor: 输入的图片或者上一层输出的结果;
  • n_filters: 设置卷积核的数量;
  • kernel_size:设置卷积核的大小;
  • batchnorm:如果是True,表示使用标准化层(类似高斯标准化,不过有尺度和位移两个可训练变量),如果是False,则不使用标准化;
  • padding:如果是’same’,表示卷积前后图片尺度相同,如果是’valid’,卷积后图片会变小一圈。

具体实现:

# 定义一个多次使用的卷积块
def conv2d_block(input_tensor, n_filters=16, kernel_size=3, batchnorm=True, padding='same'):# the first layerx = Conv2D(n_filters, kernel_size, padding=padding)(input_tensor)if batchnorm:x = BatchNormalization()(x)x = Activation('relu')(x)# the second layerx = Conv2D(n_filters, kernel_size, padding=padding)(x)if batchnorm:x = BatchNormalization()(x)X = Activation('relu')(x)return X

    其中Conv2D是TensorFlow的keras.layers中的函数,程序开头已经导入,所以这里不需要前缀即可调用。这个函数就构建了一个卷积层,input_tensor是输入,x是卷积层的输出。
    同样地,BatchNormalization可以对输入进行标准化;Activation(‘relu’)(x),表示对输入执行relu激活函数。
    重复两次“卷积层 -> 标准化层 -> 激活函数”,即创建了两个串联的卷积层,这是一种常用的特征提取器,最后输出X。

(2) 收敛路径

从UNet结构图可以知道,收敛路径主要的过程为

输入图片 -> ①(卷积模块 -> 下采样 -> dropout层) -> ②(卷积模块 ->下采样 -> dropout层) -> ③(卷积模块 ->下采样 -> dropout层) -> ④(卷积模块 ->下采样 -> dropout层)

所以根据逻辑顺序可编写程序:

# 构建一个输入
img = Input(shape=self.shape) # self.shape是图片维度大小# contracting path
c1 = conv2d_block(img, n_filters=n_filters * 1, kernel_size=3, batchnorm=batchnorm, padding=padding)
p1 = MaxPooling2D((2, 2))(c1)
p1 = Dropout(dropout * 0.5)(p1)c2 = conv2d_block(p1, n_filters=n_filters * 2, kernel_size=3, batchnorm=batchnorm, padding=padding)
p2 = MaxPooling2D((2, 2))(c2)
p2 = Dropout(dropout)(p2)c3 = conv2d_block(p2, n_filters=n_filters * 4, kernel_size=3, batchnorm=batchnorm, padding=padding)
p3 = MaxPooling2D((2, 2))(c3)
p3 = Dropout(dropout)(p3)c4 = conv2d_block(p3, n_filters=n_filters * 8, kernel_size=3, batchnorm=batchnorm, padding=padding)
p4 = MaxPooling2D((2, 2))(c4)
p4 = Dropout(dropout)(p4)

这样就完成了收缩路径程序的编写。继续往下,到了中间一个特征层:

c5 = conv2d_block(p4, n_filters=n_filters * 16, kernel_size=3, batchnorm=batchnorm, padding=padding)

(3) 扩展路径
从UNet结构图可以知道,扩展路径主要的过程为
输入上一层的输出 -> ⑥(上采样 -> 特征合并 -> dropout层 -> 卷积模块) -> ⑦(上采样 -> 特征合并 -> dropout层 -> 卷积模块) -> ⑧(上采样 -> 特征合并 -> dropout层 -> 卷积模块) -> ⑨(上采样 -> 特征合并 -> dropout层 -> 卷积模块)。

具体实现:

# extending path
u6 = Conv2DTranspose(n_filters * 8, (3, 3), strides=(2, 2), padding='same')(c5)
u6 = concatenate([u6, c4])
u6 = Dropout(dropout)(u6)
c6 = conv2d_block(u6, n_filters=n_filters * 8, kernel_size=3, batchnorm=batchnorm, padding=padding)u7 = Conv2DTranspose(n_filters * 4, (3, 3), strides=(2, 2), padding='same')(c6)
u7 = concatenate([u7, c3])
u7 = Dropout(dropout)(u7)
c7 = conv2d_block(u7, n_filters=n_filters * 4, kernel_size=3, batchnorm=batchnorm, padding=padding)u8 = Conv2DTranspose(n_filters * 2, (3, 3), strides=(2, 2), padding='same')(c7)
u8 = concatenate([u8, c2])
u8 = Dropout(dropout)(u8)
c8 = conv2d_block(u8, n_filters=n_filters * 2, kernel_size=3, batchnorm=batchnorm, padding=padding)u9 = Conv2DTranspose(n_filters * 1, (3, 3), strides=(2, 2), padding='same')(c8)
u9 = concatenate([u9, c1])
u9 = Dropout(dropout)(u9)
c9 = conv2d_block(u9, n_filters=n_filters * 1, kernel_size=3, batchnorm=batchnorm, padding=padding)

(4) 输出层

根据输出图片的数据格式,可以定义我们的输出层:

output = Conv2D(1, (1, 1), activation='sigmoid')(c9)return Model(img, output)

其中Conv2D中的第一个1表示卷积核数量,即输出图像的通道数,如果是彩色图像,把数据更改为3即可;(1,1)表示卷积核的大小,只有一个像素大小,表示对图像的每个像素进行判别;
激活函数是’sigmoid’,输出区间为[0,1]。最后返回创建的UNet网络:Model(img, output)。PS:以上程序串联起来即可得到完整的程序。

至此,我们就定义了创建UNet网络的函数,接下来只要调用函数即可创建网络。

self.unet = self.build_unet() # 创建网络变量
self.unet.summary()

执行self.unet.summary()后若打印出网络结构,则说明网络创建成功:

五、编译网络

编译网络主要设置网络基本配置,比如优化器、学习率、损失函数、评价函数等等;

1. 经常使用的优化器是Adam优化器:

# 优化器
optimizer = Adam(0.0002, 0.5)

2. 损失函数常使用’mse’,评价函数使用’accuracy’:

self.unet.compile(loss='mse',optimizer=optimizer,metrics=['accuracy'])

3. 本文使用自定义的Dice函数作为评价函数


根据Dice计算公式,定义如下评价函数,该评价函数将在网络训练过程中使用,所以需要使用tf的相关函数进行定义。

def metric_fun(self, y_true, y_pred):fz = tf.reduce_sum(2 * y_true * tf.cast(tf.greater(y_pred, 0.1), tf.float32)) + 1e-8fm = tf.reduce_sum(y_true + tf.cast(tf.greater(y_pred, 0.1), tf.float32)) + 1e-8return fz / fm

如此一来就可以对网络进行如下编译为:

self.unet.compile(loss='mse',optimizer=optimizer,metrics=[self.metric_fun])

六、训练网络

以上工作准备完成之后就到了训练网络的环节,训练网络有多种实现方法——fit方法、train_on_batch方法以及自定义训练网络的方法等,本文将介绍最简单的fit方法。

1. 导入数据集

这个函数就第三节介绍的数据预处理函数,

# 获得数据
x_train, x_label, y_train, y_label = self.load_data()

x_train, x_label是训练集;y_train, y_label是测试集;

2. 设置训练模式

# 设置训练的checkpoint
callbacks = [EarlyStopping(patience=100, verbose=2),ReduceLROnPlateau(factor=0.1, patience=20, min_lr=0.0001, verbose=2),ModelCheckpoint('./weights/best_model.h5', verbose=2, save_best_only=True)

其中EarlyStopping表示提前结束,如果验证集loss连续100次未下降,训练终止;verbose表示显示方法;
ReduceLROnPlateau表示降低学习率,如果验证集loss连续20次未下降,学习率变为原来的1/10;
ModelCheckpoint表示检查点(可实现断点续训),只保存最优的模型。

3. 开始训练

# 进行训练
results = self.unet.fit(x_train, x_label, batch_size=32, epochs=200, verbose=2,callbacks=callbacks, validation_split=0.1, shuffle=True)
  • validation_split=0.1,表示将训练集的数据分割0.1出来作为验证集(注意这不是测试集);
  • batch_size=32,表示每一批训练的数据大小为32张图片;
  • epochs=200,表示总共训练200次;
  • verbose=2,表示只在每个epoch训练完之后打印训练信息,也可以取0和1,表示不同的信息打印方式;
  • shuffle=True,表示每次训练将训练集的训练部分顺序打乱,验证集的不受影响;
  • callbacks=callbacks,表示按照先前定义的模型进行训练;

训练过程:

图6 训练过程

    笔者是在RTX2060S显卡上训练网络的,训练一个epoch大概耗时18s,200个epoch大概耗时一个小时多一点,不同显卡运算速度有所区别,读者注意一下即可。

4. 训练结果

    第三步的results保存了每个epoch的训练集loss、metric和验证集val_loss、val_metric,将它们绘制出来得到以下曲线:

图7 训练过程的loss曲线和metric曲线

    从图中结果可以看出,验证集(不参与训练)的loss总是比训练集的要大,即肿瘤分割效果总比训练集的差,这说明了我们评价网络要用未训练的数据进行测试,网络本身是通过不断地拟合训练集来达到训练效果的,所以用训练集的数据测试意义不大。验证集的Dice系数达到了0.8,这是一个比较高的值,说明网络训练效果不错。
    虽然我们设置的训练步数是200步,但中间val_loss连续100次未下降,所以提前终止训练。

七、测试评价

    对于不同的问题,使用的评价指标会不一样,读者可以仿照本文自定义评价指标的方法构建自己的评价函数。针对本文的肿瘤分割问题,评价指标主要有两个,一是分割区域精确度,也就是Dice系数;二是肿瘤分割的准确率,即accuracy。

1. Dice系数

Dice系数的计算已经在前面自定义评价函数时介绍过,所以这里直接给出测试集的Dice系数。

在300多张测试集中,分割的肿瘤平均Dice系数达到了0.832;这个结果表示网络分割的肿瘤区域与医生的掩膜极为相似。

2. 准确率

如果网络分割结果存在等于1的像素,则认为网络输出了一张有肿瘤的图片,如果网络分割结果全为0,则认为网络输出了一张没有肿瘤的图片。统计网络判断正确的图片个数,再除以总的图片数得到准确率。

实现方法:

mask = self.unet.predict(x_train[index:index + batch_size]) > 0.1
mask_true = x_label[index, :, :, 0]
if (np.sum(mask) > 0) == (np.sum(mask_true) > 0):n += 1

如果网络输出结果mask和真实值mask_true情况相同,即都有肿瘤或都无肿瘤,则计数+1;最终可求得测试集准确率为:

计算得测试集的准确率到达了99.72%,说明网络非常准确地区分出了有肿瘤图片和无肿瘤图片。

3. 网络分割效果展示

为了查看肿瘤的分割效果,笔者编写了test1函数用于随机显示肿瘤分割图片,具体效果如下:

图8 网络分割肿瘤的效果

第一列是网络的输入,第二列是网络的输出,第三列是医生提供的掩膜。这一节的测试评价都是对为训练的测试集进行的,也再次说明了网络训练得非常成功。

完整的UNet肿瘤分割讲解到这里就结束了,感谢读者们能够耐心的看到此处,也祝愿读者们能有所收获,祝好!

参考资料:
[1] http://www.tipdm.org/bdrace/tzjingsai/20181226/1544.html
[2] O. Ronneberger, P. Fischer and T. Brox, “U-net: Convolutional networks for biomedical image segmentation”, Proc. Med. Image Comput. Comput.-Assisted Intervention, pp. 234-241, 2015.

U-Net编程实战——CT影像的肿瘤分割相关推荐

  1. 【深度学习】腹部CT的肝脏肿瘤分割的探索

    [深度学习]腹部CT的肝脏肿瘤分割的探索 文章目录 1 颜色翻转 2 背景 3 U-Net网络 4 Modified U-Net (mU-Net) 5 数据集 6 数据处理及增强 7 结果 8 基于深 ...

  2. 基于深度学习的腹部CT的肝脏肿瘤分割的项目笔记

    一.前言: 1 这个肝脏肿瘤分割项目是我们大二下学期(2020年1月开始)申请的一个大创项目. 此前我有参与过一个关于深度学习的项目(基于深度学习的视频识别),但在这个项目中我算是摸摸鱼的角色,but ...

  3. 论文笔记 | CT影像结节分割研究进展

    Dong T, Wei L and Nie S D. 2021. Research progress of lung nodule segmentation based on CT images. J ...

  4. 阿里达摩院再造AI抗疫技术:20秒判读CT影像,识别准确率达96%,河南率先启用...

    允中 发自 凹非寺  量子位 报道 | 公众号 QbitAI 又是达摩院. 阿里方面最新消息,达摩院联合阿里云针对新冠肺炎临床诊断研发了一套全新AI诊断技术: AI可以在20秒内准确地对新冠疑似案例C ...

  5. 基于U-net的肝脏肿瘤分割实战(Pytorch实现)

    这是我去年本科毕业设计的一部分,去年使用tensorflow去实现的,这几天就随手用Pytorch做了一下实现.肝脏肿瘤的分割是医学影像分割里面比较重要的一部分,实现从人的CT或MRI影像中将肝脏区域 ...

  6. 医学图像分割 基于深度学习的肝脏肿瘤分割 实战(一)

    在之前的一篇博客里:医学图像分割 unet实现(一),是学习并复现别人的实验.这篇将记录下自己毕设第一阶段的实验.毕设题目为:基于深度学习的肝脏肿瘤分割. 经过几番调整,最终确定:第一阶段分割出腹部图 ...

  7. 医学图像分割 基于深度学习的肝脏肿瘤分割 实战(二)

    在医学图像分割 基于深度学习的肝脏肿瘤分割 实战(一)中,实现了对肝脏的分割,但是后续在使用相同的处理方法与模型进行肿瘤分割的时候,遇到了两次问题. 第一次,网络的dice系数,训练集上一直只能达到4 ...

  8. nnUnet肾脏肿瘤分割实战(KiTS19)

    nnUnet肾脏肿瘤分割实战 KiTS19 Challenge Homepage nnunet项目官方地址 MIC-DKFZ/nnUNet 使用nnunet之前,建议先阅读两篇论文 nnU-Net: ...

  9. 《JavaScript编程实战》

    <JavaScript编程实战> 基本信息 原书名:JavaScript programming: pushing the limits 作者: (美)Jon Raasch 译者: 吴海星 ...

最新文章

  1. ​《2021联邦学习全球研究与应用趋势报告》发布,中美为最大领跑者 | 附下载链接...
  2. [转]那些年我们一起清除过的浮动
  3. java 反射field_java – 使用反射获取Field的泛型类型
  4. 搜狐视频Redis私有云cachecloud开源了
  5. (转)Spring Boot 2 (七):Spring Boot 如何解决项目启动时初始化资源
  6. 【kubernetes】http proxy error stream error stream ID 3 INTERNAL_ERROR
  7. 鼠标移到元素上 使hover事件不生效
  8. WS2811彩带驱动库函数
  9. 《App后台开发运维和架构实践》推荐序
  10. Android 运行程序报错:Unable to execute dex: Multiple dex files define Lcom/baidu/android/pushservice/Push
  11. 战地5未能达到服务器带宽,《战地5》游戏bug汇总以及解决方案介绍
  12. 教你如何焊接BGA芯片技巧
  13. 组织机构树型数据库结构
  14. Go+ 发布 weekly release: v0.7.3
  15. Win控制台下的贪吃蛇(C++)
  16. Python:data:image/png;base64图片编码解码
  17. python中的scipy基础知识_python中SciPy是什么?
  18. 使用UltraEdit25.20.0.88进行Verilog语言编辑配置方式(详细)
  19. 百度信息流流量优化白皮书
  20. js 获取当前时间戳

热门文章

  1. springboot+uniapp(vue) 微信公众号h5开发
  2. hash表的实现和hash桶的示例(c实现)
  3. Conmi的正确答案——Centos安装ufw
  4. 【Verilog】二、Verilog基础语法
  5. AutoCAD-定义图层
  6. 传奇架设-设置GM以及刷装备教程
  7. ESXi5.5主机如何重启NTP服务
  8. c语言迷宫游戏的选题背影,【参赛作品】迷宫游戏(6918,完结)
  9. 魔兽游戏高防服务器和普通服务器的区别
  10. apache的AllowOverride以及Options使用详解