Skip to content

Conversation

@Hitendrasinhdata7
Copy link

Description

Implements 3D Radial Fourier Transform for medical imaging applications, addressing anisotropic resolution challenges and enabling rotation-invariant frequency analysis. This transform is specifically designed for medical images where voxel spacing often differs between axial, coronal, and sagittal planes (e.g., typical CT/MRI with different slice thickness vs in-plane resolution).

Medical Imaging Problem Addressed:

  • Anisotropic Resolution Normalization: Converts data to isotropic frequency domain representation
  • Rotation-Invariant Analysis: Radial frequency profiles remain consistent under 3D rotation
  • Acquisition Parameter Robustness: Reduces sensitivity to varying scan parameters across datasets

Key Features:

  • RadialFourier3D: Core transform for 3D radial frequency analysis with configurable radial bins
  • RadialFourierFeatures3D: Multi-scale frequency feature extraction for comprehensive analysis
  • Flexible Output Modes: Magnitude-only, phase-only, or complex outputs
  • Frequency Range Control: Optional maximum frequency cutoff for noise reduction
  • Inverse Transform Support: Approximate reconstruction for validation purposes
  • Medical Image Optimized: Handles common medical image shapes (batch, channel, depth, height, width)

Technical Implementation:

  • Location: monai/transforms/signal/radial_fourier.py
  • Tests: tests/test_radial_fourier.py (20/20 passing, comprehensive coverage)
  • Dependencies: Uses PyTorch's native FFT - no new dependencies
  • Performance: GPU-accelerated via PyTorch, O(N log N) complexity
  • Compatibility: Supports both PyTorch tensors and NumPy arrays
  • API Consistency: Follows MONAI transform conventions and typing

Usage Examples:

# Basic radial frequency analysis
from monai.transforms import RadialFourier3D
transform = RadialFourier3D(radial_bins=64, return_magnitude=True)
features = transform(image)  # Shape: (batch, 64)

# Full frequency analysis with phase information
transform_full = RadialFourier3D(radial_bins=None, return_magnitude=True, return_phase=True)
full_spectrum = transform_full(image)  # Full 3D spectrum with magnitude and phase

# Multi-scale feature extraction for ML pipelines
from monai.transforms import RadialFourierFeatures3D
feature_extractor = RadialFourierFeatures3D(
    n_bins_list=[16, 32, 64, 128],
    return_types=["magnitude", "phase"]
)
ml_features = feature_extractor(image)  # Comprehensive feature vector

…lysis

- Implement RadialFourier3D transform for radial frequency analysis
- Add RadialFourierFeatures3D for multi-scale feature extraction
- Include comprehensive tests (20/20 passing)
- Support for magnitude, phase, and complex outputs
- Handle anisotropic resolution in medical imaging
- Fix numpy compatibility and spatial dimension handling

Signed-off-by: Hitendrasinh Rathod<hitendrasinh.data7@gmail.com>
Signed-off-by: Hitendrasinh Rathod <Hitendrasinh.data7@gmail.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 21, 2025

Walkthrough

This pull request introduces a new 3D Radial Fourier transform framework in MONAI. Two transforms are added: RadialFourier3D performs 3D FFT with optional radial binning and returns magnitude/phase data; RadialFourierFeatures3D composes multiple RadialFourier3D instances across different bin counts to extract multi-resolution features. The implementation includes input validation, inverse transform support (for non-binned cases), and type handling for NumPy/PyTorch inputs. Supporting changes update the public API exports and introduce comprehensive test coverage.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • FFT normalization and correctness: Verify voxel-count normalization, fft shift/ifft shift operations, and handling of frequency domain coordinates
  • Radial binning logic: Review _compute_radial_spectrum binning algorithm, bin edge calculations, and correctness of radial-averaged outputs
  • Inverse transform: Validate reconstruction from magnitude/phase and correctness of phase handling; ensure NotImplementedError for binned case is intended
  • Type conversions: Confirm numpy/torch interop, dtype preservation across complex-to-magnitude/phase conversions, and final output type matching
  • Parameter validation: Verify bounds checking (max_frequency ∈ (0,1], radial_bins ≥ 1) and mutual exclusivity constraints
  • Test coverage: Confirm edge cases (zero padding, batch inputs, custom spatial dims) and inverse transform inverse-compatibility are adequately covered

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately summarizes the main addition: a 3D Radial Fourier Transform for medical imaging, clearly communicating the primary change.
Description check ✅ Passed Description is comprehensive and well-structured, covering medical problem, key features, technical implementation, and usage examples. Missing explicit test counts and documentation status checkboxes.
Docstring Coverage ✅ Passed Docstring coverage is 92.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
tests/test_radial_fourier.py (1)

76-88: Inverse transform test only checks shape, not reconstruction accuracy.

Consider adding an assertion that the reconstructed data is close to the original input to validate correctness.

         # Should have same shape
         self.assertEqual(reconstructed.shape, self.test_image_3d.shape)
