我看的代码是nnunet1的代码,预处理对应的文件:
预处理主要分为三部分:cropping,resample和normalization
-
cropping
图像裁剪就是将三维的医学图像裁剪到它的非零区域,首先在图片中寻找一个最小的bounding box,之后根据改bounding box对于图像进行裁剪。
1. 首先生成 非零掩码区域,并且对非零掩码区域进行空洞填充
#找到不同模态的数据的非零区域并并集
def create_nonzero_mask(data): #创建非零掩码区域
from scipy.ndimage import binary_fill_holes
#判断四维还是三维
#c是模态数量
assert len(data.shape) == 4 or len(data.shape) == 3, "data must have shape (C, X, Y, Z) or shape (C, X, Y)"
#创建一个与第二维度相同的布尔类型的数组
nonzero_mask = np.zeros(data.shape[1:], dtype=bool)
for c in range(data.shape[0]):#不同模态创建非零掩码区域
this_mask = data[c] != 0
nonzero_mask = nonzero_mask | this_mask #非零区域的并集
#填充空洞部分
nonzero_mask = binary_fill_holes(nonzero_mask)
return nonzero_mask #返回图片的非零掩码区域,作为mask,传递到下边的函数
2. 根据生成的非零模板,确定用于裁剪的bounding box的大小和位置
#在代码中就是要找到nonzero_mask在x,y,z三个坐标轴上值为1的最小坐标值以及最大坐标值
def get_bbox_from_mask(mask, outside_value=0):
mask_voxel_coords = np.where(mask != outside_value) #找到mask中不为零的坐标值并返回
minzidx = int(np.min(mask_voxel_coords[0]))
maxzidx = int(np.max(mask_voxel_coords[0])) + 1
minxidx = int(np.min(mask_voxel_coords[1]))
maxxidx = int(np.max(mask_voxel_coords[1])) + 1
minyidx = int(np.min(mask_voxel_coords[2]))
maxyidx = int(np.max(mask_voxel_coords[2])) + 1
return [[minzidx, maxzidx], [minxidx, maxxidx], [minyidx, maxyidx]]
#找到x,y,z三个坐标轴上值为1的最小坐标和最大坐标,作为bbox向下面传递
3. 根据bounding box对图像和分割标注seg进行裁剪,之后组合在一起
#第三步就根据bounding_box对该张图像的每个方向依次进行裁剪,然后重新组合在一起。
#根据bbox提取ROI
def crop_to_bbox(image, bbox):
assert len(image.shape) == 3, "only supports 3d images"
#x,y,z三个方向对应非零掩码区域
resizer = (slice(bbox[0][0], bbox[0][1]), slice(bbox[1][0], bbox[1][1]), slice(bbox[2][0], bbox[2][1]))
return image[resizer]
#以case为单位读取data和seg
def load_case_from_list_of_files(data_files, seg_file=None):
assert isinstance(data_files, list) or isinstance(data_files, tuple), "case must be either a list or a tuple"
#用asert语句判断数据类型,不是的话则返回后面的报错信息'case...'
properties = OrderedDict()
data_itk = [sitk.ReadImage(f) for f in data_files]
properties["original_size_of_raw_data"] = np.array(data_itk[0].GetSize())[[2, 1, 0]]
properties["original_spacing"] = np.array(data_itk[0].GetSpacing())[[2, 1, 0]]
properties["list_of_data_files"] = data_files
properties["seg_file"] = seg_file
properties["itk_origin"] = data_itk[0].GetOrigin()
properties["itk_spacing"] = data_itk[0].GetSpacing()
properties["itk_direction"] = data_itk[0].GetDirection()
#对数据进行拼接
data_npy = np.vstack([sitk.GetArrayFromImage(d)[None] for d in data_itk])
if seg_file is not None:
seg_itk = sitk.ReadImage(seg_file)
seg_npy = sitk.GetArrayFromImage(seg_itk)[None].astype(np.float32)
else:
seg_npy = None
return data_npy.astype(np.float32), seg_npy, properties
def crop_to_nonzero(data, seg=None, nonzero_label=-1):
"""
:param data:
:param seg:
:param nonzero_label: this will be written into the segmentation map
:return:
"""
nonzero_mask = create_nonzero_mask(data) # 生成非零掩码区域
bbox = get_bbox_from_mask(nonzero_mask, 0) #获得bounding——box的坐标
cropped_data = []
for c in range(data.shape[0]):#不同模态
#对每个模态的数据根据生成的box进行crop
cropped = crop_to_bbox(data[c], bbox)
#将不同模态的数据进行拼接
cropped_data.append(cropped[None])
data = np.vstack(cropped_data) #竖直堆叠后的数组
if seg is not None:
cropped_seg = []
for c in range(seg.shape[0]):#对seg进行分割
cropped = crop_to_bbox(seg[c], bbox)
cropped_seg.append(cropped[None])
seg = np.vstack(cropped_seg)
#对mask也进行crop
nonzero_mask = crop_to_bbox(nonzero_mask, bbox)[None]
if seg is not None:
#将非零区域以外的信息都标注成-1
#这样值为0的代表图像中之不为0的背景,值为-1的代表图像中值为0的背景
seg[(seg == 0) & (nonzero_mask == 0)] = nonzero_label
else:
nonzero_mask = nonzero_mask.astype(int)
nonzero_mask[nonzero_mask == 0] = nonzero_label
nonzero_mask[nonzero_mask > 0] = 0
seg = nonzero_mask
return data, seg, bbox
注意:在对seg进行裁剪之后最后将非零区域之外的信息都标注成-1。对于这一步我的理解是:
这时图像共有两块区域:背景区域和非背景区域
背景区域又分为两部分:值为0的背景(在bounding box之内的背景)和值为-1的背景(不在bounding box之内的背景)搭配下图理解
借鉴大神的,大神博客链接:知乎:如何针对三维医学图像分割任务进行通用数据预处理:nnUNet中预处理流程总结及代码分析