第一课第三周大作业-- MRI 脑肿瘤自动分割教程

Posted Tina姐

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第一课第三周大作业-- MRI 脑肿瘤自动分割教程相关的知识,希望对你有一定的参考价值。


作业文件

吴恩达-医学图像AI 专项课程-作业/第一课/第一课大作业/week3seg/C1M3_Assignmentlast.ipynb

欢迎来到“医学人工智能”课程 1 的最后一部分!

您将学习如何使用 MRI数据构建神经网络以自动分割大脑中的肿瘤区域。

你将学习:

  • MR 图像中有什么
  • MRI 数据集的标准数据准备技术
  • 分割的度量和损失函数
  • 可视化和评估分割模型

作业目录

2.1 MRI data

在这个作业中,我们将构建一个多类分割模型。我们将在每个图像中识别 3 种不同的异常:水肿、非增强肿瘤和增强肿瘤。

可以使用ITK-SNAP打开来看一下图像和标签

使用的MRI数据来自

Medical Segmentation Decathlon

这是一个极其丰富的数据集,提供 3D 表示中每个点(体素)相关联的标签。

2.2 探索数据

我们的数据集以NifTI-1 格式存储,我们将使用 NiBabel库与文件进行交互。每个训练样本由两个单独的文件组成:

第一个文件是一个图像文件,包含形状为 (240, 240, 155, 4) 的 MR 图像的 4D 阵列。

  • 前 3 个维度是 3D 体积中每个点的 X、Y 和 Z 值,通常称为体素。
  • 第 4 维是 4 个不同序列的值
    • 0:FLAIR序列:“流体衰减反转恢复”(FLAIR)
    • 1:T1w:“T1 加权”
    • 2:t1gd:“T1 加权钆对比度增强”(T1-Gd)
    • 3:T2w:“T2 加权”

每个训练示例中的第二个文件是一个标签文件,其中包含一个形状为 (240, 240, 155) 的 3D 数组。

  • 此数组中的整数值表示相应图像文件中每个体素的“标签”:
    • 0:背景
    • 1:水肿
    • 2:非强化肿瘤
    • 3:增强肿瘤

我们可以访问总共 484 张训练图像,我们将这些图像分成训练 (80%) 和验证 (20%) 数据集。

让我们从查看一个案例并可视化数据开始!您可以通过此笔记本访问 10 个不同的案例,我们强烈建议您自行进一步探索数据。

分别按横断位、冠状位、矢状位显示图像,并且把标签显示在图像上

image, label = load_case(DATA_DIR + "imagesTr/BRATS_003.nii.gz", DATA_DIR + "labelsTr/BRATS_003.nii.gz")
image = util.get_labeled_image(image, label)

util.plot_image_grid(image)

我们还编写了一个实用函数,该函数生成一个GIF,显示在每个轴上迭代的效果

image, label = load_case(DATA_DIR + "imagesTr/BRATS_003.nii.gz", DATA_DIR + "labelsTr/BRATS_003.nii.gz")
util.visualize_data_gif(util.get_labeled_image(image, label))

2.4 使用 patch 进行数据预处理

虽然我们的数据集是在配准后以 NIfTI 格式提供给我们的,但在将数据提供给我们的模型之前,我们仍然需要做一些小的预处理。

生成子体积(sub-volumes)

我们将首先生成数据的“patch 补丁”,您可以将其视为整个 MR 图像的子体积。

我们生成补丁的原因是因为原始图像太大,容易超出内存。

因此,我们将使用这种通用技术来生成空间一致的数据子体积,这些子体积可以输入到我们的网络中。

具体来说,我们将从我们的图像中生成形状为 [160, 160, 16] 的随机采样子体积。

此外,由于随机采样子体积,可能会导致大部分子体积只是脑组织或黑色背景,没有任何肿瘤,我们希望生成的子体积中至少包含一定数量肿瘤体素。

