Skip to content

transformation 🧠

Functions to transform MRIs.

Author: Simon M. Hofmann
Years: 2023-2024

apply_mask 🧠

apply_mask(data: ndarray, mask: ndarray) -> ndarray

Apply volume mask to data.

Data and the corresponding mask must have the same shape.

Parameters:

Name Type Description Default
data ndarray

Data to be masked.

required
mask ndarray

Mask to be applied.

required

Returns:

Type Description
ndarray

Masked data.

Source code in src/xai4mri/dataloader/transformation.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
def apply_mask(data: np.ndarray, mask: np.ndarray) -> np.ndarray:
    """
    Apply volume mask to data.

    Data and the corresponding mask must have the same shape.

    :param data: Data to be masked.
    :param mask: Mask to be applied.
    :return: Masked data.
    """
    if data.shape != mask.shape:
        # check orientation/shape
        msg = f"Data shape {data.shape} does not match brain mask shape {mask.shape}."
        raise ValueError(msg)
    return data * mask

clip_data 🧠

clip_data(
    data: ndarray, at_percentile: float = CLIP_PERCENTILE
) -> ndarray

Clip provided data at a certain intensity percentile as the clipping threshold.

Parameters:

Name Type Description Default
data ndarray

Data to be clipped.

required
at_percentile float

Percentile of the upper bound.

CLIP_PERCENTILE

Returns:

Type Description
ndarray

Clipped data.

Source code in src/xai4mri/dataloader/transformation.py
173
174
175
176
177
178
179
180
181
182
def clip_data(data: np.ndarray, at_percentile: float = CLIP_PERCENTILE) -> np.ndarray:
    """
    Clip provided data at a certain intensity percentile as the clipping threshold.

    :param data: Data to be clipped.
    :param at_percentile: Percentile of the upper bound.
    :return: Clipped data.
    """
    min_clip_val, max_clip_val = determine_min_max_clip(data=data, at_percentile=at_percentile)
    return np.clip(a=data, a_min=min_clip_val, a_max=max_clip_val)

compress_and_norm 🧠

compress_and_norm(
    data: ndarray,
    clip_min: float | int | None,
    clip_max: float | int | None,
    norm: bool | None,
    global_norm_min: float | int | None,
    global_norm_max: float | int | None,
) -> ndarray

Clip, normalize, and/or compress data.

Parameters:

Name Type Description Default
data ndarray

Data to be processed.

required
clip_min float | int | None

Minimum clip value.

required
clip_max float | int | None

Maximum clip value.

required
norm bool | None

Whether to normalize data.

required
global_norm_min float | int | None

Global minimum value for normalization (usually if data is part of bigger dataset).

required
global_norm_max float | int | None

Global maximum value for normalization (usually if data is part of bigger dataset).

required

Returns:

Type Description
ndarray

Processed data.

Source code in src/xai4mri/dataloader/transformation.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
def compress_and_norm(
    data: np.ndarray,
    clip_min: float | int | None,
    clip_max: float | int | None,
    norm: bool | None,
    global_norm_min: float | int | None,
    global_norm_max: float | int | None,
) -> np.ndarray:
    """
    Clip, normalize, and/or compress data.

    :param data: Data to be processed.
    :param clip_min: Minimum clip value.
    :param clip_max: Maximum clip value.
    :param norm: Whether to normalize data.
    :param global_norm_min: Global minimum value for normalization (usually if data is part of bigger dataset).
    :param global_norm_max: Global maximum value for normalization (usually if data is part of bigger dataset).
    :return: Processed data.
    """
    # Check args
    if type(clip_min) is not type(clip_max):
        msg = f"clip_min [{type(clip_min)}] and clip_max [{type(clip_max)}] must be of same type."
        raise TypeError(msg)

    compress = False  # init

    # Clip
    if clip_min is not None and clip_max is not None:
        data = np.clip(a=data, a_min=clip_min, a_max=clip_max)
        compress = True

    # Normalize
    norm = (global_norm_min is not None and global_norm_max is not None) or norm

    if norm and _check_norm(data=data, compress=compress):
        data = normalize(
            array=data,
            lower_bound=0,
            upper_bound=255,
            global_min=global_norm_min,
            global_max=global_norm_max,
        )
    # Compress
    if compress and norm:
        data = np.round(data).astype(np.uint8)
    return data

