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
File size: 11,115 Bytes
c6be546 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | """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
# -----------------------------------------------------------------
@torch.inference_mode()
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)
|