因此,我们将只选择具有最多 95% 非肿瘤区域(或至少 5% 肿瘤)的补丁(这是生成的子体积中,肿瘤和非肿瘤体素的占比

我们通过根据背景标签中存在的值过滤来做到这一点。

def get_sub_volume(image, label, 
                   orig_x = 240, orig_y = 240, orig_z = 155, 
                   output_x = 160, output_y = 160, output_z = 16,
                   num_classes = 4, max_tries = 1000, 
                   background_threshold=0.95):
    """
    Extract random sub-volume from original images.

    Args:
        image (np.array): original image, 
            of shape (orig_x, orig_y, orig_z, num_channels)
        label (np.array): original label. 
            labels coded using discrete values rather than
            a separate dimension, 
            so this is of shape (orig_x, orig_y, orig_z)
        orig_x (int): x_dim of input image
        orig_y (int): y_dim of input image
        orig_z (int): z_dim of input image
        output_x (int): desired x_dim of output
        output_y (int): desired y_dim of output
        output_z (int): desired z_dim of output
        num_classes (int): number of class labels
        max_tries (int): maximum trials to do when sampling
        background_threshold (float): limit on the fraction 
            of the sample which can be the background

    returns:
        X (np.array): sample of original image of dimension 
            (num_channels, output_x, output_y, output_z)
        y (np.array): labels which correspond to X, of dimension 
            (num_classes, output_x, output_y, output_z)
    """
    # Initialize features and labels with `None`
    X = None
    y = None

    ### START CODE HERE (REPLACE INSTANCES OF 'None' with your code) ###
    
    tries = 0
    
    while tries < max_tries:
        # randomly sample sub-volume by sampling the corner voxel
        # hint: make sure to leave enough room for the output dimensions!
        start_x = np.random.randint(0,orig_x - output_x + 1)
        start_y = np.random.randint(0,orig_y - output_y + 1)
        start_z = np.random.randint(0,orig_z - output_z + 1)

        # extract relevant area of label
        y = label[start_x: start_x + output_x,
                  start_y: start_y + output_y,
                  start_z: start_z + output_z]
        
        # One-hot encode the categories.
        # This adds a 4th dimension, 'num_classes'
        # (output_x, output_y, output_z, num_classes)
        y = keras.utils.to_categorical(y,num_classes= num_classes)


        # compute the background ratio
        bgrd_ratio = np.sum(y[:, :, :, 0])/(output_x * output_y * output_z)

        # increment tries counter
        tries += 1

        # if background ratio is below the desired threshold,
        # use that sub-volume.
        # otherwise continue the loop and try another random sub-volume
        if bgrd_ratio < background_threshold:

            # make copy of the sub-volume
            X = np.copy(image[start_x: start_x + output_x,
                              start_y: start_y + output_y,
                              start_z: start_z + output_z, :])
            
            # change dimension of X
            # from (x_dim, y_dim, z_dim, num_channels)
            # to (num_channels, x_dim, y_dim, z_dim)
            X = np.moveaxis(X,3,0)

            # change dimension of y
            # from (x_dim, y_dim, z_dim, num_classes)
            # to (num_classes, x_dim, y_dim, z_dim)
            y = np.moveaxis(y,3,0)


            ### END CODE HERE ###
            
            # take a subset of y that excludes the background class
            # in the 'num_classes' dimension
            y = y[1:, :, :, :]
    
            return X, y

    # if we've tried max_tries number of samples
    # Give up in order to avoid looping forever.
    print(f"Tried {tries} times to find a sub-volume. Giving up...")

标准化

最后,鉴于 MR 图像中的值涵盖的范围非常广泛,我们将这些值标准化为均值为 0,标准差为 1。

这是深度图像处理中的常用技术,因为标准化使网络更容易学习。

# UNQ_C2 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
def standardize(image):
    """
    Standardize mean and standard deviation 
        of each channel and z_dimension.

    Args:
        image (np.array): input image, 
            shape (num_channels, dim_x, dim_y, dim_z)

    Returns:
        standardized_image (np.array): standardized version of input image
    """
    
    ### START CODE HERE (REPLACE INSTANCES OF 'None' with your code) ###
    
    # initialize to array of zeros, with same shape as the image
    standardized_image = np.zeros(image.shape)

    # iterate over channels
    for c in range(image.shape[0]):
        # iterate over the `z` dimension
        for z in range(image.shape[3]):
            # get a slice of the image 
            # at channel c and z-th dimension `z`
            image_slice = image[c,:,:,z]

            # subtract the mean from image_slice
            mean=np.mean(image_slice)
            centered = image_slice-mean
            
            # divide by the standard deviation (only if it is different from zero)
            std=np.std(centered)
            if std != 0:
                centered_scaled = centered/std

            # update  the slice of standardized image
            # with the scaled centered and scaled image
            standardized_image[c, :, :, z] = centered_scaled

    ### END CODE HERE ###

    return standardized_image

3D U-Net

模型使用的是3D unet

4.1 骰子相似系数

除了架构之外,任何深度学习方法中最重要的元素之一就是损失函数的选择。

您可能熟悉的一个自然选择是交叉熵损失函数。

然而,由于严重的类别不平衡(通常没有很多正区域),这种损失函数对于分割任务并不理想。

分割任务的一个更常见的损失是 Dice 相似系数,它是衡量两个轮廓重叠程度的指标。

Dice = 0 表示完全不匹, Dice =1 表示完美匹配 。

DSC ( A , B ) = 2 × ∣ A ∩ B ∣ ∣ A ∣ + ∣ B ∣ . \\text{DSC}(A, B) = \\frac{2 \\times |A \\cap B|}{|A| + |B|}. DSC(A,B)=A+B2×AB.
这里我们可以解读 𝐴 和 𝐵 作为体素集,𝐴 作为预测的肿瘤区域和 𝐵 是 ground truth。

我们的模型会将每个体素映射到 0 或 1

  • 0 表示它是背景体素
  • 1 表示它是分割区域的一部分。

DSC ( f , x , y ) = 2 × ∑ i , j f ( x ) i j × y i j + ϵ ∑ i , j f ( x ) i j + ∑ i , j y i j + ϵ \\text{DSC}(f, x, y) = \\frac{2 \\times \\sum_{i, j} f(x)_{ij} \\times y_{ij} + \\epsilon}{\\sum_{i,j} f(x)_{ij} + \\sum_{i, j} y_{ij} + \\epsilon} DSC(f,x,y)=i,jf(x)ij+i,jyij+ϵ2×i,jf(x)ij×yij+ϵ

  • x x x : the input image
  • f ( x ) f(x) f(x) : the model output (prediction)
  • y y y : the label (actual ground truth)
  • ϵ \\epsilon ϵ : 是为了避免被零除而添加的小数
def single_class_dice_coefficient(y_true, y_pred, axis=(0, 1, 2), 
                                  epsilon=0.00001):
    ### START CODE HERE (REPLACE INSTANCES OF 'None' with your code) ###
    
    dice_numerator = 2 * K.sum(y_true * y_pred) + epsilon
    dice_denominator = K.sum(y_true) + K.sum(y_pred) + epsilon
    dice_coefficient = dice_numerator / dice_denominator
    return dice_coefficient

4.2 soft dice loss

L D i c e ( p , q ) = 1 − 2 × ∑ i , j p i j q i j + ϵ ( ∑ i , j p i j 2 ) + ( ∑ i , j q i j 2 ) + ϵ \\mathcal{L}_{Dice}(p, q) = 1 - \\frac{2\\times\\sum_{i, j} p_{ij}q_{ij} + \\epsilon}{\\left(\\sum_{i, j} p_{ij}^2 \\right) + \\left(\\sum_{i, j} q_{ij}^2 \\right) + \\epsilon} LDice(p,q)=1(i,jpij2)+(i,jqij2)+ϵ2×i,jpijqij+ϵ

  • p p p is our predictions
  • q q q is the ground truth
  • q i q_i qi 是 0 or 1.

soft dice loss 就是 1-DSC

如果是多类别,就把每个类别的 DSC 累加起来:
L D i c e ( p , q ) = 1 − 1 N ∑ c = 1 C 2 × ∑ i , j p c i j q c i j + ϵ ( ∑ i , j p c i j 2 ) + ( ∑ i , j q c i j 2 ) + ϵ \\mathcal{L}_{Dice}(p, q) = 1 - \\frac{1}{N} \\sum_{c=1}^{C} \\frac{2\\times\\sum_{i, j} p_{cij}q_{cij} + \\epsilon}{\\left(\\sum_{i, j} p_{cij}^2 \\right) + \\left(\\sum_{i, j} q_{cij}^2 \\right) + \\epsilon} LDice(p,q)=1N1c=1C(i,j第一课第三周3-4节--2D和3D分割的优缺点以及如何构建UNet模型

第一课第三周5-6节-数据增强以及dice损失函数

收藏第一课第二周作业-学会计算分类各种指标-超详细教程

第一课第一周大作业-胸部14种疾病分类-代码详解

第一课第三周7-10节-AI算法这么棒,为什么我们周围的医院没有使用这些系统?

吴恩达第一课第三周随笔