Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add cropping methods from FMCIB pipeline #113

Closed
wants to merge 50 commits into from

Conversation

strixy16
Copy link
Collaborator

@strixy16 strixy16 commented Jan 20, 2025

Made a new section under processing for images to add crop.py to.
Includes cropping to bounding box, centroid, max dimension cube, and the existing PyRadiomics cropping method.

Will eventually be updated with better class structure, but this works for now.

Summary by CodeRabbit

  • New Features

    • Introduced a module for processing and manipulating images.
    • Added functions for image cropping, bounding box extraction, and centroid calculation.
    • Implemented advanced image manipulation techniques with multiple cropping methods.
    • Added a new Jupyter notebook for testing and visualizing image processing functionalities.
  • Documentation

    • Added module-level docstrings for image processing functionality.
    • Enhanced code with type hints and improved error handling.
  • Tests

    • Created a test suite for image processing functions.
    • Added parameterized tests for cropping and bounding box extraction methods.
    • Introduced fixtures for testing with specific image and segmentation data.
  • Chores

    • Updated metadata in existing notebooks for improved clarity and versioning.

…for quick checking dimension match between image and centroid/mask/dimensions/etc.
@strixy16 strixy16 requested a review from jjjermiah January 20, 2025 21:37
Copy link
Contributor

coderabbitai bot commented Jan 20, 2025

Walkthrough

This pull request introduces a module for processing and manipulating images, particularly focused on medical images. It adds several functions for cropping and bounding box operations, as well as centroid calculations. New data classes for representing 3D geometric entities are also created. Additionally, a comprehensive test suite is included to validate the functionality of the cropping methods and bounding box extraction, ensuring robustness and accuracy in image processing tasks.

Changes

File Change Summary
src/readii/process/images/__init__.py Added module-level docstring and imported functions from crop module, defining the public API.
src/readii/process/images/crop.py Introduced functions for image processing, including validate_new_dimensions, find_bounding_box, check_bounding_box_single_dimension, apply_bounding_box_limits, find_centroid, and various cropping methods.
src/readii/process/images/utils/bounding_box.py Created data classes Point3D, Size3D, Coordinate, and Centroid for representing 3D geometric entities.
tests/process/images/test_crop.py Added test suite for image cropping functions with parameterized tests and fixtures to validate cropping and bounding box extraction.
notebooks/crop_testing.ipynb Introduced a Jupyter notebook for demonstrating image processing functionalities with various cropping methods.
notebooks/viz_neg_controls.ipynb Updated execution count, image data, display settings, and notebook metadata.

Possibly related PRs

  • feat: add io/loaders module  #83: The changes in the main PR are related to the functions imported from the crop module, which are also defined and modified in the retrieved PR, specifically focusing on image processing functionalities.
  • feat: add analysis functions #92: The changes in the main PR are related to the functions validate_new_dimensions, find_bounding_box, check_bounding_box_single_dimension, and others that are also present in the crop module, which is directly imported in the main PR.
  • Introduce process directory, update existing code for ruff check #112: The changes in the main PR are related to the functions defined in the retrieved PR, specifically those in the crop.py file, as they both involve modifications to image processing functions, including validate_new_dimensions, find_bounding_box, and find_centroid.

Suggested reviewers

  • jjjermiah

Poem

🐰 Hop, hop, through pixels we go,
Cropping images with scientific flow.
Bounding boxes dance, centroids take flight,
In three dimensions, our code shines bright!
Medical images, processed with care 🩺

Tip

🌐 Web search-backed reviews and chat
  • We have enabled web search-based reviews and chat for all users. This feature allows CodeRabbit to access the latest documentation and information on the web.
  • You can disable this feature by setting web_search: false in the knowledge_base settings.
  • Please share any feedback in the Discord discussion.
✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

codecov bot commented Jan 20, 2025

Codecov Report

Attention: Patch coverage is 81.35593% with 22 lines in your changes missing coverage. Please review.

Project coverage is 52.42%. Comparing base (3df3a55) to head (b934f0f).

Files with missing lines Patch % Lines
src/readii/process/images/crop.py 81.03% 22 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #113      +/-   ##
==========================================
+ Coverage   48.12%   52.42%   +4.29%     
==========================================
  Files          34       36       +2     
  Lines        1492     1610     +118     
==========================================
+ Hits          718      844     +126     
+ Misses        774      766       -8     

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

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/process/images/test_crop.py (1)

4-16: Remove unused imports.

Several imported functions from readii.process.images.crop are not used in the tests:

  • apply_bounding_box_limits
  • check_bounding_box_single_dimension
  • crop_to_bounding_box
  • crop_to_centroid
  • crop_to_maxdim_cube
  • find_bounding_box
  • find_centroid
  • resize_image
  • validate_new_dimensions
🧰 Tools
🪛 Ruff (0.8.2)

6-6: readii.process.images.crop.apply_bounding_box_limits imported but unused

Remove unused import

(F401)


7-7: readii.process.images.crop.check_bounding_box_single_dimension imported but unused

Remove unused import

(F401)


9-9: readii.process.images.crop.crop_to_bounding_box imported but unused

Remove unused import

(F401)


10-10: readii.process.images.crop.crop_to_centroid imported but unused

Remove unused import

(F401)


11-11: readii.process.images.crop.crop_to_maxdim_cube imported but unused

Remove unused import

(F401)


12-12: readii.process.images.crop.find_bounding_box imported but unused

Remove unused import

(F401)


13-13: readii.process.images.crop.find_centroid imported but unused

Remove unused import

(F401)


14-14: readii.process.images.crop.resize_image imported but unused

Remove unused import

(F401)


15-15: readii.process.images.crop.validate_new_dimensions imported but unused

Remove unused import

(F401)

🪛 GitHub Actions: CI-CD

[error] Test failure in test_crop_image_to_mask_methods_complex with parameters [resize_dimensions2-expected_size2-centroid]

src/readii/process/images/crop.py (3)

50-78: Add validation for negative or zero dimensions.

The resize_image function should validate that the resize dimensions are positive numbers to prevent potential issues with negative or zero values.

 def resize_image(image:sitk.Image,
                  resize_dimensions:tuple
                 ) -> sitk.Image:
     """Resize an image to specified dimensions via linear interpolation."""
     validate_new_dimensions(image, resize_dimensions)
+    
+    # Validate positive dimensions
+    if any(d <= 0 for d in resize_dimensions):
+        msg = f"Resize dimensions must be positive numbers, got {resize_dimensions}"
+        logger.exception(msg)
+        raise ValueError(msg)
     
     # Calculate the new spacing based on the resized dimensions

114-117: Consider implementing the bounding box class.

The TODO comment suggests moving to a class-based approach for bounding box handling. This would improve code organization and encapsulation.

Would you like me to help design and implement the bounding box class structure?


405-453: Update docstring to match actual method names.

