Skip to content

Conversation

@markak47
Copy link
Collaborator

@markak47 markak47 commented Nov 28, 2025

DUE DILIGENCE

General

  • The title of the PR is suitable to appear in the Release Notes.

Implementation

  • Unit tests cover all split configurations
    (split=None and all valid split axes for 2D and 3D).
  • Unit tests are currently executed with float32.
  • MPS testing (1 MPI process, 1 GPU) was not performed locally.
  • No dedicated benchmarks were added for this functionality.
  • Performance is maintained compared to existing ndimage operations.
  • Documentation and example usage were added where needed.

DESCRIPTION

This PR adds support for N-dimensional affine transformations to the
heat.ndimage module.

The new implementation supports:

  • 2D and 3D affine transformations (2×3 and 3×4 matrices)
  • backward-warp semantics consistent with SciPy and PyTorch
  • nearest-neighbor and bilinear interpolation
  • multiple padding modes (constant, nearest, wrap, reflect)
  • correct behavior for distributed arrays across all valid split configurations

Example scripts are included to demonstrate typical usage on image and
volume data.

Issues resolved: #1900

CHANGES PROPOSED

  • Added affine_transform function to heat.ndimage
  • Implemented helper routines for input normalization, grid construction,
    and sampling
  • Added comprehensive unit tests covering interpolation modes, padding
    modes, and split configurations
  • Added an example demonstrating affine transformations on MRI data

TYPE OF CHANGE

  • New feature (non-breaking change which adds functionality)

MEMORY REQUIREMENTS

No significant additional memory overhead beyond temporary coordinate
grids required for affine sampling. Memory usage is comparable to existing
ndimage operations in both distributed and non-distributed modes.

PERFORMANCE

Performance is comparable to existing ndimage operations. No performance
regression was observed for standard use cases.

DOES THIS CHANGE MODIFY THE BEHAVIOUR OF OTHER FUNCTIONS?

no

@markak47 markak47 force-pushed the features/1900-Image_transformations_beyond_FFT branch from 505a7e0 to 9bdd8d2 Compare November 30, 2025 17:34
@github-project-automation github-project-automation bot moved this to Todo in Roadmap Dec 2, 2025
@JuanPedroGHM JuanPedroGHM added this to the 1.8.0 milestone Dec 2, 2025
@ClaudiaComito
Copy link
Contributor

ClaudiaComito commented Dec 2, 2025

@markak47 thank you so much for the work and examples, this looks very nice already.

Before we go on with optimizing the implementation, a few tips on integrating this into the Heat codebase:

  • unless @lolacaro disagrees, we should stick to the scipy scheme, i.e. the module should be called ndimage, not transform. The function's API should adhere to scipy.ndimage.affine_transform apart from potential arguments related to distribution
  • check out our other modules to see what the structure of the files and dirs should be like, e.g. signal within core, or fft
  • the examples are great! if you haven't already, please incorporate them in the tests
  • datasets that are required for the tests should go into the heat/datasets directory.
  • please 🙏🏼 🙏🏼 🙏🏼 double-check that the test datasets can be shared publicly, if not remove them asap THANKS! 😬
  • other files that don't serve the module's purposes can be removed too

This is all a bit boring, but if we leave it for later, it'll be much more work.

Thank you so much Mark!

@markak47 markak47 force-pushed the features/1900-Image_transformations_beyond_FFT branch from 1e81333 to 188bc5c Compare December 5, 2025 14:52
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds new affine transformation functionality to the Heat ndimage module, enabling 2D and 3D image transformations including rotation, scaling, translation, and shearing. The implementation uses backward warping with support for different interpolation methods (nearest neighbor and bilinear) and multiple padding modes (constant, nearest, wrap, reflect).

Key Changes:

  • New affine_transform function supporting 2D (2x3) and 3D (3x4) transformation matrices with backward warping semantics
  • Nearest neighbor and bilinear interpolation modes with customizable boundary handling
  • Comprehensive test suite validating transformations, interpolation, and padding behaviors

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 17 comments.

File Description
heat/ndimage/affine.py Core implementation of affine transformation with coordinate mapping, interpolation methods, and padding modes
heat/ndimage/tests/test_affine_nd.py Unit tests covering 2D/3D transformations, identity, translation, rotation, scaling, interpolation, and padding modes
heat/ndimage/examples/mri_testscan.py Example demonstrating affine transformations on MRI data with rotation, scaling, translation, and shearing
Comments suppressed due to low confidence (2)

