From 853700b0ead0ffc863053132f770f2ca91d37320 Mon Sep 17 00:00:00 2001 From: Stephen Aylward Date: Thu, 11 Jun 2026 08:26:35 -0400 Subject: [PATCH 1/2] ENH: Remove docstrings on itk.Image axis ordering. Remove ANTS option. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the project guideline requiring axis order and shape to be stated in every docstring and comment touching arrays. ITK's (Z, Y, X) numpy convention is well-known and was adding noise without value. Comments for non-standard orderings (pynrrd, ANTs vector images) are kept. Apply the same policy to source files: drop "Axis ordering:" subsections and inline (Z, Y, X) / (X, Y, Z) shape annotations from register_images_icon.py, image_tools.py, labelmap_tools.py, contour_tools.py, register_images_ants.py, register_images_greedy.py, convert_image_4d_to_3d.py, transform_tools.py, and test_tools.py. Fix all callers that passed legacy method-name strings to RegisterTimeSeriesImages: - "ANTS" / "ANTS_ICON" → "Greedy" / "Greedy_ICON" in tests, tutorial_08_dirlab_pca_time_series.py, and experiments/Reconstruct4DCT/reconstruct_4d_ct_class.py - "greedy" (lowercase) → "Greedy" in tests - set_number_of_iterations_ANTS() → set_number_of_iterations_greedy() at all affected call sites - Repurpose test_registrar_initialization_ANTS as test_registrar_initialization_Greedy_ICON to cover the previously untested combined pipeline RegisterImagesANTS is unchanged; REGISTRATION_METHODS stays ["Greedy", "ICON", "Greedy_ICON"]. Baselines for affected slow tests will need regeneration. --- AGENTS.md | 2 - CLAUDE.md | 1 - .../reconstruct_4d_ct_class.py | 22 +++--- src/physiomotion4d/contour_tools.py | 4 +- src/physiomotion4d/convert_image_4d_to_3d.py | 4 +- src/physiomotion4d/image_tools.py | 15 ---- src/physiomotion4d/labelmap_tools.py | 10 --- src/physiomotion4d/register_images_ants.py | 1 - src/physiomotion4d/register_images_greedy.py | 6 +- src/physiomotion4d/register_images_icon.py | 57 +++----------- src/physiomotion4d/test_tools.py | 6 -- src/physiomotion4d/transform_tools.py | 2 - tests/test_register_time_series_images.py | 78 ++++++++++--------- .../tutorial_08_dirlab_pca_time_series.py | 2 +- 14 files changed, 70 insertions(+), 140 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9e76207..dc92f93 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -153,8 +153,6 @@ Version bumping: `bumpver update --patch`, `--minor`, or `--major`. - Masks are ITK images with integer labels. Keep anatomy group IDs consistent across segmenters. - Transforms are ITK composite transforms stored in compressed `.hdf` files. -- State axis order and shape explicitly in every docstring and comment that - touches arrays. ## Implementation Role diff --git a/CLAUDE.md b/CLAUDE.md index af531fa..ec31fe1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,7 +88,6 @@ Regenerate it after any public API change: `py utils/generate_api_map.py` export by `vtk_to_usd.lps_points_to_usd` (USD +X=Left, +Y=Superior, +Z=Anterior) - Masks: ITK images with integer labels; consistent anatomy group IDs across all segmenters - Transforms: ITK composite transforms stored in `.hdf` files with compression -- State axis order and shape explicitly in every docstring and comment that touches arrays ## Testing diff --git a/experiments/Reconstruct4DCT/reconstruct_4d_ct_class.py b/experiments/Reconstruct4DCT/reconstruct_4d_ct_class.py index 70bd246..b15deb6 100644 --- a/experiments/Reconstruct4DCT/reconstruct_4d_ct_class.py +++ b/experiments/Reconstruct4DCT/reconstruct_4d_ct_class.py @@ -53,22 +53,22 @@ num_files = len(files) reference_image_num = num_files // 2 - # Registration parameters - only ANTs for quick run - registration_methods = ["ANTS", "ICON", "ANTS_ICON"] - number_of_iterations_list = [[8, 4, 1], 5, [[8, 4, 1], 5]] # For ANTs and ICON + # Registration parameters - only Greedy for quick run + registration_methods = ["Greedy", "ICON", "Greedy_ICON"] + number_of_iterations_list = [[8, 4, 1], 5, [[8, 4, 1], 5]] # For Greedy and ICON else: print("=== FULL RUN MODE ===") num_files = len(files) files_indx = list(range(num_files)) reference_image_num = 7 - # Registration parameters - both ANTs and ICON for full run - registration_methods = ["ANTS"] # , "ICON", "ANTS_ICON"] + # Registration parameters - Greedy and ICON for full run + registration_methods = ["Greedy"] # , "ICON", "Greedy_ICON"] number_of_iterations_list = [ [30, 15, 7, 3], - ] # For ANTs + ] # For Greedy # 20, # For ICON - # [[30, 15, 7, 3], 20], # For ANTS_ICON + # [[30, 15, 7, 3], 20], # For Greedy_ICON # ] # Common parameters @@ -144,12 +144,12 @@ registrar.set_fixed_image(fixed_image) # Set iterations based on registration method - if registration_method == "ANTS": - registrar.set_number_of_iterations_ANTS(number_of_iterations) + if registration_method == "Greedy": + registrar.set_number_of_iterations_greedy(number_of_iterations) elif registration_method == "ICON": registrar.set_number_of_iterations_ICON(number_of_iterations) - elif registration_method == "ANTS_ICON": - registrar.set_number_of_iterations_ANTS(number_of_iterations[0]) + elif registration_method == "Greedy_ICON": + registrar.set_number_of_iterations_greedy(number_of_iterations[0]) registrar.set_number_of_iterations_ICON(number_of_iterations[1]) # Perform registration diff --git a/src/physiomotion4d/contour_tools.py b/src/physiomotion4d/contour_tools.py index 6e72efe..c32e076 100644 --- a/src/physiomotion4d/contour_tools.py +++ b/src/physiomotion4d/contour_tools.py @@ -214,9 +214,7 @@ def create_mask_from_mesh( voxel_grid_origin_lps[2] + voxel_pitch * binary_array.shape[2] ) - # Create ITK image from the voxel array - # ITK uses ZYX ordering (numpy array convention), trimesh uses XYZ - # Need to transpose: (X, Y, Z) -> (Z, Y, X) + # transpose to match trimesh XYZ convention binary_array_zyx = np.transpose(binary_array, (2, 1, 0)) binary_array_flip = np.flip(binary_array_zyx, axis=0) binary_image = itk.GetImageFromArray(binary_array_flip) diff --git a/src/physiomotion4d/convert_image_4d_to_3d.py b/src/physiomotion4d/convert_image_4d_to_3d.py index 642510b..88b47ba 100644 --- a/src/physiomotion4d/convert_image_4d_to_3d.py +++ b/src/physiomotion4d/convert_image_4d_to_3d.py @@ -32,7 +32,7 @@ class ConvertImage4DTo3D(PhysioMotion4DBase): - """Split a 3D/4D ITK image (X, Y, Z [, T]) into a list of 3D ITK images.""" + """Split a 3D/4D ITK image into a list of 3D ITK images.""" def __init__(self, log_level: int | str = logging.INFO) -> None: """Initialize the 4D-to-3D image converter. @@ -178,7 +178,7 @@ def _build_frames( spacing_3d: np.ndarray, direction_3d: np.ndarray, ) -> None: - """Materialize ``self.img_3d`` from a (T, Z, Y, X) array + geometry.""" + """Materialize ``self.img_3d`` from a time-series array + geometry.""" direction_matrix = itk.matrix_from_array(np.ascontiguousarray(direction_3d)) self.img_3d = [] for t in range(arr_4d.shape[0]): diff --git a/src/physiomotion4d/image_tools.py b/src/physiomotion4d/image_tools.py index c031603..e47882b 100644 --- a/src/physiomotion4d/image_tools.py +++ b/src/physiomotion4d/image_tools.py @@ -104,17 +104,11 @@ def convert_itk_image_to_sitk(self, itk_image: itk.Image) -> sitk.Image: Returns: SimpleITK image with identical data and metadata - Note: - Memory layout is preserved during conversion. Both ITK and SimpleITK - use (z, y, x) ordering for 3D images in numpy arrays. - Example: >>> tools = ImageTools() >>> itk_image = itk.imread('image.nii.gz') >>> sitk_image = tools.convert_itk_image_to_sitk(itk_image) """ - # Get numpy array from ITK image - # ITK array is in (z, y, x) or (z, y, x, components) format array = itk.array_from_image(itk_image) # Get image metadata @@ -129,11 +123,8 @@ def convert_itk_image_to_sitk(self, itk_image: itk.Image) -> sitk.Image: is_vector = n_components > 1 if is_vector: - # For vector images, SimpleITK expects (z, y, x, components) which is what we have - # Create SimpleITK image from numpy array sitk_image = sitk.GetImageFromArray(array, isVector=True) else: - # For scalar images, array is (z, y, x) sitk_image = sitk.GetImageFromArray(array, isVector=False) # Set metadata @@ -162,17 +153,11 @@ def convert_sitk_image_to_itk(self, sitk_image: sitk.Image) -> itk.Image: Returns: ITK image with identical data and metadata - Note: - Memory layout is preserved during conversion. Both SimpleITK and ITK - use (z, y, x) ordering for 3D images in numpy arrays. - Example: >>> tools = ImageTools() >>> sitk_image = sitk.ReadImage('image.nii.gz') >>> itk_image = tools.convert_sitk_image_to_itk(sitk_image) """ - # Get numpy array from SimpleITK image - # SimpleITK array is in (z, y, x) or (z, y, x, components) format array = sitk.GetArrayFromImage(sitk_image) # Get image metadata diff --git a/src/physiomotion4d/labelmap_tools.py b/src/physiomotion4d/labelmap_tools.py index 08fe4e6..9e28231 100644 --- a/src/physiomotion4d/labelmap_tools.py +++ b/src/physiomotion4d/labelmap_tools.py @@ -61,11 +61,6 @@ def convert_labelmap_to_mask( even on anisotropic grids; each per-axis count is clamped to at least 1 voxel when ``dilation_in_mm > 0``. - Axis ordering: the labelmap is a scalar 3D ``itk.Image`` in ITK - world-axis order (X, Y, Z). All thresholding is performed on the numpy - view (Z, Y, X) and written back through ``CopyInformation``, so origin, - spacing, and direction are preserved. - Args: labelmap: Multi-label or binary ``itk.Image``. Any non-zero voxel that is not excluded is treated as foreground. @@ -132,11 +127,6 @@ def create_distance_map( this continuous encoding gives every region a smoothly varying signal while preserving label identity. - Axis ordering: ``labelmap`` is a scalar 3D ``itk.Image`` in ITK - world-axis order (X, Y, Z). All work is done on the numpy view - (Z, Y, X) and written back through ``CopyInformation``, so origin, - spacing, and direction are preserved. - Args: labelmap: Multi-label (or binary) ``itk.Image`` of integer labels. max_distance_mm: Distance clip, in millimeters. Default 20.0. diff --git a/src/physiomotion4d/register_images_ants.py b/src/physiomotion4d/register_images_ants.py index d31552c..bfda419 100644 --- a/src/physiomotion4d/register_images_ants.py +++ b/src/physiomotion4d/register_images_ants.py @@ -193,7 +193,6 @@ def _itk_to_ants_image( raise ValueError(f"Unsupported dtype: {dtype}") if is_vector: - # Vector images: ITK gives (z,y,x,components) or (y,x,components) spatial_shape = data.shape[:-1] # drop components else: spatial_shape = data.shape diff --git a/src/physiomotion4d/register_images_greedy.py b/src/physiomotion4d/register_images_greedy.py index 9e3016f..c2c41f5 100644 --- a/src/physiomotion4d/register_images_greedy.py +++ b/src/physiomotion4d/register_images_greedy.py @@ -155,8 +155,8 @@ def _metric_downsample_scale(self, reference_image: itk.Image) -> float: downsampled grid lands at or just below the cap. Args: - reference_image: The fixed metric image (X, Y, Z) whose voxel count - drives the Greedy multi-component buffer size. + reference_image: The fixed metric image whose voxel count drives + the Greedy multi-component buffer size. Returns: Per-axis resampling scale in ``(0, 1]``. @@ -183,7 +183,7 @@ def _downsample_image( The physical extent is preserved exactly: the new per-axis spacing is chosen so ``new_size * new_spacing == old_size * old_spacing``, so the coarser grid covers the same world-space region with the same origin - and direction. Axis order is ITK world order (X, Y, Z). + and direction. Args: image: Scalar 3D ``itk.Image`` to resample. diff --git a/src/physiomotion4d/register_images_icon.py b/src/physiomotion4d/register_images_icon.py index e157b70..91a6dda 100644 --- a/src/physiomotion4d/register_images_icon.py +++ b/src/physiomotion4d/register_images_icon.py @@ -321,33 +321,13 @@ def _image_to_resized_tensor( Mirrors the trilinear preprocessing path used by ``icon_registration.itk_wrapper.register_pair`` exactly. - - Axis ordering: - - Input ``image`` is a scalar (single-channel) 3D ``itk.Image`` with - ITK world-axis order (X, Y, Z). ``np.array(image)`` returns a - C-contiguous array with axes **reversed** to (Z, Y, X) — ITK's - standard numpy view. - - ``torch.Tensor(arr)`` casts to ``float32`` (PyTorch's - ``FloatTensor`` constructor) regardless of the source dtype. - - Indexing with ``[None, None]`` prepends batch and channel - singleton axes, producing shape ``(1, 1, Z, Y, X)``. This is - PyTorch's NCDHW layout where ``D=Z``, ``H=Y``, ``W=X``. - - ``shape`` is ``self.net.identity_map.shape`` (5D, NCDHW); the - target spatial size is ``shape[2:] = (D_out, H_out, W_out)``. - - Return shape: ``(1, 1, D_out, H_out, W_out)``, float32, - C-contiguous on ``icon.config.device``. + ``[None, None]`` prepends batch and channel singletons; ``shape`` is + ``self.net.identity_map.shape`` (5D NCDHW). Notes: - - Single-channel scalar inputs only. Vector/multi-channel - ``itk.Image`` would yield ``(Z, Y, X, C)`` from ``np.array`` and - break the assumed NCDHW layout — not supported here, matching - ICON's own preprocessing. - - No explicit time axis: 4D series must be split into 3D - timepoints by the caller; pairs are processed one volume at a - time. - - No transpose is performed; the (Z, Y, X) numpy ordering is - consumed directly as (D, H, W). Voxel values, not world - coordinates, drive the trilinear resample. + - Single-channel scalar inputs only; vector ``itk.Image`` inputs + are not supported, matching ICON's own preprocessing. + - 4D series must be split into 3D timepoints by the caller. """ arr = np.array(image) tensor = torch.Tensor(arr).to(icon.config.device)[None, None] @@ -362,29 +342,14 @@ def _mask_to_resized_tensor( Mirrors the mask preprocessing used by ``icon_registration.itk_wrapper.register_pair_with_mask`` exactly. - - Axis ordering: - - Input ``mask`` is a scalar (single-channel) 3D ``itk.Image`` - (typically ``uint8``/short labels) with ITK world-axis order - (X, Y, Z). ``np.array(mask)`` returns a C-contiguous array with - axes **reversed** to (Z, Y, X). - - ``torch.Tensor(arr)`` casts to ``float32`` (label values become - integral-valued floats; nearest-neighbor resampling preserves - them). - - ``[None, None]`` prepends batch and channel singletons → - ``(1, 1, Z, Y, X)`` in NCDHW (``D=Z``, ``H=Y``, ``W=X``). - - Target spatial size is ``shape[2:] = (D_out, H_out, W_out)`` - from ``self.net.identity_map.shape``. - - Return shape: ``(1, 1, D_out, H_out, W_out)``, float32, - C-contiguous on ``icon.config.device``. + ``[None, None]`` prepends batch and channel singletons; ``shape`` is + ``self.net.identity_map.shape`` (5D NCDHW). Notes: - - Single-channel mask inputs only; multi-label masks are encoded - as scalar integer values, not channels. Vector ``itk.Image`` - inputs are not supported. - - No time axis: per-volume 3D processing. - - Resampling uses ``mode='nearest'`` (no ``align_corners``) so - label identities are preserved. + - Single-channel mask inputs only; multi-label masks are scalar + integer values, not channels. Vector ``itk.Image`` inputs are + not supported. + - ``mode='nearest'`` preserves label identities. """ arr = np.array(mask) tensor = torch.Tensor(arr).to(icon.config.device)[None, None] diff --git a/src/physiomotion4d/test_tools.py b/src/physiomotion4d/test_tools.py index 0889a55..653e7e8 100644 --- a/src/physiomotion4d/test_tools.py +++ b/src/physiomotion4d/test_tools.py @@ -519,12 +519,6 @@ def save_screenshot_image_slice( ) -> Path: """Extract one slice from an ITK image and save a PNG via matplotlib. - The numpy array from ``itk.array_view_from_image`` has shape ``(Z, Y, X)`` - (ITK stores X fastest; numpy reverses the axis order). Axis indices: - - axis=0: axial (constant-Z plane) - - axis=1: coronal (constant-Y plane) - - axis=2: sagittal (constant-X plane) - Saves to the configured result artifact directory. Args: diff --git a/src/physiomotion4d/transform_tools.py b/src/physiomotion4d/transform_tools.py index a1f4197..9400825 100644 --- a/src/physiomotion4d/transform_tools.py +++ b/src/physiomotion4d/transform_tools.py @@ -888,7 +888,6 @@ def convert_itk_transform_to_usd_visualization( tfm, reference_image ) - # Get displacement field as numpy array (z, y, x, 3) displacement_array = itk.GetArrayFromImage(displacement_field) # Get image size @@ -978,7 +977,6 @@ def _create_arrow_visualization( for k in range(size[2]): for j in range(size[1]): for i in range(size[0]): - # Get displacement vector at this point (z, y, x, 3) displacement = displacement_array[k, j, i, :] # Calculate magnitude diff --git a/tests/test_register_time_series_images.py b/tests/test_register_time_series_images.py index 24f3a1b..8e3e921 100644 --- a/tests/test_register_time_series_images.py +++ b/tests/test_register_time_series_images.py @@ -26,27 +26,29 @@ class TestRegisterTimeSeriesImages: _class_name = "registration_time_series_images" - def test_registrar_initialization_ANTS(self) -> None: - """Test that RegisterTimeSeriesImages initializes correctly with ANTs.""" - registrar = RegisterTimeSeriesImages(registration_method="ANTS") + def test_registrar_initialization_Greedy_ICON(self) -> None: + """Test that RegisterTimeSeriesImages initializes correctly with Greedy_ICON.""" + registrar = RegisterTimeSeriesImages(registration_method="Greedy_ICON") assert registrar is not None, "Registrar not initialized" - assert registrar.registration_method_name == "ANTS", "Method not set correctly" - assert registrar.registrar_ANTS is not None, ( - "Internal ANTs registrar not created" + assert registrar.registration_method_name == "Greedy_ICON", ( + "Method not set correctly" + ) + assert registrar.registrar_greedy is not None, ( + "Internal Greedy registrar not created" ) assert registrar.registrar_ICON is not None, ( "Internal ICON registrar not created" ) - print("\nTime series registrar initialized with ANTs") + print("\nTime series registrar initialized with Greedy_ICON") def test_registrar_initialization_ICON(self) -> None: """Test that RegisterTimeSeriesImages initializes correctly with ICON.""" registrar = RegisterTimeSeriesImages(registration_method="ICON") assert registrar is not None, "Registrar not initialized" assert registrar.registration_method_name == "ICON", "Method not set correctly" - assert registrar.registrar_ANTS is not None, ( - "Internal ANTs registrar not created" + assert registrar.registrar_greedy is not None, ( + "Internal Greedy registrar not created" ) assert registrar.registrar_ICON is not None, ( "Internal ICON registrar not created" @@ -56,9 +58,9 @@ def test_registrar_initialization_ICON(self) -> None: def test_registrar_initialization_greedy(self) -> None: """Test that RegisterTimeSeriesImages initializes correctly with Greedy.""" - registrar = RegisterTimeSeriesImages(registration_method="greedy") + registrar = RegisterTimeSeriesImages(registration_method="Greedy") assert registrar is not None, "Registrar not initialized" - assert registrar.registration_method_name == "greedy", ( + assert registrar.registration_method_name == "Greedy", ( "Method not set correctly" ) assert registrar.registrar_greedy is not None, ( @@ -79,7 +81,7 @@ def test_registrar_initialization_invalid_method(self) -> None: def test_set_modality(self) -> None: """Test setting imaging modality.""" - registrar = RegisterTimeSeriesImages(registration_method="ANTS") + registrar = RegisterTimeSeriesImages(registration_method="Greedy") registrar.set_modality("ct") assert registrar.modality == "ct", "Modality not set correctly" @@ -87,7 +89,7 @@ def test_set_modality(self) -> None: def test_set_fixed_image(self, test_images: list[Any]) -> None: """Test setting fixed image.""" - registrar = RegisterTimeSeriesImages(registration_method="ANTS") + registrar = RegisterTimeSeriesImages(registration_method="Greedy") fixed_image = test_images[0] registrar.set_fixed_image(fixed_image) @@ -98,15 +100,17 @@ def test_set_fixed_image(self, test_images: list[Any]) -> None: def test_set_number_of_iterations(self) -> None: """Test setting number of iterations.""" - registrar_ANTS = RegisterTimeSeriesImages(registration_method="ANTS") - iterations_ANTS = [30, 15, 5] - - registrar_ANTS.set_number_of_iterations_ANTS(iterations_ANTS) - assert registrar_ANTS.number_of_iterations_ANTS == iterations_ANTS, ( - "ANTs iterations not set correctly" + registrar_Greedy_ICON = RegisterTimeSeriesImages( + registration_method="Greedy_ICON" ) + iterations_Greedy_ICON = [30, 15, 5] + + registrar_Greedy_ICON.set_number_of_iterations_greedy(iterations_Greedy_ICON) + assert ( + registrar_Greedy_ICON.number_of_iterations_greedy == iterations_Greedy_ICON + ), "Greedy_ICON iterations not set correctly" - registrar_greedy = RegisterTimeSeriesImages(registration_method="greedy") + registrar_greedy = RegisterTimeSeriesImages(registration_method="Greedy") iterations_greedy = [25, 10, 3] registrar_greedy.set_number_of_iterations_greedy(iterations_greedy) @@ -136,10 +140,10 @@ def test_register_time_series_basic( print(f" Fixed image: {itk.size(fixed_image)}") print(f" Number of moving images: {len(moving_images)}") - registrar = RegisterTimeSeriesImages(registration_method="ANTS") + registrar = RegisterTimeSeriesImages(registration_method="Greedy") registrar.set_modality("ct") registrar.set_fixed_image(fixed_image) - registrar.set_number_of_iterations_ANTS([20, 10, 2]) + registrar.set_number_of_iterations_greedy([20, 10, 2]) result = registrar.register_time_series( moving_images=moving_images, @@ -217,10 +221,10 @@ def test_register_time_series_with_prior( print(f" Number of moving images: {len(moving_images)}") print(" Using prior transform weight: 0.5") - registrar = RegisterTimeSeriesImages(registration_method="ANTS") + registrar = RegisterTimeSeriesImages(registration_method="Greedy") registrar.set_modality("ct") registrar.set_fixed_image(fixed_image) - registrar.set_number_of_iterations_ANTS([20, 10, 2]) + registrar.set_number_of_iterations_greedy([20, 10, 2]) result = registrar.register_time_series( moving_images=moving_images, @@ -274,10 +278,10 @@ def test_register_time_series_identity_start(self, test_images: list[Any]) -> No print("\nRegistering time series (identity start)...") - registrar = RegisterTimeSeriesImages(registration_method="ANTS") + registrar = RegisterTimeSeriesImages(registration_method="Greedy") registrar.set_modality("ct") registrar.set_fixed_image(fixed_image) - registrar.set_number_of_iterations_ANTS([20, 10, 2]) + registrar.set_number_of_iterations_greedy([20, 10, 2]) result = registrar.register_time_series( moving_images=moving_images, @@ -302,10 +306,10 @@ def test_register_time_series_different_starting_indices( print("\nTesting different starting indices...") - registrar = RegisterTimeSeriesImages(registration_method="ANTS") + registrar = RegisterTimeSeriesImages(registration_method="Greedy") registrar.set_modality("ct") registrar.set_fixed_image(fixed_image) - registrar.set_number_of_iterations_ANTS([10, 5, 1]) + registrar.set_number_of_iterations_greedy([10, 5, 1]) # Test starting from beginning, middle, and end for starting_index in [0, 1]: @@ -325,7 +329,7 @@ def test_register_time_series_different_starting_indices( def test_register_time_series_error_no_fixed_image(self) -> None: """Test that error is raised if fixed image not set.""" - registrar = RegisterTimeSeriesImages(registration_method="ANTS") + registrar = RegisterTimeSeriesImages(registration_method="Greedy") moving_images = [None, None, None] # Dummy list @@ -338,7 +342,7 @@ def test_register_time_series_error_invalid_starting_index( self, test_images: list[Any] ) -> None: """Test that error is raised for invalid starting index.""" - registrar = RegisterTimeSeriesImages(registration_method="ANTS") + registrar = RegisterTimeSeriesImages(registration_method="Greedy") registrar.set_fixed_image(test_images[0]) moving_images = test_images[1:4] @@ -361,7 +365,7 @@ def test_register_time_series_error_invalid_prior_portion( self, test_images: list[Any] ) -> None: """Test that error is raised for invalid prior portion value.""" - registrar = RegisterTimeSeriesImages(registration_method="ANTS") + registrar = RegisterTimeSeriesImages(registration_method="Greedy") registrar.set_fixed_image(test_images[0]) moving_images = test_images[1:4] @@ -391,10 +395,10 @@ def test_transform_application_time_series( print("\nTesting transform application...") - registrar = RegisterTimeSeriesImages(registration_method="ANTS") + registrar = RegisterTimeSeriesImages(registration_method="Greedy") registrar.set_modality("ct") registrar.set_fixed_image(fixed_image) - registrar.set_number_of_iterations_ANTS([20, 10, 2]) + registrar.set_number_of_iterations_greedy([20, 10, 2]) result = registrar.register_time_series( moving_images=moving_images, @@ -487,11 +491,11 @@ def test_register_time_series_with_mask( print("\nTesting time series registration with mask...") print(f" Mask voxels: {np.sum(fixed_mask_arr)}") - registrar = RegisterTimeSeriesImages(registration_method="ANTS") + registrar = RegisterTimeSeriesImages(registration_method="Greedy") registrar.set_modality("ct") registrar.set_fixed_image(fixed_image) registrar.set_fixed_mask(fixed_mask) - registrar.set_number_of_iterations_ANTS([20, 10, 2]) + registrar.set_number_of_iterations_greedy([20, 10, 2]) result = registrar.register_time_series( moving_images=moving_images, @@ -513,10 +517,10 @@ def test_bidirectional_registration(self, test_images: list[Any]) -> None: print(f" Total images: {len(moving_images)}") print(" Starting from middle (index 2)") - registrar = RegisterTimeSeriesImages(registration_method="ANTS") + registrar = RegisterTimeSeriesImages(registration_method="Greedy") registrar.set_modality("ct") registrar.set_fixed_image(fixed_image) - registrar.set_number_of_iterations_ANTS([20, 10, 2]) + registrar.set_number_of_iterations_greedy([20, 10, 2]) result = registrar.register_time_series( moving_images=moving_images, diff --git a/tutorials/tutorial_08_dirlab_pca_time_series.py b/tutorials/tutorial_08_dirlab_pca_time_series.py index 75cd063..55302ee 100644 --- a/tutorials/tutorial_08_dirlab_pca_time_series.py +++ b/tutorials/tutorial_08_dirlab_pca_time_series.py @@ -120,7 +120,7 @@ def run_tutorial() -> dict[str, Any]: fixed_image = time_series[0] registrar = RegisterTimeSeriesImages( - registration_method="ANTS_ICON", + registration_method="Greedy_ICON", log_level=log_level, ) registrar.set_modality("ct") From 4f9d02ac91da2daba47a62a57df6764c75aa28be Mon Sep 17 00:00:00 2001 From: Stephen Aylward Date: Thu, 11 Jun 2026 08:54:46 -0400 Subject: [PATCH 2/2] ENH: Docs updated --- docs/api/registration/time_series.rst | 5 ++--- docs/api/workflows.rst | 4 ++-- docs/cli_scripts/4dct_reconstruction.rst | 4 ++-- docs/cli_scripts/best_practices.rst | 2 +- docs/cli_scripts/heart_gated_ct.rst | 10 +++++----- docs/developer/registration_images.rst | 2 +- docs/developer/workflows.rst | 2 +- docs/examples.rst | 8 ++++---- docs/quickstart.rst | 8 ++++---- docs/troubleshooting.rst | 4 ++-- docs/tutorials.rst | 2 +- src/physiomotion4d/cli/convert_image_to_usd.py | 4 ++-- tutorials/tutorial_01_heart_gated_ct_to_usd.py | 6 +++--- 13 files changed, 30 insertions(+), 31 deletions(-) diff --git a/docs/api/registration/time_series.rst b/docs/api/registration/time_series.rst index ac6ccaa..a946a2f 100644 --- a/docs/api/registration/time_series.rst +++ b/docs/api/registration/time_series.rst @@ -6,8 +6,7 @@ Time-Series Registration .. currentmodule:: physiomotion4d ``RegisterTimeSeriesImages`` registers ordered 3D image phases to a reference -frame using ANTs, Greedy, ICON, or combined ``ANTS_ICON`` / ``greedy_ICON`` -methods. +frame using Greedy, ICON, or combined ``Greedy_ICON`` methods. Class Reference =============== @@ -29,7 +28,7 @@ Basic Usage images = [itk.imread(f"phase_{idx:02d}.mha") for idx in range(10)] - registrar = RegisterTimeSeriesImages(registration_method="ANTS") + registrar = RegisterTimeSeriesImages(registration_method="Greedy") registrar.set_fixed_image(images[0]) result = registrar.register_time_series( diff --git a/docs/api/workflows.rst b/docs/api/workflows.rst index 08c0cf5..3733b57 100644 --- a/docs/api/workflows.rst +++ b/docs/api/workflows.rst @@ -56,7 +56,7 @@ Convert Image to USD output_directory="./results", project_name="patient_001", segmentation_method="ChestTotalSegmentator", - registration_method="ANTS", + registration_method="ICON", ) final_usd = workflow.process() @@ -157,7 +157,7 @@ High-Resolution 4D CT Reconstruction time_series_images=time_series_images, fixed_image=time_series_images[0], reference_frame=0, - registration_method="ANTS", + registration_method="Greedy_ICON", ) result = workflow.run_workflow(upsample_to_fixed_resolution=True) diff --git a/docs/cli_scripts/4dct_reconstruction.rst b/docs/cli_scripts/4dct_reconstruction.rst index 800b8c6..15d89da 100644 --- a/docs/cli_scripts/4dct_reconstruction.rst +++ b/docs/cli_scripts/4dct_reconstruction.rst @@ -47,8 +47,8 @@ Registration Options physiomotion4d-reconstruct-highres-4d-ct \ --time-series-images frame_*.mha \ --fixed-image highres_reference.mha \ - --registration-method ANTS \ - --ANTS-iterations 30 15 7 3 \ + --registration-method Greedy_ICON \ + --Greedy-iterations 30 15 7 3 \ --prior-weight 0.5 \ --output-dir ./results diff --git a/docs/cli_scripts/best_practices.rst b/docs/cli_scripts/best_practices.rst index 2bd2a4c..1a7679a 100644 --- a/docs/cli_scripts/best_practices.rst +++ b/docs/cli_scripts/best_practices.rst @@ -171,7 +171,7 @@ Registration Failure **Solutions**: * Select better reference image (less motion, better quality) * Increase ``--registration-iterations`` - * Try alternative registration method (``--registration-method ANTS``) + * Try alternative registration method (``--registration-method Greedy``) * Verify phases are temporally ordered correctly Memory Errors diff --git a/docs/cli_scripts/heart_gated_ct.rst b/docs/cli_scripts/heart_gated_ct.rst index d42a975..18a1089 100644 --- a/docs/cli_scripts/heart_gated_ct.rst +++ b/docs/cli_scripts/heart_gated_ct.rst @@ -112,7 +112,7 @@ Optional Arguments - Number of registration refinement iterations * - ``--registration-method`` - ``ICON`` - - Registration method: ``ICON`` or ``ANTS`` + - Registration method: ``ICON`` or ``Greedy`` Processing Pipeline =================== @@ -215,14 +215,14 @@ Research Dataset Processing --project-name ${case_name} done -With ANTs Registration ----------------------- +With Greedy Registration +------------------------ .. code-block:: bash physiomotion4d-convert-image-to-usd cardiac.nrrd \ --contrast \ - --registration-method ANTS \ + --registration-method Greedy \ --registration-iterations 50 Best Practices @@ -289,7 +289,7 @@ Registration Failures **Registration not converging** * Try different reference image (better quality phase) * Increase ``--registration-iterations`` - * Switch registration method (``--registration-method ANTS``) + * Switch registration method (``--registration-method Greedy``) **Excessive deformation** * Verify sufficient temporal overlap between phases diff --git a/docs/developer/registration_images.rst b/docs/developer/registration_images.rst index 1c0d023..5fcb67e 100644 --- a/docs/developer/registration_images.rst +++ b/docs/developer/registration_images.rst @@ -42,7 +42,7 @@ Time Series images = [itk.imread(f"phase_{idx:02d}.mha") for idx in range(10)] - registrar = RegisterTimeSeriesImages(registration_method="ANTS") + registrar = RegisterTimeSeriesImages(registration_method="Greedy") registrar.set_fixed_image(images[0]) result = registrar.register_time_series( moving_images=images, diff --git a/docs/developer/workflows.rst b/docs/developer/workflows.rst index 5c0f8e8..806cedd 100644 --- a/docs/developer/workflows.rst +++ b/docs/developer/workflows.rst @@ -39,7 +39,7 @@ Workflow Example contrast_enhanced=True, output_directory="./results", project_name="patient_001", - registration_method="ANTS", + registration_method="ICON", ) final_usd = workflow.process() diff --git a/docs/examples.rst b/docs/examples.rst index 108b07a..610be5b 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -35,7 +35,7 @@ Complete end-to-end cardiac CT processing: contrast_enhanced=True, output_directory="./results", project_name="patient_001", - registration_method="ANTS", + registration_method="ICON", ) # Run complete workflow @@ -66,7 +66,7 @@ phase images: time_series_images=time_series_images, fixed_image=fixed_image, reference_frame=0, - registration_method="ANTS", + registration_method="Greedy_ICON", ) result = workflow.run_workflow(upsample_to_fixed_resolution=True) @@ -411,7 +411,7 @@ Batch process multiple datasets: contrast_enhanced=True, output_directory=f"results/{patient_id}", project_name=patient_id, - registration_method="ANTS", + registration_method="ICON", ) try: @@ -481,7 +481,7 @@ Run the supported end-to-end workflow API: contrast_enhanced=True, output_directory="./results", project_name="cardiac_model", - registration_method="ANTS", + registration_method="ICON", ) final_usd = workflow.process() diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 2faf4e2..159a866 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -72,7 +72,7 @@ CUDA-capable GPU are required for practical runtime. python -c "from physiomotion4d import DataDownloadTools; DataDownloadTools.DownloadSlicerHeartCTData('data/test')" physiomotion4d-convert-image-to-usd data/test/TruncalValve_4DCT.seq.nrrd \ - --registration-method ANTS \ + --registration-method ICON \ --output-dir output/quickstart \ --project-name slicer_heart_quickstart @@ -116,7 +116,7 @@ For more control, use the Python API: contrast_enhanced=True, output_directory="./results", project_name="cardiac_model", - registration_method="ANTS", + registration_method="ICON", ) **Step 3: Run the workflow** @@ -151,7 +151,7 @@ For more control over individual steps: contrast_enhanced=True, output_directory="./results", project_name="cardiac_model", - registration_method="ANTS", + registration_method="ICON", ) final_usd = workflow.process() @@ -314,7 +314,7 @@ Common Issues * Resample or crop the input image before running the workflow * Process fewer frames at once -* Use ANTs registration with ``--registration-method ANTS`` when CUDA is unavailable +* Use Greedy registration with ``--registration-method Greedy`` when CUDA is unavailable **Segmentation quality issues** diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 546b954..fc9e8a0 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -15,7 +15,7 @@ CUDA Out of Memory **Solutions**: 1. Resample or crop the input image before running the workflow. -2. Use ``--registration-method ANTS`` when CUDA is unavailable. +2. Use ``--registration-method Greedy`` when CUDA is unavailable. 3. Process fewer frames per run. CUDA Version Mismatch @@ -93,7 +93,7 @@ Registration Not Converging .. code-block:: bash - physiomotion4d-convert-image-to-usd cardiac_4d.nrrd --registration-method ANTS + physiomotion4d-convert-image-to-usd cardiac_4d.nrrd --registration-method Greedy 3. Check image orientation and spacing diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 2ba1f92..7864123 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -244,7 +244,7 @@ Script ``tutorials/tutorial_08_dirlab_pca_time_series.py`` Workflow - ``RegisterTimeSeriesImages`` with ``registration_method='ANTS_ICON'`` and + ``RegisterTimeSeriesImages`` with ``registration_method='Greedy_ICON'`` and ``TransformTools`` Dataset diff --git a/src/physiomotion4d/cli/convert_image_to_usd.py b/src/physiomotion4d/cli/convert_image_to_usd.py index 61021da..5fff4c7 100644 --- a/src/physiomotion4d/cli/convert_image_to_usd.py +++ b/src/physiomotion4d/cli/convert_image_to_usd.py @@ -87,9 +87,9 @@ def main() -> int: ) parser.add_argument( "--registration-method", - choices=["ANTS", "ICON"], + choices=["Greedy", "ICON"], default="ICON", - help="Registration method to use: ANTS or ICON (default: ICON)", + help="Registration method to use: Greedy or ICON (default: ICON)", ) parser.add_argument( "--fps", diff --git a/tutorials/tutorial_01_heart_gated_ct_to_usd.py b/tutorials/tutorial_01_heart_gated_ct_to_usd.py index acb6c32..378a699 100644 --- a/tutorials/tutorial_01_heart_gated_ct_to_usd.py +++ b/tutorials/tutorial_01_heart_gated_ct_to_usd.py @@ -30,14 +30,14 @@ Strengths --------- - Single call (``WorkflowConvertImageToUSD.process()``) runs the full pipeline. -- Supports both GPU-accelerated ICON registration and CPU-capable ANTs registration. +- Supports both GPU-accelerated ICON registration and CPU-capable Greedy registration. - Automatically detects contrast enhancement and adjusts segmentation thresholds. - Output is Omniverse-ready with anatomical materials (USDAnatomyTools). Weaknesses / Limitations ------------------------ - Requires a GPU for ICON registration (``registration_method='ICON'``); use - ``registration_method='ANTS'`` for CPU-only environments (about 10x slower). + ``registration_method='Greedy'`` for CPU-only environments (about 10x slower). - Segmentation quality depends on TotalSegmentator's training distribution; unusual pathologies or pediatric anatomy may degrade results. - Large 4D datasets (>20 phases, high resolution) can require 32 GB+ RAM. @@ -93,7 +93,7 @@ FULL_DATA_DIR = DATA_DIR / "Slicer-Heart-CT" TEST_DATA_DIR = DATA_DIR / "test" / "slicer_heart_small" OUTPUT_DIR = TUTORIALS_DIR / "output" / "tutorial_01" - REGISTRATION_METHOD = "ANTS" + REGISTRATION_METHOD = "ICON" LOG_LEVEL = logging.INFO # %%