seqseg 2.1.2__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.2 → seqseg-2.1.4}/PKG-INFO +1 -1
- {seqseg-2.1.2 → seqseg-2.1.4}/pyproject.toml +1 -1
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/cli.py +21 -1
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/global_test.yaml +3 -1
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/centerline.py +34 -18
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/sitk_functions.py +33 -5
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/vtk_functions.py +22 -2
- seqseg-2.1.4/seqseg/pipeline/train.py +618 -0
- seqseg-2.1.4/seqseg/tests/test_centerline_gradient.py +49 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_cli_helpers.py +42 -0
- seqseg-2.1.4/seqseg/tests/test_resample_spacing.py +57 -0
- seqseg-2.1.4/seqseg/tests/test_train_pipeline.py +347 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg.egg-info/PKG-INFO +1 -1
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg.egg-info/SOURCES.txt +2 -0
- seqseg-2.1.2/seqseg/pipeline/train.py +0 -301
- seqseg-2.1.2/seqseg/tests/test_train_pipeline.py +0 -130
- {seqseg-2.1.2 → seqseg-2.1.4}/LICENSE +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/README.md +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/__init__.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/api.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/citations.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/__init__.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/aorta_tutorial.yaml +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/global.yaml +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/global_aorta.yaml +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/global_cereb.yaml +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/global_coro.yaml +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/global_debug.yaml +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/global_default.yaml +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/global_pulm.yaml +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/global_seg.yaml +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config_models.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/__init__.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/assembly.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/capping.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/datasets.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/evaluation.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/initialization.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/local_assembly.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/nnunet.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/params.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/prediction.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/simvascular.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/simvascular_contour.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/sweep.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/tracing.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/tracing_functions.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/vmtk_functions.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/parallel/old_auto_centerline.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/parallel/parallel_auto_centerline.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/parallel/parallel_auto_centerline2.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/parallel/parallel_auto_centerline3.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/parallel/parallel_auto_centerline4.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/pipeline/__init__.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/pipeline/classic.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/pipeline/config.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/pipeline/directories.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/pipeline/plus.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/pipeline/post.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/pipeline/single_trace.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/scripts/__init__.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/scripts/taubin_smooth.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/seqseg.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/seqseg_plus.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/conftest.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_api_entry.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_centerline_get_end_points.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_datasets_paths.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_nnunet2.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_nnunet_device.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_segmentation.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_single_trace_staging.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_start_seg.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_user_paths.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_vtk.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/user_paths.py +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg.egg-info/dependency_links.txt +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg.egg-info/entry_points.txt +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg.egg-info/requires.txt +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/seqseg.egg-info/top_level.txt +0 -0
- {seqseg-2.1.2 → seqseg-2.1.4}/setup.cfg +0 -0
|
@@ -671,12 +671,24 @@ 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)
|
|
677
678
|
sys.exit(1)
|
|
678
679
|
except Exception as e: # noqa: BLE001
|
|
680
|
+
import traceback
|
|
681
|
+
|
|
682
|
+
traceback.print_exc()
|
|
679
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
|
+
)
|
|
680
692
|
sys.exit(1)
|
|
681
693
|
|
|
682
694
|
print("\nPrepare complete.")
|
|
@@ -1038,6 +1050,14 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1038
1050
|
help="Write DatasetXXX_* here "
|
|
1039
1051
|
"(default: seqseg paths nnunet_raw / $nnUNet_raw; else --outdir)",
|
|
1040
1052
|
)
|
|
1053
|
+
p_prep.add_argument(
|
|
1054
|
+
"--img-ext",
|
|
1055
|
+
"-img_ext",
|
|
1056
|
+
default=None,
|
|
1057
|
+
type=str,
|
|
1058
|
+
help="Image/label suffix (.nii.gz, .mha, .nrrd, …). "
|
|
1059
|
+
"Default: detect from data_dir/images/; else sampler YAML IMG_EXT",
|
|
1060
|
+
)
|
|
1041
1061
|
p_prep.add_argument("--perc-dataset", default=1.0, type=float)
|
|
1042
1062
|
p_prep.add_argument("--num-cores", default=1, type=int)
|
|
1043
1063
|
p_prep.add_argument("--start-from", default=0, type=int)
|
|
@@ -1075,7 +1095,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1075
1095
|
)
|
|
1076
1096
|
p_prep.add_argument("--yes", action="store_true", help="Non-interactive confirms")
|
|
1077
1097
|
p_prep.add_argument("--verbose", action="store_true")
|
|
1078
|
-
p_prep.set_defaults(_handler=_cmd_train_prepare)
|
|
1098
|
+
p_prep.set_defaults(_handler=_cmd_train_prepare, img_ext=None)
|
|
1079
1099
|
|
|
1080
1100
|
p_nn = train_sub.add_parser(
|
|
1081
1101
|
"nnunet",
|
|
@@ -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)
|