dcccpy 0.3.0a2__tar.gz → 0.3.0a4__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.
- {dcccpy-0.3.0a2 → dcccpy-0.3.0a4}/PKG-INFO +51 -15
- dcccpy-0.3.0a2/src/dcccpy.egg-info/PKG-INFO → dcccpy-0.3.0a4/README.md +42 -37
- {dcccpy-0.3.0a2 → dcccpy-0.3.0a4}/pyproject.toml +9 -9
- {dcccpy-0.3.0a2 → dcccpy-0.3.0a4}/src/dcccpy/__init__.py +5 -0
- {dcccpy-0.3.0a2 → dcccpy-0.3.0a4}/src/dcccpy/core.py +46 -0
- {dcccpy-0.3.0a2 → dcccpy-0.3.0a4}/src/dcccpy/runtime.py +1 -1
- dcccpy-0.3.0a4/src/dcccpy/segmentation.py +123 -0
- dcccpy-0.3.0a2/README.md → dcccpy-0.3.0a4/src/dcccpy.egg-info/PKG-INFO +73 -6
- {dcccpy-0.3.0a2 → dcccpy-0.3.0a4}/src/dcccpy.egg-info/SOURCES.txt +2 -0
- {dcccpy-0.3.0a2 → dcccpy-0.3.0a4}/src/dcccpy.egg-info/requires.txt +8 -8
- {dcccpy-0.3.0a2 → dcccpy-0.3.0a4}/tests/test_core.py +105 -7
- dcccpy-0.3.0a4/tests/test_seg_distribution.py +145 -0
- {dcccpy-0.3.0a2 → dcccpy-0.3.0a4}/MANIFEST.in +0 -0
- {dcccpy-0.3.0a2 → dcccpy-0.3.0a4}/setup.cfg +0 -0
- {dcccpy-0.3.0a2 → dcccpy-0.3.0a4}/setup.py +0 -0
- {dcccpy-0.3.0a2 → dcccpy-0.3.0a4}/src/dcccpy/cli.py +0 -0
- {dcccpy-0.3.0a2 → dcccpy-0.3.0a4}/src/dcccpy/py.typed +0 -0
- {dcccpy-0.3.0a2 → dcccpy-0.3.0a4}/src/dcccpy.egg-info/dependency_links.txt +0 -0
- {dcccpy-0.3.0a2 → dcccpy-0.3.0a4}/src/dcccpy.egg-info/entry_points.txt +0 -0
- {dcccpy-0.3.0a2 → dcccpy-0.3.0a4}/src/dcccpy.egg-info/top_level.txt +0 -0
- {dcccpy-0.3.0a2 → dcccpy-0.3.0a4}/tests/test_testpypi_centiloid.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dcccpy
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.0a4
|
|
4
4
|
Summary: Python wrapper for the DCCCcore PET biomarker command-line tool
|
|
5
5
|
Author: DCCCSlicer contributors
|
|
6
6
|
License-Expression: CC-BY-NC-ND-4.0
|
|
@@ -17,20 +17,50 @@ Requires-Python: >=3.9
|
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
Requires-Dist: nibabel>=5
|
|
19
19
|
Provides-Extra: linux-runtime
|
|
20
|
-
Requires-Dist: dcccpy-linux-runtime==0.2.
|
|
21
|
-
Requires-Dist: dcccpy-linux-arm64-runtime==0.2.
|
|
20
|
+
Requires-Dist: dcccpy-linux-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "x86_64") and extra == "linux-runtime"
|
|
21
|
+
Requires-Dist: dcccpy-linux-arm64-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "aarch64") and extra == "linux-runtime"
|
|
22
22
|
Provides-Extra: windows-runtime
|
|
23
|
-
Requires-Dist: dcccpy-windows-runtime==0.2.
|
|
23
|
+
Requires-Dist: dcccpy-windows-runtime==0.2.0a3; (sys_platform == "win32" and platform_machine == "AMD64") and extra == "windows-runtime"
|
|
24
24
|
Provides-Extra: macos-runtime
|
|
25
|
-
Requires-Dist: dcccpy-macos-runtime==0.2.
|
|
25
|
+
Requires-Dist: dcccpy-macos-runtime==0.2.0a3; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "macos-runtime"
|
|
26
26
|
Provides-Extra: runtime
|
|
27
|
-
Requires-Dist: dcccpy-linux-runtime==0.2.
|
|
28
|
-
Requires-Dist: dcccpy-linux-arm64-runtime==0.2.
|
|
29
|
-
Requires-Dist: dcccpy-windows-runtime==0.2.
|
|
30
|
-
Requires-Dist: dcccpy-macos-runtime==0.2.
|
|
27
|
+
Requires-Dist: dcccpy-linux-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "x86_64") and extra == "runtime"
|
|
28
|
+
Requires-Dist: dcccpy-linux-arm64-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "aarch64") and extra == "runtime"
|
|
29
|
+
Requires-Dist: dcccpy-windows-runtime==0.2.0a3; (sys_platform == "win32" and platform_machine == "AMD64") and extra == "runtime"
|
|
30
|
+
Requires-Dist: dcccpy-macos-runtime==0.2.0a3; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "runtime"
|
|
31
31
|
|
|
32
32
|
# dcccpy
|
|
33
33
|
|
|
34
|
+
### Native PET segmentation
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import dcccpy
|
|
38
|
+
result = dcccpy.seg("pet.nii.gz", "labels.nii.gz")
|
|
39
|
+
labels = result.load_output()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Requires a DCCCcore build containing the `seg` command and its bundled model.
|
|
43
|
+
The signed integer output matches the input PET's native shape and geometry.
|
|
44
|
+
The default `mode="freesurfer"` returns raw DKT/aseg IDs; `mode="training"`
|
|
45
|
+
returns IDs 0–95. `mode="neurosegmenter"` provides partial regional compatibility:
|
|
46
|
+
brainstem/pons IDs 45/46 are unavailable, so pons SUVR is unsupported.
|
|
47
|
+
Mirror TTA is enabled by default; `tta=False` explicitly changes that protocol.
|
|
48
|
+
Use `threads=4` to select the CPU thread count, `bundle=...` to override the model
|
|
49
|
+
directory, and `debug=True` to save intermediate grids and a transform receipt.
|
|
50
|
+
The approximately 120 MB segmentation model is excluded from PyPI wheels.
|
|
51
|
+
With a slim runtime, the first `seg()` call downloads the separately published
|
|
52
|
+
`DCCCseg-4.6.0-alpha.zip` Release asset, verifies the model SHA-256 and caches it
|
|
53
|
+
under the dcccpy cache directory. A full native installation reuses its bundled
|
|
54
|
+
model. Existing `DCCCPY_AUTO_DOWNLOAD=0` disables implicit downloads; provision
|
|
55
|
+
offline use with `dcccpy.download_seg_model()` or pass `bundle=` pointing to
|
|
56
|
+
the extracted `seg` directory. `DCCCPY_SEG_MODEL_URL` selects a mirror while
|
|
57
|
+
retaining checksum verification.
|
|
58
|
+
An explicit `config=` delegates model selection to that configuration and
|
|
59
|
+
suppresses default-model provisioning. With a slim runtime and a configuration
|
|
60
|
+
that only changes other settings, also pass `bundle=dcccpy.download_seg_model()`.
|
|
61
|
+
An optional `rigid_header=...` must retain the exact native PET values and record
|
|
62
|
+
exactly two rigid steps. Existing output images are refused by the core.
|
|
63
|
+
|
|
34
64
|
`dcccpy` is a Python wrapper for the `DCCCcore` command-line tool from
|
|
35
65
|
DCCCSlicer. It keeps the native C++ core as the execution engine and provides a
|
|
36
66
|
small Python API for running common PET biomarker workflows.
|
|
@@ -147,6 +177,7 @@ dcccpy.adni_pet_core(
|
|
|
147
177
|
"standardized.nii.gz",
|
|
148
178
|
tracer="fdg",
|
|
149
179
|
level=(1, 3),
|
|
180
|
+
deface=True,
|
|
150
181
|
)
|
|
151
182
|
```
|
|
152
183
|
|
|
@@ -155,9 +186,14 @@ Level 3 is the standardized ADNI-style image. The output argument names the
|
|
|
155
186
|
highest selected level, while selected lower levels receive `_Coreg` or
|
|
156
187
|
`_Coreg_Avg` suffixes.
|
|
157
188
|
|
|
158
|
-
`
|
|
159
|
-
|
|
160
|
-
|
|
189
|
+
Set `deface=True` to zero facial voxels in every selected output. Motion and
|
|
190
|
+
spatial transforms are estimated from the original PET before the privacy mask
|
|
191
|
+
is applied, so the flag does not change the estimated alignment.
|
|
192
|
+
|
|
193
|
+
`adni_pet_core` deliberately has no default tracer. Use `abeta`, `tau`, `fdg`,
|
|
194
|
+
or `dat`; FDG selects ADNI's iterative masked global-mean normalization,
|
|
195
|
+
DAT selects PPMI-style occipital-reference normalization, and Aβ/tau retain
|
|
196
|
+
cerebellar gray normalization.
|
|
161
197
|
|
|
162
198
|
Downloaded ADNI preprocessed FDG DICOMs can differ slightly from the exact
|
|
163
199
|
published iteration. See the repository's
|
|
@@ -206,7 +242,7 @@ the calculation again.
|
|
|
206
242
|
Release wheels should vendor the matching `DCCCcore` runtime tree before build:
|
|
207
243
|
|
|
208
244
|
```bash
|
|
209
|
-
python scripts/vendor_dccccore.py --version 4.
|
|
245
|
+
python scripts/vendor_dccccore.py --version 4.6.0-alpha --release-platform ubuntu-latest-x64
|
|
210
246
|
python -m build --wheel
|
|
211
247
|
```
|
|
212
248
|
|
|
@@ -214,7 +250,7 @@ The Linux ARM64 runtime package uses the corresponding release asset:
|
|
|
214
250
|
|
|
215
251
|
```bash
|
|
216
252
|
cd python/dcccpy-linux-arm64-runtime
|
|
217
|
-
python scripts/vendor_dccccore.py --version 4.
|
|
253
|
+
python scripts/vendor_dccccore.py --version 4.6.0-alpha --release-platform ubuntu-latest-arm64
|
|
218
254
|
python -m build --wheel
|
|
219
255
|
```
|
|
220
256
|
|
|
@@ -232,7 +268,7 @@ The preferred distribution layout is:
|
|
|
232
268
|
|
|
233
269
|
## Packaging note
|
|
234
270
|
|
|
235
|
-
The current optional runtime wheels use a PyPI-size profile for DCCCcore version 4.
|
|
271
|
+
The current optional runtime wheels use a PyPI-size profile for DCCCcore version 4.6.0-alpha. They omit the
|
|
236
272
|
`fast_and_acc` registration model/config and the ADAD decoupler ONNX ensemble,
|
|
237
273
|
while keeping the default spatial normalization model and assets needed by
|
|
238
274
|
common Centiloid/CenTauR/CenTauRz workflows.
|
|
@@ -1,36 +1,35 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: dcccpy
|
|
3
|
-
Version: 0.3.0a2
|
|
4
|
-
Summary: Python wrapper for the DCCCcore PET biomarker command-line tool
|
|
5
|
-
Author: DCCCSlicer contributors
|
|
6
|
-
License-Expression: CC-BY-NC-ND-4.0
|
|
7
|
-
Project-URL: Homepage, https://github.com/tctco/DCCCSlicer
|
|
8
|
-
Project-URL: Repository, https://github.com/tctco/DCCCSlicer
|
|
9
|
-
Project-URL: Issues, https://github.com/tctco/DCCCSlicer/issues
|
|
10
|
-
Keywords: PET,neuroimaging,centiloid,DCCC,DCCCSlicer
|
|
11
|
-
Classifier: Development Status :: 3 - Alpha
|
|
12
|
-
Classifier: Intended Audience :: Science/Research
|
|
13
|
-
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
-
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
16
|
-
Requires-Python: >=3.9
|
|
17
|
-
Description-Content-Type: text/markdown
|
|
18
|
-
Requires-Dist: nibabel>=5
|
|
19
|
-
Provides-Extra: linux-runtime
|
|
20
|
-
Requires-Dist: dcccpy-linux-runtime==0.2.0a1; (sys_platform == "linux" and platform_machine == "x86_64") and extra == "linux-runtime"
|
|
21
|
-
Requires-Dist: dcccpy-linux-arm64-runtime==0.2.0a1; (sys_platform == "linux" and platform_machine == "aarch64") and extra == "linux-runtime"
|
|
22
|
-
Provides-Extra: windows-runtime
|
|
23
|
-
Requires-Dist: dcccpy-windows-runtime==0.2.0a1; (sys_platform == "win32" and platform_machine == "AMD64") and extra == "windows-runtime"
|
|
24
|
-
Provides-Extra: macos-runtime
|
|
25
|
-
Requires-Dist: dcccpy-macos-runtime==0.2.0a1; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "macos-runtime"
|
|
26
|
-
Provides-Extra: runtime
|
|
27
|
-
Requires-Dist: dcccpy-linux-runtime==0.2.0a1; (sys_platform == "linux" and platform_machine == "x86_64") and extra == "runtime"
|
|
28
|
-
Requires-Dist: dcccpy-linux-arm64-runtime==0.2.0a1; (sys_platform == "linux" and platform_machine == "aarch64") and extra == "runtime"
|
|
29
|
-
Requires-Dist: dcccpy-windows-runtime==0.2.0a1; (sys_platform == "win32" and platform_machine == "AMD64") and extra == "runtime"
|
|
30
|
-
Requires-Dist: dcccpy-macos-runtime==0.2.0a1; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "runtime"
|
|
31
|
-
|
|
32
1
|
# dcccpy
|
|
33
2
|
|
|
3
|
+
### Native PET segmentation
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
import dcccpy
|
|
7
|
+
result = dcccpy.seg("pet.nii.gz", "labels.nii.gz")
|
|
8
|
+
labels = result.load_output()
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires a DCCCcore build containing the `seg` command and its bundled model.
|
|
12
|
+
The signed integer output matches the input PET's native shape and geometry.
|
|
13
|
+
The default `mode="freesurfer"` returns raw DKT/aseg IDs; `mode="training"`
|
|
14
|
+
returns IDs 0–95. `mode="neurosegmenter"` provides partial regional compatibility:
|
|
15
|
+
brainstem/pons IDs 45/46 are unavailable, so pons SUVR is unsupported.
|
|
16
|
+
Mirror TTA is enabled by default; `tta=False` explicitly changes that protocol.
|
|
17
|
+
Use `threads=4` to select the CPU thread count, `bundle=...` to override the model
|
|
18
|
+
directory, and `debug=True` to save intermediate grids and a transform receipt.
|
|
19
|
+
The approximately 120 MB segmentation model is excluded from PyPI wheels.
|
|
20
|
+
With a slim runtime, the first `seg()` call downloads the separately published
|
|
21
|
+
`DCCCseg-4.6.0-alpha.zip` Release asset, verifies the model SHA-256 and caches it
|
|
22
|
+
under the dcccpy cache directory. A full native installation reuses its bundled
|
|
23
|
+
model. Existing `DCCCPY_AUTO_DOWNLOAD=0` disables implicit downloads; provision
|
|
24
|
+
offline use with `dcccpy.download_seg_model()` or pass `bundle=` pointing to
|
|
25
|
+
the extracted `seg` directory. `DCCCPY_SEG_MODEL_URL` selects a mirror while
|
|
26
|
+
retaining checksum verification.
|
|
27
|
+
An explicit `config=` delegates model selection to that configuration and
|
|
28
|
+
suppresses default-model provisioning. With a slim runtime and a configuration
|
|
29
|
+
that only changes other settings, also pass `bundle=dcccpy.download_seg_model()`.
|
|
30
|
+
An optional `rigid_header=...` must retain the exact native PET values and record
|
|
31
|
+
exactly two rigid steps. Existing output images are refused by the core.
|
|
32
|
+
|
|
34
33
|
`dcccpy` is a Python wrapper for the `DCCCcore` command-line tool from
|
|
35
34
|
DCCCSlicer. It keeps the native C++ core as the execution engine and provides a
|
|
36
35
|
small Python API for running common PET biomarker workflows.
|
|
@@ -147,6 +146,7 @@ dcccpy.adni_pet_core(
|
|
|
147
146
|
"standardized.nii.gz",
|
|
148
147
|
tracer="fdg",
|
|
149
148
|
level=(1, 3),
|
|
149
|
+
deface=True,
|
|
150
150
|
)
|
|
151
151
|
```
|
|
152
152
|
|
|
@@ -155,9 +155,14 @@ Level 3 is the standardized ADNI-style image. The output argument names the
|
|
|
155
155
|
highest selected level, while selected lower levels receive `_Coreg` or
|
|
156
156
|
`_Coreg_Avg` suffixes.
|
|
157
157
|
|
|
158
|
-
`
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
Set `deface=True` to zero facial voxels in every selected output. Motion and
|
|
159
|
+
spatial transforms are estimated from the original PET before the privacy mask
|
|
160
|
+
is applied, so the flag does not change the estimated alignment.
|
|
161
|
+
|
|
162
|
+
`adni_pet_core` deliberately has no default tracer. Use `abeta`, `tau`, `fdg`,
|
|
163
|
+
or `dat`; FDG selects ADNI's iterative masked global-mean normalization,
|
|
164
|
+
DAT selects PPMI-style occipital-reference normalization, and Aβ/tau retain
|
|
165
|
+
cerebellar gray normalization.
|
|
161
166
|
|
|
162
167
|
Downloaded ADNI preprocessed FDG DICOMs can differ slightly from the exact
|
|
163
168
|
published iteration. See the repository's
|
|
@@ -206,7 +211,7 @@ the calculation again.
|
|
|
206
211
|
Release wheels should vendor the matching `DCCCcore` runtime tree before build:
|
|
207
212
|
|
|
208
213
|
```bash
|
|
209
|
-
python scripts/vendor_dccccore.py --version 4.
|
|
214
|
+
python scripts/vendor_dccccore.py --version 4.6.0-alpha --release-platform ubuntu-latest-x64
|
|
210
215
|
python -m build --wheel
|
|
211
216
|
```
|
|
212
217
|
|
|
@@ -214,7 +219,7 @@ The Linux ARM64 runtime package uses the corresponding release asset:
|
|
|
214
219
|
|
|
215
220
|
```bash
|
|
216
221
|
cd python/dcccpy-linux-arm64-runtime
|
|
217
|
-
python scripts/vendor_dccccore.py --version 4.
|
|
222
|
+
python scripts/vendor_dccccore.py --version 4.6.0-alpha --release-platform ubuntu-latest-arm64
|
|
218
223
|
python -m build --wheel
|
|
219
224
|
```
|
|
220
225
|
|
|
@@ -232,7 +237,7 @@ The preferred distribution layout is:
|
|
|
232
237
|
|
|
233
238
|
## Packaging note
|
|
234
239
|
|
|
235
|
-
The current optional runtime wheels use a PyPI-size profile for DCCCcore version 4.
|
|
240
|
+
The current optional runtime wheels use a PyPI-size profile for DCCCcore version 4.6.0-alpha. They omit the
|
|
236
241
|
`fast_and_acc` registration model/config and the ADAD decoupler ONNX ensemble,
|
|
237
242
|
while keeping the default spatial normalization model and assets needed by
|
|
238
243
|
common Centiloid/CenTauR/CenTauRz workflows.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dcccpy"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.0a4"
|
|
8
8
|
description = "Python wrapper for the DCCCcore PET biomarker command-line tool"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -31,16 +31,16 @@ Issues = "https://github.com/tctco/DCCCSlicer/issues"
|
|
|
31
31
|
|
|
32
32
|
[project.optional-dependencies]
|
|
33
33
|
linux-runtime = [
|
|
34
|
-
"dcccpy-linux-runtime==0.2.
|
|
35
|
-
"dcccpy-linux-arm64-runtime==0.2.
|
|
34
|
+
"dcccpy-linux-runtime==0.2.0a3; sys_platform == 'linux' and platform_machine == 'x86_64'",
|
|
35
|
+
"dcccpy-linux-arm64-runtime==0.2.0a3; sys_platform == 'linux' and platform_machine == 'aarch64'",
|
|
36
36
|
]
|
|
37
|
-
windows-runtime = ["dcccpy-windows-runtime==0.2.
|
|
38
|
-
macos-runtime = ["dcccpy-macos-runtime==0.2.
|
|
37
|
+
windows-runtime = ["dcccpy-windows-runtime==0.2.0a3; sys_platform == 'win32' and platform_machine == 'AMD64'"]
|
|
38
|
+
macos-runtime = ["dcccpy-macos-runtime==0.2.0a3; sys_platform == 'darwin' and platform_machine == 'arm64'"]
|
|
39
39
|
runtime = [
|
|
40
|
-
"dcccpy-linux-runtime==0.2.
|
|
41
|
-
"dcccpy-linux-arm64-runtime==0.2.
|
|
42
|
-
"dcccpy-windows-runtime==0.2.
|
|
43
|
-
"dcccpy-macos-runtime==0.2.
|
|
40
|
+
"dcccpy-linux-runtime==0.2.0a3; sys_platform == 'linux' and platform_machine == 'x86_64'",
|
|
41
|
+
"dcccpy-linux-arm64-runtime==0.2.0a3; sys_platform == 'linux' and platform_machine == 'aarch64'",
|
|
42
|
+
"dcccpy-windows-runtime==0.2.0a3; sys_platform == 'win32' and platform_machine == 'AMD64'",
|
|
43
|
+
"dcccpy-macos-runtime==0.2.0a3; sys_platform == 'darwin' and platform_machine == 'arm64'",
|
|
44
44
|
]
|
|
45
45
|
|
|
46
46
|
[project.scripts]
|
|
@@ -15,12 +15,16 @@ from .core import (
|
|
|
15
15
|
parse_metrics,
|
|
16
16
|
pet_motion_correct,
|
|
17
17
|
rigid,
|
|
18
|
+
seg,
|
|
18
19
|
run,
|
|
19
20
|
suvr,
|
|
20
21
|
)
|
|
21
22
|
from .runtime import DCCCCORE_VERSION, DCCCcoreDownloadError, DCCCcoreNotFoundError, download_dccccore
|
|
23
|
+
from .segmentation import SegModelDownloadError, download_seg_model
|
|
22
24
|
|
|
23
25
|
__all__ = [
|
|
26
|
+
"SegModelDownloadError",
|
|
27
|
+
"download_seg_model",
|
|
24
28
|
"DCCCResult",
|
|
25
29
|
"DCCCCORE_VERSION",
|
|
26
30
|
"DCCCcoreDownloadError",
|
|
@@ -39,6 +43,7 @@ __all__ = [
|
|
|
39
43
|
"parse_metrics",
|
|
40
44
|
"pet_motion_correct",
|
|
41
45
|
"rigid",
|
|
46
|
+
"seg",
|
|
42
47
|
"run",
|
|
43
48
|
"suvr",
|
|
44
49
|
]
|
|
@@ -412,6 +412,7 @@ def adni_pet_core(
|
|
|
412
412
|
*,
|
|
413
413
|
tracer: str,
|
|
414
414
|
level: int | Sequence[int] = 3,
|
|
415
|
+
deface: bool = False,
|
|
415
416
|
**kwargs: object,
|
|
416
417
|
) -> DCCCResult:
|
|
417
418
|
"""Run tracer-specific ADNI PET Core preprocessing.
|
|
@@ -419,6 +420,7 @@ def adni_pet_core(
|
|
|
419
420
|
``level`` accepts one level or a sequence: 1 exports Coreg, 2 exports
|
|
420
421
|
Coreg/Avg, and 3 exports the standardized image. Multiple levels produce
|
|
421
422
|
multiple files while ``output`` identifies the deepest requested level.
|
|
423
|
+
Set ``deface=True`` to zero facial voxels after transform estimation.
|
|
422
424
|
"""
|
|
423
425
|
|
|
424
426
|
extra_args = list(kwargs.pop("extra_args", ()))
|
|
@@ -428,6 +430,8 @@ def adni_pet_core(
|
|
|
428
430
|
raise ValueError("level must contain one or more of: 1, 2, 3")
|
|
429
431
|
if levels != (3,):
|
|
430
432
|
extra_args.extend(["--level", *(str(value) for value in levels)])
|
|
433
|
+
if deface:
|
|
434
|
+
extra_args.append("--deface")
|
|
431
435
|
return _spatial_command("adni-pet-core", input, output, extra_args=extra_args, **kwargs)
|
|
432
436
|
|
|
433
437
|
|
|
@@ -439,6 +443,48 @@ def rigid(
|
|
|
439
443
|
return _spatial_command("rigid", input, output, **kwargs)
|
|
440
444
|
|
|
441
445
|
|
|
446
|
+
def seg(
|
|
447
|
+
input: object,
|
|
448
|
+
output: str | os.PathLike[str] | None = None,
|
|
449
|
+
*,
|
|
450
|
+
mode: str = "freesurfer",
|
|
451
|
+
threads: int = 4,
|
|
452
|
+
tta: bool = True,
|
|
453
|
+
bundle: str | os.PathLike[str] | None = None,
|
|
454
|
+
rigid_header: str | os.PathLike[str] | None = None,
|
|
455
|
+
debug: bool = False,
|
|
456
|
+
config: str | os.PathLike[str] | None = None,
|
|
457
|
+
check: bool = True,
|
|
458
|
+
executable: str | os.PathLike[str] | None = None,
|
|
459
|
+
) -> DCCCResult:
|
|
460
|
+
"""Segment one 3-D PET into native-space categorical labels.
|
|
461
|
+
|
|
462
|
+
``freesurfer`` emits raw DKT/aseg IDs, ``training`` IDs 0–95, and
|
|
463
|
+
``neurosegmenter`` a partial aggregation without brainstem/pons IDs 45/46.
|
|
464
|
+
Mirror TTA is enabled by default. ``rigid_header``, when supplied, must
|
|
465
|
+
retain the input's voxel values after exactly two rigid predictions.
|
|
466
|
+
"""
|
|
467
|
+
if mode not in ("freesurfer", "training", "neurosegmenter"):
|
|
468
|
+
raise ValueError("mode must be freesurfer, training, or neurosegmenter")
|
|
469
|
+
if isinstance(threads, bool) or not isinstance(threads, int) or threads < 1:
|
|
470
|
+
raise ValueError("threads must be a positive integer")
|
|
471
|
+
if bundle is None and config is None:
|
|
472
|
+
from .segmentation import default_seg_bundle
|
|
473
|
+
executable = dccccore_path(executable=executable)
|
|
474
|
+
bundle = default_seg_bundle(executable)
|
|
475
|
+
args: list[object] = ["--mode", mode, "--threads", threads]
|
|
476
|
+
if not tta:
|
|
477
|
+
args.append("--no-tta")
|
|
478
|
+
if debug:
|
|
479
|
+
args.append("--debug")
|
|
480
|
+
for flag, path in (("--bundle", bundle), ("--rigid-header", rigid_header),
|
|
481
|
+
("--config", config)):
|
|
482
|
+
if path is not None:
|
|
483
|
+
args.extend([flag, _resolve_user_path(path)])
|
|
484
|
+
return _spatial_command("seg", input, output, extra_args=args,
|
|
485
|
+
check=check, executable=executable)
|
|
486
|
+
|
|
487
|
+
|
|
442
488
|
def pet_motion_correct(
|
|
443
489
|
input: object,
|
|
444
490
|
output: str | os.PathLike[str] | None = None,
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Optional DCCC Seg model delivery, kept outside PyPI wheels."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import stat
|
|
10
|
+
import tempfile
|
|
11
|
+
import urllib.request
|
|
12
|
+
import zipfile
|
|
13
|
+
|
|
14
|
+
from .runtime import DCCCCORE_VERSION, RELEASE_REPO, auto_download_enabled, cache_root
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
SEG_MODEL_SHA256 = "8ea46dc8eff44e7a6c55b93ec9a353eb676692741d56a47b04c755a3c956faf6"
|
|
18
|
+
MODEL_FILES = ("dccc_seg.onnx", "manifest.json", "grid.json", "plans.json", "dataset.json",
|
|
19
|
+
"labels.json", "neurosegmenter_mapping.json", "validation.json")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SegModelDownloadError(RuntimeError):
|
|
23
|
+
"""The optional segmentation model could not be provisioned."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def seg_model_dir(version: str = DCCCCORE_VERSION) -> Path:
|
|
27
|
+
if not re.fullmatch(r"\d+\.\d+\.\d+(?:-[A-Za-z0-9.]+)?", version):
|
|
28
|
+
raise ValueError("Invalid segmentation model version")
|
|
29
|
+
return cache_root() / "models" / "seg" / version
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _complete(directory: Path) -> bool:
|
|
33
|
+
return all((directory / name).is_file() for name in MODEL_FILES)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _verified(directory: Path) -> bool:
|
|
37
|
+
if not _complete(directory):
|
|
38
|
+
return False
|
|
39
|
+
digest = hashlib.sha256()
|
|
40
|
+
with (directory / "dccc_seg.onnx").open("rb") as source:
|
|
41
|
+
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
|
42
|
+
digest.update(chunk)
|
|
43
|
+
return digest.hexdigest() == SEG_MODEL_SHA256
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def download_seg_model(*, version: str = DCCCCORE_VERSION, force: bool = False) -> Path:
|
|
47
|
+
"""Download and verify the optional model; return its cached bundle directory.
|
|
48
|
+
|
|
49
|
+
An explicit call provisions offline use even with DCCCPY_AUTO_DOWNLOAD=0.
|
|
50
|
+
DCCCPY_SEG_MODEL_URL can select a mirrored archive; the model SHA-256 is
|
|
51
|
+
still enforced. force=True replaces this version's model cache only.
|
|
52
|
+
"""
|
|
53
|
+
destination = seg_model_dir(version)
|
|
54
|
+
if not force and _verified(destination):
|
|
55
|
+
return destination
|
|
56
|
+
if destination.exists() and not force:
|
|
57
|
+
raise SegModelDownloadError("Cached segmentation model is incomplete or corrupt; "
|
|
58
|
+
"call download_seg_model(force=True) to repair it")
|
|
59
|
+
repo = os.environ.get("DCCCPY_RELEASE_REPO", RELEASE_REPO)
|
|
60
|
+
url = os.environ.get("DCCCPY_SEG_MODEL_URL") or (
|
|
61
|
+
f"https://github.com/{repo}/releases/download/v{version}/DCCCseg-{version}.zip")
|
|
62
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
try:
|
|
64
|
+
with tempfile.TemporaryDirectory(prefix=".seg-download-", dir=destination.parent) as temporary:
|
|
65
|
+
temporary = Path(temporary)
|
|
66
|
+
archive = temporary / "model.zip"
|
|
67
|
+
with urllib.request.urlopen(url, timeout=120) as response, archive.open("wb") as output:
|
|
68
|
+
# Bound compressed downloads as well as extracted size.
|
|
69
|
+
total = 0
|
|
70
|
+
while True:
|
|
71
|
+
chunk = response.read(1024 * 1024)
|
|
72
|
+
if not chunk:
|
|
73
|
+
break
|
|
74
|
+
total += len(chunk)
|
|
75
|
+
if total > 200 * 1024 * 1024:
|
|
76
|
+
raise SegModelDownloadError("Segmentation model archive exceeds size limit")
|
|
77
|
+
output.write(chunk)
|
|
78
|
+
with zipfile.ZipFile(archive) as source:
|
|
79
|
+
total = 0
|
|
80
|
+
for member in source.infolist():
|
|
81
|
+
path = PurePosixPath(member.filename)
|
|
82
|
+
if (path.is_absolute() or ".." in path.parts or "\\" in member.filename
|
|
83
|
+
or not path.parts or path.parts[0] != "seg"
|
|
84
|
+
or stat.S_ISLNK(member.external_attr >> 16)):
|
|
85
|
+
raise SegModelDownloadError("Unsafe segmentation model archive path")
|
|
86
|
+
total += member.file_size
|
|
87
|
+
if total > 200 * 1024 * 1024:
|
|
88
|
+
raise SegModelDownloadError("Segmentation model expands beyond size limit")
|
|
89
|
+
for name in MODEL_FILES:
|
|
90
|
+
output = temporary / "seg" / name
|
|
91
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
with source.open("seg/" + name) as handle, output.open("xb") as target:
|
|
93
|
+
shutil.copyfileobj(handle, target)
|
|
94
|
+
staged = temporary / "seg"
|
|
95
|
+
if not _verified(staged):
|
|
96
|
+
raise SegModelDownloadError("Segmentation model SHA-256 mismatch")
|
|
97
|
+
if force and destination.exists():
|
|
98
|
+
shutil.rmtree(destination)
|
|
99
|
+
try:
|
|
100
|
+
staged.rename(destination)
|
|
101
|
+
except OSError:
|
|
102
|
+
# Another process may have finished the same version concurrently.
|
|
103
|
+
if not _verified(destination):
|
|
104
|
+
raise
|
|
105
|
+
return destination
|
|
106
|
+
except SegModelDownloadError:
|
|
107
|
+
raise
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
raise SegModelDownloadError("Cannot download the DCCC Seg model; provide bundle= with "
|
|
110
|
+
"an extracted DCCCseg Release asset for offline use") from exc
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def default_seg_bundle(executable: Path) -> Path | None:
|
|
114
|
+
"""Return None for a full native installation, otherwise provision its model."""
|
|
115
|
+
if _complete(Path(executable).resolve().parent / "assets/models/seg"):
|
|
116
|
+
return None
|
|
117
|
+
destination = seg_model_dir()
|
|
118
|
+
if _verified(destination):
|
|
119
|
+
return destination
|
|
120
|
+
if not auto_download_enabled():
|
|
121
|
+
raise SegModelDownloadError("Segmentation model is absent and automatic download is disabled; "
|
|
122
|
+
"pass bundle= or call download_seg_model() explicitly")
|
|
123
|
+
return download_seg_model()
|
|
@@ -1,5 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dcccpy
|
|
3
|
+
Version: 0.3.0a4
|
|
4
|
+
Summary: Python wrapper for the DCCCcore PET biomarker command-line tool
|
|
5
|
+
Author: DCCCSlicer contributors
|
|
6
|
+
License-Expression: CC-BY-NC-ND-4.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/tctco/DCCCSlicer
|
|
8
|
+
Project-URL: Repository, https://github.com/tctco/DCCCSlicer
|
|
9
|
+
Project-URL: Issues, https://github.com/tctco/DCCCSlicer/issues
|
|
10
|
+
Keywords: PET,neuroimaging,centiloid,DCCC,DCCCSlicer
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: nibabel>=5
|
|
19
|
+
Provides-Extra: linux-runtime
|
|
20
|
+
Requires-Dist: dcccpy-linux-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "x86_64") and extra == "linux-runtime"
|
|
21
|
+
Requires-Dist: dcccpy-linux-arm64-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "aarch64") and extra == "linux-runtime"
|
|
22
|
+
Provides-Extra: windows-runtime
|
|
23
|
+
Requires-Dist: dcccpy-windows-runtime==0.2.0a3; (sys_platform == "win32" and platform_machine == "AMD64") and extra == "windows-runtime"
|
|
24
|
+
Provides-Extra: macos-runtime
|
|
25
|
+
Requires-Dist: dcccpy-macos-runtime==0.2.0a3; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "macos-runtime"
|
|
26
|
+
Provides-Extra: runtime
|
|
27
|
+
Requires-Dist: dcccpy-linux-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "x86_64") and extra == "runtime"
|
|
28
|
+
Requires-Dist: dcccpy-linux-arm64-runtime==0.2.0a3; (sys_platform == "linux" and platform_machine == "aarch64") and extra == "runtime"
|
|
29
|
+
Requires-Dist: dcccpy-windows-runtime==0.2.0a3; (sys_platform == "win32" and platform_machine == "AMD64") and extra == "runtime"
|
|
30
|
+
Requires-Dist: dcccpy-macos-runtime==0.2.0a3; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "runtime"
|
|
31
|
+
|
|
1
32
|
# dcccpy
|
|
2
33
|
|
|
34
|
+
### Native PET segmentation
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import dcccpy
|
|
38
|
+
result = dcccpy.seg("pet.nii.gz", "labels.nii.gz")
|
|
39
|
+
labels = result.load_output()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Requires a DCCCcore build containing the `seg` command and its bundled model.
|
|
43
|
+
The signed integer output matches the input PET's native shape and geometry.
|
|
44
|
+
The default `mode="freesurfer"` returns raw DKT/aseg IDs; `mode="training"`
|
|
45
|
+
returns IDs 0–95. `mode="neurosegmenter"` provides partial regional compatibility:
|
|
46
|
+
brainstem/pons IDs 45/46 are unavailable, so pons SUVR is unsupported.
|
|
47
|
+
Mirror TTA is enabled by default; `tta=False` explicitly changes that protocol.
|
|
48
|
+
Use `threads=4` to select the CPU thread count, `bundle=...` to override the model
|
|
49
|
+
directory, and `debug=True` to save intermediate grids and a transform receipt.
|
|
50
|
+
The approximately 120 MB segmentation model is excluded from PyPI wheels.
|
|
51
|
+
With a slim runtime, the first `seg()` call downloads the separately published
|
|
52
|
+
`DCCCseg-4.6.0-alpha.zip` Release asset, verifies the model SHA-256 and caches it
|
|
53
|
+
under the dcccpy cache directory. A full native installation reuses its bundled
|
|
54
|
+
model. Existing `DCCCPY_AUTO_DOWNLOAD=0` disables implicit downloads; provision
|
|
55
|
+
offline use with `dcccpy.download_seg_model()` or pass `bundle=` pointing to
|
|
56
|
+
the extracted `seg` directory. `DCCCPY_SEG_MODEL_URL` selects a mirror while
|
|
57
|
+
retaining checksum verification.
|
|
58
|
+
An explicit `config=` delegates model selection to that configuration and
|
|
59
|
+
suppresses default-model provisioning. With a slim runtime and a configuration
|
|
60
|
+
that only changes other settings, also pass `bundle=dcccpy.download_seg_model()`.
|
|
61
|
+
An optional `rigid_header=...` must retain the exact native PET values and record
|
|
62
|
+
exactly two rigid steps. Existing output images are refused by the core.
|
|
63
|
+
|
|
3
64
|
`dcccpy` is a Python wrapper for the `DCCCcore` command-line tool from
|
|
4
65
|
DCCCSlicer. It keeps the native C++ core as the execution engine and provides a
|
|
5
66
|
small Python API for running common PET biomarker workflows.
|
|
@@ -116,6 +177,7 @@ dcccpy.adni_pet_core(
|
|
|
116
177
|
"standardized.nii.gz",
|
|
117
178
|
tracer="fdg",
|
|
118
179
|
level=(1, 3),
|
|
180
|
+
deface=True,
|
|
119
181
|
)
|
|
120
182
|
```
|
|
121
183
|
|
|
@@ -124,9 +186,14 @@ Level 3 is the standardized ADNI-style image. The output argument names the
|
|
|
124
186
|
highest selected level, while selected lower levels receive `_Coreg` or
|
|
125
187
|
`_Coreg_Avg` suffixes.
|
|
126
188
|
|
|
127
|
-
`
|
|
128
|
-
|
|
129
|
-
|
|
189
|
+
Set `deface=True` to zero facial voxels in every selected output. Motion and
|
|
190
|
+
spatial transforms are estimated from the original PET before the privacy mask
|
|
191
|
+
is applied, so the flag does not change the estimated alignment.
|
|
192
|
+
|
|
193
|
+
`adni_pet_core` deliberately has no default tracer. Use `abeta`, `tau`, `fdg`,
|
|
194
|
+
or `dat`; FDG selects ADNI's iterative masked global-mean normalization,
|
|
195
|
+
DAT selects PPMI-style occipital-reference normalization, and Aβ/tau retain
|
|
196
|
+
cerebellar gray normalization.
|
|
130
197
|
|
|
131
198
|
Downloaded ADNI preprocessed FDG DICOMs can differ slightly from the exact
|
|
132
199
|
published iteration. See the repository's
|
|
@@ -175,7 +242,7 @@ the calculation again.
|
|
|
175
242
|
Release wheels should vendor the matching `DCCCcore` runtime tree before build:
|
|
176
243
|
|
|
177
244
|
```bash
|
|
178
|
-
python scripts/vendor_dccccore.py --version 4.
|
|
245
|
+
python scripts/vendor_dccccore.py --version 4.6.0-alpha --release-platform ubuntu-latest-x64
|
|
179
246
|
python -m build --wheel
|
|
180
247
|
```
|
|
181
248
|
|
|
@@ -183,7 +250,7 @@ The Linux ARM64 runtime package uses the corresponding release asset:
|
|
|
183
250
|
|
|
184
251
|
```bash
|
|
185
252
|
cd python/dcccpy-linux-arm64-runtime
|
|
186
|
-
python scripts/vendor_dccccore.py --version 4.
|
|
253
|
+
python scripts/vendor_dccccore.py --version 4.6.0-alpha --release-platform ubuntu-latest-arm64
|
|
187
254
|
python -m build --wheel
|
|
188
255
|
```
|
|
189
256
|
|
|
@@ -201,7 +268,7 @@ The preferred distribution layout is:
|
|
|
201
268
|
|
|
202
269
|
## Packaging note
|
|
203
270
|
|
|
204
|
-
The current optional runtime wheels use a PyPI-size profile for DCCCcore version 4.
|
|
271
|
+
The current optional runtime wheels use a PyPI-size profile for DCCCcore version 4.6.0-alpha. They omit the
|
|
205
272
|
`fast_and_acc` registration model/config and the ADAD decoupler ONNX ensemble,
|
|
206
273
|
while keeping the default spatial normalization model and assets needed by
|
|
207
274
|
common Centiloid/CenTauR/CenTauRz workflows.
|
|
@@ -7,6 +7,7 @@ src/dcccpy/cli.py
|
|
|
7
7
|
src/dcccpy/core.py
|
|
8
8
|
src/dcccpy/py.typed
|
|
9
9
|
src/dcccpy/runtime.py
|
|
10
|
+
src/dcccpy/segmentation.py
|
|
10
11
|
src/dcccpy.egg-info/PKG-INFO
|
|
11
12
|
src/dcccpy.egg-info/SOURCES.txt
|
|
12
13
|
src/dcccpy.egg-info/dependency_links.txt
|
|
@@ -14,4 +15,5 @@ src/dcccpy.egg-info/entry_points.txt
|
|
|
14
15
|
src/dcccpy.egg-info/requires.txt
|
|
15
16
|
src/dcccpy.egg-info/top_level.txt
|
|
16
17
|
tests/test_core.py
|
|
18
|
+
tests/test_seg_distribution.py
|
|
17
19
|
tests/test_testpypi_centiloid.py
|
|
@@ -3,31 +3,31 @@ nibabel>=5
|
|
|
3
3
|
[linux-runtime]
|
|
4
4
|
|
|
5
5
|
[linux-runtime:sys_platform == "linux" and platform_machine == "aarch64"]
|
|
6
|
-
dcccpy-linux-arm64-runtime==0.2.
|
|
6
|
+
dcccpy-linux-arm64-runtime==0.2.0a3
|
|
7
7
|
|
|
8
8
|
[linux-runtime:sys_platform == "linux" and platform_machine == "x86_64"]
|
|
9
|
-
dcccpy-linux-runtime==0.2.
|
|
9
|
+
dcccpy-linux-runtime==0.2.0a3
|
|
10
10
|
|
|
11
11
|
[macos-runtime]
|
|
12
12
|
|
|
13
13
|
[macos-runtime:sys_platform == "darwin" and platform_machine == "arm64"]
|
|
14
|
-
dcccpy-macos-runtime==0.2.
|
|
14
|
+
dcccpy-macos-runtime==0.2.0a3
|
|
15
15
|
|
|
16
16
|
[runtime]
|
|
17
17
|
|
|
18
18
|
[runtime:sys_platform == "darwin" and platform_machine == "arm64"]
|
|
19
|
-
dcccpy-macos-runtime==0.2.
|
|
19
|
+
dcccpy-macos-runtime==0.2.0a3
|
|
20
20
|
|
|
21
21
|
[runtime:sys_platform == "linux" and platform_machine == "aarch64"]
|
|
22
|
-
dcccpy-linux-arm64-runtime==0.2.
|
|
22
|
+
dcccpy-linux-arm64-runtime==0.2.0a3
|
|
23
23
|
|
|
24
24
|
[runtime:sys_platform == "linux" and platform_machine == "x86_64"]
|
|
25
|
-
dcccpy-linux-runtime==0.2.
|
|
25
|
+
dcccpy-linux-runtime==0.2.0a3
|
|
26
26
|
|
|
27
27
|
[runtime:sys_platform == "win32" and platform_machine == "AMD64"]
|
|
28
|
-
dcccpy-windows-runtime==0.2.
|
|
28
|
+
dcccpy-windows-runtime==0.2.0a3
|
|
29
29
|
|
|
30
30
|
[windows-runtime]
|
|
31
31
|
|
|
32
32
|
[windows-runtime:sys_platform == "win32" and platform_machine == "AMD64"]
|
|
33
|
-
dcccpy-windows-runtime==0.2.
|
|
33
|
+
dcccpy-windows-runtime==0.2.0a3
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import subprocess
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
import sys
|
|
6
7
|
import types
|
|
@@ -20,6 +21,11 @@ from dcccpy.runtime import (
|
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def make_fake_dccccore(tmp_path: Path) -> Path:
|
|
24
|
+
bundle = tmp_path / "assets/models/seg"
|
|
25
|
+
bundle.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
for name in ("dccc_seg.onnx", "manifest.json", "grid.json", "plans.json", "dataset.json",
|
|
27
|
+
"labels.json", "neurosegmenter_mapping.json", "validation.json"):
|
|
28
|
+
(bundle / name).write_bytes(b"test runtime resource")
|
|
23
29
|
exe = tmp_path / "DCCCcore"
|
|
24
30
|
exe.write_text(
|
|
25
31
|
"#!/usr/bin/env sh\n"
|
|
@@ -46,6 +52,66 @@ def test_parse_metrics() -> None:
|
|
|
46
52
|
assert metrics == {"fbp": 12.5, "SUVr": 1.23, "ADAD score": -0.2}
|
|
47
53
|
|
|
48
54
|
|
|
55
|
+
def test_seg_defaults_and_explicit_options(tmp_path, monkeypatch):
|
|
56
|
+
fake_args = tmp_path / "args.txt"
|
|
57
|
+
fake_exe = make_fake_dccccore(tmp_path)
|
|
58
|
+
monkeypatch.setenv("DCCCPY_FAKE_ARGS", str(fake_args))
|
|
59
|
+
result = dcccpy.seg("pet.nii", executable=fake_exe)
|
|
60
|
+
args = fake_args.read_text().splitlines()
|
|
61
|
+
assert args[0] == "seg"
|
|
62
|
+
assert args[args.index("--mode") + 1] == "freesurfer"
|
|
63
|
+
assert args[args.index("--threads") + 1] == "4"
|
|
64
|
+
assert "--no-tta" not in args
|
|
65
|
+
assert result.output.exists()
|
|
66
|
+
result = dcccpy.seg("pet.nii", tmp_path / "labels.nii.gz", mode="training",
|
|
67
|
+
threads=2, tta=False, bundle="model bundle",
|
|
68
|
+
rigid_header="rigid header.nii.gz", config="config.toml",
|
|
69
|
+
debug=True, executable=fake_exe)
|
|
70
|
+
args = fake_args.read_text().splitlines()
|
|
71
|
+
assert args[args.index("--mode") + 1] == "training"
|
|
72
|
+
assert args[args.index("--threads") + 1] == "2"
|
|
73
|
+
assert args[args.index("--bundle") + 1] == str(Path("model bundle").resolve())
|
|
74
|
+
assert args[args.index("--rigid-header") + 1] == str(Path("rigid header.nii.gz").resolve())
|
|
75
|
+
assert args[args.index("--config") + 1] == str(Path("config.toml").resolve())
|
|
76
|
+
assert "--debug" in args and "--no-tta" in args
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.parametrize("kwargs", [{"mode": "bogus"}, {"threads": 0},
|
|
80
|
+
{"threads": 1.5}, {"threads": True}])
|
|
81
|
+
def test_seg_rejects_bad_options(kwargs):
|
|
82
|
+
with pytest.raises(ValueError):
|
|
83
|
+
dcccpy.seg("pet.nii", **kwargs)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_seg_preserves_native_failure(tmp_path):
|
|
87
|
+
exe = make_fake_dccccore(tmp_path)
|
|
88
|
+
exe.write_text("#!/bin/sh\nprintf 'invalid segmentation model' >&2\nexit 7\n")
|
|
89
|
+
exe.chmod(0o755)
|
|
90
|
+
result = dcccpy.seg("pet.nii", check=False, executable=exe)
|
|
91
|
+
assert result.returncode == 7
|
|
92
|
+
assert "invalid segmentation model" in result.stderr
|
|
93
|
+
assert not result.output.exists()
|
|
94
|
+
with pytest.raises(subprocess.CalledProcessError) as error:
|
|
95
|
+
dcccpy.seg("pet.nii", executable=exe)
|
|
96
|
+
assert error.value.returncode == 7
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_seg_custom_config_does_not_provision_or_override_model(tmp_path, monkeypatch):
|
|
100
|
+
exe = make_fake_dccccore(tmp_path)
|
|
101
|
+
(tmp_path / "assets/models/seg/dccc_seg.onnx").unlink()
|
|
102
|
+
args_file = tmp_path / "args.txt"
|
|
103
|
+
monkeypatch.setenv("DCCCPY_FAKE_ARGS", str(args_file))
|
|
104
|
+
monkeypatch.setenv("DCCCPY_AUTO_DOWNLOAD", "0")
|
|
105
|
+
monkeypatch.setenv("DCCCPY_CACHE_DIR", str(tmp_path / "empty-cache"))
|
|
106
|
+
config = tmp_path / "custom.toml"
|
|
107
|
+
config.write_text('[models]\nseg = "offline/custom/dccc_seg.onnx"\n')
|
|
108
|
+
result = dcccpy.seg("pet.nii", config=config, executable=exe)
|
|
109
|
+
assert result.returncode == 0
|
|
110
|
+
args = args_file.read_text().splitlines()
|
|
111
|
+
assert args[args.index("--config") + 1] == str(config)
|
|
112
|
+
assert "--bundle" not in args
|
|
113
|
+
|
|
114
|
+
|
|
49
115
|
def test_centiloid_omits_output_by_creating_temp_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
50
116
|
fake_args = tmp_path / "args.txt"
|
|
51
117
|
fake_exe = make_fake_dccccore(tmp_path)
|
|
@@ -142,7 +208,7 @@ def test_dccccore_path_can_disable_auto_download(tmp_path: Path, monkeypatch: py
|
|
|
142
208
|
|
|
143
209
|
|
|
144
210
|
def test_dccccore_path_auto_downloads_to_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
145
|
-
asset_root = tmp_path / "asset" / "DCCCcore-4.
|
|
211
|
+
asset_root = tmp_path / "asset" / "DCCCcore-4.6.0-alpha-ubuntu-latest-x64"
|
|
146
212
|
asset_root.mkdir(parents=True)
|
|
147
213
|
exe = asset_root / "DCCCcore"
|
|
148
214
|
exe.write_text("#!/usr/bin/env sh\nprintf 'fake dccccore\\n'\n")
|
|
@@ -150,7 +216,7 @@ def test_dccccore_path_auto_downloads_to_cache(tmp_path: Path, monkeypatch: pyte
|
|
|
150
216
|
|
|
151
217
|
archive = tmp_path / "DCCCcore.zip"
|
|
152
218
|
with zipfile.ZipFile(archive, "w") as zf:
|
|
153
|
-
zf.write(exe, "DCCCcore-4.
|
|
219
|
+
zf.write(exe, "DCCCcore-4.6.0-alpha-ubuntu-latest-x64/DCCCcore")
|
|
154
220
|
|
|
155
221
|
monkeypatch.delenv("DCCCPY_DCCCCORE", raising=False)
|
|
156
222
|
monkeypatch.setenv("DCCCPY_AUTO_DOWNLOAD", "1")
|
|
@@ -175,7 +241,7 @@ def test_find_existing_dccccore_checks_macos_runtime_package(
|
|
|
175
241
|
exe.write_text("#!/usr/bin/env sh\n")
|
|
176
242
|
exe.chmod(0o755)
|
|
177
243
|
|
|
178
|
-
module = types.SimpleNamespace(DCCCCORE_VERSION="4.
|
|
244
|
+
module = types.SimpleNamespace(DCCCCORE_VERSION="4.6.0-alpha", dccccore_root=lambda: root)
|
|
179
245
|
monkeypatch.setitem(sys.modules, "dcccpy_macos_runtime", module)
|
|
180
246
|
monkeypatch.setenv("PATH", str(tmp_path / "empty-path"))
|
|
181
247
|
|
|
@@ -186,10 +252,10 @@ def test_linux_arm64_release_platform() -> None:
|
|
|
186
252
|
assert release_platform("linux-arm64") == "ubuntu-latest-arm64"
|
|
187
253
|
|
|
188
254
|
|
|
189
|
-
def
|
|
190
|
-
assert DCCCCORE_VERSION == "4.
|
|
255
|
+
def test_default_release_targets_dccccore_460_alpha() -> None:
|
|
256
|
+
assert DCCCCORE_VERSION == "4.6.0-alpha"
|
|
191
257
|
assert release_url(platform_name="ubuntu-latest-x64").endswith(
|
|
192
|
-
"/releases/download/v4.
|
|
258
|
+
"/releases/download/v4.6.0-alpha/DCCCcore-4.6.0-alpha-ubuntu-latest-x64.zip"
|
|
193
259
|
)
|
|
194
260
|
|
|
195
261
|
|
|
@@ -220,7 +286,7 @@ def test_find_existing_dccccore_checks_linux_arm64_runtime_package(
|
|
|
220
286
|
exe.write_text("#!/usr/bin/env sh\n")
|
|
221
287
|
exe.chmod(0o755)
|
|
222
288
|
|
|
223
|
-
module = types.SimpleNamespace(DCCCCORE_VERSION="4.
|
|
289
|
+
module = types.SimpleNamespace(DCCCCORE_VERSION="4.6.0-alpha", dccccore_root=lambda: root)
|
|
224
290
|
monkeypatch.setitem(sys.modules, "dcccpy_linux_arm64_runtime", module)
|
|
225
291
|
monkeypatch.setenv("PATH", str(tmp_path / "empty-path"))
|
|
226
292
|
|
|
@@ -347,6 +413,38 @@ def test_adni_pet_core_rejects_invalid_export_levels(level: object) -> None:
|
|
|
347
413
|
dcccpy.adni_pet_core("pet.nii", tracer="fdg", level=level) # type: ignore[arg-type]
|
|
348
414
|
|
|
349
415
|
|
|
416
|
+
def test_adni_pet_core_forwards_deface(
|
|
417
|
+
tmp_path: Path,
|
|
418
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
419
|
+
) -> None:
|
|
420
|
+
fake_args = tmp_path / "args.txt"
|
|
421
|
+
fake_exe = make_fake_dccccore(tmp_path)
|
|
422
|
+
input_path = tmp_path / "pet.nii"
|
|
423
|
+
output_path = tmp_path / "adni.nii"
|
|
424
|
+
input_path.write_text("fake PET")
|
|
425
|
+
monkeypatch.setenv("DCCCPY_DCCCCORE", str(fake_exe))
|
|
426
|
+
monkeypatch.setenv("DCCCPY_FAKE_ARGS", str(fake_args))
|
|
427
|
+
|
|
428
|
+
result = dcccpy.adni_pet_core(
|
|
429
|
+
input_path, output_path, tracer="fdg", level=(1, 3), deface=True
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
assert result.returncode == 0
|
|
433
|
+
assert fake_args.read_text().splitlines() == [
|
|
434
|
+
"adni-pet-core",
|
|
435
|
+
"--input",
|
|
436
|
+
str(input_path),
|
|
437
|
+
"--output",
|
|
438
|
+
str(output_path),
|
|
439
|
+
"--tracer",
|
|
440
|
+
"fdg",
|
|
441
|
+
"--level",
|
|
442
|
+
"1",
|
|
443
|
+
"3",
|
|
444
|
+
"--deface",
|
|
445
|
+
]
|
|
446
|
+
|
|
447
|
+
|
|
350
448
|
def test_run_adds_macos_security_hint_for_blocked_runtime(
|
|
351
449
|
tmp_path: Path,
|
|
352
450
|
monkeypatch: pytest.MonkeyPatch,
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import importlib.util
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import zipfile
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
FILES = ("dccc_seg.onnx", "manifest.json", "grid.json", "plans.json", "dataset.json",
|
|
12
|
+
"labels.json", "neurosegmenter_mapping.json", "validation.json")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def import_script(path):
|
|
16
|
+
spec = importlib.util.spec_from_file_location("release_script", path)
|
|
17
|
+
module = importlib.util.module_from_spec(spec)
|
|
18
|
+
spec.loader.exec_module(module)
|
|
19
|
+
return module
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def distribution(tmp_path, monkeypatch):
|
|
24
|
+
import dcccpy.segmentation as model
|
|
25
|
+
archive = tmp_path / "model.zip"
|
|
26
|
+
with zipfile.ZipFile(archive, "w") as z:
|
|
27
|
+
for name in FILES:
|
|
28
|
+
z.writestr("seg/" + name, b"model" if name.endswith(".onnx") else b"{}")
|
|
29
|
+
monkeypatch.setenv("DCCCPY_CACHE_DIR", str(tmp_path / "cache"))
|
|
30
|
+
monkeypatch.setenv("DCCCPY_SEG_MODEL_URL", archive.as_uri())
|
|
31
|
+
monkeypatch.setattr(model, "SEG_MODEL_SHA256", hashlib.sha256(b"model").hexdigest())
|
|
32
|
+
return model, archive
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_model_download_is_cached_outside_package(distribution):
|
|
36
|
+
model, archive = distribution
|
|
37
|
+
directory = model.download_seg_model()
|
|
38
|
+
assert directory.name == "4.6.0-alpha"
|
|
39
|
+
assert (directory / "dccc_seg.onnx").read_bytes() == b"model"
|
|
40
|
+
archive.unlink()
|
|
41
|
+
assert model.download_seg_model() == directory
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.mark.parametrize("kind", ["digest", "traversal", "missing"])
|
|
45
|
+
def test_model_download_rejects_bad_assets(distribution, kind, tmp_path):
|
|
46
|
+
model, archive = distribution
|
|
47
|
+
with zipfile.ZipFile(archive, "w") as z:
|
|
48
|
+
for name in FILES:
|
|
49
|
+
if kind == "missing" and name == "labels.json":
|
|
50
|
+
continue
|
|
51
|
+
z.writestr("seg/" + name, b"wrong" if kind == "digest" else
|
|
52
|
+
(b"model" if name.endswith(".onnx") else b"{}"))
|
|
53
|
+
if kind == "traversal":
|
|
54
|
+
z.writestr("../escaped", b"bad")
|
|
55
|
+
with pytest.raises(model.SegModelDownloadError):
|
|
56
|
+
model.download_seg_model()
|
|
57
|
+
assert not (tmp_path / "escaped").exists()
|
|
58
|
+
assert not model.seg_model_dir().exists()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_bundled_model_and_download_opt_out(distribution, monkeypatch, tmp_path):
|
|
62
|
+
model, archive = distribution
|
|
63
|
+
exe = tmp_path / "native" / "DCCCcore"
|
|
64
|
+
bundle = exe.parent / "assets/models/seg"
|
|
65
|
+
bundle.mkdir(parents=True)
|
|
66
|
+
for name in FILES:
|
|
67
|
+
(bundle / name).write_bytes(b"model" if name.endswith(".onnx") else b"{}")
|
|
68
|
+
monkeypatch.setenv("DCCCPY_AUTO_DOWNLOAD", "0")
|
|
69
|
+
assert model.default_seg_bundle(exe) is None
|
|
70
|
+
(bundle / "dccc_seg.onnx").unlink()
|
|
71
|
+
with pytest.raises(model.SegModelDownloadError, match="download"):
|
|
72
|
+
model.default_seg_bundle(exe)
|
|
73
|
+
cached = model.download_seg_model() # Explicit provisioning honors user's call.
|
|
74
|
+
assert model.default_seg_bundle(exe) == cached
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_native_symlink_reuses_real_bundled_model(distribution, monkeypatch, tmp_path):
|
|
78
|
+
model, _ = distribution
|
|
79
|
+
native = tmp_path / "native/DCCCcore"
|
|
80
|
+
bundle = native.parent / "assets/models/seg"
|
|
81
|
+
bundle.mkdir(parents=True)
|
|
82
|
+
native.write_text("native executable")
|
|
83
|
+
for name in FILES:
|
|
84
|
+
(bundle / name).write_bytes(b"model" if name.endswith(".onnx") else b"{}")
|
|
85
|
+
link = tmp_path / "bin/DCCCcore"
|
|
86
|
+
link.parent.mkdir()
|
|
87
|
+
link.symlink_to(native)
|
|
88
|
+
monkeypatch.setenv("DCCCPY_AUTO_DOWNLOAD", "0")
|
|
89
|
+
assert model.default_seg_bundle(link) is None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.mark.parametrize("package", ["linux", "linux-arm64", "macos", "windows"])
|
|
93
|
+
def test_every_runtime_profile_excludes_seg(tmp_path, package):
|
|
94
|
+
root = Path(__file__).resolve().parents[3]
|
|
95
|
+
vendor = import_script(root / f"python/dcccpy-{package}-runtime/scripts/vendor_dccccore.py")
|
|
96
|
+
for profile in vendor.EXCLUDE_PROFILES:
|
|
97
|
+
dest = tmp_path / profile
|
|
98
|
+
model = dest / "assets/models/seg/dccc_seg.onnx"
|
|
99
|
+
rigid = dest / "assets/models/registration/rigid.onnx"
|
|
100
|
+
model.parent.mkdir(parents=True)
|
|
101
|
+
rigid.parent.mkdir(parents=True)
|
|
102
|
+
model.write_bytes(b"bulky optional segmentation")
|
|
103
|
+
rigid.write_bytes(b"required rigid")
|
|
104
|
+
vendor.prune_profile(dest, profile)
|
|
105
|
+
assert not model.exists()
|
|
106
|
+
assert rigid.exists()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_model_release_zip_contains_runtime_controls(tmp_path):
|
|
110
|
+
root = Path(__file__).resolve().parents[3]
|
|
111
|
+
packer = import_script(root / "scripts/package-seg-model.py")
|
|
112
|
+
bundle = tmp_path / "source"
|
|
113
|
+
bundle.mkdir()
|
|
114
|
+
for name in FILES:
|
|
115
|
+
(bundle / name).write_bytes(b"model" if name.endswith(".onnx") else b"{}")
|
|
116
|
+
archive = packer.package_model(bundle, tmp_path / "dist", "4.6.0-alpha")
|
|
117
|
+
assert archive.name == "DCCCseg-4.6.0-alpha.zip"
|
|
118
|
+
with zipfile.ZipFile(archive) as z:
|
|
119
|
+
assert set(z.namelist()) == {"seg/" + name for name in FILES}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_model_release_preserves_reference_entrypoint(tmp_path):
|
|
123
|
+
root = Path(__file__).resolve().parents[3]
|
|
124
|
+
packer = import_script(root / "scripts/package-seg-model.py")
|
|
125
|
+
bundle = tmp_path / "source"
|
|
126
|
+
bundle.mkdir()
|
|
127
|
+
for name in FILES:
|
|
128
|
+
(bundle / name).write_bytes(b"model" if name.endswith(".onnx") else b"{}")
|
|
129
|
+
(bundle / "README.md").write_text("See dccc_seg.py for reference inference")
|
|
130
|
+
(bundle / "dccc_seg.py").write_text("# shipped reference entry point\n")
|
|
131
|
+
archive = packer.package_model(bundle, tmp_path / "dist", "4.6.0-alpha")
|
|
132
|
+
with zipfile.ZipFile(archive) as z:
|
|
133
|
+
assert z.read("seg/dccc_seg.py") == b"# shipped reference entry point\n"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_release_version_must_match_source():
|
|
137
|
+
root = Path(__file__).resolve().parents[3]
|
|
138
|
+
metadata = import_script(root / "scripts/release-metadata.py")
|
|
139
|
+
cmake = 'project(DCCCcore VERSION 4.6.0)\nset(DCCCCORE_VERSION_SUFFIX "-alpha")\n'
|
|
140
|
+
assert metadata.release_version(cmake, requested="4.6.0-alpha") == "4.6.0-alpha"
|
|
141
|
+
assert metadata.release_version(cmake, tag="v4.6.0-alpha") == "4.6.0-alpha"
|
|
142
|
+
with pytest.raises(ValueError):
|
|
143
|
+
metadata.release_version(cmake, requested="4.5.1-alpha")
|
|
144
|
+
with pytest.raises(ValueError):
|
|
145
|
+
metadata.release_version(cmake, tag="v4.6.0")
|
|
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
|