seqseg 2.2.0__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.0 → seqseg-2.2.2}/PKG-INFO +1 -1
- {seqseg-2.2.0 → seqseg-2.2.2}/pyproject.toml +1 -1
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/api.py +1 -1
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/cli.py +4 -4
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config_models.py +1 -1
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/assembly.py +0 -1
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/centerline.py +127 -30
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/initialization.py +0 -1
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/simvascular.py +5 -5
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/simvascular_contour.py +10 -4
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/tracing.py +3 -4
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/post.py +2 -2
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_api_entry.py +2 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_centerline_backtracking.py +53 -1
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_cli_helpers.py +33 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg.egg-info/PKG-INFO +1 -1
- {seqseg-2.2.0 → seqseg-2.2.2}/LICENSE +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/README.md +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/__init__.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/citations.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/__init__.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/aorta_tutorial.yaml +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global.yaml +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global_aorta.yaml +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global_cereb.yaml +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global_coro.yaml +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global_coro_retrace.yaml +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global_debug.yaml +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global_default.yaml +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global_pulm.yaml +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global_seg.yaml +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global_test.yaml +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/sampler/default_cm.yaml +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/sampler/default_mm.yaml +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/sampler/test.yml +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/__init__.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/capping.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/checkpoint.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/datasets.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/evaluation.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/local_assembly.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/nnunet.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/params.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/prediction.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/sitk_functions.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/sweep.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/tracing_functions.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/vmtk_functions.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/vtk_functions.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/parallel/old_auto_centerline.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/parallel/parallel_auto_centerline.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/parallel/parallel_auto_centerline2.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/parallel/parallel_auto_centerline3.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/parallel/parallel_auto_centerline4.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/__init__.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/classic.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/config.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/continue_run.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/directories.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/nnunet_cli.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/plus.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/predict.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/single_trace.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/train.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/scripts/__init__.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/scripts/prepare_casx_dataset.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/scripts/taubin_smooth.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/seqseg.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/seqseg_plus.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/conftest.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_centerline_get_end_points.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_centerline_gradient.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_checkpoint.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_datasets_paths.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_global_prediction.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_nnunet2.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_nnunet_cli.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_nnunet_device.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_prepare_casx_dataset.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_resample_spacing.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_segmentation.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_single_trace_staging.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_start_seg.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_train_data.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_train_pipeline.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_user_paths.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_vtk.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/user_paths.py +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg.egg-info/SOURCES.txt +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg.egg-info/dependency_links.txt +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg.egg-info/entry_points.txt +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg.egg-info/requires.txt +0 -0
- {seqseg-2.2.0 → seqseg-2.2.2}/seqseg.egg-info/top_level.txt +0 -0
- {seqseg-2.2.0 → 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")
|
|
@@ -1004,7 +1004,6 @@ def calc_centerline_global(predicted_vessels, initial_seeds,
|
|
|
1004
1004
|
centerline_poly, success_info = calc_multi_component_centerlines(
|
|
1005
1005
|
predicted_vessels,
|
|
1006
1006
|
nr_seeds=nr_seeds,
|
|
1007
|
-
min_res=500,
|
|
1008
1007
|
out_dir=None,
|
|
1009
1008
|
write_files=False,
|
|
1010
1009
|
move_target_if_fail=False,
|
|
@@ -1339,8 +1339,126 @@ def backtracking_gradient(gradient, distance_map_surf_np,
|
|
|
1339
1339
|
return points, success
|
|
1340
1340
|
|
|
1341
1341
|
|
|
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
|
|
1348
|
+
|
|
1349
|
+
|
|
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.
|
|
1372
|
+
|
|
1373
|
+
Parameters
|
|
1374
|
+
----------
|
|
1375
|
+
size : sequence of int
|
|
1376
|
+
Current voxel counts, one per axis.
|
|
1377
|
+
spacing : sequence of float
|
|
1378
|
+
Current spacing in millimeters, one per axis.
|
|
1379
|
+
|
|
1380
|
+
Returns
|
|
1381
|
+
-------
|
|
1382
|
+
new_size : tuple of int
|
|
1383
|
+
new_spacing : tuple of float
|
|
1384
|
+
Millimeters. Equal to ``spacing`` when the input grid is kept.
|
|
1385
|
+
target_spacing : float or None
|
|
1386
|
+
Isotropic spacing in millimeters when the grid changes. None when
|
|
1387
|
+
the input grid is kept.
|
|
1388
|
+
resample : bool
|
|
1389
|
+
False when ``new_size`` is the input size.
|
|
1390
|
+
"""
|
|
1391
|
+
size_in = tuple(int(v) for v in np.asarray(size, dtype=np.int64).reshape(3))
|
|
1392
|
+
spacing_in = tuple(
|
|
1393
|
+
float(v) for v in np.asarray(spacing, dtype=np.float64).reshape(3))
|
|
1394
|
+
size_f = np.asarray(size_in, dtype=np.float64)
|
|
1395
|
+
spacing_f = np.asarray(spacing_in, dtype=np.float64)
|
|
1396
|
+
extent = size_f * spacing_f
|
|
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
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
def resample_for_centerline(segmentation, verbose=False):
|
|
1425
|
+
"""Resample ``segmentation`` onto the centerline grid.
|
|
1426
|
+
|
|
1427
|
+
Returns
|
|
1428
|
+
-------
|
|
1429
|
+
segmentation : sitk.Image
|
|
1430
|
+
was_resampled : bool
|
|
1431
|
+
"""
|
|
1432
|
+
old_size = tuple(int(v) for v in segmentation.GetSize())
|
|
1433
|
+
old_spacing = tuple(float(v) for v in segmentation.GetSpacing())
|
|
1434
|
+
new_size, new_spacing, target, resample = centerline_grid_size(
|
|
1435
|
+
old_size, old_spacing)
|
|
1436
|
+
n_voxels = int(np.prod(new_size, dtype=np.int64))
|
|
1437
|
+
if verbose:
|
|
1438
|
+
target_text = "native" if target is None else f"{target:.4f} mm"
|
|
1439
|
+
print(
|
|
1440
|
+
f" Centerline grid: size {old_size} spacing {old_spacing} "
|
|
1441
|
+
f"-> size {new_size} spacing {new_spacing} "
|
|
1442
|
+
f"({n_voxels} voxels, target {target_text})"
|
|
1443
|
+
)
|
|
1444
|
+
if not resample:
|
|
1445
|
+
return segmentation, False
|
|
1446
|
+
|
|
1447
|
+
resampled = sitk.Resample(
|
|
1448
|
+
segmentation,
|
|
1449
|
+
list(new_size),
|
|
1450
|
+
sitk.Transform(),
|
|
1451
|
+
sitk.sitkNearestNeighbor,
|
|
1452
|
+
segmentation.GetOrigin(),
|
|
1453
|
+
list(new_spacing),
|
|
1454
|
+
segmentation.GetDirection(),
|
|
1455
|
+
0,
|
|
1456
|
+
segmentation.GetPixelID())
|
|
1457
|
+
return resampled, True
|
|
1458
|
+
|
|
1459
|
+
|
|
1342
1460
|
def calc_centerline_fmm(segmentation, seed=None, targets=None,
|
|
1343
|
-
|
|
1461
|
+
out_dir=None, write_files=False,
|
|
1344
1462
|
move_target_if_fail=False,
|
|
1345
1463
|
relax_factor=1, return_target=False,
|
|
1346
1464
|
return_target_all=False,
|
|
@@ -1381,27 +1499,11 @@ def calc_centerline_fmm(segmentation, seed=None, targets=None,
|
|
|
1381
1499
|
"""
|
|
1382
1500
|
output = None
|
|
1383
1501
|
original_segmentation = segmentation
|
|
1384
|
-
was_resampled = False
|
|
1385
1502
|
if verbose:
|
|
1386
1503
|
print(f"Resolution of segmentation: {segmentation.GetSize()}")
|
|
1387
1504
|
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
print(f" Resampling segmentation from size {segmentation.GetSize()} to size {min_res}")
|
|
1391
|
-
# Divide spacing so that the size is at least 50
|
|
1392
|
-
divide = segmentation.GetSize()[2] / min_res
|
|
1393
|
-
segmentation = sitk.Resample(segmentation,
|
|
1394
|
-
[int(x/divide) for x in segmentation
|
|
1395
|
-
.GetSize()],
|
|
1396
|
-
sitk.Transform(),
|
|
1397
|
-
sitk.sitkNearestNeighbor,
|
|
1398
|
-
segmentation.GetOrigin(),
|
|
1399
|
-
[x*divide for x in segmentation
|
|
1400
|
-
.GetSpacing()],
|
|
1401
|
-
segmentation.GetDirection(), 0,
|
|
1402
|
-
segmentation.GetPixelID())
|
|
1403
|
-
was_resampled = True
|
|
1404
|
-
print(f" New size: {segmentation.GetSize()}")
|
|
1505
|
+
segmentation, was_resampled = resample_for_centerline(
|
|
1506
|
+
segmentation, verbose=verbose)
|
|
1405
1507
|
|
|
1406
1508
|
# Keep user-provided index coordinates anatomically consistent with
|
|
1407
1509
|
# the resampled grid by mapping old-index -> physical -> new-index.
|
|
@@ -1424,10 +1526,7 @@ def calc_centerline_fmm(segmentation, seed=None, targets=None,
|
|
|
1424
1526
|
segmentation.TransformPhysicalPointToIndex(target_phys))
|
|
1425
1527
|
targets_resampled.append(target_resampled)
|
|
1426
1528
|
targets = targets_resampled
|
|
1427
|
-
|
|
1428
|
-
distance_map_surf = distance_map_from_seg(segmentation)
|
|
1429
|
-
# Switch sign of distance map
|
|
1430
|
-
distance_map_surf = distance_map_surf * -1
|
|
1529
|
+
distance_map_surf = distance_map_from_seg(segmentation) * -1
|
|
1431
1530
|
distance_map_surf_np = sitk.GetArrayFromImage(distance_map_surf).transpose(2, 1, 0)
|
|
1432
1531
|
# Get maximum value of distance map
|
|
1433
1532
|
max_surf = distance_map_surf_np.max()
|
|
@@ -3392,7 +3491,7 @@ def snap_end_points_to_lumen(end_points, radius_np, spacing, direction=None):
|
|
|
3392
3491
|
|
|
3393
3492
|
|
|
3394
3493
|
def calc_multi_component_centerlines(segmentation, nr_seeds=None,
|
|
3395
|
-
|
|
3494
|
+
out_dir=None, write_files=False,
|
|
3396
3495
|
move_target_if_fail=False, relax_factor=1,
|
|
3397
3496
|
verbose=False, return_failed=False,
|
|
3398
3497
|
post_process_kwargs=None):
|
|
@@ -3403,7 +3502,10 @@ def calc_multi_component_centerlines(segmentation, nr_seeds=None,
|
|
|
3403
3502
|
This function:
|
|
3404
3503
|
1. Analyzes the segmentation to find disconnected bodies
|
|
3405
3504
|
2. Creates optimal seed points for each disconnected body
|
|
3406
|
-
3. Calculates centerlines using fast marching method for each seed
|
|
3505
|
+
3. Calculates centerlines using fast marching method for each seed.
|
|
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`.
|
|
3407
3509
|
4. Combines all centerlines into a unified polydata structure
|
|
3408
3510
|
|
|
3409
3511
|
Parameters
|
|
@@ -3414,9 +3516,6 @@ def calc_multi_component_centerlines(segmentation, nr_seeds=None,
|
|
|
3414
3516
|
Number of seeds to generate. If None, creates one seed per disconnected body.
|
|
3415
3517
|
If the requested number exceeds the number of disconnected bodies, it will be
|
|
3416
3518
|
automatically reduced to match the number of available bodies with a warning.
|
|
3417
|
-
min_res : int, optional
|
|
3418
|
-
Minimum resolution for centerline calculation. If segmentation resolution
|
|
3419
|
-
is lower, it will be resampled. Defaults to 300.
|
|
3420
3519
|
out_dir : str, optional
|
|
3421
3520
|
Output directory for intermediate files. Defaults to None.
|
|
3422
3521
|
write_files : bool, optional
|
|
@@ -3592,7 +3691,6 @@ def calc_multi_component_centerlines(segmentation, nr_seeds=None,
|
|
|
3592
3691
|
segmentation_body,
|
|
3593
3692
|
seed=seed,
|
|
3594
3693
|
targets=None, # Let calc_centerline_fmm find targets automatically
|
|
3595
|
-
min_res=min_res,
|
|
3596
3694
|
out_dir=out_dir,
|
|
3597
3695
|
write_files=write_files,
|
|
3598
3696
|
move_target_if_fail=move_target_if_fail,
|
|
@@ -3944,7 +4042,6 @@ if __name__ == '__main__':
|
|
|
3944
4042
|
centerline_multi, success_info = calc_multi_component_centerlines(
|
|
3945
4043
|
segmentation,
|
|
3946
4044
|
nr_seeds=None,
|
|
3947
|
-
min_res=700,
|
|
3948
4045
|
out_dir=out_dir,
|
|
3949
4046
|
write_files=write_files,
|
|
3950
4047
|
move_target_if_fail=False,
|
|
@@ -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
|
|
@@ -745,7 +745,6 @@ def trace_centerline(
|
|
|
745
745
|
success) = calc_centerline_fmm(predicted_vessel,
|
|
746
746
|
seed,
|
|
747
747
|
targets,
|
|
748
|
-
min_res=40,
|
|
749
748
|
post_process_kwargs={
|
|
750
749
|
'merge_method': global_config.get(
|
|
751
750
|
'CENTERLINE_MERGE_METHOD',
|
|
@@ -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):
|
|
@@ -6,6 +6,7 @@ import SimpleITK as sitk
|
|
|
6
6
|
from seqseg.modules.centerline import (
|
|
7
7
|
backtracking_gradient,
|
|
8
8
|
calc_centerline_fmm,
|
|
9
|
+
centerline_grid_size,
|
|
9
10
|
cluster_map,
|
|
10
11
|
masked_world_frame_gradient,
|
|
11
12
|
snap_end_points_to_lumen,
|
|
@@ -160,8 +161,59 @@ def test_automatic_targets_reach_a_seed_away_from_the_widest_voxel():
|
|
|
160
161
|
seed = np.array([10.0, 6.0, 6.0])
|
|
161
162
|
|
|
162
163
|
_centerline, success, successes = calc_centerline_fmm(
|
|
163
|
-
image, seed=seed, targets=None,
|
|
164
|
+
image, seed=seed, targets=None,
|
|
164
165
|
return_success_list=True)
|
|
165
166
|
|
|
166
167
|
assert success
|
|
167
168
|
assert all(successes)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_centerline_grid_refines_the_s008_crop_to_260_million():
|
|
172
|
+
size, spacing, _target, resample = centerline_grid_size(
|
|
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
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_centerline_grid_refines_a_thick_slice_axis():
|
|
193
|
+
size, spacing, _target, resample = centerline_grid_size(
|
|
194
|
+
(256, 256, 40), (0.7, 0.7, 3.0))
|
|
195
|
+
assert resample is True
|
|
196
|
+
assert size == (722, 722, 484)
|
|
197
|
+
assert int(np.prod(size)) <= 260_000_000
|
|
198
|
+
assert spacing[0] == spacing[1] == spacing[2]
|
|
199
|
+
assert spacing[2] < 0.7
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_centerline_grid_caps_a_billion_voxels_at_260_million():
|
|
203
|
+
size, spacing, _target, resample = centerline_grid_size(
|
|
204
|
+
(1000, 1000, 1000), (0.5, 0.5, 0.5))
|
|
205
|
+
assert resample is True
|
|
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
|