The docstring mentions "bounding_box" but the code uses "bbox". Update the documentation to match the actual method names to avoid confusion.

     """Crop an image and mask to an ROI in the mask and resize to a specified crop dimensions.
     Parameters
     ----------
     crop_method : str, optional
-        Method to use to crop the image to the mask. Must be one of "bounding_box", "centroid", or "cube".
+        Method to use to crop the image to the mask. Must be one of "bbox", "centroid", "cube", or "pyradiomics".
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4b9b11c and 23a1c34.

📒 Files selected for processing (2)
  • src/readii/process/images/crop.py (1 hunks)
  • tests/process/images/test_crop.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
tests/process/images/test_crop.py

6-6: readii.process.images.crop.apply_bounding_box_limits imported but unused

Remove unused import

(F401)


7-7: readii.process.images.crop.check_bounding_box_single_dimension imported but unused

Remove unused import

(F401)


9-9: readii.process.images.crop.crop_to_bounding_box imported but unused

Remove unused import

(F401)


10-10: readii.process.images.crop.crop_to_centroid imported but unused

Remove unused import

(F401)


11-11: readii.process.images.crop.crop_to_maxdim_cube imported but unused

Remove unused import

(F401)


12-12: readii.process.images.crop.find_bounding_box imported but unused

Remove unused import

(F401)


13-13: readii.process.images.crop.find_centroid imported but unused

Remove unused import

(F401)


14-14: readii.process.images.crop.resize_image imported but unused

Remove unused import

(F401)


15-15: readii.process.images.crop.validate_new_dimensions imported but unused

Remove unused import

(F401)

🪛 GitHub Actions: CI-CD
tests/process/images/test_crop.py

[error] Test failure in test_crop_image_to_mask_methods_complex with parameters [resize_dimensions2-expected_size2-centroid]

🔇 Additional comments (5)
tests/process/images/test_crop.py (3)

19-53: LGTM! Well-structured fixtures.

The fixtures are well-organized and provide the necessary test data for both NSCLC and 4D-Lung datasets.

🧰 Tools
🪛 GitHub Actions: CI-CD

[error] Test failure in test_crop_image_to_mask_methods_complex with parameters [resize_dimensions2-expected_size2-centroid]


82-93: LGTM! Well-designed test fixture.

The fixture creates a controlled test environment with specific ROI dimensions (10x20x30) within a larger image (100x100x100), which is excellent for testing edge cases.

🧰 Tools
🪛 GitHub Actions: CI-CD

[error] Test failure in test_crop_image_to_mask_methods_complex with parameters [resize_dimensions2-expected_size2-centroid]


55-80: Verify why PyRadiomics test is commented out.

The test case for the "pyradiomics" method is commented out. Please verify if this is intentional or if there are issues with the PyRadiomics implementation that need to be addressed.

✅ Verification successful

PyRadiomics crop test is intentionally commented out due to different output dimensions

The PyRadiomics crop method naturally produces different output dimensions (22,28,14) compared to other methods that use (50,50,50). This appears to be an intentional difference in behavior rather than an issue, as the PyRadiomics functionality is working correctly in production code and is actively maintained.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if PyRadiomics is properly installed and configured
# Test: Search for PyRadiomics-related issues or TODOs
rg -g '!*.pyc' -g '!*venv*' -A 5 'pyradiomics|TODO.*PyRadiomics'

Length of output: 50810


Script:

#!/bin/bash
# Search for recent changes or issues related to PyRadiomics crop test
rg -g '!*.pyc' -g '!*venv*' -A 3 'TODO.*crop|FIXME.*crop|Issue.*crop'

# Check git history for relevant commits
git log --grep="crop" --grep="pyradiomics" -i --max-count=5 --oneline

Length of output: 65608

🧰 Tools
🪛 GitHub Actions: CI-CD

[error] Test failure in test_crop_image_to_mask_methods_complex with parameters [resize_dimensions2-expected_size2-centroid]

src/readii/process/images/crop.py (2)

13-47: LGTM! Well-implemented validation function.

The function thoroughly validates dimensions with proper error handling and logging.


365-402: Add tests for PyRadiomics cropping method.

The PyRadiomics cropping implementation looks correct, but the test case is commented out. Please add tests to verify this functionality.

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: 1

🧹 Nitpick comments (1)
tests/process/images/test_crop.py (1)

19-53: Consider improving test data management.

The test fixtures use hardcoded paths to test data. This approach could make tests brittle and fail in different environments where the data isn't available at the exact paths.

Consider:

  1. Using a test data configuration file
  2. Adding checks for data availability
  3. Using smaller, dedicated test data files instead of full medical images

Example improvement:

import os
from pathlib import Path

def get_test_data_path():
    """Get base path for test data from environment or config."""
    return os.getenv('TEST_DATA_PATH', 'tests/data')

@pytest.fixture
def nsclcCT():
    base_path = get_test_data_path()
    path = Path(base_path) / "NSCLC_Radiogenomics/R01-001/..."
    if not path.exists():
        pytest.skip("Test data not available")
    return str(path)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 23a1c34 and b1e03b6.

📒 Files selected for processing (1)
  • tests/process/images/test_crop.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
tests/process/images/test_crop.py

6-6: readii.process.images.crop.apply_bounding_box_limits imported but unused

Remove unused import

(F401)


7-7: readii.process.images.crop.check_bounding_box_single_dimension imported but unused

Remove unused import

(F401)


9-9: readii.process.images.crop.crop_to_bounding_box imported but unused

Remove unused import

(F401)


10-10: readii.process.images.crop.crop_to_centroid imported but unused

Remove unused import

(F401)


11-11: readii.process.images.crop.crop_to_maxdim_cube imported but unused

Remove unused import

(F401)


14-14: readii.process.images.crop.resize_image imported but unused

Remove unused import

(F401)


15-15: readii.process.images.crop.validate_new_dimensions imported but unused

Remove unused import

(F401)

⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: Unit-Tests (windows-latest, py312)
  • GitHub Check: Unit-Tests (windows-latest, py311)
  • GitHub Check: Unit-Tests (windows-latest, py310)
  • GitHub Check: Unit-Tests (macos-14, py312)
  • GitHub Check: Unit-Tests (macos-14, py310)
  • GitHub Check: Unit-Tests (macos-latest, py311)
  • GitHub Check: Unit-Tests (macos-latest, py310)
🔇 Additional comments (3)
tests/process/images/test_crop.py (3)

61-61: Uncomment and implement the pyradiomics test case.

The pyradiomics test case is commented out. Either implement and uncomment it, or remove it if it's no longer needed.


86-178: Well-structured and comprehensive tests for bounding box and centroid calculations!

The test cases cover:

  • Simple and complex scenarios
  • Edge cases (single-plane ROI, minimum size)
  • Error handling (bad labels)
  • Clear documentation of test parameters

Great job on the test coverage and organization!


5-16: Verify necessity of imported functions.

Several imported functions from readii.process.images.crop are flagged as unused. While they might be used internally by crop_image_to_mask, consider removing any that are truly unused to maintain clean imports.

Run this script to check if these functions are used internally by crop_image_to_mask:

🧰 Tools
🪛 Ruff (0.8.2)

6-6: readii.process.images.crop.apply_bounding_box_limits imported but unused

Remove unused import

(F401)


7-7: readii.process.images.crop.check_bounding_box_single_dimension imported but unused

Remove unused import

(F401)


9-9: readii.process.images.crop.crop_to_bounding_box imported but unused

Remove unused import

(F401)


10-10: readii.process.images.crop.crop_to_centroid imported but unused

Remove unused import

(F401)


11-11: readii.process.images.crop.crop_to_maxdim_cube imported but unused

Remove unused import

(F401)


14-14: readii.process.images.crop.resize_image imported but unused

Remove unused import

(F401)


15-15: readii.process.images.crop.validate_new_dimensions imported but unused

Remove unused import

(F401)

Comment on lines +64 to +84
def test_crop_image_to_mask_methods(
lung4D_image,
lung4D_mask,
crop_method,
expected_size,
resize_dimensions=(50, 50, 50),
):
"""Test cropping image to mask with different methods"""
cropped_image, cropped_mask = crop_image_to_mask(
lung4D_image,
lung4D_mask,
crop_method,
resize_dimensions,
)
assert (
cropped_image.GetSize() == expected_size
), f"Cropped image size is incorrect, expected {expected_size}, got {cropped_image.GetSize()}"
assert (
cropped_mask.GetSize() == expected_size
), f"Cropped mask size is incorrect, expected {expected_size}, got {cropped_mask.GetSize()}"

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance test coverage for cropping methods.

The test only verifies the dimensions of cropped images but doesn't validate:

  1. The actual content of cropped regions
  2. Edge cases (empty masks, single-pixel masks)
  3. Error cases (invalid dimensions)

Consider adding these test cases:

def test_crop_image_to_mask_content():
    """Test that cropped content matches expected regions."""
    # Create a test image with known patterns
    image = create_test_pattern_image()
    mask = create_test_mask()
    
    cropped_image, _ = crop_image_to_mask(image, mask, "bbox")
    # Verify specific voxel values in the cropped region
    assert cropped_image[25, 25, 25] == expected_value

@pytest.mark.parametrize("invalid_dims", [
    (-50, 50, 50),
    (0, 0, 0),
    (1000, 1000, 1000)  # larger than image
])
def test_crop_image_to_mask_invalid_dimensions(invalid_dims):
    """Test handling of invalid dimensions."""
    with pytest.raises(ValueError):
        crop_image_to_mask(image, mask, "bbox", invalid_dims)

Copy link
Contributor

@jjjermiah jjjermiah left a comment

Choose a reason for hiding this comment

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

overall looks good, some general comments

  1. It might be hard to test what each step is doing when some functions inevitably resize the results to whats expected

  2. wrote some extra tests to just verify that the functions are doing what they expect, commented out the pyradiomics one for now

  3. some of my review comments seem nitpicky pointing at type annotations but itll help during overall development if were confident about the granular stuff (cough med-imagetools cough)

keeping readii 2.0 in mind, we should think about how to abstract these where possible, to allow for dependency injection instead of a new function for each crop type

re: the types introduced for Centroid, Coordinate, etc. this is something I am planning to use internally in Med-ImageTools, along with other types. having to unpack tuples can become very error-prone and hard to follow, especially when debugging, and they can make validation of values a lot easier.

bounding_box = [min_x, max_x, min_y, max_y, min_z, max_z],
expected_dimensions = crop_dimensions)

return image[min_x:max_x, min_y:max_y, min_z:max_z]
Copy link
Contributor

Choose a reason for hiding this comment

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

not resizing here, output dimensions would not match the dimensions of the other crops?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

no, because the crop_dimensions are used to make the bounding box starting from the centroid. So it's being cropped to the correct dimension right away.

Comment on lines 270 to 362
def crop_to_bounding_box(image:sitk.Image,
bounding_box:tuple[int,int,int,int,int,int],
resize_dimensions:tuple[int,int,int]
) -> sitk.Image:
"""Crop an image to a given bounding box and resize to a specified crop dimensions.

