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
Resample similarity maps onto the input CT grid
Browse files- similarity_scores_to_volume(scores, model, geometry) returns the map with the input CT's shape and affine (geometry from load_ct_volume(..., return_geometry=True)); without geometry it stays on the 240x384x384 preprocessed grid
- load_ct_volume: return_affine -> return_geometry; accepts a NIfTI file or a DICOM series directory
- inference.py: saves only similarity_map.nii.gz (no --ct_output); drops the 18 CT-RATE finding scores
- README: new API, geometry, raw_spacing axis order (I, P, L), pydicom for DICOM input
- README.md +17 -11
- inference.py +12 -38
- utils.py +69 -33
README.md
CHANGED
|
@@ -43,11 +43,11 @@ Both examples use the bundled `inference.py` / `utils.py`; run them from the dow
|
|
| 43 |
```bash
|
| 44 |
hf download Deepnoid/GLINT-CT --local-dir glint-ct && cd glint-ct
|
| 45 |
python inference.py --volume volume.nii.gz --text "There is a lung nodule."
|
| 46 |
-
# prints the sentence probability and
|
| 47 |
-
# saves similarity_map.nii.gz and ct_preprocessed.nii.gz
|
| 48 |
```
|
| 49 |
|
| 50 |
-
|
|
|
|
| 51 |
|
| 52 |
### AutoModel
|
| 53 |
|
|
@@ -62,17 +62,23 @@ device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
| 62 |
model = AutoModel.from_pretrained(repo, trust_remote_code=True, dtype=torch.float32).to(device).eval()
|
| 63 |
tokenizer = AutoTokenizer.from_pretrained(repo)
|
| 64 |
|
| 65 |
-
pixel_values,
|
| 66 |
prob, scores = model_inference(pixel_values, "There is a lung nodule.", model=model, tokenizer=tokenizer)
|
| 67 |
-
|
| 68 |
-
save_similarity_map(
|
| 69 |
```
|
| 70 |
|
| 71 |
- **Outputs:** `prob` is the sentence probability (sigmoid); `scores` is the per-token similarity in [0, 1]
|
| 72 |
-
(40 × 24 × 24 tokens)
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
- **Underlying call:** `model.compute_logits(pixel_values, [encoded_text])` returns `logits` and `similarity_scores`.
|
| 77 |
|
| 78 |
Vision encoder only (`vision_only=True`):
|
|
@@ -93,7 +99,7 @@ print(out.last_hidden_state.shape) # (1, 23040, 1024)
|
|
| 93 |
|
| 94 |
## Requirements
|
| 95 |
|
| 96 |
-
`transformers==4.56.1`, `torch`, `entmax`, `monai`, `nibabel`.
|
| 97 |
|
| 98 |
> ⚠️ Keep `transformers==4.56.1`, the version the bundled code was validated with.
|
| 99 |
|
|
|
|
| 43 |
```bash
|
| 44 |
hf download Deepnoid/GLINT-CT --local-dir glint-ct && cd glint-ct
|
| 45 |
python inference.py --volume volume.nii.gz --text "There is a lung nodule."
|
| 46 |
+
# prints the sentence probability and saves similarity_map.nii.gz
|
|
|
|
| 47 |
```
|
| 48 |
|
| 49 |
+
`--volume` takes a NIfTI file or a DICOM series directory. The map has the input CT's shape and affine, so it
|
| 50 |
+
overlays the input in ITK-SNAP / 3D Slicer.
|
| 51 |
|
| 52 |
### AutoModel
|
| 53 |
|
|
|
|
| 62 |
model = AutoModel.from_pretrained(repo, trust_remote_code=True, dtype=torch.float32).to(device).eval()
|
| 63 |
tokenizer = AutoTokenizer.from_pretrained(repo)
|
| 64 |
|
| 65 |
+
pixel_values, geometry = load_ct_volume("volume.nii.gz", return_geometry=True) # [1, 80, 3, 384, 384]
|
| 66 |
prob, scores = model_inference(pixel_values, "There is a lung nodule.", model=model, tokenizer=tokenizer)
|
| 67 |
+
similarity_map = similarity_scores_to_volume(scores, model, geometry) # input CT shape, e.g. (512, 512, 205)
|
| 68 |
+
save_similarity_map(similarity_map, geometry["affine"], "similarity_map.nii.gz")
|
| 69 |
```
|
| 70 |
|
| 71 |
- **Outputs:** `prob` is the sentence probability (sigmoid); `scores` is the per-token similarity in [0, 1]
|
| 72 |
+
(40 × 24 × 24 tokens); `similarity_scores_to_volume` resamples it onto the input CT grid (0 outside the 360 mm
|
| 73 |
+
preprocessing crop), or keeps the 240×384×384 preprocessed grid without `geometry`.
|
| 74 |
+
- **`geometry`:** returned by `load_ct_volume(..., return_geometry=True)`: `shape` and `affine` of the input CT (the
|
| 75 |
+
map's NIfTI header) and `to_model`, the 4×4 input-voxel → preprocessed-voxel mapping used for the resampling; for
|
| 76 |
+
array input `affine` is `None` and the map stays on the preprocessed grid.
|
| 77 |
+
- **Inputs:** a NIfTI file or a DICOM series directory (one series); `utils.load_ct_volume` applies the training
|
| 78 |
+
preprocessing (IPL, HU [-1000, 1000] → [-1, 1], 1.5 × 0.75 × 0.75 mm, crop/pad to 240×480×480, resize to
|
| 79 |
+
240×384×384) and returns `[1, 80, 3, 384, 384]`.
|
| 80 |
+
- **Unreliable affine:** pass the voxel spacing along the reoriented I, P, L axes, e.g. `raw_spacing=(1.5, 0.7, 0.7)`,
|
| 81 |
+
to `load_ct_volume` (`--raw_spacing` in the script).
|
| 82 |
- **Underlying call:** `model.compute_logits(pixel_values, [encoded_text])` returns `logits` and `similarity_scores`.
|
| 83 |
|
| 84 |
Vision encoder only (`vision_only=True`):
|
|
|
|
| 99 |
|
| 100 |
## Requirements
|
| 101 |
|
| 102 |
+
`transformers==4.56.1`, `torch`, `entmax`, `monai`, `nibabel`; `pydicom` for DICOM series input.
|
| 103 |
|
| 104 |
> ⚠️ Keep `transformers==4.56.1`, the version the bundled code was validated with.
|
| 105 |
|
inference.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""Score a sentence against a chest CT volume
|
| 2 |
|
| 3 |
python inference.py --volume volume.nii.gz --text "There is a lung nodule."
|
| 4 |
"""
|
|
@@ -13,7 +13,6 @@ from transformers import AutoModel, AutoTokenizer
|
|
| 13 |
from utils import (
|
| 14 |
load_ct_volume,
|
| 15 |
model_inference,
|
| 16 |
-
model_inference_multiple_text,
|
| 17 |
save_similarity_map,
|
| 18 |
similarity_scores_to_volume,
|
| 19 |
)
|
|
@@ -22,27 +21,6 @@ warnings.filterwarnings("ignore", category=UserWarning)
|
|
| 22 |
|
| 23 |
REPO_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 24 |
|
| 25 |
-
CT_RATE_LABELS = [
|
| 26 |
-
"Medical material",
|
| 27 |
-
"Arterial wall calcification",
|
| 28 |
-
"Cardiomegaly",
|
| 29 |
-
"Pericardial effusion",
|
| 30 |
-
"Coronary artery wall calcification",
|
| 31 |
-
"Hiatal hernia",
|
| 32 |
-
"Lymphadenopathy",
|
| 33 |
-
"Emphysema",
|
| 34 |
-
"Atelectasis",
|
| 35 |
-
"Lung nodule",
|
| 36 |
-
"Lung opacity",
|
| 37 |
-
"Pulmonary fibrotic sequela",
|
| 38 |
-
"Pleural effusion",
|
| 39 |
-
"Mosaic attenuation pattern",
|
| 40 |
-
"Peribronchial thickening",
|
| 41 |
-
"Consolidation",
|
| 42 |
-
"Bronchiectasis",
|
| 43 |
-
"Interlobular septal thickening",
|
| 44 |
-
]
|
| 45 |
-
|
| 46 |
|
| 47 |
def load_model(device=None, dtype=torch.float32):
|
| 48 |
device = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
|
@@ -53,30 +31,26 @@ def load_model(device=None, dtype=torch.float32):
|
|
| 53 |
|
| 54 |
def main():
|
| 55 |
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
| 56 |
-
parser.add_argument("--volume", default="volume.nii.gz", help="NIfTI CT volume")
|
| 57 |
parser.add_argument("--text", default="There is a lung nodule.")
|
| 58 |
-
parser.add_argument("--output", default="similarity_map.nii.gz", help="similarity map (NIfTI)")
|
| 59 |
-
parser.add_argument(
|
| 60 |
-
"--ct_output", default="ct_preprocessed.nii.gz", help="preprocessed CT on the same grid (NIfTI)"
|
| 61 |
-
)
|
| 62 |
parser.add_argument(
|
| 63 |
-
"--raw_spacing",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
)
|
| 65 |
args = parser.parse_args()
|
| 66 |
|
| 67 |
models = load_model()
|
| 68 |
-
pixel_values,
|
| 69 |
|
| 70 |
prob, scores = model_inference(pixel_values, args.text, **models)
|
| 71 |
print(f"{args.text!r}: probability {prob.item():.3f}")
|
| 72 |
-
|
| 73 |
-
save_similarity_map(
|
| 74 |
-
print(f"similarity map saved to {args.output}
|
| 75 |
-
|
| 76 |
-
prompts = [f"There is {label}" for label in CT_RATE_LABELS] # zero-shot evaluation prompts
|
| 77 |
-
probs, _ = model_inference_multiple_text(pixel_values, prompts, **models)
|
| 78 |
-
for label, p in zip(CT_RATE_LABELS, probs.flatten().tolist()):
|
| 79 |
-
print(f" {label:42s} {p:.3f}")
|
| 80 |
|
| 81 |
|
| 82 |
if __name__ == "__main__":
|
|
|
|
| 1 |
+
"""Score a sentence against a chest CT volume and save its similarity map as NIfTI.
|
| 2 |
|
| 3 |
python inference.py --volume volume.nii.gz --text "There is a lung nodule."
|
| 4 |
"""
|
|
|
|
| 13 |
from utils import (
|
| 14 |
load_ct_volume,
|
| 15 |
model_inference,
|
|
|
|
| 16 |
save_similarity_map,
|
| 17 |
similarity_scores_to_volume,
|
| 18 |
)
|
|
|
|
| 21 |
|
| 22 |
REPO_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
def load_model(device=None, dtype=torch.float32):
|
| 26 |
device = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
|
|
|
| 31 |
|
| 32 |
def main():
|
| 33 |
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
| 34 |
+
parser.add_argument("--volume", default="volume.nii.gz", help="NIfTI CT volume or DICOM series directory")
|
| 35 |
parser.add_argument("--text", default="There is a lung nodule.")
|
| 36 |
+
parser.add_argument("--output", default="similarity_map.nii.gz", help="similarity map on the input CT grid (NIfTI)")
|
|
|
|
|
|
|
|
|
|
| 37 |
parser.add_argument(
|
| 38 |
+
"--raw_spacing",
|
| 39 |
+
type=float,
|
| 40 |
+
nargs=3,
|
| 41 |
+
default=None,
|
| 42 |
+
help="voxel spacing along the reoriented I, P, L axes (e.g. 1.5 0.7 0.7) if the input affine is unreliable",
|
| 43 |
)
|
| 44 |
args = parser.parse_args()
|
| 45 |
|
| 46 |
models = load_model()
|
| 47 |
+
pixel_values, geometry = load_ct_volume(args.volume, raw_spacing=args.raw_spacing, return_geometry=True)
|
| 48 |
|
| 49 |
prob, scores = model_inference(pixel_values, args.text, **models)
|
| 50 |
print(f"{args.text!r}: probability {prob.item():.3f}")
|
| 51 |
+
similarity_map = similarity_scores_to_volume(scores, models["model"], geometry)
|
| 52 |
+
save_similarity_map(similarity_map, geometry["affine"], args.output)
|
| 53 |
+
print(f"similarity map {tuple(similarity_map.shape)} saved to {args.output}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
|
| 56 |
if __name__ == "__main__":
|
utils.py
CHANGED
|
@@ -83,21 +83,41 @@ def normalize_pixel_values_videos(pixel_values_videos):
|
|
| 83 |
return pixel_values_videos
|
| 84 |
|
| 85 |
|
| 86 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
"""Load and preprocess a CT volume.
|
| 88 |
|
| 89 |
-
volume : path to a NIfTI (.nii/.nii.gz) file, or a (240, 384, 384)
|
| 90 |
-
already on the preprocessed grid (IPL, 1.5/0.75/0.75 mm
|
| 91 |
-
only the HU window is applied to arrays. Returns
|
| 92 |
-
(1, num_frames, 3, H, W), plus the
|
| 93 |
-
``
|
|
|
|
| 94 |
"""
|
| 95 |
-
affine = None
|
| 96 |
if isinstance(volume, str):
|
| 97 |
transform = build_ct_transform(orientd=orientd, raw_spacing=raw_spacing)
|
| 98 |
out = transform({"image": volume})
|
| 99 |
image = out["image"].squeeze(0) # (1, D, H, W) -> (D, H, W)
|
| 100 |
-
|
| 101 |
else:
|
| 102 |
image = torch.as_tensor(np.asarray(volume), dtype=torch.float32)
|
| 103 |
if tuple(image.shape) != tuple(CT_MONAI_CONFIG["final_size"]):
|
|
@@ -106,45 +126,61 @@ def load_ct_volume(volume, orientd=True, raw_spacing=None, return_affine=False):
|
|
| 106 |
)
|
| 107 |
(a_min, a_max), (b_min, b_max) = CT_MONAI_CONFIG["hu_range"], CT_MONAI_CONFIG["normalize_range"]
|
| 108 |
image = (image.clamp(a_min, a_max) - a_min) / (a_max - a_min) * (b_max - b_min) + b_min
|
|
|
|
| 109 |
|
| 110 |
pixel_values_videos = normalize_pixel_values_videos(image).unsqueeze(0) # (1, num_frames, 3, H, W)
|
| 111 |
-
return (pixel_values_videos,
|
| 112 |
|
| 113 |
|
| 114 |
-
def
|
| 115 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
vc = model.config.vision_config
|
| 117 |
patch_size = getattr(vc, "patch_size", None) or 16
|
| 118 |
frame_length = vc.num_channels * (getattr(vc, "tubelet_size", None) or 2)
|
| 119 |
grid = (vc.input_slices // frame_length, vc.img_size // patch_size, vc.img_size // patch_size)
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
|
| 129 |
-
def save_similarity_map(similarity_volume, affine, save_path, pixel_values=None, ct_save_path=None):
|
| 130 |
-
"""Save the similarity map (and optionally the preprocessed CT) as NIfTI in world space.
|
| 131 |
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
preprocessed volume (HU window [-1000, 1000]) on the same grid.
|
| 135 |
-
"""
|
| 136 |
import nibabel as nib
|
| 137 |
|
| 138 |
-
sim =
|
| 139 |
nib.save(nib.Nifti1Image(sim.astype(np.float32), affine), save_path)
|
| 140 |
|
| 141 |
-
if pixel_values is not None and ct_save_path is not None:
|
| 142 |
-
mean = torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1)
|
| 143 |
-
std = torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1)
|
| 144 |
-
frames = pixel_values[0].detach().float().cpu() * std + mean # (num_frames, 3, H, W) in [0, 1]
|
| 145 |
-
hu = (frames.reshape(-1, *frames.shape[-2:]) * 2 - 1) * 1000.0
|
| 146 |
-
nib.save(nib.Nifti1Image(hu.numpy().astype(np.float32), affine), ct_save_path)
|
| 147 |
-
|
| 148 |
|
| 149 |
def _as_pixel_values(volume, raw_spacing):
|
| 150 |
if torch.is_tensor(volume) and volume.dim() == 5: # already preprocessed by load_ct_volume
|
|
@@ -159,7 +195,7 @@ def model_inference(volume, text, model, tokenizer, raw_spacing=None):
|
|
| 159 |
similarity_prob : scalar sigmoid probability that the phrase applies.
|
| 160 |
similarity_scores : per-token similarity in [0, 1] over the V-JEPA video tokens
|
| 161 |
(shape [1, 1, num_tokens]); ``similarity_scores_to_volume``
|
| 162 |
-
maps it onto the
|
| 163 |
|
| 164 |
volume may also be the ``pixel_values`` returned by ``load_ct_volume``.
|
| 165 |
"""
|
|
|
|
| 83 |
return pixel_values_videos
|
| 84 |
|
| 85 |
|
| 86 |
+
def _input_geometry(image, orientd, raw_spacing):
|
| 87 |
+
"""Input voxel grid (shape, affine) and its 4x4 voxel mapping onto the preprocessed grid."""
|
| 88 |
+
import nibabel as nib
|
| 89 |
+
|
| 90 |
+
affine = np.asarray(image.meta["original_affine"], dtype=np.float64)
|
| 91 |
+
shape = tuple(int(s) for s in image.meta["spatial_shape"])
|
| 92 |
+
ref_affine = affine
|
| 93 |
+
if raw_spacing is not None: # the pipeline replaced the affine with diag(raw_spacing) after reorientation
|
| 94 |
+
to_oriented = np.eye(4)
|
| 95 |
+
if orientd:
|
| 96 |
+
ornt = nib.orientations.ornt_transform(
|
| 97 |
+
nib.orientations.io_orientation(affine),
|
| 98 |
+
nib.orientations.axcodes2ornt(CT_MONAI_CONFIG["orientation"]),
|
| 99 |
+
)
|
| 100 |
+
to_oriented = np.linalg.inv(nib.orientations.inv_ornt_aff(ornt, shape))
|
| 101 |
+
ref_affine = np.diag([*map(float, raw_spacing), 1.0]) @ to_oriented
|
| 102 |
+
to_model = np.linalg.inv(np.asarray(image.affine, dtype=np.float64)) @ ref_affine
|
| 103 |
+
return {"shape": shape, "affine": affine, "to_model": to_model}
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def load_ct_volume(volume, orientd=True, raw_spacing=None, return_geometry=False):
|
| 107 |
"""Load and preprocess a CT volume.
|
| 108 |
|
| 109 |
+
volume : path to a NIfTI (.nii/.nii.gz) file or a DICOM series directory, or a (240, 384, 384)
|
| 110 |
+
tensor/ndarray in HU that is already on the preprocessed grid (IPL, 1.5/0.75/0.75 mm
|
| 111 |
+
resampled, cropped/padded, resized); only the HU window is applied to arrays. Returns
|
| 112 |
+
pixel_values of shape (1, num_frames, 3, H, W), plus the input grid if ``return_geometry``
|
| 113 |
+
(``shape``, ``affine`` (None for array input) and ``to_model``, its voxel mapping onto the
|
| 114 |
+
preprocessed grid), which ``similarity_scores_to_volume`` maps the scores back onto.
|
| 115 |
"""
|
|
|
|
| 116 |
if isinstance(volume, str):
|
| 117 |
transform = build_ct_transform(orientd=orientd, raw_spacing=raw_spacing)
|
| 118 |
out = transform({"image": volume})
|
| 119 |
image = out["image"].squeeze(0) # (1, D, H, W) -> (D, H, W)
|
| 120 |
+
geometry = _input_geometry(out["image"], orientd, raw_spacing)
|
| 121 |
else:
|
| 122 |
image = torch.as_tensor(np.asarray(volume), dtype=torch.float32)
|
| 123 |
if tuple(image.shape) != tuple(CT_MONAI_CONFIG["final_size"]):
|
|
|
|
| 126 |
)
|
| 127 |
(a_min, a_max), (b_min, b_max) = CT_MONAI_CONFIG["hu_range"], CT_MONAI_CONFIG["normalize_range"]
|
| 128 |
image = (image.clamp(a_min, a_max) - a_min) / (a_max - a_min) * (b_max - b_min) + b_min
|
| 129 |
+
geometry = {"shape": tuple(image.shape), "affine": None, "to_model": np.eye(4)}
|
| 130 |
|
| 131 |
pixel_values_videos = normalize_pixel_values_videos(image).unsqueeze(0) # (1, num_frames, 3, H, W)
|
| 132 |
+
return (pixel_values_videos, geometry) if return_geometry else pixel_values_videos
|
| 133 |
|
| 134 |
|
| 135 |
+
def _resample_to_input(volume, to_model, shape, chunk=32):
|
| 136 |
+
"""Trilinearly sample a preprocessed-grid volume at every input voxel; 0 outside the preprocessing crop."""
|
| 137 |
+
if np.allclose(to_model, np.eye(4)) and tuple(shape) == tuple(volume.shape):
|
| 138 |
+
return volume
|
| 139 |
+
device = volume.device
|
| 140 |
+
M = torch.as_tensor(to_model, dtype=torch.float32, device=device)
|
| 141 |
+
size = torch.tensor(volume.shape, dtype=torch.float32, device=device)
|
| 142 |
+
ni, nj, nk = shape
|
| 143 |
+
jk = torch.stack(torch.meshgrid(torch.arange(nj), torch.arange(nk), indexing="ij"), -1).reshape(-1, 2).to(device)
|
| 144 |
+
out = torch.empty(tuple(shape), dtype=torch.float32, device=device)
|
| 145 |
+
for i0 in range(0, ni, chunk):
|
| 146 |
+
ii = torch.arange(i0, min(i0 + chunk, ni), device=device)
|
| 147 |
+
ijk = torch.cat([ii.repeat_interleave(len(jk))[:, None], jk.repeat(len(ii), 1)], 1).float()
|
| 148 |
+
src = ijk @ M[:3, :3].T + M[:3, 3] # input voxel -> preprocessed voxel
|
| 149 |
+
grid = (2 * src / (size - 1) - 1).flip(-1) # (d, h, w) -> grid_sample's (x, y, z)
|
| 150 |
+
out[i0 : i0 + len(ii)] = torch.nn.functional.grid_sample(
|
| 151 |
+
volume[None, None], grid.view(1, -1, 1, 1, 3), mode="bilinear", padding_mode="zeros", align_corners=True
|
| 152 |
+
).view(len(ii), nj, nk)
|
| 153 |
+
return out
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def similarity_scores_to_volume(similarity_scores, model, geometry=None):
|
| 157 |
+
"""Per-token scores ``[1, 1, num_tokens]`` -> similarity map on the input CT grid.
|
| 158 |
+
|
| 159 |
+
The token grid is upsampled (trilinear) to the 240x384x384 preprocessed grid and, with ``geometry`` from
|
| 160 |
+
``load_ct_volume``, resampled onto the input voxels so the map has the input's shape and affine (0 outside
|
| 161 |
+
the 360 mm preprocessing crop). Without ``geometry`` the map stays on the preprocessed grid.
|
| 162 |
+
"""
|
| 163 |
vc = model.config.vision_config
|
| 164 |
patch_size = getattr(vc, "patch_size", None) or 16
|
| 165 |
frame_length = vc.num_channels * (getattr(vc, "tubelet_size", None) or 2)
|
| 166 |
grid = (vc.input_slices // frame_length, vc.img_size // patch_size, vc.img_size // patch_size)
|
| 167 |
|
| 168 |
+
similarity_map = similarity_scores.float().reshape(grid)
|
| 169 |
+
similarity_map = torch.nn.functional.interpolate(
|
| 170 |
+
similarity_map[None, None], size=CT_MONAI_CONFIG["final_size"], mode="trilinear", align_corners=False
|
| 171 |
+
)[0, 0]
|
| 172 |
+
if geometry is None:
|
| 173 |
+
return similarity_map
|
| 174 |
+
return _resample_to_input(similarity_map, geometry["to_model"], geometry["shape"])
|
| 175 |
|
|
|
|
|
|
|
| 176 |
|
| 177 |
+
def save_similarity_map(similarity_map, affine, save_path):
|
| 178 |
+
"""Save the similarity map as NIfTI; with ``geometry["affine"]`` it overlays the input CT voxel for voxel."""
|
|
|
|
|
|
|
| 179 |
import nibabel as nib
|
| 180 |
|
| 181 |
+
sim = similarity_map.detach().float().cpu().numpy() if torch.is_tensor(similarity_map) else similarity_map
|
| 182 |
nib.save(nib.Nifti1Image(sim.astype(np.float32), affine), save_path)
|
| 183 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
def _as_pixel_values(volume, raw_spacing):
|
| 186 |
if torch.is_tensor(volume) and volume.dim() == 5: # already preprocessed by load_ct_volume
|
|
|
|
| 195 |
similarity_prob : scalar sigmoid probability that the phrase applies.
|
| 196 |
similarity_scores : per-token similarity in [0, 1] over the V-JEPA video tokens
|
| 197 |
(shape [1, 1, num_tokens]); ``similarity_scores_to_volume``
|
| 198 |
+
maps it onto the input CT grid.
|
| 199 |
|
| 200 |
volume may also be the ``pixel_values`` returned by ``load_ct_volume``.
|
| 201 |
"""
|