Source code for fastrad.voxel_extractor

import torch
import numpy as np
from types import SimpleNamespace
from typing import Dict, Any, Tuple
from fastrad.settings import FeatureSettings
from fastrad.image import MedicalImage, Mask
from fastrad.extractor import FeatureExtractor
from .logger import logger

[docs] class VoxelFeatureExtractor: """ Experimental sliding-window feature extractor for voxel-wise radiomic maps. WARNING: Native 3D patch extraction memory consumption grows factorially. This calculates standard macro radiomics iteratively over spatial bounding boxes. It is recommended solely for small Region of Interests or with powerful GPU limits. """
[docs] def __init__(self, settings: FeatureSettings, kernel_size: int = 3): self.settings = settings self.extractor = FeatureExtractor(settings) # Ensure kernel size is odd for symmetric padding if kernel_size % 2 == 0: raise ValueError("Voxel kernel size must be an odd integer (e.g., 3, 5).") self.kernel_size = kernel_size self.pad = kernel_size // 2
[docs] def extract(self, image: MedicalImage, mask: Mask) -> dict[str, torch.Tensor]: device = self.extractor.device img_tensor = image.tensor.to(device) mask_tensor = mask.tensor.to(device) # We process ONLY the bounding box region to profoundly limit iterations coords = torch.nonzero(mask_tensor > 0.5, as_tuple=False) if coords.numel() == 0: return {} mins = coords.min(dim=0).values maxs = coords.max(dim=0).values z_min, y_min, x_min = mins z_max, y_max, x_max = maxs # Dimensions of the bounding box D, H, W = z_max - z_min + 1, y_max - y_min + 1, x_max - x_min + 1 # Create output maps dictionary output_maps = {} first_pass = True import torch.nn.functional as F padded_img = F.pad(img_tensor, (self.pad, self.pad, self.pad, self.pad, self.pad, self.pad), mode='replicate') padded_mask = F.pad(mask_tensor, (self.pad, self.pad, self.pad, self.pad, self.pad, self.pad), mode='constant', value=0.0) logger.info(f"Initiating VoxelFeatureExtractor for bounding volume: {D}x{H}x{W}") for z in range(z_min, z_max + 1): for y in range(y_min, y_max + 1): for x in range(x_min, x_max + 1): # We only calculate features at centers where the mask is natively present if mask_tensor[z, y, x] <= 0.5: continue # Extract local patch context z_p = z + self.pad y_p = y + self.pad x_p = x + self.pad z_slice = slice(z_p - self.pad, z_p + self.pad + 1) y_slice = slice(y_p - self.pad, y_p + self.pad + 1) x_slice = slice(x_p - self.pad, x_p + self.pad + 1) img_patch = padded_img[z_slice, y_slice, x_slice] # We utilize the full patch as valid space to simulate standard macro conditions mask_patch = torch.ones_like(img_patch) patch_image = MedicalImage(img_patch, image.spacing) patch_mask = Mask(mask_patch, mask.spacing) try: features = self.extractor.extract(patch_image, patch_mask) except Exception as e: # Log but do not interrupt map spatial generation # Empty patches trigger native ValueErrors in PyRadiomics architecture bounds continue if first_pass: for key in features.keys(): output_maps[key] = torch.zeros(img_tensor.shape, dtype=torch.float32, device=device) first_pass = False for key, val in features.items(): output_maps[key][z, y, x] = val return output_maps