heat/ndimage/examples/mri_testscan.py:79

  • Variable cx is not used.
    cx, cy = W/2, H/2

heat/ndimage/examples/mri_testscan.py:79

  • Variable cy is not used.
    cx, cy = W/2, H/2

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

mode="nearest",
constant_value=0.0,
expand=False, # kept for later; currently no-op
):
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The order parameter is not validated. Invalid values (e.g., order=5) will cause the function to silently use bilinear interpolation via the else branch at line 235. Add validation to ensure order is either 0 (nearest) or 1 (bilinear), and raise a ValueError for unsupported interpolation orders.

Suggested change
):
):
if order not in (0, 1):
raise ValueError(f"Unsupported interpolation order: {order}. Only 0 (nearest) and 1 (bilinear) are supported.")

Copilot uses AI. Check for mistakes.
Comment on lines 83 to 86
"""
Input: 2x3 affine matrix.
Output: 2x3 affine matrix recentered around the image center.
"""
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indentation of the docstring is inconsistent with the function definition. The opening triple quotes should align with the function's indentation level (8 spaces), not have extra indentation (12 spaces).

Suggested change
"""
Input: 2x3 affine matrix.
Output: 2x3 affine matrix recentered around the image center.
"""
"""
Input: 2x3 affine matrix.
Output: 2x3 affine matrix recentered around the image center.
"""

Copilot uses AI. Check for mistakes.
b = torch.tensor(M[:, ND:], device=device).reshape(ND, 1)

# Backward warp matrix
A_inv = torch.inverse(A)
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The matrix inversion at line 199 can fail if the transformation matrix A is singular (non-invertible). This will raise an exception from torch.inverse, but without a clear error message explaining the issue. Consider adding a try-except block to catch this case and provide a more informative error message to users about why their transformation matrix is invalid.

Copilot uses AI. Check for mistakes.
# Define transforms (all center-aware)
# ========================================================

cx, cy = W/2, H/2
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variables cx and cy are defined but never used (they are redefined within the recenter function at line 88). Consider removing this line to avoid confusion.

Suggested change
cx, cy = W/2, H/2

Copilot uses AI. Check for mistakes.
Comment on lines 12 to 51
def _normalize_input(x, ND):
orig_shape = x.shape
t = x.larray

# 2D image: (H, W) or (C, H, W)
if ND == 2:
if x.ndim == 2: # (H,W)
t = t.unsqueeze(0).unsqueeze(0) # → (1,1,H,W)
elif x.ndim == 3: # (C,H,W)
t = t.unsqueeze(0) # → (1,C,H,W)

# 3D image: (D,H,W) or (C,D,H,W)
else:
if x.ndim == 3: # (D,H,W)
t = t.unsqueeze(0).unsqueeze(0) # → (1,1,D,H,W)
elif x.ndim == 4: # (C,D,H,W)
t = t.unsqueeze(0) # → (1,C,D,H,W)

return t, orig_shape
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _normalize_input function doesn't handle invalid input dimensions. For 2D transforms (ND=2), if the input has ndim != 2 and ndim != 3, the function returns the original tensor without proper normalization. Similarly for 3D. This will cause errors downstream in the sampling functions. Add an else clause to raise a ValueError with a clear message about expected dimensions.

Copilot uses AI. Check for mistakes.
Comment on lines 192 to 203
def test_expand_rotation(self):
x = ht.array([
[1.,0.],
[0.,1.]
])

M = [[0,-1,0],
[1,0,0]]

y = affine_transform(x, M, expand=True)

self.assertTrue(y.shape[0] >= 2 and y.shape[1] >= 2)
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test checks that the shape doesn't shrink when expand=True, but since expand is currently a no-op (as documented in affine.py line 178), this test doesn't actually validate the expand functionality. Consider either skipping this test with a clear comment explaining the feature is not yet implemented, or adding an assertion that the output shape equals the input shape to make the test's purpose clearer.

Copilot uses AI. Check for mistakes.
@ClaudiaComito
Copy link
Contributor

ClaudiaComito commented Dec 12, 2025

TODOs after today's meeting:

  • fix pre-commit error
  • incorporate co-pilot suggestions (after reviewing them)
  • update branch
  • mark ready for review
  • move examples to dedicated directory in heat/examples
  • TBD: make a jupyter notebook out of examples?