Parameters
----------
image : sitk.Image
Image to crop.
bounding_box : tuple[int,int,int,int,int,int]
Bounding box to crop the image to. The order is (min_x, min_y, min_z, max_x, max_y, max_z).
resize_dimensions : tuple[int,int,int]
Dimensions to resize the image to.

Returns
-------
cropped_image : sitk.Image
Cropped image.
"""
# Check that the number of dimensions in the crop dimensions matches the number of dimensions in the image
validate_new_dimensions(image, resize_dimensions)

# Check that the number of bounding box dimensions match the image dimensions
validate_new_dimensions(image, int(len(bounding_box)/2))

# Get bounding box dimensions for limit testing
bounding_box_dimensions = np.array(bounding_box[3:]) - np.array(bounding_box[:3])

# Test if bounding box coordinates are within the image, move to image edge if not
min_x, max_x, min_y, max_y, min_z, max_z = apply_bounding_box_limits(image, bounding_box, bounding_box_dimensions)

# Crop image to the bounding box
img_crop = image[min_x:max_x, min_y:max_y, min_z:max_z]
# Resample the image to the new dimensions and spacing
img_crop = resize_image(img_crop, resize_dimensions)
return img_crop



def crop_to_maxdim_cube(image:sitk.Image,
bounding_box:tuple[int,int,int,int,int,int],
resize_dimensions:tuple[int,int,int]
) -> sitk.Image:
"""
Crop given image to a cube based on the max dim from a bounding box and resize to specified input size.

Parameters
----------
image : sitk.Image
Image to crop.
bounding_box : tuple[int,int,int,int,int,int]
Bounding box to find maximum dimension from. The order is (min_x, min_y, min_z, max_x, max_y, max_z).
resize_dimensions : tuple[int,int,int]
Crop dimensions to resize the image to.

