mrid-python 0.1.3__py3-none-any.whl → 0.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mrid/atlas/MNI152/__init__.py +7 -0
- mrid/preprocessing/CTseg.py +81 -0
- mrid/preprocessing/__init__.py +6 -6
- mrid/preprocessing/hd_bet.py +242 -0
- mrid/preprocessing/mask.py +42 -0
- mrid/preprocessing/{registration.py → simple_elastix.py} +20 -82
- mrid/preprocessing/spatial.py +81 -0
- mrid/preprocessing/synthstrip.py +259 -0
- mrid/study.py +223 -36
- mrid/training/__init__.py +0 -0
- mrid/training/slicer.py +235 -0
- mrid/training/transforms.py +98 -0
- mrid/utils/__init__.py +1 -0
- mrid/utils/plotting.py +5 -3
- mrid_python-0.1.4.dist-info/METADATA +77 -0
- mrid_python-0.1.4.dist-info/RECORD +34 -0
- mrid/preprocessing/skullstripping.py +0 -185
- mrid_python-0.1.3.dist-info/METADATA +0 -140
- mrid_python-0.1.3.dist-info/RECORD +0 -27
- {mrid_python-0.1.3.dist-info → mrid_python-0.1.4.dist-info}/WHEEL +0 -0
- {mrid_python-0.1.3.dist-info → mrid_python-0.1.4.dist-info}/top_level.txt +0 -0
mrid/atlas/MNI152/__init__.py
CHANGED
|
@@ -62,6 +62,13 @@ def get_mni152(
|
|
|
62
62
|
):
|
|
63
63
|
"""Returns path to .nii.gz file of specified MNI-152 template.
|
|
64
64
|
|
|
65
|
+
The following templates are available:
|
|
66
|
+
- ``"2006 T1w symmetric"``
|
|
67
|
+
- ``"2009a T1w symmetric"``
|
|
68
|
+
- ``"2009a T2w symmetric"``
|
|
69
|
+
- ``"2009a T1w asymmetric"``
|
|
70
|
+
- ``"2009a T2w asymmetric"``
|
|
71
|
+
|
|
65
72
|
Descriptions of templates are available here https://zenodo.org/records/15470657
|
|
66
73
|
"""
|
|
67
74
|
filename = f"{type} {bool(skullstripped)}.nii.gz"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
""""
|
|
2
|
+
This requires CTseg docker image to be present in the system.
|
|
3
|
+
|
|
4
|
+
If you haven't already, install docker to your OS following this https://docs.docker.com/engine/install/
|
|
5
|
+
|
|
6
|
+
CTseg is available in the following repository: https://github.com/WCHN/CTseg
|
|
7
|
+
|
|
8
|
+
Navigate to any folder, open the terminal in that folder and type in ``git clone https://github.com/WCHN/CTseg``. You might have to [install git](https://git-scm.com/install/) if you don't have it installed. This will create a new directory called ``CTseg`` in the folder and download the repository to it. Alternatively you can open the repository in your web browser, click on the green "Code" button near the top and click "Download ZIP", and unpack the archive to a folder named ``CTseg``.
|
|
9
|
+
|
|
10
|
+
Then type in the command specified in "Docker" section of the read-me in https://github.com/WCHN/CTseg to build an image from the Dockerfile in this repository. I decided to not copy the command here in case it gets updated in the repository. This will download and build a docker image, usually to ``/var/lib/docker/`` on Linux, or inside the Docker Desktop virtual machine disk image on Windows. Note that this will download 3 GB of data, so it might take some time.
|
|
11
|
+
|
|
12
|
+
After it is done, the CTseg functions from mrid can be used.
|
|
13
|
+
"""
|
|
14
|
+
import os
|
|
15
|
+
import subprocess
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from ..loading import tositk
|
|
19
|
+
|
|
20
|
+
def run_CTseg(
|
|
21
|
+
pth_ct: str | os.PathLike,
|
|
22
|
+
dir_out: str = "",
|
|
23
|
+
docker_image = "ubuntu:ctseg",
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Runs ``CTseg`` command-line routine via ``subprocess.run``.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
pth_ct (str | os.PathLike): path to a file which must be in a ``*.nii`` format.
|
|
29
|
+
dir_out (str, optional):
|
|
30
|
+
optional name of a directory that will be created next to ``path_ct`` nii file to save CTseg outputs to.
|
|
31
|
+
If empty, outputs are saved next to ``path_ct`` file. Defaults to ''.
|
|
32
|
+
docker_image (str, optional): name of the docker image that ``CTseg`` is installed in. Defaults to "ubuntu:ctseg".
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# docker run --rm -it -v "/home/jj/data":/data ubuntu:ctseg function spm_CTseg '/data/CT.nii'
|
|
36
|
+
# better
|
|
37
|
+
# docker run --rm -it -v "/home/jj/data":/data ubuntu:ctseg eval "spm_CTseg('/data/CT.nii', '', true, true, true, true, 1.0)"
|
|
38
|
+
pth_ct = Path(pth_ct)
|
|
39
|
+
|
|
40
|
+
if dir_out != "":
|
|
41
|
+
if "/" in dir_out or "\\" in dir_out:
|
|
42
|
+
raise RuntimeError(
|
|
43
|
+
"dir_out should be name of directory that will be created next to `path_ct`. "
|
|
44
|
+
f"It can't be a path. Got '{dir_out}'"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
dir_out = f"/data/{dir_out}"
|
|
48
|
+
|
|
49
|
+
command = [
|
|
50
|
+
"docker",
|
|
51
|
+
"run",
|
|
52
|
+
|
|
53
|
+
# --rm automatically removes the container's file system after the container exits. This is useful for running temporary containers.
|
|
54
|
+
"--rm",
|
|
55
|
+
|
|
56
|
+
# This is a combination of two flags, -i and -t:
|
|
57
|
+
# -i (interactive): Keeps the standard input (STDIN) open, allowing you to interact with the container.
|
|
58
|
+
# -t (tty): Allocates a pseudo-TTY, which makes the container behave like a normal terminal session. (doesn't work with subprocess)
|
|
59
|
+
#"-it",
|
|
60
|
+
"-i",
|
|
61
|
+
|
|
62
|
+
# -v used for mounting volumes, which allows you to connect a file path on your host machine to a path inside the container. This option requires a specific format: -v <host_path>:<container_path>.
|
|
63
|
+
"-v",
|
|
64
|
+
f"{os.path.normpath(pth_ct.parent)}:/data",
|
|
65
|
+
|
|
66
|
+
# docker image name
|
|
67
|
+
docker_image,
|
|
68
|
+
|
|
69
|
+
# evaluate matlab code
|
|
70
|
+
"eval",
|
|
71
|
+
|
|
72
|
+
# code to evaluate
|
|
73
|
+
f"spm_CTseg('/data/{pth_ct.name}', '{dir_out}', true, true, true, true, 1.0)",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
# run dcm2niix
|
|
77
|
+
subprocess.run(command, check=True)
|
|
78
|
+
|
|
79
|
+
# this creates
|
|
80
|
+
# wc01_1_00001_temp_CT_CTseg.nii
|
|
81
|
+
# wc02_1_00001_temp_CT_CTseg
|
mrid/preprocessing/__init__.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from .bias_field_correction import n4_bias_field_correction
|
|
2
2
|
from .cropping import crop_bg, crop_bg_D
|
|
3
|
-
from .
|
|
4
|
-
from .skullstripping import skullstrip, skullstrip_D, run_hd_bet, predict_brain_mask
|
|
3
|
+
from .spatial import downsample, resample_to, resize
|
|
5
4
|
|
|
5
|
+
# lib wrappers
|
|
6
|
+
from . import hd_bet, CTseg, simple_elastix, synthstrip, mask
|
|
6
7
|
__all__ = [
|
|
7
8
|
"n4_bias_field_correction",
|
|
8
9
|
"crop_bg", "crop_bg_D",
|
|
9
|
-
"resample_to", "
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
]
|
|
10
|
+
"resample_to", "resize", "downsample",
|
|
11
|
+
"hd_bet", "CTseg", "simple_elastix", "synthstrip", "mask",
|
|
12
|
+
]
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import tempfile
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
import SimpleITK as sitk
|
|
8
|
+
|
|
9
|
+
from ..loading.convert import ImageLike, tositk
|
|
10
|
+
from ..utils.torch_utils import CUDA_IF_AVAILABLE
|
|
11
|
+
from .simple_elastix import register, register_D
|
|
12
|
+
from .mask import expand_binary_mask, apply_mask
|
|
13
|
+
|
|
14
|
+
# hd_bet -h
|
|
15
|
+
|
|
16
|
+
# -i INPUT, --input INPUT
|
|
17
|
+
# input. Can be either a single file name or an input folder. If file: must be nifti (.nii.gz) and can only be 3D. No support for 4d images, use fslsplit to split 4d sequences into 3d images. If folder: all files ending with .nii.gz within that folder will be brain extracted.
|
|
18
|
+
# -o OUTPUT, --output OUTPUT
|
|
19
|
+
# output. Can be either a filename or a folder. If it does not exist, the folder will be created
|
|
20
|
+
# -device DEVICE used to set on which device the prediction will run. Can be 'cuda' (=GPU), 'cpu' or 'mps'. Default: cuda
|
|
21
|
+
# --disable_tta Set this flag to disable test time augmentation. This will make prediction faster at a slight decrease in prediction quality. Recommended for device cpu
|
|
22
|
+
# --save_bet_mask Set this flag to keep the bet masks. Otherwise they will be removed once HD_BET is done
|
|
23
|
+
# --no_bet_image Set this flag to disable generating the skull stripped/brain extracted image. Only makes sense if you also set --save_bet_mask
|
|
24
|
+
# --verbose Talk to me.
|
|
25
|
+
|
|
26
|
+
def run_hd_bet(
|
|
27
|
+
input: str | os.PathLike,
|
|
28
|
+
output: str | os.PathLike,
|
|
29
|
+
device: Literal['cpu', 'cuda', 'mps'] = CUDA_IF_AVAILABLE,
|
|
30
|
+
disable_tta: bool = False,
|
|
31
|
+
save_bet_mask: bool = True,
|
|
32
|
+
no_bet_image: bool = False,
|
|
33
|
+
verbose: bool = False,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Runs HD-BET command-line routine via ``subprocess.run``.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
input (str | os.PathLike):
|
|
39
|
+
input. Can be either a single file name or an input folder.
|
|
40
|
+
If file: must be nifti (.nii.gz) and can only be 3D.
|
|
41
|
+
No support for 4d images, use fslsplit to split 4d sequences into 3d images.
|
|
42
|
+
If folder: all files ending with .nii.gz within that folder will be brain extracted.
|
|
43
|
+
output (str | os.PathLike):
|
|
44
|
+
output. Can be either a filename or a folder. If it does not exist, the folder will be created
|
|
45
|
+
device (str, optional):
|
|
46
|
+
used to set on which device the prediction will run.
|
|
47
|
+
Can be 'cuda' (=GPU), 'cpu' or 'mps'. Default: cuda. Defaults to CUDA_IF_AVAILABLE.
|
|
48
|
+
disable_tta (bool, optional):
|
|
49
|
+
Set this flag to disable test time augmentation.
|
|
50
|
+
This will make prediction faster at a slight decrease in prediction quality.
|
|
51
|
+
Recommended for device cpu. Defaults to False.
|
|
52
|
+
save_bet_mask (bool, optional):
|
|
53
|
+
Set this flag to keep the bet masks.
|
|
54
|
+
Otherwise they will be removed once HD_BET is done. Defaults to True.
|
|
55
|
+
no_bet_image (bool, optional):
|
|
56
|
+
Set this flag to disable generating the skull stripped/brain extracted image.
|
|
57
|
+
Only makes sense if you also set --save_bet_mask. Defaults to False.
|
|
58
|
+
verbose (bool, optional): Talk to me. Defaults to False.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
command = [
|
|
62
|
+
"hd-bet",
|
|
63
|
+
"-i", os.path.normpath(input),
|
|
64
|
+
"-o", os.path.normpath(output),
|
|
65
|
+
"-device", device,
|
|
66
|
+
]
|
|
67
|
+
if disable_tta: command.append("--disable_tta")
|
|
68
|
+
if save_bet_mask: command.append("--save_bet_mask")
|
|
69
|
+
if no_bet_image: command.append("--no_bet_image")
|
|
70
|
+
if verbose: command.append("--verbose")
|
|
71
|
+
|
|
72
|
+
# run dcm2niix
|
|
73
|
+
subprocess.run(command, check=True)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def predict_brain_mask(
|
|
77
|
+
input: ImageLike,
|
|
78
|
+
register_to_mni152: Literal["T1", "T2"] | None = None,
|
|
79
|
+
device: Literal["cpu", "cuda", "mps"] = CUDA_IF_AVAILABLE,
|
|
80
|
+
disable_tta: bool = False,
|
|
81
|
+
verbose: bool = False,
|
|
82
|
+
) -> sitk.Image:
|
|
83
|
+
"""Returns brain mask of ``input`` predicted by HD-BET.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
input (ImageLike): input to skullstrip. Recommended T1-w, postcontrast T1-w, T2-w or FLAIR sequence in MNI152 space.
|
|
87
|
+
register_to_mni152 (str | None, optional):
|
|
88
|
+
Modality of MNI152 template to pre-register ``input`` to. Should be ``"T1"``, ``"T2"`` or ``None``.
|
|
89
|
+
if specified, ``input`` will be registered to specified MNI152 template,
|
|
90
|
+
then after prediction the brain mask registered back to original ``input``.
|
|
91
|
+
Note that HD-BET expects images to be in MNI152 space. Defaults to None.
|
|
92
|
+
device (str, optional):
|
|
93
|
+
used to set on which device the prediction will run. Can be 'cuda' (=GPU), 'cpu' or 'mps'.
|
|
94
|
+
Defaults to CUDA_IF_AVAILABLE.
|
|
95
|
+
disable_tta (bool, optional):
|
|
96
|
+
Set this flag to disable test time augmentation.
|
|
97
|
+
This will make prediction faster at a slight decrease in prediction quality.
|
|
98
|
+
Recommended for device cpu. Defaults to False.
|
|
99
|
+
verbose (bool, optional): Talk to me. Defaults to False.
|
|
100
|
+
"""
|
|
101
|
+
input = tositk(input)
|
|
102
|
+
|
|
103
|
+
# ---------------------------- register to mni152 ---------------------------- #
|
|
104
|
+
if register_to_mni152 is not None:
|
|
105
|
+
from ..atlas.MNI152 import get_mni152
|
|
106
|
+
mni152 = get_mni152(f"2009a {register_to_mni152}w asymmetric", skullstripped=False) # type:ignore
|
|
107
|
+
input_mni = register(input, mni152)
|
|
108
|
+
|
|
109
|
+
else:
|
|
110
|
+
input_mni = input
|
|
111
|
+
|
|
112
|
+
# ---------------------------- predict brain mask ---------------------------- #
|
|
113
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
114
|
+
sitk.WriteImage(input_mni, os.path.join(tmpdir, "input.nii.gz"))
|
|
115
|
+
|
|
116
|
+
run_hd_bet(
|
|
117
|
+
input = os.path.join(tmpdir, "input.nii.gz"),
|
|
118
|
+
output = os.path.join(tmpdir, "output.nii.gz"),
|
|
119
|
+
device=device, disable_tta=disable_tta, save_bet_mask=True, verbose=verbose,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
brain_mask_mni = tositk(os.path.join(tmpdir, "output_bet.nii.gz"))
|
|
123
|
+
|
|
124
|
+
# ------------------------- unregister mask if needed ------------------------ #
|
|
125
|
+
if register_to_mni152 is not None:
|
|
126
|
+
study_mni = dict(image=input_mni, seg_brain=brain_mask_mni)
|
|
127
|
+
study = register_D(study_mni, key="image", to=input)
|
|
128
|
+
brain_mask = study["seg_brain"]
|
|
129
|
+
|
|
130
|
+
else:
|
|
131
|
+
brain_mask = brain_mask_mni
|
|
132
|
+
|
|
133
|
+
return brain_mask
|
|
134
|
+
|
|
135
|
+
def skullstrip(
|
|
136
|
+
input: ImageLike,
|
|
137
|
+
register_to_mni152: Literal["T1", "T2"] | None = None,
|
|
138
|
+
device: Literal["cpu", "cuda", "mps"] = CUDA_IF_AVAILABLE,
|
|
139
|
+
disable_tta: bool = False,
|
|
140
|
+
verbose: bool = False,
|
|
141
|
+
|
|
142
|
+
expand: int = 0,
|
|
143
|
+
) -> sitk.Image:
|
|
144
|
+
"""Skullstrips ``input`` using HD-BET.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
input (ImageLike): input to skullstrip. Recommended T1-w, postcontrast T1-w, T2-w or FLAIR sequence in MNI152 space.
|
|
148
|
+
register_to_mni152 (str | None, optional):
|
|
149
|
+
Modality of MNI152 template to pre-register ``input`` to. Should be ``"T1"``, ``"T2"`` or ``None``.
|
|
150
|
+
if specified, ``input`` will be registered to specified MNI152 template,
|
|
151
|
+
then after prediction the brain mask registered back to original ``input``.
|
|
152
|
+
Note that HD-BET expects images to be in MNI152 space. Defaults to None.
|
|
153
|
+
device (str, optional):
|
|
154
|
+
used to set on which device the prediction will run. Can be 'cuda' (=GPU), 'cpu' or 'mps'.
|
|
155
|
+
Defaults to CUDA_IF_AVAILABLE.
|
|
156
|
+
disable_tta (bool, optional):
|
|
157
|
+
Set this flag to disable test time augmentation. This will make prediction faster
|
|
158
|
+
at a slight decrease in prediction quality. Recommended for device cpu. Defaults to False.
|
|
159
|
+
verbose (bool, optional): Talk to me. Defaults to False.
|
|
160
|
+
expand (int, optional):
|
|
161
|
+
Positive values expand brain mask by this many pixels, meaning inner parts of the skull will be included;
|
|
162
|
+
Negative values dilate brain mask by this many pixels, meaning outer parts of the brain will be excluded.
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
input = tositk(input)
|
|
166
|
+
mask = predict_brain_mask(input=input, register_to_mni152=register_to_mni152,
|
|
167
|
+
device=device, disable_tta=disable_tta, verbose=verbose)
|
|
168
|
+
|
|
169
|
+
if expand != 0:
|
|
170
|
+
mask = expand_binary_mask(mask, expand=expand)
|
|
171
|
+
|
|
172
|
+
return apply_mask(input, mask)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def skullstrip_D(
|
|
176
|
+
images: Mapping[str, ImageLike],
|
|
177
|
+
key: str,
|
|
178
|
+
register_to_mni152: Literal["T1", "T2"] | None = None,
|
|
179
|
+
device: Literal["cpu", "cuda", "mps"] = CUDA_IF_AVAILABLE,
|
|
180
|
+
disable_tta: bool = False,
|
|
181
|
+
verbose: bool = False,
|
|
182
|
+
|
|
183
|
+
expand: int = 0,
|
|
184
|
+
|
|
185
|
+
include_mask: bool = False,
|
|
186
|
+
keep_original: bool = False,
|
|
187
|
+
) -> dict[str, sitk.Image]:
|
|
188
|
+
"""Predicts brain mask of ``images[key]`` using HD-BET, then uses this mask to skull strip all values in ``images``.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
images (Mapping[str, ImageLike]): dictionary of images that align with each other.
|
|
192
|
+
key (str): key of the image to pass to HD-BET for brain mask prediction.
|
|
193
|
+
register_to_mni152 (str | None, optional):
|
|
194
|
+
Modality of MNI152 template to pre-register ``input`` to. Should be ``"T1"``, ``"T2"`` or ``None``.
|
|
195
|
+
if specified, ``input`` will be registered to specified MNI152 template,
|
|
196
|
+
then after prediction the brain mask registered back to original ``input``.
|
|
197
|
+
Note that HD-BET expects images to be in MNI152 space. Defaults to None.
|
|
198
|
+
device (str, optional):
|
|
199
|
+
used to set on which device the prediction will run. Can be 'cuda' (=GPU), 'cpu' or 'mps'.
|
|
200
|
+
Defaults to CUDA_IF_AVAILABLE.
|
|
201
|
+
disable_tta (bool, optional):
|
|
202
|
+
Set this flag to disable test time augmentation. This will make prediction faster
|
|
203
|
+
at a slight decrease in prediction quality. Recommended for device cpu. Defaults to False.
|
|
204
|
+
verbose (bool, optional): Talk to me. Defaults to False.
|
|
205
|
+
expand (int, optional):
|
|
206
|
+
Positive values expand brain mask by this many pixels, meaning inner parts of the skull will be included;
|
|
207
|
+
Negative values dilate brain mask by this many pixels, meaning outer parts of the brain will be excluded.
|
|
208
|
+
include_mask (bool, optional):
|
|
209
|
+
if True, adds ``"seg_seg_hd_bet"`` with brain mask predicted by HD-BET to returned dictionary.
|
|
210
|
+
This adds brain mask BEFORE expanding/dilating if ``expand`` argument is specified.
|
|
211
|
+
keep_original (bool, Optional):
|
|
212
|
+
if True, skull-stripped images are added to the dictionary
|
|
213
|
+
with ``"_hd_bet" ``postfix, rather than replacing.
|
|
214
|
+
"""
|
|
215
|
+
images = {k: tositk(v) for k,v in images.items()}
|
|
216
|
+
|
|
217
|
+
mask = predict_brain_mask(input=images[key], register_to_mni152=register_to_mni152,
|
|
218
|
+
device=device, disable_tta=disable_tta, verbose=verbose)
|
|
219
|
+
|
|
220
|
+
skullstripped = {}
|
|
221
|
+
|
|
222
|
+
# include mask before expanding
|
|
223
|
+
if include_mask:
|
|
224
|
+
mask_sitk = tositk(mask)
|
|
225
|
+
mask_sitk.CopyInformation(images[key])
|
|
226
|
+
skullstripped["seg_hd_bet"] = mask_sitk
|
|
227
|
+
|
|
228
|
+
# expand
|
|
229
|
+
if expand != 0:
|
|
230
|
+
mask = expand_binary_mask(mask, expand=expand)
|
|
231
|
+
|
|
232
|
+
# apply mask
|
|
233
|
+
for k,v in images.items():
|
|
234
|
+
skullstripped[k] = apply_mask(v, mask)
|
|
235
|
+
|
|
236
|
+
# optionally add with skullstripped postfix
|
|
237
|
+
if keep_original:
|
|
238
|
+
skullstripped = {f"{k}_hd_bet": v for k,v in skullstripped}
|
|
239
|
+
skullstripped.update(images.copy())
|
|
240
|
+
|
|
241
|
+
return skullstripped
|
|
242
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import SimpleITK as sitk
|
|
2
|
+
import numpy as np
|
|
3
|
+
import SimpleITK as sitk
|
|
4
|
+
|
|
5
|
+
from ..loading.convert import ImageLike, tositk, tonumpy
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def expand_binary_mask(binary_mask: ImageLike, expand: int) -> sitk.Image:
|
|
9
|
+
"""Expand or dilate a binary mask.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
binary_mask (ImageLike): mask
|
|
13
|
+
expand (int, optional):
|
|
14
|
+
Positive values expand the mask by this many pixels;
|
|
15
|
+
Negative values dilate the mask by this many pixels.
|
|
16
|
+
"""
|
|
17
|
+
binary_mask = tositk(binary_mask)
|
|
18
|
+
if expand > 0:
|
|
19
|
+
inverted_mask = 1 - binary_mask
|
|
20
|
+
return 1 - sitk.BinaryDilate(inverted_mask, (expand, expand, expand))
|
|
21
|
+
|
|
22
|
+
if expand < 0:
|
|
23
|
+
return sitk.BinaryDilate(binary_mask, (-expand, -expand, -expand))
|
|
24
|
+
|
|
25
|
+
return binary_mask
|
|
26
|
+
|
|
27
|
+
def apply_mask(image: ImageLike, mask: ImageLike) -> sitk.Image:
|
|
28
|
+
"""Applies ``mask`` to ``image``, that is all values where ``mask > 0`` are kept.
|
|
29
|
+
|
|
30
|
+
This function sets all values outside of the mask to smallest value within the mask."""
|
|
31
|
+
image = tositk(image)
|
|
32
|
+
mask = tositk(mask)
|
|
33
|
+
|
|
34
|
+
mask = sitk.Cast(mask, image.GetPixelID())
|
|
35
|
+
|
|
36
|
+
image_np = sitk.GetArrayFromImage(image)
|
|
37
|
+
mask_np = (tonumpy(mask) > 0).astype(np.bool)
|
|
38
|
+
image_ma = np.ma.masked_array(image_np, ~mask_np)
|
|
39
|
+
|
|
40
|
+
image_applied = tositk(image_ma.filled(image_ma.min()))
|
|
41
|
+
image_applied.CopyInformation(image)
|
|
42
|
+
return image_applied
|
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
from collections.abc import Mapping, Sequence
|
|
2
2
|
from typing import TYPE_CHECKING, Any
|
|
3
3
|
|
|
4
|
+
import os
|
|
4
5
|
import numpy as np
|
|
5
6
|
import SimpleITK as sitk
|
|
6
7
|
|
|
7
8
|
from ..loading.convert import tositk, ImageLike
|
|
8
9
|
|
|
9
|
-
def resample_to(input: ImageLike, to: ImageLike, interpolation=sitk.sitkNearestNeighbor) -> sitk.Image:
|
|
10
|
-
"""Resample ``input`` to ``reference``.
|
|
11
|
-
|
|
12
|
-
Resampling uses spatial information embedded in the sitk.Image - size, origin, spacing and direction.
|
|
13
|
-
|
|
14
|
-
Note that this information is only available when certain imaging formats are loaded, such as DICOM and NIfTI.
|
|
15
|
-
|
|
16
|
-
``input`` is transformed in such a way that those attributes will match ``reference``.
|
|
17
|
-
"""
|
|
18
|
-
return sitk.Resample(tositk(input), tositk(to), sitk.Transform(), interpolation)
|
|
19
|
-
|
|
20
10
|
|
|
21
11
|
def _default_pmap():
|
|
22
12
|
"""Default parameter maps for registration"""
|
|
@@ -29,8 +19,8 @@ def _default_pmap():
|
|
|
29
19
|
pmap.append(sitk.GetDefaultParameterMap("affine"))
|
|
30
20
|
return pmap
|
|
31
21
|
|
|
32
|
-
class
|
|
33
|
-
"""Class for image registration.
|
|
22
|
+
class SimpleElastix:
|
|
23
|
+
"""Class for image registration via SimpleElastix.
|
|
34
24
|
|
|
35
25
|
Args:
|
|
36
26
|
pmap (Any, optional): parameter map, if None, uses default parameter map. Defaults to None.
|
|
@@ -50,7 +40,7 @@ class Registration:
|
|
|
50
40
|
|
|
51
41
|
self._moving = None
|
|
52
42
|
self._transformed = None
|
|
53
|
-
self.inverse: "
|
|
43
|
+
self.inverse: "SimpleElastix | None" = None
|
|
54
44
|
|
|
55
45
|
def find_transform(self, input: ImageLike, to: ImageLike) -> sitk.Image:
|
|
56
46
|
"""Find a transform that transforms ``input`` to ``to`` and save it to this ``Registration`` object.
|
|
@@ -62,6 +52,7 @@ class Registration:
|
|
|
62
52
|
"""
|
|
63
53
|
if self._transformed is not None:
|
|
64
54
|
raise RuntimeError("`find_transform` has already been called on this Registration object.")
|
|
55
|
+
|
|
65
56
|
self._moving = tositk(input)
|
|
66
57
|
to = tositk(to)
|
|
67
58
|
|
|
@@ -88,6 +79,9 @@ class Registration:
|
|
|
88
79
|
if self._transformed is None:
|
|
89
80
|
raise RuntimeError("First find transform parameters using `find_transform` method.")
|
|
90
81
|
|
|
82
|
+
input = tositk(input)
|
|
83
|
+
input.CopyInformation(self._moving)
|
|
84
|
+
|
|
91
85
|
transform = sitk.TransformixImageFilter()
|
|
92
86
|
tmap = self.elastix.GetTransformParameterMap()
|
|
93
87
|
if use_nearest_interpolation:
|
|
@@ -118,7 +112,7 @@ class Registration:
|
|
|
118
112
|
inverse_pmap = self.elastix.GetParameterMap() # this returns a copy
|
|
119
113
|
# for p in inverse_pmap:
|
|
120
114
|
# p["Metric"] = "MeanSquaredDifference" # not implemented
|
|
121
|
-
self.inverse =
|
|
115
|
+
self.inverse = SimpleElastix(pmap=inverse_pmap, log_to_console=self.log_to_console)
|
|
122
116
|
self.inverse.find_transform(
|
|
123
117
|
input=self._transformed,
|
|
124
118
|
to=self._moving
|
|
@@ -138,7 +132,7 @@ def register(input: ImageLike, to: ImageLike, pmap: Any = None, log_to_console=F
|
|
|
138
132
|
and install https://pypi.org/project/SimpleITK-SimpleElastix/, don't worry, it's
|
|
139
133
|
the same as SimpleITK but it additionally includes SimpleElastix.
|
|
140
134
|
"""
|
|
141
|
-
reg =
|
|
135
|
+
reg = SimpleElastix(pmap=pmap, log_to_console=log_to_console)
|
|
142
136
|
return reg.find_transform(input=input, to=to)
|
|
143
137
|
|
|
144
138
|
|
|
@@ -155,7 +149,7 @@ def register_D(
|
|
|
155
149
|
Make sure segmentation with hard edges is under a key that starts with ``"seg"``,
|
|
156
150
|
it will use nearest neighbour interpolation, otherwise it will mess up the edges.
|
|
157
151
|
"""
|
|
158
|
-
reg =
|
|
152
|
+
reg = SimpleElastix(pmap=pmap, log_to_console=log_to_console)
|
|
159
153
|
registered = {key: reg.find_transform(images[key], to)}
|
|
160
154
|
|
|
161
155
|
# process segs last because it sets resample interpolator to nearest
|
|
@@ -169,18 +163,23 @@ def register_D(
|
|
|
169
163
|
def register_each(
|
|
170
164
|
images: Mapping[str, ImageLike],
|
|
171
165
|
key: str,
|
|
172
|
-
to: ImageLike,
|
|
166
|
+
to: "ImageLike | None" = None,
|
|
173
167
|
pmap: Any = None,
|
|
174
168
|
log_to_console=False,
|
|
175
169
|
) -> dict[str, sitk.Image]:
|
|
176
|
-
"""
|
|
170
|
+
"""Registers all other images to ``images[key]``.
|
|
171
|
+
If ``to`` is specified, register ``images[key]`` to ``to`` beforehand.
|
|
172
|
+
Uses SimpleElastix.
|
|
177
173
|
|
|
178
174
|
Use this when you have multiple modalities that do not align."""
|
|
179
175
|
images = {k: tositk(v) for k,v in images.items()}
|
|
180
|
-
to = tositk(to)
|
|
181
176
|
|
|
182
177
|
input = images[key]
|
|
183
|
-
|
|
178
|
+
if to is not None:
|
|
179
|
+
to = tositk(to)
|
|
180
|
+
input_reg = register(input=input, to=to, pmap=pmap, log_to_console=log_to_console)
|
|
181
|
+
else:
|
|
182
|
+
input_reg = input
|
|
184
183
|
|
|
185
184
|
registered = {key: input_reg}
|
|
186
185
|
for k,v in images.items():
|
|
@@ -188,64 +187,3 @@ def register_each(
|
|
|
188
187
|
registered[k] = register(input=v, to=input_reg, pmap=pmap, log_to_console=log_to_console)
|
|
189
188
|
|
|
190
189
|
return registered
|
|
191
|
-
|
|
192
|
-
def resize(img: ImageLike, new_size: Sequence[int], interpolator=sitk.sitkLinear) -> sitk.Image:
|
|
193
|
-
"""Resize ``sitk.Image`` to ``new_size``. Retains correct spatial information.
|
|
194
|
-
source: https://gist.github.com/lixinqi98/1bbd3596492f20b776fed2778f7cd48c"""
|
|
195
|
-
img = tositk(img)
|
|
196
|
-
new_size = list(reversed(new_size))
|
|
197
|
-
|
|
198
|
-
# img = sitk.ReadImage(img)
|
|
199
|
-
dimension = img.GetDimension()
|
|
200
|
-
|
|
201
|
-
# Physical image size corresponds to the largest physical size in the training set, or any other arbitrary size.
|
|
202
|
-
reference_physical_size = np.zeros(dimension)
|
|
203
|
-
|
|
204
|
-
reference_physical_size[:] = [(sz - 1) * spc if sz * spc > mx else mx for sz, spc, mx in
|
|
205
|
-
zip(img.GetSize(), img.GetSpacing(), reference_physical_size)]
|
|
206
|
-
|
|
207
|
-
# Create the reference image with a zero origin, identity direction cosine matrix and dimension
|
|
208
|
-
reference_origin = np.zeros(dimension)
|
|
209
|
-
reference_direction = np.identity(dimension).flatten()
|
|
210
|
-
reference_size = new_size
|
|
211
|
-
reference_spacing = [phys_sz / (sz - 1) for sz, phys_sz in zip(reference_size, reference_physical_size)]
|
|
212
|
-
|
|
213
|
-
reference_image = sitk.Image(reference_size, img.GetPixelIDValue())
|
|
214
|
-
reference_image.SetOrigin(reference_origin)
|
|
215
|
-
reference_image.SetSpacing(reference_spacing)
|
|
216
|
-
reference_image.SetDirection(reference_direction)
|
|
217
|
-
|
|
218
|
-
# Always use the TransformContinuousIndexToPhysicalPoint to compute an indexed point's physical coordinates as
|
|
219
|
-
# this takes into account size, spacing and direction cosines. For the vast majority of images the direction
|
|
220
|
-
# cosines are the identity matrix, but when this isn't the case simply multiplying the central index by the
|
|
221
|
-
# spacing will not yield the correct coordinates resulting in a long debugging session.
|
|
222
|
-
reference_center = np.array(
|
|
223
|
-
reference_image.TransformContinuousIndexToPhysicalPoint(np.array(reference_image.GetSize()) / 2.0))
|
|
224
|
-
|
|
225
|
-
# Transform which maps from the reference_image to the current img with the translation mapping the image
|
|
226
|
-
# origins to each other.
|
|
227
|
-
transform = sitk.AffineTransform(dimension)
|
|
228
|
-
transform.SetMatrix(img.GetDirection())
|
|
229
|
-
transform.SetTranslation(np.array(img.GetOrigin()) - reference_origin)
|
|
230
|
-
# Modify the transformation to align the centers of the original and reference image instead of their origins.
|
|
231
|
-
centering_transform = sitk.TranslationTransform(dimension)
|
|
232
|
-
img_center = np.array(img.TransformContinuousIndexToPhysicalPoint(np.array(img.GetSize()) / 2.0))
|
|
233
|
-
centering_transform.SetOffset(np.array(transform.GetInverse().TransformPoint(img_center) - reference_center))
|
|
234
|
-
|
|
235
|
-
# centered_transform = sitk.Transform(transform)
|
|
236
|
-
# centered_transform.AddTransform(centering_transform)
|
|
237
|
-
|
|
238
|
-
centered_transform = sitk.CompositeTransform([transform, centering_transform])
|
|
239
|
-
|
|
240
|
-
# Using the linear interpolator as these are intensity images, if there is a need to resample a ground truth
|
|
241
|
-
# segmentation then the segmentation image should be resampled using the NearestNeighbor interpolator so that
|
|
242
|
-
# no new labels are introduced.
|
|
243
|
-
|
|
244
|
-
return sitk.Resample(img, reference_image, centered_transform, interpolator, 0.0)
|
|
245
|
-
|
|
246
|
-
def downsample(image:ImageLike, factor:float, dims: Sequence[int] | None, interpolator=sitk.sitkLinear) -> sitk.Image:
|
|
247
|
-
"""factor = 2 for 2x downsampling"""
|
|
248
|
-
image = tositk(image)
|
|
249
|
-
size = sitk.GetArrayFromImage(image).shape
|
|
250
|
-
size = [round(s/factor) if (dims is None or i in dims) else s for i,s in enumerate(size)]
|
|
251
|
-
return resize(image, size, interpolator=interpolator)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from collections.abc import Mapping, Sequence
|
|
2
|
+
from typing import TYPE_CHECKING, Any
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import SimpleITK as sitk
|
|
6
|
+
|
|
7
|
+
from ..loading.convert import tositk, ImageLike
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def resample_to(input: ImageLike, to: ImageLike, interpolation=sitk.sitkNearestNeighbor) -> sitk.Image:
|
|
11
|
+
"""Resample ``input`` to ``reference``.
|
|
12
|
+
|
|
13
|
+
Resampling uses spatial information embedded in the sitk.Image - size, origin, spacing and direction.
|
|
14
|
+
|
|
15
|
+
Note that this information is only available when certain imaging formats are loaded, such as DICOM and NIfTI.
|
|
16
|
+
|
|
17
|
+
``input`` is transformed in such a way that those attributes will match ``reference``.
|
|
18
|
+
"""
|
|
19
|
+
return sitk.Resample(tositk(input), tositk(to), sitk.Transform(), interpolation)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resize(img: ImageLike, new_size: Sequence[int], interpolator=sitk.sitkLinear) -> sitk.Image:
|
|
23
|
+
"""Resize ``sitk.Image`` to ``new_size``. Retains correct spatial information.
|
|
24
|
+
source: https://gist.github.com/lixinqi98/1bbd3596492f20b776fed2778f7cd48c"""
|
|
25
|
+
img = tositk(img)
|
|
26
|
+
new_size = list(reversed(new_size))
|
|
27
|
+
|
|
28
|
+
# img = sitk.ReadImage(img)
|
|
29
|
+
dimension = img.GetDimension()
|
|
30
|
+
|
|
31
|
+
# Physical image size corresponds to the largest physical size in the training set, or any other arbitrary size.
|
|
32
|
+
reference_physical_size = np.zeros(dimension)
|
|
33
|
+
|
|
34
|
+
reference_physical_size[:] = [(sz - 1) * spc if sz * spc > mx else mx for sz, spc, mx in
|
|
35
|
+
zip(img.GetSize(), img.GetSpacing(), reference_physical_size)]
|
|
36
|
+
|
|
37
|
+
# Create the reference image with a zero origin, identity direction cosine matrix and dimension
|
|
38
|
+
reference_origin = np.zeros(dimension)
|
|
39
|
+
reference_direction = np.identity(dimension).flatten()
|
|
40
|
+
reference_size = new_size
|
|
41
|
+
reference_spacing = [phys_sz / (sz - 1) for sz, phys_sz in zip(reference_size, reference_physical_size)]
|
|
42
|
+
|
|
43
|
+
reference_image = sitk.Image(reference_size, img.GetPixelIDValue())
|
|
44
|
+
reference_image.SetOrigin(reference_origin)
|
|
45
|
+
reference_image.SetSpacing(reference_spacing)
|
|
46
|
+
reference_image.SetDirection(reference_direction)
|
|
47
|
+
|
|
48
|
+
# Always use the TransformContinuousIndexToPhysicalPoint to compute an indexed point's physical coordinates as
|
|
49
|
+
# this takes into account size, spacing and direction cosines. For the vast majority of images the direction
|
|
50
|
+
# cosines are the identity matrix, but when this isn't the case simply multiplying the central index by the
|
|
51
|
+
# spacing will not yield the correct coordinates resulting in a long debugging session.
|
|
52
|
+
reference_center = np.array(
|
|
53
|
+
reference_image.TransformContinuousIndexToPhysicalPoint(np.array(reference_image.GetSize()) / 2.0))
|
|
54
|
+
|
|
55
|
+
# Transform which maps from the reference_image to the current img with the translation mapping the image
|
|
56
|
+
# origins to each other.
|
|
57
|
+
transform = sitk.AffineTransform(dimension)
|
|
58
|
+
transform.SetMatrix(img.GetDirection())
|
|
59
|
+
transform.SetTranslation(np.array(img.GetOrigin()) - reference_origin)
|
|
60
|
+
# Modify the transformation to align the centers of the original and reference image instead of their origins.
|
|
61
|
+
centering_transform = sitk.TranslationTransform(dimension)
|
|
62
|
+
img_center = np.array(img.TransformContinuousIndexToPhysicalPoint(np.array(img.GetSize()) / 2.0))
|
|
63
|
+
centering_transform.SetOffset(np.array(transform.GetInverse().TransformPoint(img_center) - reference_center))
|
|
64
|
+
|
|
65
|
+
# centered_transform = sitk.Transform(transform)
|
|
66
|
+
# centered_transform.AddTransform(centering_transform)
|
|
67
|
+
|
|
68
|
+
centered_transform = sitk.CompositeTransform([transform, centering_transform])
|
|
69
|
+
|
|
70
|
+
# Using the linear interpolator as these are intensity images, if there is a need to resample a ground truth
|
|
71
|
+
# segmentation then the segmentation image should be resampled using the NearestNeighbor interpolator so that
|
|
72
|
+
# no new labels are introduced.
|
|
73
|
+
|
|
74
|
+
return sitk.Resample(img, reference_image, centered_transform, interpolator, 0.0)
|
|
75
|
+
|
|
76
|
+
def downsample(image:ImageLike, factor:float, dims: Sequence[int] | None, interpolator=sitk.sitkLinear) -> sitk.Image:
|
|
77
|
+
"""factor = 2 for 2x downsampling"""
|
|
78
|
+
image = tositk(image)
|
|
79
|
+
size = sitk.GetArrayFromImage(image).shape
|
|
80
|
+
size = [round(s/factor) if (dims is None or i in dims) else s for i,s in enumerate(size)]
|
|
81
|
+
return resize(image, size, interpolator=interpolator)
|