mrid-python 0.1.4__py3-none-any.whl → 0.1.5__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/preprocessing/hd_bet.py +1 -1
- mrid/preprocessing/mask.py +4 -5
- mrid/preprocessing/spatial.py +2 -1
- mrid/preprocessing/synthstrip.py +13 -2
- mrid/study.py +18 -3
- mrid/training/slicer.py +11 -2
- mrid/training/transforms.py +24 -13
- mrid/utils/plotting.py +9 -7
- mrid/utils/stl_utils.py +90 -18
- {mrid_python-0.1.4.dist-info → mrid_python-0.1.5.dist-info}/METADATA +3 -7
- {mrid_python-0.1.4.dist-info → mrid_python-0.1.5.dist-info}/RECORD +13 -13
- {mrid_python-0.1.4.dist-info → mrid_python-0.1.5.dist-info}/WHEEL +0 -0
- {mrid_python-0.1.4.dist-info → mrid_python-0.1.5.dist-info}/top_level.txt +0 -0
mrid/preprocessing/hd_bet.py
CHANGED
|
@@ -235,7 +235,7 @@ def skullstrip_D(
|
|
|
235
235
|
|
|
236
236
|
# optionally add with skullstripped postfix
|
|
237
237
|
if keep_original:
|
|
238
|
-
skullstripped = {f"{k}_hd_bet": v for k,v in skullstripped}
|
|
238
|
+
skullstripped = {(f"{k}_hd_bet" if k != "seg_hd_bet" else k): v for k,v in skullstripped.items()}
|
|
239
239
|
skullstripped.update(images.copy())
|
|
240
240
|
|
|
241
241
|
return skullstripped
|
mrid/preprocessing/mask.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import SimpleITK as sitk
|
|
2
2
|
import numpy as np
|
|
3
|
-
import SimpleITK as sitk
|
|
4
3
|
|
|
5
4
|
from ..loading.convert import ImageLike, tositk, tonumpy
|
|
6
5
|
|
|
@@ -15,12 +14,12 @@ def expand_binary_mask(binary_mask: ImageLike, expand: int) -> sitk.Image:
|
|
|
15
14
|
Negative values dilate the mask by this many pixels.
|
|
16
15
|
"""
|
|
17
16
|
binary_mask = tositk(binary_mask)
|
|
18
|
-
if expand
|
|
17
|
+
if expand < 0:
|
|
19
18
|
inverted_mask = 1 - binary_mask
|
|
20
|
-
return 1 - sitk.BinaryDilate(inverted_mask, (expand, expand, expand))
|
|
19
|
+
return 1 - sitk.BinaryDilate(inverted_mask, (-expand, -expand, -expand))
|
|
21
20
|
|
|
22
|
-
if expand
|
|
23
|
-
return sitk.BinaryDilate(binary_mask, (
|
|
21
|
+
if expand > 0:
|
|
22
|
+
return sitk.BinaryDilate(binary_mask, (expand, expand, expand))
|
|
24
23
|
|
|
25
24
|
return binary_mask
|
|
26
25
|
|
mrid/preprocessing/spatial.py
CHANGED
|
@@ -73,8 +73,9 @@ def resize(img: ImageLike, new_size: Sequence[int], interpolator=sitk.sitkLinear
|
|
|
73
73
|
|
|
74
74
|
return sitk.Resample(img, reference_image, centered_transform, interpolator, 0.0)
|
|
75
75
|
|
|
76
|
-
def downsample(image:ImageLike, factor:float, dims: Sequence[int] | None, interpolator=sitk.sitkLinear) -> sitk.Image:
|
|
76
|
+
def downsample(image:ImageLike, factor:float, dims: int | Sequence[int] | None, interpolator=sitk.sitkLinear) -> sitk.Image:
|
|
77
77
|
"""factor = 2 for 2x downsampling"""
|
|
78
|
+
if isinstance(dims, int): dims = (dims, )
|
|
78
79
|
image = tositk(image)
|
|
79
80
|
size = sitk.GetArrayFromImage(image).shape
|
|
80
81
|
size = [round(s/factor) if (dims is None or i in dims) else s for i,s in enumerate(size)]
|
mrid/preprocessing/synthstrip.py
CHANGED
|
@@ -70,6 +70,7 @@ def run_synthstrip(
|
|
|
70
70
|
fill: int | None = None,
|
|
71
71
|
no_csf: bool | None = None,
|
|
72
72
|
model: str | os.PathLike | None = None,
|
|
73
|
+
verbose: bool = True,
|
|
73
74
|
):
|
|
74
75
|
"""Runs ``synthstrip`` command-line routine via ``subprocess.run``.
|
|
75
76
|
|
|
@@ -108,7 +109,10 @@ def run_synthstrip(
|
|
|
108
109
|
if model is not None: command.extend(["--model", os.path.normpath(model)])
|
|
109
110
|
|
|
110
111
|
# run
|
|
111
|
-
|
|
112
|
+
if verbose:
|
|
113
|
+
subprocess.run(command, check=True)
|
|
114
|
+
else:
|
|
115
|
+
subprocess.run(command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
|
112
116
|
|
|
113
117
|
def predict_brain_mask(
|
|
114
118
|
synthstrip_script_path: str | os.PathLike,
|
|
@@ -117,6 +121,7 @@ def predict_brain_mask(
|
|
|
117
121
|
border: int | None = None,
|
|
118
122
|
threads: int | None = None,
|
|
119
123
|
model: str | os.PathLike | None = None,
|
|
124
|
+
verbose: bool = True,
|
|
120
125
|
):
|
|
121
126
|
"""Returns brain mask of ``input`` predicted by ``synthstrip``.
|
|
122
127
|
|
|
@@ -141,6 +146,7 @@ def predict_brain_mask(
|
|
|
141
146
|
border=border,
|
|
142
147
|
threads=threads,
|
|
143
148
|
model=model,
|
|
149
|
+
verbose=verbose,
|
|
144
150
|
)
|
|
145
151
|
|
|
146
152
|
brain_mask = tositk(os.path.join(tmpdir, "synthstrip_mask.nii.gz"))
|
|
@@ -155,6 +161,7 @@ def skullstrip(
|
|
|
155
161
|
threads: int | None = None,
|
|
156
162
|
model: str | os.PathLike | None = None,
|
|
157
163
|
expand: int = 0,
|
|
164
|
+
verbose: bool = True,
|
|
158
165
|
):
|
|
159
166
|
"""Skullstrips ``input`` using synthstrip.
|
|
160
167
|
|
|
@@ -181,6 +188,7 @@ def skullstrip(
|
|
|
181
188
|
border=border,
|
|
182
189
|
threads=threads,
|
|
183
190
|
model=model,
|
|
191
|
+
verbose=verbose
|
|
184
192
|
)
|
|
185
193
|
if expand != 0:
|
|
186
194
|
mask = expand_binary_mask(mask, expand=expand)
|
|
@@ -202,6 +210,8 @@ def skullstrip_D(
|
|
|
202
210
|
include_mask: bool = False,
|
|
203
211
|
keep_original: bool = False,
|
|
204
212
|
|
|
213
|
+
verbose: bool = True,
|
|
214
|
+
|
|
205
215
|
) -> dict[str, sitk.Image]:
|
|
206
216
|
"""Predicts brain mask of ``images[key]`` using synthstrip, then uses this mask to skull strip all values in ``images``.
|
|
207
217
|
|
|
@@ -233,6 +243,7 @@ def skullstrip_D(
|
|
|
233
243
|
border=border,
|
|
234
244
|
threads=threads,
|
|
235
245
|
model=model,
|
|
246
|
+
verbose=verbose,
|
|
236
247
|
)
|
|
237
248
|
skullstripped = {}
|
|
238
249
|
|
|
@@ -252,7 +263,7 @@ def skullstrip_D(
|
|
|
252
263
|
|
|
253
264
|
# optionally add with skullstripped postfix
|
|
254
265
|
if keep_original:
|
|
255
|
-
skullstripped = {f"{k}_synthstrip": v for k,v in skullstripped}
|
|
266
|
+
skullstripped = {(f"{k}_synthstrip" if k != "seg_synthstrip" else k): v for k,v in skullstripped.items()}
|
|
256
267
|
skullstripped.update(images.copy())
|
|
257
268
|
|
|
258
269
|
return skullstripped
|
mrid/study.py
CHANGED
|
@@ -79,6 +79,19 @@ class Study(UserDict[str, sitk.Image | Any]):
|
|
|
79
79
|
study[key] = item
|
|
80
80
|
return study
|
|
81
81
|
|
|
82
|
+
def remove(self, *keys: str | Sequence[str]):
|
|
83
|
+
"""Returns a new study without specified keys"""
|
|
84
|
+
keys_proc = []
|
|
85
|
+
for k in keys:
|
|
86
|
+
if isinstance(k, str): keys_proc.append(k)
|
|
87
|
+
else: keys_proc.extend(k)
|
|
88
|
+
|
|
89
|
+
study = self.copy()
|
|
90
|
+
for k in keys_proc:
|
|
91
|
+
del study[k]
|
|
92
|
+
|
|
93
|
+
return study
|
|
94
|
+
|
|
82
95
|
def get_scans(self):
|
|
83
96
|
"""Returns a new ``Study`` with segmentations and info removed."""
|
|
84
97
|
return self.__class__({k:v for k,v in self.items() if not k.startswith(("seg", "info"))})
|
|
@@ -158,7 +171,7 @@ class Study(UserDict[str, sitk.Image | Any]):
|
|
|
158
171
|
and uses that bounding box to crop all other images, including segmentations.
|
|
159
172
|
|
|
160
173
|
Args:
|
|
161
|
-
key: The key of the image to use for finding the foreground bounding box.
|
|
174
|
+
key: The key of the image (scan or segmentation) to use for finding the foreground bounding box.
|
|
162
175
|
"""
|
|
163
176
|
d = preprocessing.cropping.crop_bg_D(self.get_images(), key)
|
|
164
177
|
return Study(**d, **self.get_info())
|
|
@@ -231,6 +244,8 @@ class Study(UserDict[str, sitk.Image | Any]):
|
|
|
231
244
|
|
|
232
245
|
include_mask: bool = False,
|
|
233
246
|
keep_original: bool = False,
|
|
247
|
+
|
|
248
|
+
verbose: bool = True,
|
|
234
249
|
):
|
|
235
250
|
"""Returns a new study with all scans skullstripped.
|
|
236
251
|
|
|
@@ -260,6 +275,7 @@ class Study(UserDict[str, sitk.Image | Any]):
|
|
|
260
275
|
key=key,
|
|
261
276
|
gpu=gpu, border=border, threads=threads, model=model,
|
|
262
277
|
expand=expand, include_mask=include_mask, keep_original=keep_original,
|
|
278
|
+
verbose=verbose,
|
|
263
279
|
)
|
|
264
280
|
return Study(**d, **self.get_segmentations(), **self.get_info())
|
|
265
281
|
|
|
@@ -460,7 +476,6 @@ class Study(UserDict[str, sitk.Image | Any]):
|
|
|
460
476
|
arr = tfm(self.numpy(key))
|
|
461
477
|
return self.add(f'{key}{postfix}', arr, reference_key=key)
|
|
462
478
|
|
|
463
|
-
|
|
464
479
|
def numpy(self, key: str):
|
|
465
480
|
"""returns ``study[key]`` converted to a numpy array."""
|
|
466
481
|
return tonumpy(self[key])
|
|
@@ -527,7 +542,7 @@ class Study(UserDict[str, sitk.Image | Any]):
|
|
|
527
542
|
|
|
528
543
|
def plot(self):
|
|
529
544
|
from .utils.plotting import plot_study
|
|
530
|
-
plot_study(self.get_images().numpy_dict())
|
|
545
|
+
return plot_study(self.get_images().numpy_dict())
|
|
531
546
|
|
|
532
547
|
def save(
|
|
533
548
|
self,
|
mrid/training/slicer.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import random
|
|
2
3
|
from functools import partial
|
|
3
4
|
from typing import Any, Literal, cast
|
|
4
5
|
from collections.abc import Callable, Sequence
|
|
5
6
|
import torch
|
|
6
7
|
|
|
7
|
-
from ..loading import ImageLike, totensor
|
|
8
|
+
from ..loading import ImageLike, totensor, tonumpy
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class SliceSampler:
|
|
@@ -216,6 +217,7 @@ class SliceDataset(torch.utils.data.Dataset):
|
|
|
216
217
|
randflip: bool = True,
|
|
217
218
|
flatten: bool = True,
|
|
218
219
|
repeat: int = 1,
|
|
220
|
+
tfm: Callable[[torch.Tensor, torch.Tensor], Any] | None = None,
|
|
219
221
|
):
|
|
220
222
|
super().__init__()
|
|
221
223
|
self._callables = [s.random_weighted_callable(
|
|
@@ -223,6 +225,8 @@ class SliceDataset(torch.utils.data.Dataset):
|
|
|
223
225
|
|
|
224
226
|
self._repeat = repeat
|
|
225
227
|
|
|
228
|
+
self.tfm = tfm
|
|
229
|
+
|
|
226
230
|
def __len__(self):
|
|
227
231
|
return len(self._callables) * self._repeat
|
|
228
232
|
|
|
@@ -232,4 +236,9 @@ class SliceDataset(torch.utils.data.Dataset):
|
|
|
232
236
|
if i >= length:
|
|
233
237
|
raise IndexError(f"Index {i} is larger than length of SliceDataset {length}")
|
|
234
238
|
|
|
235
|
-
|
|
239
|
+
img, seg = self._callables[i % len(self._callables)]()
|
|
240
|
+
|
|
241
|
+
if self.tfm is not None:
|
|
242
|
+
return self.tfm(img, seg)
|
|
243
|
+
|
|
244
|
+
return img, seg
|
mrid/training/transforms.py
CHANGED
|
@@ -9,29 +9,41 @@ ARR = TypeVar("ARR", bound=Any)
|
|
|
9
9
|
def crop(
|
|
10
10
|
arr: ARR,
|
|
11
11
|
reduction: Sequence[int],
|
|
12
|
-
where: Literal["start", "end", "center"] = "center",
|
|
12
|
+
where: Literal["start", "end", "center", "random"] = "center",
|
|
13
13
|
) -> ARR:
|
|
14
|
-
"""Crop ``
|
|
14
|
+
"""Crop ``arr`` such that ``output.shape[i] = input.shape[i] - reduction[i]``"""
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
slices = [(int(i / 2), -int(i / 2)) if i % 2 == 0 else (int(i / 2), -int(i / 2) - 1) for i in reduction]
|
|
16
|
+
shape = arr.shape[-len(reduction):]
|
|
17
|
+
slices = []
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
for r, sh in zip(reduction, shape):
|
|
20
|
+
if r == 0:
|
|
21
|
+
slices.append(slice(None))
|
|
22
|
+
continue
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
if r < 0: raise ValueError("Reduction cannot be negative")
|
|
25
|
+
if r > sh: raise ValueError(f"Reduction {r} exceeds dimension size {sh}")
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
if where == 'start': start, end = 0, sh - r
|
|
28
|
+
elif where == 'end': start, end = r, sh
|
|
29
|
+
elif where == 'center':
|
|
30
|
+
start = r // 2
|
|
31
|
+
end = start + (sh - r)
|
|
32
|
+
elif where == 'random':
|
|
33
|
+
start = random.randint(0, r)
|
|
34
|
+
end = start + (sh - r)
|
|
35
|
+
else:
|
|
36
|
+
raise ValueError(f"Invalid where: {where}")
|
|
27
37
|
|
|
28
|
-
|
|
38
|
+
slices.append(slice(start, end))
|
|
39
|
+
|
|
40
|
+
# apply with broadcasting
|
|
29
41
|
return arr[(..., *slices)]
|
|
30
42
|
|
|
31
43
|
def crop_to_shape(
|
|
32
44
|
input: ARR,
|
|
33
45
|
shape: Sequence[int],
|
|
34
|
-
where: Literal["start", "end", "center"] = "center",
|
|
46
|
+
where: Literal["start", "end", "center", "random"] = "center",
|
|
35
47
|
) -> ARR:
|
|
36
48
|
"""Crop ``input`` to ``shape``."""
|
|
37
49
|
|
|
@@ -42,7 +54,6 @@ def crop_to_shape(
|
|
|
42
54
|
return crop(input, [i - j for i, j in zip(input.shape, shape)], where=where)
|
|
43
55
|
|
|
44
56
|
|
|
45
|
-
|
|
46
57
|
def shuffle_channels(x:torch.Tensor):
|
|
47
58
|
"""Shuffle first axis in a ``(C, *)`` tensor"""
|
|
48
59
|
return x[torch.randperm(x.shape[0])]
|
mrid/utils/plotting.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from collections.abc import Mapping
|
|
2
2
|
|
|
3
|
-
import matplotlib.gridspec as gridspec
|
|
4
|
-
import matplotlib.pyplot as plt
|
|
5
3
|
import numpy as np
|
|
6
4
|
from ..loading import ImageLike, tonumpy
|
|
7
5
|
|
|
8
6
|
def plot_study(data: Mapping[str, ImageLike]):
|
|
7
|
+
import matplotlib.gridspec as gridspec
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
|
|
9
10
|
data = {k: tonumpy(v) for k,v in data.items()}
|
|
10
11
|
n_vals = len(data)
|
|
11
12
|
|
|
@@ -77,7 +78,8 @@ def plot_study(data: Mapping[str, ImageLike]):
|
|
|
77
78
|
fig.text(box.x0 + box.width/2, box.y1 + 0.04, modality_name,
|
|
78
79
|
ha='center', va='bottom', fontsize=14, fontweight='bold')
|
|
79
80
|
|
|
80
|
-
plt.show()
|
|
81
|
+
# plt.show()
|
|
82
|
+
return fig
|
|
81
83
|
|
|
82
84
|
if __name__ == "__main__":
|
|
83
85
|
|
|
@@ -97,8 +99,8 @@ if __name__ == "__main__":
|
|
|
97
99
|
return mask
|
|
98
100
|
|
|
99
101
|
plot_study({
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"
|
|
102
|
+
"Sphere": dummy_data((60, 60, 60), 'sphere'),
|
|
103
|
+
"Cube": dummy_data((60, 60, 60), 'cube'),
|
|
104
|
+
"Noise": dummy_data((60, 60, 60), 'noise'),
|
|
105
|
+
"Another sphere": dummy_data((60, 60, 60), 'sphere'),
|
|
104
106
|
})
|
mrid/utils/stl_utils.py
CHANGED
|
@@ -6,6 +6,69 @@ import SimpleITK as sitk
|
|
|
6
6
|
|
|
7
7
|
from ..loading.convert import tositk
|
|
8
8
|
|
|
9
|
+
# def stl2sitk(
|
|
10
|
+
# stl_path: str | os.PathLike,
|
|
11
|
+
# reference: str | os.PathLike | sitk.Image,
|
|
12
|
+
# fix_holes: bool = False,
|
|
13
|
+
# ):
|
|
14
|
+
# """
|
|
15
|
+
# Loads an STL file under ``stl_path`` and converts it to ``sitk.Image`` aligned with ``reference``.
|
|
16
|
+
# Note that this might take a few minutes.
|
|
17
|
+
|
|
18
|
+
# Args:
|
|
19
|
+
# stl_path (str): Path to the STL segmentation file. The STL coordinates
|
|
20
|
+
# MUST be in the same coordinate system as the ``reference``.
|
|
21
|
+
# reference (str | os.PathLike | sitk.Image): path to a directory of DICOM files or a NIfTI file, or a ``sitk.Image``.
|
|
22
|
+
# fix_holes (bool, optional): whether to try to fix holes in STL if they are detected (this can be very slow).
|
|
23
|
+
# """
|
|
24
|
+
# import trimesh
|
|
25
|
+
|
|
26
|
+
# # ------------------------------ load reference ------------------------------ #
|
|
27
|
+
# reference = tositk(reference)
|
|
28
|
+
|
|
29
|
+
# origin = np.array(reference.GetOrigin())
|
|
30
|
+
# spacing = np.array(reference.GetSpacing())
|
|
31
|
+
# # ct_direction = np.array(ct_image.GetDirection()).reshape(3, 3)
|
|
32
|
+
# size = np.array(reference.GetSize()) # Order: x, y, z
|
|
33
|
+
# shape_xyz = size
|
|
34
|
+
# shape_zyx = size[::-1]
|
|
35
|
+
|
|
36
|
+
# # --------------------------------- load STL --------------------------------- #
|
|
37
|
+
# mesh = trimesh.load_mesh(stl_path)
|
|
38
|
+
|
|
39
|
+
# if not mesh.is_watertight:
|
|
40
|
+
# warnings.warn(f"Warning: STL mesh '{stl_path}' is not watertight. Voxelization using 'contains' might be inaccurate.")
|
|
41
|
+
# if fix_holes:
|
|
42
|
+
# mesh.fill_holes()
|
|
43
|
+
# if not mesh.is_watertight:
|
|
44
|
+
# warnings.warn("Warning: Failed to make mesh watertight after filling holes.")
|
|
45
|
+
|
|
46
|
+
# # ------------------------------- voxelize STL ------------------------------- #
|
|
47
|
+
# x_coords = origin[0] + np.arange(shape_xyz[0]) * spacing[0]
|
|
48
|
+
# y_coords = origin[1] + np.arange(shape_xyz[1]) * spacing[1]
|
|
49
|
+
# z_coords = origin[2] + np.arange(shape_xyz[2]) * spacing[2]
|
|
50
|
+
|
|
51
|
+
# # Use meshgrid to create a grid of coordinates
|
|
52
|
+
# # Note the 'ij' indexing to match the z, y, x array structure
|
|
53
|
+
# zz, yy, xx = np.meshgrid(z_coords, y_coords, x_coords, indexing='ij')
|
|
54
|
+
|
|
55
|
+
# # Stack coordinates into a (N, 3) array where N = Z*Y*X
|
|
56
|
+
# voxel_centers_xyz = np.stack([xx.ravel(), yy.ravel(), zz.ravel()], axis=-1)
|
|
57
|
+
|
|
58
|
+
# # This checks which voxel center points fall inside the mesh volume
|
|
59
|
+
# voxel_mask_flat = mesh.contains(voxel_centers_xyz)
|
|
60
|
+
|
|
61
|
+
# # Reshape the flat boolean mask back into the 3D CT shape (z, y, x)
|
|
62
|
+
# stl_array = voxel_mask_flat.reshape(shape_zyx).astype(np.uint8) # Use uint8 for masks
|
|
63
|
+
|
|
64
|
+
# # ------------------------------ make sitk.Image ----------------------------- #
|
|
65
|
+
# stl_sitk = sitk.GetImageFromArray(stl_array)
|
|
66
|
+
|
|
67
|
+
# stl_sitk.SetOrigin(reference.GetOrigin())
|
|
68
|
+
# stl_sitk.SetSpacing(reference.GetSpacing())
|
|
69
|
+
# stl_sitk.SetDirection(reference.GetDirection())
|
|
70
|
+
# return stl_sitk
|
|
71
|
+
|
|
9
72
|
def stl2sitk(
|
|
10
73
|
stl_path: str | os.PathLike,
|
|
11
74
|
reference: str | os.PathLike | sitk.Image,
|
|
@@ -25,11 +88,10 @@ def stl2sitk(
|
|
|
25
88
|
|
|
26
89
|
# ------------------------------ load reference ------------------------------ #
|
|
27
90
|
reference = tositk(reference)
|
|
28
|
-
|
|
29
91
|
origin = np.array(reference.GetOrigin())
|
|
30
92
|
spacing = np.array(reference.GetSpacing())
|
|
31
|
-
|
|
32
|
-
|
|
93
|
+
size = np.array(reference.GetSize()) # x, y, z
|
|
94
|
+
|
|
33
95
|
shape_xyz = size
|
|
34
96
|
shape_zyx = size[::-1]
|
|
35
97
|
|
|
@@ -37,34 +99,44 @@ def stl2sitk(
|
|
|
37
99
|
mesh = trimesh.load_mesh(stl_path)
|
|
38
100
|
|
|
39
101
|
if not mesh.is_watertight:
|
|
40
|
-
warnings.warn(f"
|
|
102
|
+
warnings.warn(f"STL mesh '{stl_path}' is not watertight.")
|
|
41
103
|
if fix_holes:
|
|
42
104
|
mesh.fill_holes()
|
|
43
|
-
if not mesh.is_watertight:
|
|
44
|
-
print("Warning: Failed to make mesh watertight after filling holes.")
|
|
45
105
|
|
|
46
|
-
#
|
|
106
|
+
# ----------------------- find bounding box of the mask ---------------------- #
|
|
107
|
+
bounds = mesh.bounds # [[min_x, min_y, min_z], [max_x, max_y, max_z]]
|
|
108
|
+
voxel_min = np.floor((bounds[0] - origin) / spacing).astype(int)
|
|
109
|
+
voxel_max = np.ceil((bounds[1] - origin) / spacing).astype(int)
|
|
110
|
+
|
|
111
|
+
z_min, z_max = np.clip([voxel_min[2], voxel_max[2]], 0, shape_xyz[2] - 1)
|
|
112
|
+
|
|
113
|
+
# --------------------------------- voxelize --------------------------------- #
|
|
114
|
+
stl_array = np.zeros(shape_zyx, dtype=np.uint8)
|
|
47
115
|
x_coords = origin[0] + np.arange(shape_xyz[0]) * spacing[0]
|
|
48
116
|
y_coords = origin[1] + np.arange(shape_xyz[1]) * spacing[1]
|
|
49
|
-
z_coords = origin[2] + np.arange(shape_xyz[2]) * spacing[2]
|
|
50
117
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
118
|
+
yy, xx = np.meshgrid(y_coords, x_coords, indexing='ij')
|
|
119
|
+
points_2d = np.stack([xx.ravel(), yy.ravel()], axis=-1)
|
|
120
|
+
|
|
121
|
+
for z_idx in range(z_min, z_max + 1):
|
|
122
|
+
z_val = origin[2] + z_idx * spacing[2]
|
|
54
123
|
|
|
55
|
-
|
|
56
|
-
|
|
124
|
+
# Create 3D points for this slice: [X, Y, current_Z]
|
|
125
|
+
points_3d = np.column_stack([
|
|
126
|
+
points_2d,
|
|
127
|
+
np.full(points_2d.shape[0], z_val)
|
|
128
|
+
])
|
|
57
129
|
|
|
58
|
-
|
|
59
|
-
|
|
130
|
+
# Check containment for this slice only
|
|
131
|
+
mask_flat = mesh.contains(points_3d)
|
|
60
132
|
|
|
61
|
-
|
|
62
|
-
|
|
133
|
+
# Reshape and insert into the 3D array
|
|
134
|
+
stl_array[z_idx, :, :] = mask_flat.reshape(shape_xyz[1], shape_xyz[0])
|
|
63
135
|
|
|
64
136
|
# ------------------------------ make sitk.Image ----------------------------- #
|
|
65
137
|
stl_sitk = sitk.GetImageFromArray(stl_array)
|
|
66
|
-
|
|
67
138
|
stl_sitk.SetOrigin(reference.GetOrigin())
|
|
68
139
|
stl_sitk.SetSpacing(reference.GetSpacing())
|
|
69
140
|
stl_sitk.SetDirection(reference.GetDirection())
|
|
141
|
+
|
|
70
142
|
return stl_sitk
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mrid-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Tools for working with 3D medical images and segmentations - registration, brain skull-stripping, etc.
|
|
5
5
|
Author-email: Ivan Nikishev <nkshv2@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/inikishev/mrid
|
|
@@ -16,7 +16,7 @@ Requires-Dist: SimpleITK
|
|
|
16
16
|
|
|
17
17
|
mrid is a library for preprocessing of 3D images, particularly medical images.
|
|
18
18
|
|
|
19
|
-
It provide interfaces for many medical image processing tools such as [SimpleElastix](https://simpleelastix.github.io/), [HD-BET](https://github.com/MIC-DKFZ/HD-BET#Installation), [SynthStrip](https://surfer.nmr.mgh.harvard.edu/docs/synthstrip/), [CTSeg](https://github.com/WCHN/CTseg). Note that those libraries are not bundled
|
|
19
|
+
It provide interfaces for many medical image processing tools such as [SimpleElastix](https://simpleelastix.github.io/), [HD-BET](https://github.com/MIC-DKFZ/HD-BET#Installation), [SynthStrip](https://surfer.nmr.mgh.harvard.edu/docs/synthstrip/), [CTSeg](https://github.com/WCHN/CTseg). Note that those libraries are not bundled, installation instructions are included in all examples below.
|
|
20
20
|
|
|
21
21
|
### Installation
|
|
22
22
|
|
|
@@ -32,10 +32,6 @@ or
|
|
|
32
32
|
pip install git+https://github.com/inikishev/mrid
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
### Basics
|
|
36
|
-
|
|
37
|
-
The images you pass to all functions in mrid can be path to a .nii.gz file, DICOM directory, sitk.Image, numpy array or torch tensor. All functions return results as `sitk.Image`. If you need a numpy array, you can use `mrid.tonumpy(sitk_image)`.
|
|
38
|
-
|
|
39
35
|
### Registering images with SimpleITK-SimpleElastix
|
|
40
36
|
|
|
41
37
|
[SimpleElastix](https://simpleelastix.github.io/) is a robust tool for image registration which works really well out-of-the-box. It works on both Windows and Linux.
|
|
@@ -69,7 +65,7 @@ Many [BraTS](https://www.synapse.org/brats) datasets are provided as skullstripp
|
|
|
69
65
|
|
|
70
66
|
<img width="828" height="849" alt="image" src="https://github.com/user-attachments/assets/f1b38db3-6648-4660-a381-d68a2eb8508d" />
|
|
71
67
|
|
|
72
|
-
(T1n image looks weird because that's just how it is in the dataset)
|
|
68
|
+
(T1n image looks weird because that's just how it is in the zenodo dataset)
|
|
73
69
|
|
|
74
70
|
### References
|
|
75
71
|
The MRIs for all images above are from https://zenodo.org/records/7213153.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
mrid/__init__.py,sha256=d-h6l6e-sfPUKDo27_dXcx8IRdZ5ByDC1YtdkM6y39o,138
|
|
2
|
-
mrid/study.py,sha256=
|
|
2
|
+
mrid/study.py,sha256=32dZe3x40lbKvIFsuQ8xaPJLEKElM-wGUv5Sm56J13g,29524
|
|
3
3
|
mrid/atlas/__init__.py,sha256=ecOAVXUOt0TUI_wK19Ed05arF6b_G61wucVq2c8U4ZY,110
|
|
4
4
|
mrid/atlas/MNI152/__init__.py,sha256=XTakiwb_rFxpO08O45tVKyHDzcT3xvJoLLMLu5EJhGw,3038
|
|
5
5
|
mrid/atlas/SRI24/__init__.py,sha256=jFFY6LeJcWwCtxVJGbdWPEUGFKZjqzxu01ZTbbSAwEo,2939
|
|
@@ -9,26 +9,26 @@ mrid/preprocessing/CTseg.py,sha256=Zum1bvvU28JptrXLIOAaWVQA6LWPfaheCo-6liN1Gq4,3
|
|
|
9
9
|
mrid/preprocessing/__init__.py,sha256=UNkuK_OKqBSdAt7n8dc7t5p7206W2wwksJwGy7rXdr0,412
|
|
10
10
|
mrid/preprocessing/bias_field_correction.py,sha256=K2w75JkVIV_Gj7W-9gpHUwz2teEvgSRt5wX-CqQeRik,1143
|
|
11
11
|
mrid/preprocessing/cropping.py,sha256=I26hxo7RuWqdjx8ApJ2t9FBPdi3PtRsBdMlLC2KgKTk,1336
|
|
12
|
-
mrid/preprocessing/hd_bet.py,sha256=
|
|
13
|
-
mrid/preprocessing/mask.py,sha256
|
|
12
|
+
mrid/preprocessing/hd_bet.py,sha256=ybjtKfrdI5sFkjPZHh1rTPaMPy8JA7cIR71UlElKZ2A,11269
|
|
13
|
+
mrid/preprocessing/mask.py,sha256=-x-9-CMbUR7Jx8p7YVtBb3r-OnGut9YefmEVI04jyDc,1350
|
|
14
14
|
mrid/preprocessing/simple_elastix.py,sha256=gddw-B-IpGIyhfqj05kex8Zqb3_gMD8Z8w0BiLlx8dA,7975
|
|
15
|
-
mrid/preprocessing/spatial.py,sha256=
|
|
16
|
-
mrid/preprocessing/synthstrip.py,sha256=
|
|
15
|
+
mrid/preprocessing/spatial.py,sha256=hsbGprUEcbeH0FY0_cgHZHjDOXGF55-ojtznuHttLkU,4316
|
|
16
|
+
mrid/preprocessing/synthstrip.py,sha256=TaRFJn4vPmZ7V-SmKdaOTR3yZakoA3OfhwdjiBQg2Xs,10501
|
|
17
17
|
mrid/training/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
mrid/training/slicer.py,sha256=
|
|
19
|
-
mrid/training/transforms.py,sha256=
|
|
18
|
+
mrid/training/slicer.py,sha256=7eRQMDQ40FvpHs2J_eg_VoMnI42cOXG_tmgTDtYvVx8,10454
|
|
19
|
+
mrid/training/transforms.py,sha256=fWoXH3KbkN3s-_A3h0bqt2yz9Ek5K8PzSIBaz3s5goM,4005
|
|
20
20
|
mrid/utils/__init__.py,sha256=vDuaVk8uKs84R_Fg4RmGhQ-qHmpzS4--wPM86opvgig,153
|
|
21
21
|
mrid/utils/dcm2niix.py,sha256=KHU7dgUVsU7WOEa82HFO-G0SsGIxYjeUFEBcX27mkWQ,4124
|
|
22
22
|
mrid/utils/dicom_uid_fixer.py,sha256=xjjfvAF13Ni5CEgwPcr7oDT7excdEoeRGQetvjGkSRM,4046
|
|
23
|
-
mrid/utils/plotting.py,sha256=
|
|
23
|
+
mrid/utils/plotting.py,sha256=bALTvNQxGXWl0fxGdKbSON47dz4OXfxVYEn6WeqdNK4,3885
|
|
24
24
|
mrid/utils/python_utils.py,sha256=IYB8-KN4d6l-5ILgUNYux0vNrK0D0vh0AbSRqWhs-fo,1793
|
|
25
|
-
mrid/utils/stl_utils.py,sha256=
|
|
25
|
+
mrid/utils/stl_utils.py,sha256=vR6CqG1D4cpDmjkcZU0H5jCMVROeLwOTAzvoQzipgxs,5869
|
|
26
26
|
mrid/utils/torch_utils.py,sha256=nKby_tPWzCGdEASkDUq7xNo-iUe9zjwmhq_chV0RCGU,407
|
|
27
27
|
tests/test_loading.py,sha256=CciTvnqQ7hY-Vk2GgH2SWSu8nyS8lLeBqHu3XzClBLY,2141
|
|
28
28
|
tests/test_preprocessing.py,sha256=HcoubITfmlJkgPaBbkxZiY04fJv9URBGMWDJWivDqLU,1300
|
|
29
29
|
tests/test_study.py,sha256=pK__azqnMz8DSyOYaWQmBkpAqLntCIIjEz0m8oBS4RQ,3984
|
|
30
30
|
tests/test_utils.py,sha256=Q4uf5dog673RDofOC6RPLYnxVN5NAIVRTiyTbf9Az74,359
|
|
31
|
-
mrid_python-0.1.
|
|
32
|
-
mrid_python-0.1.
|
|
33
|
-
mrid_python-0.1.
|
|
34
|
-
mrid_python-0.1.
|
|
31
|
+
mrid_python-0.1.5.dist-info/METADATA,sha256=F5EwkIBp-KDPbLvwuMFWbsE-S-AUMk4OkuoK9u0jkm0,3795
|
|
32
|
+
mrid_python-0.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
33
|
+
mrid_python-0.1.5.dist-info/top_level.txt,sha256=lBv75ms7UoIM4elDVX3CbKiSlh-X0vABtaUwj4TGx4o,11
|
|
34
|
+
mrid_python-0.1.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|