Views
No views yet
⚠️ Not a medical device. Research/education prototype only. Not for clinical use.
| File | What it is |
|---|---|
backbone.onnx | DenseNet121 feature extractor (image 224×224×3 → 7×7×1024), for onnxruntime |
gradcam_head.npz | Classification-head weights (fc1/bn1/fc2/bn2/prob) — run in NumPy for prediction + Grad-CAM |
best_model.keras | Original TensorFlow/Keras model (reference; the ONNX+NumPy path is validated identical) |
threshold.json | Operating threshold (0.324) tuned on validation for sensitivity ≥ 0.92 |
temperature.json | Calibration temperature (T = 0.72) |
triage.json | Calibrated threshold + abstention band for the 3-zone triage |
tf.image.resize, which is bilinear
without antialiasing. PIL.Image.resize — with its default resample or with
BILINEAR — antialiases when downscaling. It is a different operator, and swapping it
in measurably changes the model.| Resize | Sensitivity | ROC-AUC | False negatives |
|---|---|---|---|
PIL.Image.resize(..., BILINEAR) | 0.9089 | 0.9830 | 39 |
tf.image-equivalent (correct) | 0.9439 | 0.9863 | 24 |
1import numpy as np, onnxruntime as ort
2from PIL import Image
3
4def resize_bilinear(arr, size=224):
5 """Matches tf.image.resize(..., antialias=False): half-pixel centers, no antialias."""
6 h, w = arr.shape[:2]
7
8 def axis(n_out, n_in):
9 src = np.clip((np.arange(n_out) + 0.5) * (n_in / n_out) - 0.5, 0, n_in - 1)
10 lo = np.floor(src).astype(int)
11 return lo, np.minimum(lo + 1, n_in - 1), (src - lo).astype("float32")
12
13 y0, y1, wy = axis(size, h)
14 x0, x1, wx = axis(size, w)
15 top = arr[y0][:, x0] + (arr[y0][:, x1] - arr[y0][:, x0]) * wx[None, :, None]
16 bot = arr[y1][:, x0] + (arr[y1][:, x1] - arr[y1][:, x0]) * wx[None, :, None]
17 return top + (bot - top) * wy[:, None, None]
18
19sess = ort.InferenceSession("backbone.onnx")
20H = dict(np.load("gradcam_head.npz"))
21
22img = Image.open("xray.jpg").convert("RGB")
23arr = resize_bilinear(np.asarray(img, dtype="float32")) # NOT img.resize(...)
24x = (arr / 255.0 - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225]
25conv = sess.run(None, {sess.get_inputs()[0].name: x[None].astype("float32")})[0][0]
26
27gap = conv.mean((0, 1))
28a1 = np.maximum(gap @ H["fc1_W"] + H["fc1_b"], 0)
29n1 = H["bn1_gamma"] * (a1 - H["bn1_mean"]) / np.sqrt(H["bn1_var"] + H["bn1_eps"]) + H["bn1_beta"]
30a2 = np.maximum(n1 @ H["fc2_W"] + H["fc2_b"], 0)
31n2 = H["bn2_gamma"] * (a2 - H["bn2_mean"]) / np.sqrt(H["bn2_var"] + H["bn2_eps"]) + H["bn2_beta"]
32prob = 1 / (1 + np.exp(-(n2 @ H["prob_W"][:, 0] + H["prob_b"][0])))
33print("P(pneumonia) =", float(prob))temperature.json, T = 0.72) and then the calibrated operating
threshold and abstention band from triage.json —
see api/inference.py.