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.
Files changed (94) hide show
  1. {seqseg-2.2.1 → seqseg-2.2.2}/PKG-INFO +1 -1
  2. {seqseg-2.2.1 → seqseg-2.2.2}/pyproject.toml +1 -1
  3. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/api.py +1 -1
  4. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/cli.py +4 -4
  5. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config_models.py +1 -1
  6. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/centerline.py +66 -57
  7. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/simvascular.py +5 -5
  8. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/simvascular_contour.py +10 -4
  9. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/tracing.py +3 -3
  10. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/post.py +2 -2
  11. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_api_entry.py +2 -0
  12. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_centerline_backtracking.py +38 -25
  13. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_cli_helpers.py +33 -0
  14. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg.egg-info/PKG-INFO +1 -1
  15. {seqseg-2.2.1 → seqseg-2.2.2}/LICENSE +0 -0
  16. {seqseg-2.2.1 → seqseg-2.2.2}/README.md +0 -0
  17. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/__init__.py +0 -0
  18. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/citations.py +0 -0
  19. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/__init__.py +0 -0
  20. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/aorta_tutorial.yaml +0 -0
  21. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global.yaml +0 -0
  22. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global_aorta.yaml +0 -0
  23. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global_cereb.yaml +0 -0
  24. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global_coro.yaml +0 -0
  25. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global_coro_retrace.yaml +0 -0
  26. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global_debug.yaml +0 -0
  27. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global_default.yaml +0 -0
  28. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global_pulm.yaml +0 -0
  29. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global_seg.yaml +0 -0
  30. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/global_test.yaml +0 -0
  31. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/sampler/default_cm.yaml +0 -0
  32. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/sampler/default_mm.yaml +0 -0
  33. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/config/sampler/test.yml +0 -0
  34. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/__init__.py +0 -0
  35. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/assembly.py +0 -0
  36. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/capping.py +0 -0
  37. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/checkpoint.py +0 -0
  38. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/datasets.py +0 -0
  39. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/evaluation.py +0 -0
  40. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/initialization.py +0 -0
  41. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/local_assembly.py +0 -0
  42. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/nnunet.py +0 -0
  43. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/params.py +0 -0
  44. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/prediction.py +0 -0
  45. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/sitk_functions.py +0 -0
  46. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/sweep.py +0 -0
  47. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/tracing_functions.py +0 -0
  48. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/vmtk_functions.py +0 -0
  49. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/modules/vtk_functions.py +0 -0
  50. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/parallel/old_auto_centerline.py +0 -0
  51. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/parallel/parallel_auto_centerline.py +0 -0
  52. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/parallel/parallel_auto_centerline2.py +0 -0
  53. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/parallel/parallel_auto_centerline3.py +0 -0
  54. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/parallel/parallel_auto_centerline4.py +0 -0
  55. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/__init__.py +0 -0
  56. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/classic.py +0 -0
  57. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/config.py +0 -0
  58. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/continue_run.py +0 -0
  59. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/directories.py +0 -0
  60. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/nnunet_cli.py +0 -0
  61. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/plus.py +0 -0
  62. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/predict.py +0 -0
  63. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/single_trace.py +0 -0
  64. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/pipeline/train.py +0 -0
  65. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/scripts/__init__.py +0 -0
  66. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/scripts/prepare_casx_dataset.py +0 -0
  67. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/scripts/taubin_smooth.py +0 -0
  68. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/seqseg.py +0 -0
  69. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/seqseg_plus.py +0 -0
  70. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/conftest.py +0 -0
  71. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_centerline_get_end_points.py +0 -0
  72. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_centerline_gradient.py +0 -0
  73. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_checkpoint.py +0 -0
  74. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_datasets_paths.py +0 -0
  75. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_global_prediction.py +0 -0
  76. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_nnunet2.py +0 -0
  77. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_nnunet_cli.py +0 -0
  78. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_nnunet_device.py +0 -0
  79. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_prepare_casx_dataset.py +0 -0
  80. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_resample_spacing.py +0 -0
  81. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_segmentation.py +0 -0
  82. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_single_trace_staging.py +0 -0
  83. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_start_seg.py +0 -0
  84. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_train_data.py +0 -0
  85. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_train_pipeline.py +0 -0
  86. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_user_paths.py +0 -0
  87. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/tests/test_vtk.py +0 -0
  88. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg/user_paths.py +0 -0
  89. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg.egg-info/SOURCES.txt +0 -0
  90. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg.egg-info/dependency_links.txt +0 -0
  91. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg.egg-info/entry_points.txt +0 -0
  92. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg.egg-info/requires.txt +0 -0
  93. {seqseg-2.2.1 → seqseg-2.2.2}/seqseg.egg-info/top_level.txt +0 -0
  94. {seqseg-2.2.1 → 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.1
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.1"
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
 
@@ -1339,19 +1339,36 @@ def backtracking_gradient(gradient, distance_map_surf_np,
1339
1339
  return points, success
1340
1340
 
1341
1341
 
1342
- # Centerline grids are isotropic. Three voxels across the inscribed radius
1343
- # keeps the lumen wide enough for the finite-difference gradient. The
1344
- # spacing clamp refuses both a one-voxel tube and a needlessly fine grid,
1345
- # and the voxel budget caps the float64 gradient.
1346
- _VOXELS_PER_RADIUS = 3.0
1347
- _MIN_SPACING_MM = 0.35
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 centerline_grid_size(size, spacing, radius_mm):
1354
- """Choose an isotropic centerline grid from the inscribed radius.
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
- Spacing the radius and voxel budget asked for, before the
1373
- 10% size check. None when the radius does not define a grid.
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
- target = float(np.clip(
1388
- radius / _VOXELS_PER_RADIUS, _MIN_SPACING_MM, _MAX_SPACING_MM))
1389
- extent_prod = float(np.prod(extent))
1390
- if extent_prod > 0.0 and target > 0.0:
1391
- if extent_prod / (target ** 3) > _MAX_CENTERLINE_VOXELS:
1392
- target = (extent_prod / _MAX_CENTERLINE_VOXELS) ** (1.0 / 3.0)
1393
- new_size = np.maximum(1, np.rint(extent / target)).astype(np.int64)
1394
- # Rounding can land a hair over the budget the closed form aimed at.
1395
- while (int(np.prod(new_size, dtype=np.int64)) > _MAX_CENTERLINE_VOXELS
1396
- and target < 1.0e6):
1397
- target *= 1.01
1398
- new_size = np.maximum(1, np.rint(extent / target)).astype(np.int64)
1399
- if np.all(np.abs(new_size.astype(np.float64) - size_f)
1400
- <= _GRID_SIZE_TOLERANCE * np.maximum(size_f, 1.0)):
1401
- return size_in, spacing_in, float(target), False
1402
- return (tuple(int(v) for v in new_size),
1403
- (float(target), float(target), float(target)),
1404
- float(target), True)
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, radius)
1435
+ old_size, old_spacing)
1426
1436
  n_voxels = int(np.prod(new_size, dtype=np.int64))
1427
1437
  if verbose:
1428
- target_text = "n/a" if target is None else f"{target:.4f} mm"
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"radius {radius:.3f} mm target spacing {target_text} "
1432
- f"-> size {new_size} ({n_voxels} voxels)"
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, distance, False
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, None, True
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, distance_map_surf, was_resampled = resample_for_centerline(
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
- # The radius check already built this map when the grid was kept.
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 chosen from its inscribed radius and a
3499
- voxel budget; see :func:`centerline_grid_size`.
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 = "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
@@ -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):
@@ -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 test_centerline_grid_keeps_a_fine_isotropic_crop():
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
- (320, 289, 184), (0.5, 0.5, 0.5), 1.18)
175
- assert resample is False
176
- assert size == (320, 289, 184)
177
- assert spacing == (0.5, 0.5, 0.5)
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), 2.0)
194
+ (256, 256, 40), (0.7, 0.7, 3.0))
183
195
  assert resample is True
184
- assert size == (269, 269, 180)
185
- assert int(np.prod(size)) <= 20_000_000
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] < 1.0
199
+ assert spacing[2] < 0.7
188
200
 
189
201
 
190
- def test_centerline_grid_coarsens_to_the_voxel_budget():
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), 3.0)
204
+ (1000, 1000, 1000), (0.5, 0.5, 0.5))
193
205
  assert resample is True
194
- assert int(np.prod(size)) <= 20_000_000
195
- assert spacing[0] > 1.0
196
- assert size[0] < 1000
197
-
198
-
199
- def test_centerline_grid_keeps_the_input_when_radius_is_non_positive():
200
- for radius in (0.0, -1.0, float("nan")):
201
- size, spacing, target, resample = centerline_grid_size(
202
- (40, 30, 20), (0.8, 0.8, 2.0), radius)
203
- assert resample is False
204
- assert target is None
205
- assert size == (40, 30, 20)
206
- assert spacing == (0.8, 0.8, 2.0)
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.1
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