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.
Files changed (94) hide show
  1. {seqseg-2.2.0 → seqseg-2.2.2}/PKG-INFO +1 -1
  2. {seqseg-2.2.0 → seqseg-2.2.2}/pyproject.toml +1 -1
  3. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/api.py +1 -1
  4. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/cli.py +4 -4
  5. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config_models.py +1 -1
  6. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/assembly.py +0 -1
  7. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/centerline.py +127 -30
  8. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/initialization.py +0 -1
  9. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/simvascular.py +5 -5
  10. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/simvascular_contour.py +10 -4
  11. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/tracing.py +3 -4
  12. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/post.py +2 -2
  13. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_api_entry.py +2 -0
  14. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_centerline_backtracking.py +53 -1
  15. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_cli_helpers.py +33 -0
  16. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg.egg-info/PKG-INFO +1 -1
  17. {seqseg-2.2.0 → seqseg-2.2.2}/LICENSE +0 -0
  18. {seqseg-2.2.0 → seqseg-2.2.2}/README.md +0 -0
  19. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/__init__.py +0 -0
  20. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/citations.py +0 -0
  21. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/__init__.py +0 -0
  22. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/aorta_tutorial.yaml +0 -0
  23. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global.yaml +0 -0
  24. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global_aorta.yaml +0 -0
  25. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global_cereb.yaml +0 -0
  26. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global_coro.yaml +0 -0
  27. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global_coro_retrace.yaml +0 -0
  28. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global_debug.yaml +0 -0
  29. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global_default.yaml +0 -0
  30. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global_pulm.yaml +0 -0
  31. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global_seg.yaml +0 -0
  32. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/global_test.yaml +0 -0
  33. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/sampler/default_cm.yaml +0 -0
  34. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/sampler/default_mm.yaml +0 -0
  35. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/config/sampler/test.yml +0 -0
  36. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/__init__.py +0 -0
  37. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/capping.py +0 -0
  38. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/checkpoint.py +0 -0
  39. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/datasets.py +0 -0
  40. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/evaluation.py +0 -0
  41. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/local_assembly.py +0 -0
  42. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/nnunet.py +0 -0
  43. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/params.py +0 -0
  44. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/prediction.py +0 -0
  45. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/sitk_functions.py +0 -0
  46. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/sweep.py +0 -0
  47. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/tracing_functions.py +0 -0
  48. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/vmtk_functions.py +0 -0
  49. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/modules/vtk_functions.py +0 -0
  50. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/parallel/old_auto_centerline.py +0 -0
  51. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/parallel/parallel_auto_centerline.py +0 -0
  52. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/parallel/parallel_auto_centerline2.py +0 -0
  53. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/parallel/parallel_auto_centerline3.py +0 -0
  54. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/parallel/parallel_auto_centerline4.py +0 -0
  55. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/__init__.py +0 -0
  56. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/classic.py +0 -0
  57. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/config.py +0 -0
  58. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/continue_run.py +0 -0
  59. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/directories.py +0 -0
  60. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/nnunet_cli.py +0 -0
  61. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/plus.py +0 -0
  62. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/predict.py +0 -0
  63. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/single_trace.py +0 -0
  64. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/pipeline/train.py +0 -0
  65. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/scripts/__init__.py +0 -0
  66. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/scripts/prepare_casx_dataset.py +0 -0
  67. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/scripts/taubin_smooth.py +0 -0
  68. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/seqseg.py +0 -0
  69. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/seqseg_plus.py +0 -0
  70. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/conftest.py +0 -0
  71. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_centerline_get_end_points.py +0 -0
  72. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_centerline_gradient.py +0 -0
  73. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_checkpoint.py +0 -0
  74. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_datasets_paths.py +0 -0
  75. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_global_prediction.py +0 -0
  76. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_nnunet2.py +0 -0
  77. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_nnunet_cli.py +0 -0
  78. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_nnunet_device.py +0 -0
  79. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_prepare_casx_dataset.py +0 -0
  80. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_resample_spacing.py +0 -0
  81. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_segmentation.py +0 -0
  82. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_single_trace_staging.py +0 -0
  83. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_start_seg.py +0 -0
  84. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_train_data.py +0 -0
  85. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_train_pipeline.py +0 -0
  86. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_user_paths.py +0 -0
  87. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/tests/test_vtk.py +0 -0
  88. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg/user_paths.py +0 -0
  89. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg.egg-info/SOURCES.txt +0 -0
  90. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg.egg-info/dependency_links.txt +0 -0
  91. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg.egg-info/entry_points.txt +0 -0
  92. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg.egg-info/requires.txt +0 -0
  93. {seqseg-2.2.0 → seqseg-2.2.2}/seqseg.egg-info/top_level.txt +0 -0
  94. {seqseg-2.2.0 → seqseg-2.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: seqseg
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: A deep learning-based medical image blood vessel tracking and segmentation tool.
5
5
  Author-email: Numi Sveinsson Cepero <numi@berkeley.com>
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "seqseg"
7
- version = "2.2.0"
7
+ version = "2.2.2"
8
8
  description = "A deep learning-based medical image blood vessel tracking and segmentation tool."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -122,7 +122,7 @@ class TracingOptions:
122
122
  write_samples: bool = False
123
123
  disk_io: bool = True
124
124
  simvascular: bool = False
125
- unit: str = "cm"
125
+ unit: str = "mm"
126
126
  scale: float = 1.0
127
127
  fold: str = "all"
128
128
  force_cpu: bool = False
@@ -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="cm", type=str)
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="cm", type=str)
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="cm", type=str)
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="cm", type=str)
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")
@@ -118,7 +118,7 @@ class TracingLimits:
118
118
  max_n_branches: int = 100
119
119
  max_n_steps_per_branch: int = 100
120
120
  write_samples: bool = False
121
- unit: str = "cm"
121
+ unit: str = "mm"
122
122
  scale: float = 1.0
123
123
 
124
124
 
@@ -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
- min_res=300, out_dir=None, write_files=False,
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
- # Resample if segmentation resolution is too low
1389
- if segmentation.GetSize()[2] < min_res:
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
- # Create distance map
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
- min_res=300, out_dir=None, write_files=False,
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,
@@ -255,7 +255,6 @@ def initialize_from_seg(segmentation,
255
255
  centerline, success_info = calc_multi_component_centerlines(
256
256
  segmentation,
257
257
  nr_seeds=2,
258
- min_res=500,
259
258
  out_dir=dir_output,
260
259
  write_files=write_samples,
261
260
  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 = "cm") -> 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 = "cm") -> float:
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 = "cm") -> tuple:
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 = "cm") -> tuple:
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="cm",
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 = "5",
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 = "cm",
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
- subdivision_spacing = 0.078125
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 = "cm",
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 = "cm"
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='cm',
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 ('cm' or 'mm'), default 'cm'
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 = "cm",
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 = "cm",
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, min_res=10,
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():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: seqseg
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: A deep learning-based medical image blood vessel tracking and segmentation tool.
5
5
  Author-email: Numi Sveinsson Cepero <numi@berkeley.com>
6
6
  License: Apache-2.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes