Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/nightly-health.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
# Python 3.11 is required because the [physicsnemo] extra pulls in
# nvidia-physicsnemo, which requires Python >= 3.11.
run: |
& "C:\Users\saylward\AppData\Local\Programs\Python\Python311\python.exe" -m venv "$env:RUNNER_TEMP\physiomotion4d-venv"
& py -m venv "$env:RUNNER_TEMP\physiomotion4d-venv"
echo "$env:RUNNER_TEMP\physiomotion4d-venv\Scripts" >> $env:GITHUB_PATH
Comment on lines 58 to 62

- name: Cache uv packages
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ print(WorkflowConvertImageToUSD.__name__)

### Key Dependencies

- **Medical Imaging**: ITK, TubeTK, MONAI, nibabel, PyVista
- **Medical Imaging**: ITK, MONAI, nibabel, PyVista
- **AI/ML**: PyTorch, CuPy (CUDA 13), transformers, MONAI
- **Registration**: icon-registration, unigradicon
- **Visualization**: USD-core, PyVista
Expand Down
1 change: 0 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ def __getattr__(cls, name):


# Mock modules that need special handling
sys.modules["itk.TubeTK"] = Mock()
sys.modules["icon_registration.losses"] = Mock()
sys.modules["icon_registration.network_wrappers"] = Mock()

Expand Down
2 changes: 1 addition & 1 deletion docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Software Dependencies

PhysioMotion4D relies on several key packages:

* **Medical Imaging**: ITK, TubeTK, MONAI, nibabel, PyVista
* **Medical Imaging**: ITK, MONAI, nibabel, PyVista
* **AI/ML**: PyTorch, CuPy (CUDA 13), transformers, MONAI
* **Registration**: icon-registration, unigradicon
* **Visualization**: USD-core, PyVista
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@
import itk
import numpy as np
import pyvista as pv
from itk import TubeTK as ttk

# Import from PhysioMotion4D package
from physiomotion4d import (
ContourTools,
RegisterModelsICPITK,
TransformTools,
)
from physiomotion4d.image_tools import ImageTools
from physiomotion4d.test_tools import TestTools

# %% [markdown]
Expand Down Expand Up @@ -64,10 +64,7 @@

# Resample to 1mm isotropic spacing
print("Resampling to sotropic...")
resampler = ttk.ResampleImage.New(Input=patient_image)
resampler.SetMakeHighResIso(True)
resampler.Update()
patient_image = resampler.GetOutput()
patient_image = ImageTools().make_isotropic_image(patient_image)

