def subtract_img(source_img: SpatialImage, target_img: SpatialImage, edge_dist: int = 0) -> SpatialImage: """Subtracts 3D array y from x. Optionally, y is expanded by the edge_dist value (in milimeters), using the x and y affine values to calculate Euclidean distance.""" source_data = source_img.get_data() # min target_data = target_img.get_data() # sub if source_data.shape != target_data.shape: raise ShapeError(source_data.shape, target_data.shape) difference = np.zeros(source_data.shape) source_voxels = np.asarray(np.where(source_data == 1)).transpose() target_voxels = np.asarray(np.where(target_data == 1)).transpose() # Calculate Euclidean distance from outermost voxels in y source_ref = nib.affines.apply_affine(source_img.affine, source_voxels) target_ref = nib.affines.apply_affine(target_img.affine, target_voxels) dist = distance.cdist(target_ref, source_ref, 'euclidean') # for each target voxel get the minimum distance value dist = np.amin(dist, axis=0) x, y, z = source_voxels[np.where(dist > edge_dist), :].squeeze()\ .transpose() difference[x, y, z] = 1 return nib.Nifti1Image(np.float32(difference), source_img.affine, source_img.header)
def mask_image(image: SpatialImage, mask: np.ndarray, data_type: type = None) -> np.ndarray: """Mask image after optionally casting its type. Parameters ---------- image Image to mask. Can include time as the last dimension. mask Mask to apply. Must have the same shape as the image data. data_type Type to cast image to. Returns ------- np.ndarray Masked image. Raises ------ ValueError Image data and masks have different shapes. """ image_data = image.get_data() if image_data.shape[:3] != mask.shape: raise ValueError("Image data and mask have different shapes.") if data_type is not None: cast_data = image_data.astype(data_type) else: cast_data = image_data return cast_data[mask]
def mask_image(image: SpatialImage, mask: np.ndarray, data_type: type = None ) -> np.ndarray: """Mask image after optionally casting its type. Parameters ---------- image Image to mask. Can include time as the last dimension. mask Mask to apply. Must have the same shape as the image data. data_type Type to cast image to. Returns ------- np.ndarray Masked image. Raises ------ ValueError Image data and masks have different shapes. """ image_data = image.get_data() if image_data.shape[:3] != mask.shape: raise ValueError("Image data and mask have different shapes.") if data_type is not None: cast_data = image_data.astype(data_type) else: cast_data = image_data return cast_data[mask]
def median_filter_img(img: SpatialImage, dist: int = 1) -> SpatialImage: """Median filtering of non-zero elements in an image. Median filter all selected voxels of a binary 3D input image and a 2-iteration dilation border around it. Voxel distance dist is used to determine the size of the area for computing the median, where a distance of 1 results in a 3*3*3 shape. The number of repetitions nrep determines how often the median filter will be repeated. """ if not img_is_3d(img): raise ShapeError(3, len(img.shape)) data = img.get_data() dilated = binary_dilation(data, iterations=2).astype(int) voxels = np.asarray(np.where(dilated == 1)).transpose() filtered = np.zeros(img.shape) for x, y, z in voxels: area = data[x-dist:x+(dist+1), y-dist:y+(dist+1), z-dist:z+(dist+1)] if np.median(area) > float_info.min: filtered[x, y, z] = 1 return nib.Nifti1Image(np.float32(filtered), img.affine, img.header)
def get_mask_indices(img: SpatialImage, order: str = 'C') -> np.ndarray: """Get voxel space coordinates (indices) of seed voxels Parameters ---------- img : SpatialImage Mask NIfTI image order : str, optional Order that the seed-mask voxels will be extracted in. The resulting indices will be listed in this way Returns ------- np.ndarray 2D array of shape (n_voxels, 3) containing the 3D coordinates of all mask image voxels """ if order not in ('C', 'F', 'c', 'f'): raise ValueError('Order has unexpected value: expected %s, got \'%s\'' % ("'C' or 'F'", order)) data = img.get_data() indices = np.asarray(tuple(zip(*np.where(data == 1)))) if order.upper() == 'F': # indices are C order and must become F order reorder = get_c2f_order(img) indices = indices[reorder] return indices
def subsample_img(img: SpatialImage, f: int = 2) -> SpatialImage: """Reduce image features of a 3D array by a given factor f.""" if not img_is_3d(img): raise ShapeError(3, len(img.shape)) data = img.get_data().astype(int) mask = np.zeros(img.shape).astype(int) mask[::f, ::f, ::f] = 1 data *= mask return nib.Nifti1Image(data, img.affine, img.header)
def binarize_3d(img: SpatialImage, threshold: float = 0.0) -> SpatialImage: """binarize 3D spatial image. NaNs and infs in the image are set to 0.""" if not img_is_3d(img): raise ShapeError(3, len(img.shape)) data = img.get_data() data[np.where(np.isnan(data))] = 0 data[np.where(np.isinf(data))] = 0 data = np.where(data > threshold, 1, 0) return nib.Nifti1Image(data, img.affine, img.header)
def get_masked_series(time_series: SpatialImage, mask: SpatialImage): """Apply a 3D mask to a 4D image Parameters ---------- time_series : SpatialImage 4D nifti image, with time-series on the 4th dimension mask : SpatialImage 3D boolean mask image """ if not imgs_equal_3d([time_series, mask]): raise ValueError('Time-series and mask do not have equal shape ' 'and/or affine') if not img_is_4d(time_series): raise ShapeError(4, len(time_series.shape)) time_series_data = time_series.get_data() mask_data = mask.get_data().astype(bool) return time_series_data[mask_data].T
def extract_regions(atlas: SpatialImage, region_ids: Union[list, int]) -> SpatialImage: """Extract regions from an atlas""" if isinstance(region_ids, int): region_ids = [region_ids] data = atlas.get_data() if not np.all(np.isin(region_ids, data)): raise ValueError('could not find some (or all) of the given ' 'region-ids in the atlas') data[~np.isin(data, region_ids)] = 0 data[np.where(data > 0)] = 1 if len(np.unique(data)) != 2: raise ValueError('mask is empty after extracting region from atlas') return nib.Nifti1Image(data.astype(int), atlas.affine, atlas.header)
def get_c2f_order(img: SpatialImage) -> np.ndarray: """The order in which voxels are extracted from a mask is either F-contiguous or C-contiguous (C by default in NumPy), which is reflected by the order in which they are placed in an array. This function provides reordering indices such that a C extraction order is turned into an F extraction order. Parameters ---------- img : SpatialImage Mask NIfTI image """ mask = img.get_data() reorder = np.arange(int(np.prod(mask.shape))) reorder = reorder.reshape(mask.shape, order='F') reorder = reorder[mask.astype(bool)] reorder = np.argsort(reorder) return reorder
def _preproc_binarize(field: str, img: SpatialImage, bin_threshold: float = 0.0) -> SpatialImage: data = img.get_data() if not np.array_equal(data, data.astype(bool)): n_nans = np.count_nonzero(np.isnan(data)) n_infs = np.count_nonzero(np.isinf(data)) if n_nans > 0: logging.warning( '%s NaN values in %s. Please manually verify %s.nii' % (n_nans, field, field)) if n_infs > 0: logging.warning( '%s inf values in %s. Please manually verify %s.nii' % (n_infs, field, field)) img = binarize_3d(img, threshold=bin_threshold) return img
def stretch_img(source_img: SpatialImage, target: Union[tuple, SpatialImage]) -> SpatialImage: """Stretch a binary image to meet the dimensions of a template image. The stretching process will not generate new indices of ones, instead keeping the original amount of mask values but spacing them out over the template dimensions. This function assumes that the template dimensions are larger than the input image dimensions and the input image is binary. """ try: target_shape, target_affine = target.shape, target.affine except AttributeError: target_shape, target_affine = target s_affine = np.abs(np.diag(source_img.affine[:3])) t_affine = np.abs(np.diag(target_affine[:3])) if np.all(s_affine <= t_affine): raise ValueError('This function is meant for upsampling, not ' 'downsampling') x, y, z = np.nonzero(source_img.get_data()) xyz = np.asarray([x, y, z, np.ones(len(z))]).T # 'Stretch' coordinates so they space out in a larger template xyz = np.diag(np.linalg.solve(target_affine, source_img.affine)) * xyz xyz = np.unique(np.round(xyz), axis=0) xyz = xyz.astype(int) x, y, z, _ = np.hsplit(xyz, 4) m = np.zeros(target_shape) m[x, y, z] = 1 return nib.Nifti1Image(np.float32(m), target_affine, source_img.header)
def images(spatial_image: SpatialImage) -> Iterable[SpatialImage]: images = [spatial_image] image_data = spatial_image.get_data().copy() image_data[1, 1, 1, 0] = 2 images.append(Nifti1Pair(image_data, np.eye(4))) return images
def images(spatial_image: SpatialImage) -> Iterable[SpatialImage]: images = [spatial_image] image_data = spatial_image.get_data().copy() image_data[1, 1, 1] = 2 images.append(Nifti1Pair(image_data, np.eye(4))) return images