seqseg 2.1.2__tar.gz → 2.1.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.
- {seqseg-2.1.2 → seqseg-2.1.3}/PKG-INFO +1 -1
- {seqseg-2.1.2 → seqseg-2.1.3}/pyproject.toml +1 -1
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/cli.py +10 -1
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/pipeline/train.py +265 -3
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/tests/test_cli_helpers.py +2 -0
- seqseg-2.1.3/seqseg/tests/test_train_pipeline.py +288 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg.egg-info/PKG-INFO +1 -1
- seqseg-2.1.2/seqseg/tests/test_train_pipeline.py +0 -130
- {seqseg-2.1.2 → seqseg-2.1.3}/LICENSE +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/README.md +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/__init__.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/api.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/citations.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/config/__init__.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/config/aorta_tutorial.yaml +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/config/global.yaml +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/config/global_aorta.yaml +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/config/global_cereb.yaml +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/config/global_coro.yaml +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/config/global_debug.yaml +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/config/global_default.yaml +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/config/global_pulm.yaml +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/config/global_seg.yaml +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/config/global_test.yaml +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/config_models.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/modules/__init__.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/modules/assembly.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/modules/capping.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/modules/centerline.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/modules/datasets.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/modules/evaluation.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/modules/initialization.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/modules/local_assembly.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/modules/nnunet.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/modules/params.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/modules/prediction.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/modules/simvascular.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/modules/simvascular_contour.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/modules/sitk_functions.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/modules/sweep.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/modules/tracing.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/modules/tracing_functions.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/modules/vmtk_functions.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/modules/vtk_functions.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/parallel/old_auto_centerline.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/parallel/parallel_auto_centerline.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/parallel/parallel_auto_centerline2.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/parallel/parallel_auto_centerline3.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/parallel/parallel_auto_centerline4.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/pipeline/__init__.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/pipeline/classic.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/pipeline/config.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/pipeline/directories.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/pipeline/plus.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/pipeline/post.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/pipeline/single_trace.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/scripts/__init__.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/scripts/taubin_smooth.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/seqseg.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/seqseg_plus.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/tests/conftest.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/tests/test_api_entry.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/tests/test_centerline_get_end_points.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/tests/test_datasets_paths.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/tests/test_nnunet2.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/tests/test_nnunet_device.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/tests/test_segmentation.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/tests/test_single_trace_staging.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/tests/test_start_seg.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/tests/test_user_paths.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/tests/test_vtk.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg/user_paths.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg.egg-info/SOURCES.txt +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg.egg-info/dependency_links.txt +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg.egg-info/entry_points.txt +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg.egg-info/requires.txt +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/seqseg.egg-info/top_level.txt +0 -0
- {seqseg-2.1.2 → seqseg-2.1.3}/setup.cfg +0 -0
|
@@ -671,6 +671,7 @@ def _cmd_train_prepare(ns: argparse.Namespace) -> None:
|
|
|
671
671
|
also_test=ns.also_test,
|
|
672
672
|
yes=ns.yes,
|
|
673
673
|
verbose=ns.verbose,
|
|
674
|
+
img_ext=ns.img_ext,
|
|
674
675
|
)
|
|
675
676
|
except TrainDependencyError as e:
|
|
676
677
|
print(str(e), file=sys.stderr)
|
|
@@ -1038,6 +1039,14 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1038
1039
|
help="Write DatasetXXX_* here "
|
|
1039
1040
|
"(default: seqseg paths nnunet_raw / $nnUNet_raw; else --outdir)",
|
|
1040
1041
|
)
|
|
1042
|
+
p_prep.add_argument(
|
|
1043
|
+
"--img-ext",
|
|
1044
|
+
"-img_ext",
|
|
1045
|
+
default=None,
|
|
1046
|
+
type=str,
|
|
1047
|
+
help="Image/label suffix (.nii.gz, .mha, .nrrd, …). "
|
|
1048
|
+
"Default: detect from data_dir/images/; else sampler YAML IMG_EXT",
|
|
1049
|
+
)
|
|
1041
1050
|
p_prep.add_argument("--perc-dataset", default=1.0, type=float)
|
|
1042
1051
|
p_prep.add_argument("--num-cores", default=1, type=int)
|
|
1043
1052
|
p_prep.add_argument("--start-from", default=0, type=int)
|
|
@@ -1075,7 +1084,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1075
1084
|
)
|
|
1076
1085
|
p_prep.add_argument("--yes", action="store_true", help="Non-interactive confirms")
|
|
1077
1086
|
p_prep.add_argument("--verbose", action="store_true")
|
|
1078
|
-
p_prep.set_defaults(_handler=_cmd_train_prepare)
|
|
1087
|
+
p_prep.set_defaults(_handler=_cmd_train_prepare, img_ext=None)
|
|
1079
1088
|
|
|
1080
1089
|
p_nn = train_sub.add_parser(
|
|
1081
1090
|
"nnunet",
|
|
@@ -5,12 +5,15 @@ from __future__ import annotations
|
|
|
5
5
|
import os
|
|
6
6
|
import shutil
|
|
7
7
|
import subprocess
|
|
8
|
+
from contextlib import contextmanager
|
|
8
9
|
from dataclasses import dataclass
|
|
9
10
|
from pathlib import Path
|
|
10
|
-
from typing import List, Optional, Sequence, Union
|
|
11
|
+
from typing import Iterator, List, Optional, Sequence, Union
|
|
11
12
|
|
|
12
13
|
from seqseg.user_paths import apply_nnunet_env, ensure_nnunet_dirs, resolve_path
|
|
13
14
|
|
|
15
|
+
_VOLUME_EXTS = (".nii.gz", ".nrrd", ".nii", ".mha", ".mhd")
|
|
16
|
+
|
|
14
17
|
|
|
15
18
|
class TrainDependencyError(RuntimeError):
|
|
16
19
|
"""Raised when optional training dependencies are missing."""
|
|
@@ -48,6 +51,250 @@ def _dir_for_sampler(path: str) -> str:
|
|
|
48
51
|
return os.path.join(os.path.abspath(os.path.expanduser(path)), "")
|
|
49
52
|
|
|
50
53
|
|
|
54
|
+
def _is_missing_stats_csv(exc: BaseException) -> bool:
|
|
55
|
+
return isinstance(exc, FileNotFoundError) and "Sample_stats.csv" in str(exc)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _normalize_img_ext(ext: str) -> str:
|
|
59
|
+
ext = str(ext).strip()
|
|
60
|
+
if not ext.startswith("."):
|
|
61
|
+
ext = "." + ext
|
|
62
|
+
return ext
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _volume_ext_of(name: str) -> Optional[str]:
|
|
66
|
+
lower = name.lower()
|
|
67
|
+
for ext in _VOLUME_EXTS:
|
|
68
|
+
if lower.endswith(ext):
|
|
69
|
+
return ext
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _detect_img_ext(images_dir: Path) -> Optional[str]:
|
|
74
|
+
"""Pick the most common volume suffix under ``images/`` (``.nii.gz`` first)."""
|
|
75
|
+
if not images_dir.is_dir():
|
|
76
|
+
return None
|
|
77
|
+
counts: dict[str, int] = {}
|
|
78
|
+
for name in os.listdir(images_dir):
|
|
79
|
+
if name.startswith("."):
|
|
80
|
+
continue
|
|
81
|
+
ext = _volume_ext_of(name)
|
|
82
|
+
if ext:
|
|
83
|
+
counts[ext] = counts.get(ext, 0) + 1
|
|
84
|
+
if not counts:
|
|
85
|
+
return None
|
|
86
|
+
best = max(counts.values())
|
|
87
|
+
for ext in _VOLUME_EXTS:
|
|
88
|
+
if counts.get(ext) == best:
|
|
89
|
+
return ext
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _load_sampler_config(config: Union[str, dict]) -> dict:
|
|
94
|
+
if isinstance(config, dict):
|
|
95
|
+
return dict(config)
|
|
96
|
+
try:
|
|
97
|
+
from vascular_segment_sampler.sampling.extract import _load_config
|
|
98
|
+
|
|
99
|
+
return dict(_load_config(config))
|
|
100
|
+
except Exception: # noqa: BLE001 — tests and missing sampler YAML
|
|
101
|
+
return {"IMG_EXT": ".nrrd"}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _resolve_img_ext(
|
|
105
|
+
data_dir: str,
|
|
106
|
+
config: dict,
|
|
107
|
+
img_ext: Optional[str] = None,
|
|
108
|
+
) -> str:
|
|
109
|
+
if img_ext:
|
|
110
|
+
return _normalize_img_ext(img_ext)
|
|
111
|
+
detected = _detect_img_ext(Path(data_dir) / "images")
|
|
112
|
+
if detected:
|
|
113
|
+
return detected
|
|
114
|
+
return _normalize_img_ext(str(config.get("IMG_EXT") or ".nrrd"))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _stems_with_suffix(directory: Path, suffix: str) -> List[str]:
|
|
118
|
+
if not directory.is_dir():
|
|
119
|
+
return []
|
|
120
|
+
stems: List[str] = []
|
|
121
|
+
for name in os.listdir(directory):
|
|
122
|
+
if name.startswith("."):
|
|
123
|
+
continue
|
|
124
|
+
if name.endswith(suffix):
|
|
125
|
+
stems.append(name[: -len(suffix)])
|
|
126
|
+
return sorted(stems)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _count_volume_files(directory: str) -> int:
|
|
130
|
+
if not os.path.isdir(directory):
|
|
131
|
+
return 0
|
|
132
|
+
n = 0
|
|
133
|
+
for name in os.listdir(directory):
|
|
134
|
+
if name.startswith("."):
|
|
135
|
+
continue
|
|
136
|
+
if name.endswith(".nii.gz") or name.endswith(_VOLUME_EXTS[1:]):
|
|
137
|
+
n += 1
|
|
138
|
+
return n
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _preflight_training_data(
|
|
142
|
+
data_dir: str,
|
|
143
|
+
*,
|
|
144
|
+
img_ext: str,
|
|
145
|
+
truth_from_surface: bool,
|
|
146
|
+
) -> None:
|
|
147
|
+
"""Fail fast when the sampler would iterate zero usable cases."""
|
|
148
|
+
root = Path(data_dir)
|
|
149
|
+
images = root / "images"
|
|
150
|
+
centerlines = root / "centerlines"
|
|
151
|
+
missing = [
|
|
152
|
+
str(p)
|
|
153
|
+
for p, label in ((images, "images"), (centerlines, "centerlines"))
|
|
154
|
+
if not p.is_dir()
|
|
155
|
+
]
|
|
156
|
+
if missing:
|
|
157
|
+
raise FileNotFoundError(
|
|
158
|
+
"Training data_dir must contain images/ and centerlines/.\n"
|
|
159
|
+
f" data_dir: {root}\n"
|
|
160
|
+
" missing: " + ", ".join(missing)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
img_stems = _stems_with_suffix(images, img_ext)
|
|
164
|
+
if not img_stems:
|
|
165
|
+
present = sorted(
|
|
166
|
+
{
|
|
167
|
+
name
|
|
168
|
+
for name in os.listdir(images)
|
|
169
|
+
if not name.startswith(".")
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
preview = ", ".join(present[:8]) if present else "(empty)"
|
|
173
|
+
raise FileNotFoundError(
|
|
174
|
+
f"No images with IMG_EXT={img_ext!r} in {images}.\n"
|
|
175
|
+
f" files found: {preview}\n"
|
|
176
|
+
"Pass --img-ext (e.g. .nii.gz or .mha) or convert images."
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
cent_stems = _stems_with_suffix(centerlines, ".vtp")
|
|
180
|
+
if not cent_stems:
|
|
181
|
+
raise FileNotFoundError(
|
|
182
|
+
f"No .vtp centerlines in {centerlines}."
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
overlap = sorted(set(img_stems) & set(cent_stems))
|
|
186
|
+
if not overlap:
|
|
187
|
+
raise FileNotFoundError(
|
|
188
|
+
"No case has both an image and a matching centerline stem.\n"
|
|
189
|
+
f" image stems ({img_ext}): {img_stems[:8]}\n"
|
|
190
|
+
f" centerline stems (.vtp): {cent_stems[:8]}"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if truth_from_surface:
|
|
194
|
+
surfaces = root / "surfaces"
|
|
195
|
+
if not surfaces.is_dir():
|
|
196
|
+
raise FileNotFoundError(
|
|
197
|
+
"--truth-from-surface requires a surfaces/ folder under "
|
|
198
|
+
f"{root}."
|
|
199
|
+
)
|
|
200
|
+
surf_stems = _stems_with_suffix(surfaces, ".vtp") + _stems_with_suffix(
|
|
201
|
+
surfaces, ".stl"
|
|
202
|
+
)
|
|
203
|
+
if not surf_stems:
|
|
204
|
+
raise FileNotFoundError(
|
|
205
|
+
f"No .vtp/.stl surfaces in {surfaces}."
|
|
206
|
+
)
|
|
207
|
+
surf_overlap = sorted(set(overlap) & set(surf_stems))
|
|
208
|
+
if not surf_overlap:
|
|
209
|
+
raise FileNotFoundError(
|
|
210
|
+
"No case has an image, centerline, and surface with the same stem.\n"
|
|
211
|
+
f" usable image+centerline stems: {overlap[:8]}\n"
|
|
212
|
+
f" surface stems: {sorted(set(surf_stems))[:8]}"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _missing_patches_error(outdir: str, modalities: Sequence[str]) -> RuntimeError:
|
|
217
|
+
lines = [
|
|
218
|
+
"Patch extraction finished without writing any samples.",
|
|
219
|
+
f" outdir: {os.path.abspath(outdir)}",
|
|
220
|
+
]
|
|
221
|
+
for mod in modalities:
|
|
222
|
+
img_dir = os.path.join(outdir, f"{mod.lower()}_train")
|
|
223
|
+
mask_dir = os.path.join(outdir, f"{mod.lower()}_train_masks")
|
|
224
|
+
lines.append(
|
|
225
|
+
f" {mod}: {_count_volume_files(img_dir)} images in {img_dir}, "
|
|
226
|
+
f"{_count_volume_files(mask_dir)} masks in {mask_dir}"
|
|
227
|
+
)
|
|
228
|
+
lines.extend(
|
|
229
|
+
[
|
|
230
|
+
"",
|
|
231
|
+
"vascular-segment-sampler always opens "
|
|
232
|
+
"{modality}_train_Sample_stats.csv at the end, even when no case "
|
|
233
|
+
"completed (so that FileNotFoundError is a symptom, not the cause).",
|
|
234
|
+
"Typical causes:",
|
|
235
|
+
" - Worker crashes with --num-cores > 1 (exceptions are not re-raised).",
|
|
236
|
+
" Re-run with --num-cores 1 to see the real error.",
|
|
237
|
+
" - Image extension mismatch (pass --img-ext, e.g. .nii.gz).",
|
|
238
|
+
" - Missing truths/ (or surfaces/ when using --truth-from-surface).",
|
|
239
|
+
" - All cases listed in outdir/done.txt, so they were skipped.",
|
|
240
|
+
]
|
|
241
|
+
)
|
|
242
|
+
return RuntimeError("\n".join(lines))
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _assert_patches_extracted(outdir: str, modalities: Sequence[str]) -> None:
|
|
246
|
+
for mod in modalities:
|
|
247
|
+
img_dir = os.path.join(outdir, f"{mod.lower()}_train")
|
|
248
|
+
mask_dir = os.path.join(outdir, f"{mod.lower()}_train_masks")
|
|
249
|
+
if _count_volume_files(img_dir) == 0 or _count_volume_files(mask_dir) == 0:
|
|
250
|
+
raise _missing_patches_error(outdir, modalities)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@contextmanager
|
|
254
|
+
def _tolerate_missing_sampler_stats() -> Iterator[None]:
|
|
255
|
+
"""Skip the sampler's end-of-run CSV summary when no case wrote it."""
|
|
256
|
+
try:
|
|
257
|
+
import vascular_segment_sampler.sampling.extract as extract_mod
|
|
258
|
+
except ImportError:
|
|
259
|
+
yield
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
orig = getattr(extract_mod, "print_csv_stats", None)
|
|
263
|
+
if orig is None:
|
|
264
|
+
yield
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
def _safe(out_dir, global_config, modality):
|
|
268
|
+
suffix = global_config.get("OUTPUT_SUFFIX", "") or ""
|
|
269
|
+
if global_config.get("TESTING"):
|
|
270
|
+
csv_file = f"{modality}_test{suffix}_Sample_stats.csv"
|
|
271
|
+
else:
|
|
272
|
+
csv_file = f"{modality}_train{suffix}_Sample_stats.csv"
|
|
273
|
+
path = out_dir + csv_file
|
|
274
|
+
if not os.path.isfile(path):
|
|
275
|
+
print(
|
|
276
|
+
f"Warning: no sampler stats file at {path}; continuing."
|
|
277
|
+
)
|
|
278
|
+
return
|
|
279
|
+
orig(out_dir, global_config, modality)
|
|
280
|
+
|
|
281
|
+
extract_mod.print_csv_stats = _safe
|
|
282
|
+
try:
|
|
283
|
+
yield
|
|
284
|
+
finally:
|
|
285
|
+
extract_mod.print_csv_stats = orig
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _run_extract_patches(extract_patches, **kwargs) -> None:
|
|
289
|
+
try:
|
|
290
|
+
with _tolerate_missing_sampler_stats():
|
|
291
|
+
extract_patches(**kwargs)
|
|
292
|
+
except FileNotFoundError as e:
|
|
293
|
+
if not _is_missing_stats_csv(e):
|
|
294
|
+
raise
|
|
295
|
+
print(f"Warning: {e}")
|
|
296
|
+
|
|
297
|
+
|
|
51
298
|
def expected_nnunet_dataset_name(name: str, dataset_number: int, modality: str) -> str:
|
|
52
299
|
"""Match vascular-segment-sampler naming for DatasetXXX_* folders."""
|
|
53
300
|
if dataset_number < 10:
|
|
@@ -91,6 +338,7 @@ def prepare_training_dataset(
|
|
|
91
338
|
also_test: bool = False,
|
|
92
339
|
yes: bool = False,
|
|
93
340
|
verbose: bool = False,
|
|
341
|
+
img_ext: Optional[str] = None,
|
|
94
342
|
) -> PrepareResult:
|
|
95
343
|
"""
|
|
96
344
|
Extract SeqSeg-style patches and convert them to nnU-Net raw datasets.
|
|
@@ -118,14 +366,27 @@ def prepare_training_dataset(
|
|
|
118
366
|
modalities = _parse_modalities(modality)
|
|
119
367
|
modality_arg = ",".join(modalities)
|
|
120
368
|
|
|
369
|
+
sampler_config = _load_sampler_config(config)
|
|
370
|
+
resolved_img_ext = _resolve_img_ext(data_dir, sampler_config, img_ext)
|
|
371
|
+
sampler_config["IMG_EXT"] = resolved_img_ext
|
|
372
|
+
|
|
121
373
|
if not skip_sample:
|
|
374
|
+
_preflight_training_data(
|
|
375
|
+
data_dir,
|
|
376
|
+
img_ext=resolved_img_ext,
|
|
377
|
+
truth_from_surface=truth_from_surface,
|
|
378
|
+
)
|
|
122
379
|
print("=" * 72)
|
|
123
380
|
print("Extracting vascular segment patches")
|
|
381
|
+
print(f" data_dir: {data_dir}")
|
|
382
|
+
print(f" outdir: {outdir}")
|
|
383
|
+
print(f" IMG_EXT: {resolved_img_ext}")
|
|
124
384
|
print("=" * 72)
|
|
125
|
-
|
|
385
|
+
_run_extract_patches(
|
|
386
|
+
extract_patches,
|
|
126
387
|
data_dir=data_dir,
|
|
127
388
|
outdir=outdir,
|
|
128
|
-
config=
|
|
389
|
+
config=sampler_config,
|
|
129
390
|
perc_dataset=perc_dataset,
|
|
130
391
|
num_cores=num_cores,
|
|
131
392
|
start_from=start_from,
|
|
@@ -142,6 +403,7 @@ def prepare_training_dataset(
|
|
|
142
403
|
yes=yes,
|
|
143
404
|
verbose=verbose,
|
|
144
405
|
)
|
|
406
|
+
_assert_patches_extracted(outdir, modalities)
|
|
145
407
|
else:
|
|
146
408
|
print("Skipping patch extraction (--skip-sample)")
|
|
147
409
|
|
|
@@ -288,6 +288,7 @@ def test_parser_train_prepare():
|
|
|
288
288
|
assert ns.data_dir == "/tmp/data"
|
|
289
289
|
assert ns.name == "MYDATA"
|
|
290
290
|
assert ns.dataset_number == 999
|
|
291
|
+
assert ns.img_ext is None
|
|
291
292
|
|
|
292
293
|
|
|
293
294
|
def test_parser_paths_init():
|
|
@@ -349,6 +350,7 @@ def test_cmd_train_prepare_missing_dep(tmp_path, capsys):
|
|
|
349
350
|
also_test=False,
|
|
350
351
|
yes=False,
|
|
351
352
|
verbose=False,
|
|
353
|
+
img_ext=None,
|
|
352
354
|
)
|
|
353
355
|
with patch(
|
|
354
356
|
"seqseg.cli.prepare_training_dataset",
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Unit tests for seqseg.pipeline.train helpers (no GPU / sampler required)."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from seqseg.pipeline.train import (
|
|
12
|
+
TrainDependencyError,
|
|
13
|
+
_detect_img_ext,
|
|
14
|
+
_dir_for_sampler,
|
|
15
|
+
_normalize_img_ext,
|
|
16
|
+
_resolve_img_ext,
|
|
17
|
+
dataset_id_from_name,
|
|
18
|
+
expected_nnunet_dataset_name,
|
|
19
|
+
prepare_training_dataset,
|
|
20
|
+
run_nnunet_training,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _make_training_data(tmp_path, *, surfaces=False, img_ext=".nrrd"):
|
|
25
|
+
data = tmp_path / "data"
|
|
26
|
+
(data / "images").mkdir(parents=True)
|
|
27
|
+
(data / "centerlines").mkdir()
|
|
28
|
+
(data / "images" / f"case1{img_ext}").write_bytes(b"x")
|
|
29
|
+
(data / "centerlines" / "case1.vtp").write_bytes(b"x")
|
|
30
|
+
if surfaces:
|
|
31
|
+
(data / "surfaces").mkdir()
|
|
32
|
+
(data / "surfaces" / "case1.vtp").write_bytes(b"x")
|
|
33
|
+
return str(data)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _make_patch_dirs(extracted, modality="ct"):
|
|
37
|
+
img_dir = extracted / f"{modality}_train"
|
|
38
|
+
mask_dir = extracted / f"{modality}_train_masks"
|
|
39
|
+
img_dir.mkdir(parents=True)
|
|
40
|
+
mask_dir.mkdir()
|
|
41
|
+
(img_dir / "a.nrrd").write_bytes(b"x")
|
|
42
|
+
(mask_dir / "a.nrrd").write_bytes(b"x")
|
|
43
|
+
return img_dir, mask_dir
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_dir_for_sampler_adds_trailing_sep(tmp_path):
|
|
47
|
+
raw = str(tmp_path / "seqseg_train")
|
|
48
|
+
out = _dir_for_sampler(raw)
|
|
49
|
+
assert out.endswith(os.sep)
|
|
50
|
+
assert out.rstrip("/\\") == os.path.abspath(raw)
|
|
51
|
+
# Sampler concatenates outdir + "ct_train_Sample_stats.csv"
|
|
52
|
+
stats = out + "ct_train_Sample_stats.csv"
|
|
53
|
+
assert os.path.dirname(stats) == os.path.abspath(raw)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_expected_nnunet_dataset_name_padding():
|
|
57
|
+
assert expected_nnunet_dataset_name("AORTAS", 1, "ct") == "Dataset001_AORTASCT"
|
|
58
|
+
assert expected_nnunet_dataset_name("AORTAS", 10, "mr") == "Dataset010_AORTASMR"
|
|
59
|
+
assert expected_nnunet_dataset_name("MYDATA", 999, "CT") == "Dataset0999_MYDATACT"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_dataset_id_from_name_errors():
|
|
63
|
+
with pytest.raises(ValueError):
|
|
64
|
+
dataset_id_from_name("not_a_dataset")
|
|
65
|
+
with pytest.raises(ValueError):
|
|
66
|
+
dataset_id_from_name("Dataset_FOO")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_prepare_requires_sampler():
|
|
70
|
+
with patch(
|
|
71
|
+
"seqseg.pipeline.train._require_sampler",
|
|
72
|
+
side_effect=TrainDependencyError("nope"),
|
|
73
|
+
):
|
|
74
|
+
with pytest.raises(TrainDependencyError):
|
|
75
|
+
prepare_training_dataset(
|
|
76
|
+
"/tmp/data",
|
|
77
|
+
"/tmp/out",
|
|
78
|
+
name="X",
|
|
79
|
+
dataset_number=1,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_prepare_calls_sampler_apis(tmp_path):
|
|
84
|
+
extract = MagicMock()
|
|
85
|
+
write = MagicMock(return_value=str(tmp_path / "Dataset0999_MYDATACT"))
|
|
86
|
+
data = _make_training_data(tmp_path)
|
|
87
|
+
extracted = tmp_path / "extracted"
|
|
88
|
+
_make_patch_dirs(extracted)
|
|
89
|
+
|
|
90
|
+
with patch(
|
|
91
|
+
"seqseg.pipeline.train._require_sampler",
|
|
92
|
+
return_value=(extract, write),
|
|
93
|
+
):
|
|
94
|
+
result = prepare_training_dataset(
|
|
95
|
+
data,
|
|
96
|
+
str(extracted),
|
|
97
|
+
name="MYDATA",
|
|
98
|
+
dataset_number=999,
|
|
99
|
+
modality="CT",
|
|
100
|
+
yes=True,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
extract.assert_called_once()
|
|
104
|
+
write.assert_called_once()
|
|
105
|
+
outdir_arg = extract.call_args.kwargs["outdir"]
|
|
106
|
+
assert outdir_arg.endswith(os.sep)
|
|
107
|
+
assert os.path.basename(os.path.normpath(outdir_arg)) == "extracted"
|
|
108
|
+
assert extract.call_args.kwargs["config"]["IMG_EXT"] == ".nrrd"
|
|
109
|
+
assert result.dataset_names == ["Dataset0999_MYDATACT"]
|
|
110
|
+
assert result.modalities == ["CT"]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_detect_img_ext_prefers_nii_gz(tmp_path):
|
|
114
|
+
images = tmp_path / "images"
|
|
115
|
+
images.mkdir()
|
|
116
|
+
(images / "716.nii.gz").write_bytes(b"x")
|
|
117
|
+
(images / "notes.txt").write_text("nope")
|
|
118
|
+
assert _detect_img_ext(images) == ".nii.gz"
|
|
119
|
+
assert _normalize_img_ext("nii.gz") == ".nii.gz"
|
|
120
|
+
assert _resolve_img_ext(str(tmp_path), {"IMG_EXT": ".nrrd"}) == ".nii.gz"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_prepare_detects_nii_gz_and_sets_sampler_config(tmp_path):
|
|
124
|
+
extract = MagicMock()
|
|
125
|
+
write = MagicMock(return_value=str(tmp_path / "Dataset0999_MYDATACT"))
|
|
126
|
+
data = _make_training_data(tmp_path, img_ext=".nii.gz")
|
|
127
|
+
extracted = tmp_path / "extracted"
|
|
128
|
+
_make_patch_dirs(extracted)
|
|
129
|
+
|
|
130
|
+
with patch(
|
|
131
|
+
"seqseg.pipeline.train._require_sampler",
|
|
132
|
+
return_value=(extract, write),
|
|
133
|
+
):
|
|
134
|
+
prepare_training_dataset(
|
|
135
|
+
data,
|
|
136
|
+
str(extracted),
|
|
137
|
+
name="MYDATA",
|
|
138
|
+
dataset_number=999,
|
|
139
|
+
yes=True,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
assert extract.call_args.kwargs["config"]["IMG_EXT"] == ".nii.gz"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_prepare_explicit_img_ext_overrides_detection(tmp_path):
|
|
146
|
+
extract = MagicMock()
|
|
147
|
+
write = MagicMock(return_value=str(tmp_path / "Dataset0999_MYDATACT"))
|
|
148
|
+
data = Path(_make_training_data(tmp_path, img_ext=".nii.gz"))
|
|
149
|
+
(data / "images" / "case1.mha").write_bytes(b"x")
|
|
150
|
+
extracted = tmp_path / "extracted"
|
|
151
|
+
_make_patch_dirs(extracted)
|
|
152
|
+
|
|
153
|
+
with patch(
|
|
154
|
+
"seqseg.pipeline.train._require_sampler",
|
|
155
|
+
return_value=(extract, write),
|
|
156
|
+
):
|
|
157
|
+
prepare_training_dataset(
|
|
158
|
+
str(data),
|
|
159
|
+
str(extracted),
|
|
160
|
+
name="MYDATA",
|
|
161
|
+
dataset_number=999,
|
|
162
|
+
img_ext="mha",
|
|
163
|
+
yes=True,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
assert extract.call_args.kwargs["config"]["IMG_EXT"] == ".mha"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_prepare_multi_modality_increments_ids(tmp_path):
|
|
170
|
+
extract = MagicMock()
|
|
171
|
+
write = MagicMock(
|
|
172
|
+
side_effect=[
|
|
173
|
+
str(tmp_path / "Dataset0999_MYDATACT"),
|
|
174
|
+
str(tmp_path / "Dataset1000_MYDATAMR"),
|
|
175
|
+
]
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
with patch(
|
|
179
|
+
"seqseg.pipeline.train._require_sampler",
|
|
180
|
+
return_value=(extract, write),
|
|
181
|
+
):
|
|
182
|
+
result = prepare_training_dataset(
|
|
183
|
+
str(tmp_path / "data"),
|
|
184
|
+
str(tmp_path / "extracted"),
|
|
185
|
+
name="MYDATA",
|
|
186
|
+
dataset_number=999,
|
|
187
|
+
modality="CT,MR",
|
|
188
|
+
skip_sample=True,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
assert write.call_count == 2
|
|
192
|
+
assert result.dataset_names == [
|
|
193
|
+
"Dataset0999_MYDATACT",
|
|
194
|
+
"Dataset01000_MYDATAMR",
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_prepare_preflight_rejects_missing_images(tmp_path):
|
|
199
|
+
extract = MagicMock()
|
|
200
|
+
write = MagicMock()
|
|
201
|
+
empty = tmp_path / "data"
|
|
202
|
+
empty.mkdir()
|
|
203
|
+
with patch(
|
|
204
|
+
"seqseg.pipeline.train._require_sampler",
|
|
205
|
+
return_value=(extract, write),
|
|
206
|
+
):
|
|
207
|
+
with pytest.raises(FileNotFoundError, match="images/ and centerlines"):
|
|
208
|
+
prepare_training_dataset(
|
|
209
|
+
str(empty),
|
|
210
|
+
str(tmp_path / "extracted"),
|
|
211
|
+
name="MYDATA",
|
|
212
|
+
dataset_number=999,
|
|
213
|
+
yes=True,
|
|
214
|
+
)
|
|
215
|
+
extract.assert_not_called()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_prepare_continues_if_stats_csv_missing_but_patches_exist(tmp_path):
|
|
219
|
+
extracted = tmp_path / "extracted"
|
|
220
|
+
_make_patch_dirs(extracted)
|
|
221
|
+
extract = MagicMock(
|
|
222
|
+
side_effect=FileNotFoundError(
|
|
223
|
+
f"[Errno 2] No such file or directory: "
|
|
224
|
+
f"'{extracted}/ct_train_Sample_stats.csv'"
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
write = MagicMock(return_value=str(tmp_path / "Dataset0999_MYDATACT"))
|
|
228
|
+
|
|
229
|
+
with patch(
|
|
230
|
+
"seqseg.pipeline.train._require_sampler",
|
|
231
|
+
return_value=(extract, write),
|
|
232
|
+
):
|
|
233
|
+
result = prepare_training_dataset(
|
|
234
|
+
_make_training_data(tmp_path),
|
|
235
|
+
str(extracted),
|
|
236
|
+
name="MYDATA",
|
|
237
|
+
dataset_number=999,
|
|
238
|
+
yes=True,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
write.assert_called_once()
|
|
242
|
+
assert result.dataset_names == ["Dataset0999_MYDATACT"]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_prepare_missing_stats_csv_without_patches_is_actionable(tmp_path):
|
|
246
|
+
extract = MagicMock(
|
|
247
|
+
side_effect=FileNotFoundError(
|
|
248
|
+
"[Errno 2] No such file or directory: "
|
|
249
|
+
"'/scratch/11178/numi/seqseg_train/ct_train_Sample_stats.csv'"
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
write = MagicMock()
|
|
253
|
+
|
|
254
|
+
with patch(
|
|
255
|
+
"seqseg.pipeline.train._require_sampler",
|
|
256
|
+
return_value=(extract, write),
|
|
257
|
+
):
|
|
258
|
+
with pytest.raises(RuntimeError, match="--num-cores 1"):
|
|
259
|
+
prepare_training_dataset(
|
|
260
|
+
_make_training_data(tmp_path),
|
|
261
|
+
str(tmp_path / "extracted"),
|
|
262
|
+
name="MYDATA",
|
|
263
|
+
dataset_number=999,
|
|
264
|
+
yes=True,
|
|
265
|
+
)
|
|
266
|
+
write.assert_not_called()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_run_nnunet_training_requires_env(tmp_path, monkeypatch):
|
|
270
|
+
monkeypatch.setenv("SEQSEG_PATHS_FILE", str(tmp_path / "no_paths.yaml"))
|
|
271
|
+
for key in ("nnUNet_raw", "nnUNet_preprocessed", "nnUNet_results"):
|
|
272
|
+
monkeypatch.delenv(key, raising=False)
|
|
273
|
+
with pytest.raises(RuntimeError, match="nnU-Net paths"):
|
|
274
|
+
run_nnunet_training(999, plan_only=True)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_run_nnunet_training_plan_only(monkeypatch):
|
|
278
|
+
monkeypatch.setenv("nnUNet_raw", "/raw")
|
|
279
|
+
monkeypatch.setenv("nnUNet_preprocessed", "/pre")
|
|
280
|
+
monkeypatch.setenv("nnUNet_results", "/res")
|
|
281
|
+
|
|
282
|
+
with patch("seqseg.pipeline.train.subprocess.run") as run:
|
|
283
|
+
run_nnunet_training(999, plan_only=True, configuration="3d_fullres")
|
|
284
|
+
assert run.call_count == 1
|
|
285
|
+
cmd = run.call_args[0][0]
|
|
286
|
+
assert "nnUNetv2_plan_and_preprocess" in cmd[0]
|
|
287
|
+
assert cmd[cmd.index("-d") + 1] == "999"
|
|
288
|
+
assert cmd[cmd.index("-c") + 1] == "3d_fullres"
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
"""Unit tests for seqseg.pipeline.train helpers (no GPU / sampler required)."""
|
|
2
|
-
|
|
3
|
-
from unittest.mock import MagicMock, patch
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
import os
|
|
8
|
-
|
|
9
|
-
from seqseg.pipeline.train import (
|
|
10
|
-
TrainDependencyError,
|
|
11
|
-
_dir_for_sampler,
|
|
12
|
-
dataset_id_from_name,
|
|
13
|
-
expected_nnunet_dataset_name,
|
|
14
|
-
prepare_training_dataset,
|
|
15
|
-
run_nnunet_training,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def test_dir_for_sampler_adds_trailing_sep(tmp_path):
|
|
20
|
-
raw = str(tmp_path / "seqseg_train")
|
|
21
|
-
out = _dir_for_sampler(raw)
|
|
22
|
-
assert out.endswith(os.sep)
|
|
23
|
-
assert out.rstrip("/\\") == os.path.abspath(raw)
|
|
24
|
-
# Sampler concatenates outdir + "ct_train_Sample_stats.csv"
|
|
25
|
-
stats = out + "ct_train_Sample_stats.csv"
|
|
26
|
-
assert os.path.dirname(stats) == os.path.abspath(raw)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def test_expected_nnunet_dataset_name_padding():
|
|
30
|
-
assert expected_nnunet_dataset_name("AORTAS", 1, "ct") == "Dataset001_AORTASCT"
|
|
31
|
-
assert expected_nnunet_dataset_name("AORTAS", 10, "mr") == "Dataset010_AORTASMR"
|
|
32
|
-
assert expected_nnunet_dataset_name("MYDATA", 999, "CT") == "Dataset0999_MYDATACT"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def test_dataset_id_from_name_errors():
|
|
36
|
-
with pytest.raises(ValueError):
|
|
37
|
-
dataset_id_from_name("not_a_dataset")
|
|
38
|
-
with pytest.raises(ValueError):
|
|
39
|
-
dataset_id_from_name("Dataset_FOO")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def test_prepare_requires_sampler():
|
|
43
|
-
with patch(
|
|
44
|
-
"seqseg.pipeline.train._require_sampler",
|
|
45
|
-
side_effect=TrainDependencyError("nope"),
|
|
46
|
-
):
|
|
47
|
-
with pytest.raises(TrainDependencyError):
|
|
48
|
-
prepare_training_dataset(
|
|
49
|
-
"/tmp/data",
|
|
50
|
-
"/tmp/out",
|
|
51
|
-
name="X",
|
|
52
|
-
dataset_number=1,
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def test_prepare_calls_sampler_apis(tmp_path):
|
|
57
|
-
extract = MagicMock()
|
|
58
|
-
write = MagicMock(return_value=str(tmp_path / "Dataset0999_MYDATACT"))
|
|
59
|
-
|
|
60
|
-
with patch(
|
|
61
|
-
"seqseg.pipeline.train._require_sampler",
|
|
62
|
-
return_value=(extract, write),
|
|
63
|
-
):
|
|
64
|
-
result = prepare_training_dataset(
|
|
65
|
-
str(tmp_path / "data"),
|
|
66
|
-
str(tmp_path / "extracted"),
|
|
67
|
-
name="MYDATA",
|
|
68
|
-
dataset_number=999,
|
|
69
|
-
modality="CT",
|
|
70
|
-
yes=True,
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
extract.assert_called_once()
|
|
74
|
-
write.assert_called_once()
|
|
75
|
-
outdir_arg = extract.call_args.kwargs["outdir"]
|
|
76
|
-
assert outdir_arg.endswith(os.sep)
|
|
77
|
-
assert os.path.basename(os.path.normpath(outdir_arg)) == "extracted"
|
|
78
|
-
assert result.dataset_names == ["Dataset0999_MYDATACT"]
|
|
79
|
-
assert result.modalities == ["CT"]
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def test_prepare_multi_modality_increments_ids(tmp_path):
|
|
83
|
-
extract = MagicMock()
|
|
84
|
-
write = MagicMock(
|
|
85
|
-
side_effect=[
|
|
86
|
-
str(tmp_path / "Dataset0999_MYDATACT"),
|
|
87
|
-
str(tmp_path / "Dataset1000_MYDATAMR"),
|
|
88
|
-
]
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
with patch(
|
|
92
|
-
"seqseg.pipeline.train._require_sampler",
|
|
93
|
-
return_value=(extract, write),
|
|
94
|
-
):
|
|
95
|
-
result = prepare_training_dataset(
|
|
96
|
-
str(tmp_path / "data"),
|
|
97
|
-
str(tmp_path / "extracted"),
|
|
98
|
-
name="MYDATA",
|
|
99
|
-
dataset_number=999,
|
|
100
|
-
modality="CT,MR",
|
|
101
|
-
skip_sample=True,
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
assert write.call_count == 2
|
|
105
|
-
assert result.dataset_names == [
|
|
106
|
-
"Dataset0999_MYDATACT",
|
|
107
|
-
"Dataset01000_MYDATAMR",
|
|
108
|
-
]
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def test_run_nnunet_training_requires_env(tmp_path, monkeypatch):
|
|
112
|
-
monkeypatch.setenv("SEQSEG_PATHS_FILE", str(tmp_path / "no_paths.yaml"))
|
|
113
|
-
for key in ("nnUNet_raw", "nnUNet_preprocessed", "nnUNet_results"):
|
|
114
|
-
monkeypatch.delenv(key, raising=False)
|
|
115
|
-
with pytest.raises(RuntimeError, match="nnU-Net paths"):
|
|
116
|
-
run_nnunet_training(999, plan_only=True)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def test_run_nnunet_training_plan_only(monkeypatch):
|
|
120
|
-
monkeypatch.setenv("nnUNet_raw", "/raw")
|
|
121
|
-
monkeypatch.setenv("nnUNet_preprocessed", "/pre")
|
|
122
|
-
monkeypatch.setenv("nnUNet_results", "/res")
|
|
123
|
-
|
|
124
|
-
with patch("seqseg.pipeline.train.subprocess.run") as run:
|
|
125
|
-
run_nnunet_training(999, plan_only=True, configuration="3d_fullres")
|
|
126
|
-
assert run.call_count == 1
|
|
127
|
-
cmd = run.call_args[0][0]
|
|
128
|
-
assert "nnUNetv2_plan_and_preprocess" in cmd[0]
|
|
129
|
-
assert cmd[cmd.index("-d") + 1] == "999"
|
|
130
|
-
assert cmd[cmd.index("-c") + 1] == "3d_fullres"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|