Views
No views yet
| Input Modality | Encoder | Fusion Layer | Prediction Heads (Output) |
|---|---|---|---|
| 3D MRI Volume (96x96x96, 1ch) | 3D ResNet + CBAM + SE Blocks + ASPP | Bidirectional Cross-Attention (MRI ↔ Tabular) + Gated Residuals | → Diagnosis: CN / MCI / AD Classification |
| Clinical Data (13 Features + Masks) | MLP (LayerNorm + GELU) Linear Projection | → Progression: MCI to AD Conversion & Timeline Estimation | |
| Auxiliary Supervision (Training Mode) | Contrastive Learning Alignment + Modality-specific Logits |
| Input/Patch Size | Params | Accuracy | ROC-AUC | F1 Score | F1 Score (Median) | Recall | Precision | F2 Score | MCC | Specificity | FPR | FNR |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 96³ (T1) + 13 fts. (opt.) | 16.85M | %80.67 | %95.39 | %78.68 | %82.70 | %80.67 | %84.14 | %79.24 | %73.69 | %90.33 | %9.67 | %19.33 |
| Class | Preicision | Recall | F1 Score | Support |
|---|---|---|---|---|
| CN | %85.22 | %98.00 | %91.16 | 100 |
| MCI | %95.83 | %46.00 | %62.16 | 100 |
| AD | %71.53 | %98.00 | %82.70 | 100 |
1"""
2Vbai-2.6AD Standalone Inference Script
3===============================================
4A self-contained script containing the full model architecture and inference logic.
5Designed for open-source distribution.
6
7Usage:
8python vbai-2.6ad_test.py --weights path/to/model.pt --mri path/to/scan.nii --clinical "Age:75.2, Sex:1, MMSE:25"
9"""
10import argparse
11import os
12import torch
13import torch.nn as nn
14import torch.nn.functional as F
15import numpy as np
16
17try:
18 import nibabel as nib
19 from scipy.ndimage import zoom
20 HAS_NIBABEL = True
21except ImportError:
22 HAS_NIBABEL = False
23
24# ============================================================
25# Configuration Constants
26# ============================================================
27FEATURE_NAMES = [
28 "Age", "Sex", "MMSE", "CDRSB", "APOE4_count",
29 "CSF_ABETA42", "CSF_TAU", "CSF_PTAU", "CSF_AB42_AB40",
30 "PLASMA_PTAU", "PLASMA_NFL", "PLASMA_AB42_AB40", "PLASMA_GFAP"
31]
32CLASS_NAMES = ["CN", "MCI", "AD"]
33
34class ModelConfig:
35 def __init__(self):
36 self.mri_input_shape = (1, 96, 96, 96)
37 self.mri_encoder_channels = [32, 64, 128, 256]
38 self.mri_bottleneck_channels = 512
39 self.mri_feature_dim = 512
40 self.mri_dropout = 0.4
41 self.use_cbam = True
42 self.use_se_block = True
43
44 self.num_tabular_inputs = len(FEATURE_NAMES) * 2
45 self.tabular_hidden_dims = [128, 256]
46 self.tabular_feature_dim = 256
47 self.tabular_dropout = 0.3
48
49 self.fusion_dim = 512
50 self.fusion_num_heads = 8
51 self.fusion_dropout = 0.3
52
53 self.num_classes = 3
54 self.progression_hidden_dim = 256
55 self.max_progression_months = 120
56 self.num_time_bins = 24
57
58# ============================================================
59# Attention Modules
60# ============================================================
61class ChannelAttention3D(nn.Module):
62 def __init__(self, ch, r=16):
63 super().__init__()
64 m = max(ch // r, 8)
65 self.mlp = nn.Sequential(nn.Linear(ch, m), nn.ReLU(inplace=True), nn.Linear(m, ch))
66
67 def forward(self, x):
68 a = x.mean(dim=[2, 3, 4]); b = x.amax(dim=[2, 3, 4])
69 attn = torch.sigmoid(self.mlp(a) + self.mlp(b))
70 return x * attn[..., None, None, None]
71
72class SpatialAttention3D(nn.Module):
73 def __init__(self, k=7):
74 super().__init__()
75 self.conv = nn.Conv3d(2, 1, k, padding=k // 2, bias=False)
76
77 def forward(self, x):
78 avg = x.mean(dim=1, keepdim=True); mx = x.amax(dim=1, keepdim=True)
79 attn = torch.sigmoid(self.conv(torch.cat([avg, mx], dim=1)))
80 return x * attn
81
82class CBAM3D(nn.Module):
83 def __init__(self, ch, r=16):
84 super().__init__()
85 self.c = ChannelAttention3D(ch, r); self.s = SpatialAttention3D()
86 def forward(self, x): return self.s(self.c(x))
87
88class SEBlock3D(nn.Module):
89 def __init__(self, ch, r=16):
90 super().__init__()
91 m = max(ch // r, 8)
92 self.fc = nn.Sequential(nn.Linear(ch, m), nn.ReLU(True), nn.Linear(m, ch), nn.Sigmoid())
93 def forward(self, x):
94 s = x.mean(dim=[2, 3, 4]); s = self.fc(s)[..., None, None, None]
95 return x * s
96
97# ============================================================
98# Encoders
99# ============================================================
100class ResBlock3D(nn.Module):
101 def __init__(self, in_ch, out_ch, stride=1, use_cbam=True, use_se=True, drop_path=0.0):
102 super().__init__()
103 self.conv1 = nn.Conv3d(in_ch, out_ch, 3, stride, 1, bias=False)
104 self.bn1 = nn.BatchNorm3d(out_ch)
105 self.conv2 = nn.Conv3d(out_ch, out_ch, 3, 1, 1, bias=False)
106 self.bn2 = nn.BatchNorm3d(out_ch)
107 self.act = nn.GELU()
108 self.cbam = CBAM3D(out_ch) if use_cbam else nn.Identity()
109 self.se = SEBlock3D(out_ch) if use_se else nn.Identity()
110 self.skip = nn.Identity() if (in_ch == out_ch and stride == 1) else nn.Sequential(
111 nn.Conv3d(in_ch, out_ch, 1, stride, bias=False), nn.BatchNorm3d(out_ch))
112
113 def forward(self, x):
114 identity = self.skip(x)
115 out = self.act(self.bn1(self.conv1(x)))
116 out = self.bn2(self.conv2(out))
117 out = self.cbam(out); out = self.se(out)
118 return self.act(out + identity)
119
120class ASPP3D(nn.Module):
121 def __init__(self, in_ch, out_ch, dilations=(1, 6, 12, 18)):
122 super().__init__()
123 per = out_ch // len(dilations)
124 self.branches = nn.ModuleList([
125 nn.Sequential(nn.Conv3d(in_ch, per, 3, padding=d, dilation=d, bias=False),
126 nn.BatchNorm3d(per), nn.GELU())
127 for d in dilations
128 ])
129 self.gp = nn.Sequential(
130 nn.AdaptiveAvgPool3d(1),
131 nn.Conv3d(in_ch, per, 1, bias=False),
132 nn.BatchNorm3d(per), nn.GELU())
133 self.fuse = nn.Sequential(nn.Conv3d(per * (len(dilations) + 1), out_ch, 1, bias=False),
134 nn.BatchNorm3d(out_ch), nn.GELU())
135
136 def forward(self, x):
137 feats = [b(x) for b in self.branches]
138 g = self.gp(x)
139 g = F.interpolate(g, size=x.shape[2:], mode="trilinear", align_corners=False)
140 feats.append(g)
141 return self.fuse(torch.cat(feats, dim=1))
142
143class MRIEncoder3D(nn.Module):
144 def __init__(self, mcfg: ModelConfig):
145 super().__init__()
146 ch = mcfg.mri_encoder_channels
147 self.stem = nn.Sequential(
148 nn.Conv3d(1, ch[0], 7, 2, 3, bias=False), nn.BatchNorm3d(ch[0]), nn.GELU(),
149 nn.MaxPool3d(3, 2, 1))
150 self.stage1 = self._make(ch[0], ch[0], 2, 1, mcfg)
151 self.stage2 = self._make(ch[0], ch[1], 2, 2, mcfg)
152 self.stage3 = self._make(ch[1], ch[2], 2, 2, mcfg)
153 self.stage4 = self._make(ch[2], ch[3], 2, 2, mcfg)
154 self.aspp = ASPP3D(ch[3], mcfg.mri_bottleneck_channels)
155 self.pool = nn.AdaptiveAvgPool3d(1)
156 self.proj = nn.Sequential(
157 nn.Linear(mcfg.mri_bottleneck_channels, mcfg.mri_feature_dim),
158 nn.GELU(), nn.Dropout(mcfg.mri_dropout))
159
160 def _make(self, in_ch, out_ch, n, stride, mcfg):
161 layers = [ResBlock3D(in_ch, out_ch, stride, mcfg.use_cbam, mcfg.use_se_block)]
162 for _ in range(1, n):
163 layers.append(ResBlock3D(out_ch, out_ch, 1, mcfg.use_cbam, mcfg.use_se_block))
164 return nn.Sequential(*layers)
165
166 def forward(self, x):
167 x = self.stem(x)
168 x = self.stage1(x); x = self.stage2(x); x = self.stage3(x); x = self.stage4(x)
169 x = self.aspp(x); x = self.pool(x).flatten(1)
170 return self.proj(x)
171
172class TabularEncoder(nn.Module):
173 def __init__(self, mcfg: ModelConfig):
174 super().__init__()
175 prev = mcfg.num_tabular_inputs
176 layers = []
177 for h in mcfg.tabular_hidden_dims:
178 layers += [nn.Linear(prev, h), nn.LayerNorm(h), nn.GELU(), nn.Dropout(mcfg.tabular_dropout)]
179 prev = h
180 layers += [nn.Linear(prev, mcfg.tabular_feature_dim)]
181 self.net = nn.Sequential(*layers)
182
183 def forward(self, x):
184 return self.net(x)
185
186# ============================================================
187# Fusion & Heads
188# ============================================================
189class CrossModalFusion(nn.Module):
190 def __init__(self, mri_dim, tab_dim, fdim, heads=8, dropout=0.1):
191 super().__init__()
192 self.pm = nn.Linear(mri_dim, fdim); self.pt = nn.Linear(tab_dim, fdim)
193 self.a_mt = nn.MultiheadAttention(fdim, heads, dropout=dropout, batch_first=True)
194 self.a_tm = nn.MultiheadAttention(fdim, heads, dropout=dropout, batch_first=True)
195 self.lnm = nn.LayerNorm(fdim); self.lnt = nn.LayerNorm(fdim)
196 self.gate = nn.Sequential(nn.Linear(fdim * 2, fdim), nn.Sigmoid())
197 self.out = nn.Sequential(nn.Linear(fdim * 2, fdim), nn.GELU(), nn.Dropout(dropout))
198
199 def forward(self, m, t):
200 m1 = self.pm(m).unsqueeze(1); t1 = self.pt(t).unsqueeze(1)
201 ma, _ = self.a_mt(m1, t1, t1); ta, _ = self.a_tm(t1, m1, m1)
202 m2 = self.lnm(m1 + ma).squeeze(1); t2 = self.lnt(t1 + ta).squeeze(1)
203 cat = torch.cat([m2, t2], dim=-1)
204 g = self.gate(cat); o = self.out(cat)
205 return g * m2 + (1 - g) * t2 + o
206
207class ClsHead(nn.Module):
208 def __init__(self, in_dim, num_classes, dropout=0.3):
209 super().__init__()
210 self.h = nn.Sequential(
211 nn.Linear(in_dim, 256), nn.GELU(), nn.Dropout(dropout),
212 nn.Linear(256, 128), nn.GELU(), nn.Dropout(dropout),
213 nn.Linear(128, num_classes))
214 def forward(self, x): return self.h(x)
215
216class ProgressionHead(nn.Module):
217 def __init__(self, in_dim, hidden=256, max_months=120, n_bins=24):
218 super().__init__()
219 self.max_months = float(max_months); self.n_bins = n_bins
220 self.shared = nn.Sequential(nn.Linear(in_dim, hidden), nn.GELU(), nn.Dropout(0.3))
221 self.binary = nn.Linear(hidden, 1)
222 self.time = nn.Sequential(nn.Linear(hidden, 64), nn.GELU(), nn.Linear(64, 1))
223
224 def forward(self, x):
225 h = self.shared(x)
226 logits = self.binary(h).squeeze(-1)
227 return {
228 "will_progress": torch.sigmoid(logits),
229 "time_to_conversion": torch.clamp(F.softplus(self.time(h)).squeeze(-1), min=0.0, max=self.max_months),
230 }
231
232# ============================================================
233# Main Model Class
234# ============================================================
235class HFv3AModel(nn.Module):
236 def __init__(self, mcfg: ModelConfig = None):
237 super().__init__()
238 self.cfg = mcfg or ModelConfig()
239 self.mri_encoder = MRIEncoder3D(self.cfg)
240 self.tab_encoder = TabularEncoder(self.cfg)
241 self.mri_classifier = ClsHead(self.cfg.mri_feature_dim, self.cfg.num_classes, self.cfg.mri_dropout)
242 self.tab_classifier = ClsHead(self.cfg.tabular_feature_dim, self.cfg.num_classes, self.cfg.tabular_dropout)
243 self.fusion = CrossModalFusion(self.cfg.mri_feature_dim, self.cfg.tabular_feature_dim, self.cfg.fusion_dim, self.cfg.fusion_num_heads, self.cfg.fusion_dropout)
244 self.fused_classifier = ClsHead(self.cfg.fusion_dim, self.cfg.num_classes, self.cfg.fusion_dropout)
245 self.progression_head = ProgressionHead(self.cfg.fusion_dim, self.cfg.progression_hidden_dim, self.cfg.max_progression_months, self.cfg.num_time_bins)
246
247 def forward(self, mri=None, tab=None):
248 out = {}
249 m_feat = t_feat = None
250 if mri is not None:
251 m_feat = self.mri_encoder(mri)
252 out["mri_logits"] = self.mri_classifier(m_feat)
253 if tab is not None:
254 t_feat = self.tab_encoder(tab)
255 out["tab_logits"] = self.tab_classifier(t_feat)
256
257 if m_feat is not None and t_feat is not None:
258 f = self.fusion(m_feat, t_feat)
259 out["fused_logits"] = self.fused_classifier(f)
260 out["progression"] = self.progression_head(f)
261 elif m_feat is not None:
262 out["fused_logits"] = out["mri_logits"]
263 elif t_feat is not None:
264 out["fused_logits"] = out["tab_logits"]
265
266 return out
267
268 @torch.no_grad()
269 def predict(self, mri=None, tab=None):
270 self.eval()
271 out = self.forward(mri=mri, tab=tab)
272 probs = F.softmax(out["fused_logits"], dim=-1)
273 pred = probs.argmax(dim=-1)
274 result = {
275 "pred_class": pred,
276 "class_probs": probs,
277 "class_name": CLASS_NAMES[pred.item()]
278 }
279 if "progression" in out:
280 p = out["progression"]
281 result["will_progress"] = p["will_progress"].item()
282 result["time_to_conversion_months"] = p["time_to_conversion"].item()
283 return result
284
285# ============================================================
286# Preprocessing Helpers
287# ============================================================
288def load_mri_tensor(path: str, target_shape=(96, 96, 96)):
289 if not HAS_NIBABEL:
290 raise ImportError("Please install nibabel and scipy to process NIfTI MRI images: pip install nibabel scipy")
291
292 img = nib.load(path)
293 data = img.get_fdata().astype(np.float32)
294 if data.ndim == 4:
295 data = data[..., 0]
296
297 mask = data > 0
298 if mask.sum() > 0:
299 vals = data[mask]
300 lo, hi = np.percentile(vals, [1.0, 99.0])
301 data = np.clip(data, lo, hi)
302 m, s = vals.mean(), vals.std()
303 if s > 0:
304 data = (data - m) / s
305 data[~mask] = 0
306
307 if data.shape != target_shape:
308 f = [t / s for t, s in zip(target_shape, data.shape)]
309 data = zoom(data, f, order=1)
310
311 tensor = torch.from_numpy(np.ascontiguousarray(data)).unsqueeze(0).unsqueeze(0).float()
312 return tensor
313
314def parse_clinical_data(clinical_str: str):
315 """
316 Parses a string like "Age:75.2, Sex:1, MMSE:25" into a tabular tensor.
317 If normalizer values are not provided, it passes raw values.
318 """
319 pairs = [p.strip().split(':') for p in clinical_str.split(',') if ':' in p]
320 val_dict = {k.strip(): float(v.strip()) for k, v in pairs}
321
322 vals = []
323 masks = []
324 for feat in FEATURE_NAMES:
325 if feat in val_dict:
326 vals.append(val_dict[feat])
327 masks.append(1.0)
328 else:
329 vals.append(0.0)
330 masks.append(0.0)
331
332 # Note: Without the original training normalizer state, scaling might be inaccurate.
333 # We pass the raw values here. For accurate production use, you should apply your normalizer parameters.
334 tab_tensor = torch.tensor(vals + masks, dtype=torch.float32).unsqueeze(0)
335 return tab_tensor
336
337# ============================================================
338# Inference Pipeline
339# ============================================================
340def run_inference(args):
341 device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
342 print(f"[*] Running on device: {device}")
343
344 print("[*] Initializing Model...")
345 model = HFv3AModel().to(device)
346 total_params = sum(p.numel() for p in model.parameters())
347 print(f"[*] Model Parameters: {total_params:,} ({(total_params/1e6):.2f}M)")
348
349 if args.weights:
350 if os.path.exists(args.weights):
351 print(f"[*] Loading weights from {args.weights}")
352 try:
353 ckpt = torch.load(args.weights, map_location=device, weights_only=False)
354 # Flexible loading depending on how weights were saved
355 state_dict = ckpt["model"] if "model" in ckpt else (ckpt["model_state_dict"] if "model_state_dict" in ckpt else ckpt)
356 model.load_state_dict(state_dict, strict=False)
357 except Exception as e:
358 print(f"[!] Error loading weights: {e}")
359 else:
360 print(f"[!] Warning: Weights file not found at {args.weights}. Using untrained model.")
361
362 model.eval()
363
364 mri_tensor = None
365 tab_tensor = None
366
367 if args.mri:
368 if os.path.exists(args.mri):
369 print(f"[*] Processing MRI: {args.mri}")
370 mri_tensor = load_mri_tensor(args.mri).to(device)
371 else:
372 print(f"[!] Error: MRI file not found at {args.mri}")
373 return
374
375 if args.clinical:
376 print(f"[*] Processing Clinical Data: {args.clinical}")
377 tab_tensor = parse_clinical_data(args.clinical).to(device)
378
379 if mri_tensor is None and tab_tensor is None:
380 print("[!] Error: You must provide either --mri or --clinical inputs.")
381 return
382
383 print("[*] Running Prediction...")
384 result = model.predict(mri=mri_tensor, tab=tab_tensor)
385
386 print("\n" + "="*40)
387 print(" PREDICTION RESULTS ")
388 print("="*40)
389 print(f"Diagnosis : {result['class_name']} (Class {result['pred_class'].item()})")
390
391 probs = result['class_probs'].squeeze().tolist()
392 print(f"Confidence (CN) : {probs[0]:.4f}")
393 print(f"Confidence (MCI) : {probs[1]:.4f}")
394 print(f"Confidence (AD) : {probs[2]:.4f}")
395
396 if "will_progress" in result:
397 print("-" * 40)
398 print(f"Progression Risk : {result['will_progress']:.2%}")
399 print(f"Est. Time to Convert: {result['time_to_conversion_months']:.1f} months")
400 print("="*40)
401
402if __name__ == "__main__":
403 parser = argparse.ArgumentParser(description="Vbai-2.6AD Inference Script")
404 parser.add_argument("--weights", type=str, help="Path to the model .pt weights file")
405 parser.add_argument("--mri", type=str, help="Path to the input NIfTI (.nii / .nii.gz) MRI scan")
406 parser.add_argument("--clinical", type=str, help='Clinical data string, e.g., "Age:75.2, Sex:1, MMSE:25"')
407
408 args = parser.parse_args()
409
410 if not any([args.mri, args.clinical]):
411 parser.print_help()
412 else:
413 run_inference(args)
4141"""
2Vbai-2.6AD Standalone Inference Script
3===============================================
4A self-contained script containing the full model architecture and inference logic.
5Designed for open-source distribution.
6
7Usage:
8python vbai-2.6ad_test.py --weights path/to/model --mri path/to/scan.nii --clinical "Age:75.2, Sex:1, MMSE:25"
9"""
10import io, sys
11sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
12sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
13
14import argparse
15import os
16import json
17import numpy as np
18
19try:
20 import nibabel as nib
21 from scipy.ndimage import zoom
22 HAS_NIBABEL = True
23except ImportError:
24 HAS_NIBABEL = False
25
26try:
27 import onnxruntime as ort
28except ImportError:
29 raise ImportError("onnxruntime is required: pip install onnxruntime (GPU: pip install onnxruntime-gpu)")
30
31# ============================================================
32# Configuration Constants
33# ============================================================
34FEATURE_NAMES = [
35 "Age", "Sex", "MMSE", "CDRSB", "APOE4_count",
36 "CSF_ABETA42", "CSF_TAU", "CSF_PTAU", "CSF_AB42_AB40",
37 "PLASMA_PTAU", "PLASMA_NFL", "PLASMA_AB42_AB40", "PLASMA_GFAP"
38]
39CLASS_NAMES = ["CN", "MCI", "AD"]
40
41_ORT_PROVIDERS = ["CUDAExecutionProvider", "CPUExecutionProvider"]
42
43def _resolve_onnx(weights_arg, suffix):
44 if weights_arg and weights_arg.endswith(".onnx"):
45 return weights_arg
46 base = (weights_arg or "").rstrip("/\\")
47 for candidate in [base + suffix, os.path.join(base, "Vbai-2.6AD" + suffix)]:
48 if os.path.exists(candidate):
49 return candidate
50 return base + suffix
51
52def _load_normalizer(weights_arg):
53 base = (weights_arg or "").rstrip("/\\")
54 for candidate in [
55 os.path.join(os.path.dirname(base), "Vbai-2.6AD_normalizer.json"),
56 os.path.join(base, "Vbai-2.6AD_normalizer.json"),
57 base.replace(".onnx", "_normalizer.json"),
58 ]:
59 if os.path.exists(candidate):
60 with open(candidate, "r", encoding="utf-8") as f:
61 return json.load(f)
62 return None
63
64def _load_session(onnx_path):
65 avail = ort.get_available_providers()
66 providers = [p for p in _ORT_PROVIDERS if p in avail] or ["CPUExecutionProvider"]
67 return ort.InferenceSession(onnx_path, providers=providers)
68
69# ============================================================
70# Preprocessing Helpers
71# ============================================================
72def load_mri_tensor(path: str, target_shape=(96, 96, 96)):
73 if not HAS_NIBABEL:
74 raise ImportError("Please install nibabel and scipy to process NIfTI MRI images: pip install nibabel scipy")
75
76 img = nib.load(path)
77 data = img.get_fdata().astype(np.float32)
78 if data.ndim == 4:
79 data = data[..., 0]
80
81 mask = data > 0
82 if mask.sum() > 0:
83 vals = data[mask]
84 lo, hi = np.percentile(vals, [1.0, 99.0])
85 data = np.clip(data, lo, hi)
86 m, s = vals.mean(), vals.std()
87 if s > 0:
88 data = (data - m) / s
89 data[~mask] = 0
90
91 if data.shape != target_shape:
92 f = [t / s for t, s in zip(target_shape, data.shape)]
93 data = zoom(data, f, order=1)
94
95 return np.ascontiguousarray(data[np.newaxis, np.newaxis], dtype=np.float32)
96
97def parse_clinical_data(clinical_str: str, normalizer=None):
98 """
99 Parses a string like "Age:75.2, Sex:1, MMSE:25" into a tabular tensor.
100 If normalizer values are not provided, it passes raw values.
101 """
102 pairs = [p.strip().split(':') for p in clinical_str.split(',') if ':' in p]
103 val_dict = {k.strip(): float(v.strip()) for k, v in pairs}
104
105 vals = []
106 masks = []
107 for feat in FEATURE_NAMES:
108 if feat in val_dict:
109 v = val_dict[feat]
110 if normalizer:
111 mean = normalizer["mean"].get(feat, 0.0)
112 std = normalizer["std"].get(feat, 1.0)
113 v = (v - mean) / (std + 1e-8)
114 vals.append(v)
115 masks.append(1.0)
116 else:
117 vals.append(0.0)
118 masks.append(0.0)
119
120 # Note: Without the original training normalizer state, scaling might be inaccurate.
121 # We pass the raw values here. For accurate production use, you should apply your normalizer parameters.
122 return np.array([vals + masks], dtype=np.float32)
123
124# ============================================================
125# Inference Pipeline
126# ============================================================
127def run_inference(args):
128 avail_providers = ort.get_available_providers()
129 device_info = "GPU (CUDA)" if "CUDAExecutionProvider" in avail_providers else "CPU"
130 print(f"[*] Running on device: {device_info}")
131
132 mri_np = None
133 tab_np = None
134
135 normalizer = _load_normalizer(args.weights) if args.clinical else None
136
137 if args.mri:
138 if os.path.exists(args.mri):
139 print(f"[*] Processing MRI: {args.mri}")
140 mri_np = load_mri_tensor(args.mri)
141 else:
142 print(f"[!] Error: MRI file not found at {args.mri}")
143 return
144
145 if args.clinical:
146 print(f"[*] Processing Clinical Data: {args.clinical}")
147 tab_np = parse_clinical_data(args.clinical, normalizer=normalizer)
148
149 if mri_np is None and tab_np is None:
150 print("[!] Error: You must provide either --mri or --clinical inputs.")
151 return
152
153 if mri_np is not None and tab_np is not None:
154 onnx_path = _resolve_onnx(args.weights, "_full.onnx")
155 mode = "full"
156 elif mri_np is not None:
157 onnx_path = _resolve_onnx(args.weights, "_mri.onnx")
158 mode = "mri_only"
159 else:
160 onnx_path = _resolve_onnx(args.weights, "_tab.onnx")
161 mode = "tab_only"
162
163 print("[*] Initializing Model...")
164 if not os.path.exists(onnx_path):
165 print(f"[!] Error: ONNX model not found at {onnx_path}")
166 return
167
168 sess = _load_session(onnx_path)
169 mb = os.path.getsize(onnx_path) / 1024**2
170 print(f"[*] Model loaded: {os.path.basename(onnx_path)} ({mb:.1f} MB)")
171 print(f"[*] Providers: {sess.get_providers()}")
172
173 print("[*] Running Prediction...")
174
175 if mode == "full":
176 ort_out = sess.run(None, {"mri_input": mri_np, "tab_input": tab_np})
177 class_probs, will_progress, time_to_conv = ort_out
178 elif mode == "mri_only":
179 ort_out = sess.run(None, {"mri_input": mri_np})
180 class_probs = ort_out[0]
181 will_progress = time_to_conv = None
182 else:
183 ort_out = sess.run(None, {"tab_input": tab_np})
184 class_probs = ort_out[0]
185 will_progress = time_to_conv = None
186
187 probs = class_probs[0].tolist()
188 pred_idx = int(np.argmax(probs))
189
190 print("\n" + "="*40)
191 print(" PREDICTION RESULTS ")
192 print("="*40)
193 print(f"Diagnosis : {CLASS_NAMES[pred_idx]} (Class {pred_idx})")
194
195 print(f"Confidence (CN) : {probs[0]:.4f}")
196 print(f"Confidence (MCI) : {probs[1]:.4f}")
197 print(f"Confidence (AD) : {probs[2]:.4f}")
198
199 if will_progress is not None:
200 print("-" * 40)
201 print(f"Progression Risk : {float(will_progress[0]):.2%}")
202 print(f"Est. Time to Convert: {float(time_to_conv[0]):.1f} months")
203 print("="*40)
204
205if __name__ == "__main__":
206 parser = argparse.ArgumentParser(description="Vbai-2.6AD Inference Script")
207 parser.add_argument("--weights", type=str, help="Path to the ONNX model file or directory containing ONNX files")
208 parser.add_argument("--mri", type=str, help="Path to the input NIfTI (.nii / .nii.gz) MRI scan")
209 parser.add_argument("--clinical", type=str, help='Clinical data string, e.g., "Age:75.2, Sex:1, MMSE:25"')
210
211 args = parser.parse_args()
212
213 if not any([args.mri, args.clinical]):
214 parser.print_help()
215 else:
216 run_inference(args)
217 requirements.txt for full dependency list