def __call__(self, data_array, affine=None, interp_order=3): """ Args: data_array (ndarray): in shape (num_channels, H[, W, ...]). affine (matrix): (N+1)x(N+1) original affine matrix for spatially ND `data_array`. Defaults to identity. interp_order (int): The order of the spline interpolation, default is 3. The order has to be in the range 0-5. https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.zoom.html Returns: data_array (resampled into `self.pixdim`), original pixdim, current pixdim. """ sr = data_array.ndim - 1 if sr <= 0: raise ValueError( "the array should have at least one spatial dimension.") if affine is None: # default to identity affine = np.eye(sr + 1, dtype=np.float64) affine_ = np.eye(sr + 1, dtype=np.float64) else: affine_ = to_affine_nd(sr, affine) out_d = self.pixdim[:sr] if out_d.size < sr: out_d = np.append(out_d, [1.0] * (out_d.size - sr)) if np.any(out_d <= 0): raise ValueError(f"pixdim must be positive, got {out_d}") # compute output affine, shape and offset new_affine = zoom_affine(affine_, out_d, diagonal=self.diagonal) output_shape, offset = compute_shape_offset(data_array.shape[1:], affine_, new_affine) new_affine[:sr, -1] = offset[:sr] transform = np.linalg.inv(affine_) @ new_affine # adapt to the actual rank transform_ = to_affine_nd(sr, transform) # resample dtype = data_array.dtype if self.dtype is None else self.dtype output_data = [] for data in data_array: data_ = scipy.ndimage.affine_transform( data.astype(dtype), matrix=transform_, output_shape=output_shape, order=interp_order, mode=self.mode, cval=self.cval, ) output_data.append(data_) output_data = np.stack(output_data) new_affine = to_affine_nd(affine, new_affine) return output_data, affine, new_affine
def write_nifti( data: np.ndarray, file_name: str, affine: Optional[np.ndarray] = None, target_affine: Optional[np.ndarray] = None, resample: bool = True, output_spatial_shape: Optional[Sequence[int]] = None, mode: Union[GridSampleMode, str] = GridSampleMode.BILINEAR, padding_mode: Union[GridSamplePadMode, str] = GridSamplePadMode.BORDER, align_corners: bool = False, dtype: Optional[np.dtype] = np.float64, output_dtype: Optional[np.dtype] = np.float32, ) -> None: """ Write numpy data into NIfTI files to disk. This function converts data into the coordinate system defined by `target_affine` when `target_affine` is specified. If the coordinate transform between `affine` and `target_affine` could be achieved by simply transposing and flipping `data`, no resampling will happen. otherwise this function will resample `data` using the coordinate transform computed from `affine` and `target_affine`. Note that the shape of the resampled `data` may subject to some rounding errors. For example, resampling a 20x20 pixel image from pixel size (1.5, 1.5)-mm to (3.0, 3.0)-mm space will return a 10x10-pixel image. However, resampling a 20x20-pixel image from pixel size (2.0, 2.0)-mm to (3.0, 3.0)-mma space will output a 14x14-pixel image, where the image shape is rounded from 13.333x13.333 pixels. In this case `output_spatial_shape` could be specified so that this function writes image data to a designated shape. When `affine` and `target_affine` are None, the data will be saved with an identity matrix as the image affine. This function assumes the NIfTI dimension notations. Spatially it supports up to three dimensions, that is, H, HW, HWD for 1D, 2D, 3D respectively. When saving multiple time steps or multiple channels `data`, time and/or modality axes should be appended after the first three dimensions. For example, shape of 2D eight-class segmentation probabilities to be saved could be `(64, 64, 1, 8)`. Also, data in shape (64, 64, 8), (64, 64, 8, 1) will be considered as a single-channel 3D image. Args: data: input data to write to file. file_name: expected file name that saved on disk. affine: the current affine of `data`. Defaults to `np.eye(4)` target_affine: before saving the (`data`, `affine`) as a Nifti1Image, transform the data into the coordinates defined by `target_affine`. resample: whether to run resampling when the target affine could not be achieved by swapping/flipping data axes. output_spatial_shape: spatial shape of the output image. This option is used when resample = True. mode: {``"bilinear"``, ``"nearest"``} This option is used when ``resample = True``. Interpolation mode to calculate output values. Defaults to ``"bilinear"``. See also: https://pytorch.org/docs/stable/nn.functional.html#grid-sample padding_mode: {``"zeros"``, ``"border"``, ``"reflection"``} This option is used when ``resample = True``. Padding mode for outside grid values. Defaults to ``"border"``. See also: https://pytorch.org/docs/stable/nn.functional.html#grid-sample align_corners: Geometrically, we consider the pixels of the input as squares rather than points. See also: https://pytorch.org/docs/stable/nn.functional.html#grid-sample dtype: data type for resampling computation. Defaults to ``np.float64`` for best precision. If None, use the data type of input data. To be compatible with other modules, the output data type is always ``np.float32``. output_dtype: data type for saving data. Defaults to ``np.float32``. """ assert isinstance(data, np.ndarray), "input data must be numpy array." dtype = dtype or data.dtype sr = min(data.ndim, 3) if affine is None: affine = np.eye(4, dtype=np.float64) affine = to_affine_nd(sr, affine) if target_affine is None: target_affine = affine target_affine = to_affine_nd(sr, target_affine) if np.allclose(affine, target_affine, atol=1e-3): # no affine changes, save (data, affine) results_img = nib.Nifti1Image(data.astype(output_dtype), to_affine_nd(3, target_affine)) nib.save(results_img, file_name) return # resolve orientation start_ornt = nib.orientations.io_orientation(affine) target_ornt = nib.orientations.io_orientation(target_affine) ornt_transform = nib.orientations.ornt_transform(start_ornt, target_ornt) data_shape = data.shape data = nib.orientations.apply_orientation(data, ornt_transform) _affine = affine @ nib.orientations.inv_ornt_aff(ornt_transform, data_shape) if np.allclose(_affine, target_affine, atol=1e-3) or not resample: results_img = nib.Nifti1Image(data.astype(output_dtype), to_affine_nd(3, target_affine)) nib.save(results_img, file_name) return # need resampling affine_xform = AffineTransform( normalized=False, mode=mode, padding_mode=padding_mode, align_corners=align_corners, reverse_indexing=True ) transform = np.linalg.inv(_affine) @ target_affine if output_spatial_shape is None: output_spatial_shape, _ = compute_shape_offset(data.shape, _affine, target_affine) output_spatial_shape_ = list(output_spatial_shape) if data.ndim > 3: # multi channel, resampling each channel while len(output_spatial_shape_) < 3: output_spatial_shape_ = output_spatial_shape_ + [1] spatial_shape, channel_shape = data.shape[:3], data.shape[3:] data_np = data.reshape(list(spatial_shape) + [-1]) data_np = np.moveaxis(data_np, -1, 0) # channel first for pytorch data_torch = affine_xform( torch.as_tensor(np.ascontiguousarray(data_np).astype(dtype)).unsqueeze(0), torch.as_tensor(np.ascontiguousarray(transform).astype(dtype)), spatial_size=output_spatial_shape_[:3], ) data_np = data_torch.squeeze(0).detach().cpu().numpy() data_np = np.moveaxis(data_np, 0, -1) # channel last for nifti data_np = data_np.reshape(list(data_np.shape[:3]) + list(channel_shape)) else: # single channel image, need to expand to have batch and channel while len(output_spatial_shape_) < len(data.shape): output_spatial_shape_ = output_spatial_shape_ + [1] data_torch = affine_xform( torch.as_tensor(np.ascontiguousarray(data).astype(dtype)[None, None]), torch.as_tensor(np.ascontiguousarray(transform).astype(dtype)), spatial_size=output_spatial_shape_[: len(data.shape)], ) data_np = data_torch.squeeze(0).squeeze(0).detach().cpu().numpy() results_img = nib.Nifti1Image(data_np.astype(output_dtype), to_affine_nd(3, target_affine)) nib.save(results_img, file_name) return
def write_nifti( data, file_name: str, affine=None, target_affine=None, resample: bool = True, output_shape=None, interp_order: str = "bilinear", mode: str = "border", dtype=None, ): """ Write numpy data into NIfTI files to disk. This function converts data into the coordinate system defined by `target_affine` when `target_affine` is specified. If the coordinate transform between `affine` and `target_affine` could be achieved by simply transposing and flipping `data`, no resampling will happen. otherwise this function will resample `data` using the coordinate transform computed from `affine` and `target_affine`. Note that the shape of the resampled `data` may subject to some rounding errors. For example, resampling a 20x20 pixel image from pixel size (1.5, 1.5)-mm to (3.0, 3.0)-mm space will return a 10x10-pixel image. However, resampling a 20x20-pixel image from pixel size (2.0, 2.0)-mm to (3.0, 3.0)-mma space will output a 14x14-pixel image, where the image shape is rounded from 13.333x13.333 pixels. In this case `output_shape` could be specified so that this function writes image data to a designated shape. When `affine` and `target_affine` are None, the data will be saved with an identity matrix as the image affine. This function assumes the NIfTI dimension notations. Spatially it supports up to three dimensions, that is, H, HW, HWD for 1D, 2D, 3D respectively. When saving multiple time steps or multiple channels `data`, time and/or modality axes should be appended after the first three dimensions. For example, shape of 2D eight-class segmentation probabilities to be saved could be `(64, 64, 1, 8)`. Also, data in shape (64, 64, 8), (64, 64, 8, 1) will be considered as a single-channel 3D image. Args: data (numpy.ndarray): input data to write to file. file_name: expected file name that saved on disk. affine (numpy.ndarray): the current affine of `data`. Defaults to `np.eye(4)` target_affine (numpy.ndarray, optional): before saving the (`data`, `affine`) as a Nifti1Image, transform the data into the coordinates defined by `target_affine`. resample: whether to run resampling when the target affine could not be achieved by swapping/flipping data axes. output_shape (None or tuple of ints): output image shape. This option is used when resample = True. interp_order (`nearest|bilinear`): the interpolation mode, default is "bilinear". See also: https://pytorch.org/docs/stable/nn.functional.html#grid-sample This option is used when `resample = True`. mode (`zeros|border|reflection`): The mode parameter determines how the input array is extended beyond its boundaries. Defaults to "border". This option is used when `resample = True`. dtype (np.dtype, optional): convert the image to save to this data type. """ assert isinstance(data, np.ndarray), "input data must be numpy array." sr = min(data.ndim, 3) if affine is None: affine = np.eye(4, dtype=np.float64) affine = to_affine_nd(sr, affine) if target_affine is None: target_affine = affine target_affine = to_affine_nd(sr, target_affine) if np.allclose(affine, target_affine, atol=1e-3): # no affine changes, save (data, affine) results_img = nib.Nifti1Image(data.astype(dtype), to_affine_nd(3, target_affine)) nib.save(results_img, file_name) return # resolve orientation start_ornt = nib.orientations.io_orientation(affine) target_ornt = nib.orientations.io_orientation(target_affine) ornt_transform = nib.orientations.ornt_transform(start_ornt, target_ornt) data_shape = data.shape data = nib.orientations.apply_orientation(data, ornt_transform) _affine = affine @ nib.orientations.inv_ornt_aff(ornt_transform, data_shape) if np.allclose(_affine, target_affine, atol=1e-3) or not resample: results_img = nib.Nifti1Image(data.astype(dtype), to_affine_nd(3, target_affine)) nib.save(results_img, file_name) return # need resampling affine_xform = AffineTransform(normalized=False, mode=interp_order, padding_mode=mode, align_corners=True, reverse_indexing=True) transform = np.linalg.inv(_affine) @ target_affine if output_shape is None: output_shape, _ = compute_shape_offset(data.shape, _affine, target_affine) if data.ndim > 3: # multi channel, resampling each channel while len(output_shape) < 3: output_shape = list(output_shape) + [1] spatial_shape, channel_shape = data.shape[:3], data.shape[3:] data_ = data.reshape(list(spatial_shape) + [-1]) data_ = np.moveaxis(data_, -1, 0) # channel first for pytorch data_ = affine_xform( torch.from_numpy((data_.astype(np.float64))[None]), torch.from_numpy(transform.astype(np.float64)), spatial_size=output_shape[:3], ) data_ = data_.squeeze(0).detach().cpu().numpy() data_ = np.moveaxis(data_, 0, -1) # channel last for nifti data_ = data_.reshape(list(data_.shape[:3]) + list(channel_shape)) else: # single channel image, need to expand to have batch and channel while len(output_shape) < len(data.shape): output_shape = list(output_shape) + [1] data_ = affine_xform( torch.from_numpy((data.astype(np.float64))[None, None]), torch.from_numpy(transform.astype(np.float64)), spatial_size=output_shape[:len(data.shape)], ) data_ = data_.squeeze(0).squeeze(0).detach().cpu().numpy() dtype = dtype or data.dtype results_img = nib.Nifti1Image(data_.astype(dtype), to_affine_nd(3, target_affine)) nib.save(results_img, file_name) return
def write_nifti( data, file_name, affine=None, target_affine=None, resample=True, output_shape=None, interp_order=3, mode="constant", cval=0, dtype=None, ): """ Write numpy data into NIfTI files to disk. This function converts data into the coordinate system defined by `target_affine` when `target_affine` is specified. if the coordinate transform between `affine` and `target_affine` could be achieved by simply transposing and flipping `data`, no resampling will happen. otherwise this function will resample `data` using the coordinate transform computed from `affine` and `target_affine`. Note that the shape of the resampled `data` may subject to some rounding errors. For example, resampling a 20x20 pixel image from pixel size (1.5, 1.5)-mm to (3.0, 3.0)-mm space will return a 10x10-pixel image. However, resampling a 20x20-pixel image from pixel size (2.0, 2.0)-mm to (3.0, 3.0)-mma space will output a 14x14-pixel image, where the image shape is rounded from 13.333x13.333 pixels. In this case `output_shape` could be specified so that this function writes image data to a designated shape. when `affine` and `target_affine` are None, the data will be saved with an identity matrix as the image affine. This function assumes the NIfTI dimension notations. Spatially It supports up to three dimensions, that is, H, HW, HWD for 1D, 2D, 3D respectively. When saving multiple time steps or multiple channels `data`, time and/or modality axes should be appended after the first three dimensions. For example, shape of 2D eight-class segmentation probabilities to be saved could be `(64, 64, 1, 8)`, Args: data (numpy.ndarray): input data to write to file. file_name (string): expected file name that saved on disk. affine (numpy.ndarray): the current affine of `data`. Defaults to `np.eye(4)` target_affine (numpy.ndarray, optional): before saving the (`data`, `affine`) as a Nifti1Image, transform the data into the coordinates defined by `target_affine`. resample (bool): whether to run resampling when the target affine could not be achieved by swapping/flipping data axes. output_shape (None or tuple of ints): output image shape. this option is used when resample = True. interp_order (int): the order of the spline interpolation, default is 3. The order has to be in the range 0 - 5. https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.affine_transform.html this option is used when `resample = True`. mode (`reflect|constant|nearest|mirror|wrap`): The mode parameter determines how the input array is extended beyond its boundaries. this option is used when `resample = True`. cval (scalar): Value to fill past edges of input if mode is "constant". Default is 0.0. this option is used when `resample = True`. dtype (np.dtype, optional): convert the image to save to this data type. """ assert isinstance(data, np.ndarray), "input data must be numpy array." sr = min(data.ndim, 3) if affine is None: affine = np.eye(4, dtype=np.float64) affine = to_affine_nd(sr, affine) if target_affine is None: target_affine = affine target_affine = to_affine_nd(sr, target_affine) if np.allclose(affine, target_affine): # no affine changes, save (data, affine) results_img = nib.Nifti1Image(data.astype(dtype), to_affine_nd(3, target_affine)) nib.save(results_img, file_name) return # resolve orientation start_ornt = nib.orientations.io_orientation(affine) target_ornt = nib.orientations.io_orientation(target_affine) ornt_transform = nib.orientations.ornt_transform(start_ornt, target_ornt) data_shape = data.shape data = nib.orientations.apply_orientation(data, ornt_transform) _affine = affine @ nib.orientations.inv_ornt_aff(ornt_transform, data_shape) if np.allclose(_affine, target_affine) or not resample: results_img = nib.Nifti1Image(data.astype(dtype), to_affine_nd(3, target_affine)) nib.save(results_img, file_name) return # need resampling transform = np.linalg.inv(_affine) @ target_affine if output_shape is None: output_shape, _ = compute_shape_offset(data.shape, _affine, target_affine) dtype = dtype or data.dtype if data.ndim > 3: # multi channel, resampling each channel spatial_shape, channel_shape = data.shape[:3], data.shape[3:] data_ = data.astype(dtype).reshape(list(spatial_shape) + [-1]) data_chns = [] for chn in range(data_.shape[-1]): data_chns.append( scipy.ndimage.affine_transform( data_[..., chn], matrix=transform, output_shape=output_shape[:3], order=interp_order, mode=mode, cval=cval, )) data_chns = np.stack(data_chns, axis=-1) data_ = data_chns.reshape( list(data_chns.shape[:3]) + list(channel_shape)) else: data_ = data.astype(dtype) data_ = scipy.ndimage.affine_transform( data_, matrix=transform, output_shape=output_shape[:data_.ndim], order=interp_order, mode=mode, cval=cval) results_img = nib.Nifti1Image(data_, to_affine_nd(3, target_affine)) nib.save(results_img, file_name) return