File size: 5,172 Bytes
2d41719
 
0291c7a
2d41719
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0291c7a
2d41719
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb799f3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2d41719
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0201ce2
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import json
from pathlib import Path
import spaces
import numpy as np
import torch
import torchvision.transforms as T
import gradio as gr
from PIL import Image
from matplotlib import colormaps

from model import SceneCNN, grad_cam

ROOT = Path(__file__).parent

# ---------- load model ----------
meta = json.loads((ROOT / "cnn_intel_meta.json").read_text())
CLASSES  = meta["class_names"]
IMG_SIZE = meta["img_size"]

model = SceneCNN(base=meta["base"], num_classes=len(CLASSES))
model.load_state_dict(torch.load(ROOT / "cnn_intel.pth", map_location="cpu"))
model.eval()

preprocess = T.Compose([
    T.Resize((IMG_SIZE, IMG_SIZE)),
    T.ToTensor(),
    T.Normalize(meta["mean"], meta["std"]),
])

@spaces.GPU
# ---------- prediction ----------
def predict(img):
    if img is None:
        return None, None
    img = img.convert("RGB")
    cam, probs = grad_cam(model, preprocess(img))

    show = 384
    base = np.asarray(img.resize((show, show))) / 255.0
    cam_big = np.asarray(Image.fromarray((cam * 255).astype(np.uint8)).resize((show, show), Image.BILINEAR)) / 255.0
    heat = colormaps["jet"](cam_big)[..., :3]
    overlay = (0.55 * base + 0.45 * heat).clip(0, 1)

    label = {c: float(p) for c, p in zip(CLASSES, probs)}
    return label, (overlay * 255).astype(np.uint8)

# ---------- metrics ----------
def metrics_markdown():
    p = ROOT / "metrics.json"
    if not p.exists():
        return "_metrics.json not found._"
    m = json.loads(p.read_text())
    cm = np.array(m["confusion_matrix"])
    tp = cm.diagonal()
    precision = tp / cm.sum(axis=0).clip(min=1)
    recall    = tp / cm.sum(axis=1).clip(min=1)
    f1        = 2 * precision * recall / (precision + recall + 1e-12)

    head = [f"**Test accuracy:** {m['test_accuracy']:.2%}",
            f"**Macro F1:** {m['macro_f1']:.3f}"]
    if m.get("best_val_accuracy"):
        head.append(f"**Best val accuracy:** {m['best_val_accuracy']:.2%}")
    if m.get("params"):
        head.append(f"**Parameters:** {m['params']:,}")

    out = ["  Β·  ".join(head), "",
           "| Class | Precision | Recall | F1 | Test images |",
           "|---|---|---|---|---|"]
    for i, c in enumerate(CLASSES):
        out.append(f"| {c} | {precision[i]:.3f} | {recall[i]:.3f} | {f1[i]:.3f} | {cm[i].sum()} |")

    # top confusions
    off = [(cm[i, j], CLASSES[i], CLASSES[j]) for i in range(len(CLASSES))
           for j in range(len(CLASSES)) if i != j]
    out += ["", "**Most confused (actual β†’ predicted):** " +
            ", ".join(f"{a} β†’ {b} ({n})" for n, a, b in sorted(off, reverse=True)[:4])]
    return "\n".join(out)

PLOTS = [
    ("10_curves.png",               "Training curves"),
    ("11_confusion_matrix.png",     "Confusion matrix"),
    ("12_per_class_accuracy.png",   "Per-class accuracy"),
    ("13_confidence.png",           "Confidence: correct vs wrong"),
    ("14_mistakes.png",             "Most confident mistakes"),
    ("15_gradcam.png",              "Grad-CAM per class"),
    ("16_gradcam_mistakes.png",     "Grad-CAM on mistakes"),
    ("08_feature_maps_after.png",   "Feature maps after training"),
    ("09_filters_after.png",        "First-layer filters after training"),
    ("04_augmentation.png",         "Augmentation"),
    ("01_samples.png",              "Dataset samples"),
    ("02_class_distribution.png",   "Class distribution"),
]
gallery_items = [(str(ROOT / "images" / f), cap) for f, cap in PLOTS if (ROOT / "images" / f).exists()]
examples = [[str(p)] for p in sorted((ROOT / "examples").glob("*.jpg"))] if (ROOT / "examples").exists() else []

# ---------- UI ----------
with gr.Blocks(title="Intel Scene CNN", theme=gr.themes.Soft()) as demo:
    gr.Markdown(
        "# Intel Scene Classifier: a CNN built from scratch\n"
        "Upload a photo of a **building, forest, glacier, mountain, sea or street**. "
        "The CNN predicts the scene, and Grad-CAM shows which pixels drove the decision. "
        "No pretrained backbone: 4 conv blocks trained from scratch on ~14k images."
    )
    with gr.Row():
        inp = gr.Image(type="pil", label="Upload a scene", height=384)
        with gr.Column():
            out_label = gr.Label(num_top_classes=6, label="Prediction")
            out_cam = gr.Image(label="Grad-CAM (red = most important)", height=384)
    inp.change(predict, inputs=inp, outputs=[out_label, out_cam])
    if examples:
        gr.Examples(examples=examples, inputs=inp, cache_examples=False)

    gr.Markdown("> Note: the model always picks one of the 6 classes, even for a photo of a cat.")

    gr.Markdown("## Test-set metrics (seg_test, 3,000 held-out images)")
    gr.Markdown(metrics_markdown())

    if gallery_items:
        gr.Markdown("## Training and evaluation plots")
        gr.Gallery(value=gallery_items, columns=2, height="auto", object_fit="contain")

    gr.Markdown(
        "## Architecture\n"
        "`[Conv3x3-BN-ReLU] x2 β†’ MaxPool β†’ Dropout2d` Γ— 4 blocks "
        f"(channels {meta['base']}β†’{meta['base']*2}β†’{meta['base']*4}β†’{meta['base']*8}) "
        "β†’ Global Avg Pool β†’ Dropout β†’ FC 128 β†’ ReLU β†’ FC 6"
    )

demo.launch(show_api=False, ssr_mode=False)