seqseg 2.1.2__tar.gz → 2.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. {seqseg-2.1.2 → seqseg-2.1.4}/PKG-INFO +1 -1
  2. {seqseg-2.1.2 → seqseg-2.1.4}/pyproject.toml +1 -1
  3. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/cli.py +21 -1
  4. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/global_test.yaml +3 -1
  5. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/centerline.py +34 -18
  6. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/sitk_functions.py +33 -5
  7. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/vtk_functions.py +22 -2
  8. seqseg-2.1.4/seqseg/pipeline/train.py +618 -0
  9. seqseg-2.1.4/seqseg/tests/test_centerline_gradient.py +49 -0
  10. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_cli_helpers.py +42 -0
  11. seqseg-2.1.4/seqseg/tests/test_resample_spacing.py +57 -0
  12. seqseg-2.1.4/seqseg/tests/test_train_pipeline.py +347 -0
  13. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg.egg-info/PKG-INFO +1 -1
  14. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg.egg-info/SOURCES.txt +2 -0
  15. seqseg-2.1.2/seqseg/pipeline/train.py +0 -301
  16. seqseg-2.1.2/seqseg/tests/test_train_pipeline.py +0 -130
  17. {seqseg-2.1.2 → seqseg-2.1.4}/LICENSE +0 -0
  18. {seqseg-2.1.2 → seqseg-2.1.4}/README.md +0 -0
  19. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/__init__.py +0 -0
  20. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/api.py +0 -0
  21. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/citations.py +0 -0
  22. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/__init__.py +0 -0
  23. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/aorta_tutorial.yaml +0 -0
  24. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/global.yaml +0 -0
  25. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/global_aorta.yaml +0 -0
  26. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/global_cereb.yaml +0 -0
  27. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/global_coro.yaml +0 -0
  28. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/global_debug.yaml +0 -0
  29. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/global_default.yaml +0 -0
  30. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/global_pulm.yaml +0 -0
  31. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config/global_seg.yaml +0 -0
  32. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/config_models.py +0 -0
  33. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/__init__.py +0 -0
  34. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/assembly.py +0 -0
  35. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/capping.py +0 -0
  36. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/datasets.py +0 -0
  37. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/evaluation.py +0 -0
  38. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/initialization.py +0 -0
  39. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/local_assembly.py +0 -0
  40. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/nnunet.py +0 -0
  41. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/params.py +0 -0
  42. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/prediction.py +0 -0
  43. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/simvascular.py +0 -0
  44. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/simvascular_contour.py +0 -0
  45. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/sweep.py +0 -0
  46. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/tracing.py +0 -0
  47. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/tracing_functions.py +0 -0
  48. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/modules/vmtk_functions.py +0 -0
  49. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/parallel/old_auto_centerline.py +0 -0
  50. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/parallel/parallel_auto_centerline.py +0 -0
  51. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/parallel/parallel_auto_centerline2.py +0 -0
  52. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/parallel/parallel_auto_centerline3.py +0 -0
  53. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/parallel/parallel_auto_centerline4.py +0 -0
  54. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/pipeline/__init__.py +0 -0
  55. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/pipeline/classic.py +0 -0
  56. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/pipeline/config.py +0 -0
  57. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/pipeline/directories.py +0 -0
  58. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/pipeline/plus.py +0 -0
  59. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/pipeline/post.py +0 -0
  60. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/pipeline/single_trace.py +0 -0
  61. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/scripts/__init__.py +0 -0
  62. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/scripts/taubin_smooth.py +0 -0
  63. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/seqseg.py +0 -0
  64. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/seqseg_plus.py +0 -0
  65. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/conftest.py +0 -0
  66. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_api_entry.py +0 -0
  67. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_centerline_get_end_points.py +0 -0
  68. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_datasets_paths.py +0 -0
  69. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_nnunet2.py +0 -0
  70. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_nnunet_device.py +0 -0
  71. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_segmentation.py +0 -0
  72. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_single_trace_staging.py +0 -0
  73. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_start_seg.py +0 -0
  74. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_user_paths.py +0 -0
  75. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/tests/test_vtk.py +0 -0
  76. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg/user_paths.py +0 -0
  77. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg.egg-info/dependency_links.txt +0 -0
  78. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg.egg-info/entry_points.txt +0 -0
  79. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg.egg-info/requires.txt +0 -0
  80. {seqseg-2.1.2 → seqseg-2.1.4}/seqseg.egg-info/top_level.txt +0 -0
  81. {seqseg-2.1.2 → seqseg-2.1.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: seqseg
3
- Version: 2.1.2
3
+ Version: 2.1.4
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.1.2"
7
+ version = "2.1.4"
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"
@@ -671,12 +671,24 @@ def _cmd_train_prepare(ns: argparse.Namespace) -> None:
671
671
  also_test=ns.also_test,
672
672
  yes=ns.yes,
673
673
  verbose=ns.verbose,
674
+ img_ext=ns.img_ext,
674
675
  )
