seqseg 2.1.3__tar.gz → 2.1.4__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.3 → seqseg-2.1.4}/PKG-INFO +1 -1
- {seqseg-2.1.3 → seqseg-2.1.4}/pyproject.toml +1 -1
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/cli.py +11 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/config/global_test.yaml +3 -1
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/modules/centerline.py +34 -18
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/modules/sitk_functions.py +33 -5
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/modules/vtk_functions.py +22 -2
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/pipeline/train.py +55 -0
- seqseg-2.1.4/seqseg/tests/test_centerline_gradient.py +49 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/tests/test_cli_helpers.py +40 -0
- seqseg-2.1.4/seqseg/tests/test_resample_spacing.py +57 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/tests/test_train_pipeline.py +59 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg.egg-info/PKG-INFO +1 -1
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg.egg-info/SOURCES.txt +2 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/LICENSE +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/README.md +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/__init__.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/api.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/citations.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/config/__init__.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/config/aorta_tutorial.yaml +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/config/global.yaml +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/config/global_aorta.yaml +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/config/global_cereb.yaml +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/config/global_coro.yaml +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/config/global_debug.yaml +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/config/global_default.yaml +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/config/global_pulm.yaml +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/config/global_seg.yaml +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/config_models.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/modules/__init__.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/modules/assembly.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/modules/capping.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/modules/datasets.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/modules/evaluation.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/modules/initialization.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/modules/local_assembly.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/modules/nnunet.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/modules/params.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/modules/prediction.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/modules/simvascular.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/modules/simvascular_contour.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/modules/sweep.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/modules/tracing.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/modules/tracing_functions.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/modules/vmtk_functions.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/parallel/old_auto_centerline.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/parallel/parallel_auto_centerline.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/parallel/parallel_auto_centerline2.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/parallel/parallel_auto_centerline3.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/parallel/parallel_auto_centerline4.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/pipeline/__init__.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/pipeline/classic.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/pipeline/config.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/pipeline/directories.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/pipeline/plus.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/pipeline/post.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/pipeline/single_trace.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/scripts/__init__.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/scripts/taubin_smooth.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/seqseg.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/seqseg_plus.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/tests/conftest.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/tests/test_api_entry.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/tests/test_centerline_get_end_points.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/tests/test_datasets_paths.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/tests/test_nnunet2.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/tests/test_nnunet_device.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/tests/test_segmentation.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/tests/test_single_trace_staging.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/tests/test_start_seg.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/tests/test_user_paths.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/tests/test_vtk.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg/user_paths.py +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg.egg-info/dependency_links.txt +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg.egg-info/entry_points.txt +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg.egg-info/requires.txt +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/seqseg.egg-info/top_level.txt +0 -0
- {seqseg-2.1.3 → seqseg-2.1.4}/setup.cfg +0 -0
|
@@ -677,7 +677,18 @@ def _cmd_train_prepare(ns: argparse.Namespace) -> None:
|
|
|
677
677
|
print(str(e), file=sys.stderr)
|
|
678
678
|
sys.exit(1)
|
|
679
679
|
except Exception as e: # noqa: BLE001
|
|
680
|
+
import traceback
|
|
681
|
+
|
|
682
|
+
traceback.print_exc()
|
|
680
683
|
print(f"seqseg train prepare failed: {e}", file=sys.stderr)
|
|
684
|
+
if "GetDataType" in str(e):
|
|
685
|
+
print(
|
|
686
|
+
"A VTK array was None while reading centerlines/ (or "
|
|
687
|
+
"rasterizing surfaces/). Centerline .vtp files need points "
|
|
688
|
+
"and MaximumInscribedSphereRadius (VMTK/SimVascular). "
|
|
689
|
+
"Update SeqSeg and re-run with --num-cores 1.",
|
|
690
|
+
file=sys.stderr,
|
|
691
|
+
)
|
|
681
692
|
sys.exit(1)
|
|
682
693
|
|
|
683
694
|
print("\nPrepare complete.")
|
|
@@ -33,7 +33,9 @@ USE_BUFFER: True # buffer the assembly (so not to mess with retrace prevention)
|
|
|
33
33
|
BUFFER_N: 20 # add to assembly steps buff_N steps ago
|
|
34
34
|
WEIGHTED_ASSEMBLY: True # if assembly should be weighted
|
|
35
35
|
WEIGHT_TYPE: gaussian # type of weighting for assembly
|
|
36
|
-
|
|
36
|
+
# Disabled for CI: the test CT is 512x512x258 (~0.65 mm). Refining the full
|
|
37
|
+
# assembly to 0.3 mm is ~1e9 voxels and OOMs GitHub-hosted runners (exit 137).
|
|
38
|
+
CENT_MAX_SPACING: null
|
|
37
39
|
|
|
38
40
|
FORCEFUL_SIDEBRANCH: False
|
|
39
41
|
FORCEFUL_SIDEBRANCH_MAGN_RADIUS: 1.1
|
|
@@ -598,6 +598,25 @@ def gradient_matrix(x):
|
|
|
598
598
|
return gradients
|
|
599
599
|
|
|
600
600
|
|
|
601
|
+
def world_frame_gradient(grid_values, spacing, direction):
|
|
602
|
+
"""
|
|
603
|
+
Convert a 3-D scalar field sampled on the image grid to a world-space
|
|
604
|
+
gradient (value per mm, components along the image direction axes).
|
|
605
|
+
|
|
606
|
+
``np.gradient`` returns finite differences along array axes (per voxel).
|
|
607
|
+
Physical conversion is ``grad_world = D @ diag(1/spacing) @ grad_grid``.
|
|
608
|
+
Computation is in float64 so ITK FastMarching sentinels (~1e38 in
|
|
609
|
+
float32) do not overflow when scaled by ``1/spacing``.
|
|
610
|
+
"""
|
|
611
|
+
values = np.asarray(grid_values, dtype=np.float64)
|
|
612
|
+
gradient = np.stack(np.gradient(values), axis=0)
|
|
613
|
+
spacing = np.asarray(spacing, dtype=np.float64).reshape(3)
|
|
614
|
+
inv_spacing = 1.0 / np.maximum(spacing, np.finfo(np.float64).tiny)
|
|
615
|
+
direction_matrix = np.asarray(direction, dtype=np.float64).reshape(3, 3)
|
|
616
|
+
gradient = gradient * inv_spacing[:, None, None, None]
|
|
617
|
+
return np.tensordot(direction_matrix, gradient, axes=([1], [0]))
|
|
618
|
+
|
|
619
|
+
|
|
601
620
|
def hessian_matrix(x):
|
|
602
621
|
"""
|
|
603
622
|
Calculate the hessian matrix with finite differences
|
|
@@ -1347,25 +1366,22 @@ def calc_centerline_fmm(segmentation, seed=None, targets=None,
|
|
|
1347
1366
|
os.path.join(out_dir, 'masked_out_fmm.mha'))
|
|
1348
1367
|
sitk.WriteImage(segmentation,
|
|
1349
1368
|
os.path.join(out_dir, 'segmentation_from_fmm.mha'))
|
|
1350
|
-
#
|
|
1351
|
-
#
|
|
1352
|
-
#
|
|
1353
|
-
#
|
|
1354
|
-
#
|
|
1369
|
+
# Gradient of the *masked* arrival-time map. Unreached FastMarching
|
|
1370
|
+
# voxels keep ITK's float32 LargeValue (~1e38); differentiating that
|
|
1371
|
+
# field and scaling by 1/spacing overflows float32 (CI RuntimeWarning)
|
|
1372
|
+
# and yields inf/NaN steps. Mask + clip keeps the gradient inside the
|
|
1373
|
+
# vessel and finite. Conversion to world mm^-1 is:
|
|
1355
1374
|
# grad_world = D @ diag(1/spacing) @ grad_grid
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
gradient = gradient * inv_spacing[:, None, None, None]
|
|
1367
|
-
gradient = np.tensordot(direction_matrix, gradient,
|
|
1368
|
-
axes=([1], [0]))
|
|
1375
|
+
arrival = np.clip(
|
|
1376
|
+
np.asarray(
|
|
1377
|
+
sitk.GetArrayFromImage(output_mask).transpose(2, 1, 0),
|
|
1378
|
+
dtype=np.float64,
|
|
1379
|
+
),
|
|
1380
|
+
-1.0e6,
|
|
1381
|
+
1.0e6,
|
|
1382
|
+
)
|
|
1383
|
+
gradient = world_frame_gradient(
|
|
1384
|
+
arrival, segmentation.GetSpacing(), segmentation.GetDirection())
|
|
1369
1385
|
if verbose:
|
|
1370
1386
|
# Calculate and print gradient magnitude statistics
|
|
1371
1387
|
gradient_magnitude = np.sqrt(np.sum(gradient**2, axis=0))
|
|
@@ -55,7 +55,27 @@ def copy_settings(img, ref_img):
|
|
|
55
55
|
return img
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
# Refuse pathological upsampling (e.g. CENT_MAX_SPACING in mm applied with
|
|
59
|
+
# ``-unit cm``) before SimpleITK tries to allocate a multi-billion-voxel grid
|
|
60
|
+
# and the process is SIGKILL'd (exit 137) on CI runners.
|
|
61
|
+
DEFAULT_MAX_RESAMPLE_VOXELS = 200_000_000
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def resampled_grid_size(orig_size, orig_spacing, new_spacing):
|
|
65
|
+
"""Voxel size that :func:`resample_to_spacing` would produce."""
|
|
66
|
+
orig_size = np.asarray(orig_size, dtype=np.int64)
|
|
67
|
+
orig_spacing = np.asarray(orig_spacing, dtype=np.float64)
|
|
68
|
+
target_spacing = np.asarray(new_spacing, dtype=np.float64)
|
|
69
|
+
if np.any(target_spacing <= 0):
|
|
70
|
+
raise ValueError(f"new_spacing must be > 0, got {target_spacing.tolist()}")
|
|
71
|
+
return np.maximum(
|
|
72
|
+
1,
|
|
73
|
+
np.rint(orig_size * (orig_spacing / target_spacing)).astype(np.int64),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def resample_to_spacing(image, new_spacing, is_label=False,
|
|
78
|
+
max_voxels=DEFAULT_MAX_RESAMPLE_VOXELS):
|
|
59
79
|
"""
|
|
60
80
|
Resample a SITK image to a target spacing while preserving physical extent.
|
|
61
81
|
|
|
@@ -63,15 +83,23 @@ def resample_to_spacing(image, new_spacing, is_label=False):
|
|
|
63
83
|
image (sitk.Image): Input image.
|
|
64
84
|
new_spacing (iterable): Target spacing [sx, sy, sz].
|
|
65
85
|
is_label (bool): Use nearest-neighbor interpolation for labels.
|
|
86
|
+
max_voxels (int | None): If set, raise ``ValueError`` when the
|
|
87
|
+
output grid would exceed this many voxels instead of allocating.
|
|
66
88
|
"""
|
|
67
89
|
orig_size = np.array(image.GetSize(), dtype=np.int64)
|
|
68
90
|
orig_spacing = np.array(image.GetSpacing(), dtype=np.float64)
|
|
69
91
|
target_spacing = np.array(new_spacing, dtype=np.float64)
|
|
70
92
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
93
|
+
new_size_arr = resampled_grid_size(orig_size, orig_spacing, target_spacing)
|
|
94
|
+
n_vox = int(np.prod(new_size_arr))
|
|
95
|
+
if max_voxels is not None and n_vox > int(max_voxels):
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Resampling to spacing {target_spacing.tolist()} would create "
|
|
98
|
+
f"{tuple(int(s) for s in new_size_arr)} voxels ({n_vox:,}), "
|
|
99
|
+
f"exceeding max_voxels={int(max_voxels):,}. Check CENT_MAX_SPACING "
|
|
100
|
+
f"and -unit (mm vs cm) before refining a full-volume assembly."
|
|
101
|
+
)
|
|
102
|
+
new_size = new_size_arr.tolist()
|
|
75
103
|
|
|
76
104
|
resample = sitk.ResampleImageFilter()
|
|
77
105
|
resample.SetOutputSpacing(target_spacing.tolist())
|
|
@@ -87,10 +87,20 @@ class ClosestPoints:
|
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
def collect_arrays(output):
|
|
90
|
+
"""Convert VTK field/point/cell data arrays to numpy.
|
|
91
|
+
|
|
92
|
+
Skip unnamed arrays and non-numeric slots (``GetArray(i)`` is None for
|
|
93
|
+
string/abstract arrays). ``vtk_to_numpy(None)`` raises
|
|
94
|
+
``'NoneType' object has no attribute 'GetDataType'``.
|
|
95
|
+
"""
|
|
90
96
|
res = {}
|
|
97
|
+
if output is None:
|
|
98
|
+
return res
|
|
91
99
|
for i in range(output.GetNumberOfArrays()):
|
|
92
100
|
name = output.GetArrayName(i)
|
|
93
101
|
data = output.GetArray(i)
|
|
102
|
+
if name is None or data is None:
|
|
103
|
+
continue
|
|
94
104
|
res[name] = v2n(data)
|
|
95
105
|
return res
|
|
96
106
|
|
|
@@ -1284,8 +1294,18 @@ def convertPolyDataToImageData(poly, ref_im):
|
|
|
1284
1294
|
output: resulted vtkImageData
|
|
1285
1295
|
"""
|
|
1286
1296
|
|
|
1287
|
-
ref_im.GetPointData().
|
|
1288
|
-
|
|
1297
|
+
sc = ref_im.GetPointData().GetScalars()
|
|
1298
|
+
if sc is None:
|
|
1299
|
+
dims = ref_im.GetDimensions()
|
|
1300
|
+
n = int(dims[0]) * int(dims[1]) * int(dims[2])
|
|
1301
|
+
if n <= 0:
|
|
1302
|
+
raise RuntimeError(
|
|
1303
|
+
f"VTK image has no scalars and empty dimensions {dims}"
|
|
1304
|
+
)
|
|
1305
|
+
shape = n
|
|
1306
|
+
else:
|
|
1307
|
+
shape = v2n(sc).shape
|
|
1308
|
+
ref_im.GetPointData().SetScalars(n2v(np.zeros(shape)))
|
|
1289
1309
|
ply2im = vtk.vtkPolyDataToImageStencil()
|
|
1290
1310
|
ply2im.SetTolerance(0.05)
|
|
1291
1311
|
ply2im.SetInputData(poly)
|
|
@@ -31,9 +31,64 @@ def _require_sampler():
|
|
|
31
31
|
"or:\n"
|
|
32
32
|
" pip install vascular-segment-sampler"
|
|
33
33
|
) from e
|
|
34
|
+
_harden_sampler_vtk()
|
|
34
35
|
return extract_patches, write_nnunet_dataset
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
def _harden_sampler_vtk() -> None:
|
|
39
|
+
"""Skip non-numeric VTK arrays the sampler used to pass to vtk_to_numpy.
|
|
40
|
+
|
|
41
|
+
Centerline .vtp files from SimVascular/Slicer often include string/abstract
|
|
42
|
+
arrays. ``GetArray(i)`` is None for those, which became
|
|
43
|
+
``'NoneType' object has no attribute 'GetDataType'``.
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
import vascular_segment_sampler.sampling.extract as extract_mod
|
|
47
|
+
import vascular_segment_sampler.sampling.functions as samp_fn
|
|
48
|
+
import vascular_segment_sampler.vtk_functions as vf
|
|
49
|
+
from seqseg.modules.vtk_functions import (
|
|
50
|
+
collect_arrays,
|
|
51
|
+
convertPolyDataToImageData,
|
|
52
|
+
)
|
|
53
|
+
except ImportError:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
vf.collect_arrays = collect_arrays
|
|
57
|
+
samp_fn.collect_arrays = collect_arrays
|
|
58
|
+
vf.convertPolyDataToImageData = convertPolyDataToImageData
|
|
59
|
+
|
|
60
|
+
orig_sort = getattr(samp_fn, "sort_centerline", None)
|
|
61
|
+
if orig_sort is not None and not getattr(orig_sort, "_seqseg_hardened", False):
|
|
62
|
+
|
|
63
|
+
def _sort_centerline(centerline, *args, **kwargs):
|
|
64
|
+
if centerline is None or centerline.GetNumberOfPoints() == 0:
|
|
65
|
+
raise RuntimeError(
|
|
66
|
+
"Centerline has no points. Each case needs a non-empty .vtp "
|
|
67
|
+
"in centerlines/ (VMTK/SimVascular)."
|
|
68
|
+
)
|
|
69
|
+
pts = centerline.GetPoints()
|
|
70
|
+
if pts is None or pts.GetData() is None:
|
|
71
|
+
raise RuntimeError(
|
|
72
|
+
"Centerline vtkPoints are empty. Check centerlines/*.vtp."
|
|
73
|
+
)
|
|
74
|
+
try:
|
|
75
|
+
return orig_sort(centerline, *args, **kwargs)
|
|
76
|
+
except KeyError as e:
|
|
77
|
+
key = str(e).strip("'\"")
|
|
78
|
+
if key in ("f", "MaximumInscribedSphereRadius", "Radius"):
|
|
79
|
+
raise RuntimeError(
|
|
80
|
+
"Centerline has no MaximumInscribedSphereRadius (or "
|
|
81
|
+
"Radius) point-data array. Export VMTK/SimVascular "
|
|
82
|
+
"centerlines."
|
|
83
|
+
) from e
|
|
84
|
+
raise
|
|
85
|
+
|
|
86
|
+
_sort_centerline._seqseg_hardened = True
|
|
87
|
+
samp_fn.sort_centerline = _sort_centerline
|
|
88
|
+
|
|
89
|
+
extract_mod.sort_centerline = samp_fn.sort_centerline
|
|
90
|
+
|
|
91
|
+
|
|
37
92
|
def _parse_modalities(modality: Optional[str]) -> List[str]:
|
|
38
93
|
if not modality:
|
|
39
94
|
return ["CT"]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""World-frame FMM gradient conversion (overflow / direction)."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from seqseg.modules.centerline import world_frame_gradient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_world_frame_gradient_identity_spacing():
|
|
9
|
+
# f(x,y,z) = 2x + 3y + 4z on a unit grid -> grad = (2, 3, 4)
|
|
10
|
+
x = np.arange(5)[:, None, None]
|
|
11
|
+
y = np.arange(5)[None, :, None]
|
|
12
|
+
z = np.arange(5)[None, None, :]
|
|
13
|
+
field = 2.0 * x + 3.0 * y + 4.0 * z
|
|
14
|
+
identity = (1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)
|
|
15
|
+
grad = world_frame_gradient(field, (1.0, 1.0, 1.0), identity)
|
|
16
|
+
interior = grad[:, 1:-1, 1:-1, 1:-1]
|
|
17
|
+
np.testing.assert_allclose(interior[0], 2.0, atol=1e-12)
|
|
18
|
+
np.testing.assert_allclose(interior[1], 3.0, atol=1e-12)
|
|
19
|
+
np.testing.assert_allclose(interior[2], 4.0, atol=1e-12)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_world_frame_gradient_scales_by_inv_spacing():
|
|
23
|
+
x = np.arange(5, dtype=np.float64)[:, None, None]
|
|
24
|
+
field = np.broadcast_to(x, (5, 5, 5)).copy()
|
|
25
|
+
identity = (1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)
|
|
26
|
+
# Two voxels of +1 in x, spacing 2 mm -> 0.5 / mm
|
|
27
|
+
grad = world_frame_gradient(field, (2.0, 2.0, 2.0), identity)
|
|
28
|
+
np.testing.assert_allclose(grad[0, 1:-1, :, :], 0.5, atol=1e-12)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_world_frame_gradient_float32_largevalue_does_not_overflow():
|
|
32
|
+
"""ITK FastMarching unreached voxels are ~1e38 in float32."""
|
|
33
|
+
field = np.zeros((6, 6, 6), dtype=np.float32)
|
|
34
|
+
field[2:4, 2:4, 2:4] = 10.0
|
|
35
|
+
field[0, 0, 0] = np.float32(1.7e38)
|
|
36
|
+
identity = (1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)
|
|
37
|
+
with np.errstate(over="raise"):
|
|
38
|
+
grad = world_frame_gradient(field, (0.65, 0.65, 1.0), identity)
|
|
39
|
+
assert np.isfinite(grad).all()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_world_frame_gradient_applies_direction_flip():
|
|
43
|
+
x = np.arange(5, dtype=np.float64)[:, None, None]
|
|
44
|
+
field = np.broadcast_to(x, (5, 5, 5)).copy()
|
|
45
|
+
flip_x = (-1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)
|
|
46
|
+
grad = world_frame_gradient(field, (1.0, 1.0, 1.0), flip_x)
|
|
47
|
+
np.testing.assert_allclose(grad[0, 1:-1, :, :], -1.0, atol=1e-12)
|
|
48
|
+
np.testing.assert_allclose(grad[1], 0.0, atol=1e-12)
|
|
49
|
+
np.testing.assert_allclose(grad[2], 0.0, atol=1e-12)
|
|
@@ -362,6 +362,46 @@ def test_cmd_train_prepare_missing_dep(tmp_path, capsys):
|
|
|
362
362
|
assert "missing" in capsys.readouterr().err
|
|
363
363
|
|
|
364
364
|
|
|
365
|
+
def test_cmd_train_prepare_getdatatype_hint(tmp_path, capsys):
|
|
366
|
+
ns = argparse.Namespace(
|
|
367
|
+
data_dir=str(tmp_path),
|
|
368
|
+
outdir=str(tmp_path / "out"),
|
|
369
|
+
name="MYDATA",
|
|
370
|
+
dataset_number=999,
|
|
371
|
+
modality="CT",
|
|
372
|
+
config_name="global",
|
|
373
|
+
nnunet_raw=None,
|
|
374
|
+
perc_dataset=1.0,
|
|
375
|
+
num_cores=1,
|
|
376
|
+
start_from=0,
|
|
377
|
+
end_at=-1,
|
|
378
|
+
testing=False,
|
|
379
|
+
validation_prop=None,
|
|
380
|
+
max_samples=None,
|
|
381
|
+
truth_from_surface=False,
|
|
382
|
+
truth_target_spacing=None,
|
|
383
|
+
truth_regenerate=False,
|
|
384
|
+
skip_sample=False,
|
|
385
|
+
skip_convert=False,
|
|
386
|
+
also_test=False,
|
|
387
|
+
yes=False,
|
|
388
|
+
verbose=False,
|
|
389
|
+
img_ext=None,
|
|
390
|
+
)
|
|
391
|
+
with patch(
|
|
392
|
+
"seqseg.cli.prepare_training_dataset",
|
|
393
|
+
side_effect=AttributeError(
|
|
394
|
+
"'NoneType' object has no attribute 'GetDataType'"
|
|
395
|
+
),
|
|
396
|
+
):
|
|
397
|
+
with pytest.raises(SystemExit) as exc:
|
|
398
|
+
_cmd_train_prepare(ns)
|
|
399
|
+
assert exc.value.code == 1
|
|
400
|
+
err = capsys.readouterr().err
|
|
401
|
+
assert "GetDataType" in err
|
|
402
|
+
assert "centerlines" in err
|
|
403
|
+
|
|
404
|
+
|
|
365
405
|
def test_cmd_train_nnunet_resolves_name(monkeypatch):
|
|
366
406
|
called = {}
|
|
367
407
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Assembly spacing cap and resample size guards (CI OOM prevention)."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
import SimpleITK as sitk
|
|
6
|
+
|
|
7
|
+
from seqseg.config_models import AlgorithmConfig
|
|
8
|
+
from seqseg.modules.sitk_functions import (
|
|
9
|
+
capped_target_spacing,
|
|
10
|
+
resample_to_spacing,
|
|
11
|
+
resampled_grid_size,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_capped_target_spacing_mm_caps_coarse_dims():
|
|
16
|
+
target = capped_target_spacing((0.65, 0.65, 1.0), 0.3, "mm")
|
|
17
|
+
assert target == pytest.approx([0.3, 0.3, 0.3])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_capped_target_spacing_already_fine_returns_none():
|
|
21
|
+
assert capped_target_spacing((0.2, 0.2, 0.2), 0.3, "mm") is None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_capped_target_spacing_cm_is_tenfold_finer():
|
|
25
|
+
# Image spacing stored in mm, but -unit cm treats 0.3 mm as 0.03.
|
|
26
|
+
target = capped_target_spacing((0.65, 0.65, 1.0), 0.3, "cm")
|
|
27
|
+
assert target == pytest.approx([0.03, 0.03, 0.03])
|
|
28
|
+
n_vox = int(np.prod(resampled_grid_size(
|
|
29
|
+
(512, 512, 258), (0.65, 0.65, 1.0), target
|
|
30
|
+
)))
|
|
31
|
+
# ~1e9 voxels — this is what SIGKILL'd the macos CI job.
|
|
32
|
+
assert n_vox > 200_000_000
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_resampled_grid_size_identity():
|
|
36
|
+
size = resampled_grid_size((10, 20, 30), (1.0, 1.0, 1.0), (1.0, 1.0, 1.0))
|
|
37
|
+
np.testing.assert_array_equal(size, [10, 20, 30])
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_resample_to_spacing_refuses_huge_grid():
|
|
41
|
+
img = sitk.Image(8, 8, 8, sitk.sitkUInt8)
|
|
42
|
+
img.SetSpacing((1.0, 1.0, 1.0))
|
|
43
|
+
with pytest.raises(ValueError, match="max_voxels"):
|
|
44
|
+
resample_to_spacing(img, (0.01, 0.01, 0.01), max_voxels=1_000)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_global_test_config_skips_assembly_upsample():
|
|
48
|
+
cfg = AlgorithmConfig.from_name("global_test")
|
|
49
|
+
assert cfg.get("CENT_MAX_SPACING") is None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_resample_to_spacing_small_grid_runs():
|
|
53
|
+
img = sitk.Image(4, 4, 4, sitk.sitkFloat32)
|
|
54
|
+
img.SetSpacing((2.0, 2.0, 2.0))
|
|
55
|
+
out = resample_to_spacing(img, (1.0, 1.0, 1.0))
|
|
56
|
+
assert tuple(out.GetSize()) == (8, 8, 8)
|
|
57
|
+
assert out.GetSpacing() == pytest.approx((1.0, 1.0, 1.0))
|
|
@@ -8,10 +8,13 @@ import os
|
|
|
8
8
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
11
13
|
from seqseg.pipeline.train import (
|
|
12
14
|
TrainDependencyError,
|
|
13
15
|
_detect_img_ext,
|
|
14
16
|
_dir_for_sampler,
|
|
17
|
+
_harden_sampler_vtk,
|
|
15
18
|
_normalize_img_ext,
|
|
16
19
|
_resolve_img_ext,
|
|
17
20
|
dataset_id_from_name,
|
|
@@ -266,6 +269,62 @@ def test_prepare_missing_stats_csv_without_patches_is_actionable(tmp_path):
|
|
|
266
269
|
write.assert_not_called()
|
|
267
270
|
|
|
268
271
|
|
|
272
|
+
def test_collect_arrays_skips_string_arrays():
|
|
273
|
+
import vtk
|
|
274
|
+
from vtk.util.numpy_support import numpy_to_vtk
|
|
275
|
+
|
|
276
|
+
from seqseg.modules.vtk_functions import collect_arrays
|
|
277
|
+
|
|
278
|
+
pts = vtk.vtkPoints()
|
|
279
|
+
pts.InsertNextPoint(0.0, 0.0, 0.0)
|
|
280
|
+
poly = vtk.vtkPolyData()
|
|
281
|
+
poly.SetPoints(pts)
|
|
282
|
+
|
|
283
|
+
rad = numpy_to_vtk(np.array([1.5], dtype=np.float64))
|
|
284
|
+
rad.SetName("MaximumInscribedSphereRadius")
|
|
285
|
+
poly.GetPointData().AddArray(rad)
|
|
286
|
+
|
|
287
|
+
names = vtk.vtkStringArray()
|
|
288
|
+
names.SetName("ModelName")
|
|
289
|
+
names.InsertNextValue("aorta")
|
|
290
|
+
poly.GetPointData().AddArray(names)
|
|
291
|
+
|
|
292
|
+
data = collect_arrays(poly.GetPointData())
|
|
293
|
+
assert "MaximumInscribedSphereRadius" in data
|
|
294
|
+
assert "ModelName" not in data
|
|
295
|
+
assert float(data["MaximumInscribedSphereRadius"][0]) == 1.5
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def test_convert_poly_to_image_without_scalars():
|
|
299
|
+
import vtk
|
|
300
|
+
|
|
301
|
+
from seqseg.modules.vtk_functions import convertPolyDataToImageData
|
|
302
|
+
|
|
303
|
+
sphere = vtk.vtkSphereSource()
|
|
304
|
+
sphere.SetCenter(4, 4, 4)
|
|
305
|
+
sphere.SetRadius(2.0)
|
|
306
|
+
sphere.Update()
|
|
307
|
+
|
|
308
|
+
ref = vtk.vtkImageData()
|
|
309
|
+
ref.SetDimensions(8, 8, 8)
|
|
310
|
+
ref.SetSpacing(1.0, 1.0, 1.0)
|
|
311
|
+
ref.SetOrigin(0, 0, 0)
|
|
312
|
+
|
|
313
|
+
out = convertPolyDataToImageData(sphere.GetOutput(), ref)
|
|
314
|
+
assert out.GetPointData().GetScalars() is not None
|
|
315
|
+
assert out.GetDimensions() == (8, 8, 8)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def test_harden_sampler_vtk_patches_collect_arrays():
|
|
319
|
+
pytest.importorskip("vascular_segment_sampler")
|
|
320
|
+
import vascular_segment_sampler.sampling.functions as samp_fn
|
|
321
|
+
|
|
322
|
+
from seqseg.modules.vtk_functions import collect_arrays
|
|
323
|
+
|
|
324
|
+
_harden_sampler_vtk()
|
|
325
|
+
assert samp_fn.collect_arrays is collect_arrays
|
|
326
|
+
|
|
327
|
+
|
|
269
328
|
def test_run_nnunet_training_requires_env(tmp_path, monkeypatch):
|
|
270
329
|
monkeypatch.setenv("SEQSEG_PATHS_FILE", str(tmp_path / "no_paths.yaml"))
|
|
271
330
|
for key in ("nnUNet_raw", "nnUNet_preprocessed", "nnUNet_results"):
|
|
@@ -63,10 +63,12 @@ seqseg/scripts/taubin_smooth.py
|
|
|
63
63
|
seqseg/tests/conftest.py
|
|
64
64
|
seqseg/tests/test_api_entry.py
|
|
65
65
|
seqseg/tests/test_centerline_get_end_points.py
|
|
66
|
+
seqseg/tests/test_centerline_gradient.py
|
|
66
67
|
seqseg/tests/test_cli_helpers.py
|
|
67
68
|
seqseg/tests/test_datasets_paths.py
|
|
68
69
|
seqseg/tests/test_nnunet2.py
|
|
69
70
|
seqseg/tests/test_nnunet_device.py
|
|
71
|
+
seqseg/tests/test_resample_spacing.py
|
|
70
72
|
seqseg/tests/test_segmentation.py
|
|
71
73
|
seqseg/tests/test_single_trace_staging.py
|
|
72
74
|
seqseg/tests/test_start_seg.py
|
|
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
|