Image Segmentation
Transformers
PyTorch
repcontur
feature-extraction
medical
radiology
ct
3d
tumor-segmentation
referring-segmentation
report-generation
visual-question-answering
custom_code
Instructions to use Oncosoft-Research/RepConTUR with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Oncosoft-Research/RepConTUR with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-segmentation", model="Oncosoft-Research/RepConTUR", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("Oncosoft-Research/RepConTUR", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Download preprocessing.py from Oncosoft-Research/RepConTUR: direct link, hf CLI and curl.
- Browser
- Download file 12.4 kB
-
https://huggingface.co/Oncosoft-Research/RepConTUR/resolve/main/preprocessing.py
- Command line
-
hf download hf://Oncosoft-Research/RepConTUR/preprocessing.py
-
curl -L -o preprocessing.py https://huggingface.co/Oncosoft-Research/RepConTUR/resolve/main/preprocessing.py
12.4 kB
| """ | |
| MONAI-based 3D preprocessing pipeline for VoxTell grounding volumes. | |
| Pipeline (matches VoxTell's nnUNet-style training preprocessing): | |
| 1. EnsureChannelFirstd — add channel dim | |
| 2. Orientationd(RAS) — standard anatomical orientation | |
| 3. Spacingd((1,1,1)) — isotropic 1 mm voxel spacing | |
| 4. NormalizeIntensityd — z-score normalization (mean/std over all voxels) | |
| 5. SpatialPadd(192) — ensure volume is at least 192^3 | |
| 6. RandCropByPosNegLabeld(192^3, pos=1, neg=1) — foreground-biased crop | |
| Replaces MonaiSAM2Preprocessor for the VoxTell segmentation backbone. | |
| """ | |
| from typing import List, Optional, Tuple | |
| import numpy as np | |
| import torch | |
| from ._compat import print_log | |
| from monai.data import MetaTensor | |
| from monai.transforms import ( | |
| Compose, | |
| EnsureChannelFirstd, | |
| NormalizeIntensityd, | |
| Orientationd, | |
| RandCropByPosNegLabeld, | |
| SpatialPadd, | |
| CenterSpatialCropd, | |
| Spacingd, | |
| Transposed, | |
| SpatialPad | |
| ) | |
| from monai.utils import ensure_tuple | |
| class RandCropByPosNegLabelWithRandomOffsetd(RandCropByPosNegLabeld): | |
| """ | |
| Random Cropping with consistent output spacing and size, using random offset. | |
| Pipeline: Pad -> RandomCrop (with random offset) -> Resize to target_spacing -> CenterCrop to spatial_size | |
| """ | |
| def randomize( | |
| self, | |
| label: torch.Tensor, | |
| fg_indices: np.ndarray | None = None, | |
| bg_indices: np.ndarray | None = None, | |
| image: torch.Tensor | None = None, | |
| ) -> None: | |
| # Call parent's randomize to set self.centers | |
| super().randomize(label, fg_indices, bg_indices, image) | |
| if self.cropper.centers is None: | |
| return | |
| # Vectorized random offset and clamping | |
| centers = np.array(self.cropper.centers) # (num_centers, num_dims) | |
| spatial_size = np.array(ensure_tuple(self.cropper.spatial_size)) | |
| spatial_shape = np.array(label.shape[1:]) | |
| # Generate random offsets for all centers at once: [0, size) - size//2 | |
| offsets = (self.R.rand(*centers.shape) * spatial_size).astype(np.int64) - spatial_size // 2 | |
| # Apply offsets and clamp to valid bounds | |
| new_centers = centers + offsets | |
| min_centers = spatial_size // 2 | |
| max_centers = spatial_shape - spatial_size + spatial_size // 2 | |
| new_centers = np.clip(new_centers, min_centers, max_centers).astype(np.int64) | |
| # Convert back to tuple of tuples (required format) | |
| self.cropper.centers = tuple(map(tuple, new_centers.tolist())) | |
| class SmartRandCropByPosNegLabeld(RandCropByPosNegLabelWithRandomOffsetd): | |
| """ | |
| Random Cropping with consistent output spacing and size and random offset. | |
| Pipeline: Pad -> RandomCrop -> Resize to target_spacing -> CenterCrop to spatial_size | |
| """ | |
| def __init__( | |
| self, | |
| keys, | |
| label_key, | |
| spatial_size, | |
| target_spacing, | |
| pad_mode="constant", | |
| resize_mode=('bilinear', 'nearest'), | |
| **kwargs | |
| ): | |
| """ | |
| Args: | |
| keys: keys of the corresponding items to be transformed. | |
| label_key: key for label used for positive/negative sampling. | |
| spatial_size: final output spatial size after all transforms. | |
| target_spacing: target spacing to resample to (e.g., [1.0, 1.0, 1.0]). | |
| pad_mode: padding mode for SpatialPadd. | |
| resize_mode: interpolation mode for Spacingd ("bilinear", "nearest", etc.). | |
| **kwargs: additional arguments for RandCropByPosNegLabeld. | |
| """ | |
| self.spatial_size = np.array(spatial_size) | |
| self.target_spacing = np.array(target_spacing) | |
| self.pad_mode = pad_mode | |
| # Initialize parent with dummy spatial_size, will be computed per-image | |
| super().__init__(keys=keys, label_key=label_key, spatial_size=(1, 1, 1), **kwargs) | |
| self.spacer = Spacingd( | |
| keys=self.keys, | |
| pixdim=self.target_spacing.tolist(), | |
| mode=resize_mode | |
| ) | |
| self.center_cropper = CenterSpatialCropd(keys=self.keys, roi_size=self.spatial_size.tolist()) | |
| def _get_crop_size(self, label): | |
| """Compute crop size in voxels based on physical size and image spacing.""" | |
| if isinstance(label, MetaTensor): | |
| spacing = torch.norm(label.affine[:3, :3], dim=0) | |
| else: | |
| print_log(f"Image has no affine, using default spacing of 1.0", logger='current') | |
| spacing = self.target_spacing | |
| # Compute crop voxels: physical_extent / current_spacing | |
| # physical_extent = spatial_size[i] * target_spacing[i] | |
| crop_size = [torch.ceil(self.spatial_size[i] * (self.target_spacing[i] / spacing[i])).int().item() for i in range(3)] | |
| crop_size = [s + 5 for s in crop_size] | |
| return tuple(crop_size) | |
| def __call__(self, data, lazy=None): | |
| d = dict(data) | |
| label = d[self.label_key] | |
| # Step 1: Compute crop size based on image spacing | |
| crop_size = self._get_crop_size(label) | |
| self.cropper.spatial_size = crop_size # This is used for random offset sampling | |
| # Step 2: Pad if volume is smaller than crop_size | |
| for key in self.key_iterator(d): | |
| img = d[key] | |
| if self.pad_mode == 'min_value' or self.pad_mode == 'constant': | |
| pad_mode = 'constant' | |
| constant_values = img.min() if self.pad_mode == 'min_value' else 0 | |
| padder = SpatialPad(spatial_size=crop_size, mode=pad_mode, constant_values=constant_values) | |
| else: | |
| pad_mode = self.pad_mode | |
| padder = SpatialPad(spatial_size=crop_size, mode=pad_mode) | |
| d[key] = padder(img) | |
| # Step 3: Random crop (uses parent's __call__) with random offset | |
| cropped_list = super().__call__(d, lazy=lazy) | |
| # From list of dicts to dict of lists | |
| stacked_list = [] | |
| for sample in cropped_list: | |
| # Step 4: Resample to target spacing | |
| sample = self.spacer(sample) | |
| # Step 5: Center crop to exact spatial size | |
| sample = self.center_cropper(sample) | |
| stacked_list.append(sample) | |
| return stacked_list | |
| class VoxTellPreprocessor: | |
| """ | |
| 3D preprocessing for VoxTell grounding input. | |
| Takes a raw CT volume + binary mask (any orientation, any spacing) | |
| and produces one or more 192^3 patches ready for VoxTell's encoder. | |
| In **eval mode** (set via ``set_eval_mode()``), returns the full | |
| preprocessed volume (orient → z-score → resample 1mm → transpose) | |
| without any cropping, for sliding window inference. | |
| Args: | |
| patch_size: Spatial size of each output patch. | |
| pos: Weight for positive (foreground) patches. | |
| neg: Weight for negative (background) patches. | |
| num_samples: Number of 192^3 patches to sample per volume. | |
| """ | |
| def __init__( | |
| self, | |
| patch_size: Tuple[int, int, int] = (192, 192, 192), | |
| pos: float = 1.0, | |
| neg: float = 1.0, | |
| num_samples: int = 1, | |
| ): | |
| self.patch_size = patch_size | |
| self.num_samples = num_samples | |
| self._eval_mode = False | |
| self.transform = Compose([ | |
| # 1. Add channel dimension: (W,H,D) → (1,W,H,D) | |
| EnsureChannelFirstd( | |
| keys=['image', 'label'], | |
| channel_dim='no_channel', | |
| ), | |
| # 2. Reorient to RAS (Right-Anterior-Superior) | |
| Orientationd( | |
| keys=['image', 'label'], | |
| axcodes='RAS', | |
| ), | |
| # 3. Z-score normalization over all voxels | |
| NormalizeIntensityd( | |
| keys=['image'] | |
| ), | |
| # 4. Random crop with foreground bias | |
| SmartRandCropByPosNegLabeld( | |
| keys=['image', 'label'], | |
| label_key='label', | |
| spatial_size=patch_size, | |
| target_spacing=(1.0, 1.0, 1.0), | |
| resize_mode=('bilinear', 'nearest'), | |
| pos=pos, | |
| neg=neg, | |
| num_samples=num_samples, | |
| ), | |
| # 5. Reorient to RAS (Right-Anterior-Superior) | |
| Transposed( | |
| keys=['image', 'label'], | |
| indices=[0, 3, 2, 1] # CWHD to CDHW | |
| ), | |
| ]) | |
| # Eval pipeline: same as training but no cropping. | |
| # orient → normalize → resample 1mm → transpose | |
| self.eval_transform = Compose([ | |
| EnsureChannelFirstd( | |
| keys=['image', 'label'], | |
| channel_dim='no_channel', | |
| ), | |
| Orientationd( | |
| keys=['image', 'label'], | |
| axcodes='RAS', | |
| ), | |
| NormalizeIntensityd( | |
| keys=['image'], | |
| ), | |
| Spacingd( | |
| keys=['image', 'label'], | |
| pixdim=(1.0, 1.0, 1.0), | |
| mode=('bilinear', 'nearest'), | |
| ), | |
| Transposed( | |
| keys=['image', 'label'], | |
| indices=[0, 3, 2, 1], # CWHD to CDHW | |
| ), | |
| ]) | |
| def set_eval_mode(self): | |
| """Switch to eval mode: full-volume preprocessing without cropping.""" | |
| self._eval_mode = True | |
| def apply_image( | |
| self, | |
| volume: np.ndarray, | |
| mask: np.ndarray, | |
| affine: Optional[np.ndarray] = None, | |
| data_source: Optional[str] = None, | |
| ) -> Optional[Tuple[torch.Tensor, torch.Tensor]]: | |
| """ | |
| Preprocess volume + mask and return VoxTell-ready 3D patches. | |
| In eval mode, returns the full preprocessed volume (no cropping) | |
| for sliding window inference. In training mode, returns one or | |
| more foreground-biased 192^3 patches. | |
| Args: | |
| volume: Raw CT volume, any axis order (matching affine). | |
| mask: Binary segmentation mask, same shape as volume. | |
| affine: 4x4 affine matrix (required for orientation/spacing). | |
| data_source: Unused (kept for interface compatibility). | |
| Returns: | |
| Tuple of ``(volume_tensor, mask_tensor)`` where shapes are: | |
| - Training: ``(1, D, H, W)`` with D=H=W=192. | |
| - Eval: ``(1, D', H', W')`` at 1 mm isotropic spacing. | |
| If ``num_samples > 1`` (training only), returns a list of | |
| such tuples. Returns None on failure. | |
| """ | |
| try: | |
| assert affine is not None, 'Affine matrix is required' | |
| img_tensor = MetaTensor( | |
| volume.astype(np.float32), affine=affine, | |
| ) | |
| lbl_tensor = MetaTensor( | |
| mask.astype(np.float32), affine=affine, | |
| ) | |
| data_dict = { | |
| 'image': img_tensor, | |
| 'label': lbl_tensor, | |
| } | |
| if self._eval_mode: | |
| result = self.eval_transform(data_dict) | |
| vol = result['image'].as_tensor() # (1, D', H', W') | |
| msk = result['label'].as_tensor() | |
| return vol, msk | |
| result = self.transform(data_dict) | |
| # RandCropByPosNegLabeld returns a list of dicts | |
| if isinstance(result, list): | |
| if self.num_samples == 1: | |
| r = result[0] | |
| vol_patch = r['image'].as_tensor() # (1, D, H, W) | |
| msk_patch = r['label'].as_tensor() | |
| return vol_patch, msk_patch | |
| else: | |
| patches = [] | |
| for r in result: | |
| vol_patch = r['image'].as_tensor() | |
| msk_patch = r['label'].as_tensor() | |
| patches.append((vol_patch, msk_patch)) | |
| return patches | |
| else: | |
| vol_patch = result['image'].as_tensor() | |
| msk_patch = result['label'].as_tensor() | |
| return vol_patch, msk_patch | |
| except Exception as e: | |
| print_log( | |
| f'VoxTellPreprocessor error: {e}', logger='current', | |
| ) | |
| import traceback | |
| print_log(traceback.format_exc(), logger='current') | |
| return None | |