Returns
-------
sitk.Image: The cropped and resized image.
"""
# Check that the number of dimensions in the crop dimensions matches the number of dimensions in the image
validate_new_dimensions(image, resize_dimensions)

# Check that the number of bounding box dimensions match the image dimensions
validate_new_dimensions(image, len(bounding_box)//2)

# Extract out the bounding box coordinates
min_x, max_x, min_y, max_y, min_z, max_z = bounding_box

# Get maximum dimension of bounding box
max_dim = max(max_x - min_x, max_y - min_y, max_z - min_z)
mean_x = int((max_x + min_x) // 2)
mean_y = int((max_y + min_y) // 2)
mean_z = int((max_z + min_z) // 2)

# define new bounding boxes based on the maximum dimension of ROI bounding box
min_x = int(mean_x - max_dim // 2)
max_x = int(mean_x + max_dim // 2)
min_y = int(mean_y - max_dim // 2)
max_y = int(mean_y + max_dim // 2)
min_z = int(mean_z - max_dim // 2)
max_z = int(mean_z + max_dim // 2)

# Test if bounding box coordinates are within the image, move to image edge if not
min_x, max_x, min_y, max_y, min_z, max_z = apply_bounding_box_limits(image,
bounding_box = [min_x, max_x, min_y, max_y, min_z, max_z],
expected_dimensions = [max_dim, max_dim, max_dim])
# Crop image to the cube bounding box
img_crop = image[min_x:max_x, min_y:max_y, min_z:max_z]
# Resample the image to the new dimensions and spacing
img_crop = resize_image(img_crop, resize_dimensions)
return img_crop

Copy link
Contributor

Choose a reason for hiding this comment

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

this code is a little confusing:

both signatures say that the bounding_box parameter passed in is The order is (min_x, min_y, min_z, max_x, max_y, max_z)

and it looks like bounding_box_dimensions = np.array(bounding_box[3:]) - np.array(bounding_box[:3]) is used to get the sizes in crop_to_bounding_box

but then in the cube one we see min_x, max_x, min_y, max_y, min_z, max_z = bounding_box

the find_bounding_box function used to get the bounding boxes returns:
xstart, xend, ystart, yend, zstart, zend

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: 9

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b1e03b6 and 0e0ac80.

📒 Files selected for processing (3)
  • src/readii/process/images/__init__.py (1 hunks)
  • src/readii/process/images/crop.py (1 hunks)
  • tests/process/images/test_crop.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/readii/process/images/init.py
🧰 Additional context used
🪛 Ruff (0.8.2)
tests/process/images/test_crop.py

6-6: readii.process.images.crop.apply_bounding_box_limits imported but unused

Remove unused import

(F401)


7-7: readii.process.images.crop.check_bounding_box_single_dimension imported but unused

Remove unused import

(F401)


9-9: readii.process.images.crop.crop_to_bounding_box imported but unused

Remove unused import

(F401)


10-10: readii.process.images.crop.crop_to_centroid imported but unused

Remove unused import

(F401)


11-11: readii.process.images.crop.crop_to_maxdim_cube imported but unused

Remove unused import

(F401)


14-14: readii.process.images.crop.validate_new_dimensions imported but unused

Remove unused import

(F401)

⏰ Context from checks skipped due to timeout of 90000ms (8)
  • GitHub Check: Unit-Tests (windows-latest, py312)
  • GitHub Check: Unit-Tests (windows-latest, py311)
  • GitHub Check: Unit-Tests (windows-latest, py310)
  • GitHub Check: Unit-Tests (macos-14, py312)
  • GitHub Check: Unit-Tests (macos-14, py311)
  • GitHub Check: Unit-Tests (macos-14, py310)
  • GitHub Check: Unit-Tests (macos-latest, py311)
  • GitHub Check: Unit-Tests (macos-latest, py310)
🔇 Additional comments (7)
tests/process/images/test_crop.py (3)

111-119: LGTM!

The test properly verifies error handling for invalid labels in both find_bounding_box and find_centroid functions.


125-152: LGTM!

The test provides good coverage for bounding box calculation with various ROI configurations, including edge cases.


158-177: LGTM!

The test provides good coverage for centroid calculation with various ROI configurations, including edge cases.

src/readii/process/images/crop.py (4)

13-46: LGTM!

The function is well-implemented with proper type checking using isinstance(), error handling, and logging.


90-133: LGTM!

The function is well-implemented with proper error handling and includes clear examples in the docstring.


197-235: ⚠️ Potential issue

Add handling for large resize dimensions.

The function fails when crop dimensions are larger than the image dimensions.

Apply this diff to add handling for large resize dimensions:

    validate_new_dimensions(image, centroid)

+    # Get image dimensions
+    img_size = image.GetSize()
+    
+    # If any crop dimension is larger than image dimension, return resized full image
+    if any(c > i for c, i in zip(crop_dimensions, img_size)):
+        return resize(image, size=crop_dimensions)
+
    min_x = int(centroid[0] - crop_dimensions[0] // 2)

Likely invalid or redundant comment.


50-87: ⚠️ Potential issue

Fix docstring return type.

The docstring indicates that the function returns a np.ndarray, but it actually returns a tuple.

Apply this diff to fix the docstring:

    Returns
    -------
-    bounding_box : np.ndarray
-        Numpy array containing the bounding box coordinates around the ROI.
+    bounding_box : tuple
+        Tuple containing the bounding box coordinates (xstart, xend, ystart, yend, zstart, zend).

Likely invalid or redundant comment.

Comment on lines 54 to 62
@pytest.mark.parametrize(
"crop_method, expected_size",
[
("bbox", (50, 50, 50)),
("centroid", (50, 50, 50)),
("cube", (50, 50, 50)),
# ("pyradiomics", (22, 28, 14)),
],
)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance test coverage for cropping methods.

The test only verifies the dimensions of cropped images but doesn't validate the actual content of cropped regions.

Add test cases to verify that the cropped content matches expected regions:

@pytest.mark.parametrize(
    "crop_method, expected_size, expected_content",
    [
        ("bbox", (50, 50, 50), [(25, 25, 25, 100)]),  # x, y, z, expected_value
        ("centroid", (50, 50, 50), [(25, 25, 25, 100)]),
        ("cube", (50, 50, 50), [(25, 25, 25, 100)]),
    ],
)
def test_crop_image_to_mask_content(lung4D_image, lung4D_mask, crop_method, expected_size, expected_content):
    """Test that cropped content matches expected regions."""
    cropped_image, _ = crop_image_to_mask(lung4D_image, lung4D_mask, crop_method, expected_size)
    for x, y, z, expected_value in expected_content:
        assert cropped_image[x, y, z] == expected_value

Comment on lines +63 to +82
def test_crop_image_to_mask_methods(
lung4D_image,
lung4D_mask,
crop_method,
expected_size,
resize_dimensions=(50, 50, 50),
):
"""Test cropping image to mask with different methods"""
cropped_image, cropped_mask = crop_image_to_mask(
lung4D_image,
lung4D_mask,
crop_method,
resize_dimensions,
)
assert (
cropped_image.GetSize() == expected_size
), f"Cropped image size is incorrect, expected {expected_size}, got {cropped_image.GetSize()}"
assert (
cropped_mask.GetSize() == expected_size
), f"Cropped mask size is incorrect, expected {expected_size}, got {cropped_mask.GetSize()}"
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add test cases for error handling.

The test doesn't verify error handling for invalid inputs.

Add test cases to verify error handling:

@pytest.mark.parametrize(
    "crop_method, resize_dimensions, expected_error",
    [
        ("invalid_method", (50, 50, 50), ValueError),
        ("bbox", (-50, 50, 50), ValueError),
        ("centroid", (0, 0, 0), ValueError),
        ("cube", (1000, 1000, 1000), ValueError),
    ],
)
def test_crop_image_to_mask_error_handling(lung4D_image, lung4D_mask, crop_method, resize_dimensions, expected_error):
    """Test error handling for invalid inputs."""
    with pytest.raises(expected_error):
        crop_image_to_mask(lung4D_image, lung4D_mask, crop_method, resize_dimensions)

Comment on lines 172 to 193
def find_centroid(mask:sitk.Image) -> np.ndarray:
"""Find the centroid of a region of interest (ROI) in a given binary mask image.

Parameters
----------
mask : sitk.Image
Mask image to find the centroid within.

Returns
-------
centroid : np.ndarray
Numpy array containing the coordinates of the ROI centroid.
"""
# Convert the mask to a uint8 image
mask_uint = sitk.Cast(mask, sitk.sitkUInt8)
stats = sitk.LabelShapeStatisticsImageFilter()
stats.Execute(mask_uint)
# Get the centroid coordinates as a physical point in the mask
centroid_coords = stats.GetCentroid(1)
# Convert the physical point to an index in the mask array
centroid_idx = mask.TransformPhysicalPointToIndex(centroid_coords)
return centroid_idx
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

Fix docstring return type.

The docstring indicates that the function returns a np.ndarray, but it actually returns a tuple.

Apply this diff to fix the docstring:

    Returns
    -------
-    centroid : np.ndarray
-        Numpy array containing the coordinates of the ROI centroid.
+    centroid : tuple
+        Tuple containing the coordinates of the ROI centroid.
📝 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 find_centroid(mask:sitk.Image) -> np.ndarray:
"""Find the centroid of a region of interest (ROI) in a given binary mask image.
Parameters
----------
mask : sitk.Image
Mask image to find the centroid within.
Returns
-------
centroid : np.ndarray
Numpy array containing the coordinates of the ROI centroid.
"""
# Convert the mask to a uint8 image
mask_uint = sitk.Cast(mask, sitk.sitkUInt8)
stats = sitk.LabelShapeStatisticsImageFilter()
stats.Execute(mask_uint)
# Get the centroid coordinates as a physical point in the mask
centroid_coords = stats.GetCentroid(1)
# Convert the physical point to an index in the mask array
centroid_idx = mask.TransformPhysicalPointToIndex(centroid_coords)
return centroid_idx
def find_centroid(mask:sitk.Image) -> np.ndarray:
"""Find the centroid of a region of interest (ROI) in a given binary mask image.
Parameters
----------
mask : sitk.Image
Mask image to find the centroid within.
Returns
-------
centroid : tuple
Tuple containing the coordinates of the ROI centroid.
"""
# Convert the mask to a uint8 image
mask_uint = sitk.Cast(mask, sitk.sitkUInt8)
stats = sitk.LabelShapeStatisticsImageFilter()
stats.Execute(mask_uint)
# Get the centroid coordinates as a physical point in the mask
centroid_coords = stats.GetCentroid(1)
# Convert the physical point to an index in the mask array
centroid_idx = mask.TransformPhysicalPointToIndex(centroid_coords)
return centroid_idx

Comment on lines 239 to 275
def crop_to_bounding_box(image:sitk.Image,
bounding_box:tuple[int,int,int,int,int,int],
resize_dimensions:tuple[int,int,int]
) -> sitk.Image:
"""Crop an image to a given bounding box and resize to a specified crop dimensions.

