Advertisement

医学图像分割——Unet

阅读量:

这是一个学习记录博~可能有错,欢迎讨论

P.S. 本文所用的unet源码来自Unet源码

目标

实现胃部超声图像的病灶分割

医学数据以及预处理简介

医学影像的数据形式具有多样性。其中包含CT(Computed Tomography)图像、MRI(Magnetic Resonance Imaging)以及超声(Ultrasound)等不同类型的影像数据。在数据格式方面,则主要采用DICOM文件格式(如.dcm或.dma),其对应的Python处理库包括SimpleITK和pydicom;此外还有NIfit文件类型(.nii或.nii.gz),其对应的Python处理库包括SimpleITK和nibabel;还有NRRD文件类型(.nrrd),其对应的Python处理库包括SimpleITK和pynrrd等。

对于CT图像而言,在术语上存在两种数值概念:一是HU值(Hounsfield unit),这是医学领域专用的概念;另一个是图像值(image value),属于计算机科学领域的范畴。HU值反映了物体组织对X射线的能量吸收程度,并通常在-1024到3071之间波动;而图像值则用于表示灰度级范围,在典型的灰度图中一般为0到255。

在网上的地方有看过这样的解释(CT图像之Hu值变换与窗宽窗位调整):

复制代码
    对于nii格式的图片,经过测试,nibabel, simpleitk常用的api接口,都会自动的进行上述转化过程,即取出来的值已经是Hu了。(除非专门用nib.load('xx').dataobj.get_unscaled()或者itk.ReadImage('xx').GetPixel(x,y,z)才能取得原始数据)
    
    对于dcm格式的图片,经过测试,simpleitk, pydicom常用的api接口都不会将原始数据自动转化为Hu!!(itk snap软件读入dcm或nii都不会对数据进行scale操作)

但我认为这个解释不够准确,在使用simpleItk处理dcm数据时,计算出的结果直接就是Hu值(我不太清楚其中原因),因此我觉得大家最好先将数据输出并查看数值分布情况来辅助判断是否需要进行转换。

对CT图像,在Hu值问题得到良好解决后,则需对window width和window center进行预处理(window width及window center的具体定义参考自CT图像之Hu值变换与window width及window center调整)。其中window width 和 window center 是选择感兴趣 CT 值范围的关键参数,在不同组织或病变具有不同 CT 值特征的前提下,在追求最佳显示效果时,则需要根据目标组织或病变的特点来设定合适的 window width 和 window center。

举个例子,CT原图可能是这样的:

在这里插入图片描述

而在选取了窗宽窗位并对原图进行了clip之后的图是这样的:

在这里插入图片描述

是不是清晰了很多呢。

本实验数据及预处理

实验数据

在本实验中所使用的数据基于胃部超声图像。通常情况下,这里的超声数据是以.jpg格式导出的(通常会转换为nrrd或dcm以进行病变定位)

数据预处理

增强对比度

