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 modeling_repcontur.py from Oncosoft-Research/RepConTUR: direct link, hf CLI and curl.
- Browser
- Download file 11.1 kB
-
https://huggingface.co/Oncosoft-Research/RepConTUR/resolve/main/modeling_repcontur.py
- Command line
-
hf download hf://Oncosoft-Research/RepConTUR/modeling_repcontur.py
-
curl -L -o modeling_repcontur.py https://huggingface.co/Oncosoft-Research/RepConTUR/resolve/main/modeling_repcontur.py
11.1 kB
| """Standalone HuggingFace model for RepConTUR. | |
| Composes an InternVL backbone (hub remote code) whose 2D vision pipeline is | |
| replaced by a 3D CT-ViT encoder + attentional pooler, with a VoxTell 3D | |
| text-prompted grounding decoder. Loadable via | |
| ``AutoModel.from_pretrained(..., trust_remote_code=True)``; all weights ship in | |
| the model's safetensors. | |
| """ | |
| import logging | |
| import os | |
| import types | |
| from typing import List, Optional | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| from transformers import ( | |
| AutoConfig, | |
| AutoModel, | |
| AutoTokenizer, | |
| GenerationConfig, | |
| PreTrainedModel, | |
| ) | |
| from .configuration_repcontur import RepConTURConfig | |
| from .ct_attentional_pooler import CTAttentionalPoolProjector | |
| from .ct_volume_encoder import CTVolumeEncoder | |
| from .voxtell_wrapper import VoxTellGroundingEncoder | |
| from ._utils import get_stop_criteria | |
| # HF's trust_remote_code loader copies only the direct `from .x import y` imports | |
| # of this file into its module cache (one level, this syntax), so name every | |
| # vendored leaf here to force the copy. | |
| from ._compat import print_log # noqa: F401 | |
| from .attention import Attention # noqa: F401 | |
| from .ctvit import CTViT # noqa: F401 | |
| from .nnunet import NNUNet3DEncoder # noqa: F401 | |
| from .transformer import TransformerDecoder # noqa: F401 | |
| from .voxtell_model import VoxTellModel # noqa: F401 | |
| from .preprocessing import VoxTellPreprocessor # noqa: F401 | |
| logger = logging.getLogger("repcontur") | |
| _DTYPE_MAP = { | |
| 'bfloat16': torch.bfloat16, | |
| 'float16': torch.float16, | |
| 'float32': torch.float32, | |
| } | |
| def _ct_extract_feature(self, pixel_values): | |
| """CT replacement for InternVL.extract_feature (no CLS/pixel-shuffle).""" | |
| vit_embeds = self.vision_model(pixel_values) | |
| vit_embeds = self.mlp1(vit_embeds) | |
| return vit_embeds | |
| class _MllmWrapper(nn.Module): | |
| """Holds the InternVL backbone under ``.model`` to mirror checkpoint keys.""" | |
| def __init__(self, model): | |
| super().__init__() | |
| self.model = model | |
| class RepConTURModel(PreTrainedModel): | |
| config_class = RepConTURConfig | |
| _no_split_modules = [] | |
| def __init__(self, config: RepConTURConfig): | |
| super().__init__(config) | |
| self.torch_dtype = _DTYPE_MAP.get(config.torch_dtype, torch.bfloat16) | |
| # ── InternVL backbone (architecture only; weights load from safetensors) | |
| internvl_config = AutoConfig.from_pretrained( | |
| config.internvl_model_path, trust_remote_code=True, | |
| ) | |
| internvl = AutoModel.from_config(internvl_config, trust_remote_code=True) | |
| internvl = internvl.to(self.torch_dtype) | |
| # Resize to the RepConTUR vocab (base + special tokens). mean_resizing | |
| # would call .item() on meta tensors during from_pretrained; the added | |
| # rows are overwritten by the loaded weights, so plain resizing is fine. | |
| internvl.language_model.resize_token_embeddings( | |
| config.vocab_size, mean_resizing=False, | |
| ) | |
| internvl.img_context_token_id = config.img_context_token_id | |
| # ── Replace the 2D vision pipeline with CT equivalents ────────── | |
| del internvl.vision_model | |
| del internvl.mlp1 | |
| ct_encoder_config = { | |
| k: v for k, v in config.ct_encoder.items() | |
| if k not in ('checkpoint_path', 'ct_clip_source_path') | |
| } | |
| internvl.vision_model = CTVolumeEncoder( | |
| checkpoint_path=None, **ct_encoder_config, | |
| ).to(self.torch_dtype) | |
| llm_hidden = internvl.config.llm_config.hidden_size | |
| if config.llm_hidden_size is not None and config.llm_hidden_size != llm_hidden: | |
| raise ValueError( | |
| f'config.llm_hidden_size={config.llm_hidden_size} does not match the ' | |
| f'InternVL backbone hidden size {llm_hidden}') | |
| internvl.mlp1 = CTAttentionalPoolProjector( | |
| context_dim=internvl.vision_model.dim, | |
| output_dim=llm_hidden, | |
| **config.ct_pooler, | |
| ) | |
| internvl.extract_feature = types.MethodType(_ct_extract_feature, internvl) | |
| self.mllm = _MllmWrapper(internvl) | |
| # ── VoxTell 3D grounding encoder ──────────────────────────────── | |
| self.grounding_encoder = VoxTellGroundingEncoder( | |
| arch_kwargs=config.voxtell_arch_kwargs, | |
| kw_requires_import=config.voxtell_kw_requires_import, | |
| decoder_layer=config.voxtell_decoder_layer, | |
| text_embedding_dim=config.voxtell_text_embedding_dim, | |
| num_maskformer_stages=config.voxtell_num_maskformer_stages, | |
| num_heads=config.voxtell_num_heads, | |
| query_dim=config.voxtell_query_dim, | |
| ) | |
| # ── Cross-modal bridge: LLM hidden → VoxTell query_dim ────────── | |
| self.text_hidden_fcs = nn.Sequential( | |
| nn.Linear(llm_hidden, llm_hidden), | |
| nn.ReLU(inplace=True), | |
| nn.Linear(llm_hidden, config.voxtell_query_dim), | |
| nn.Dropout(0.0), | |
| ) | |
| self.seg_token_idx = config.seg_token_idx | |
| self.tokenizer = None | |
| self.gen_config = None | |
| self.stop_criteria = None | |
| self.template = config.template | |
| # ----------------------------------------------------------------- | |
| def preparing_for_generation(self, tokenizer=None, generation_kwargs=None): | |
| """Set up tokenizer, generation config, and stop criteria for inference.""" | |
| if tokenizer is None: | |
| tokenizer = AutoTokenizer.from_pretrained( | |
| self.config._name_or_path, trust_remote_code=True, | |
| ) | |
| self.tokenizer = tokenizer | |
| stop_words = list(self.template.get('STOP_WORDS', [])) | |
| self.stop_criteria = get_stop_criteria(tokenizer=tokenizer, stop_words=stop_words) | |
| default_gen_kwargs = dict( | |
| max_new_tokens=512, | |
| do_sample=False, | |
| eos_token_id=tokenizer.eos_token_id, | |
| pad_token_id=( | |
| tokenizer.pad_token_id | |
| if tokenizer.pad_token_id is not None | |
| else tokenizer.eos_token_id | |
| ), | |
| ) | |
| default_gen_kwargs.update(self.config.generation_kwargs) | |
| if generation_kwargs: | |
| default_gen_kwargs.update(generation_kwargs) | |
| self.gen_config = GenerationConfig(**default_gen_kwargs) | |
| # Cast the whole model (incl. grounding_encoder), matching the source | |
| # eval harness, so VoxTell does not stay fp32 at inference. | |
| self.to(self.torch_dtype) | |
| return self | |
| # ----------------------------------------------------------------- | |
| def predict_volume( | |
| self, | |
| pixel_values: torch.Tensor, | |
| input_ids: list, | |
| g_pixel_values: torch.Tensor = None, | |
| ) -> dict: | |
| """Autoregressive generation + VoxTell sliding-window segmentation. | |
| Args: | |
| pixel_values: (N, 1, D, H, W) CT volume for the vision encoder. | |
| input_ids: prompt token IDs ending at the assistant-turn opener. | |
| g_pixel_values: (1, D', H', W') preprocessed full volume for VoxTell. | |
| Returns: | |
| dict with ``prediction`` (str) and ``prediction_mask`` ((D',H',W') uint8 or None). | |
| """ | |
| assert self.gen_config is not None, "call preparing_for_generation() first" | |
| language_model = self.mllm.model.language_model | |
| input_ids_t = torch.tensor(input_ids, device=pixel_values.device).unsqueeze(0) | |
| attention_mask = torch.ones_like(input_ids_t, dtype=torch.bool) | |
| input_embeds = language_model.get_input_embeddings()(input_ids_t).clone() | |
| pixel_values = pixel_values.to(dtype=self.torch_dtype) | |
| vit_embeds = self.mllm.model.extract_feature(pixel_values) | |
| vit_embeds = vit_embeds.to(input_embeds.dtype) | |
| image_flags = torch.sum(pixel_values, dim=tuple(range(1, pixel_values.ndim))) != 0 | |
| vit_embeds = vit_embeds[image_flags.long() == 1] | |
| batch, seq_len, channels = input_embeds.shape | |
| input_embeds_flat = input_embeds.reshape(batch * seq_len, channels) | |
| img_token_mask = (input_ids_t.reshape(-1) == self.mllm.model.img_context_token_id) | |
| if img_token_mask.sum() > 0 and vit_embeds.numel() > 0: | |
| visual_flat = vit_embeds.reshape(-1, channels) | |
| n_vis = min(img_token_mask.sum().item(), visual_flat.shape[0]) | |
| mask_indices = img_token_mask.nonzero(as_tuple=True)[0] | |
| input_embeds_flat[mask_indices[:n_vis]] = visual_flat[:n_vis] | |
| input_embeds = input_embeds_flat.reshape(batch, seq_len, channels) | |
| generate_output = language_model.generate( | |
| inputs_embeds=input_embeds, | |
| attention_mask=attention_mask, | |
| generation_config=self.gen_config, | |
| stopping_criteria=self.stop_criteria, | |
| bos_token_id=self.tokenizer.bos_token_id, | |
| output_hidden_states=True, | |
| return_dict_in_generate=True, | |
| ) | |
| pred_text = self.tokenizer.decode( | |
| generate_output.sequences[0], skip_special_tokens=False, | |
| ).strip() | |
| hidden_states = generate_output.hidden_states | |
| sequences = generate_output.sequences[0] | |
| prompt_len = input_embeds.shape[1] | |
| gen_start = prompt_len if len(sequences) >= prompt_len + 2 else 0 | |
| generated_ids = sequences[gen_start:] | |
| seg_hidden = self._collect_seg_hidden(hidden_states, generated_ids) | |
| pred_mask = None | |
| if len(seg_hidden) > 0 and g_pixel_values is not None: | |
| if len(seg_hidden) > 1: | |
| logger.warning("predict_volume: %d [SEG] tokens, using first", len(seg_hidden)) | |
| seg_hidden = seg_hidden[:1] | |
| seg_embd = self.text_hidden_fcs(seg_hidden).unsqueeze(1) # (1, 1, 2048) | |
| vol = g_pixel_values.unsqueeze(0).to( | |
| dtype=torch.bfloat16, device=seg_embd.device, | |
| ) | |
| spatial = g_pixel_values.shape[1:] | |
| if any(s > 192 for s in spatial): | |
| logits = self.grounding_encoder.predict_sliding_window(vol, seg_embd, num_objs=1) | |
| else: | |
| logits = self.grounding_encoder.forward_with_language(vol, seg_embd, num_objs=1) | |
| pred_mask = (logits.squeeze(0).float().sigmoid() > 0.5).cpu().numpy().astype(np.uint8) | |
| return {'prediction': pred_text, 'prediction_mask': pred_mask} | |
| def _collect_seg_hidden(self, hidden_states, generated_ids): | |
| """Gather last-layer hidden states at generated [SEG] tokens → (n_seg, H).""" | |
| n_steps = len(hidden_states) | |
| seg_list = [] | |
| for gen_idx, tok_id in enumerate(generated_ids): | |
| if tok_id == self.seg_token_idx: | |
| step = gen_idx + 1 | |
| if step < n_steps: | |
| seg_list.append(hidden_states[step][-1][0]) | |
| ref = hidden_states[0][-1] | |
| if seg_list: | |
| return torch.cat(seg_list, dim=0) | |
| return torch.empty(0, ref.shape[-1], device=ref.device, dtype=ref.dtype) | |