seqseg 2.2.1__tar.gz → 2.2.2__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.2.1 → seqseg-2.2.2}/PKG-INFO +1 -1
- {seqseg-2.2.1 → seqseg-2.2.2}/pyproject.toml +1 -1
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/api.py +1 -1
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/cli.py +4 -4
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config_models.py +1 -1
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/centerline.py +66 -57
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/simvascular.py +5 -5
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/simvascular_contour.py +10 -4
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/tracing.py +3 -3
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/post.py +2 -2
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_api_entry.py +2 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_centerline_backtracking.py +38 -25
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_cli_helpers.py +33 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg.egg-info/PKG-INFO +1 -1
- {seqseg-2.2.1 → seqseg-2.2.2}/LICENSE +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/README.md +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/__init__.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/citations.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/__init__.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/aorta_tutorial.yaml +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global.yaml +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global_aorta.yaml +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global_cereb.yaml +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global_coro.yaml +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global_coro_retrace.yaml +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global_debug.yaml +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global_default.yaml +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global_pulm.yaml +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global_seg.yaml +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global_test.yaml +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/sampler/default_cm.yaml +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/sampler/default_mm.yaml +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/sampler/test.yml +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/__init__.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/assembly.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/capping.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/checkpoint.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/datasets.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/evaluation.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/initialization.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/local_assembly.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/nnunet.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/params.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/prediction.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/sitk_functions.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/sweep.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/tracing_functions.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/vmtk_functions.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/vtk_functions.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/parallel/old_auto_centerline.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/parallel/parallel_auto_centerline.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/parallel/parallel_auto_centerline2.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/parallel/parallel_auto_centerline3.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/parallel/parallel_auto_centerline4.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/__init__.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/classic.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/config.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/continue_run.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/directories.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/nnunet_cli.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/plus.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/predict.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/single_trace.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/train.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/scripts/__init__.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/scripts/prepare_casx_dataset.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/scripts/taubin_smooth.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/seqseg.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/seqseg_plus.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/conftest.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_centerline_get_end_points.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_centerline_gradient.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_checkpoint.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_datasets_paths.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_global_prediction.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_nnunet2.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_nnunet_cli.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_nnunet_device.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_prepare_casx_dataset.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_resample_spacing.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_segmentation.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_single_trace_staging.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_start_seg.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_train_data.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_train_pipeline.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_user_paths.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_vtk.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/user_paths.py +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg.egg-info/SOURCES.txt +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg.egg-info/dependency_links.txt +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg.egg-info/entry_points.txt +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg.egg-info/requires.txt +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/seqseg.egg-info/top_level.txt +0 -0
- {seqseg-2.2.1 → seqseg-2.2.2}/setup.cfg +0 -0
|
@@ -129,7 +129,7 @@ def _add_classic_trace_args(p: argparse.ArgumentParser) -> None:
|
|
|
129
129
|
p.add_argument("-max_n_steps", "--max_n_steps", default=1000, type=int)
|
|
130
130
|
p.add_argument("-max_n_steps_per_branch", "--max_n_steps_per_branch", default=100, type=int)
|
|
131
131
|
p.add_argument("-max_n_branches", "--max_n_branches", default=100, type=int)
|
|
132
|
-
p.add_argument("-unit", "--unit", default="
|
|
132
|
+
p.add_argument("-unit", "--unit", default="mm", type=str)
|
|
133
133
|
p.add_argument("-config_name", "--config_name", default="global", type=str)
|
|
134
134
|
p.add_argument("-pt_centerline", "--pt_centerline", default=50, type=int)
|
|
135
135
|
p.add_argument("-num_seeds_centerline", "--num_seeds_centerline", default=1, type=int)
|
|
@@ -1029,7 +1029,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1029
1029
|
)
|
|
1030
1030
|
p_single.add_argument("--config-name", default="global", type=str)
|
|
1031
1031
|
p_single.add_argument("--scale", default=1.0, type=float)
|
|
1032
|
-
p_single.add_argument("--unit", default="
|
|
1032
|
+
p_single.add_argument("--unit", default="mm", type=str)
|
|
1033
1033
|
p_single.add_argument("--max-n-steps", default=1000, type=int)
|
|
1034
1034
|
p_single.add_argument("--max-n-branches", default=100, type=int)
|
|
1035
1035
|
p_single.add_argument("--max-n-steps-per-branch", default=100, type=int)
|
|
@@ -1218,7 +1218,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1218
1218
|
g1.add_argument("--config-name", default=None, type=str)
|
|
1219
1219
|
g1.add_argument("--merge-method", default="clean", type=str)
|
|
1220
1220
|
g1.add_argument("--directory-data", default=None, type=str)
|
|
1221
|
-
g1.add_argument("--unit", default="
|
|
1221
|
+
g1.add_argument("--unit", default="mm", type=str)
|
|
1222
1222
|
g1.set_defaults(_handler=_cmd_post_gcl_single)
|
|
1223
1223
|
|
|
1224
1224
|
g2 = gcl_sub.add_parser("batch", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
|
@@ -1234,7 +1234,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1234
1234
|
g2.add_argument("--config-name", default=None, type=str)
|
|
1235
1235
|
g2.add_argument("--merge-method", default="clean", type=str)
|
|
1236
1236
|
g2.add_argument("--directory-data", default=None, type=str)
|
|
1237
|
-
g2.add_argument("--unit", default="
|
|
1237
|
+
g2.add_argument("--unit", default="mm", type=str)
|
|
1238
1238
|
g2.set_defaults(_handler=_cmd_post_gcl_batch)
|
|
1239
1239
|
|
|
1240
1240
|
sv = sub.add_parser("simvascular", help="SimVascular project layout")
|
|
@@ -1339,19 +1339,36 @@ def backtracking_gradient(gradient, distance_map_surf_np,
|
|
|
1339
1339
|
return points, success
|
|
1340
1340
|
|
|
1341
1341
|
|
|
1342
|
-
#
|
|
1343
|
-
#
|
|
1344
|
-
#
|
|
1345
|
-
#
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
_MAX_SPACING_MM = 1.0
|
|
1349
|
-
_MAX_CENTERLINE_VOXELS = 20_000_000
|
|
1350
|
-
_GRID_SIZE_TOLERANCE = 0.10
|
|
1342
|
+
# Prefer an isotropic grid of about 260 million voxels when that is finer
|
|
1343
|
+
# than the segmentation. Never exceed 260 million voxels. On the S-008
|
|
1344
|
+
# crop, 250 million still dropped distal tips and 260 million kept every
|
|
1345
|
+
# end cluster.
|
|
1346
|
+
_TARGET_CENTERLINE_VOXELS = 260_000_000
|
|
1347
|
+
_MAX_CENTERLINE_VOXELS = 260_000_000
|
|
1351
1348
|
|
|
1352
1349
|
|
|
1353
|
-
def
|
|
1354
|
-
"""
|
|
1350
|
+
def _isotropic_grid(extent, n_voxels):
|
|
1351
|
+
"""Round ``extent`` onto an isotropic spacing of at most ``n_voxels``."""
|
|
1352
|
+
extent = np.asarray(extent, dtype=np.float64).reshape(3)
|
|
1353
|
+
extent_prod = float(np.prod(extent))
|
|
1354
|
+
if extent_prod <= 0.0 or n_voxels < 1:
|
|
1355
|
+
return np.ones(3, dtype=np.int64), 1.0
|
|
1356
|
+
spacing = (extent_prod / float(n_voxels)) ** (1.0 / 3.0)
|
|
1357
|
+
new_size = np.maximum(1, np.rint(extent / spacing)).astype(np.int64)
|
|
1358
|
+
while (int(np.prod(new_size, dtype=np.int64)) > n_voxels
|
|
1359
|
+
and spacing < 1.0e6):
|
|
1360
|
+
spacing *= 1.01
|
|
1361
|
+
new_size = np.maximum(1, np.rint(extent / spacing)).astype(np.int64)
|
|
1362
|
+
return new_size, float(spacing)
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
def centerline_grid_size(size, spacing):
|
|
1366
|
+
"""Choose the centerline grid from the native size and a voxel budget.
|
|
1367
|
+
|
|
1368
|
+
The native grid is compared with an isotropic grid of
|
|
1369
|
+
``_TARGET_CENTERLINE_VOXELS`` voxels. The one with more voxels is kept.
|
|
1370
|
+
A result above ``_MAX_CENTERLINE_VOXELS`` is replaced by an isotropic
|
|
1371
|
+
grid of that many voxels.
|
|
1355
1372
|
|
|
1356
1373
|
Parameters
|
|
1357
1374
|
----------
|
|
@@ -1359,9 +1376,6 @@ def centerline_grid_size(size, spacing, radius_mm):
|
|
|
1359
1376
|
Current voxel counts, one per axis.
|
|
1360
1377
|
spacing : sequence of float
|
|
1361
1378
|
Current spacing in millimeters, one per axis.
|
|
1362
|
-
radius_mm : float
|
|
1363
|
-
Maximum inscribed radius in millimeters. A non-positive or
|
|
1364
|
-
non-finite radius keeps the input grid.
|
|
1365
1379
|
|
|
1366
1380
|
Returns
|
|
1367
1381
|
-------
|
|
@@ -1369,70 +1383,66 @@ def centerline_grid_size(size, spacing, radius_mm):
|
|
|
1369
1383
|
new_spacing : tuple of float
|
|
1370
1384
|
Millimeters. Equal to ``spacing`` when the input grid is kept.
|
|
1371
1385
|
target_spacing : float or None
|
|
1372
|
-
|
|
1373
|
-
|
|
1386
|
+
Isotropic spacing in millimeters when the grid changes. None when
|
|
1387
|
+
the input grid is kept.
|
|
1374
1388
|
resample : bool
|
|
1375
1389
|
False when ``new_size`` is the input size.
|
|
1376
1390
|
"""
|
|
1377
1391
|
size_in = tuple(int(v) for v in np.asarray(size, dtype=np.int64).reshape(3))
|
|
1378
1392
|
spacing_in = tuple(
|
|
1379
1393
|
float(v) for v in np.asarray(spacing, dtype=np.float64).reshape(3))
|
|
1380
|
-
radius = float(radius_mm)
|
|
1381
|
-
if not np.isfinite(radius) or radius <= 0.0:
|
|
1382
|
-
return size_in, spacing_in, None, False
|
|
1383
|
-
|
|
1384
1394
|
size_f = np.asarray(size_in, dtype=np.float64)
|
|
1385
1395
|
spacing_f = np.asarray(spacing_in, dtype=np.float64)
|
|
1386
1396
|
extent = size_f * spacing_f
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1397
|
+
native_voxels = int(np.prod(np.asarray(size_in, dtype=np.int64)))
|
|
1398
|
+
|
|
1399
|
+
budget_size, budget_spacing = _isotropic_grid(
|
|
1400
|
+
extent, _TARGET_CENTERLINE_VOXELS)
|
|
1401
|
+
budget_voxels = int(np.prod(budget_size, dtype=np.int64))
|
|
1402
|
+
if budget_voxels > native_voxels:
|
|
1403
|
+
chosen_size = budget_size
|
|
1404
|
+
chosen_spacing = (budget_spacing, budget_spacing, budget_spacing)
|
|
1405
|
+
chosen_voxels = budget_voxels
|
|
1406
|
+
else:
|
|
1407
|
+
chosen_size = np.asarray(size_in, dtype=np.int64)
|
|
1408
|
+
chosen_spacing = spacing_in
|
|
1409
|
+
chosen_voxels = native_voxels
|
|
1410
|
+
|
|
1411
|
+
if chosen_voxels > _MAX_CENTERLINE_VOXELS:
|
|
1412
|
+
cap_size, cap_spacing = _isotropic_grid(
|
|
1413
|
+
extent, _MAX_CENTERLINE_VOXELS)
|
|
1414
|
+
chosen_size = cap_size
|
|
1415
|
+
chosen_spacing = (cap_spacing, cap_spacing, cap_spacing)
|
|
1416
|
+
|
|
1417
|
+
chosen_size_t = tuple(int(v) for v in chosen_size)
|
|
1418
|
+
if chosen_size_t == size_in:
|
|
1419
|
+
return size_in, spacing_in, None, False
|
|
1420
|
+
return chosen_size_t, tuple(float(v) for v in chosen_spacing), float(
|
|
1421
|
+
chosen_spacing[0]), True
|
|
1405
1422
|
|
|
1406
1423
|
|
|
1407
1424
|
def resample_for_centerline(segmentation, verbose=False):
|
|
1408
1425
|
"""Resample ``segmentation`` onto the centerline grid.
|
|
1409
1426
|
|
|
1410
|
-
The inscribed-radius distance map (positive inside) is returned when
|
|
1411
|
-
the grid is unchanged, so the caller can skip a second distance
|
|
1412
|
-
transform. A resampled image returns ``None`` for that map.
|
|
1413
|
-
|
|
1414
1427
|
Returns
|
|
1415
1428
|
-------
|
|
1416
1429
|
segmentation : sitk.Image
|
|
1417
|
-
distance_map : sitk.Image or None
|
|
1418
1430
|
was_resampled : bool
|
|
1419
1431
|
"""
|
|
1420
|
-
distance = distance_map_from_seg(segmentation) * -1
|
|
1421
|
-
radius = float(sitk.GetArrayViewFromImage(distance).max())
|
|
1422
1432
|
old_size = tuple(int(v) for v in segmentation.GetSize())
|
|
1423
1433
|
old_spacing = tuple(float(v) for v in segmentation.GetSpacing())
|
|
1424
1434
|
new_size, new_spacing, target, resample = centerline_grid_size(
|
|
1425
|
-
old_size, old_spacing
|
|
1435
|
+
old_size, old_spacing)
|
|
1426
1436
|
n_voxels = int(np.prod(new_size, dtype=np.int64))
|
|
1427
1437
|
if verbose:
|
|
1428
|
-
target_text = "
|
|
1438
|
+
target_text = "native" if target is None else f"{target:.4f} mm"
|
|
1429
1439
|
print(
|
|
1430
1440
|
f" Centerline grid: size {old_size} spacing {old_spacing} "
|
|
1431
|
-
f"
|
|
1432
|
-
f"
|
|
1441
|
+
f"-> size {new_size} spacing {new_spacing} "
|
|
1442
|
+
f"({n_voxels} voxels, target {target_text})"
|
|
1433
1443
|
)
|
|
1434
1444
|
if not resample:
|
|
1435
|
-
return segmentation,
|
|
1445
|
+
return segmentation, False
|
|
1436
1446
|
|
|
1437
1447
|
resampled = sitk.Resample(
|
|
1438
1448
|
segmentation,
|
|
@@ -1444,7 +1454,7 @@ def resample_for_centerline(segmentation, verbose=False):
|
|
|
1444
1454
|
segmentation.GetDirection(),
|
|
1445
1455
|
0,
|
|
1446
1456
|
segmentation.GetPixelID())
|
|
1447
|
-
return resampled,
|
|
1457
|
+
return resampled, True
|
|
1448
1458
|
|
|
1449
1459
|
|
|
1450
1460
|
def calc_centerline_fmm(segmentation, seed=None, targets=None,
|
|
@@ -1492,7 +1502,7 @@ def calc_centerline_fmm(segmentation, seed=None, targets=None,
|
|
|
1492
1502
|
if verbose:
|
|
1493
1503
|
print(f"Resolution of segmentation: {segmentation.GetSize()}")
|
|
1494
1504
|
|
|
1495
|
-
segmentation,
|
|
1505
|
+
segmentation, was_resampled = resample_for_centerline(
|
|
1496
1506
|
segmentation, verbose=verbose)
|
|
1497
1507
|
|
|
1498
1508
|
# Keep user-provided index coordinates anatomically consistent with
|
|
@@ -1516,9 +1526,7 @@ def calc_centerline_fmm(segmentation, seed=None, targets=None,
|
|
|
1516
1526
|
segmentation.TransformPhysicalPointToIndex(target_phys))
|
|
1517
1527
|
targets_resampled.append(target_resampled)
|
|
1518
1528
|
targets = targets_resampled
|
|
1519
|
-
|
|
1520
|
-
if distance_map_surf is None:
|
|
1521
|
-
distance_map_surf = distance_map_from_seg(segmentation) * -1
|
|
1529
|
+
distance_map_surf = distance_map_from_seg(segmentation) * -1
|
|
1522
1530
|
distance_map_surf_np = sitk.GetArrayFromImage(distance_map_surf).transpose(2, 1, 0)
|
|
1523
1531
|
# Get maximum value of distance map
|
|
1524
1532
|
max_surf = distance_map_surf_np.max()
|
|
@@ -3495,8 +3503,9 @@ def calc_multi_component_centerlines(segmentation, nr_seeds=None,
|
|
|
3495
3503
|
1. Analyzes the segmentation to find disconnected bodies
|
|
3496
3504
|
2. Creates optimal seed points for each disconnected body
|
|
3497
3505
|
3. Calculates centerlines using fast marching method for each seed.
|
|
3498
|
-
Each component's grid is
|
|
3499
|
-
voxel
|
|
3506
|
+
Each component's grid is the native grid or an isotropic 260 million
|
|
3507
|
+
voxel grid, whichever has more voxels, and is never larger than
|
|
3508
|
+
260 million voxels. See :func:`centerline_grid_size`.
|
|
3500
3509
|
4. Combines all centerlines into a unified polydata structure
|
|
3501
3510
|
|
|
3502
3511
|
Parameters
|
|
@@ -261,7 +261,7 @@ def resample_path_like_simvascular(
|
|
|
261
261
|
return spline_points
|
|
262
262
|
|
|
263
263
|
|
|
264
|
-
def reslice_size_for_unit(unit: str = "
|
|
264
|
+
def reslice_size_for_unit(unit: str = "mm") -> str:
|
|
265
265
|
"""SimVascular XML ``reslice_size`` for the project's physical units.
|
|
266
266
|
|
|
267
267
|
SimVascular expects ``5`` when coordinates are in centimeters and ``50``
|
|
@@ -279,7 +279,7 @@ _MM_CONTOUR_POINT_2D = 3.0
|
|
|
279
279
|
_MM_CONTOUR_POINT_3D = 5.0
|
|
280
280
|
|
|
281
281
|
|
|
282
|
-
def _physical_unit_scale(unit: str = "
|
|
282
|
+
def _physical_unit_scale(unit: str = "mm") -> float:
|
|
283
283
|
"""Scale factor from mm-native display sizes to project ``unit``."""
|
|
284
284
|
if str(unit).strip().lower() == "mm":
|
|
285
285
|
return 1.0
|
|
@@ -294,7 +294,7 @@ def _format_sv_display_size(value: float) -> str:
|
|
|
294
294
|
return f"{v:g}"
|
|
295
295
|
|
|
296
296
|
|
|
297
|
-
def path_point_display_sizes_for_unit(unit: str = "
|
|
297
|
+
def path_point_display_sizes_for_unit(unit: str = "mm") -> tuple:
|
|
298
298
|
"""
|
|
299
299
|
SimVascular path XML ``point_2D_display_size`` and ``point_size`` (3D).
|
|
300
300
|
|
|
@@ -307,7 +307,7 @@ def path_point_display_sizes_for_unit(unit: str = "cm") -> tuple:
|
|
|
307
307
|
)
|
|
308
308
|
|
|
309
309
|
|
|
310
|
-
def contour_point_display_sizes_for_unit(unit: str = "
|
|
310
|
+
def contour_point_display_sizes_for_unit(unit: str = "mm") -> tuple:
|
|
311
311
|
"""
|
|
312
312
|
SimVascular contour group XML ``point_2D_display_size`` and ``point_size`` (3D).
|
|
313
313
|
|
|
@@ -523,7 +523,7 @@ def create_pth(
|
|
|
523
523
|
calculation_number=100,
|
|
524
524
|
spacing=0.0,
|
|
525
525
|
further_subdivision_number=10,
|
|
526
|
-
unit="
|
|
526
|
+
unit="mm",
|
|
527
527
|
):
|
|
528
528
|
"""Create a SimVascular-style .pth XML file given a list of (x,y,z) points.
|
|
529
529
|
|
|
@@ -38,6 +38,7 @@ from .simvascular import (
|
|
|
38
38
|
_perpendicular_normal_unit,
|
|
39
39
|
compute_tangents,
|
|
40
40
|
contour_point_display_sizes_for_unit,
|
|
41
|
+
reslice_size_for_unit,
|
|
41
42
|
indent,
|
|
42
43
|
resample_path_like_simvascular,
|
|
43
44
|
)
|
|
@@ -669,12 +670,12 @@ def write_ctgr(
|
|
|
669
670
|
contours_xyz: Sequence[Optional[np.ndarray]],
|
|
670
671
|
output_path: str,
|
|
671
672
|
*,
|
|
672
|
-
reslice_size: str =
|
|
673
|
+
reslice_size: Optional[str] = None,
|
|
673
674
|
lofting_attribs: Optional[Dict[str, str]] = None,
|
|
674
675
|
contour_method: str = "LevelSet",
|
|
675
676
|
n_control_points: int = DEFAULT_SPLINE_POLYGON_N_CONTROL,
|
|
676
677
|
subdivision_spacing: Optional[float] = None,
|
|
677
|
-
unit: str = "
|
|
678
|
+
unit: str = "mm",
|
|
678
679
|
) -> str:
|
|
679
680
|
"""
|
|
680
681
|
Write a SimVascular ``.ctgr`` contour group XML.
|
|
@@ -690,6 +691,8 @@ def write_ctgr(
|
|
|
690
691
|
"""
|
|
691
692
|
if len(path_points) != len(contours_xyz):
|
|
692
693
|
raise ValueError("path_points and contours_xyz must have the same length")
|
|
694
|
+
if reslice_size is None:
|
|
695
|
+
reslice_size = reslice_size_for_unit(unit)
|
|
693
696
|
|
|
694
697
|
loft = dict(_DEFAULT_LOFTING_ATTRIBS)
|
|
695
698
|
if lofting_attribs:
|
|
@@ -712,7 +715,10 @@ def write_ctgr(
|
|
|
712
715
|
ET.SubElement(ts, "lofting_parameters", loft)
|
|
713
716
|
|
|
714
717
|
if subdivision_spacing is None or float(subdivision_spacing) <= 0.0:
|
|
715
|
-
|
|
718
|
+
# ~0.8 mm between contour samples; centimeter projects use one tenth.
|
|
719
|
+
subdivision_spacing = (
|
|
720
|
+
0.78125 if str(unit).strip().lower() == "mm" else 0.078125
|
|
721
|
+
)
|
|
716
722
|
subdiv_str = _fmt_coord(float(subdivision_spacing))
|
|
717
723
|
n_spline = max(4, int(n_control_points))
|
|
718
724
|
n_xml_ctrl = n_spline + SIMVASCULAR_SPLINE_RESERVED_CONTROLS
|
|
@@ -862,7 +868,7 @@ def write_ctgr_for_pth(
|
|
|
862
868
|
contour_method: str = "LevelSet",
|
|
863
869
|
component_selection: str = "closest_to_path",
|
|
864
870
|
n_control_points: int = DEFAULT_SPLINE_POLYGON_N_CONTROL,
|
|
865
|
-
unit: str = "
|
|
871
|
+
unit: str = "mm",
|
|
866
872
|
) -> str:
|
|
867
873
|
"""
|
|
868
874
|
Parse ``pth_path``, extract contours from ``sitk_volume``, write ``.ctgr``.
|
|
@@ -95,7 +95,7 @@ class TracingContext:
|
|
|
95
95
|
max_n_branches: int
|
|
96
96
|
max_n_steps_per_branch: int
|
|
97
97
|
global_config: Mapping[str, Any]
|
|
98
|
-
unit: str = "
|
|
98
|
+
unit: str = "mm"
|
|
99
99
|
scale: float = 1.0
|
|
100
100
|
seg_file: Optional[Union[str, sitk.Image]] = None
|
|
101
101
|
start_seg: Any = None
|
|
@@ -193,7 +193,7 @@ def trace_centerline(
|
|
|
193
193
|
max_n_branches,
|
|
194
194
|
max_n_steps_per_branch,
|
|
195
195
|
global_config,
|
|
196
|
-
unit='
|
|
196
|
+
unit='mm',
|
|
197
197
|
scale=1,
|
|
198
198
|
seg_file=None,
|
|
199
199
|
start_seg=None,
|
|
@@ -244,7 +244,7 @@ def trace_centerline(
|
|
|
244
244
|
global_config : dict
|
|
245
245
|
Configuration parameters
|
|
246
246
|
unit : str, optional
|
|
247
|
-
Image units ('
|
|
247
|
+
Image units ('mm' or 'cm'), default 'mm'
|
|
248
248
|
scale : float, optional
|
|
249
249
|
Scaling factor for image data, default 1
|
|
250
250
|
seg_file : str or sitk.Image, optional
|
|
@@ -109,7 +109,7 @@ def run_global_centerline_single(
|
|
|
109
109
|
case_name: Optional[str] = None,
|
|
110
110
|
merge_method: str = "clean",
|
|
111
111
|
directory_data: Optional[str] = None,
|
|
112
|
-
unit: str = "
|
|
112
|
+
unit: str = "mm",
|
|
113
113
|
global_config: Optional[Mapping[str, Any]] = None,
|
|
114
114
|
) -> None:
|
|
115
115
|
"""
|
|
@@ -193,7 +193,7 @@ def run_global_centerline_batch(
|
|
|
193
193
|
out_dir: Optional[str] = None,
|
|
194
194
|
merge_method: str = "clean",
|
|
195
195
|
directory_data: Optional[str] = None,
|
|
196
|
-
unit: str = "
|
|
196
|
+
unit: str = "mm",
|
|
197
197
|
global_config: Optional[Mapping[str, Any]] = None,
|
|
198
198
|
) -> None:
|
|
199
199
|
"""Batch variant of :func:`run_global_centerline_single` using filename-derived case ids.
|
|
@@ -67,6 +67,8 @@ def test_tracing_options_defaults():
|
|
|
67
67
|
assert opts.max_n_steps == 1000
|
|
68
68
|
assert opts.fold == "all"
|
|
69
69
|
assert opts.force_cpu is False
|
|
70
|
+
assert opts.unit == "mm"
|
|
71
|
+
assert opts.scale == 1.0
|
|
70
72
|
|
|
71
73
|
|
|
72
74
|
def test_run_tracing_builds_context_and_calls_tracing(monkeypatch):
|
|
@@ -168,39 +168,52 @@ def test_automatic_targets_reach_a_seed_away_from_the_widest_voxel():
|
|
|
168
168
|
assert all(successes)
|
|
169
169
|
|
|
170
170
|
|
|
171
|
-
def
|
|
172
|
-
"""A 0.5 mm crop with a 1.18 mm radius is already within 10% of the target."""
|
|
171
|
+
def test_centerline_grid_refines_the_s008_crop_to_260_million():
|
|
173
172
|
size, spacing, _target, resample = centerline_grid_size(
|
|
174
|
-
(
|
|
175
|
-
assert resample is
|
|
176
|
-
assert size == (
|
|
177
|
-
assert spacing ==
|
|
173
|
+
(325, 294, 252), (0.39453125, 0.39453125, 0.4))
|
|
174
|
+
assert resample is True
|
|
175
|
+
assert size == (715, 647, 562)
|
|
176
|
+
assert spacing[0] == spacing[1] == spacing[2]
|
|
177
|
+
assert abs(spacing[0] - 0.179) < 0.01
|
|
178
|
+
assert int(np.prod(size)) <= 260_000_000
|
|
179
|
+
assert size[0] > 325 and size[2] > 252
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_centerline_grid_refines_a_half_millimeter_crop_to_260_million():
|
|
183
|
+
size, spacing, _target, resample = centerline_grid_size(
|
|
184
|
+
(320, 289, 184), (0.5, 0.5, 0.5))
|
|
185
|
+
assert resample is True
|
|
186
|
+
assert size == (786, 710, 452)
|
|
187
|
+
assert spacing[0] == spacing[1] == spacing[2]
|
|
188
|
+
assert abs(spacing[0] - 0.204) < 0.01
|
|
189
|
+
assert int(np.prod(size)) <= 260_000_000
|
|
178
190
|
|
|
179
191
|
|
|
180
192
|
def test_centerline_grid_refines_a_thick_slice_axis():
|
|
181
193
|
size, spacing, _target, resample = centerline_grid_size(
|
|
182
|
-
(256, 256, 40), (0.7, 0.7, 3.0)
|
|
194
|
+
(256, 256, 40), (0.7, 0.7, 3.0))
|
|
183
195
|
assert resample is True
|
|
184
|
-
assert size == (
|
|
185
|
-
assert int(np.prod(size)) <=
|
|
196
|
+
assert size == (722, 722, 484)
|
|
197
|
+
assert int(np.prod(size)) <= 260_000_000
|
|
186
198
|
assert spacing[0] == spacing[1] == spacing[2]
|
|
187
|
-
assert spacing[2] <
|
|
199
|
+
assert spacing[2] < 0.7
|
|
188
200
|
|
|
189
201
|
|
|
190
|
-
def
|
|
202
|
+
def test_centerline_grid_caps_a_billion_voxels_at_260_million():
|
|
191
203
|
size, spacing, _target, resample = centerline_grid_size(
|
|
192
|
-
(1000, 1000, 1000), (0.5, 0.5, 0.5)
|
|
204
|
+
(1000, 1000, 1000), (0.5, 0.5, 0.5))
|
|
193
205
|
assert resample is True
|
|
194
|
-
assert
|
|
195
|
-
assert spacing[0]
|
|
196
|
-
assert
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
206
|
+
assert size == (638, 638, 638)
|
|
207
|
+
assert spacing[0] == spacing[1] == spacing[2]
|
|
208
|
+
assert abs(spacing[0] - 0.783) < 0.01
|
|
209
|
+
assert int(np.prod(size)) <= 260_000_000
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_centerline_grid_keeps_a_native_grid_at_the_budget():
|
|
213
|
+
size, spacing, target, resample = centerline_grid_size(
|
|
214
|
+
(638, 638, 638), (0.5, 0.5, 0.5))
|
|
215
|
+
assert resample is False
|
|
216
|
+
assert target is None
|
|
217
|
+
assert size == (638, 638, 638)
|
|
218
|
+
assert spacing == (0.5, 0.5, 0.5)
|
|
219
|
+
assert int(np.prod(size)) <= 260_000_000
|
|
@@ -356,6 +356,39 @@ def test_parser_run_single_cpu_flag():
|
|
|
356
356
|
]
|
|
357
357
|
)
|
|
358
358
|
assert ns.force_cpu is True
|
|
359
|
+
assert ns.unit == "mm"
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def test_parser_unit_defaults_to_mm():
|
|
363
|
+
parser = _build_parser()
|
|
364
|
+
batch = parser.parse_args(["run", "batch"])
|
|
365
|
+
assert batch.unit == "mm"
|
|
366
|
+
plus = parser.parse_args(["run", "plus", "batch"])
|
|
367
|
+
assert plus.unit == "mm"
|
|
368
|
+
post = parser.parse_args(
|
|
369
|
+
[
|
|
370
|
+
"post",
|
|
371
|
+
"global-centerline",
|
|
372
|
+
"single",
|
|
373
|
+
"--seg",
|
|
374
|
+
"/tmp/seg.mha",
|
|
375
|
+
"--out",
|
|
376
|
+
"/tmp/out.vtp",
|
|
377
|
+
]
|
|
378
|
+
)
|
|
379
|
+
assert post.unit == "mm"
|
|
380
|
+
post_batch = parser.parse_args(
|
|
381
|
+
[
|
|
382
|
+
"post",
|
|
383
|
+
"global-centerline",
|
|
384
|
+
"batch",
|
|
385
|
+
"--seg-dir",
|
|
386
|
+
"/tmp/segs",
|
|
387
|
+
"--seg-glob",
|
|
388
|
+
"*.mha",
|
|
389
|
+
]
|
|
390
|
+
)
|
|
391
|
+
assert post_batch.unit == "mm"
|
|
359
392
|
|
|
360
393
|
|
|
361
394
|
def test_parser_init_dataset():
|
|
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
|
|
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
|