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

   日期:2020-08-21     浏览:338    评论:0    
核心提示:U-Net编程实战——CT影像的肿瘤分割    本文将介绍如何使用tf2.0的keras框架实现典型的U-net网络,并以CT影像的肿瘤分割为案例进行讲解。本文将详细地介绍UNet肿瘤分割的实现过程,这将有助于读者快速掌握UNet这一网络,并熟悉深度学习的基本使用方法。预备知识点:掌握Python中函数、模块和类的使用方法;掌握基本的图像处理方法(数字图像处理);掌握基本的深度学习知识(深度学习的基本概念);能配置tf2.0的运行环境;数据集和源代码可以从GitHub中下载:或者从百度网

    本文将介绍如何使用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/1bSxMIeepQhyCxnYhdwds7g 提取码:66gq

目录

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

一、问题的描述

    在医学诊断中,通过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 layer
    x = Conv2D(n_filters, kernel_size, padding=padding)(
        input_tensor)
    if batchnorm:
        x = BatchNormalization()(x)
    x = Activation('relu')(x)

    # the second layer
    x = 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-8
    fm = tf.reduce_sum(y_true + tf.cast(tf.greater(y_pred, 0.1), tf.float32)) + 1e-8
    return 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.

 
打赏
 本文转载自:网络 
所有权利归属于原作者,如文章来源标示错误或侵犯了您的权利请联系微信13520258486
更多>最近资讯中心
更多>最新资讯中心
0相关评论

推荐图文
推荐资讯中心
点击排行
最新信息
新手指南
采购商服务
供应商服务
交易安全
关注我们
手机网站:
新浪微博:
微信关注:

13520258486

周一至周五 9:00-18:00
(其他时间联系在线客服)

24小时在线客服