print(f" Resampled size: {itk.size(patient_image)}")
print(f" Resampled spacing: {itk.spacing(patient_image)}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import itk
import numpy as np
import pyvista as pv
from itk import TubeTK as ttk

# Import from PhysioMotion4D package
from physiomotion4d import (
Expand All @@ -33,6 +32,7 @@
RegisterModelsPCA,
TransformTools,
)
from physiomotion4d.image_tools import ImageTools
from physiomotion4d.test_tools import TestTools

# %% [markdown]
Expand Down Expand Up @@ -75,10 +75,7 @@

# Resample to 1mm isotropic spacing
print("Resampling to sotropic...")
resampler = ttk.ResampleImage.New(Input=patient_image)
resampler.SetMakeHighResIso(True)
resampler.Update()
patient_image = resampler.GetOutput()
patient_image = ImageTools().make_isotropic_image(patient_image)

print(f" Resampled size: {itk.size(patient_image)}")
print(f" Resampled spacing: {itk.spacing(patient_image)}")
Expand Down
7 changes: 2 additions & 5 deletions experiments/Lung-GatedCT_To_USD/0-register_dirlab_4dct.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import itk
import numpy as np
from data_dirlab_4d_ct import DataDirLab4DCT
from itk import TubeTK as tube

from physiomotion4d.image_tools import ImageTools
from physiomotion4d.register_images_icon import RegisterImagesICON
from physiomotion4d.segment_chest_total_segmentator import SegmentChestTotalSegmentator
from physiomotion4d.transform_tools import TransformTools
Expand All @@ -33,10 +33,7 @@
# %%
def dilate_mask(mask: Optional[itk.image], dilation: int) -> Optional[itk.image]:
if mask is not None:
im_math = tube.ImageMath.New(mask)
im_math.Dilate(dilation, 1, 0)
dilated_mask = im_math.GetOutputShort()
return dilated_mask
return ImageTools().binary_dilate_image(mask, dilation, 1, 0)
return None

def register_image(
Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ keywords = [
dependencies = [
# Core medical imaging
"itk>=5.3.0",
"itk-tubetk>=1.4.0",
"nibabel>=4.0.0",
"numpy>=1.21.0",
"pydicom>=2.4.0",
Expand Down
115 changes: 112 additions & 3 deletions src/physiomotion4d/image_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,6 @@ def convert_array_to_image_of_vectors(
def make_isotropic_image(self, image: itk.Image) -> itk.Image:
"""Resample a 3-D *image* to isotropic spacing using the finest voxel pitch.

Equivalent to TubeTK's ResampleImage.SetMakeHighResIso(True), implemented
with standard ITK ResampleImageFilter so that TubeTK is not needed here.

Args:
image: 3-D ITK image to resample.

Expand Down Expand Up @@ -273,6 +270,118 @@ def make_isotropic_image(self, image: itk.Image) -> itk.Image:
result.DisconnectPipeline()
return result

def binary_dilate_image(
self,
image: itk.Image,
radius: int,
foreground_value: int = 1,
background_value: int = 0,
) -> itk.Image:
"""Binary-dilate *image* with a ball structuring element.

Args:
image: Binary (or label) image to dilate.
radius: Radius, in voxels, of the ball structuring element.
foreground_value: Pixel value treated as foreground (default: 1).
background_value: Pixel value written for background voxels
(default: 0).

Returns:
Dilated image with the same pixel type as *image*.
"""
ImageType = type(image)
dimension = image.GetImageDimension()
StructuringElementType = itk.FlatStructuringElement[dimension]
structuring_element = StructuringElementType.Ball(int(radius))

dilate_filter = itk.BinaryDilateImageFilter[
ImageType, ImageType, StructuringElementType
].New()
dilate_filter.SetInput(image)
dilate_filter.SetKernel(structuring_element)
dilate_filter.SetForegroundValue(foreground_value)
dilate_filter.SetBackgroundValue(background_value)
dilate_filter.Update()
result = dilate_filter.GetOutput()
result.DisconnectPipeline()
return result

def binary_erode_image(
self,
image: itk.Image,
radius: int,
foreground_value: int = 1,
background_value: int = 0,
) -> itk.Image:
"""Binary-erode *image* with a ball structuring element.

Args:
image: Binary (or label) image to erode.
radius: Radius, in voxels, of the ball structuring element.
foreground_value: Pixel value treated as foreground (default: 1).
background_value: Pixel value written for eroded-away voxels
(default: 0).

Returns:
Eroded image with the same pixel type as *image*.
"""
ImageType = type(image)
dimension = image.GetImageDimension()
StructuringElementType = itk.FlatStructuringElement[dimension]
structuring_element = StructuringElementType.Ball(int(radius))

erode_filter = itk.BinaryErodeImageFilter[
ImageType, ImageType, StructuringElementType
].New()
erode_filter.SetInput(image)
erode_filter.SetKernel(structuring_element)
erode_filter.SetForegroundValue(foreground_value)
erode_filter.SetBackgroundValue(background_value)
erode_filter.Update()
result = erode_filter.GetOutput()
result.DisconnectPipeline()
return result

def keep_largest_connected_component(
self,
image: itk.Image,
foreground_value: int = 1,
fully_connected: bool = False,
) -> itk.Image:
"""Keep only the largest connected component of a binary image.

Args:
image: Binary (non-zero = foreground) image.
foreground_value: Value written for the retained component's
voxels (default: 1).
fully_connected: Whether diagonally-adjacent voxels are
considered connected (default: False, i.e. face connectivity
only).

Returns:
Binary image, same pixel type as *image*, containing only the
largest connected component with value *foreground_value*
(background is 0).
"""
ImageType = type(image)

connected_component_image = itk.connected_component_image_filter(
image, fully_connected=fully_connected
)
relabeled_image = itk.relabel_component_image_filter(
connected_component_image, sort_by_object_size=True
)
result = itk.binary_threshold_image_filter(
Input=relabeled_image,
LowerThreshold=1,
UpperThreshold=1,
InsideValue=foreground_value,
OutsideValue=0,
)
if type(result) is not ImageType:
result = itk.cast_image_filter(result, ttype=(type(result), ImageType))
return result

@overload
def flip_image(
self,
Expand Down
3 changes: 1 addition & 2 deletions src/physiomotion4d/register_images_icon.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@

def _load_icon():
"""Lazy-load icon_registration, torch, and unigradicon to avoid
initializing GPU/CUDA resources at import time, which interferes with
TubeTK's memory allocator on Windows."""
initializing GPU/CUDA resources at import time."""
import icon_registration as icon
import icon_registration.itk_wrapper
import torch
Expand Down
13 changes: 8 additions & 5 deletions src/physiomotion4d/segment_anatomy_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@

import itk
import numpy as np
from itk import TubeTK as tube

from .anatomy_taxonomy import AnatomyTaxonomy
from .image_tools import ImageTools
from .physiomotion4d_base import PhysioMotion4DBase


Expand Down Expand Up @@ -427,10 +427,13 @@ def segment_connected_component(
InsideValue=1,
OutsideValue=0,
)
imMath = tube.ImageMath.New(connected_component_image)
imMath.Dilate(hole_fill, 1, 0)
imMath.Erode(hole_fill, 1, 0)
connected_component_image = imMath.GetOutputUChar()
image_tools = ImageTools()
connected_component_image = image_tools.binary_dilate_image(
connected_component_image, hole_fill, 1, 0
)
connected_component_image = image_tools.binary_erode_image(
connected_component_image, hole_fill, 1, 0
)

labelmap_arr = itk.GetArrayFromImage(labelmap_image)
connected_component_arr = itk.GetArrayFromImage(connected_component_image)
Expand Down
13 changes: 8 additions & 5 deletions src/physiomotion4d/segment_heart_simpleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@

import itk
import numpy as np
from itk import TubeTK as tube

from .image_tools import ImageTools
from .segment_anatomy_base import SegmentAnatomyBase


Expand Down Expand Up @@ -304,11 +304,14 @@ def segmentation_method(self, preprocessed_image: itk.image) -> itk.image:
# Dilate the interior regions to simulate 3mm myocardium (heart)
interior_image = itk.GetImageFromArray(interior_array.astype(np.uint8))
interior_image.CopyInformation(preprocessed_image)
imMath = tube.ImageMath.New(interior_image)
imMath = ImageTools()
spacing = interior_image.GetSpacing()
imMath.Dilate(round(7 / spacing[0]), 1, 0)
imMath.Erode(round(4 / spacing[0]), 1, 0)
exterior_image = imMath.GetOutputUChar()
exterior_image = imMath.binary_dilate_image(
interior_image, round(7 / spacing[0]), 1, 0
)
Comment on lines +307 to +311
exterior_image = imMath.binary_erode_image(
exterior_image, round(4 / spacing[0]), 1, 0
)
exterior_array = itk.GetArrayFromImage(exterior_image)
mask_id = 6 # Heart mask id
exterior_array = exterior_array * mask_id
Expand Down
Loading
Loading