File size: 3,585 Bytes
c0db47c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7db9f93
c0db47c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Pinned model inference and official test split; no training or remote code."""
import hashlib
import os
from functools import lru_cache
from pathlib import Path
from urllib.request import urlopen

ROOT = Path(__file__).resolve().parent
CACHE = Path(os.environ.get("APP_CACHE", ROOT / ".cache"))
os.environ.setdefault("HF_HOME", str(CACHE / "huggingface"))
os.environ.setdefault("HF_HUB_DISABLE_XET", "1")

import numpy as np
import torch
from PIL import Image, ImageOps

MODEL_ID = "therayelab/pneumoniamnist-resnet18"
REVISION = "a5e0ebebfcf20bee6701e6d528e33c5bf7a33b94"
DATA_URL = "https://zenodo.org/records/10519652/files/pneumoniamnist.npz?download=1"
DATA_MD5 = "28209eda62fecd6e6a2d98b1501bb15f"
LABELS = ["Normal", "Pneumonia"]
torch.set_num_threads(min(4, os.cpu_count() or 1))

@lru_cache(maxsize=1)
def model_bundle():
    from transformers import AutoImageProcessor, AutoModelForImageClassification
    processor = AutoImageProcessor.from_pretrained(MODEL_ID, revision=REVISION, use_fast=False)
    model = AutoModelForImageClassification.from_pretrained(
        MODEL_ID, revision=REVISION, use_safetensors=True, trust_remote_code=False
    ).eval()
    if model.config.id2label != {0: "0", 1: "1"}:
        raise ValueError("Unexpected model label mapping; review before using this demo.")
    return processor, model

def clean_image(image):
    if image is None:
        raise ValueError("Upload an image first.")
    if image.width * image.height > 20_000_000:
        raise ValueError("Please use an image smaller than 20 megapixels.")
    return ImageOps.exif_transpose(image).convert("RGB")

def predict(images):
    processor, model = model_bundle()
    inputs = processor(images=[clean_image(i) for i in images], return_tensors="pt")
    with torch.inference_mode():
        probabilities = model(**inputs).logits.softmax(dim=-1).cpu().numpy()
    return probabilities

@lru_cache(maxsize=1)
def test_data():
    CACHE.mkdir(parents=True, exist_ok=True)
    path = CACHE / "pneumoniamnist.npz"
    if not path.exists():
        with urlopen(DATA_URL, timeout=120) as response:
            data = response.read(20_000_000)
        if hashlib.md5(data).hexdigest() != DATA_MD5:
            raise ValueError("Dataset checksum failed; please retry later.")
        path.write_bytes(data)
    if hashlib.md5(path.read_bytes()).hexdigest() != DATA_MD5:
        raise ValueError("Dataset checksum failed; remove the cached dataset and retry.")
    with np.load(path, allow_pickle=False) as data:
        images = data["test_images"].copy()
        labels = data["test_labels"].reshape(-1).copy()
    assert images.shape == (624, 28, 28) and set(labels) == {0, 1}
    return images, labels

@lru_cache(maxsize=1)
def test_predictions():
    images, _ = test_data()
    return np.concatenate([predict([Image.fromarray(i) for i in images[s:s+16]])
                           for s in range(0, len(images), 16)])

def metrics(labels, probabilities, threshold):
    predicted = (np.asarray(probabilities)[:, 1] >= threshold).astype(int)
    labels = np.asarray(labels)
    tn = int(np.sum((labels == 0) & (predicted == 0)))
    fp = int(np.sum((labels == 0) & (predicted == 1)))
    fn = int(np.sum((labels == 1) & (predicted == 0)))
    tp = int(np.sum((labels == 1) & (predicted == 1)))
    divide = lambda a, b: a / b if b else None
    return {"n": len(labels), "accuracy": divide(tp+tn, len(labels)),
            "sensitivity": divide(tp, tp+fn), "specificity": divide(tn, tn+fp),
            "precision": divide(tp, tp+fp), "tn": tn, "fp": fp, "fn": fn, "tp": tp}