diff --git a/README.md b/README.md index bda0ddb..7b659fc 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,27 @@ [![codecov](https://codecov.io/gh/LOAMRI/asltk/graph/badge.svg?token=1W8GQ7SLU9)](https://codecov.io/gh/LOAMRI/asltk) ![CI_main](https://github.com/LOAMRI/asltk/actions/workflows/ci_main.yaml/badge.svg) [![CI_develop](https://github.com/LOAMRI/asltk/actions/workflows/ci_develop.yaml/badge.svg)](https://github.com/LOAMRI/asltk/actions/workflows/ci_develop.yaml) +[![PyPI version](https://badge.fury.io/py/asltk.svg)](https://badge.fury.io/py/asltk) # Arterial Spin Labeling Toolkit (asltk) -Welcome to the ASLtk project! +Welcome to the ASL toolkit! + +This library was designed to assist users in processing Arterial Spin Labeling (ASL) MRI images, from basic imaging protocols to the state-of-the-art models provided in the scientific literature. + +The major objective of this project is to give an open-source alternative to researchers in the MRI field. A profound knowledge of computing and data modelling is not a prior demand. It is expected that a simple set of Python commands can be helpful in fast prototyping an ASL experiment or even collecting simple quantitative ASL-based information. + +Please read the [full documentation](https://asltk.readthedocs.io/en/main/) to get more details about the usage, implementation and updates in the `asltk` library. + +Also, feel free to contribute directly to the project! Check it out the [issues](https://github.com/LOAMRI/asltk/issues) at the repository and get in touch to the developers of the project. -Access the [full documentation](https://asltk.readthedocs.io/en/main/) to check the usage and development of this tool. ## How to install -Requires Python 3.10 or higher: +A quick to use install is via `pip`, as follows: + +> [!NOTE] +> The installation requires Python 3.10 or higher ```bash pip install asltk diff --git a/asltk/asldata.py b/asltk/asldata.py index 91e4ff4..0788e2d 100644 --- a/asltk/asldata.py +++ b/asltk/asldata.py @@ -162,11 +162,11 @@ def set_te(self, te_values: list): self._parameters['te'] = te_values def get_dw(self): - """Obtain the PLD array values""" + """Obtain the Diffusion b values array""" return self._parameters['dw'] def set_dw(self, dw_values: list): - """Set the DW values. + """Set the Diffusion b values. The proper way to inform the values here is using a list of int or float data. The total quantity of values depends on the image diff --git a/asltk/reconstruction.py b/asltk/reconstruction.py index 295e954..676b825 100644 --- a/asltk/reconstruction.py +++ b/asltk/reconstruction.py @@ -8,10 +8,13 @@ from asltk.asldata import ASLData from asltk.mri_parameters import MRIParameters -from asltk.utils import asl_model_buxton, asl_model_multi_te +from asltk.utils import ( + asl_model_buxton, + asl_model_multi_dw, + asl_model_multi_te, +) # TODO Opcao para aplicar filtro no mapa de saida (Gauss, etc) -# TODO Brain mask como input ou opção de processamento? class CBFMapping(MRIParameters): @@ -27,7 +30,7 @@ def __init__(self, asl_data: ASLData) -> None: Examples: The default MRIParameters are used as default in the object constructor - >>> asl_data = ASLData(pcasl='./tests/files/pcasl.nii.gz',m0='./tests/files/m0.nii.gz') + >>> asl_data = ASLData(pcasl='./tests/files/pcasl_mte.nii.gz',m0='./tests/files/m0.nii.gz') >>> cbf = CBFMapping(asl_data) >>> cbf.get_constant('T1csf') 1400.0 @@ -71,8 +74,7 @@ def set_brain_mask(self, brain_mask: np.ndarray, label: int = 1): Args: brain_mask (np.ndarray): The image representing the brain mask label (int, optional): The label value used to define the foreground tissue (brain). Defaults to 1. """ - # TODO Add flag to create new mask using fsl.bet - self._check_mask_values(brain_mask, label) + _check_mask_values(brain_mask, label, self._asl_data('m0').shape) binary_mask = (brain_mask == label).astype(np.uint8) * label self._brain_mask = binary_mask @@ -167,36 +169,6 @@ def mod_buxton(Xdata, par1, par2): 'att': self._att_map, } - def _check_mask_values(self, mask, label): - # Check wheter mask input is an numpy array - if not isinstance(mask, np.ndarray): - raise TypeError(f'mask is not an numpy array. Type {type(mask)}') - - # Check whether the mask provided is a binary image - unique_values = np.unique(mask) - if unique_values.size > 2: - warnings.warn( - 'Mask image is not a binary image. Any value > 0 will be assumed as brain label.', - UserWarning, - ) - - # Check whether the label value is found in the mask image - label_ok = False - for value in unique_values: - if label == value: - label_ok = True - break - if not label_ok: - raise ValueError('Label value is not found in the mask provided.') - - # Check whether the dimensions between mask and input volume matches - mask_shape = mask.shape - input_vol_shape = self._asl_data('pcasl')[0, 0, :, :, :].shape - if mask_shape != input_vol_shape: - raise TypeError( - f'Image mask dimension does not match with input 3D volume. Mask shape {mask_shape} not equal to {input_vol_shape}' - ) - class MultiTE_ASLMapping(MRIParameters): def __init__(self, asl_data: ASLData) -> None: @@ -212,7 +184,7 @@ def __init__(self, asl_data: ASLData) -> None: Examples: The default MRIParameters are used as default in the object constructor - >>> asl_data = ASLData(pcasl='./tests/files/pcasl.nii.gz',m0='./tests/files/m0.nii.gz', te_values=[13.2, 25.7, 50.4]) + >>> asl_data = ASLData(pcasl='./tests/files/pcasl_mte.nii.gz',m0='./tests/files/m0.nii.gz', te_values=[13.2, 25.7, 50.4]) >>> mte = MultiTE_ASLMapping(asl_data) >>> mte.get_constant('T1csf') 1400.0 @@ -261,8 +233,7 @@ def set_brain_mask(self, brain_mask: np.ndarray, label: int = 1): Args: brain_mask (np.ndarray): The image representing the brain mask label (int, optional): The label value used to define the foreground tissue (brain). Defaults to 1. """ - # TODO Add flag to create new mask using fsl.bet - self._check_mask_values(brain_mask, label) + _check_mask_values(brain_mask, label, self._asl_data('m0').shape) binary_mask = (brain_mask == label).astype(np.uint8) * label self._brain_mask = binary_mask @@ -313,36 +284,6 @@ def get_att_map(self): """ return self._att_map - def _check_mask_values(self, mask, label): - # Check wheter mask input is an numpy array - if not isinstance(mask, np.ndarray): - raise TypeError(f'mask is not an numpy array. Type {type(mask)}') - - # Check whether the mask provided is a binary image - unique_values = np.unique(mask) - if unique_values.size > 2: - warnings.warn( - 'Mask image is not a binary image. Any value > 0 will be assumed as brain label.', - UserWarning, - ) - - # Check whether the label value is found in the mask image - label_ok = False - for value in unique_values: - if label == value: - label_ok = True - break - if not label_ok: - raise ValueError('Label value is not found in the mask provided.') - - # Check whether the dimensions between mask and input volume matches - mask_shape = mask.shape - input_vol_shape = self._asl_data('pcasl')[0, 0, :, :, :].shape - if mask_shape != input_vol_shape: - raise TypeError( - f'Image mask dimension does not match with input 3D volume. Mask shape {mask_shape} not equal to {input_vol_shape}' - ) - def create_map( self, ub: list = [np.inf], @@ -422,7 +363,6 @@ def mod_2comp(Xdata, par1): self.T2gm, ) - # Ydata = np.swapaxes(asl_img[:,:,k,j,i],0,1).reshape((len(pld) * len(te), 1), order='F').flatten('F') Ydata = ( self._asl_data('pcasl')[:, :, k, j, i] .reshape( @@ -449,7 +389,7 @@ def mod_2comp(Xdata, par1): bounds=(lb, ub), ) self._t1blgm_map[k, j, i] = par_fit[0] - except RuntimeError: + except RuntimeError: # pragma: no cover self._t1blgm_map[k, j, i] = 0.0 # Adjusting output image boundaries @@ -485,3 +425,276 @@ def _adjust_image_limits(self, map, init_guess): img = thr_filter.Execute(img) return sitk.GetArrayFromImage(img) + + +class MultiDW_ASLMapping(MRIParameters): + def __init__(self, asl_data: ASLData): + super().__init__() + self._asl_data = asl_data + self._basic_maps = CBFMapping(asl_data) + if self._asl_data.get_dw() is None: + raise ValueError( + 'ASLData is incomplete. MultiDW_ASLMapping need a list of DW values.' + ) + + self._brain_mask = np.ones(self._asl_data('m0').shape) + self._cbf_map = np.zeros(self._asl_data('m0').shape) + self._att_map = np.zeros(self._asl_data('m0').shape) + + self._b_values = self._asl_data.get_dw() + # self._A1 = np.zeros(tuple([len(self._b_values)]) + self._asl_data('m0').shape) + self._A1 = np.zeros(self._asl_data('m0').shape) + # self._D1 = np.zeros(tuple([1]) +self._asl_data('m0').shape) + self._D1 = np.zeros(self._asl_data('m0').shape) + self._A2 = np.zeros(self._asl_data('m0').shape) + # self._A2 = np.zeros(tuple([len(self._b_values)]) + self._asl_data('m0').shape) + # self._D2 = np.zeros(tuple([1]) +self._asl_data('m0').shape) + self._D2 = np.zeros(self._asl_data('m0').shape) + self._kw = np.zeros(self._asl_data('m0').shape) + + def set_brain_mask(self, brain_mask: np.ndarray, label: int = 1): + """Defines whether a brain a mask is applied to the MultiDW_ASLMapping + calculation + + A image mask is simply an image that defines the voxels where the ASL + calculation should be made. Basically any integer value can be used as + proper label mask. + + A most common approach is to use a binary image (zeros for background + and 1 for the brain tissues). Anyway, the default behavior of the + method can transform a integer-pixel values image to a binary mask with + the `label` parameter provided by the user + + Args: + brain_mask (np.ndarray): The image representing the brain mask label (int, optional): The label value used to define the foreground tissue (brain). Defaults to 1. + """ + _check_mask_values(brain_mask, label, self._asl_data('m0').shape) + + binary_mask = (brain_mask == label).astype(np.uint8) * label + self._brain_mask = binary_mask + + def get_brain_mask(self): + """Get the brain mask image + + Returns: + (np.ndarray): The brain mask image + """ + return self._brain_mask + + def set_cbf_map(self, cbf_map: np.ndarray): + """Set the CBF map to the MultiDW_ASLMapping object. + + Note: + The CBF maps must have the original scale in order to calculate the + T1blGM map correclty. Hence, if the CBF map was made using + CBFMapping class, one can use the 'cbf' output. + + Args: + cbf_map (np.ndarray): The CBF map that is set in the MultiDW_ASLMapping object + """ + self._cbf_map = cbf_map + + def get_cbf_map(self) -> np.ndarray: + """Get the CBF map storaged at the MultiDW_ASLMapping object + + Returns: + (np.ndarray): The CBF map that is storaged in the + MultiDW_ASLMapping object + """ + return self._cbf_map + + def set_att_map(self, att_map: np.ndarray): + """Set the ATT map to the MultiDW_ASLMapping object. + + Args: + att_map (np.ndarray): The ATT map that is set in the MultiDW_ASLMapping object + """ + self._att_map = att_map + + def get_att_map(self): + """Get the ATT map storaged at the MultiDW_ASLMapping object + + Returns: + (np.ndarray): _description_ + """ + return self._att_map + + def create_map( + self, + lb: list = [0.0, 0.0, 0.0, 0.0], + ub: list = [np.inf, np.inf, np.inf, np.inf], + par0: list = [0.5, 0.000005, 0.5, 0.000005], + ): + self._basic_maps.set_brain_mask(self._brain_mask) + + basic_maps = {'cbf': self._cbf_map, 'att': self._att_map} + if np.mean(self._cbf_map) == 0 or np.mean(self._att_map) == 0: + # If the CBF/ATT maps are zero (empty), then a new one is created + print( + '[blue][INFO] The CBF/ATT map were not provided. Creating these maps before next step...' + ) # pragma: no cover + basic_maps = self._basic_maps.create_map() # pragma: no cover + self._cbf_map = basic_maps['cbf'] # pragma: no cover + self._att_map = basic_maps['att'] # pragma: no cover + + x_axis = self._asl_data('m0').shape[2] # height + y_axis = self._asl_data('m0').shape[1] # width + z_axis = self._asl_data('m0').shape[0] # depth + + for i in track( + range(x_axis), description='[green]multiDW-ASL processing...' + ): + for j in range(y_axis): + for k in range(z_axis): + if self._brain_mask[k, j, i] != 0: + # Calculates the diffusion components for (A1, D1), (A2, D2) + def mod_diff(Xdata, par1, par2, par3, par4): + return asl_model_multi_dw( + b_values=Xdata, + A1=par1, + D1=par2, + A2=par3, + D2=par4, + ) + + # M(t,b)/M(t,0) + Ydata = ( + self._asl_data('pcasl')[:, :, k, j, i] + .reshape( + ( + len(self._asl_data.get_ld()) + * len(self._asl_data.get_dw()), + 1, + ) + ) + .flatten() + / self._asl_data('m0')[k, j, i] + ) + + try: + # Xdata = self._b_values + Xdata = self._create_x_data( + self._asl_data.get_ld(), + self._asl_data.get_pld(), + self._asl_data.get_dw(), + ) + + par_fit, _ = curve_fit( + mod_diff, + Xdata[:, 2], + Ydata, + p0=par0, + bounds=(lb, ub), + ) + self._A1[k, j, i] = par_fit[0] + self._D1[k, j, i] = par_fit[1] + self._A2[k, j, i] = par_fit[2] + self._D2[k, j, i] = par_fit[3] + except RuntimeError: + self._A1[k, j, i] = 0 + self._D1[k, j, i] = 0 + self._A2[k, j, i] = 0 + self._D2[k, j, i] = 0 + + # Calculates the Mc fitting to alpha = kw + T1blood + m0_px = self._asl_data('m0')[k, j, i] + + # def mod_2comp(Xdata, par1): + # ... + # # return asl_model_multi_te( + # # Xdata[:, 0], + # # Xdata[:, 1], + # # Xdata[:, 2], + # # m0_px, + # # basic_maps['cbf'][k, j, i], + # # basic_maps['att'][k, j, i], + # # par1, + # # self.T2bl, + # # self.T2gm, + # # ) + + # Ydata = ( + # self._asl_data('pcasl')[:, :, k, j, i] + # .reshape( + # ( + # len(self._asl_data.get_ld()) + # * len(self._asl_data.get_te()), + # 1, + # ) + # ) + # .flatten() + # ) + + # try: + # Xdata = self._create_x_data( + # self._asl_data.get_ld(), + # self._asl_data.get_pld(), + # self._asl_data.get_dw(), + # ) + # par_fit, _ = curve_fit( + # mod_2comp, + # Xdata, + # Ydata, + # p0=par0, + # bounds=(lb, ub), + # ) + # self._kw[k, j, i] = par_fit[0] + # except RuntimeError: + # self._kw[k, j, i] = 0.0 + + # # Adjusting output image boundaries + # self._kw = self._adjust_image_limits(self._kw, par0[0]) + + return { + 'cbf': self._cbf_map, + 'cbf_norm': self._cbf_map * (60 * 60 * 1000), + 'att': self._att_map, + 'a1': self._A1, + 'd1': self._D1, + 'a2': self._A2, + 'd2': self._D2, + 'kw': self._kw, + } + + def _create_x_data(self, ld, pld, dw): + # array for the x values, assuming an arbitrary size based on the PLD + # and TE vector size + Xdata = np.zeros((len(pld) * len(dw), 3)) + + count = 0 + for i in range(len(pld)): + for j in range(len(dw)): + Xdata[count] = [ld[i], pld[i], dw[j]] + count += 1 + + return Xdata + + +def _check_mask_values(mask, label, ref_shape): + # Check wheter mask input is an numpy array + if not isinstance(mask, np.ndarray): + raise TypeError(f'mask is not an numpy array. Type {type(mask)}') + + # Check whether the mask provided is a binary image + unique_values = np.unique(mask) + if unique_values.size > 2: + warnings.warn( + 'Mask image is not a binary image. Any value > 0 will be assumed as brain label.', + UserWarning, + ) + + # Check whether the label value is found in the mask image + label_ok = False + for value in unique_values: + if label == value: + label_ok = True + break + if not label_ok: + raise ValueError('Label value is not found in the mask provided.') + + # Check whether the dimensions between mask and input volume matches + mask_shape = mask.shape + if mask_shape != ref_shape: + raise TypeError( + f'Image mask dimension does not match with input 3D volume. Mask shape {mask_shape} not equal to {ref_shape}' + ) diff --git a/asltk/scripts/cbf.py b/asltk/scripts/cbf.py index 2a64e1d..cda0c32 100644 --- a/asltk/scripts/cbf.py +++ b/asltk/scripts/cbf.py @@ -13,44 +13,51 @@ from asltk.utils import load_image, save_image parser = argparse.ArgumentParser( - description='Python script to calculate the basic CBF and ATT maps from ASL data.' + prog='CBF/ATT Mapping', + description='Python script to calculate the basic CBF and ATT maps from ASL data.', ) +parser._action_groups.pop() +required = parser.add_argument_group(title='Required parameters') +optional = parser.add_argument_group(title='Optional parameters') -parser.add_argument( + +required.add_argument( 'pcasl', type=str, help='ASL raw data obtained from the MRI scanner. This must be the basic PLD ASL MRI acquisition protocol.', ) -parser.add_argument('m0', type=str, help='M0 image in Nifti format.') -parser.add_argument( +required.add_argument( + 'm0', type=str, help='M0 image reference used to calculate the ASL signal.' +) +optional.add_argument( 'mask', type=str, nargs='?', default='', - help='Image mask defining the ROI where the calculations must be done. Any pixel value different from zero will be assumed as the ROI area. Outside the mask (value=0) will be ignored.', + help='Image mask defining the ROI where the calculations must be done. Any pixel value different from zero will be assumed as the ROI area. Outside the mask (value=0) will be ignored. If not provided, the entire image space will be calculated.', ) -parser.add_argument( +required.add_argument( 'out_folder', type=str, nargs='?', default=os.path.expanduser('~'), help='The output folder that is the reference to save all the output images in the script. The images selected to be saved are given as tags in the script caller, e.g. the options --cbf_map and --att_map. By default, the TblGM map is placed in the output folder with the name tblgm_map.nii.gz', ) -parser.add_argument( +required.add_argument( '--pld', type=str, nargs='+', required=True, help='Posts Labeling Delay (PLD) trend, arranged in a sequence of float numbers', ) -parser.add_argument( +required.add_argument( '--ld', type=str, nargs='+', required=True, help='Labeling Duration trend (LD), arranged in a sequence of float numbers.', ) -parser.add_argument( +optional.add_argument( '--verbose', action='store_true', help='Show more details thoughout the processing.', diff --git a/asltk/scripts/dw_asl.py b/asltk/scripts/dw_asl.py new file mode 100644 index 0000000..6b770d4 --- /dev/null +++ b/asltk/scripts/dw_asl.py @@ -0,0 +1,196 @@ +import argparse +import os +from functools import * + +import numpy as np +import SimpleITK as sitk +from rich import print + +from asltk.asldata import ASLData +from asltk.reconstruction import MultiDW_ASLMapping +from asltk.utils import load_image, save_image + +parser = argparse.ArgumentParser( + prog='Multi-DW ASL Mapping', + description='Python script to calculate the Multi-DW ASL data.', +) +parser._action_groups.pop() +required = parser.add_argument_group(title='Required parameters') +optional = parser.add_argument_group(title='Optional parameters') + +required.add_argument( + 'pcasl', + type=str, + help='ASL raw data obtained from the MRI scanner. This must be the multi-DW ASL MRI acquisition protocol.', +) +required.add_argument( + 'm0', type=str, help='M0 image reference used to calculate the ASL signal.' +) +optional.add_argument( + 'mask', + type=str, + nargs='?', + default='', + help='Image mask defining the ROI where the calculations must be done. Any pixel value different from zero will be assumed as the ROI area. Outside the mask (value=0) will be ignored. If not provided, the entire image space will be calculated.', +) +required.add_argument( + 'out_folder', + type=str, + nargs='?', + default=os.path.expanduser('~'), + help='The output folder that is the reference to save all the output images in the script. The images selected to be saved are given as tags in the script caller, e.g. the options --cbf_map and --att_map. By default, the TblGM map is placed in the output folder with the name tblgm_map.nii.gz', +) +optional.add_argument( + '--cbf', + type=str, + nargs='?', + required=False, + help='The CBF map that is provided to skip this step in the MultiTE-ASL calculation. If CBF is not provided, than a CBF map is calculated at the runtime. Important: The CBF passed here is with the original voxel scale, i.e. without voxel normalization.', +) +optional.add_argument( + '--att', + type=str, + nargs='?', + required=False, + help='The ATT map that is provided to skip this step in the MultiTE-ASL calculation. If ATT is not provided, than a ATT map is calculated at the runtime.', +) +required.add_argument( + '--pld', + type=str, + nargs='+', + required=True, + help='Posts Labeling Delay (PLD) trend, arranged in a sequence of float numbers', +) +required.add_argument( + '--ld', + type=str, + nargs='+', + required=True, + help='Labeling Duration trend (LD), arranged in a sequence of float numbers.', +) +required.add_argument( + '--dw', + type=str, + nargs='+', + required=True, + help='Diffusion b-values arranged in a sequence of float numbers.', +) +optional.add_argument( + '--verbose', + action='store_true', + help='Show more details thoughout the processing.', +) + +args = parser.parse_args() + +# Script check-up parameters +def checkUpParameters(): + is_ok = True + # Check output folder exist + if not (os.path.isdir(args.out_folder)): + print( + f'Output folder path does not exist (path: {args.out_folder}). Please create the folder before executing the script.' + ) + is_ok = False + + # Check ASL image exist + if not (os.path.isfile(args.pcasl)): + print( + f'ASL input file does not exist (file path: {args.asl}). Please check the input file before executing the script.' + ) + is_ok = False + + # Check M0 image exist + if not (os.path.isfile(args.m0)): + print( + f'M0 input file does not exist (file path: {args.m0}). Please check the input file before executing the script.' + ) + is_ok = False + + return is_ok + + +asl_img = load_image(args.pcasl) +m0_img = load_image(args.m0) + +mask_img = np.ones(asl_img[0, 0, :, :, :].shape) +if args.mask != '': + mask_img = load_image(args.mask) + + +cbf_map = None +if args.cbf is not None: + cbf_map = load_image(args.cbf) + +att_map = None +if args.att is not None: + att_map = load_image(args.att) + + +try: + dw = [float(s) for s in args.dw] + pld = [float(s) for s in args.pld] + ld = [float(s) for s in args.ld] +except: + dw = [float(s) for s in str(args.dw[0]).split()] + pld = [float(s) for s in str(args.pld[0]).split()] + ld = [float(s) for s in str(args.ld[0]).split()] + +if not checkUpParameters(): + raise RuntimeError( + 'One or more arguments are not well defined. Please, revise the script call.' + ) + + +# Step 2: Show the input information to assist manual conference +if args.verbose: + print(' --- Script Input Data ---') + print('ASL file path: ' + args.pcasl) + print('ASL image dimension: ' + str(asl_img.shape)) + print('Mask file path: ' + args.mask) + print('Mask image dimension: ' + str(mask_img.shape)) + print('M0 file path: ' + args.m0) + print('M0 image dimension: ' + str(m0_img.shape)) + print('PLD: ' + str(pld)) + print('LD: ' + str(ld)) + print('DW: ' + str(dw)) + if args.cbf != '': + print('(optional) CBF map: ' + str(args.cbf)) + if args.att != '': + print('(optional) ATT map: ' + str(args.att)) + + +data = ASLData( + pcasl=args.pcasl, m0=args.m0, ld_values=ld, pld_values=pld, dw_values=dw +) +recon = MultiDW_ASLMapping(data) +recon.set_brain_mask(mask_img) +if isinstance(cbf_map, np.ndarray) and isinstance(att_map, np.ndarray): + recon.set_cbf_map(cbf_map) + recon.set_att_map(att_map) + +maps = recon.create_map() + + +save_path = args.out_folder + os.path.sep + 'cbf_map.nii.gz' +if args.verbose and cbf_map is not None: + print('Saving CBF map - Path: ' + save_path) +save_image(maps['cbf'], save_path) + +save_path = args.out_folder + os.path.sep + 'cbf_map_normalized.nii.gz' +if args.verbose and cbf_map is not None: + print('Saving normalized CBF map - Path: ' + save_path) +save_image(maps['cbf_norm'], save_path) + +save_path = args.out_folder + os.path.sep + 'att_map.nii.gz' +if args.verbose and att_map is not None: + print('Saving ATT map - Path: ' + save_path) +save_image(maps['att'], save_path) + +save_path = args.out_folder + os.path.sep + 'mte_kw_map.nii.gz' +if args.verbose: + print('Saving multiDW-ASL kw map - Path: ' + save_path) +save_image(maps['kw'], save_path) + +if args.verbose: + print('Execution: ' + parser.prog + ' finished successfully!') diff --git a/asltk/scripts/te_asl.py b/asltk/scripts/te_asl.py index 3f06c89..e8251ae 100644 --- a/asltk/scripts/te_asl.py +++ b/asltk/scripts/te_asl.py @@ -11,67 +11,71 @@ from asltk.utils import load_image, save_image parser = argparse.ArgumentParser( - description='Python script to calculate the Multi-TE ASL data.' + prog='Multi-TE ASL Mapping', + description='Python script to calculate the Multi-TE ASL map for the T1 relaxation exchange between blood and Gray Matter (GM).', ) +parser._action_groups.pop() +required = parser.add_argument_group(title='Required parameters') +optional = parser.add_argument_group(title='Optional parameters') -parser.add_argument( +required.add_argument( 'pcasl', type=str, help='ASL raw data obtained from the MRI scanner. This must be the multi-TE ASL MRI acquisition protocol.', ) -parser.add_argument('m0', type=str, help='M0 image in Nifti format.') -parser.add_argument( +required.add_argument( + 'm0', type=str, help='M0 image reference used to calculate the ASL signal.' +) +optional.add_argument( 'mask', type=str, nargs='?', default='', - help='Image mask defining the ROI where the calculations must be done. Any pixel value different from zero will be assumed as the ROI area. Outside the mask (value=0) will be ignored.', + help='Image mask defining the ROI where the calculations must be done. Any pixel value different from zero will be assumed as the ROI area. Outside the mask (value=0) will be ignored. If not provided, the entire image space will be calculated.', ) -parser.add_argument( +required.add_argument( 'out_folder', type=str, nargs='?', default=os.path.expanduser('~'), help='The output folder that is the reference to save all the output images in the script. The images selected to be saved are given as tags in the script caller, e.g. the options --cbf_map and --att_map. By default, the TblGM map is placed in the output folder with the name tblgm_map.nii.gz', ) -parser.add_argument( +optional.add_argument( '--cbf', type=str, nargs='?', required=False, help='The CBF map that is provided to skip this step in the MultiTE-ASL calculation. If CBF is not provided, than a CBF map is calculated at the runtime. Important: The CBF passed here is with the original voxel scale, i.e. without voxel normalization.', ) -parser.add_argument( +optional.add_argument( '--att', type=str, nargs='?', required=False, help='The ATT map that is provided to skip this step in the MultiTE-ASL calculation. If ATT is not provided, than a ATT map is calculated at the runtime.', ) -parser.add_argument( +required.add_argument( '--pld', type=str, nargs='+', required=True, help='Posts Labeling Delay (PLD) trend, arranged in a sequence of float numbers', ) -parser.add_argument( +required.add_argument( '--ld', type=str, nargs='+', required=True, help='Labeling Duration trend (LD), arranged in a sequence of float numbers.', ) -parser.add_argument( +required.add_argument( '--te', type=str, nargs='+', required=True, help='Time of Echos (TE), arranged in a sequence of float numbers.', ) -# TODO Colocar CBF e ATT como opcionais, se o usuario passar, economiza o processamento para direto multiTE -# TODO Se usuario fornece CBF e ATT, então não salva o arquivo final (tratar a parte de salvar dados ) -parser.add_argument( +optional.add_argument( '--verbose', action='store_true', help='Show more details thoughout the processing.', @@ -115,11 +119,11 @@ def checkUpParameters(): cbf_map = None -if args.cbf != '': +if args.cbf is not None: cbf_map = load_image(args.cbf) att_map = None -if args.cbf != '': +if args.att is not None: att_map = load_image(args.att) diff --git a/asltk/utils.py b/asltk/utils.py index 8953f27..52ea824 100644 --- a/asltk/utils.py +++ b/asltk/utils.py @@ -134,7 +134,7 @@ def asl_model_buxton( * math.exp(-att / t1b) * math.exp(-(t[i] - tau[i] - att) / t1bp) ) - except OverflowError: + except OverflowError: # pragma: no cover m_values[i] = 0.0 return m_values @@ -361,10 +361,26 @@ def asl_model_multi_te( Scsf = S1b * (1 - math.exp(-te[i] / tblcsf)) * math.exp( -te[i] / t2csf ) + S1csf * math.exp(-te[i] / t2csf) - except (OverflowError, RuntimeError): + except (OverflowError, RuntimeError): # pragma: no cover Sb = 0.0 Scsf = 0.0 mag_total[i] = Sb + Scsf return mag_total + + +def asl_model_multi_dw( + b_values: list, A1: list, D1: float, A2: list, D2: float +): + mag_total = np.zeros(len(b_values)) + + for i in range(0, len(b_values)): + try: + mag_total[i] = A1 * math.exp(-b_values[i] * D1) + A2 * math.exp( + -b_values[i] * D2 + ) + except (OverflowError, RuntimeError): # pragma: no cover + mag_total[i] = 0.0 + + return mag_total diff --git a/codecov.yaml b/codecov.yaml new file mode 100644 index 0000000..a035a2a --- /dev/null +++ b/codecov.yaml @@ -0,0 +1,11 @@ +coverage: + status: + patch: + default: + target: 80% + project: #add everything under here, more options at https://docs.codecov.com/docs/commit-status + default: + # basic + target: 80% #default + threshold: 0% + base: auto \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4145971..e009b1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "asltk" -version = "0.1.1" +version = "0.1.3" description = "A quick to use library to process images for MRI Arterial Spin Labeling imaging protocols." authors = ["Antonio Senra Filho "] readme = "README.md" diff --git a/tests/files/pcasl_mdw.nii.gz b/tests/files/pcasl_mdw.nii.gz new file mode 100644 index 0000000..1402bc8 Binary files /dev/null and b/tests/files/pcasl_mdw.nii.gz differ diff --git a/tests/files/pcasl.nii.gz b/tests/files/pcasl_mte.nii.gz similarity index 100% rename from tests/files/pcasl.nii.gz rename to tests/files/pcasl_mte.nii.gz diff --git a/tests/test_asldata.py b/tests/test_asldata.py index e618909..fd274d4 100644 --- a/tests/test_asldata.py +++ b/tests/test_asldata.py @@ -8,7 +8,7 @@ SEP = os.sep T1_MRI = f'tests' + SEP + 'files' + SEP + 't1-mri.nrrd' -PCASL = f'tests' + SEP + 'files' + SEP + 'pcasl.nii.gz' +PCASL_MTE = f'tests' + SEP + 'files' + SEP + 'pcasl_mte.nii.gz' M0 = f'tests' + SEP + 'files' + SEP + 'm0.nii.gz' M0_BRAIN_MASK = f'tests' + SEP + 'files' + SEP + 'm0_brain_mask.nii.gz' @@ -25,14 +25,14 @@ def test_create_successfuly_asldata_object_with_inputs(): assert len(obj_0.get_pld()) == 0 assert obj_0.get_te() == None assert obj_0.get_dw() == None - obj_1 = asldata.ASLData(pcasl=PCASL) + obj_1 = asldata.ASLData(pcasl=PCASL_MTE) assert isinstance(obj_1, asldata.ASLData) assert len(obj_1.get_ld()) == 0 assert len(obj_1.get_pld()) == 0 assert obj_1.get_te() == None assert obj_1.get_dw() == None obj_3 = asldata.ASLData( - pcasl=PCASL, ld_values=[1, 2, 3], pld_values=[1, 2, 3] + pcasl=PCASL_MTE, ld_values=[1, 2, 3], pld_values=[1, 2, 3] ) assert isinstance(obj_3, asldata.ASLData) assert len(obj_3.get_ld()) == 3 @@ -40,7 +40,7 @@ def test_create_successfuly_asldata_object_with_inputs(): assert obj_3.get_te() == None assert obj_3.get_dw() == None obj_4 = asldata.ASLData( - pcasl=PCASL, + pcasl=PCASL_MTE, ld_values=[1, 2, 3], pld_values=[1, 2, 3], te_values=[1, 2, 3], @@ -51,7 +51,7 @@ def test_create_successfuly_asldata_object_with_inputs(): assert len(obj_4.get_te()) == 3 assert obj_4.get_dw() == None obj_5 = asldata.ASLData( - pcasl=PCASL, + pcasl=PCASL_MTE, ld_values=[1, 2, 3], pld_values=[1, 2, 3], te_values=[1, 2, 3], @@ -65,7 +65,7 @@ def test_create_successfuly_asldata_object_with_inputs(): def test_create_object_with_different_image_formats(): - obj = asldata.ASLData(pcasl=PCASL) + obj = asldata.ASLData(pcasl=PCASL_MTE) assert isinstance(obj, asldata.ASLData) obj = asldata.ASLData(m0=M0) assert isinstance(obj, asldata.ASLData) diff --git a/tests/test_reconstruction.py b/tests/test_reconstruction.py index ae0e6d0..2fe831c 100644 --- a/tests/test_reconstruction.py +++ b/tests/test_reconstruction.py @@ -6,24 +6,36 @@ import pytest from asltk.asldata import ASLData -from asltk.reconstruction import CBFMapping, MultiTE_ASLMapping +from asltk.reconstruction import ( + CBFMapping, + MultiDW_ASLMapping, + MultiTE_ASLMapping, +) from asltk.utils import load_image SEP = os.sep T1_MRI = f'tests' + SEP + 'files' + SEP + 't1-mri.nrrd' -PCASL = f'tests' + SEP + 'files' + SEP + 'pcasl.nii.gz' +PCASL_MTE = f'tests' + SEP + 'files' + SEP + 'pcasl_mte.nii.gz' +PCASL_MDW = f'tests' + SEP + 'files' + SEP + 'pcasl_mdw.nii.gz' M0 = f'tests' + SEP + 'files' + SEP + 'm0.nii.gz' M0_BRAIN_MASK = f'tests' + SEP + 'files' + SEP + 'm0_brain_mask.nii.gz' -asldata = ASLData( - pcasl=PCASL, +asldata_te = ASLData( + pcasl=PCASL_MTE, m0=M0, ld_values=[100.0, 100.0, 150.0, 150.0, 400.0, 800.0, 1800.0], pld_values=[170.0, 270.0, 370.0, 520.0, 670.0, 1070.0, 1870.0], te_values=[13.56, 67.82, 122.08, 176.33, 230.59, 284.84, 339.100, 393.36], ) -incomplete_asldata = ASLData(pcasl=PCASL) +asldata_dw = ASLData( + pcasl=PCASL_MDW, + m0=M0, + ld_values=[100.0, 100.0, 150.0, 150.0, 400.0, 800.0, 1800.0], + pld_values=[170.0, 270.0, 370.0, 520.0, 670.0, 1070.0, 1870.0], + dw_values=[0, 50.0, 100.0, 250.0], +) +incomplete_asldata = ASLData(pcasl=PCASL_MTE) def test_cbf_object_raises_error_if_asldata_does_not_have_pcasl_or_m0_image(): @@ -49,21 +61,21 @@ def test_cbf_object_raises_error_if_asldata_does_not_have_pcasl_or_m0_image(): ], ) def test_cbf_object_set_mri_parameters_values(value, param): - cbf = CBFMapping(asldata) + cbf = CBFMapping(asldata_te) mri_default = cbf.get_constant(param) cbf.set_constant(value, param) assert cbf.get_constant(param) != mri_default def test_cbf_add_brain_mask_success(): - cbf = CBFMapping(asldata) + cbf = CBFMapping(asldata_te) mask = load_image(M0_BRAIN_MASK) cbf.set_brain_mask(mask) assert isinstance(cbf._brain_mask, np.ndarray) def test_cbf_object_create_map_raise_error_if_ld_or_pld_are_not_provided(): - data = ASLData(pcasl=PCASL, m0=M0) + data = ASLData(pcasl=PCASL_MTE, m0=M0) cbf = CBFMapping(data) with pytest.raises(Exception) as e: cbf.create_map() @@ -71,7 +83,7 @@ def test_cbf_object_create_map_raise_error_if_ld_or_pld_are_not_provided(): def test_set_brain_mask_verify_if_input_is_a_label_mask(): - cbf = CBFMapping(asldata) + cbf = CBFMapping(asldata_te) not_mask = load_image(T1_MRI) with pytest.warns(UserWarning): warnings.warn( @@ -81,7 +93,7 @@ def test_set_brain_mask_verify_if_input_is_a_label_mask(): def test_set_brain_mask_set_label_value(): - cbf = CBFMapping(asldata) + cbf = CBFMapping(asldata_te) mask = load_image(M0_BRAIN_MASK) cbf.set_brain_mask(mask, label=1) assert np.unique(cbf._brain_mask).size == 2 @@ -92,7 +104,7 @@ def test_set_brain_mask_set_label_value(): def test_set_brain_mask_set_label_value_raise_error_value_not_found_in_mask( label, ): - cbf = CBFMapping(asldata) + cbf = CBFMapping(asldata_te) mask = load_image(M0_BRAIN_MASK) with pytest.raises(Exception) as e: cbf.set_brain_mask(mask, label=label) @@ -100,7 +112,7 @@ def test_set_brain_mask_set_label_value_raise_error_value_not_found_in_mask( def test_set_brain_mask_gives_binary_image_using_correct_label_value(): - cbf = CBFMapping(asldata) + cbf = CBFMapping(asldata_te) img = np.zeros((5, 35, 35)) img[1, 16:30, 16:30] = 250 img[1, 0:15, 0:15] = 1 @@ -112,8 +124,8 @@ def test_set_brain_mask_gives_binary_image_using_correct_label_value(): # def test_ TODO Teste se mask tem mesma dimensao que 3D asl def test_set_brain_mask_raise_error_if_image_dimension_is_different_from_3d_volume(): - cbf = CBFMapping(asldata) - pcasl_3d_vol = load_image(PCASL)[0, 0, :, :, :] + cbf = CBFMapping(asldata_te) + pcasl_3d_vol = load_image(PCASL_MTE)[0, 0, :, :, :] fake_mask = np.array(((1, 1, 1), (0, 1, 0))) with pytest.raises(Exception) as error: cbf.set_brain_mask(fake_mask) @@ -124,14 +136,14 @@ def test_set_brain_mask_raise_error_if_image_dimension_is_different_from_3d_volu def test_set_brain_mask_creates_3d_volume_of_ones_if_not_set_in_cbf_object(): - cbf = CBFMapping(asldata) - vol_shape = asldata('m0').shape + cbf = CBFMapping(asldata_te) + vol_shape = asldata_te('m0').shape mask_shape = cbf._brain_mask.shape assert vol_shape == mask_shape def test_set_brain_mask_raise_error_mask_is_not_an_numpy_array(): - cbf = CBFMapping(asldata) + cbf = CBFMapping(asldata_te) with pytest.raises(Exception) as e: cbf.set_brain_mask(M0_BRAIN_MASK) assert ( @@ -140,8 +152,17 @@ def test_set_brain_mask_raise_error_mask_is_not_an_numpy_array(): ) +def test_cbf_mapping_get_brain_mask_return_adjusted_brain_mask_image_in_the_object(): + cbf = CBFMapping(asldata_te) + assert np.mean(cbf.get_brain_mask()) == 1 + + mask = load_image(M0_BRAIN_MASK) + cbf.set_brain_mask(mask) + assert np.unique(cbf.get_brain_mask()).tolist() == [0, 1] + + def test_cbf_object_create_map_success(): - cbf = CBFMapping(asldata) + cbf = CBFMapping(asldata_te) out = cbf.create_map() assert isinstance(out['cbf'], np.ndarray) assert np.mean(out['cbf']) < 0.0001 @@ -150,7 +171,7 @@ def test_cbf_object_create_map_success(): def test_cbf_map_normalized_flag_true_result_cbf_map_rescaled(): - cbf = CBFMapping(asldata) + cbf = CBFMapping(asldata_te) out = cbf.create_map() out['cbf_norm'][out['cbf_norm'] == 0] = np.nan mean_px_value = np.nanmean(out['cbf_norm']) @@ -158,7 +179,7 @@ def test_cbf_map_normalized_flag_true_result_cbf_map_rescaled(): def test_multite_asl_object_constructor_created_sucessfully(): - mte = MultiTE_ASLMapping(asldata) + mte = MultiTE_ASLMapping(asldata_te) assert isinstance(mte._asl_data, ASLData) assert isinstance(mte._basic_maps, CBFMapping) assert isinstance(mte._brain_mask, np.ndarray) @@ -168,35 +189,35 @@ def test_multite_asl_object_constructor_created_sucessfully(): def test_multite_asl_set_brain_mask_success(): - mte = MultiTE_ASLMapping(asldata) + mte = MultiTE_ASLMapping(asldata_te) mask = load_image(M0_BRAIN_MASK) mte.set_brain_mask(mask) assert isinstance(mte._brain_mask, np.ndarray) def test_multite_asl_set_cbf_map_success(): - mte = MultiTE_ASLMapping(asldata) + mte = MultiTE_ASLMapping(asldata_te) fake_cbf = np.ones((10, 10)) * 20 mte.set_cbf_map(fake_cbf) assert np.mean(mte._cbf_map) == 20 def test_multite_asl_get_cbf_map_success(): - mte = MultiTE_ASLMapping(asldata) + mte = MultiTE_ASLMapping(asldata_te) fake_cbf = np.ones((10, 10)) * 20 mte.set_cbf_map(fake_cbf) assert np.mean(mte.get_cbf_map()) == 20 def test_multite_asl_set_att_map_success(): - mte = MultiTE_ASLMapping(asldata) + mte = MultiTE_ASLMapping(asldata_te) fake_att = np.ones((10, 10)) * 20 mte.set_att_map(fake_att) assert np.mean(mte._att_map) == 20 def test_multite_asl_get_att_map_success(): - mte = MultiTE_ASLMapping(asldata) + mte = MultiTE_ASLMapping(asldata_te) fake_att = np.ones((10, 10)) * 20 mte.set_att_map(fake_att) assert np.mean(mte.get_att_map()) == 20 @@ -206,7 +227,7 @@ def test_multite_asl_get_att_map_success(): def test_multite_asl_set_brain_mask_set_label_value_raise_error_value_not_found_in_mask( label, ): - mte = MultiTE_ASLMapping(asldata) + mte = MultiTE_ASLMapping(asldata_te) mask = load_image(M0_BRAIN_MASK) with pytest.raises(Exception) as e: mte.set_brain_mask(mask, label=label) @@ -214,7 +235,7 @@ def test_multite_asl_set_brain_mask_set_label_value_raise_error_value_not_found_ def test_multite_asl_set_brain_mask_verify_if_input_is_a_label_mask(): - mte = MultiTE_ASLMapping(asldata) + mte = MultiTE_ASLMapping(asldata_te) not_mask = load_image(M0) with pytest.warns(UserWarning): mte.set_brain_mask(not_mask / np.max(not_mask)) @@ -225,8 +246,8 @@ def test_multite_asl_set_brain_mask_verify_if_input_is_a_label_mask(): def test_multite_asl_set_brain_mask_raise_error_if_image_dimension_is_different_from_3d_volume(): - mte = MultiTE_ASLMapping(asldata) - pcasl_3d_vol = load_image(PCASL)[0, 0, :, :, :] + mte = MultiTE_ASLMapping(asldata_te) + pcasl_3d_vol = load_image(PCASL_MTE)[0, 0, :, :, :] fake_mask = np.array(((1, 1, 1), (0, 1, 0))) with pytest.raises(Exception) as error: mte.set_brain_mask(fake_mask) @@ -236,8 +257,17 @@ def test_multite_asl_set_brain_mask_raise_error_if_image_dimension_is_different_ ) +def test_multite_mapping_get_brain_mask_return_adjusted_brain_mask_image_in_the_object(): + mte = MultiTE_ASLMapping(asldata_te) + assert np.mean(mte.get_brain_mask()) == 1 + + mask = load_image(M0_BRAIN_MASK) + mte.set_brain_mask(mask) + assert np.unique(mte.get_brain_mask()).tolist() == [0, 1] + + def test_multite_asl_object_create_map_success(): - mte = MultiTE_ASLMapping(asldata) + mte = MultiTE_ASLMapping(asldata_te) out = mte.create_map() assert isinstance(out['cbf'], np.ndarray) assert np.mean(out['cbf']) < 0.0001 @@ -259,7 +289,7 @@ def test_multite_asl_object_raises_error_if_asldata_does_not_have_pcasl_or_m0_im def test_multite_asl_object_raises_error_if_asldata_does_not_have_te_values(): incompleted_asldata = ASLData( - pcasl=PCASL, + pcasl=PCASL_MTE, m0=M0, ld_values=[100.0, 100.0, 150.0, 150.0, 400.0, 800.0, 1800.0], pld_values=[170.0, 270.0, 370.0, 520.0, 670.0, 1070.0, 1870.0], @@ -274,7 +304,7 @@ def test_multite_asl_object_raises_error_if_asldata_does_not_have_te_values(): def test_multite_asl_object_set_cbf_and_att_maps_before_create_map(): - mte = MultiTE_ASLMapping(asldata) + mte = MultiTE_ASLMapping(asldata_te) assert np.mean(mte.get_brain_mask()) == 1 mask = load_image(M0_BRAIN_MASK) @@ -296,7 +326,7 @@ def test_multite_asl_object_set_cbf_and_att_maps_before_create_map(): def test_multite_asl_object_create_map_using_provided_cbf_att_maps(capfd): - mte = MultiTE_ASLMapping(asldata) + mte = MultiTE_ASLMapping(asldata_te) mask = load_image(M0_BRAIN_MASK) cbf = np.ones(mask.shape) * 100 att = np.ones(mask.shape) * 1500 @@ -311,3 +341,172 @@ def test_multite_asl_object_create_map_using_provided_cbf_att_maps(capfd): if re.search('multiTE-ASL', out): test_pass = True assert test_pass + + +def test_multi_dw_asl_object_constructor_created_sucessfully(): + mte = MultiDW_ASLMapping(asldata_dw) + assert isinstance(mte._asl_data, ASLData) + assert isinstance(mte._basic_maps, CBFMapping) + assert isinstance(mte._brain_mask, np.ndarray) + assert isinstance(mte._cbf_map, np.ndarray) + assert isinstance(mte._att_map, np.ndarray) + assert isinstance(mte._A1, np.ndarray) + assert isinstance(mte._D1, np.ndarray) + assert isinstance(mte._A2, np.ndarray) + assert isinstance(mte._D2, np.ndarray) + assert isinstance(mte._kw, np.ndarray) + + +def test_multi_dw_asl_set_brain_mask_success(): + mte = MultiDW_ASLMapping(asldata_dw) + mask = load_image(M0_BRAIN_MASK) + mte.set_brain_mask(mask) + assert isinstance(mte._brain_mask, np.ndarray) + + +def test_multi_dw_asl_set_cbf_map_success(): + mte = MultiDW_ASLMapping(asldata_dw) + fake_cbf = np.ones((10, 10)) * 20 + mte.set_cbf_map(fake_cbf) + assert np.mean(mte._cbf_map) == 20 + + +def test_multi_dw_asl_get_cbf_map_success(): + mte = MultiDW_ASLMapping(asldata_dw) + fake_cbf = np.ones((10, 10)) * 20 + mte.set_cbf_map(fake_cbf) + assert np.mean(mte.get_cbf_map()) == 20 + + +def test_multi_dw_asl_set_att_map_success(): + mte = MultiDW_ASLMapping(asldata_dw) + fake_att = np.ones((10, 10)) * 20 + mte.set_att_map(fake_att) + assert np.mean(mte._att_map) == 20 + + +def test_multi_dw_asl_get_att_map_success(): + mte = MultiDW_ASLMapping(asldata_dw) + fake_att = np.ones((10, 10)) * 20 + mte.set_att_map(fake_att) + assert np.mean(mte.get_att_map()) == 20 + + +@pytest.mark.parametrize('label', [(3), (-1), (1000000), (-1.1), (2.1)]) +def test_multi_dw_asl_set_brain_mask_set_label_value_raise_error_value_not_found_in_mask( + label, +): + mte = MultiDW_ASLMapping(asldata_dw) + mask = load_image(M0_BRAIN_MASK) + with pytest.raises(Exception) as e: + mte.set_brain_mask(mask, label=label) + assert e.value.args[0] == 'Label value is not found in the mask provided.' + + +def test_multi_dw_asl_set_brain_mask_verify_if_input_is_a_label_mask(): + mte = MultiDW_ASLMapping(asldata_dw) + not_mask = load_image(M0) + with pytest.warns(UserWarning): + mte.set_brain_mask(not_mask / np.max(not_mask)) + warnings.warn( + 'Mask image is not a binary image. Any value > 0 will be assumed as brain label.', + UserWarning, + ) + + +def test_multi_dw_asl_set_brain_mask_raise_error_if_image_dimension_is_different_from_3d_volume(): + mte = MultiDW_ASLMapping(asldata_dw) + pcasl_3d_vol = load_image(PCASL_MDW)[0, 0, :, :, :] + fake_mask = np.array(((1, 1, 1), (0, 1, 0))) + with pytest.raises(Exception) as error: + mte.set_brain_mask(fake_mask) + assert ( + error.value.args[0] + == f'Image mask dimension does not match with input 3D volume. Mask shape {fake_mask.shape} not equal to {pcasl_3d_vol.shape}' + ) + + +def test_multi_dw_mapping_get_brain_mask_return_adjusted_brain_mask_image_in_the_object(): + mdw = MultiDW_ASLMapping(asldata_dw) + assert np.mean(mdw.get_brain_mask()) == 1 + + mask = load_image(M0_BRAIN_MASK) + mdw.set_brain_mask(mask) + assert np.unique(mdw.get_brain_mask()).tolist() == [0, 1] + + +# def test_multi_dw_asl_object_create_map_success(): +# mte = MultiDW_ASLMapping(asldata_dw) +# out = mte.create_map() +# assert isinstance(out['cbf'], np.ndarray) +# assert np.mean(out['cbf']) < 0.0001 +# assert isinstance(out['att'], np.ndarray) +# assert np.mean(out['att']) > 10 +# assert isinstance(out['t1blgm'], np.ndarray) +# assert np.mean(out['t1blgm']) > 50 + + +def test_multi_dw_asl_object_raises_error_if_asldata_does_not_have_pcasl_or_m0_image(): + with pytest.raises(Exception) as error: + mte = MultiDW_ASLMapping(incomplete_asldata) + + assert ( + error.value.args[0] + == 'ASLData is incomplete. CBFMapping need pcasl and m0 images.' + ) + + +def test_multi_dw_asl_object_raises_error_if_asldata_does_not_have_te_values(): + incompleted_asldata = ASLData( + pcasl=PCASL_MDW, + m0=M0, + ld_values=[100.0, 100.0, 150.0, 150.0, 400.0, 800.0, 1800.0], + pld_values=[170.0, 270.0, 370.0, 520.0, 670.0, 1070.0, 1870.0], + ) + with pytest.raises(Exception) as error: + mte = MultiDW_ASLMapping(incompleted_asldata) + + assert ( + error.value.args[0] + == 'ASLData is incomplete. MultiDW_ASLMapping need a list of DW values.' + ) + + +def test_multi_dw_asl_object_set_cbf_and_att_maps_before_create_map(): + mte = MultiDW_ASLMapping(asldata_dw) + assert np.mean(mte.get_brain_mask()) == 1 + + mask = load_image(M0_BRAIN_MASK) + mte.set_brain_mask(mask) + assert np.mean(mte.get_brain_mask()) < 1 + + # Test if CBF/ATT are empty (fresh obj creation) + assert np.mean(mte.get_att_map()) == 0 and np.mean(mte.get_cbf_map()) == 0 + + # Update CBF/ATT maps and test if it changed in the obj + cbf = np.ones(mask.shape) * 100 + att = np.ones(mask.shape) * 1500 + mte.set_cbf_map(cbf) + mte.set_att_map(att) + assert ( + np.mean(mte.get_att_map()) == 1500 + and np.mean(mte.get_cbf_map()) == 100 + ) + + +def test_multi_dw_asl_object_create_map_using_provided_cbf_att_maps(capfd): + mte = MultiDW_ASLMapping(asldata_dw) + mask = load_image(M0_BRAIN_MASK) + cbf = np.ones(mask.shape) * 100 + att = np.ones(mask.shape) * 1500 + + mte.set_brain_mask(mask) + mte.set_cbf_map(cbf) + mte.set_att_map(att) + + _ = mte.create_map() + out, err = capfd.readouterr() + test_pass = False + if re.search('multiDW-ASL', out): + test_pass = True + assert test_pass diff --git a/tests/test_utils.py b/tests/test_utils.py index cffe390..4c09cd4 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -8,13 +8,13 @@ SEP = os.sep T1_MRI = f'tests' + SEP + 'files' + SEP + 't1-mri.nrrd' -PCASL = f'tests' + SEP + 'files' + SEP + 'pcasl.nii.gz' +PCASL_MTE = f'tests' + SEP + 'files' + SEP + 'pcasl_mte.nii.gz' M0 = f'tests' + SEP + 'files' + SEP + 'm0.nii.gz' M0_BRAIN_MASK = f'tests' + SEP + 'files' + SEP + 'm0_brain_mask.nii.gz' def test_load_image_pcasl_type_update_object_image_reference(): - img = utils.load_image(PCASL) + img = utils.load_image(PCASL_MTE) assert isinstance(img, np.ndarray) @@ -79,7 +79,7 @@ def test_asl_model_buxton_tau_raise_errors_with_wrong_inputs(input): @pytest.mark.parametrize('input', [('a'), (2), (100.1)]) -def test_asl_model_buxton_tau_raise_errors_with_wrong_inputs(input): +def test_asl_model_buxton_tau_raise_errors_with_wrong_inputs_type(input): with pytest.raises(Exception) as e: buxton_values = utils.asl_model_buxton( tau=input, w=[10, 20, 30], m0=1000, cbf=450, att=1500 @@ -89,6 +89,15 @@ def test_asl_model_buxton_tau_raise_errors_with_wrong_inputs(input): ) +@pytest.mark.parametrize('input', [(['a']), (['2']), (['100.1'])]) +def test_asl_model_buxton_tau_raise_errors_with_wrong_inputs_values(input): + with pytest.raises(Exception) as e: + buxton_values = utils.asl_model_buxton( + tau=input, w=[10, 20, 30], m0=1000, cbf=450, att=1500 + ) + assert e.value.args[0] == 'tau list must contain float or int values' + + @pytest.mark.parametrize( 'input', [(['a', 'b', 'c']), (['a', 'b', 2]), ([100.1, 200.0, np.ndarray])] )