超声图像有时亮度较低,在提升对比度方面可以通过以下方法实现:例如采用Python+OpenCV中的直方图均衡化技术(参考博客

数据增强

在数据样本不足的情况下应采取实施数据增强措施以弥补数据短缺的问题。这可以通过调用keras模块中的Data generator类来实现具体操作建议请参考Unet源码详细了解相关技术细节。

提取包含病灶的slices

CT图像通常包含大量层状结构,在实际应用中并非所有切片都会显示病变区域因此,在分析时需要特别关注那些含有病变区域的切片尽管本研究未深入探讨这一问题但为了全面理解整个流程仍需介绍相关细节具体来说,则是通过检查mask数据来确定病变区域的位置如果发现病变则需将其对应的切片予以保留反之则予以剔除

复制代码
    def getRangImageDepth(image):
    """
    args:
    image ndarray of shape (depth, height, weight)
    """
    # 得到轴向上出现过目标(label>=1)的切片
    ### image.shape为(depth,height,width),否则要改axis的值
    z = np.any(image, axis=(1,2)) # z.shape:(depth,)
    ###  np.where(z)[0] 输出满足条件,即true的元素位置,返回为一个列表,用0,-1取出第一个位置和最后一个位置的值
    startposition,endposition = np.where(z)[0][[0,-1]]
    return startposition, endposition

Unet训练中需要注意的几个点

这个Unet源码是用keras写的

图像的位深

若输入的数据图片格式为.jpg类型,则可能会影响模型对图像深度信息的完整感知进而影响模型预测效果

检查数据类型

在该源代码库GitHub上的Unet项目的data.py文件中包含图像预处理操作。例如将其重塑为网络所需维度的四维张量形式。此外还包含将图像像素值除以255以便将其转换为适合网络使用的浮点数值。这些调整通常因数据集的不同而有所变化。因此建议在开始开发前先仔细检查这一部分代码是否正确无误。

输入图像大小的设置

在UNet架构中经历了四次下采样的过程,并在此基础上设计了多个解码器模块来完成特征图的重建工作。值得注意的是,在这一过程中还需要考虑各个解码器模块之间的连接关系以保证网络性能的最大化。具体而言,在每个解码器模块中都需要将该区域的特征图与上一层编码器产生的特征图进行拼接以实现信息的有效传递和融合。这将导致后续解码器模块在处理信息时遇到困难。
举个例子来说,在这种情况下(如原始图像尺寸为37×37),经过一次池化后变为18×18;随后经过多次池化后可能会出现尺寸无法被2整除的情况(如变为19×19),进而导致解码器模块无法正确对齐编码层的信息。

为了解决这一问题, 一个较为直接的方法是将图片配置为每次下采样后都能满足偶数尺寸的要求. 例如256x256这样的尺寸; 如果不想每次都保持这种尺寸的话, 则可以使用tf.pad对图像张量进行填充.

复制代码
    ### 判断concatenate的特征图大小是否一致
    if drop4.get_shape().as_list() != up6.get_shape().as_list():
    # _,height, width, depth = up6.shape
    # 只要你使用Model,就必须保证该函数内全为layer,不能有其他函数,如果有其他函数必须用Lambda封装为layer
    up6_padded = Lambda(lambda x: tf.pad(x, [[0, 0], [1, 0], [0, 0], [0, 0]], 'REFLECT'))(up6)
    merge6 = concatenate([drop4, up6_padded], axis=3)
    else:
    merge6 = concatenate([drop4, up6], axis=3)

这里对tf.pad进行一下简要说明,

复制代码
    tf.pad(
    tensor,
    paddings,
    mode='CONSTANT',
    name=None
    )
  • tensor是一个可填充的张量
  • paddings用于指定在哪个维度上进行填充,并确定采用何种填充方式需要注意的是paddings的秩必须与张量保持一致
  • mode用于指定采用何种填充值类型,默认情况下有三种选择:CONSTANT、REFLECT或SYMMETRIC。其中当mode设为CONSTANT时将使用0进行填充;若选REFLECT则会根据边缘镜像反射而不复制边缘;若选SYMMETRIC则会从对称轴开始复制并扩展边缘内容。
  • name表示该操作节点对应的名称

在本实验研究中,我们使用了一个四维张量[[i,j,k,l]]来进行参数化建模。该张量分别代表了[,height,width,length]三个维度的空间信息以及一个类别标签信息。其中i,j,k,l四个索引的具体含义如下:当i=1时,则表示该位置需要特别关注;当i=0时则表示该位置为背景区域;同理j=1时则表示该位置具有显著特征;j=0时则表示该位置具有平滑过渡特性;k值为1时表示该位置需要对上下边缘进行特殊处理;l值为1时表示该位置需要对左右边缘进行特殊处理;具体来说:当k=1且l=1时,则同时对上下左右四个方向进行特殊处理;当k=1且l=2时,则仅对上下边缘进行特殊处理;当k=2且l=3时,则仅对左右边缘进行特殊处理;同样的情况也可以通过不同的组合来实现多方向的边缘处理效果。

更换loss函数

Unet源码框架中采用了交叉熵损失函数来进行训练操作。在医学图像处理场景下存在一些区域具有极小病灶面积的情况时有发生,在这种情况下使用交叉熵损失可能会导致模型收敛困难。具体解决方案可参考以下详细讲解文章:从loss处理图像分割中类别极度不均衡的状况—keras

例如将交叉熵损失换成Dice loss,具体做法为:

复制代码
    def dice_coef(y_true, y_pred, smooth=1):
    intersection = K.sum(y_true * y_pred, axis=[1,2,3])
    union = K.sum(y_true, axis=[1,2,3]) + K.sum(y_pred, axis=[1,2,3])
    return K.mean( (2. * intersection + smooth) / (union + smooth), axis=0)
    
    
    def dice_coef_loss(y_true, y_pred):
    
    return 1 - dice_coef(y_true, y_pred, smooth=1)

然后将modle.compile改一下:

复制代码
    model.compile(optimizer = Adam(lr = 1e-5), loss = dice_coef_loss, metrics = [dice_coef])

更改学习率、batchsize和epoch

如果学习率设置不当,则可能导致模型无法有效收敛;因此建议根据具体情况自行调节参数。此外,在调整超参数时还需注意Batch大小和训练轮次这两个因素同样需要根据具体的数据集来确定。

emmm…暂时就这么多啦~

全部评论 (0)

还没有任何评论哟~