赞
踩
在上一次分享中,我们在Dataset方法里,已经使用了transform函数,这节课对transform做一个详细的介绍。
上一次视频连接:MONAI中,一定要学会的三种Dataset
transform大致可以分为以下几个类别
想要什么样类别的变换,就在该类别下去找。
目录
2. 加载NIfTI 格式的文件【 LoadImage/ LoadImaged】
4. 强度变换 [NormalizeIntensityd / ScaleIntensityRanged]
接下来在实战中介绍部分常用的transform,其余的类似。以下演示基于字典变换。
这次使用的数据是医学图像十项全能挑战赛里面的CT数据(Task09_Spleen)。数据来源:http://medicaldecathlon.com/
- resource = "https://msd-for-monai.s3-us-west-2.amazonaws.com/Task09_Spleen.tar"
- md5 = "410d4a301da4e5b2f6f86ec3ddba524e"
-
- compressed_file = os.path.join(root_dir, "Task09_Spleen.tar")
- data_dir = os.path.join(root_dir, "Task09_Spleen")
- if not os.path.exists(data_dir):
- download_and_extract(resource, compressed_file, root_dir, md5)
-
- # root_dir: 存储地址,如‘.data/’
使用该方法考验网络的速度,毕竟是国外的网址。
"https://msd-for-monai.s3-us-west-2.amazonaws.com/Task09_Spleen.tar"
数据是三维的CT腹部图像,任务是脾脏的分割。
下载好数据后,先把image和label的地址加载到data_dict中,便于后续做变换。
- import glob
- data_dir = '/Volumes/Backup Plus/data/Task09_Spleen/'
- train_images = sorted(glob.glob(os.path.join(data_dir, "imagesTr", "*.nii.gz")))
- train_labels = sorted(glob.glob(os.path.join(data_dir, "labelsTr", "*.nii.gz")))
- data_dicts = [
- {"image": image_name, "label": label_name}
- for image_name, label_name in zip(train_images, train_labels)
- ]
- train_data_dicts, val_data_dicts = data_dicts[:-9], data_dicts[-9:]
MONAI的一个设计选择是,它不仅提供高级工作流组件,而且以最小的功能形式提供相对较低级别的api。
例如,LoadImage类是底层Nibabel映像加载器的简单可调用包装器。在使用一些必要的系统参数构造加载程序之后,使用NIfTI文件名调用加载程序实例将返回图像数据数组以及元数据,例如仿射信息和体素大小。简单说就是,如果是nii.gz格式的文件,调用LoadImage,它会自动调用Nibabel来打开数据。在python中,nii.gz一般都是通过Nibabel来打开的。
LoadImage/ LoadImaged在使用时会有细小的差别,通过举例来说明。
注意注意
如果这些变换不是在Compose里面进行组合使用,单独调用时都是需要先实例化的。
- loader = LoadImage(dtype=np.float32) # 实例化
- image, metadata = loader(train_data_dicts[0]["image"]) # 把image的地址传给loader
- print(f"input: {train_data_dicts[0]['image']}")
- print(f"image shape: {image.shape}")
- print(f"image affine:\n{metadata['affine']}")
- print(f"image pixdim:\n{metadata['pixdim']}")
如上图所示,LoadImage是会加载图像的值和元数据的。元数据里面包含分辨率,放射值,具体包含什么可以通过获取键来查询(metadata.keys())
其中,最后一个”filename_or_obj“也是一个重要的信息,它是image的地址。
在实际应用中,我们不需要metadata的话,可以这样加载
- loader = LoadImage(image_only=True, dtype=np.float32) # 表示只需要图像值
- image = loader(train_data_dicts[0]["image"])
- loader = LoadImaged(keys=("image", "label"))
- data_dict = loader(train_data_dicts[0])
- print(f"input:, {train_data_dicts[0]}")
- print(f"image shape: {data_dict['image'].shape}")
- print(f"label shape: {data_dict['label'].shape}")
- print(f"image pixdim:\n{data_dict['image_meta_dict']['pixdim']}")
这里的参数”keys“是你在data_dicts中设置的keys。表示要对image做变换还是label做变换。如果都做,就都写。比如keys=["image", "label"], keys = 'image', keys = 'label'
我们可以通过可视化方法,查看加载进来的数据。使用LoadImage/LoadImaged加载进来的数据是numpy格式
- image, label = data_dict["image"], data_dict["label"]
- plt.figure("visualize", (8, 4))
- plt.subplot(1, 2, 1)
- plt.title("image")
- plt.imshow(image[:, :, 30], cmap="gray")
- plt.subplot(1, 2, 2)
- plt.title("label")
- plt.imshow(label[:, :, 30])
- plt.show()
使用monai的模型时,默认是通道优先的格式。要求数据尺寸为:[num_channels, spatial_dim_1, spatial_dim_2, ... ,spatial_dim_n]。如上,我们的数据尺寸为[
512, 512, 55], 需要在前面一维通道。变成[1,
512, 512, 55],使用AddChanneld就可以帮我们轻松搞定。
- add_channel = AddChanneld(keys=["image", "label"])
- datac_dict = add_channel(data_dict)
- print(f"image shape: {datac_dict['image'].shape}")
改变换应该用在加载数据LoadImaged后面。
这两种都是对图像值强度进行变换的,像CT和MRI的值都是从-1000—+3000多的不等,通常需要进行归一化。
查看源码发现,该函数使用的归一化方法是 img-subtrahend/divisor
参 数 介 绍
subtrahend:被减数, 可以自己指定,默认为整个图像的均值。
divisor: 除数, 可以自己指定,默认为整个图像的方差。
nonzero: 布尔值。等于True,表示只对图像的非0区域做归一化。
channel_wise: 布尔值。当不指定subtrahend和divisor,为True, 表示在每个通道上进行计算均值和方差,为False,则在整个图像上计算均值和方差
- from monai.transforms import NormalizeIntensityd, ScaleIntensityRanged
- norm = NormalizeIntensityd(keys='image', nonzero=False, dtype=np.float32)
- datac_dict_norm = norm(datac_dict)
ScaleIntensityRanged和NormalizeIntensityd不同之处在于, ScaleIntensityRanged可以指定把哪些范围值缩放到那个区间。
比如对脾脏的分割中,我们只在于脾脏的CT值范围(假设在-300-- +300之间),而骨头等高强度的信号(大于2000)我们不需要。如果直接将这个强度进行归一化,脾脏内部的值范围就很小。我们就可以直接把脾脏的CT值范围 [-300,+300] 进行归一化到 [0, 1], 而不在[-300,+300] 这中间的值都为0。事实上,很多论文都是这样做的。
参 数 介 绍
a_min:float,强度原始范围最小值。可以理解为需要被归一化的最小值,如我们这个例子中的-300(需要写成小数,-300.0)
a_max: float, 强度原始范围最大值。可以理解为需要被归一化的最大值,如我们这个例子中的300(需要写成小数,300.0)
b_min: float, 强度目标范围最小值。可以理解为归一化后的最小值,通常设置为0.0
b_max: float, 强度目标范围最大值。可以理解为归一化后的最大值,通常设置为1.0
clip: 布尔值。设置为True, 才会把[-300,+300]之外的值都设置为0.通常为True
- scale = ScaleIntensityRanged(keys='image', a_min=-300.0, a_max=300.0, b_min=0.0, b_max=1.0, clip=True)
-
- # 由于上面我们把datac_dict归一化了,因此,我们要重新制造一个datac_dict
- data_dict = loader(train_data_dicts[2])
- datac_dict = add_channel(data_dict)
- data_scale = scale(datac_dict)
-
- print('original max value', datac_dict['image'].max())
- print('original min value', datac_dict['image'].min())
- print('target max value', data_scale['image'].max())
- print('target min value', data_scale['image'].min())
参 数 介 绍
prob: float, 旋转的概率,默认为0.1, 也就是10%的概率被旋转
max_k: int, 旋转90度的次数,默认为3
spatial_axes: 元祖(int,int)。指定围绕哪个面旋转,默认为(0,1),即按前两个轴旋转。
- from monai.transforms import RandRotate90d, Resized
- rotate = RandRotate90d(keys=['image', 'label'], prob=0.5, max_k=1, spatial_axes=(2,1))
- data_rotate = rotate(data_scale)
- print(data_rotate['image'].shape)
- print(data_rotate['label'].shape)
注意注意
使用旋转,尤其是随机旋转的时候一定要注意,如果是分割,image和label应当一同旋转,并且旋转方式要一模一样。
参 数 介 绍
spatial_size: 序列[int, int,], 期大小调整操作之后的空间尺寸的形状
mode: resize使用的插值方式,默认为”area, “可以选择"nearest"
, "linear"
, "bilinear"
, "bicubic"
, "trilinear"
- resize = Resized(keys=['image', 'label'], spatial_size=(256,256,256))
- data_resize = resize(data_rotate)
- print(data_resize['image'].shape)
-
- # (1, 256, 256, 256)
由于篇幅过长,剩下的下一篇进行分享。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。