seqseg 2.1.4__tar.gz → 2.1.6__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.
- {seqseg-2.1.4 → seqseg-2.1.6}/PKG-INFO +2 -2
- {seqseg-2.1.4 → seqseg-2.1.6}/README.md +1 -1
- {seqseg-2.1.4 → seqseg-2.1.6}/pyproject.toml +1 -1
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/cli.py +14 -6
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/modules/sitk_functions.py +62 -4
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/pipeline/classic.py +30 -9
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/pipeline/plus.py +27 -6
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/pipeline/train.py +215 -48
- seqseg-2.1.6/seqseg/scripts/prepare_casx_dataset.py +437 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/tests/test_cli_helpers.py +31 -0
- seqseg-2.1.6/seqseg/tests/test_prepare_casx_dataset.py +45 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/tests/test_resample_spacing.py +36 -2
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/tests/test_train_pipeline.py +147 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg.egg-info/PKG-INFO +2 -2
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg.egg-info/SOURCES.txt +2 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/LICENSE +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/__init__.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/api.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/citations.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/config/__init__.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/config/aorta_tutorial.yaml +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/config/global.yaml +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/config/global_aorta.yaml +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/config/global_cereb.yaml +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/config/global_coro.yaml +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/config/global_debug.yaml +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/config/global_default.yaml +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/config/global_pulm.yaml +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/config/global_seg.yaml +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/config/global_test.yaml +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/config_models.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/modules/__init__.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/modules/assembly.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/modules/capping.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/modules/centerline.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/modules/datasets.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/modules/evaluation.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/modules/initialization.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/modules/local_assembly.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/modules/nnunet.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/modules/params.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/modules/prediction.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/modules/simvascular.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/modules/simvascular_contour.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/modules/sweep.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/modules/tracing.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/modules/tracing_functions.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/modules/vmtk_functions.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/modules/vtk_functions.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/parallel/old_auto_centerline.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/parallel/parallel_auto_centerline.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/parallel/parallel_auto_centerline2.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/parallel/parallel_auto_centerline3.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/parallel/parallel_auto_centerline4.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/pipeline/__init__.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/pipeline/config.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/pipeline/directories.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/pipeline/post.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/pipeline/single_trace.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/scripts/__init__.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/scripts/taubin_smooth.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/seqseg.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/seqseg_plus.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/tests/conftest.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/tests/test_api_entry.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/tests/test_centerline_get_end_points.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/tests/test_centerline_gradient.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/tests/test_datasets_paths.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/tests/test_nnunet2.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/tests/test_nnunet_device.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/tests/test_segmentation.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/tests/test_single_trace_staging.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/tests/test_start_seg.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/tests/test_user_paths.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/tests/test_vtk.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg/user_paths.py +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg.egg-info/dependency_links.txt +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg.egg-info/entry_points.txt +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg.egg-info/requires.txt +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/seqseg.egg-info/top_level.txt +0 -0
- {seqseg-2.1.4 → seqseg-2.1.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: seqseg
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.6
|
|
4
4
|
Summary: A deep learning-based medical image blood vessel tracking and segmentation tool.
|
|
5
5
|
Author-email: Numi Sveinsson Cepero <numi@berkeley.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -84,7 +84,7 @@ SeqSeg **2.0** refactors the package around a clearer CLI and a stable Python AP
|
|
|
84
84
|
| **`seqseg run plus batch`** | Global nnU-Net sweep, then SeqSeg (replaces monolithic `seqseg_plus` script flow) |
|
|
85
85
|
| **`seqseg init dataset`** | Scaffold `images/`, `centerlines/`, `truths/`, and template `seeds.json` |
|
|
86
86
|
| **`seqseg paths init` / `set` / `show`** | Save default nnU-Net / data / out dirs in `~/.seqseg/paths.yaml` |
|
|
87
|
-
| **`seqseg train prepare`** | Extract patches + build nnU-Net Dataset (needs `pip install "seqseg[train]"`) |
|
|
87
|
+
| **`seqseg train prepare`** | Extract patches or whole volumes + build nnU-Net Dataset (needs `pip install "seqseg[train]"`) |
|
|
88
88
|
| **`seqseg train nnunet`** | Run nnU-Net plan/preprocess and training |
|
|
89
89
|
| **`seqseg doctor`** | Check imports (SimpleITK, vtk, nnunetv2, scipy, optional sampler) and paths |
|
|
90
90
|
| **`seqseg config dump` / `fingerprint`** | Inspect or diff packaged YAML configs |
|
|
@@ -51,7 +51,7 @@ SeqSeg **2.0** refactors the package around a clearer CLI and a stable Python AP
|
|
|
51
51
|
| **`seqseg run plus batch`** | Global nnU-Net sweep, then SeqSeg (replaces monolithic `seqseg_plus` script flow) |
|
|
52
52
|
| **`seqseg init dataset`** | Scaffold `images/`, `centerlines/`, `truths/`, and template `seeds.json` |
|
|
53
53
|
| **`seqseg paths init` / `set` / `show`** | Save default nnU-Net / data / out dirs in `~/.seqseg/paths.yaml` |
|
|
54
|
-
| **`seqseg train prepare`** | Extract patches + build nnU-Net Dataset (needs `pip install "seqseg[train]"`) |
|
|
54
|
+
| **`seqseg train prepare`** | Extract patches or whole volumes + build nnU-Net Dataset (needs `pip install "seqseg[train]"`) |
|
|
55
55
|
| **`seqseg train nnunet`** | Run nnU-Net plan/preprocess and training |
|
|
56
56
|
| **`seqseg doctor`** | Check imports (SimpleITK, vtk, nnunetv2, scipy, optional sampler) and paths |
|
|
57
57
|
| **`seqseg config dump` / `fingerprint`** | Inspect or diff packaged YAML configs |
|
|
@@ -672,6 +672,7 @@ def _cmd_train_prepare(ns: argparse.Namespace) -> None:
|
|
|
672
672
|
yes=ns.yes,
|
|
673
673
|
verbose=ns.verbose,
|
|
674
674
|
img_ext=ns.img_ext,
|
|
675
|
+
global_volumes=ns.global_volumes,
|
|
675
676
|
)
|
|
676
677
|
except TrainDependencyError as e:
|
|
677
678
|
print(str(e), file=sys.stderr)
|
|
@@ -692,7 +693,8 @@ def _cmd_train_prepare(ns: argparse.Namespace) -> None:
|
|
|
692
693
|
sys.exit(1)
|
|
693
694
|
|
|
694
695
|
print("\nPrepare complete.")
|
|
695
|
-
|
|
696
|
+
kind = "Whole volumes" if ns.global_volumes else "Patches"
|
|
697
|
+
print(f" {kind}: {result.extracted_dir}")
|
|
696
698
|
for name, path in zip(result.dataset_names, result.dataset_dirs):
|
|
697
699
|
try:
|
|
698
700
|
ds_id = dataset_id_from_name(name)
|
|
@@ -990,13 +992,13 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
990
992
|
|
|
991
993
|
train_p = sub.add_parser(
|
|
992
994
|
"train",
|
|
993
|
-
help="Prepare patch datasets and train nnU-Net models
|
|
995
|
+
help="Prepare patch or whole-volume datasets and train nnU-Net models",
|
|
994
996
|
)
|
|
995
997
|
train_sub = train_p.add_subparsers(dest="train_cmd", required=True)
|
|
996
998
|
|
|
997
999
|
p_prep = train_sub.add_parser(
|
|
998
1000
|
"prepare",
|
|
999
|
-
help="
|
|
1001
|
+
help="Prepare patches or whole volumes and convert to nnU-Net DatasetXXX format",
|
|
1000
1002
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
1001
1003
|
)
|
|
1002
1004
|
p_prep.add_argument(
|
|
@@ -1012,7 +1014,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1012
1014
|
"-outdir",
|
|
1013
1015
|
default=None,
|
|
1014
1016
|
type=str,
|
|
1015
|
-
help="Directory for extracted patches "
|
|
1017
|
+
help="Directory for extracted patches or whole volumes "
|
|
1016
1018
|
"(default: seqseg paths outdir, else ./extracted_data/)",
|
|
1017
1019
|
)
|
|
1018
1020
|
p_prep.add_argument(
|
|
@@ -1078,15 +1080,21 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1078
1080
|
default=None,
|
|
1079
1081
|
)
|
|
1080
1082
|
p_prep.add_argument("--truth-regenerate", action="store_true")
|
|
1083
|
+
p_prep.add_argument(
|
|
1084
|
+
"--global-volumes",
|
|
1085
|
+
"--whole-volumes",
|
|
1086
|
+
action="store_true",
|
|
1087
|
+
help="Copy each case's full image and label instead of sampling patches",
|
|
1088
|
+
)
|
|
1081
1089
|
p_prep.add_argument(
|
|
1082
1090
|
"--skip-sample",
|
|
1083
1091
|
action="store_true",
|
|
1084
|
-
help="Only convert existing
|
|
1092
|
+
help="Only convert existing volumes in --outdir",
|
|
1085
1093
|
)
|
|
1086
1094
|
p_prep.add_argument(
|
|
1087
1095
|
"--skip-convert",
|
|
1088
1096
|
action="store_true",
|
|
1089
|
-
help="Only
|
|
1097
|
+
help="Only write volumes; skip nnU-Net Dataset conversion",
|
|
1090
1098
|
)
|
|
1091
1099
|
p_prep.add_argument(
|
|
1092
1100
|
"--also-test",
|
|
@@ -55,10 +55,10 @@ def copy_settings(img, ref_img):
|
|
|
55
55
|
return img
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
DEFAULT_MAX_RESAMPLE_VOXELS =
|
|
58
|
+
# Cap assembly upsampling (e.g. CENT_MAX_SPACING in mm applied with ``-unit cm``)
|
|
59
|
+
# before SimpleITK tries to allocate a multi-billion-voxel grid and the process
|
|
60
|
+
# is SIGKILL'd (exit 137). 500M float32 voxels is ~2 GB for one volume.
|
|
61
|
+
DEFAULT_MAX_RESAMPLE_VOXELS = 500_000_000
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
def resampled_grid_size(orig_size, orig_spacing, new_spacing):
|
|
@@ -74,6 +74,64 @@ def resampled_grid_size(orig_size, orig_spacing, new_spacing):
|
|
|
74
74
|
)
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
def fit_spacing_to_max_voxels(
|
|
78
|
+
orig_size,
|
|
79
|
+
orig_spacing,
|
|
80
|
+
target_spacing,
|
|
81
|
+
max_voxels=DEFAULT_MAX_RESAMPLE_VOXELS,
|
|
82
|
+
):
|
|
83
|
+
"""
|
|
84
|
+
Coarsen ``target_spacing`` so the resampled grid has at most ``max_voxels``.
|
|
85
|
+
|
|
86
|
+
Axes that are already at ``orig_spacing`` are left unchanged. Remaining
|
|
87
|
+
(finer) axes are scaled uniformly. Never coarsens past ``orig_spacing``.
|
|
88
|
+
|
|
89
|
+
Returns
|
|
90
|
+
-------
|
|
91
|
+
list[float] | None
|
|
92
|
+
Spacing to resample to, or ``None`` if no axis can be refined without
|
|
93
|
+
exceeding ``max_voxels`` (caller should skip resampling).
|
|
94
|
+
"""
|
|
95
|
+
if max_voxels is None:
|
|
96
|
+
return [float(s) for s in target_spacing]
|
|
97
|
+
|
|
98
|
+
orig_size = np.asarray(orig_size, dtype=np.float64).reshape(3)
|
|
99
|
+
orig_spacing = np.asarray(orig_spacing, dtype=np.float64).reshape(3)
|
|
100
|
+
target = np.asarray(target_spacing, dtype=np.float64).reshape(3)
|
|
101
|
+
max_voxels = int(max_voxels)
|
|
102
|
+
if max_voxels <= 0:
|
|
103
|
+
raise ValueError("max_voxels must be > 0")
|
|
104
|
+
|
|
105
|
+
def n_vox_for(spacing):
|
|
106
|
+
return int(np.prod(resampled_grid_size(orig_size, orig_spacing, spacing)))
|
|
107
|
+
|
|
108
|
+
n_vox = n_vox_for(target)
|
|
109
|
+
if n_vox <= max_voxels:
|
|
110
|
+
return target.tolist()
|
|
111
|
+
|
|
112
|
+
for _ in range(32):
|
|
113
|
+
finer = target < orig_spacing * (1.0 - 1e-12)
|
|
114
|
+
n_fine = int(np.count_nonzero(finer))
|
|
115
|
+
if n_fine == 0:
|
|
116
|
+
return None
|
|
117
|
+
k = (n_vox / float(max_voxels)) ** (1.0 / n_fine)
|
|
118
|
+
new_target = target.copy()
|
|
119
|
+
new_target[finer] = np.minimum(orig_spacing[finer], target[finer] * k)
|
|
120
|
+
if np.allclose(new_target, target):
|
|
121
|
+
new_target[finer] = np.minimum(
|
|
122
|
+
orig_spacing[finer], target[finer] * 1.001
|
|
123
|
+
)
|
|
124
|
+
if np.allclose(new_target, target):
|
|
125
|
+
return None
|
|
126
|
+
target = new_target
|
|
127
|
+
n_vox = n_vox_for(target)
|
|
128
|
+
if n_vox <= max_voxels:
|
|
129
|
+
if not np.any(target < orig_spacing):
|
|
130
|
+
return None
|
|
131
|
+
return target.tolist()
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
77
135
|
def resample_to_spacing(image, new_spacing, is_label=False,
|
|
78
136
|
max_voxels=DEFAULT_MAX_RESAMPLE_VOXELS):
|
|
79
137
|
"""
|
|
@@ -175,17 +175,38 @@ def run_classic_batch(
|
|
|
175
175
|
assembly.GetSpacing(), cent_max_spacing, unit
|
|
176
176
|
)
|
|
177
177
|
if target_spacing is not None:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
178
|
+
fitted = sf.fit_spacing_to_max_voxels(
|
|
179
|
+
assembly.GetSize(),
|
|
180
|
+
assembly.GetSpacing(),
|
|
181
|
+
target_spacing,
|
|
181
182
|
)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
183
|
+
if fitted is None:
|
|
184
|
+
print(
|
|
185
|
+
"Skipping assembly upsample: even a coarsened grid "
|
|
186
|
+
f"would exceed {sf.DEFAULT_MAX_RESAMPLE_VOXELS:,} voxels"
|
|
187
|
+
)
|
|
188
|
+
else:
|
|
189
|
+
if any(
|
|
190
|
+
abs(a - b) > 1e-12
|
|
191
|
+
for a, b in zip(fitted, target_spacing)
|
|
192
|
+
):
|
|
193
|
+
print(
|
|
194
|
+
"Relaxed assembly spacing from "
|
|
195
|
+
f"{target_spacing} to {fitted} to stay <= "
|
|
196
|
+
f"{sf.DEFAULT_MAX_RESAMPLE_VOXELS:,} voxels"
|
|
197
|
+
)
|
|
198
|
+
target_spacing = fitted
|
|
199
|
+
print(
|
|
200
|
+
f"Resampling final assembly to spacing <= "
|
|
201
|
+
f"{float(cent_max_spacing)} mm: {target_spacing}"
|
|
202
|
+
)
|
|
203
|
+
assembly = sf.resample_to_spacing(
|
|
204
|
+
assembly, target_spacing, is_label=False
|
|
188
205
|
)
|
|
206
|
+
if write_samples:
|
|
207
|
+
n_udpates = sf.resample_to_spacing(
|
|
208
|
+
n_udpates, target_spacing, is_label=True
|
|
209
|
+
)
|
|
189
210
|
|
|
190
211
|
assembly_binary = sitk.BinaryThreshold(
|
|
191
212
|
assembly,
|
|
@@ -206,13 +206,34 @@ def run_plus_batch(
|
|
|
206
206
|
assembly.GetSpacing(), cent_max_spacing, unit
|
|
207
207
|
)
|
|
208
208
|
if target_spacing is not None:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
assembly = sf.resample_to_spacing(
|
|
214
|
-
assembly, target_spacing, is_label=False
|
|
209
|
+
fitted = sf.fit_spacing_to_max_voxels(
|
|
210
|
+
assembly.GetSize(),
|
|
211
|
+
assembly.GetSpacing(),
|
|
212
|
+
target_spacing,
|
|
215
213
|
)
|
|
214
|
+
if fitted is None:
|
|
215
|
+
print(
|
|
216
|
+
"Skipping assembly upsample: even a coarsened grid "
|
|
217
|
+
f"would exceed {sf.DEFAULT_MAX_RESAMPLE_VOXELS:,} voxels"
|
|
218
|
+
)
|
|
219
|
+
else:
|
|
220
|
+
if any(
|
|
221
|
+
abs(a - b) > 1e-12
|
|
222
|
+
for a, b in zip(fitted, target_spacing)
|
|
223
|
+
):
|
|
224
|
+
print(
|
|
225
|
+
"Relaxed assembly spacing from "
|
|
226
|
+
f"{target_spacing} to {fitted} to stay <= "
|
|
227
|
+
f"{sf.DEFAULT_MAX_RESAMPLE_VOXELS:,} voxels"
|
|
228
|
+
)
|
|
229
|
+
target_spacing = fitted
|
|
230
|
+
print(
|
|
231
|
+
f"Resampling final assembly to spacing <= "
|
|
232
|
+
f"{float(cent_max_spacing)} mm: {target_spacing}"
|
|
233
|
+
)
|
|
234
|
+
assembly = sf.resample_to_spacing(
|
|
235
|
+
assembly, target_spacing, is_label=False
|
|
236
|
+
)
|
|
216
237
|
|
|
217
238
|
assembly_binary = sitk.BinaryThreshold(
|
|
218
239
|
assembly,
|
|
@@ -8,7 +8,9 @@ import subprocess
|
|
|
8
8
|
from contextlib import contextmanager
|
|
9
9
|
from dataclasses import dataclass
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Iterator, List, Optional, Sequence, Union
|
|
11
|
+
from typing import Any, Callable, Iterator, List, Optional, Sequence, Union
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
12
14
|
|
|
13
15
|
from seqseg.user_paths import apply_nnunet_env, ensure_nnunet_dirs, resolve_path
|
|
14
16
|
|
|
@@ -35,12 +37,99 @@ def _require_sampler():
|
|
|
35
37
|
return extract_patches, write_nnunet_dataset
|
|
36
38
|
|
|
37
39
|
|
|
40
|
+
def _require_gather_global_volumes():
|
|
41
|
+
try:
|
|
42
|
+
from vascular_segment_sampler.sampling import gather_global_volumes
|
|
43
|
+
except ImportError as e:
|
|
44
|
+
raise TrainDependencyError(
|
|
45
|
+
"Whole-volume dataset construction requires gather_global_volumes "
|
|
46
|
+
"from a recent vascular-segment-sampler.\n"
|
|
47
|
+
"Upgrade with:\n"
|
|
48
|
+
' pip install -U "seqseg[train]"\n'
|
|
49
|
+
"or:\n"
|
|
50
|
+
" pip install -U vascular-segment-sampler"
|
|
51
|
+
) from e
|
|
52
|
+
return gather_global_volumes
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _reduction_or_nan(arr: Any, fn: Callable[[Any], Any]) -> float:
|
|
56
|
+
"""``np.amin``/``mean`` on an empty crop raise; treat that as missing."""
|
|
57
|
+
arr = np.asarray(arr)
|
|
58
|
+
if arr.size == 0:
|
|
59
|
+
return float("nan")
|
|
60
|
+
return float(fn(arr))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _point_list(location: Any) -> list:
|
|
64
|
+
if hasattr(location, "tolist"):
|
|
65
|
+
return location.tolist()
|
|
66
|
+
return list(location)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _safe_add_image_stats(stats: dict, im_np: Any) -> dict:
|
|
70
|
+
stats.update(
|
|
71
|
+
{
|
|
72
|
+
"IM_MEAN": _reduction_or_nan(im_np, np.mean),
|
|
73
|
+
"IM_MIN": _reduction_or_nan(im_np, np.amin),
|
|
74
|
+
"IM_STD": _reduction_or_nan(im_np, np.std),
|
|
75
|
+
"IM_MAX": _reduction_or_nan(im_np, np.amax),
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
return stats
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _safe_add_local_stats(
|
|
82
|
+
stats: dict,
|
|
83
|
+
location: Any,
|
|
84
|
+
diff_cent: Any,
|
|
85
|
+
blood_np: Any,
|
|
86
|
+
ground_truth: Any,
|
|
87
|
+
means: Sequence,
|
|
88
|
+
removed_seg: Any,
|
|
89
|
+
im_np: Any,
|
|
90
|
+
O: int,
|
|
91
|
+
) -> tuple[dict, int]:
|
|
92
|
+
"""Same stats as the sampler, but empty labeled crops become NaN not errors."""
|
|
93
|
+
stats.update(
|
|
94
|
+
{
|
|
95
|
+
"DIFF_CENT": diff_cent,
|
|
96
|
+
"POINT_CENT": _point_list(location),
|
|
97
|
+
"BLOOD_MEAN": _reduction_or_nan(blood_np, np.mean),
|
|
98
|
+
"BLOOD_MIN": _reduction_or_nan(blood_np, np.amin),
|
|
99
|
+
"BLOOD_STD": _reduction_or_nan(blood_np, np.std),
|
|
100
|
+
"BLOOD_MAX": _reduction_or_nan(blood_np, np.amax),
|
|
101
|
+
"GT_MEAN": _reduction_or_nan(ground_truth, np.mean),
|
|
102
|
+
"GT_STD": _reduction_or_nan(ground_truth, np.std),
|
|
103
|
+
"GT_MAX": _reduction_or_nan(ground_truth, np.amax),
|
|
104
|
+
"GT_MIN": _reduction_or_nan(ground_truth, np.amin),
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
if len(means) != 1:
|
|
108
|
+
import SimpleITK as sitk
|
|
109
|
+
|
|
110
|
+
larg_np = sitk.GetArrayFromImage(removed_seg)
|
|
111
|
+
rem_np = np.asarray(im_np)[np.asarray(larg_np) > 0.1]
|
|
112
|
+
stats.update(
|
|
113
|
+
{
|
|
114
|
+
"LARGEST_MEAN": _reduction_or_nan(rem_np, np.mean),
|
|
115
|
+
"LARGEST_STD": _reduction_or_nan(rem_np, np.std),
|
|
116
|
+
"LARGEST_MAX": _reduction_or_nan(rem_np, np.amax),
|
|
117
|
+
"LARGEST_MIN": _reduction_or_nan(rem_np, np.amin),
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
O += 1
|
|
121
|
+
return stats, O
|
|
122
|
+
|
|
123
|
+
|
|
38
124
|
def _harden_sampler_vtk() -> None:
|
|
39
|
-
"""
|
|
125
|
+
"""Patch sampler helpers that break on common SeqSeg datasets.
|
|
40
126
|
|
|
41
127
|
Centerline .vtp files from SimVascular/Slicer often include string/abstract
|
|
42
128
|
arrays. ``GetArray(i)`` is None for those, which became
|
|
43
129
|
``'NoneType' object has no attribute 'GetDataType'``.
|
|
130
|
+
|
|
131
|
+
Crops with no labeled voxels used to raise
|
|
132
|
+
``zero-size array to reduction operation minimum`` in ``add_local_stats``.
|
|
44
133
|
"""
|
|
45
134
|
try:
|
|
46
135
|
import vascular_segment_sampler.sampling.extract as extract_mod
|
|
@@ -57,6 +146,13 @@ def _harden_sampler_vtk() -> None:
|
|
|
57
146
|
samp_fn.collect_arrays = collect_arrays
|
|
58
147
|
vf.convertPolyDataToImageData = convertPolyDataToImageData
|
|
59
148
|
|
|
149
|
+
if not getattr(samp_fn.add_local_stats, "_seqseg_hardened", False):
|
|
150
|
+
_safe_add_local_stats._seqseg_hardened = True # type: ignore[attr-defined]
|
|
151
|
+
samp_fn.add_local_stats = _safe_add_local_stats
|
|
152
|
+
if not getattr(samp_fn.add_image_stats, "_seqseg_hardened", False):
|
|
153
|
+
_safe_add_image_stats._seqseg_hardened = True # type: ignore[attr-defined]
|
|
154
|
+
samp_fn.add_image_stats = _safe_add_image_stats
|
|
155
|
+
|
|
60
156
|
orig_sort = getattr(samp_fn, "sort_centerline", None)
|
|
61
157
|
if orig_sort is not None and not getattr(orig_sort, "_seqseg_hardened", False):
|
|
62
158
|
|
|
@@ -268,11 +364,22 @@ def _preflight_training_data(
|
|
|
268
364
|
)
|
|
269
365
|
|
|
270
366
|
|
|
271
|
-
def
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
367
|
+
def _missing_volumes_error(
|
|
368
|
+
outdir: str,
|
|
369
|
+
modalities: Sequence[str],
|
|
370
|
+
*,
|
|
371
|
+
global_volumes: bool = False,
|
|
372
|
+
) -> RuntimeError:
|
|
373
|
+
if global_volumes:
|
|
374
|
+
lines = [
|
|
375
|
+
"Whole-volume gathering finished without writing any cases.",
|
|
376
|
+
f" outdir: {os.path.abspath(outdir)}",
|
|
377
|
+
]
|
|
378
|
+
else:
|
|
379
|
+
lines = [
|
|
380
|
+
"Patch extraction finished without writing any samples.",
|
|
381
|
+
f" outdir: {os.path.abspath(outdir)}",
|
|
382
|
+
]
|
|
276
383
|
for mod in modalities:
|
|
277
384
|
img_dir = os.path.join(outdir, f"{mod.lower()}_train")
|
|
278
385
|
mask_dir = os.path.join(outdir, f"{mod.lower()}_train_masks")
|
|
@@ -280,29 +387,47 @@ def _missing_patches_error(outdir: str, modalities: Sequence[str]) -> RuntimeErr
|
|
|
280
387
|
f" {mod}: {_count_volume_files(img_dir)} images in {img_dir}, "
|
|
281
388
|
f"{_count_volume_files(mask_dir)} masks in {mask_dir}"
|
|
282
389
|
)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
390
|
+
if global_volumes:
|
|
391
|
+
lines.extend(
|
|
392
|
+
[
|
|
393
|
+
"",
|
|
394
|
+
"Typical causes:",
|
|
395
|
+
" - Image extension mismatch (pass --img-ext, e.g. .nii.gz).",
|
|
396
|
+
" - Missing truths/ (or surfaces/ when using --truth-from-surface).",
|
|
397
|
+
" - All cases listed in outdir/done.txt, so they were skipped.",
|
|
398
|
+
]
|
|
399
|
+
)
|
|
400
|
+
else:
|
|
401
|
+
lines.extend(
|
|
402
|
+
[
|
|
403
|
+
"",
|
|
404
|
+
"vascular-segment-sampler always opens "
|
|
405
|
+
"{modality}_train_Sample_stats.csv at the end, even when no case "
|
|
406
|
+
"completed (so that FileNotFoundError is a symptom, not the cause).",
|
|
407
|
+
"Typical causes:",
|
|
408
|
+
" - Worker crashes with --num-cores > 1 (exceptions are not re-raised).",
|
|
409
|
+
" Re-run with --num-cores 1 to see the real error.",
|
|
410
|
+
" - Image extension mismatch (pass --img-ext, e.g. .nii.gz).",
|
|
411
|
+
" - Missing truths/ (or surfaces/ when using --truth-from-surface).",
|
|
412
|
+
" - All cases listed in outdir/done.txt, so they were skipped.",
|
|
413
|
+
]
|
|
414
|
+
)
|
|
297
415
|
return RuntimeError("\n".join(lines))
|
|
298
416
|
|
|
299
417
|
|
|
300
|
-
def
|
|
418
|
+
def _assert_volumes_written(
|
|
419
|
+
outdir: str,
|
|
420
|
+
modalities: Sequence[str],
|
|
421
|
+
*,
|
|
422
|
+
global_volumes: bool = False,
|
|
423
|
+
) -> None:
|
|
301
424
|
for mod in modalities:
|
|
302
425
|
img_dir = os.path.join(outdir, f"{mod.lower()}_train")
|
|
303
426
|
mask_dir = os.path.join(outdir, f"{mod.lower()}_train_masks")
|
|
304
427
|
if _count_volume_files(img_dir) == 0 or _count_volume_files(mask_dir) == 0:
|
|
305
|
-
raise
|
|
428
|
+
raise _missing_volumes_error(
|
|
429
|
+
outdir, modalities, global_volumes=global_volumes
|
|
430
|
+
)
|
|
306
431
|
|
|
307
432
|
|
|
308
433
|
@contextmanager
|
|
@@ -394,9 +519,13 @@ def prepare_training_dataset(
|
|
|
394
519
|
yes: bool = False,
|
|
395
520
|
verbose: bool = False,
|
|
396
521
|
img_ext: Optional[str] = None,
|
|
522
|
+
global_volumes: bool = False,
|
|
397
523
|
) -> PrepareResult:
|
|
398
524
|
"""
|
|
399
|
-
|
|
525
|
+
Prepare training volumes and convert them to nnU-Net raw datasets.
|
|
526
|
+
|
|
527
|
+
By default extracts SeqSeg-style patches. With ``global_volumes=True``,
|
|
528
|
+
copies each case's full image and label instead (``gather_global_volumes``).
|
|
400
529
|
|
|
401
530
|
Uses ``vascular-segment-sampler`` (``pip install seqseg[train]``).
|
|
402
531
|
"""
|
|
@@ -432,35 +561,70 @@ def prepare_training_dataset(
|
|
|
432
561
|
truth_from_surface=truth_from_surface,
|
|
433
562
|
)
|
|
434
563
|
print("=" * 72)
|
|
435
|
-
|
|
564
|
+
if global_volumes:
|
|
565
|
+
print("Gathering whole-case volumes (no patch sampling)")
|
|
566
|
+
else:
|
|
567
|
+
print("Extracting vascular segment patches")
|
|
436
568
|
print(f" data_dir: {data_dir}")
|
|
437
569
|
print(f" outdir: {outdir}")
|
|
438
570
|
print(f" IMG_EXT: {resolved_img_ext}")
|
|
439
571
|
print("=" * 72)
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
572
|
+
if global_volumes:
|
|
573
|
+
gather_global_volumes = _require_gather_global_volumes()
|
|
574
|
+
gather_global_volumes(
|
|
575
|
+
data_dir=data_dir,
|
|
576
|
+
outdir=outdir,
|
|
577
|
+
config=sampler_config,
|
|
578
|
+
perc_dataset=perc_dataset,
|
|
579
|
+
num_cores=num_cores,
|
|
580
|
+
start_from=start_from,
|
|
581
|
+
end_at=end_at,
|
|
582
|
+
testing=testing,
|
|
583
|
+
validation_prop=validation_prop,
|
|
584
|
+
modality=modality_arg,
|
|
585
|
+
truth_from_surface=truth_from_surface,
|
|
586
|
+
truth_target_spacing=(
|
|
587
|
+
list(truth_target_spacing)
|
|
588
|
+
if truth_target_spacing is not None
|
|
589
|
+
else None
|
|
590
|
+
),
|
|
591
|
+
truth_regenerate=truth_regenerate,
|
|
592
|
+
yes=yes,
|
|
593
|
+
verbose=verbose,
|
|
594
|
+
)
|
|
595
|
+
else:
|
|
596
|
+
_run_extract_patches(
|
|
597
|
+
extract_patches,
|
|
598
|
+
data_dir=data_dir,
|
|
599
|
+
outdir=outdir,
|
|
600
|
+
config=sampler_config,
|
|
601
|
+
perc_dataset=perc_dataset,
|
|
602
|
+
num_cores=num_cores,
|
|
603
|
+
start_from=start_from,
|
|
604
|
+
end_at=end_at,
|
|
605
|
+
testing=testing,
|
|
606
|
+
validation_prop=validation_prop,
|
|
607
|
+
max_samples=max_samples,
|
|
608
|
+
modality=modality_arg,
|
|
609
|
+
truth_from_surface=truth_from_surface,
|
|
610
|
+
truth_target_spacing=(
|
|
611
|
+
list(truth_target_spacing)
|
|
612
|
+
if truth_target_spacing is not None
|
|
613
|
+
else None
|
|
614
|
+
),
|
|
615
|
+
truth_regenerate=truth_regenerate,
|
|
616
|
+
yes=yes,
|
|
617
|
+
verbose=verbose,
|
|
618
|
+
)
|
|
619
|
+
_assert_volumes_written(
|
|
620
|
+
outdir, modalities, global_volumes=global_volumes
|
|
460
621
|
)
|
|
461
|
-
_assert_patches_extracted(outdir, modalities)
|
|
462
622
|
else:
|
|
463
|
-
print(
|
|
623
|
+
print(
|
|
624
|
+
"Skipping volume gathering (--skip-sample)"
|
|
625
|
+
if global_volumes
|
|
626
|
+
else "Skipping patch extraction (--skip-sample)"
|
|
627
|
+
)
|
|
464
628
|
|
|
465
629
|
dataset_dirs: List[str] = []
|
|
466
630
|
dataset_names: List[str] = []
|
|
@@ -474,7 +638,10 @@ def prepare_training_dataset(
|
|
|
474
638
|
os.makedirs(convert_outdir, exist_ok=True)
|
|
475
639
|
|
|
476
640
|
print("=" * 72)
|
|
477
|
-
print(
|
|
641
|
+
print(
|
|
642
|
+
f"Converting {'volumes' if global_volumes else 'patches'} "
|
|
643
|
+
f"to nnU-Net format under {convert_outdir}"
|
|
644
|
+
)
|
|
478
645
|
print("=" * 72)
|
|
479
646
|
|
|
480
647
|
# Dataset numbers must be unique per modality when converting several.
|