Views
No views yet
hlip package required to
register the custom visual encoder for loading.1from pathlib import Path
2import os, sys, json, torch, importlib
3
4from huggingface_hub import snapshot_download
5from open_clip.factory import _MODEL_CONFIGS
6from open_clip import create_model_and_transforms, get_tokenizer, build_zero_shot_classifier
7
8import safetensors.torch as st
9from torchvision.transforms import Normalize
10from timm.data.constants import IMAGENET_DEFAULT_MEAN, IMAGENET_DEFAULT_STD
11
12
13def loader(study_path: str, num_slices: int):
14 """
15 study_path: folder containing per-slice tensors saved with torch.save()
16 each file is a [C, H, W] or [C, H, W, 1] tensor in [0, 255]
17 returns: image tensor of shape [1, n_scans, 1, D, H, W]
18 """
19 imgs = []
20 for scan in [os.path.join(study_path, p) for p in os.listdir(study_path)]:
21 # load image tensor
22 img = torch.load(scan, weights_only=True)
23 if len(img.shape) == 4:
24 # [C, H, W, 1] -> [C, H, W]
25 img = img[:, :, :, 0]
26 img = img.float() / 255.0 # [C, H, W]
27 _, h, w = img.shape
28
29 # pad to square
30 size = max(h, w)
31 pad_h = size - h
32 pad_w = size - w
33 left = pad_w // 2
34 right = pad_w - left
35 top = pad_h // 2
36 bottom = pad_h - top
37 img = torch.nn.functional.pad(
38 img, (left, right, top, bottom), mode="constant", value=0
39 )
40
41 # resize to 256, make depth=num_slices, center-crop to 224
42 img = torch.nn.functional.interpolate(
43 img[None, ...], size=(256, 256), mode="bilinear"
44 )[0]
45 img = torch.nn.functional.interpolate(
46 img[None, None, ...], size=(num_slices, 256, 256), mode="nearest-exact"
47 )[0, 0]
48 img = img[:, 16:240, 16:240] # [D, 224, 224]
49
50 # normalize (scalar mean/std across slices-as-channels)
51 normalizer = Normalize(
52 torch.as_tensor(IMAGENET_DEFAULT_MEAN).mean(),
53 torch.as_tensor(IMAGENET_DEFAULT_STD).mean(),
54 )
55 img = normalizer(img[None, ...]) # [1, D, H, W]
56 imgs.append(img)
57
58 # [1, n_scans, 1, D, H, W]
59 return torch.stack(imgs, dim=0)[None, ...]
60
61
62# ---- constants ----
63REPO_ID = "zch0414/hlip-2025_10_08"
64MODEL_NAME = "ablate_seqposemb_clip_vit_base_multiscan_h2_dinotxt1568"
65DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
66# -------------------
67
68# 1) download snapshot and make vendored package importable
69repo_dir = Path(snapshot_download(repo_id=REPO_ID))
70sys.path.append(str(repo_dir))
71importlib.invalidate_caches()
72print(f"[OK] repo_dir = {repo_dir}")
73
74# 2) import your registry so timm/OpenCLIP knows the custom visual encoder
75import hlip.visual_encoder # registers the custom visual encoder with timm
76import hlip.visual_encoder_rope # optional depending on model; safe to import if vendored
77
78# 3) load the vendored HLIP model config and register it under MODEL_NAME
79cfg_path = repo_dir / "hlip" / "model_configs" / f"{MODEL_NAME}.json"
80model_cfg = json.loads(cfg_path.read_text())
81model_cfg.setdefault("text_cfg", {})
82model_cfg["text_cfg"]["hf_tokenizer_name"] = REPO_ID
83_MODEL_CONFIGS[MODEL_NAME] = model_cfg
84print("[OK] registered MODEL_CONFIGS key:", MODEL_NAME)
85
86# 4) build model and tokenizer
87model, _, _ = create_model_and_transforms(
88 MODEL_NAME,
89 device=DEVICE,
90 output_dict=True,
91)
92tokenizer = get_tokenizer(MODEL_NAME)
93print("[OK] model built on", DEVICE)
94print("[OK] tokenizer ready")
95
96# 5) load pretrained weights from the snapshot (prefer safetensors)
97weight_path = None
98for fname in ("model.safetensors", "pytorch_model.bin"):
99 p = repo_dir / fname
100 if p.exists():
101 weight_path = p
102 break
103assert weight_path is not None, "No weights found in repo snapshot."
104
105if weight_path.suffix == ".safetensors":
106 state_dict = st.load_file(str(weight_path))
107else:
108 state_dict = torch.load(str(weight_path), map_location="cpu")
109
110missing, unexpected = model.load_state_dict(state_dict, strict=False)
111print(
112 f"[OK] loaded weights: {weight_path.name} | "
113 f"missing={len(missing)} unexpected={len(unexpected)}"
114)
115
116# 6) build zero-shot classifier for brain MRI labels
117from hlip.zeroshot_metadata_pubbrain5 import PROMPTS, TEMPLATES
118
119classifier = build_zero_shot_classifier(
120 model,
121 tokenizer=tokenizer,
122 classnames=PROMPTS["prompt"],
123 templates=TEMPLATES["template"],
124 num_classes_per_batch=None, # use all classes
125 device=DEVICE,
126 use_tqdm=False,
127)
128
129# 7) example data and inference
130# This snapshot includes an example study under docs/.
131# Replace this with your own study folder of per-slice tensors if needed.
132study_dir = repo_dir / "docs" / "BraTS-GLI-00459-000"
133image = loader(str(study_dir), num_slices=48).to(DEVICE, non_blocking=True)
134
135model.eval()
136with torch.no_grad():
137 output = model(image=image) # image: [1, n_scans, 1, D, H, W]
138 # HLIP returns per-scan image features; use the first scan token to match eval scripts.
139 image_features = output["image_features"][:, 0, :] # [1, feature_dim]
140 logit_scale = output["logit_scale"]
141 logits_per_image = logit_scale * (image_features @ classifier) # [1, num_classes]
142 probs = logits_per_image.softmax(dim=-1).detach().cpu()
143
144print("Zero-shot class probabilities:")
145for i, prompt in enumerate(PROMPTS["prompt"]):
146 print(f"{prompt}: {float(probs[0, i]):.4f}")