675
676
  except TrainDependencyError as e:
676
677
  print(str(e), file=sys.stderr)
677
678
  sys.exit(1)
678
679
  except Exception as e: # noqa: BLE001
680
+ import traceback
681
+
682
+ traceback.print_exc()
679
683
  print(f"seqseg train prepare failed: {e}", file=sys.stderr)
684
+ if "GetDataType" in str(e):
685
+ print(
686
+ "A VTK array was None while reading centerlines/ (or "
687
+ "rasterizing surfaces/). Centerline .vtp files need points "
688
+ "and MaximumInscribedSphereRadius (VMTK/SimVascular). "
689
+ "Update SeqSeg and re-run with --num-cores 1.",
690
+ file=sys.stderr,
691
+ )
680
692
  sys.exit(1)
681
693
 
682
694
  print("\nPrepare complete.")
@@ -1038,6 +1050,14 @@ def _build_parser() -> argparse.ArgumentParser:
1038
1050
  help="Write DatasetXXX_* here "
1039
1051
  "(default: seqseg paths nnunet_raw / $nnUNet_raw; else --outdir)",
1040
1052
  )
1053
+ p_prep.add_argument(
1054
+ "--img-ext",
1055
+ "-img_ext",
1056
+ default=None,
1057
+ type=str,
1058
+ help="Image/label suffix (.nii.gz, .mha, .nrrd, …). "
1059
+ "Default: detect from data_dir/images/; else sampler YAML IMG_EXT",
1060
+ )
1041
1061
  p_prep.add_argument("--perc-dataset", default=1.0, type=float)
1042
1062
  p_prep.add_argument("--num-cores", default=1, type=int)
1043
1063
  p_prep.add_argument("--start-from", default=0, type=int)
@@ -1075,7 +1095,7 @@ def _build_parser() -> argparse.ArgumentParser:
1075
1095
  )
1076
1096
  p_prep.add_argument("--yes", action="store_true", help="Non-interactive confirms")
1077
1097
  p_prep.add_argument("--verbose", action="store_true")
1078
- p_prep.set_defaults(_handler=_cmd_train_prepare)
1098
+ p_prep.set_defaults(_handler=_cmd_train_prepare, img_ext=None)
1079
1099
 
