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.
@@ -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
@@ -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 .registration import resample_to, register, register_D, register_each, resize, downsample, Registration
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", "register", "register_D", "register_each", "resize", "downsample",
10
- "skullstrip", "skullstrip_D", "run_hd_bet", "predict_brain_mask"
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 Registration:
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: "Registration | None" = None
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 = Registration(pmap=inverse_pmap, log_to_console=self.log_to_console)
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 = Registration(pmap=pmap, log_to_console=log_to_console)
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 = Registration(pmap=pmap, log_to_console=log_to_console)
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
- """Register ``images[key]`` to ``reference``, then register all other values in ``images`` to registered ``images[key]``.
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
- input_reg = register(input=input, to=to, pmap=pmap, log_to_console=log_to_console)
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)