Parameters
----------
image : sitk.Image
Image to crop.
bounding_box : tuple[int,int,int,int,int,int]
Bounding box to crop the image to. The order is (min_x, min_y, min_z, max_x, max_y, max_z).
resize_dimensions : tuple[int,int,int]
Dimensions to resize the image to.

Returns
-------
cropped_image : sitk.Image
Cropped image.
"""
# Check that the number of dimensions in the crop dimensions matches the number of dimensions in the image
validate_new_dimensions(image, resize_dimensions)

# Check that the number of bounding box dimensions match the image dimensions
validate_new_dimensions(image, int(len(bounding_box)/2))

# Get bounding box dimensions for limit testing
bounding_box_dimensions = np.array(bounding_box[3:]) - np.array(bounding_box[:3])

# Test if bounding box coordinates are within the image, move to image edge if not
min_x, max_x, min_y, max_y, min_z, max_z = apply_bounding_box_limits(image, bounding_box, bounding_box_dimensions)

# Crop image to the bounding box
img_crop = image[min_x:max_x, min_y:max_y, min_z:max_z]
# Resample the image to the new dimensions and spacing
img_crop = resize(img_crop, size = resize_dimensions)
return img_crop
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

Fix docstring parameter description.

The docstring for bounding_box parameter doesn't match the actual order of coordinates.

Apply this diff to fix the docstring:

    bounding_box : tuple[int,int,int,int,int,int]
-        Bounding box to crop the image to. The order is (min_x, min_y, min_z, max_x, max_y, max_z).
+        Bounding box to crop the image to. The order is (min_x, max_x, min_y, max_y, min_z, max_z).
📝 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 crop_to_bounding_box(image:sitk.Image,
bounding_box:tuple[int,int,int,int,int,int],
resize_dimensions:tuple[int,int,int]
) -> sitk.Image:
"""Crop an image to a given bounding box and resize to a specified crop dimensions.
Parameters
----------
image : sitk.Image
Image to crop.
bounding_box : tuple[int,int,int,int,int,int]
Bounding box to crop the image to. The order is (min_x, min_y, min_z, max_x, max_y, max_z).
resize_dimensions : tuple[int,int,int]
Dimensions to resize the image to.
Returns
-------
cropped_image : sitk.Image
Cropped image.
"""
# Check that the number of dimensions in the crop dimensions matches the number of dimensions in the image
validate_new_dimensions(image, resize_dimensions)
# Check that the number of bounding box dimensions match the image dimensions
validate_new_dimensions(image, int(len(bounding_box)/2))
# Get bounding box dimensions for limit testing
bounding_box_dimensions = np.array(bounding_box[3:]) - np.array(bounding_box[:3])
# Test if bounding box coordinates are within the image, move to image edge if not
min_x, max_x, min_y, max_y, min_z, max_z = apply_bounding_box_limits(image, bounding_box, bounding_box_dimensions)
# Crop image to the bounding box
img_crop = image[min_x:max_x, min_y:max_y, min_z:max_z]
# Resample the image to the new dimensions and spacing
img_crop = resize(img_crop, size = resize_dimensions)
return img_crop
def crop_to_bounding_box(image:sitk.Image,
bounding_box:tuple[int,int,int,int,int,int],
resize_dimensions:tuple[int,int,int]
) -> sitk.Image:
"""Crop an image to a given bounding box and resize to a specified crop dimensions.
Parameters
----------
image : sitk.Image
Image to crop.
bounding_box : tuple[int,int,int,int,int,int]
Bounding box to crop the image to. The order is (min_x, max_x, min_y, max_y, min_z, max_z).
resize_dimensions : tuple[int,int,int]
Dimensions to resize the image to.
Returns
-------
cropped_image : sitk.Image
Cropped image.
"""
# Check that the number of dimensions in the crop dimensions matches the number of dimensions in the image
validate_new_dimensions(image, resize_dimensions)
# Check that the number of bounding box dimensions match the image dimensions
validate_new_dimensions(image, int(len(bounding_box)/2))
# Get bounding box dimensions for limit testing
bounding_box_dimensions = np.array(bounding_box[3:]) - np.array(bounding_box[:3])
# Test if bounding box coordinates are within the image, move to image edge if not
min_x, max_x, min_y, max_y, min_z, max_z = apply_bounding_box_limits(image, bounding_box, bounding_box_dimensions)
# Crop image to the bounding box
img_crop = image[min_x:max_x, min_y:max_y, min_z:max_z]
# Resample the image to the new dimensions and spacing
img_crop = resize(img_crop, size = resize_dimensions)
return img_crop

Comment on lines 137 to 168
def apply_bounding_box_limits(image:sitk.Image,
bounding_box:tuple[int,int,int,int,int,int],
expected_dimensions:tuple[int,int,int]
) -> tuple:
"""Check that bounding box coordinates are within the image dimensions. If not, move bounding box to the edge of the image and expand to expected dimension.

Parameters
----------
image : sitk.Image
Image to check the bounding box coordinates against.
bounding_box : tuple[int,int,int,int,int,int]
Bounding box to check the coordinates of.
expected_dimensions : tuple[int,int,int]
Expected dimensions of the bounding box. Used if the bounding box needs to be shifted to the edge of the image.

