segresnet-mlx 0.2.2__tar.gz → 0.2.3__tar.gz
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.
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/PKG-INFO +5 -2
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/README.md +4 -1
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/pyproject.toml +1 -1
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/src/segresnet_mlx/__init__.py +1 -1
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/src/segresnet_mlx/medmlx.py +44 -8
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/src/segresnet_mlx/network.py +9 -4
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/src/segresnet_mlx/predictor.py +3 -2
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/src/segresnet_mlx/topology.py +1 -1
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/src/segresnet_mlx/transforms.py +7 -2
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/src/segresnet_mlx/weights.py +22 -4
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/.gitignore +0 -0
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/LICENSE +0 -0
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/THIRD_PARTY_NOTICES.md +0 -0
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/src/segresnet_mlx/_arrays.py +0 -0
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/src/segresnet_mlx/_nifti.py +0 -0
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/src/segresnet_mlx/_records.py +0 -0
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/src/segresnet_mlx/_runtime.py +0 -0
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/src/segresnet_mlx/_types.py +0 -0
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/src/segresnet_mlx/inference.py +0 -0
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/src/segresnet_mlx/py.typed +0 -0
- {segresnet_mlx-0.2.2 → segresnet_mlx-0.2.3}/src/segresnet_mlx/telemetry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: segresnet-mlx
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Native MLX SegResNet with NumPy sliding-window logit inference
|
|
5
5
|
Project-URL: Homepage, https://github.com/MedMLX/segresnet-mlx
|
|
6
6
|
Project-URL: Documentation, https://github.com/MedMLX/segresnet-mlx/blob/main/docs/technical.md
|
|
@@ -59,7 +59,10 @@ from huggingface_hub import hf_hub_download
|
|
|
59
59
|
from segresnet_mlx.medmlx import run
|
|
60
60
|
|
|
61
61
|
bundle = "brats_mri_segmentation"
|
|
62
|
-
weights = hf_hub_download(
|
|
62
|
+
weights = hf_hub_download(
|
|
63
|
+
"MedMLX/segresnet-brats-mlx", "segresnet-brats.safetensors",
|
|
64
|
+
revision="14f9cda3649965729663c7365c49fd839c81a01b",
|
|
65
|
+
)
|
|
63
66
|
|
|
64
67
|
result = run(
|
|
65
68
|
bundle=bundle,
|
|
@@ -30,7 +30,10 @@ from huggingface_hub import hf_hub_download
|
|
|
30
30
|
from segresnet_mlx.medmlx import run
|
|
31
31
|
|
|
32
32
|
bundle = "brats_mri_segmentation"
|
|
33
|
-
weights = hf_hub_download(
|
|
33
|
+
weights = hf_hub_download(
|
|
34
|
+
"MedMLX/segresnet-brats-mlx", "segresnet-brats.safetensors",
|
|
35
|
+
revision="14f9cda3649965729663c7365c49fd839c81a01b",
|
|
36
|
+
)
|
|
34
37
|
|
|
35
38
|
result = run(
|
|
36
39
|
bundle=bundle,
|
|
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
|
|
|
21
21
|
from segresnet_mlx.weights import load_model as load_model
|
|
22
22
|
from segresnet_mlx.weights import required_shapes as required_shapes
|
|
23
23
|
|
|
24
|
-
__version__ = "0.2.
|
|
24
|
+
__version__ = "0.2.3"
|
|
25
25
|
|
|
26
26
|
_EXPORTS = {
|
|
27
27
|
"BUNDLE_CONFIGS": "segresnet_mlx.predictor",
|
|
@@ -292,16 +292,23 @@ def run(
|
|
|
292
292
|
``brats_mri_segmentation`` (``t1c_path``, ``t1_path``, ``t2_path``,
|
|
293
293
|
``flair_path``: co-registered MRI on one grid), or ``wholeBody_ct_segmentation``
|
|
294
294
|
/ ``wholeBody_ct_segmentation_lowres`` (``image_path``: one CT volume, segmented
|
|
295
|
-
at 1.5 mm or 3 mm respectively). ``weights_path`` is
|
|
296
|
-
``
|
|
297
|
-
|
|
295
|
+
at 1.5 mm or 3 mm respectively). ``weights_path`` is a native ``.safetensors``
|
|
296
|
+
file: ``segresnet-brats.safetensors`` from ``MedMLX/segresnet-brats-mlx`` for
|
|
297
|
+
BraTS, or ``segresnet-renal-cect.safetensors`` from
|
|
298
|
+
``MedMLX/segresnet-renal-cect-mlx`` for renal CECT. These published files need
|
|
299
|
+
no conversion. For other checkpoints, convert the original ``models/model.pt``
|
|
300
|
+
(``models/model_lowres.pt`` for lowres whole-body) with
|
|
301
|
+
:func:`segresnet_mlx.convert_checkpoint` and pass the resulting file.
|
|
298
302
|
|
|
299
303
|
Inputs must be finite 3D NIfTI volumes; multi-input bundles need identical shape
|
|
300
304
|
and affine (atol 1e-5). Writes ``<bundle>_mask.nii.gz`` in ``output_dir`` on the
|
|
301
305
|
first input's shape and reader-selected affine (see ``Volume``): renal labels 0-5
|
|
302
306
|
and whole-body labels 0-104 as float32, BraTS labels 0, 1, 2, 4 as uint8, as the
|
|
303
307
|
bundles' ``SaveImaged`` wrote them. An existing mask is replaced only when
|
|
304
|
-
``overwrite`` is true; output aliases of input images are
|
|
308
|
+
``overwrite`` is true; output aliases of input images are refused by a
|
|
309
|
+
preflight check. Spatial affine values are assumed to be in millimeters; the
|
|
310
|
+
NIfTI spatial-unit field is not checked or converted. Paths are not locked
|
|
311
|
+
against concurrent replacement after the alias check.
|
|
305
312
|
|
|
306
313
|
``arithmetic_profile="compensated-fp32"`` uses Metal float pairs for
|
|
307
314
|
convolution and GroupNorm accumulation, with float32 layer boundaries.
|
|
@@ -312,6 +319,27 @@ def run(
|
|
|
312
319
|
requires transpose-convolution upsampling and compares with the unchanged
|
|
313
320
|
original graph evaluated in float64 throughout its network and inferer.
|
|
314
321
|
|
|
322
|
+
Args:
|
|
323
|
+
bundle: ``brats_mri_segmentation``, ``renalStructures_CECT_segmentation``,
|
|
324
|
+
``wholeBody_ct_segmentation`` or ``wholeBody_ct_segmentation_lowres``.
|
|
325
|
+
weights_path: Path to the matching native ``.safetensors`` file, such as
|
|
326
|
+
``segresnet-brats.safetensors`` or ``segresnet-renal-cect.safetensors``.
|
|
327
|
+
Hugging Face cache file paths are accepted; directories and Torch ``.pt``
|
|
328
|
+
checkpoints are not.
|
|
329
|
+
output_dir: Directory for ``<bundle>_mask.nii.gz``; created if missing.
|
|
330
|
+
arterial_path: Renal arterial-phase 3D NIfTI; required for the renal bundle.
|
|
331
|
+
venous_path: Renal venous-phase 3D NIfTI on the arterial grid; required for renal.
|
|
332
|
+
excretory_path: Renal excretory-phase 3D NIfTI on the arterial grid; required
|
|
333
|
+
for renal.
|
|
334
|
+
t1c_path: BraTS contrast-enhanced T1 3D NIfTI; required for BraTS.
|
|
335
|
+
t1_path: BraTS T1 3D NIfTI on the T1c grid; required for BraTS.
|
|
336
|
+
t2_path: BraTS T2 3D NIfTI on the T1c grid; required for BraTS.
|
|
337
|
+
flair_path: BraTS FLAIR 3D NIfTI on the T1c grid; required for BraTS.
|
|
338
|
+
image_path: Single 3D CT NIfTI; required for either whole-body bundle.
|
|
339
|
+
overwrite: Replace an existing output mask when true; defaults to false.
|
|
340
|
+
arithmetic_profile: ``torch-order-fp32`` (default), ``compensated-fp32``,
|
|
341
|
+
or ``compensated-fp48``; the last supports only the renal bundle.
|
|
342
|
+
|
|
315
343
|
Returns:
|
|
316
344
|
``{"outputs": {"mask": path}, "details": {...}}``; ``details`` is described
|
|
317
345
|
by ``segresnet_mlx._records.Details`` and always has
|
|
@@ -534,7 +562,9 @@ def postprocess_wholebody(logits: Float32Array, inverse: SpatialInverse) -> Floa
|
|
|
534
562
|
|
|
535
563
|
|
|
536
564
|
def _weights_file(weights_path: str) -> Path:
|
|
537
|
-
|
|
565
|
+
# MLX identifies the file format from the supplied name. Hub cache paths are
|
|
566
|
+
# named .safetensors symlinks whose underlying content-addressed blobs have no suffix.
|
|
567
|
+
path = Path(weights_path).expanduser().absolute()
|
|
538
568
|
hint = (
|
|
539
569
|
"Convert the bundle's models/model.pt once with "
|
|
540
570
|
"segresnet_mlx.convert_checkpoint(model_pt, destination, spec=...) "
|
|
@@ -556,8 +586,9 @@ def _weights_file(weights_path: str) -> Path:
|
|
|
556
586
|
def load_inputs(roles: list[tuple[str, str | None]]) -> list[Volume]:
|
|
557
587
|
"""Load each ``(role, path)`` and require one shared voxel grid.
|
|
558
588
|
|
|
559
|
-
Every volume must match the first one's shape and affine (``atol=1e-5``)
|
|
560
|
-
|
|
589
|
+
Every volume must match the first one's shape and affine (``atol=1e-5``).
|
|
590
|
+
This check does not align mismatched inputs; bundle preprocessing may still
|
|
591
|
+
resample the admitted grid afterward.
|
|
561
592
|
"""
|
|
562
593
|
volumes = [load_volume(Path(str(path)).expanduser().resolve(), role) for role, path in roles]
|
|
563
594
|
reference = volumes[0]
|
|
@@ -573,7 +604,12 @@ def load_inputs(roles: list[tuple[str, str | None]]) -> list[Volume]:
|
|
|
573
604
|
|
|
574
605
|
|
|
575
606
|
def load_volume(path: Path, role: str) -> Volume:
|
|
576
|
-
"""Read
|
|
607
|
+
"""Read 3D voxels using the affine selected by the upstream nibabel reader.
|
|
608
|
+
|
|
609
|
+
Source voxels are checked for finiteness before conversion to float32; values
|
|
610
|
+
outside the float32 range can overflow during that conversion. Spatial units
|
|
611
|
+
and affine invertibility are not checked here.
|
|
612
|
+
"""
|
|
577
613
|
from segresnet_mlx._nifti import ImageFileError, nib
|
|
578
614
|
|
|
579
615
|
if not path.is_file():
|
|
@@ -21,7 +21,8 @@ def group_mean(values: Array, mx: MLX) -> Array:
|
|
|
21
21
|
"""Mean over the last axis of ``(N, G, S)`` values, summed in 256-element tiles.
|
|
22
22
|
|
|
23
23
|
Tiling bounds the float32 accumulation depth of a group that spans a whole
|
|
24
|
-
volume. Returns ``(N, G, 1)``. ``SegResNet``
|
|
24
|
+
volume. The last dimension must be nonempty. Returns ``(N, G, 1)``. ``SegResNet``
|
|
25
|
+
does not call this (its GroupNorm
|
|
25
26
|
comes from the ``medmlx_core`` operators); it is exercised by
|
|
26
27
|
``tests/test_equivalence.py``.
|
|
27
28
|
"""
|
|
@@ -34,16 +35,20 @@ def group_mean(values: Array, mx: MLX) -> Array:
|
|
|
34
35
|
class SegResNet:
|
|
35
36
|
"""Raw-logit forward pass for MONAI's 3-D, group-normalized SegResNet.
|
|
36
37
|
|
|
37
|
-
Tensors are channels-first ``(N, C, D, H, W)
|
|
38
|
+
Tensors are channels-first ``(N, C, D, H, W)``; the supported inference
|
|
39
|
+
contract uses float32 inputs and returned logits. This class checks rank,
|
|
40
|
+
channel count and spatial divisibility but does not itself check input dtype
|
|
41
|
+
or finiteness; the public patch and sliding-window helpers do.
|
|
38
42
|
``weights`` uses MONAI's state-dict keys and Torch parameter layouts
|
|
39
43
|
(convolution weights are ``(out, in, kD, kH, kW)``); ``required_shapes`` in
|
|
40
44
|
``segresnet_mlx.weights`` lists them. Inference only: dropout is omitted and
|
|
41
45
|
no activation is applied to the output.
|
|
42
46
|
|
|
43
|
-
``arithmetic_profile`` selects the accumulation scheme (see
|
|
47
|
+
``arithmetic_profile`` selects the accumulation scheme (see ``docs/technical.md``):
|
|
44
48
|
``"torch-order-fp32"`` (default), ``"compensated-fp32"``, or
|
|
45
49
|
``"compensated-fp48"``, which needs ``upsample_mode="deconv"`` and raises
|
|
46
|
-
``ValueError`` otherwise.
|
|
50
|
+
``ValueError`` otherwise. The first two profiles use float32 layer boundaries;
|
|
51
|
+
``compensated-fp48`` carries paired float32 values through the hidden layers.
|
|
47
52
|
"""
|
|
48
53
|
|
|
49
54
|
def __init__(
|
|
@@ -50,8 +50,9 @@ class SlidingWindowConfig:
|
|
|
50
50
|
def from_bundle_config(cls, config: InfererConfig | BundleConfig) -> SlidingWindowConfig:
|
|
51
51
|
"""Read the staged JSON's inferer section without evaluating bundle expressions.
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
``roi_size`` is required; the other listed keys default to MONAI's
|
|
54
|
+
values. Values are taken literally, so a reference such as
|
|
55
|
+
``@displayable_configs#sw_overlap`` is not resolved.
|
|
55
56
|
"""
|
|
56
57
|
inferer = cast(InfererConfig, config.get("inferer", config))
|
|
57
58
|
return cls(
|
|
@@ -6,7 +6,7 @@ import math
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from typing import Literal
|
|
8
8
|
|
|
9
|
-
# Accumulation scheme for convolution and GroupNorm; see
|
|
9
|
+
# Accumulation scheme for convolution and GroupNorm; see docs/technical.md for the three
|
|
10
10
|
# profiles. ``compensated-fp48`` additionally requires transpose-convolution upsampling.
|
|
11
11
|
type ArithmeticProfile = Literal["torch-order-fp32", "compensated-fp32", "compensated-fp48"]
|
|
12
12
|
|
|
@@ -10,7 +10,8 @@ bundle's ``MergeClassesd`` and the NIfTI writer. Spatial resampling follows MONA
|
|
|
10
10
|
as ``Spacing`` does and returned as float32.
|
|
11
11
|
|
|
12
12
|
Image arrays are channel-first float32 ``(C, *spatial)`` and affines are 4x4 float64
|
|
13
|
-
voxel-to-world matrices as stored in the NIfTI header
|
|
13
|
+
voxel-to-world matrices as stored in the NIfTI header. Bundle ``pixdim`` targets
|
|
14
|
+
are in mm; these functions do not inspect or convert the input's spatial units.
|
|
14
15
|
"""
|
|
15
16
|
|
|
16
17
|
from __future__ import annotations
|
|
@@ -373,7 +374,11 @@ def brats_labels(binary: Float32Array) -> npt.NDArray[np.int64]:
|
|
|
373
374
|
|
|
374
375
|
|
|
375
376
|
def write_nifti(path: Path, data: HostArray, affine: Float64Array, dtype: npt.DTypeLike) -> None:
|
|
376
|
-
"""
|
|
377
|
+
"""Cast and write a NIfTI with sform and qform code 1.
|
|
378
|
+
|
|
379
|
+
The sform stores the affine within NIfTI precision. The qform cannot
|
|
380
|
+
represent shear, which NiBabel removes when encoding that form.
|
|
381
|
+
"""
|
|
377
382
|
array = np.asarray(data).astype(dtype, copy=False)
|
|
378
383
|
matrix = to_affine_nd(3, affine)
|
|
379
384
|
image = nib.Nifti1Image(array, matrix)
|
|
@@ -78,6 +78,8 @@ def convert_state_dict(
|
|
|
78
78
|
|
|
79
79
|
``state`` is either a raw state dict or a checkpoint holding it under the
|
|
80
80
|
``"model"`` key. Arrays keep their Torch layout and are returned C-contiguous.
|
|
81
|
+
Validation checks the parameter inventory and values, not the checkpoint's
|
|
82
|
+
identity or origin; other checkpoints with the same topology are admissible.
|
|
81
83
|
|
|
82
84
|
Raises:
|
|
83
85
|
InvalidInputError: If a required parameter is missing, an unsupported one is
|
|
@@ -117,11 +119,26 @@ def convert_checkpoint(checkpoint_path: Path, destination: Path, *, spec: SegRes
|
|
|
117
119
|
|
|
118
120
|
The checkpoint is read with the shared loader (``torch.load`` with its default
|
|
119
121
|
``weights_only=True``, so Torch from the ``conversion`` extra is required) and
|
|
120
|
-
checked by ``convert_state_dict``.
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
checked by ``convert_state_dict``. ``destination`` must end in
|
|
123
|
+
``.safetensors``. Parent directories of ``destination`` are
|
|
124
|
+
created. An existing destination is replaced, except when it aliases the
|
|
125
|
+
source checkpoint. Alias checks occur before conversion and do not lock out
|
|
126
|
+
subsequent filesystem changes. MLX is also needed to write the file, so this
|
|
127
|
+
runs only on a supported Apple Silicon host.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
InvalidInputError: The destination has a different suffix or aliases the
|
|
131
|
+
source checkpoint, or checkpoint contents fail ``convert_state_dict``.
|
|
123
132
|
"""
|
|
124
133
|
|
|
134
|
+
if destination.suffix != ".safetensors":
|
|
135
|
+
raise InvalidInputError("Converted weights destination must end in .safetensors")
|
|
136
|
+
if checkpoint_path.resolve() == destination.resolve() or (
|
|
137
|
+
checkpoint_path.is_file()
|
|
138
|
+
and destination.is_file()
|
|
139
|
+
and checkpoint_path.samefile(destination)
|
|
140
|
+
):
|
|
141
|
+
raise InvalidInputError("Converted weights must not overwrite their source checkpoint")
|
|
125
142
|
mx = import_mlx()
|
|
126
143
|
converted = convert_state_dict(load_torch_checkpoint(checkpoint_path), spec)
|
|
127
144
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -141,8 +158,9 @@ def load_model(
|
|
|
141
158
|
"""Load converted float32 safetensors into a ``SegResNet`` for ``spec``.
|
|
142
159
|
|
|
143
160
|
Raises:
|
|
144
|
-
InvalidInputError: If the
|
|
161
|
+
InvalidInputError: If the loaded value is not a mapping, its keys or shapes differ
|
|
145
162
|
from ``required_shapes(spec)``, or a parameter is not finite float32.
|
|
163
|
+
RuntimeError: If MLX cannot open or decode the weights file.
|
|
146
164
|
"""
|
|
147
165
|
mx = import_mlx()
|
|
148
166
|
raw: object = mx.load(str(weights_path))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|