Source code for unravel.voxel_stats.z_score_cwd
#!/usr/bin/env python3
"""
Use ``vstats_z_score_cwd`` (``zsc``) from UNRAVEL to z-score .nii.gz images in the current directory.
Prereqs:
- ``vstats_prep``
Outputs:
- <path/input_img>_z.nii.gz (float32) saved in the same directory as the input image.
Note:
- z-score = (img.nii.gz - mean pixel intensity in brain)/standard deviation of intensity in brain
- Voxels outside the mask are set to zero.
Next commands for voxel-wise stats:
- Aggregate atlas space IF images with ``utils_agg_files``.
- If analyzing whole brains, consider using ``vstats_whole_to_avg`` to average left and right hemispheres.
- If using side-specific z-scoring, use ``vstats_hemi_to_avg`` to average the images.
- Prepend condition names with ``utils_prepend``.
- Check images in FSLeyes (e.g., with ``vstats_check_fsleyes``) and run ``vstats`` to perform voxel-wise stats.
Usage:
------
vstats_z_score_cwd -i '<asterisk>.nii.gz' [-mas path/mask1.nii.gz path/mask2.nii.gz] [-s z] [-v]
"""
import nibabel as nib
import numpy as np
from pathlib import Path
from rich import print
from rich.traceback import install
from unravel.core.img_io import load_nii
from unravel.voxel_stats.apply_mask import load_mask
from unravel.core.help_formatter import RichArgumentParser, SuppressMetavar, SM
from unravel.core.config import Configuration
from unravel.core.utils import log_command, match_files, verbose_start_msg, verbose_end_msg, print_func_name_args_times
[docs]
def parse_args():
parser = RichArgumentParser(formatter_class=SuppressMetavar, add_help=False, docstring=__doc__)
reqs = parser.add_argument_group('Required arguments')
reqs.add_argument('-mas', '--masks', help='Path(s) or pattern(s) to mask .nii.gz files to restrict analysis. Default: None', nargs='*', required=True, action=SM)
opts = parser.add_argument_group('Optional arguments')
opts.add_argument('-i', '--input', help='Path to the image or images to z-score. Default: "*.nii.gz"', default='*.nii.gz', action=SM)
opts.add_argument('-s', '--suffix', help='Output suffix. Default: z (.nii.gz replaced w/ _z.nii.gz)', default='z', action=SM)
general = parser.add_argument_group('General arguments')
general.add_argument('-v', '--verbose', help='Increase verbosity. Default: False', action='store_true', default=False)
return parser.parse_args()
# TODO: Set voxels outside the mask(s) to zero
[docs]
@print_func_name_args_times()
def z_score_img(img, mask_img):
"""Z-score an ndarray using a mask.
Parameters:
- img (np.ndarray): the ndarray to be z-scored.
- mask_img (np.ndarray): the brain mask ndarray
Returns:
- img_z (np.ndarray): the z-scored ndarray"""
# Extract only the masked (nonzero) voxels
masked_voxels = img[mask_img != 0]
# Prevent division by zero
if masked_voxels.size == 0:
raise ValueError("Mask is empty or does not cover any nonzero values in the image.")
mean_intensity = masked_voxels.mean()
std_dev = masked_voxels.std()
if std_dev == 0:
raise ValueError("Standard deviation is zero. All masked voxels have the same intensity.")
# Z-score the entire image (including unmasked areas)
z_scored_img = (img - mean_intensity) / std_dev
# Zero out voxels outside the mask
z_scored_img[mask_img == 0] = 0
return z_scored_img
[docs]
@log_command
def main():
install()
args = parse_args()
Configuration.verbose = args.verbose
verbose_start_msg()
# Load the first image to get the shape
nii_paths = match_files(args.input)
input_nii_path = nii_paths[0]
img = load_nii(input_nii_path)
# Load the mask(s) and combine them
mask_paths = match_files(args.masks) if args.masks else []
mask_imgs = [load_mask(p) for p in mask_paths]
mask_img = np.ones(img.shape, dtype=bool) if not mask_imgs else np.logical_and.reduce(mask_imgs)
# Z-score the image using the mask and save the output
for nii_path in nii_paths:
nii = nib.load(nii_path)
img = np.asanyarray(nii.dataobj, dtype=nii.header.get_data_dtype()).squeeze()
z_scored_img = z_score_img(img, mask_img)
# Save the z-scored image
output_path = Path(str(nii_path).replace('.nii.gz', f'_{args.suffix}.nii.gz'))
z_scored_nii = nib.Nifti1Image(z_scored_img, nii.affine, nii.header)
z_scored_nii.header.set_data_dtype(np.float32)
nib.save(z_scored_nii, output_path)
verbose_end_msg()
if __name__ == '__main__':
main()