Returns
-------
min_x, min_y, min_z, max_x, max_y, max_z : tuple[int,int,int,int,int,int]
Updated bounding box coordinates.
"""
# Get the size of the image to use to determine if crop dimensions are larger than the image
img_x, img_y, img_z = image.GetSize()

# Extract the bounding box coordinates
min_x, max_x, min_y, max_y, min_z, max_z = bounding_box

# Check each bounding box dimensions coordinates and move to image edge if not within image
min_x, max_x = check_bounding_box_single_dimension(min_x, max_x, expected_dimensions[0], img_x)
min_y, max_y = check_bounding_box_single_dimension(min_y, max_y, expected_dimensions[1], img_y)
min_z, max_z = check_bounding_box_single_dimension(min_z, max_z, expected_dimensions[2], img_z)

return min_x, max_x, min_y, max_y, min_z, max_z
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

Fix docstring parameter description.

The docstring for bounding_box parameter doesn't match the actual order of coordinates.

Apply this diff to fix the docstring:

    bounding_box : tuple[int,int,int,int,int,int]
-        Bounding box to check the coordinates of.
+        Bounding box to check the coordinates of. The order is (min_x, max_x, min_y, max_y, min_z, max_z).
📝 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 apply_bounding_box_limits(image:sitk.Image,
bounding_box:tuple[int,int,int,int,int,int],
expected_dimensions:tuple[int,int,int]
) -> tuple:
"""Check that bounding box coordinates are within the image dimensions. If not, move bounding box to the edge of the image and expand to expected dimension.
Parameters
----------
image : sitk.Image
Image to check the bounding box coordinates against.
bounding_box : tuple[int,int,int,int,int,int]
Bounding box to check the coordinates of.
expected_dimensions : tuple[int,int,int]
Expected dimensions of the bounding box. Used if the bounding box needs to be shifted to the edge of the image.
Returns
-------
min_x, min_y, min_z, max_x, max_y, max_z : tuple[int,int,int,int,int,int]
Updated bounding box coordinates.
"""
# Get the size of the image to use to determine if crop dimensions are larger than the image
img_x, img_y, img_z = image.GetSize()
# Extract the bounding box coordinates
min_x, max_x, min_y, max_y, min_z, max_z = bounding_box
# Check each bounding box dimensions coordinates and move to image edge if not within image
min_x, max_x = check_bounding_box_single_dimension(min_x, max_x, expected_dimensions[0], img_x)
min_y, max_y = check_bounding_box_single_dimension(min_y, max_y, expected_dimensions[1], img_y)
min_z, max_z = check_bounding_box_single_dimension(min_z, max_z, expected_dimensions[2], img_z)
return min_x, max_x, min_y, max_y, min_z, max_z
def apply_bounding_box_limits(image:sitk.Image,
bounding_box:tuple[int,int,int,int,int,int],
expected_dimensions:tuple[int,int,int]
) -> tuple:
"""Check that bounding box coordinates are within the image dimensions. If not, move bounding box to the edge of the image and expand to expected dimension.
Parameters
----------
image : sitk.Image
Image to check the bounding box coordinates against.
bounding_box : tuple[int,int,int,int,int,int]
Bounding box to check the coordinates of. The order is (min_x, max_x, min_y, max_y, min_z, max_z).
expected_dimensions : tuple[int,int,int]
Expected dimensions of the bounding box. Used if the bounding box needs to be shifted to the edge of the image.
Returns
-------
min_x, min_y, min_z, max_x, max_y, max_z : tuple[int,int,int,int,int,int]
Updated bounding box coordinates.
"""
# Get the size of the image to use to determine if crop dimensions are larger than the image
img_x, img_y, img_z = image.GetSize()
# Extract the bounding box coordinates
min_x, max_x, min_y, max_y, min_z, max_z = bounding_box
# Check each bounding box dimensions coordinates and move to image edge if not within image
min_x, max_x = check_bounding_box_single_dimension(min_x, max_x, expected_dimensions[0], img_x)
min_y, max_y = check_bounding_box_single_dimension(min_y, max_y, expected_dimensions[1], img_y)
min_z, max_z = check_bounding_box_single_dimension(min_z, max_z, expected_dimensions[2], img_z)
return min_x, max_x, min_y, max_y, min_z, max_z

@strixy16
Copy link
Collaborator Author

strixy16 commented Jan 21, 2025

So I was just pretty much copying what was working in the FMCIB pipeline and then planning to make it better as we worked on READII 2.0, so that kind of answers most of these but figured I'd address these now.

  1. It might be hard to test what each step is doing when some functions inevitably resize the results to whats expected

Yeah, I've already noticed this as I started writing test functions.

  1. some of my review comments seem nitpicky pointing at type annotations but itll help during overall development if were confident about the granular stuff (cough med-imagetools cough)

Agreed, I've been using ruff pretty regularly to try and catch them all, but appreciate the extra check.

keeping readii 2.0 in mind, we should think about how to abstract these where possible, to allow for dependency injection instead of a new function for each crop type

I would like to organize these like the negative controls, just didn't have the brain power to do that the other day.

re: the types introduced for Centroid, Coordinate, etc. this is something I am planning to use internally in Med-ImageTools, along with other types. having to unpack tuples can become very error-prone and hard to follow, especially when debugging, and they can make validation of values a lot easier.

I totally agree. I just got really stuck trying to mix between my FMCIB code I knew worked and figuring out how to use those objects. Had to commit to one of them.

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: 4

🧹 Nitpick comments (1)
src/readii/process/images/crop.py (1)

83-85: Consider implementing the suggested bounding box class.

The TODO comment suggests using a class for bounding box coordinates, which would improve type safety and make the code more maintainable. This would help avoid coordinate order confusion and make the code more self-documenting.

Would you like me to help design the BoundingBox class with proper coordinate handling?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e0ac80 and 2f23865.

📒 Files selected for processing (1)
  • src/readii/process/images/crop.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
src/readii/process/images/crop.py

268-268: Undefined name max_x

(F821)


268-268: Undefined name min_x

(F821)


268-268: Undefined name max_y

(F821)


268-268: Undefined name min_y

(F821)


268-268: Undefined name max_z

(F821)


268-268: Undefined name min_z

(F821)


398-398: Use != to compare constant literals

Replace is not with !=

(F632)

🪛 GitHub Actions: CI-CD
src/readii/process/images/crop.py

[error] 268-268: UnboundLocalError: Cannot access local variable 'max_x' where it is not associated with a value in apply_bounding_box_limits function


[warning] 398-398: SyntaxWarning: 'is not' with a literal. Did you mean '!='? in condition 'crop_method is not "pyradiomics"'

🔇 Additional comments (1)
src/readii/process/images/crop.py (1)

172-193: Fix docstring return type.

The docstring indicates that the function returns a np.ndarray, but it actually returns a tuple.

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: 3

🧹 Nitpick comments (3)
src/readii/process/images/crop.py (3)

13-47: Consider consolidating error logging.

The error logging pattern is repeated multiple times. Consider creating a helper function to handle error logging and raising.

+def _raise_dimension_error(msg: str) -> None:
+    logger.exception(msg)
+    raise ValueError(msg)

 def validate_new_dimensions(image:sitk.Image,
                           new_dimensions:tuple | int
                           ) -> None:
     if isinstance(new_dimensions, tuple):
         if len(new_dimensions) != image.GetDimension():
             msg = f"Number of dimensions in new_dimensions ({len(new_dimensions)}) does not match the number of dimensions in the image ({image.GetDimension()})."
-            logger.exception(msg)
-            raise ValueError(msg)
+            _raise_dimension_error(msg)

83-85: Consider implementing the suggested class structure.

The TODO comment suggests using a class for the bounding box, which would improve type safety and make the code more maintainable.

Would you like me to help design the Coordinate and BoundingBox classes?


365-368: Improve mask correction logic.

The condition for updating the mask could be more explicit.

-    if corrected_mask:
+    if corrected_mask is not None:
         mask = corrected_mask
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f23865 and eca1bf3.

📒 Files selected for processing (1)
  • src/readii/process/images/crop.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
src/readii/process/images/crop.py

266-266: Undefined name max_x

(F821)


266-266: Undefined name min_x

(F821)


266-266: Undefined name max_y

(F821)


266-266: Undefined name min_y

(F821)


266-266: Undefined name max_z

(F821)


266-266: Undefined name min_z

(F821)

🪛 GitHub Actions: CI-CD
src/readii/process/images/crop.py

[error] 426-426: Invalid crop method handling: The code expects 'bounding_box' but test uses 'bbox'. Method name mismatch in crop_image_to_mask function.

🔇 Additional comments (2)
src/readii/process/images/crop.py (2)

1-11: LGTM! Well-organized imports with proper type hints.

The imports are logically grouped and all necessary for the functionality.


401-403: ⚠️ Potential issue

Fix string comparison operator.

Using is not with string literals is incorrect and could lead to unexpected behavior.

-    if resize_dimensions is None and crop_method is not "pyradiomics":
+    if resize_dimensions is None and crop_method != "pyradiomics":

Likely invalid or redundant comment.


def crop_image_to_mask(image:sitk.Image,
mask:sitk.Image,
crop_method:Literal["bounding_box", "centroid", "cube", "pyradiomics"],
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

Fix inconsistent method names.

The code uses "bounding_box" but the error message uses "bbox", causing confusion and test failures.

-                      crop_method:Literal["bounding_box", "centroid", "cube", "pyradiomics"],
+                      crop_method:Literal["bbox", "centroid", "cube", "pyradiomics"],

-        case "bounding_box":
+        case "bbox":

-            msg = f"Invalid crop method: {crop_method}. Must be one of 'bbox', 'centroid', 'cube', or 'pyradiomics'."
+            msg = f"Invalid crop method: {crop_method}. Must be one of 'bbox', 'centroid', 'cube', or 'pyradiomics'."

Also applies to: 406-406, 425-425

Comment on lines 64 to 65
bounding_box : np.ndarray
Numpy array containing the bounding box coordinates around the ROI. Order is [min_x, max_x, min_y, max_y, min_z, max_z].
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

Fix return type annotation mismatch.

The docstring specifies np.ndarray but the function's return type hint is tuple.

-def find_bounding_box(mask:sitk.Image,
-                     min_dim_size:int = 4
-                     ) -> tuple:
+def find_bounding_box(mask:sitk.Image,
+                     min_dim_size:int = 4
+                     ) -> tuple[int, int, int, int, int, int]:
📝 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
bounding_box : np.ndarray
Numpy array containing the bounding box coordinates around the ROI. Order is [min_x, max_x, min_y, max_y, min_z, max_z].
def find_bounding_box(mask:sitk.Image,
min_dim_size:int = 4
) -> tuple[int, int, int, int, int, int]:

Comment on lines 265 to 267
# Current bounding box dimensions
current_image_dimensions = (max_x - min_x, max_y - min_y, max_z - min_z)

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

Fix undefined variables usage.

The variables min_x, max_x, etc. are used before they're extracted from the bounding box.

-    # Current bounding box dimensions
-    current_image_dimensions = (max_x - min_x, max_y - min_y, max_z - min_z)
+    # Extract the bounding box coordinates first
+    min_x, max_x, min_y, max_y, min_z, max_z = bounding_box
+    
+    # Current bounding box dimensions
+    current_image_dimensions = (max_x - min_x, max_y - min_y, max_z - min_z)
📝 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
# Current bounding box dimensions
current_image_dimensions = (max_x - min_x, max_y - min_y, max_z - min_z)
# Extract the bounding box coordinates first
min_x, max_x, min_y, max_y, min_z, max_z = bounding_box
# Current bounding box dimensions
current_image_dimensions = (max_x - min_x, max_y - min_y, max_z - min_z)
🧰 Tools
🪛 Ruff (0.8.2)

266-266: Undefined name max_x

(F821)


266-266: Undefined name min_x

(F821)


266-266: Undefined name max_y

(F821)


266-266: Undefined name min_y

(F821)


266-266: Undefined name max_z

(F821)


266-266: Undefined name min_z

(F821)

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: 4

🧹 Nitpick comments (5)
src/readii/process/images/utils/bounding_box.py (2)

27-31: Consider adding dimension validation.

Since this class represents dimensions, consider adding validation to ensure values are positive.

@dataclass
class Size3D(Point3D):
    """Represent the size of a 3D object using its width, height, and depth."""
-    pass
+    def __post_init__(self):
+        """Validate that dimensions are positive."""
+        if any(dim < 0 for dim in (self.x, self.y, self.z)):
+            raise ValueError("Dimensions must be positive")

34-44: Consider adding bounds validation.

Since this represents a coordinate in 3D space, consider adding validation to ensure coordinates are within valid bounds when performing operations.

def __add__(self, other: Size3D) -> Coordinate:
    """Add a size to a coordinate to get a second coordinate."""
+    # Optional: Add validation for maximum bounds
+    max_bounds = (1000, 1000, 1000)  # Example bounds
+    result = Coordinate(x=self.x + other.x, y=self.y + other.y, z=self.z + other.z)
+    if any(coord > bound for coord, bound in zip((result.x, result.y, result.z), max_bounds)):
+        raise ValueError("Resulting coordinate exceeds maximum bounds")
-    return Coordinate(x=self.x + other.x, y=self.y + other.y, z=self.z + other.z)
+    return result
tests/process/images/test_crop.py (2)

18-51: Consider using pathlib for test paths.

Using pathlib would make the path handling more robust across different operating systems.

+from pathlib import Path

@pytest.fixture
def nsclcCT():
-    return "tests/NSCLC_Radiogenomics/R01-001/09-06-1990-NA-CT_CHEST_ABD_PELVIS_WITH_CON-98785/3.000000-THORAX_1.0_B45f-95741"
+    return Path("tests/NSCLC_Radiogenomics/R01-001/09-06-1990-NA-CT_CHEST_ABD_PELVIS_WITH_CON-98785/3.000000-THORAX_1.0_B45f-95741")

125-177: LGTM! Well-structured test cases.

The parameterized tests cover a good range of scenarios. Consider adding edge cases:

  1. Empty mask (all zeros)
  2. Full mask (all ones)
  3. Mask with multiple disconnected regions
@pytest.mark.parametrize(
    "image_and_mask_with_roi, expected_bbox",
    [
        # Empty mask
        ((0, 0, 0, 0, 0, 0), (0, 4, 0, 4, 0, 4)),
        # Full mask
        ((0, 100, 0, 100, 0, 100), (0, 100, 0, 100, 0, 100)),
    ],
    indirect=["image_and_mask_with_roi"],
)
src/readii/process/images/crop.py (1)

364-365: Use explicit None check for mask_label.

The condition could be more explicit to handle edge cases better.

-    if mask_label is None:
+    if mask_label is None:
+        # Log that we're using default label
+        logger.info("No mask label provided, using first non-zero voxel label")
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eca1bf3 and ee6eac3.

⛔ Files ignored due to path filters (1)
  • pixi.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • notebooks/crop_testing.ipynb (1 hunks)
  • notebooks/viz_neg_controls.ipynb (4 hunks)
  • src/readii/process/images/crop.py (1 hunks)
  • src/readii/process/images/utils/bounding_box.py (1 hunks)
  • tests/process/images/test_crop.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
src/readii/process/images/crop.py

270-270: Undefined name max_x

(F821)


270-270: Undefined name min_x

(F821)


270-270: Undefined name max_y

(F821)


270-270: Undefined name min_y

(F821)


270-270: Undefined name max_z

(F821)


270-270: Undefined name min_z

(F821)

tests/process/images/test_crop.py

6-6: readii.process.images.crop.apply_bounding_box_limits imported but unused

Remove unused import

(F401)


7-7: readii.process.images.crop.check_bounding_box_single_dimension imported but unused

Remove unused import

(F401)


9-9: readii.process.images.crop.crop_to_bounding_box imported but unused

Remove unused import

(F401)


10-10: readii.process.images.crop.crop_to_centroid imported but unused

Remove unused import

(F401)


11-11: readii.process.images.crop.crop_to_maxdim_cube imported but unused

Remove unused import

(F401)


14-14: readii.process.images.crop.validate_new_dimensions imported but unused

Remove unused import

(F401)

🔇 Additional comments (7)
notebooks/viz_neg_controls.ipynb (2)

102-108: Great improvements to the visualization!

The changes enhance the visual presentation by:

  • Using appropriate color maps ('grey' for the donut image and 'Greens' for the mask)
  • Removing axis ticks for a cleaner look

388-402: Environment configuration looks good.

The kernel name and Python version updates maintain consistency with the development environment.

src/readii/process/images/utils/bounding_box.py (2)

6-24: LGTM! Clean implementation of the base Point3D class.

The implementation is well-structured with proper type hints and clear docstrings. The use of @DataClass reduces boilerplate code.


48-62: LGTM! Well-documented centroid class.

The docstring provides clear and detailed information about the class purpose and attributes.

src/readii/process/images/crop.py (3)

14-47: LGTM! Robust dimension validation.

The function properly validates dimensions with clear error messages and good type checking.


406-408: ⚠️ Potential issue

Fix string comparison operator.

Using is not with string literals is incorrect. Use != instead.

-    if resize_dimensions is None and crop_method is not "pyradiomics":  
+    if resize_dimensions is None and crop_method != "pyradiomics":  

Likely invalid or redundant comment.


381-431: ⚠️ Potential issue

Fix inconsistent method names.

The code uses "bounding_box" in the Literal type but "bbox" elsewhere. This inconsistency could cause errors.

-                      crop_method:Literal["bounding_box", "centroid", "cube", "pyradiomics"],
+                      crop_method:Literal["bbox", "centroid", "cube", "pyradiomics"],

-        case "bounding_box":
+        case "bbox":

-            msg = f"Invalid crop method: {crop_method}. Must be one of 'bounding_box', 'centroid', 'cube', or 'pyradiomics'."
+            msg = f"Invalid crop method: {crop_method}. Must be one of 'bbox', 'centroid', 'cube', or 'pyradiomics'."

Likely invalid or redundant comment.

Comment on lines +54 to +83
@pytest.mark.parametrize(
"crop_method, expected_size",
[
("bounding_box", (50, 50, 50)),
("centroid", (50, 50, 50)),
("cube", (50, 50, 50)),
# ("pyradiomics", (22, 28, 14)),
],
)
def test_crop_image_to_mask_methods(
lung4D_image,
lung4D_mask,
crop_method,
expected_size,
resize_dimensions=(50, 50, 50),
):
"""Test cropping image to mask with different methods"""
cropped_image, cropped_mask = crop_image_to_mask(
lung4D_image,
lung4D_mask,
crop_method,
resize_dimensions,
)
assert (
cropped_image.GetSize() == expected_size
), f"Cropped image size is incorrect, expected {expected_size}, got {cropped_image.GetSize()}"
assert (
cropped_mask.GetSize() == expected_size
), f"Cropped mask size is incorrect, expected {expected_size}, got {cropped_mask.GetSize()}"

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance test coverage for cropping methods.

The test only verifies dimensions but should also validate:

  1. The actual content of cropped regions
  2. Edge cases (empty masks, single-pixel masks)
  3. Error cases (invalid dimensions)
@pytest.mark.parametrize("invalid_input", [
    (None, None),
    (sitk.Image(1,1,1), None),
    (None, sitk.Image(1,1,1))
])
def test_crop_image_to_mask_invalid_input(invalid_input):
    """Test error handling for invalid inputs."""
    image, mask = invalid_input
    with pytest.raises(ValueError):
        crop_image_to_mask(image, mask, "bbox", (50,50,50))

Comment on lines +64 to +68
"source": [
"centroid = Centroid(centroid_x, centroid_y, centroid_z)\n",
"crop_dimensions = Size3D(50,50,50)\n",
"\n",
"centroid - crop_dimensions "
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

Fix error in centroid calculation.

The notebook shows an AttributeError in the centroid calculation. This needs to be fixed before merging.

-centroid - crop_dimensions 
+# First ensure the centroid coordinates are valid
+if not all(0 <= c < s for c, s in zip(centroid.as_tuple, image.GetSize())):
+    raise ValueError("Centroid coordinates must be within image dimensions")
+# Then perform the calculation
+result = centroid - crop_dimensions
📝 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
"source": [
"centroid = Centroid(centroid_x, centroid_y, centroid_z)\n",
"crop_dimensions = Size3D(50,50,50)\n",
"\n",
"centroid - crop_dimensions "
"source": [
"centroid = Centroid(centroid_x, centroid_y, centroid_z)\n",
"crop_dimensions = Size3D(50,50,50)\n",
"\n",
"# First ensure the centroid coordinates are valid\n",
"if not all(0 <= c < s for c, s in zip(centroid.as_tuple, image.GetSize())):\n",
" raise ValueError(\"Centroid coordinates must be within image dimensions\")\n",
"# Then perform the calculation\n",
"result = centroid - crop_dimensions"
]

Comment on lines +23 to +24
"ct_path = \"/Users/katyscott/Documents/READII-2-ROQC/readii-fmcib/archive/temp_complete_output/procdata/RADCURE/images/mit_outputs/0_RADCURE-0020/CT/CT.nii.gz\"\n",
"mask_path = \"/Users/katyscott/Documents/READII-2-ROQC/readii-fmcib/archive/temp_complete_output/procdata/RADCURE/images/mit_outputs/0_RADCURE-0020/RTSTRUCT_CT/GTV.nii.gz\"\n",
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

Remove hardcoded paths containing sensitive information.

The paths contain usernames and potentially sensitive directory structures. Consider:

  1. Using environment variables or config files
  2. Using relative paths
  3. Removing sensitive information
-ct_path = "/Users/katyscott/Documents/READII-2-ROQC/readii-fmcib/archive/temp_complete_output/procdata/RADCURE/images/mit_outputs/0_RADCURE-0020/CT/CT.nii.gz"
-mask_path = "/Users/katyscott/Documents/READII-2-ROQC/readii-fmcib/archive/temp_complete_output/procdata/RADCURE/images/mit_outputs/0_RADCURE-0020/RTSTRUCT_CT/GTV.nii.gz"
+from pathlib import Path
+ct_path = Path("data/test/CT.nii.gz")
+mask_path = Path("data/test/GTV.nii.gz")
📝 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
"ct_path = \"/Users/katyscott/Documents/READII-2-ROQC/readii-fmcib/archive/temp_complete_output/procdata/RADCURE/images/mit_outputs/0_RADCURE-0020/CT/CT.nii.gz\"\n",
"mask_path = \"/Users/katyscott/Documents/READII-2-ROQC/readii-fmcib/archive/temp_complete_output/procdata/RADCURE/images/mit_outputs/0_RADCURE-0020/RTSTRUCT_CT/GTV.nii.gz\"\n",
from pathlib import Path
ct_path = Path("data/test/CT.nii.gz")
mask_path = Path("data/test/GTV.nii.gz")

Comment on lines +269 to +271
# Current bounding box dimensions
current_image_dimensions = (max_x - min_x, max_y - min_y, max_z - min_z)
# bounding_box[1] - bounding_box[0], bounding_box[]
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

Fix undefined variables in bounding box dimensions calculation.

The variables max_x, min_x, etc. are used before they're extracted from the bounding box.

-    # Current bounding box dimensions
-    current_image_dimensions = (max_x - min_x, max_y - min_y, max_z - min_z)
-    # bounding_box[1] - bounding_box[0], bounding_box[]
+    # Extract coordinates first
+    min_x, max_x, min_y, max_y, min_z, max_z = bounding_box
+    # Calculate current bounding box dimensions
+    current_image_dimensions = (max_x - min_x, max_y - min_y, max_z - min_z)
📝 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
# Current bounding box dimensions
current_image_dimensions = (max_x - min_x, max_y - min_y, max_z - min_z)
# bounding_box[1] - bounding_box[0], bounding_box[]
# Extract coordinates first
min_x, max_x, min_y, max_y, min_z, max_z = bounding_box
# Calculate current bounding box dimensions
current_image_dimensions = (max_x - min_x, max_y - min_y, max_z - min_z)
🧰 Tools
🪛 Ruff (0.8.2)

270-270: Undefined name max_x

(F821)


270-270: Undefined name min_x

(F821)


270-270: Undefined name max_y

(F821)


270-270: Undefined name min_y

(F821)


270-270: Undefined name max_z

(F821)


270-270: Undefined name min_z

(F821)

@strixy16 strixy16 marked this pull request as draft February 5, 2025 20:21
@strixy16 strixy16 closed this Feb 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

2 participants