使用 Python 和 Monai 来扩充您的数据集以进行肿瘤或器官分割

1.介绍

我们在上一篇文章中讨论了如何为肿瘤分割预处理 3D volumes,因此在本文中,我们将讨论处理深度学习项目时的另一个重要步骤。这是数据增强步骤。

2.什么是数据增强?

我们都知道,为了训练神经网络,需要大量数据才能获得准确的模型以及可以处理该特定任务中大多数情况的稳健模型。然而,在任何任务中,特别是在医疗保健项目中,并不总是能够获得大量的自然数据。因为医学成像中的一个输入是具有多个切片的单个患者,并且我们都知道收集这类数据(许多患者)的数据集是多么困难。

出于这个原因,我们必须通过创建合成数据来帮助自己,以便稍微改善我们的结果。

这些合成数据的生成称为数据增强,这意味着我们从数据集开始,然后执行一些转换以生成新数据。

3.工具和操作

如果您以前为具有正常任务和 2D 图像的项目进行过数据增强,您就会明白我想说什么;否则,别担心,我们会一步一步来,直到得到结果。

要生成这些合成数据,我们必须首先对原始数据应用一些仿射变换。这些变换可以包括旋转、缩放、平移(移位)、噪声、翻转等。

但要小心,因为在处理普通图像时,我们可以使用这些变换中的任何一种;但是,在处理医学图像时,我们不能使用所有变换,因为它可能会创建一个与人体无关的形状,这不是重点。

而且由于我们将使用 3D volumes,如果我们不小心,转换会变得更糟。

我们将始终使用我们用于处理此操作的相同 monai。对于不熟悉monai的人来说,它是一个基于Pytorch的开源框架,可以用来对医学图像进行分割或分类。

4.可以使用哪些变换?

以我的经验,只有少数几种变换可以随机混合在一起以产生合成患者。以下是我发现有效的最佳转换:

  • 翻转
  • 旋转
  • 平移
  • 高斯噪声
    因此,我将向您展示如何使用 monai 轻松应用这些转换。

关于如何应用这些转换,您应该知道一些事情。正如我之前所说,monai 是基于 Python 的,因此没有像 tensorflow 这样的特定函数可以根据您在参数中提供的数量生成特定数量的数据。相反,有三种方法可以用 monai 扩充数据。

1- 第一个是在训练时使用转换。这意味着在每个训练阶段中,您应用的转换与之前的转换不同,因此如果您进行 100 次训练,您将获得 100 种不同的数据表示。

PS:数据量会和以前一样,但是transforms会随着每个epoch的变化而变化。

2- 第二种方法是在训练之前应用转换,获得合成数据,然后使用增加的数据量(原始数据加上合成数据)训练模型。

3- 第三个几乎与第二个相同,除了不是单独生成数据,您将应用转换并将生成的数据保存在张量中,这意味着仅在 RAM 中,并且训练将自动开始(与使用tensorflow时相同的事情,但在这种情况下,您需要自己创建保存在RAM中的函数)。

对我来说,我尝试了第一种和第二种方法;第二个最适合我,我会解释原因。正如我之前所说,当我们使用这种仿射变换时,会得到一些现实中不存在的形状,这会影响训练,因此您应该单独创建合成数据,对其进行验证,然后将其用于训练。

但是,我将演示如何执行第一个和第二个。

5.编码部分

在我们了解了理论之后,让我们开始编写这些变换的代码。我们需要做的第一件事是创建一个字典来帮助我们处理数据和标签;我不会在这里详细介绍,因为您可以在上一篇文章中找到解释。这是制作字典的代码。

data_dir = 'Path to your data'

train_images = sorted(glob(os.path.join(data_dir, 'TrainData', '*.nii.gz')))
train_labels = sorted(glob(os.path.join(data_dir, 'TrainLabels', '*.nii.gz')))

val_images = sorted(glob(os.path.join(data_dir, 'ValData', '*.nii.gz')))
val_labels = sorted(glob(os.path.join(data_dir, 'ValLabels', '*.nii.gz')))