1080
1100
  p_nn = train_sub.add_parser(
1081
1101
  "nnunet",
@@ -33,7 +33,9 @@ USE_BUFFER: True # buffer the assembly (so not to mess with retrace prevention)
33
33
  BUFFER_N: 20 # add to assembly steps buff_N steps ago
34
34
  WEIGHTED_ASSEMBLY: True # if assembly should be weighted
35
35
  WEIGHT_TYPE: gaussian # type of weighting for assembly
36
- CENT_MAX_SPACING: 0.3 # mm; max spacing per dim for final assembly (refine only)
36
+ # Disabled for CI: the test CT is 512x512x258 (~0.65 mm). Refining the full
37
+ # assembly to 0.3 mm is ~1e9 voxels and OOMs GitHub-hosted runners (exit 137).
38
+ CENT_MAX_SPACING: null
37
39
 
38
40
  FORCEFUL_SIDEBRANCH: False
39
41
  FORCEFUL_SIDEBRANCH_MAGN_RADIUS: 1.1
@@ -598,6 +598,25 @@ def gradient_matrix(x):
598
598
  return gradients
599
599
 
600
600
 
601
+ def world_frame_gradient(grid_values, spacing, direction):
602
+ """
603
+ Convert a 3-D scalar field sampled on the image grid to a world-space
604
+ gradient (value per mm, components along the image direction axes).
605
+
606
+ ``np.gradient`` returns finite differences along array axes (per voxel).
607
+ Physical conversion is ``grad_world = D @ diag(1/spacing) @ grad_grid``.
608
+ Computation is in float64 so ITK FastMarching sentinels (~1e38 in
609
+ float32) do not overflow when scaled by ``1/spacing``.
610
+ """
611
+ values = np.asarray(grid_values, dtype=np.float64)
612
+ gradient = np.stack(np.gradient(values), axis=0)
613
+ spacing = np.asarray(spacing, dtype=np.float64).reshape(3)
614
+ inv_spacing = 1.0 / np.maximum(spacing, np.finfo(np.float64).tiny)
615
+ direction_matrix = np.asarray(direction, dtype=np.float64).reshape(3, 3)
616
+ gradient = gradient * inv_spacing[:, None, None, None]
617
+ return np.tensordot(direction_matrix, gradient, axes=([1], [0]))
618
+
619
+
601
620
  def hessian_matrix(x):
602
621
  """
603
622
  Calculate the hessian matrix with finite differences
@@ -1347,25 +1366,22 @@ def calc_centerline_fmm(segmentation, seed=None, targets=None,
1347
1366
  os.path.join(out_dir, 'masked_out_fmm.mha'))
1348
1367
  sitk.WriteImage(segmentation,
1349
1368
  os.path.join(out_dir, 'segmentation_from_fmm.mha'))
1350
- # Get gradient of distance map. ``np.gradient`` returns finite
1351
- # differences along the array (image-grid) axes, in units of "value
1352
- # per voxel". To use this to step a *physical* point we need a
1353
- # gradient in world-space units (value per mm, components along
1354
- # world axes). The conversion is:
1369
+ # Gradient of the *masked* arrival-time map. Unreached FastMarching
1370
+ # voxels keep ITK's float32 LargeValue (~1e38); differentiating that
1371
+ # field and scaling by 1/spacing overflows float32 (CI RuntimeWarning)
1372
+ # and yields inf/NaN steps. Mask + clip keeps the gradient inside the
1373
+ # vessel and finite. Conversion to world mm^-1 is:
1355
1374
  # grad_world = D @ diag(1/spacing) @ grad_grid
1356
- # where D is the SimpleITK direction matrix. Doing the conversion
1357
- # here once means every consumer (``backtracking_gradient``,
1358
- # ``interpolate_gradient``) gets a geometrically correct gradient
1359
- # without having to know about the image direction.
1360
- gradient = gradient_matrix(
1361
- sitk.GetArrayFromImage(output).transpose(2, 1, 0))
1362
- inv_spacing = 1.0 / np.asarray(segmentation.GetSpacing(),
1363
- dtype=np.float64)
1364
- direction_matrix = np.asarray(segmentation.GetDirection(),
1365
- dtype=np.float64).reshape(3, 3)
1366
- gradient = gradient * inv_spacing[:, None, None, None]
1367
- gradient = np.tensordot(direction_matrix, gradient,
1368
- axes=([1], [0]))
1375
+ arrival = np.clip(
1376
+ np.asarray(
1377
+ sitk.GetArrayFromImage(output_mask).transpose(2, 1, 0),
1378
+ dtype=np.float64,
1379
+ ),
1380
+ -1.0e6,
1381
+ 1.0e6,
1382
+ )
1383
+ gradient = world_frame_gradient(
1384
+ arrival, segmentation.GetSpacing(), segmentation.GetDirection())
1369
1385
  if verbose:
1370
1386
  # Calculate and print gradient magnitude statistics
1371
1387
  gradient_magnitude = np.sqrt(np.sum(gradient**2, axis=0))
@@ -55,7 +55,27 @@ def copy_settings(img, ref_img):
55
55
  return img
56
56
 
57
57
 
58
- def resample_to_spacing(image, new_spacing, is_label=False):
58
+ # Refuse pathological upsampling (e.g. CENT_MAX_SPACING in mm applied with
59
+ # ``-unit cm``) before SimpleITK tries to allocate a multi-billion-voxel grid
60
+ # and the process is SIGKILL'd (exit 137) on CI runners.
61
+ DEFAULT_MAX_RESAMPLE_VOXELS = 200_000_000
62
+
63
+
64
+ def resampled_grid_size(orig_size, orig_spacing, new_spacing):
65
+ """Voxel size that :func:`resample_to_spacing` would produce."""
66
+ orig_size = np.asarray(orig_size, dtype=np.int64)
67
+ orig_spacing = np.asarray(orig_spacing, dtype=np.float64)
68
+ target_spacing = np.asarray(new_spacing, dtype=np.float64)
69
+ if np.any(target_spacing <= 0):
70
+ raise ValueError(f"new_spacing must be > 0, got {target_spacing.tolist()}")
71
+ return np.maximum(
72
+ 1,
73
+ np.rint(orig_size * (orig_spacing / target_spacing)).astype(np.int64),
74
+ )
75
+
76
+
77
+ def resample_to_spacing(image, new_spacing, is_label=False,
78
+ max_voxels=DEFAULT_MAX_RESAMPLE_VOXELS):
59
79
  """
60
80
  Resample a SITK image to a target spacing while preserving physical extent.
61
81
 
@@ -63,15 +83,23 @@ def resample_to_spacing(image, new_spacing, is_label=False):
63
83
  image (sitk.Image): Input image.
64
84
  new_spacing (iterable): Target spacing [sx, sy, sz].
65
85
  is_label (bool): Use nearest-neighbor interpolation for labels.
86
+ max_voxels (int | None): If set, raise ``ValueError`` when the
87
+ output grid would exceed this many voxels instead of allocating.
66
88
  """
67
89
  orig_size = np.array(image.GetSize(), dtype=np.int64)
68
90
  orig_spacing = np.array(image.GetSpacing(), dtype=np.float64)
69
91
  target_spacing = np.array(new_spacing, dtype=np.float64)
70
92
 
71
- new_size = np.maximum(
72
- 1,
73
- np.rint(orig_size * (orig_spacing / target_spacing)).astype(np.int64)
74
- ).tolist()
93
+ new_size_arr = resampled_grid_size(orig_size, orig_spacing, target_spacing)
94
+ n_vox = int(np.prod(new_size_arr))
95
+ if max_voxels is not None and n_vox > int(max_voxels):
96
+ raise ValueError(
97
+ f"Resampling to spacing {target_spacing.tolist()} would create "
98
+ f"{tuple(int(s) for s in new_size_arr)} voxels ({n_vox:,}), "
99
+ f"exceeding max_voxels={int(max_voxels):,}. Check CENT_MAX_SPACING "
100
+ f"and -unit (mm vs cm) before refining a full-volume assembly."
101
+ )
102
+ new_size = new_size_arr.tolist()
75
103
 
76
104
  resample = sitk.ResampleImageFilter()
77
105
  resample.SetOutputSpacing(target_spacing.tolist())
@@ -87,10 +87,20 @@ class ClosestPoints:
87
87
 
88
88
 
89
89
  def collect_arrays(output):
90
+ """Convert VTK field/point/cell data arrays to numpy.
91
+
92
+ Skip unnamed arrays and non-numeric slots (``GetArray(i)`` is None for
93
+ string/abstract arrays). ``vtk_to_numpy(None)`` raises
94
+ ``'NoneType' object has no attribute 'GetDataType'``.
95
+ """
90
96
  res = {}
97
+ if output is None:
98
+ return res
91
99
  for i in range(output.GetNumberOfArrays()):
92
100
  name = output.GetArrayName(i)
93
101
  data = output.GetArray(i)
102
+ if name is None or data is None:
103
+ continue
94
104
  res[name] = v2n(data)
95
105
  return res
96
106
 
@@ -1284,8 +1294,18 @@ def convertPolyDataToImageData(poly, ref_im):
1284
1294
  output: resulted vtkImageData
1285
1295
  """
1286
1296
 
1287
- ref_im.GetPointData().SetScalars(
1288
- n2v(np.zeros(v2n(ref_im.GetPointData().GetScalars()).shape)))
1297
+ sc = ref_im.GetPointData().GetScalars()
1298
+ if sc is None:
1299
+ dims = ref_im.GetDimensions()
1300
+ n = int(dims[0]) * int(dims[1]) * int(dims[2])
1301
+ if n <= 0:
1302
+ raise RuntimeError(
1303
+ f"VTK image has no scalars and empty dimensions {dims}"
1304
+ )
1305
+ shape = n
1306
+ else:
1307
+ shape = v2n(sc).shape
1308
+ ref_im.GetPointData().SetScalars(n2v(np.zeros(shape)))
1289
1309
  ply2im = vtk.vtkPolyDataToImageStencil()
1290
1310
  ply2im.SetTolerance(0.05)
1291
1311
  ply2im.SetInputData(poly)