+
+        # Should approximately reconstruct original
+        self.assertTrue(torch.allclose(reconstructed, self.test_image_3d, atol=1e-5))
monai/transforms/signal/radial_fourier.py (3)

137-144: Loop-based binning may be slow for large radial_bins.

Consider vectorized binning using torch.bucketize for better performance, though current implementation is correct.


34-62: Docstring missing Raises section.

Per coding guidelines, docstrings should document raised exceptions.

     Example:
         >>> transform = RadialFourier3D(radial_bins=64, return_magnitude=True)
         >>> image = torch.randn(1, 128, 128, 96)  # Batch, Height, Width, Depth
         >>> result = transform(image)  # Shape: (1, 64)
+
+    Raises:
+        ValueError: If max_frequency not in (0.0, 1.0], radial_bins < 1, or both
+            return_magnitude and return_phase are False.
     """

30-31: Unused import.

spatial is imported but never used.

-# Optional imports for type checking
-spatial, _ = optional_import("monai.utils", name="spatial")
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 15fd428 and cb0546d.

📒 Files selected for processing (4)
  • monai/transforms/__init__.py (1 hunks)
  • monai/transforms/signal/__init__.py (1 hunks)
  • monai/transforms/signal/radial_fourier.py (1 hunks)
  • tests/test_radial_fourier.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

⚙️ CodeRabbit configuration file

Review the Python code for quality and correctness. Ensure variable names adhere to PEP8 style guides, are sensible and informative in regards to their function, though permitting simple names for loop and comprehension variables. Ensure routine names are meaningful in regards to their function and use verbs, adjectives, and nouns in a semantically appropriate way. Docstrings should be present for all definition which describe each variable, return value, and raised exception in the appropriate section of the Google-style of docstrings. Examine code for logical error or inconsistencies, and suggest what may be changed to addressed these. Suggest any enhancements for code improving efficiency, maintainability, comprehensibility, and correctness. Ensure new or modified definitions will be covered by existing or new unit tests.

Files:

  • tests/test_radial_fourier.py
  • monai/transforms/signal/radial_fourier.py
  • monai/transforms/signal/__init__.py
  • monai/transforms/__init__.py
🧬 Code graph analysis (3)
tests/test_radial_fourier.py (1)
monai/transforms/signal/radial_fourier.py (3)
  • RadialFourier3D (34-279)
  • RadialFourierFeatures3D (282-350)
  • inverse (239-279)
monai/transforms/signal/__init__.py (1)
monai/transforms/signal/radial_fourier.py (2)
  • RadialFourier3D (34-279)
  • RadialFourierFeatures3D (282-350)
monai/transforms/__init__.py (2)
monai/transforms/signal/array.py (1)
  • SignalRemoveFrequency (387-419)
monai/transforms/signal/radial_fourier.py (2)
  • RadialFourier3D (34-279)
  • RadialFourierFeatures3D (282-350)
🪛 Ruff (0.14.8)
monai/transforms/signal/radial_fourier.py

86-86: Avoid specifying long messages outside the exception class

(TRY003)


88-88: Avoid specifying long messages outside the exception class

(TRY003)


90-90: Avoid specifying long messages outside the exception class

(TRY003)


166-166: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (7)
monai/transforms/__init__.py (1)

379-381: LGTM!

New radial Fourier transforms are correctly imported and exported at the package level.

monai/transforms/signal/__init__.py (1)

11-17: LGTM!

Module docstring and exports are correctly set up.

tests/test_radial_fourier.py (2)

25-136: Good test coverage for RadialFourier3D.

Tests cover key scenarios including edge cases, type handling, and parameter validation.


138-193: Good test coverage for RadialFourierFeatures3D.

Multi-scale feature extraction and numpy compatibility are well tested.

monai/transforms/signal/radial_fourier.py (3)

64-91: LGTM!

Parameter validation is thorough and handles edge cases correctly.


239-279: LGTM!

Inverse transform correctly handles the non-binned case with proper FFT shift operations.


343-348: Edge case: when transforms list is empty, output = img may cause issues.

If img is a tensor and transforms is empty, output = img is returned. Then isinstance(img, np.ndarray) is False, so output.cpu().numpy() is never called. This is correct.

However, if img is already a numpy array and transforms is empty, the function returns the numpy array directly without conversion, which is the expected behavior.

Comment on lines +92 to +113
def _compute_radial_coordinates(self, shape: tuple[int, ...]) -> torch.Tensor:
"""
Compute radial distance from frequency domain center.

Args:
shape: spatial dimensions (D, H, W) or (H, W, D) depending on dims order.

