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

fix: add type hint for keys method and improve modality handling in ImageAutoInput #218

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
469bd14
chore(sem-ver): 1.22.0
Feb 7, 2025
d6b5eee
ci:
jjjermiah Feb 7, 2025
7f8a666
fix: handle image attribute errors and update Scan class inheritance
jjjermiah Feb 7, 2025
76d3995
fix: add type hint for keys method and improve modality handling in I…
jjjermiah Feb 7, 2025
30a129f
feat: add desired_size to centroid bounding box generation (#219)
strixy16 Feb 7, 2025
f19f5fd
Merge remote-tracking branch 'origin/main' into jjjermiah/input_classes
jjjermiah Feb 7, 2025
5a9e8e8
feat: enhance ImageCSVLoader and Scan classes with improved type hint…
jjjermiah Feb 7, 2025
eb32552
chore: devnotes doc
jjjermiah Feb 7, 2025
db7663e
feat: simplify __getitem__ method in ImageCSVLoader and add __repr__ …
jjjermiah Feb 7, 2025
67bd800
feat: enhance DICOM reading functions with improved parameter handlin…
jjjermiah Feb 7, 2025
9db034a
feat: add PETImageType to module exports for enhanced functionality
jjjermiah Feb 7, 2025
8eeb423
feat: add desired_size to centroid bounding box generation (#219)
strixy16 Feb 7, 2025
ba4304e
fix: handle odd extra dimension values in expand_to_min_size (#220)
strixy16 Feb 7, 2025
c93a6c3
chore: update lockfile
jjjermiah Feb 7, 2025
3365556
chore: update pixi-version to v0.41.1 in GitHub workflows
jjjermiah Feb 7, 2025
083aab4
chore: set locked to false in GitHub workflows
jjjermiah Feb 7, 2025
201409f
feat: add support for Python 3.13 in workflows and configuration
jjjermiah Feb 7, 2025
481e90b
fix: add function to handle out of bounds coordinates for a RegionBox…
strixy16 Feb 7, 2025
0c643e3
refactor: add type hints and improve code readability in ops and scan…
jjjermiah Feb 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, macos-13, windows-latest]
env: ["py310", "py311", "py312"]
env: ["py310", "py311", "py312", "py313"]

steps:
- uses: actions/checkout@v4
Expand All @@ -31,8 +31,9 @@ jobs:
uses: prefix-dev/setup-pixi@v0.8.1
with:
environments: ${{ matrix.env }}
pixi-version: v0.40.0
pixi-version: v0.41.1
cache: true
locked: false

- name: Run pytest
env:
Expand Down Expand Up @@ -74,7 +75,7 @@ jobs:
uses: prefix-dev/setup-pixi@v0.8.1
with:
environments: ${{ matrix.env }}
pixi-version: v0.40.0
pixi-version: v0.41.1
cache: true
locked: false # wont be the same because of the tag

Expand Down Expand Up @@ -117,7 +118,7 @@ jobs:
uses: prefix-dev/setup-pixi@v0.8.1
with:
environments: ${{ matrix.env }}
pixi-version: v0.40.0
pixi-version: v0.41.1
cache: true
locked: false # wont be the same because of the tag

Expand Down Expand Up @@ -228,7 +229,6 @@ jobs:

- name: Publish to PyPI
# only push to pypi if on main
if: github.ref == 'refs/heads/main'
uses: pypa/gh-action-pypi-publish@release/v1
with:
verbose: true
Expand Down
318 changes: 318 additions & 0 deletions devnotes/input_classes.ipynb

Large diffs are not rendered by default.

3,830 changes: 3,207 additions & 623 deletions pixi.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ test = ["test", "quality"]
py310 = { features = ["py310", "test"], no-default-feature = true }
py311 = { features = ["py311", "test"], no-default-feature = true }
py312 = { features = ["py312", "test"], no-default-feature = true }
py313 = { features = ["py313", "test"], no-default-feature = true }

############################################## DEV ###############################################
[feature.dev.pypi-dependencies]
Expand Down Expand Up @@ -49,6 +50,8 @@ python = "3.10.*"
python = "3.11.*"
[feature.py312.dependencies]
python = "3.12.*"
[feature.py313.dependencies]
python = "3.13.*"

############################################## TEST ################################################
[feature.test.pypi-dependencies]
Expand Down
5 changes: 4 additions & 1 deletion src/imgtools/autopipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,10 @@ def process_one_subject(self, subject_id):

# Process image (CT/MR)
elif modality == "CT" or modality == 'MR':
image = read_results[i].image
try:
image = read_results[i].image
except AttributeError:
image = read_results[i]
if len(image.GetSize()) == 4:
assert image.GetSize()[-1] == 1, f"There is more than one volume in this CT file for {subject_id}."
extractor = sitk.ExtractImageFilter()
Expand Down
62 changes: 43 additions & 19 deletions src/imgtools/coretypes/box.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import math
from dataclasses import dataclass, field
from enum import Enum

Expand Down Expand Up @@ -108,7 +109,7 @@ def from_tuple(
return cls(Coordinate3D(*coordmin), Coordinate3D(*coordmax))

@classmethod
def from_mask_centroid(cls, mask: sitk.Image, label: int = 1) -> RegionBox:
def from_mask_centroid(cls, mask: sitk.Image, label: int = 1, desired_size: int | None = None) -> RegionBox:
"""Creates a RegionBox from the centroid of a mask image.

Parameters
Expand All @@ -117,6 +118,8 @@ def from_mask_centroid(cls, mask: sitk.Image, label: int = 1) -> RegionBox:
The input mask image.
label : int, optional
label in the mask image to calculate the centroid.
desired_size : int | None, optional
The desired size of the box. If None, the minimum size default from `expand_to_min_size` is used.

Returns
-------
Expand All @@ -131,8 +134,9 @@ def from_mask_centroid(cls, mask: sitk.Image, label: int = 1) -> RegionBox:
centroid_idx = mask.TransformPhysicalPointToIndex(centroid)

return RegionBox(
Coordinate3D(*centroid_idx), Coordinate3D(*centroid_idx)
)
Coordinate3D(*centroid_idx),
Coordinate3D(*centroid_idx)
).expand_to_cube(desired_size)

@classmethod
def from_mask_bbox(cls, mask: sitk.Image, label: int = 1) -> RegionBox:
Expand Down Expand Up @@ -275,15 +279,22 @@ def expand_to_min_size(self, size: int = 5) -> RegionBox:
Notes
-----
Validation is done to ensure that any min coordinates that are negative are set to 0,
and the difference is added to the maximum coordinates
and the difference is added to the maximum coordinates.

If an extra dimension is not an integer (e.g. 1.5), the bounding box is shifted 1 voxel towards the minimum in that dimension.
"""
extra_x = max(0, size - self.size.width) // 2
extra_y = max(0, size - self.size.height) // 2
extra_z = max(0, size - self.size.depth) // 2
# Calculate extra dimensions to add to the existing coordinates
extra_x = max(0, size - self.size.width) / 2
extra_y = max(0, size - self.size.height) / 2
extra_z = max(0, size - self.size.depth) / 2

min_coord = self.min - (extra_x, extra_y, extra_z)
max_coord = self.max + (extra_x, extra_y, extra_z)
# Round extra dimension values UP to the nearest integer before adding to existing minimum coordinates
min_coord = self.min - tuple([math.ceil(extra) for extra in(extra_x, extra_y, extra_z)])

# Round extra dimensions DOWN to the nearest integer before adding to existing maximum coordinates
max_coord = self.max + tuple([math.floor(extra) for extra in(extra_x, extra_y, extra_z)])

# Adjust negative coordinates to ensure that the min values are not negative
self._adjust_negative_coordinates(min_coord, max_coord)

return RegionBox(min=min_coord, max=max_coord)
Expand All @@ -302,6 +313,21 @@ def _adjust_negative_coordinates(
setattr(min_coord, axis, 0)
setattr(max_coord, axis, getattr(max_coord, axis) + diff)


def check_out_of_bounds_coordinates(self, image: sitk.Image) -> None:
"""Adjust the coordinates to ensure that the max values are not greater than the image size."""
# if any of the max values are greater than the image size, set them to the image size,
# and subtract the difference from the min values
for idx, axis in enumerate(["x", "y", "z"]):
max_value = getattr(self.max, axis)
image_size = image.GetSize()[idx]
if max_value > image_size:
logger.debug(f"Adjusting box {axis} coordinates to be within the image size.")
diff = max_value - image_size
setattr(self.min, axis, getattr(self.min, axis) - diff)
setattr(self.max, axis, image_size)


def crop_image(self, image: sitk.Image) -> sitk.Image:
"""Crop an image to the coordinates defined by the box.

Expand All @@ -316,17 +342,12 @@ def crop_image(self, image: sitk.Image) -> sitk.Image:
The cropped image.
"""
try:
self.check_out_of_bounds_coordinates(image)
cropped_image = sitk.RegionOfInterest(image, self.size, self.min)
except RuntimeError as e:
msg = (
f"The box {self} is outside"
f" the image size {calculate_image_boundaries(image)} "
)
# this is probably due to the box being outside the image
# try to crop the image to the largest possible region
# we could handle this in the future, for now let it raise the error
# and make user try again with an adjusted box
raise BoundingBoxOutsideImageError(msg) from e
except Exception as e:
msg = f"Error cropping image to the box: {e}"
logger.exception(msg)
raise e
else:
return cropped_image

Expand Down Expand Up @@ -369,6 +390,9 @@ def __repr__(self) -> str:
f")"
)

def copy(self) -> RegionBox:
"""Create a copy of the RegionBox."""
return RegionBox(self.min, self.max)

if __name__ == "__main__": # pragma: no cover
from rich import print # noqa
Expand Down
Loading