Extending the Framework¶
The framework is designed for extensibility at three levels: image formats, algorithms, and datasets.
Adding an Image Format¶
Subclass Image and implement load() and save():
from medical_image.data.image import Image
class NiftiImage(Image):
def load(self):
import nibabel as nib
nii = nib.load(self.file_path)
data = nii.get_fdata().astype("float32")
self.pixel_data = torch.from_numpy(data)
self._width = data.shape[1]
self._height = data.shape[0]
self._post_load() # applies deferred device migration
return self
def save(self):
# Write modified pixel_data back to disk
...
Key points:
Call
self._post_load()at the end ofload()to apply deferred.to()calls.Set
self._widthandself._heightfrom the loaded data.
Adding an Algorithm¶
Subclass Algorithm and implement apply():
from medical_image.algorithms.algorithm import Algorithm
from medical_image.process.filters import Filters
from medical_image.process.threshold import Threshold
class AdaptiveEnhancer(Algorithm):
def __init__(self, sigma=2.0, k=0.3, device=None):
super().__init__(device=device)
self.sigma = sigma
self.k = k
def apply(self, image, output):
# Step 1: enhance
Filters.gaussian_filter(
image, output, sigma=self.sigma, device=self.device
)
# Step 2: threshold
temp = output.clone()
Threshold.sauvola_threshold(
output, temp, window_size=15, k=self.k, device=self.device
)
output.pixel_data = temp.pixel_data
return output
The base class __call__ automatically handles mixed-precision wrapping via self.precision.
Adding a Dataset¶
Subclass BaseDataset and implement two methods:
from medical_image.datasets.base_dataset import BaseDataset
class MyDataset(BaseDataset):
def _build_sample_list(self):
"""Scan root_dir and populate self._samples with metadata."""
for dcm_path in sorted(self.root_dir.glob("**/*.dcm")):
mask_path = dcm_path.with_suffix(".png")
self._samples.append({
"image_path": dcm_path,
"mask_path": mask_path if mask_path.exists() else None,
})
def _load_sample(self, idx):
"""Load a single sample and return the standard dict."""
info = self._samples[idx]
image = DicomImage(str(info["image_path"]))
image.load()
if info["mask_path"]:
mask = PNGImage(str(info["mask_path"]))
mask.load()
mask_tensor = mask.pixel_data
else:
mask_tensor = torch.zeros_like(image.pixel_data)
return {
"image": image.pixel_data.unsqueeze(0),
"mask": mask_tensor.unsqueeze(0),
"metadata": {"path": str(info["image_path"])},
}
The base class provides __len__, __getitem__ (with transforms), and collate_fn.
Adding a Processing Operation¶
Add static methods to existing process classes, or create a new one:
from medical_image.data.image import requires_loaded
class MyOperations:
@staticmethod
@requires_loaded
def custom_filter(image, output, param=1.0, device=None):
from medical_image.utils.device import resolve_device
device = resolve_device(image, explicit=device)
img = image.pixel_data.to(device).float()
# ... your processing logic ...
output.pixel_data = result