Returns:
Tensor of same spatial shape with radial distances.
"""
# Create frequency coordinates for each dimension
coords = []
for dim_size in shape:
# Create frequency range from -0.5 to 0.5
freq = torch.fft.fftfreq(dim_size)
coords.append(freq)

# Create meshgrid and compute radial distance
mesh = torch.meshgrid(coords, indexing="ij")
radial = torch.sqrt(sum(c**2 for c in mesh))

return radial
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential device mismatch: radial coordinates created on CPU.

_compute_radial_coordinates creates tensors on CPU. When the input is on GPU, this will cause device mismatch in _compute_radial_spectrum at line 139 where radial_coords is compared with bin_edges (which is on spectrum.device).

Proposed fix

Pass device to the method and create tensors on correct device:

-    def _compute_radial_coordinates(self, shape: tuple[int, ...]) -> torch.Tensor:
+    def _compute_radial_coordinates(self, shape: tuple[int, ...], device: torch.device = None) -> torch.Tensor:
         """
         Compute radial distance from frequency domain center.

         Args:
             shape: spatial dimensions (D, H, W) or (H, W, D) depending on dims order.
+            device: device to create tensor on.

         Returns:
             Tensor of same spatial shape with radial distances.
         """
         # Create frequency coordinates for each dimension
         coords = []
         for dim_size in shape:
             # Create frequency range from -0.5 to 0.5
-            freq = torch.fft.fftfreq(dim_size)
+            freq = torch.fft.fftfreq(dim_size, device=device)
             coords.append(freq)

Then update the call site at line 179:

-        radial_coords = self._compute_radial_coordinates(spatial_shape)
+        radial_coords = self._compute_radial_coordinates(spatial_shape, device=img_tensor.device)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _compute_radial_coordinates(self, shape: tuple[int, ...]) -> torch.Tensor:
"""
Compute radial distance from frequency domain center.
Args:
shape: spatial dimensions (D, H, W) or (H, W, D) depending on dims order.
Returns:
Tensor of same spatial shape with radial distances.
"""
# Create frequency coordinates for each dimension
coords = []
for dim_size in shape:
# Create frequency range from -0.5 to 0.5
freq = torch.fft.fftfreq(dim_size)
coords.append(freq)
# Create meshgrid and compute radial distance
mesh = torch.meshgrid(coords, indexing="ij")
radial = torch.sqrt(sum(c**2 for c in mesh))
return radial
def _compute_radial_coordinates(self, shape: tuple[int, ...], device: torch.device = None) -> torch.Tensor:
"""
Compute radial distance from frequency domain center.
Args:
shape: spatial dimensions (D, H, W) or (H, W, D) depending on dims order.
device: device to create tensor on.
Returns:
Tensor of same spatial shape with radial distances.
"""
# Create frequency coordinates for each dimension
coords = []
for dim_size in shape:
# Create frequency range from -0.5 to 0.5
freq = torch.fft.fftfreq(dim_size, device=device)
coords.append(freq)
# Create meshgrid and compute radial distance
mesh = torch.meshgrid(coords, indexing="ij")
radial = torch.sqrt(sum(c**2 for c in mesh))
return radial
🤖 Prompt for AI Agents
In monai/transforms/signal/radial_fourier.py around lines 92 to 113,
_compute_radial_coordinates currently creates frequency tensors on CPU which
causes device-mismatch when used with GPU tensors; modify the method to accept a
device (and optionally dtype) parameter and create all frequency coordinate
tensors and the meshgrid on that device so the returned radial tensor lives on
the same device as the spectrum, and update the call site at line 179 to pass
spectrum.device (and spectrum.dtype if needed) when invoking
_compute_radial_coordinates.

Comment on lines +216 to +222
# Apply frequency mask if max_frequency < 1.0
if self.max_frequency < 1.0:
freq_mask = radial_coords <= (self.max_frequency * 0.5)
# Expand mask to match spectrum dimensions
for _ in range(len(self.spatial_dims)):
freq_mask = freq_mask.unsqueeze(0)
spectrum = spectrum * freq_mask
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Frequency mask expansion may be incorrect for inputs with more than 3 non-spatial dimensions.

The loop adds len(spatial_dims) (always 3) leading dimensions, but should add dimensions equal to len(spectrum.shape) - len(spatial_shape) to properly broadcast.

Proposed fix
             if self.max_frequency < 1.0:
                 freq_mask = radial_coords <= (self.max_frequency * 0.5)
                 # Expand mask to match spectrum dimensions
-                for _ in range(len(self.spatial_dims)):
+                n_non_spatial = len(spectrum.shape) - len(spatial_shape)
+                for _ in range(n_non_spatial):
                     freq_mask = freq_mask.unsqueeze(0)
                 spectrum = spectrum * freq_mask

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In monai/transforms/signal/radial_fourier.py around lines 216 to 222, the code
always unsqueezes the radial frequency mask len(self.spatial_dims) times
(effectively 3), which is incorrect when spectrum has more than 3 non-spatial
leading dimensions; compute num_leading = len(spectrum.shape) -
len(self.spatial_dims) and unsqueeze the mask that many times (or
reshape/prepend that many singleton dimensions) so the mask broadcasts correctly
to spectrum before multiplying.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant