import nibabel as nib
import nibabel.processing as nbp
import numpy as np
[docs]
def crop_to_nonzero(img):
"""Crops the image to the bounding box of non-zero voxels."""
data = img.get_fdata()
nonzero_coords = np.array(np.nonzero(data))
start_coords = nonzero_coords.min(axis=1)
end_coords = nonzero_coords.max(axis=1) + 1
cropped_data = data[
start_coords[0] : end_coords[0],
start_coords[1] : end_coords[1],
start_coords[2] : end_coords[2],
]
new_affine = img.affine.copy()
new_affine[:3, 3] = nib.affines.apply_affine(img.affine, start_coords)
return nib.Nifti1Image(cropped_data, new_affine)
[docs]
def prepare_t1_for_nn(t1_img, orientation="RAS", out_shape_dynamic=False):
t1_img_cropped = crop_to_nonzero(t1_img)
if out_shape_dynamic:
divisor = 64
min_shape = 128
max_shape = 320
s = np.array(t1_img_cropped.shape[:3])
out_shape = tuple(
np.clip(np.ceil(s / divisor).astype(int) * divisor, min_shape, max_shape)
)
else:
out_shape = (256, 256, 256)
t1_img_conformed = nbp.conform(
t1_img_cropped,
out_shape=out_shape,
voxel_size=(1.0, 1.0, 1.0),
orientation=orientation,
order=1,
)
t1_data = t1_img_conformed.get_fdata()
p02 = np.nanpercentile(t1_data, 2)
p98 = np.nanpercentile(t1_data, 98)
t1_data = np.clip(t1_data, p02, p98)
t1_data = (t1_data - p02) / (p98 - p02)
return t1_data, t1_img_conformed.affine
[docs]
def resample_output(output, conformed_affine, t1_img):
return nbp.resample_from_to(
nib.Nifti1Image(output.astype(np.uint8), conformed_affine),
t1_img,
order=0,
)
[docs]
def merge_PVEs(PVE_a, PVE_b):
"""
Merge two PVE (Partial Volume Estimation) images, PVE_a and PVE_b,
into a single PVE image. The merging is done by taking the maximum
WM estimate from both images, then the maximum GM estimate, and
finally normalizing.
"""
csf_a, gm_a, wm_a = PVE_a[..., 0], PVE_a[..., 1], PVE_a[..., 2]
csf_b, gm_b, wm_b = PVE_b[..., 0], PVE_b[..., 1], PVE_b[..., 2]
new_wm = np.maximum(wm_a, wm_b)
# 2. Choose the source (a or b) with the larger GM estimate, per voxel
use_a = gm_a >= gm_b
chosen_csf = np.where(use_a, csf_a, csf_b)
chosen_gm = np.where(use_a, gm_a, gm_b)
# 3. Normalize chosen csf+gm to fill the remaining mass (1 - new_wm)
remaining = 1.0 - new_wm
chosen_sum = chosen_csf + chosen_gm
# avoid divide-by-zero where chosen_sum is 0 (e.g. pure-WM voxel)
scale = np.divide(
remaining, chosen_sum, out=np.zeros_like(chosen_sum), where=chosen_sum > 0
)
new_csf = chosen_csf * scale
new_gm = chosen_gm * scale
PVE = np.stack([new_csf, new_gm, new_wm], axis=-1)
return PVE