Feature Extraction
Transformers
Safetensors
medical
radiology
vision-language
chest-ct
3d-medical-imaging
glint
custom_code
Instructions to use Deepnoid/GLINT-CT with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Deepnoid/GLINT-CT with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("feature-extraction", model="Deepnoid/GLINT-CT", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("Deepnoid/GLINT-CT", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
File size: 10,269 Bytes
c5a246d db46ad7 c5a246d db46ad7 c5a246d db46ad7 c5a246d db46ad7 c5a246d db46ad7 c5a246d db46ad7 c5a246d db46ad7 c5a246d db46ad7 c5a246d db46ad7 c5a246d db46ad7 c5a246d | 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 | """Inference helpers for the GLINT-CT vision-language model.
CT volumes are preprocessed with the same MONAI pipeline used during GLINT
training/zero-shot evaluation (orientation IPL, resample to 1.5/0.75/0.75 mm,
HU window [-1000, 1000] -> [-1, 1], 240x384x384), then regrouped into
``[num_frames=80, 3, 384, 384]`` and ImageNet-normalised.
"""
import numpy as np
import torch
# GLINT-CT pretraining preprocessing (glint/configs/chest_ct_monai.yaml)
CT_MONAI_CONFIG = {
"orientation": "IPL",
"target_spacing": (1.5, 0.75, 0.75),
"roi_size": (240, 480, 480),
"final_size": (240, 384, 384),
"hu_range": (-1000.0, 1000.0),
"normalize_range": (-1.0, 1.0),
}
def build_ct_transform(orientd=True, raw_spacing=None):
from monai.transforms import (
CenterSpatialCropd,
Compose,
EnsureChannelFirstd,
EnsureTyped,
Lambdad,
LoadImaged,
Orientationd,
Resized,
ScaleIntensityRanged,
Spacingd,
SpatialPadd,
)
cfg = CT_MONAI_CONFIG
b_min, b_max = cfg["normalize_range"]
def _patch_affine(img):
if raw_spacing is not None and hasattr(img, "affine"):
sx, sy, sz = (float(x) for x in raw_spacing)
a = torch.eye(4, dtype=img.affine.dtype, device=img.affine.device)
a[0, 0], a[1, 1], a[2, 2] = sx, sy, sz
img.affine = a
return img
steps = [
LoadImaged(keys=["image"], image_only=False),
EnsureChannelFirstd(keys=["image"]),
*([Orientationd(keys=["image"], axcodes=cfg["orientation"])] if orientd else []),
*([Lambdad(keys=["image"], func=_patch_affine)] if raw_spacing is not None else []),
ScaleIntensityRanged(
keys=["image"],
a_min=cfg["hu_range"][0],
a_max=cfg["hu_range"][1],
b_min=b_min,
b_max=b_max,
clip=True,
),
Spacingd(keys=["image"], pixdim=cfg["target_spacing"], mode="bilinear"),
CenterSpatialCropd(keys=["image"], roi_size=cfg["roi_size"]),
SpatialPadd(keys=["image"], spatial_size=cfg["roi_size"], mode="constant", constant_values=b_min),
Resized(keys=["image"], spatial_size=cfg["final_size"], mode="trilinear"),
EnsureTyped(keys=["image"], dtype=torch.float32),
]
return Compose(steps)
def normalize_pixel_values_videos(pixel_values_videos):
"""(D, H, W) in [-1, 1] -> (num_frames, 3, H, W) ImageNet-normalised."""
s = pixel_values_videos.shape
num_channels = 3
pixel_values_videos = pixel_values_videos.reshape(-1, num_channels, s[-2], s[-1])
pixel_values_videos = (pixel_values_videos + 1) / 2
image_mean = torch.tensor([0.485, 0.456, 0.406], device=pixel_values_videos.device).view(1, 3, 1, 1)
image_std = torch.tensor([0.229, 0.224, 0.225], device=pixel_values_videos.device).view(1, 3, 1, 1)
pixel_values_videos = (pixel_values_videos - image_mean) / image_std
return pixel_values_videos
def _input_geometry(image, orientd, raw_spacing):
"""Input voxel grid (shape, affine) and its 4x4 voxel mapping onto the preprocessed grid."""
import nibabel as nib
affine = np.asarray(image.meta["original_affine"], dtype=np.float64)
shape = tuple(int(s) for s in image.meta["spatial_shape"])
ref_affine = affine
if raw_spacing is not None: # the pipeline replaced the affine with diag(raw_spacing) after reorientation
to_oriented = np.eye(4)
if orientd:
ornt = nib.orientations.ornt_transform(
nib.orientations.io_orientation(affine),
nib.orientations.axcodes2ornt(CT_MONAI_CONFIG["orientation"]),
)
to_oriented = np.linalg.inv(nib.orientations.inv_ornt_aff(ornt, shape))
ref_affine = np.diag([*map(float, raw_spacing), 1.0]) @ to_oriented
to_model = np.linalg.inv(np.asarray(image.affine, dtype=np.float64)) @ ref_affine
return {"shape": shape, "affine": affine, "to_model": to_model}
def load_ct_volume(volume, orientd=True, raw_spacing=None, return_geometry=False):
"""Load and preprocess a CT volume.
volume : path to a NIfTI (.nii/.nii.gz) file or a DICOM series directory, or a (240, 384, 384)
tensor/ndarray in HU that is already on the preprocessed grid (IPL, 1.5/0.75/0.75 mm
resampled, cropped/padded, resized); only the HU window is applied to arrays. Returns
pixel_values of shape (1, num_frames, 3, H, W), plus the input grid if ``return_geometry``
(``shape``, ``affine`` (None for array input) and ``to_model``, its voxel mapping onto the
preprocessed grid), which ``similarity_scores_to_volume`` maps the scores back onto.
"""
if isinstance(volume, str):
transform = build_ct_transform(orientd=orientd, raw_spacing=raw_spacing)
out = transform({"image": volume})
image = out["image"].squeeze(0) # (1, D, H, W) -> (D, H, W)
geometry = _input_geometry(out["image"], orientd, raw_spacing)
else:
image = torch.as_tensor(np.asarray(volume), dtype=torch.float32)
if tuple(image.shape) != tuple(CT_MONAI_CONFIG["final_size"]):
raise ValueError(
f"Array input must be on the preprocessed grid {CT_MONAI_CONFIG['final_size']}, got {tuple(image.shape)}"
)
(a_min, a_max), (b_min, b_max) = CT_MONAI_CONFIG["hu_range"], CT_MONAI_CONFIG["normalize_range"]
image = (image.clamp(a_min, a_max) - a_min) / (a_max - a_min) * (b_max - b_min) + b_min
geometry = {"shape": tuple(image.shape), "affine": None, "to_model": np.eye(4)}
pixel_values_videos = normalize_pixel_values_videos(image).unsqueeze(0) # (1, num_frames, 3, H, W)
return (pixel_values_videos, geometry) if return_geometry else pixel_values_videos
def _resample_to_input(volume, to_model, shape, chunk=32):
"""Trilinearly sample a preprocessed-grid volume at every input voxel; 0 outside the preprocessing crop."""
if np.allclose(to_model, np.eye(4)) and tuple(shape) == tuple(volume.shape):
return volume
device = volume.device
M = torch.as_tensor(to_model, dtype=torch.float32, device=device)
size = torch.tensor(volume.shape, dtype=torch.float32, device=device)
ni, nj, nk = shape
jk = torch.stack(torch.meshgrid(torch.arange(nj), torch.arange(nk), indexing="ij"), -1).reshape(-1, 2).to(device)
out = torch.empty(tuple(shape), dtype=torch.float32, device=device)
for i0 in range(0, ni, chunk):
ii = torch.arange(i0, min(i0 + chunk, ni), device=device)
ijk = torch.cat([ii.repeat_interleave(len(jk))[:, None], jk.repeat(len(ii), 1)], 1).float()
src = ijk @ M[:3, :3].T + M[:3, 3] # input voxel -> preprocessed voxel
grid = (2 * src / (size - 1) - 1).flip(-1) # (d, h, w) -> grid_sample's (x, y, z)
out[i0 : i0 + len(ii)] = torch.nn.functional.grid_sample(
volume[None, None], grid.view(1, -1, 1, 1, 3), mode="bilinear", padding_mode="zeros", align_corners=True
).view(len(ii), nj, nk)
return out
def similarity_scores_to_volume(similarity_scores, model, geometry=None):
"""Per-token scores ``[1, 1, num_tokens]`` -> similarity map on the input CT grid.
The token grid is upsampled (trilinear) to the 240x384x384 preprocessed grid and, with ``geometry`` from
``load_ct_volume``, resampled onto the input voxels so the map has the input's shape and affine (0 outside
the 360 mm preprocessing crop). Without ``geometry`` the map stays on the preprocessed grid.
"""
vc = model.config.vision_config
patch_size = getattr(vc, "patch_size", None) or 16
frame_length = vc.num_channels * (getattr(vc, "tubelet_size", None) or 2)
grid = (vc.input_slices // frame_length, vc.img_size // patch_size, vc.img_size // patch_size)
similarity_map = similarity_scores.float().reshape(grid)
similarity_map = torch.nn.functional.interpolate(
similarity_map[None, None], size=CT_MONAI_CONFIG["final_size"], mode="trilinear", align_corners=False
)[0, 0]
if geometry is None:
return similarity_map
return _resample_to_input(similarity_map, geometry["to_model"], geometry["shape"])
def save_similarity_map(similarity_map, affine, save_path):
"""Save the similarity map as NIfTI; with ``geometry["affine"]`` it overlays the input CT voxel for voxel."""
import nibabel as nib
sim = similarity_map.detach().float().cpu().numpy() if torch.is_tensor(similarity_map) else similarity_map
nib.save(nib.Nifti1Image(sim.astype(np.float32), affine), save_path)
def _as_pixel_values(volume, raw_spacing):
if torch.is_tensor(volume) and volume.dim() == 5: # already preprocessed by load_ct_volume
return volume
return load_ct_volume(volume, raw_spacing=raw_spacing)
@torch.no_grad()
def model_inference(volume, text, model, tokenizer, raw_spacing=None):
"""Return (similarity_prob, similarity_scores) for one CT volume and one phrase.
similarity_prob : scalar sigmoid probability that the phrase applies.
similarity_scores : per-token similarity in [0, 1] over the V-JEPA video tokens
(shape [1, 1, num_tokens]); ``similarity_scores_to_volume``
maps it onto the input CT grid.
volume may also be the ``pixel_values`` returned by ``load_ct_volume``.
"""
pixel_values = _as_pixel_values(volume, raw_spacing).to(model.device, dtype=model.dtype)
tokenized_text = tokenizer(
text,
padding=True,
truncation=True,
return_tensors="pt",
).to(model.device)
output = model.compute_logits(pixel_values, [tokenized_text])
similarity_prob = output["logits"].sigmoid()
return similarity_prob, output.get("similarity_scores")
@torch.no_grad()
def model_inference_multiple_text(volume, text_list, model, tokenizer, raw_spacing=None):
pixel_values = _as_pixel_values(volume, raw_spacing).to(model.device, dtype=model.dtype)
enc = tokenizer(text_list, padding=True, truncation=True, return_tensors="pt").to(model.device)
output = model.compute_logits(pixel_values, [enc])
return output["logits"].sigmoid(), output.get("similarity_scores")
|