train_files = [{"image": image_name, 'label': label_name} for image_name, label_name in zip(train_images, train_labels)]
val_files = [{"image": image_name, 'label': label_name} for image_name, label_name in zip(val_images, val_labels)]

正如我之前所说,我将演示如何使用第一种和第二种方法进行数据增强。

5.1训练期间的数据增强

如果您在训练期间使用增强,您会将变换与预处理部分结合起来,这样每个患者都会同时进行预处理和变换。

以下是我们将使用的转换:

  • Flipd → 将翻转应用于字典
  • Rotated →将旋转应用于字典
  • Zoomd→ 应用缩放
  • RandGaussianNoised → 将高斯噪声应用于字典
  • 最后还有RandAffined → 其实这个函数可以同时进行多个变换;我们将使用它来执行平移变换,但如果您不想使用Rotated功能,也可以使用它来执行旋转。

以下是使用预处理和转换函数生成合成数据的代码:

generat_transforms = Compose(
    [
        LoadImaged(keys=["image", "label"]),
        AddChanneld(keys=["image", "label"]),
        Spacingd(keys=["image", "label"], pixdim=(1.5, 1.5, 2.0), mode=("bilinear", "nearest")),
        Orientationd(keys=["image", "label"], axcodes="RAS"),
        ScaleIntensityRanged(keys=["image"], a_min=-200, a_max=200,b_min=0.0, b_max=1.0, clip=True,), 
        RandAffined(keys=['image', 'label'], prob=0.5, translate_range=10), 
        RandRotated(keys=['image', 'label'], prob=0.5, range_x=10.0),
        RandGaussianNoised(keys='image', prob=0.5),
        ToTensord(keys=["image", "label"]),
    ]
)

5.2 训练前的数据增强

本节与上一节类似,但在应用转换后,我们需要添加一个函数,将 Torch 张量保存到 nifti 文件中。我们将使用本文中讨论的方法来完成此操作。

现在由您决定是仅生成数据并保存它,还是进行预处理和扩充然后保存所有数据,这样您就不需要在训练阶段进行任何转换。但我会向你展示两者,你可以选择你喜欢的。

5.2.1 带预处理

如果您想同时进行预处理和数据扩充,您可以使用上一个方法中的代码,但我们将添加另一个代码来保存卷。

预处理+扩充的代码:

generat_transforms = Compose(
    [
        LoadImaged(keys=["image", "label"]),
        AddChanneld(keys=["image", "label"]),
        Spacingd(keys=["image", "label"], pixdim=(1.5, 1.5, 2.0), mode=("bilinear", "nearest")),
        Orientationd(keys=["image", "label"], axcodes="RAS"),
        ScaleIntensityRanged(keys=["image"], a_min=-200, a_max=200,b_min=0.0, b_max=1.0, clip=True,), 
        RandAffined(keys=['image', 'label'], prob=0.5, translate_range=10), 
        RandRotated(keys=['image', 'label'], prob=0.5, range_x=10.0),
        RandGaussianNoised(keys='image', prob=0.5),
        ToTensord(keys=["image", "label"]),
    ]
)

5.2.2 无需预处理

无需预处理即可进行扩充的代码:

generat_transforms = Compose(
    [
        LoadImaged(keys=["image", "label"]),
        AddChanneld(keys=["image", "label"]),

        RandAffined(keys=['image', 'label'], prob=0.5, translate_range=10), 
        RandRotated(keys=['image', 'label'], prob=0.5, range_x=10.0),
        RandGaussianNoised(keys='image', prob=0.5),
        ToTensord(keys=["image", "label"]),
    ]
)

这是相同的切片,但具有我们所讨论的不同转换:
在这里插入图片描述
然后我们使用这个函数将torch张量转换成numpy数组,然后再转换成nifti