next meeting Tues 15.12. 15:00 with @lolacaro @markak47 @ClaudiaComito

@markak47 markak47 force-pushed the features/1900-Image_transformations_beyond_FFT branch from 8a83636 to d986169 Compare December 14, 2025 11:47
@markak47 markak47 force-pushed the features/1900-Image_transformations_beyond_FFT branch from b0619f6 to 2cda26b Compare December 14, 2025 11:49
@markak47 markak47 force-pushed the features/1900-Image_transformations_beyond_FFT branch from c0b55a5 to 970c25c Compare December 14, 2025 11:53
@markak47 markak47 self-assigned this Dec 14, 2025
@markak47 markak47 force-pushed the features/1900-Image_transformations_beyond_FFT branch from 345ac2c to 2a29fda Compare December 14, 2025 12:02
@markak47
Copy link
Collaborator Author

**@github-copilot please re-review the PR after the latest changes.
**

@helmholtz-analytics helmholtz-analytics deleted a comment from Copilot AI Dec 14, 2025
@helmholtz-analytics helmholtz-analytics deleted a comment from Copilot AI Dec 14, 2025
@helmholtz-analytics helmholtz-analytics deleted a comment from Copilot AI Dec 14, 2025
@helmholtz-analytics helmholtz-analytics deleted a comment from Copilot AI Dec 14, 2025
@markak47 markak47 force-pushed the features/1900-Image_transformations_beyond_FFT branch from 176c194 to 20ec2d9 Compare December 14, 2025 12:43
@markak47 markak47 force-pushed the features/1900-Image_transformations_beyond_FFT branch from a1731ee to 74f37fa Compare December 14, 2025 13:24
@ClaudiaComito ClaudiaComito marked this pull request as ready for review December 15, 2025 04:33
@github-actions
Copy link
Contributor

Thank you for the PR!

@markak47 markak47 force-pushed the features/1900-Image_transformations_beyond_FFT branch from e3d66ad to 144a884 Compare January 4, 2026 12:45
@github-actions
Copy link
Contributor

github-actions bot commented Jan 4, 2026

Thank you for the PR!

@codecov
Copy link

codecov bot commented Jan 4, 2026

Codecov Report

❌ Patch coverage is 54.26829% with 75 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.50%. Comparing base (1a69cc7) to head (2ebbdf0).
⚠️ Report is 127 commits behind head on main.

Files with missing lines Patch % Lines
heat/ndimage/affine.py 54.26% 75 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2048      +/-   ##
==========================================
- Coverage   91.96%   91.50%   -0.46%     
==========================================
  Files          88       89       +1     
  Lines       13496    13660     +164     
==========================================
+ Hits        12411    12500      +89     
- Misses       1085     1160      +75     
Flag Coverage Δ
unit 91.50% <54.26%> (-0.46%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 9, 2026

Thank you for the PR!

@ClaudiaComito ClaudiaComito moved this from Todo to In Progress in Roadmap Jan 9, 2026
Copy link
Contributor

@ClaudiaComito ClaudiaComito left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post-meeting comments for @markak47 , more later!

Comment on lines 393 to 395
if x.split is not None:
x_full = x.resplit(None)
y_full = _affine_transform_local(x_full, M, order, mode, constant_value, expand)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is bad because x might be a huge DNDarray that doesn't fit in the RAM of the single process

Copy link
Contributor

@ClaudiaComito ClaudiaComito Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if

  • M is 2-D and
  • x is 3-D and
  • x.split = 0

example: huge stack of images, (1M, 1024, 1024) distributed along the stacking dimension

then _affine_transform_local can be applied to the local slices and resplit(None) is not necessary

same with 3-D M and 4-D x

Copy link
Contributor

@ClaudiaComito ClaudiaComito Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if x.is_distributed() and x.split != 0:
   raise NotImplementedError("Affine transforms along the distributed axis not supported yet")

ht.DNDarray
Transformed array.
"""
M = np.asarray(M)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why numpy?

@markak47 markak47 force-pushed the features/1900-Image_transformations_beyond_FFT branch from 43a3ebe to 7a6fd1c Compare January 16, 2026 08:54
@github-actions
Copy link
Contributor

Thank you for the PR!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

Image transformations beyond FFT

4 participants