determine_min_max_clip 🧠

determine_min_max_clip(
    data: ndarray, at_percentile: float = CLIP_PERCENTILE
) -> tuple[float, float]

Determine the min and max clip value for the given data.

This clips the data at the given percentile. That is, all values below the min clip value are set to the min clip value, and all values above the max clip value are set to the max clip value.

Parameters:

Name Type Description Default
data ndarray

data as numpy array

required
at_percentile float

percentile of the upperbound

CLIP_PERCENTILE

Returns:

Type Description
tuple[float, float]

min and max clipping values

Source code in src/xai4mri/dataloader/transformation.py
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def determine_min_max_clip(data: np.ndarray, at_percentile: float = CLIP_PERCENTILE) -> tuple[float, float]:
    """
    Determine the min and max clip value for the given data.

    This clips the data at the given percentile.
    That is, all values below the min clip value are set to the min clip value,
    and all values above the max clip value are set to the max clip value.

    :param data: data as numpy array
    :param at_percentile: percentile of the upperbound
    :return: min and max clipping values
    """
    bg = float(BG_VALUE)  # asserting background value at zero
    d = data[data != bg]  # filter zeros / background, leave informative signal
    n_dec = len(str(at_percentile)) - 1
    max_clip_val = np.nanpercentile(d, q=at_percentile).round(decimals=n_dec)
    min_clip_val = np.minimum(
        bg,  # min_clip_val must be negative or zero
        np.nanpercentile(d, q=100 - at_percentile).round(decimals=n_dec),
    )
    return min_clip_val, max_clip_val

file_to_ref_orientation 🧠

file_to_ref_orientation(
    image_file: Nifti1Image,
    reference_space: str = GLOBAL_ORIENTATION_SPACE,
) -> Nifti1Image

Take a Nibabel NIfTI-file (not array) and return it reoriented to the (global) reference orientation space.

Parameters:

Name Type Description Default
image_file Nifti1Image

NIfTI image file.

required
reference_space str

Reference orientation space. For example, 'LIA' (default) or 'RSP' (as in ANTsPy)

GLOBAL_ORIENTATION_SPACE

Returns:

Type Description
Nifti1Image

reoriented NIfTI image.

Source code in src/xai4mri/dataloader/transformation.py
58
59
60
61
62
63
64
65
66
67
68
69
70
def file_to_ref_orientation(
    image_file: nib.Nifti1Image, reference_space: str = GLOBAL_ORIENTATION_SPACE
) -> nib.Nifti1Image:
    """
    Take a Nibabel NIfTI-file (not array) and return it reoriented to the (global) reference orientation space.

    :param image_file: NIfTI image file.
    :param reference_space: Reference orientation space.
                            For example, 'LIA' (default) or 'RSP' (as in `ANTsPy`)
    :return: reoriented NIfTI image.
    """
    orient_trans = get_orientation_transform(affine=image_file.affine, reference_space=reference_space)
    return image_file.as_reoriented(orient_trans)

get_orientation_transform 🧠

get_orientation_transform(
    affine: ndarray,
    reference_space: str = GLOBAL_ORIENTATION_SPACE,
) -> Nifti1Image

Get the orientation transform from a given affine matrix to the reference space.

The resulting orientation transform (orient_trans) can be used to reorient an MRI to a reference space:

nibabel_image_file.as_reoriented(orient_trans)

Parameters:

Name Type Description Default
affine ndarray

affine matrix

required
reference_space str

reference space

GLOBAL_ORIENTATION_SPACE

Returns:

Type Description
Nifti1Image

orientation transform