def save_nifti(in_image, in_label, out, index = 0):
    # Convert the torch tensors into numpy array
    volume = np.array(in_image.detach().cpu()[0, :, :, :], dtype=np.float32)
    lab = np.array(in_label.detach().cpu()[0, :, :, :], dtype=np.float32)
    
    # Convert the numpy array into nifti file
    volume = nib.Nifti1Image(volume, np.eye(4))
    lab = nib.Nifti1Image(lab, np.eye(4))
    
    # Create the path to save the images and labels
    path_out_images = os.path.join(out, 'Images')
    path_out_labels = os.path.join(out, 'Labels')
    
    # Make directory if not existing
    if not os.path.exists(path_out_images):
        os.mkdir(path_out_images)
    if not os.path.exists(path_out_labels):
        os.mkdir(path_out_labels)
    
    path_data = os.path.join(out, 'Images')
    path_label = os.path.join(out, 'Labels')
    nib.save(volume, os.path.join(path_data, f'patient_generated_{index}.nii.gz'))
    nib.save(lab, os.path.join(path_label, f'patient_generated_{index}.nii.gz'))

    print(f'patient_generated_{index} is saved', end='\r')

现在我们必须创建一个循环,该循环将使用我们指定的运行次数应用各种转换;这个运行次数将乘以数据量。这是我使用的循环:

output_path = 'D:/3_Stage/ALL_THE_DATA/generated_data'
number_runs = 10
for i in range(number_runs):
    name_folder = 'generated_data_' + str(i)
    os.mkdir(os.path.join(output_path, name_folder))
    output = os.path.join(output_path, name_folder)
    check_ds = Dataset(data=train_files, transform=generat_transforms)
    check_loader = DataLoader(check_ds, batch_size=1)
    check_data = first(check_loader)
    for index, patient in enumerate(check_loader):
        save_nifti(patient['image'], patient['label'], output, index)
    print(f'step {i} done')

6.显示合成患者

让我们看一位患者变换前后。这是将用于创建数据加载器的代码:

original_ds = Dataset(data=train_files, transform=original_transforms)
original_loader = DataLoader(original_ds, batch_size=1)
original_patient = first(original_loader)

generat_ds = Dataset(data=train_files, transform=generat_transforms)
generat_loader = DataLoader(generat_ds, batch_size=1)
generat_patient = first(generat_loader)

这是显示图像的代码:

number_slice = 30
plt.figure("display", (12, 6))
plt.subplot(1, 2, 1)
plt.title(f"Original patient slice {number_slice}")
plt.imshow(original_patient["image"][0, 0, :, :, number_slice], cmap="gray")
plt.subplot(1, 2, 2)
plt.title(f"Generated patient slice {number_slice}")
plt.imshow(generat_patient["image"][0, 0, :, :, number_slice], cmap="gray")

结果如下:
在这里插入图片描述
您可以看到,对于同一患者的同一切片,我们有两个不同的身体部位,这只是一个简单的转换,因此您可以添加更多以获得更复杂的东西。

注意:如果您使用第二种方法并保存生成的数据,请返回并检查它,因为您会发现一些具有与正常身体不匹配的随机形状的患者,必须从您的数据集中删除。

7.完整代码

from monai.utils import first
from monai.transforms import (
    AddChanneld,
    Compose,
    CropForegroundd,
    LoadImaged,
    Orientationd,
    RandSpatialCropd,
    ScaleIntensityRanged,
    Spacingd,
    Resized,
    ToTensord,
    ScaleIntensityd,
    RandRotated,
    RandZoomd,
    RandGaussianNoised,
    Flipd,
    RandAffined,
)


from monai.data import DataLoader, Dataset
import matplotlib.pyplot as plt
import os
import glob
import numpy as np
import os
import torch
import nibabel as nib
from tqdm import tqdm

def save_nifti(in_image, in_label, out, index = 0):
    # Convert the torch tensors into numpy array
    volume = np.array(in_image.detach().cpu()[0, :, :, :], dtype=np.float32)
    lab = np.array(in_label.detach().cpu()[0, :, :, :], dtype=np.float32)
    
    # Convert the numpy array into nifti file
    volume = nib.Nifti1Image(volume, np.eye(4))
    lab = nib.Nifti1Image(lab, np.eye(4))
    
    # Create the path to save the images and labels
    path_out_images = os.path.join(out, 'Images')
    path_out_labels = os.path.join(out, 'Labels')
    
    # Make directory if not existing
    if not os.path.exists(path_out_images):
        os.mkdir(path_out_images)
    if not os.path.exists(path_out_labels):
        os.mkdir(path_out_labels)
    
    path_data = os.path.join(out, 'Images')
    path_label = os.path.join(out, 'Labels')
    nib.save(volume, os.path.join(path_data, f'patient_generated_{index}.nii.gz'))
    nib.save(lab, os.path.join(path_label, f'patient_generated_{index}.nii.gz'))

    print(f'patient_generated_{index} is saved', end='\r')

data_dir = 'D:/3_Stage/ALL_THE_DATA/fixed_data_03_august/all_together'
train_images = sorted(glob.glob(os.path.join(data_dir, "TrainData", "*.nii.gz")))
train_labels = sorted(glob.glob(os.path.join(data_dir, "TrainLabels", "*.nii.gz")))

train_files = [{"image": image_name, "label": label_name} for image_name, label_name in zip(train_images, train_labels)]


original_transforms = Compose(
    [
        LoadImaged(keys=["image", "label"]),
        AddChanneld(keys=["image", "label"]),
        Spacingd(keys=["image", "label"], pixdim=(1.5, 1.5, 2.0), mode=("bilinear", "nearest")),
        Orientationd(keys=["image", "label"], axcodes="RAS"),
        ScaleIntensityRanged(keys=["image"], a_min=-200, a_max=200,b_min=0.0, b_max=1.0, clip=True,), 
        ToTensord(keys=["image", "label"]),
    ]
)

generat_transforms = Compose(
    [
        LoadImaged(keys=["image", "label"]),
        AddChanneld(keys=["image", "label"]),
        Spacingd(keys=["image", "label"], pixdim=(1.5, 1.5, 2.0), mode=("bilinear", "nearest")),
        Orientationd(keys=["image", "label"], axcodes="RAS"),
        ScaleIntensityRanged(keys=["image"], a_min=-200, a_max=200,b_min=0.0, b_max=1.0, clip=True,), 
        RandAffined(keys=['image', 'label'], prob=0.5, translate_range=10), 
        RandRotated(keys=['image', 'label'], prob=0.5, range_x=10.0),
        RandGaussianNoised(keys='image', prob=0.5),
        ToTensord(keys=["image", "label"]),
    ]
)


original_ds = Dataset(data=train_files, transform=original_transforms)
original_loader = DataLoader(original_ds, batch_size=1)
original_patient = first(original_loader)

generat_ds = Dataset(data=train_files, transform=generat_transforms)
generat_loader = DataLoader(generat_ds, batch_size=1)
generat_patient = first(generat_loader)

# 显示原始患者和生成的患者
number_slice = 30
plt.figure("display", (12, 6))
plt.subplot(1, 2, 1)
plt.title(f"Original patient slice {number_slice}")
plt.imshow(original_patient["image"][0, 0, :, :, number_slice], cmap="gray")
plt.subplot(1, 2, 2)
plt.title(f"Generated patient slice {number_slice}")
plt.imshow(generat_patient["image"][0, 0, :, :, number_slice], cmap="gray")


plt.show()

# 保存生成的患者
# 变量“number_runs”将控制您将数据相乘的次数。
output_path = 'D:/3_Stage/ALL_THE_DATA/generated_data'
number_runs = 10
for i in range(number_runs):
    name_folder = 'generated_data_' + str(i)
    os.mkdir(os.path.join(output_path, name_folder))
    output = os.path.join(output_path, name_folder)
    check_ds = Dataset(data=train_files, transform=generat_transforms)
    check_loader = DataLoader(check_ds, batch_size=1)
    check_data = first(check_loader)
    for index, patient in enumerate(check_loader):
        save_nifti(patient['image'], patient['label'], output, index)
    print(f'step {i} done')

在这里插入图片描述

参考目录

https://pycad.co/3d-volumes-augmentation-for-tumor-segmentation/

  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值