Source code in src/xai4mri/dataloader/transformation.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def get_orientation_transform(
    affine: np.ndarray,
    reference_space: str = GLOBAL_ORIENTATION_SPACE,
) -> nib.Nifti1Image:
    """
    Get the orientation transform from a given affine matrix to the reference space.

    The resulting orientation transform (`orient_trans`) can be used to reorient an MRI to a reference space:

        nibabel_image_file.as_reoriented(orient_trans)

    :param affine: affine matrix
    :param reference_space: reference space
    :return: orientation transform
    """
    return nib.orientations.ornt_transform(
        start_ornt=nib.orientations.io_orientation(affine),
        end_ornt=nib.orientations.axcodes2ornt(reference_space),
    )

mri_to_ref_orientation 🧠

mri_to_ref_orientation(
    image: ndarray,
    affine: ndarray,
    reference_space: str = GLOBAL_ORIENTATION_SPACE,
) -> ndarray

Reorient an MRI array to a reference orientation space.

Take an MRI array plus its corresponding affine matrix and return the MRI reoriented to the (global) reference space.

Parameters:

Name Type Description Default
image ndarray

MRI array.

required
affine ndarray

Corresponding affine matrix of the MRI array.

required
reference_space str

Reference orientation space. For example, 'LIA' (default) or 'RSP' (as in ANTsPy)

GLOBAL_ORIENTATION_SPACE

Returns:

Type Description
ndarray

reoriented MRI array.

Source code in src/xai4mri/dataloader/transformation.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def mri_to_ref_orientation(
    image: np.ndarray,
    affine: np.ndarray,
    reference_space: str = GLOBAL_ORIENTATION_SPACE,
) -> np.ndarray:
    """
    Reorient an MRI array to a reference orientation space.

    Take an MRI array plus its corresponding affine matrix
    and return the MRI reoriented to the (global) reference space.

    :param image: MRI array.
    :param affine: Corresponding affine matrix of the MRI array.
    :param reference_space: Reference orientation space.
                            For example, 'LIA' (default) or 'RSP' (as in `ANTsPy`)
    :return: reoriented MRI array.
    """
    # Create orientation transform object first
    orient_trans = get_orientation_transform(affine=affine, reference_space=reference_space)
    return nib.apply_orientation(image, orient_trans)

save_ants_warpers 🧠

save_ants_warpers(
    tx: dict[str, Any],
    folder_path: str | Path,
    image_name: str,
) -> None

Save warper files from ANTsPy's tx object to the given folder_path.

Parameters:

Name Type Description Default
tx dict[str, Any]

ANTsPy transformation object (via ants.registration()).

required
folder_path str | Path

folder path to save warper files

required
image_name str

Image name that will be used as prefix for the warper files.

required
Source code in src/xai4mri/dataloader/transformation.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def save_ants_warpers(tx: dict[str, Any], folder_path: str | Path, image_name: str) -> None:
    """
    Save warper files from `ANTsPy`'s `tx` object to the given `folder_path`.

    :param tx: `ANTsPy` transformation object (via `ants.registration()`).
    :param folder_path: folder path to save warper files
    :param image_name: Image name that will be used as prefix for the warper files.
    """
    if "fwdtransforms" not in list(tx.keys()) or "invtransforms" not in list(tx.keys()):
        msg = "tx object misses forward and/or inverse transformation files."
        raise ValueError(msg)

    # Check whether linear transformation ("Rigid") or non-linear ("SyN")
    linear = tx["fwdtransforms"] == tx["invtransforms"]

    # # Set paths
    # for forward warper
    save_path_name_fwd = str(Path(folder_path, f"{image_name}1Warp.nii.gz"))
    # for inverse warper
    save_path_name_inv = str(Path(folder_path, f"{image_name}1InverseWarp.nii.gz"))
    # # Save also linear transformation .mat file
    save_path_name_mat = str(Path(folder_path, f"{image_name}0GenericAffine.mat"))

    # # Copy warper files from the temporary tx folder file to new location
    if linear:
        copyfile(tx["fwdtransforms"][0], save_path_name_mat)
    else:
        copyfile(tx["fwdtransforms"][0], save_path_name_fwd)
        copyfile(tx["invtransforms"][1], save_path_name_inv)
        copyfile(tx["invtransforms"][0], save_path_name_mat)  # == ['fwdtransforms'][1]