File size: 6,096 Bytes
1ea7ba6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
"""
End-to-end batch validator: run fusion inference on EVERY image in test/.

Proves the pipeline (load -> preprocess -> features -> model -> predict) is
robust to arbitrary inputs from disk β€” any extension, any mode, any size,
EXIF-rotated, etc. Reports per-class and overall accuracy.

Usage:
    python batch_test.py                       # uses test/ + best.pth fusion
    python batch_test.py --folder test --mode fusion
    python batch_test.py --folder /any/path --mode image --ckpt outputs/checkpoints/p1_best.pth
"""
import os, sys, time, argparse, traceback
from pathlib import Path
from collections import defaultdict

_base = "/mnt/d/SpiceNet" if os.path.exists("/mnt/d/SpiceNet") else "D:/SpiceNet"
sys.path.insert(0, _base)

import numpy as np
import torch
import torch.nn.functional as F

import config
from src import features
from src.model import load_checkpoint
from interactive_test import load_image_rgb, preprocess_image, SUPPORTED_EXTS


def collect_images(root: Path):
    """Walk root/<class>/*.<ext>. Class is the immediate parent folder name.
    If the folder has no class subdirs, treat all images as unlabeled."""
    by_class = defaultdict(list)
    unlabeled = []

    # case-insensitive lookup of class names
    cls_lookup = {c.lower(): c for c in config.CLASSES}

    for p in root.rglob("*"):
        if not p.is_file():
            continue
        if p.suffix.lower() not in SUPPORTED_EXTS:
            continue
        parent = p.parent.name.lower()
        if parent in cls_lookup:
            by_class[cls_lookup[parent]].append(p)
        else:
            unlabeled.append(p)
    return by_class, unlabeled


@torch.no_grad()
def predict_one(model, img_rgb, mode, device):
    x = preprocess_image(img_rgb).to(device)
    if mode == "fusion":
        tex_np, col_np = features.extract_all(img_rgb)
        tex = torch.from_numpy(tex_np).unsqueeze(0).to(device)
        col = torch.from_numpy(col_np).unsqueeze(0).to(device)
        logits, _ = model.forward_fusion(x, tex, col)
    else:
        logits = model.forward_image(x)
    probs = F.softmax(logits, dim=1)[0].cpu()
    return int(probs.argmax()), float(probs.max())


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--folder", default=str(Path(_base) / "test"))
    ap.add_argument("--mode",   default="fusion", choices=["fusion", "image"])
    ap.add_argument("--ckpt",   default=None,
                    help="checkpoint (default: best.pth for fusion, p1_best.pth for image)")
    ap.add_argument("--limit",  type=int, default=0,
                    help="cap images per class (0 = all)")
    args = ap.parse_args()

    folder = Path(args.folder)
    if not folder.exists():
        print(f"  [ERROR] folder not found: {folder}")
        sys.exit(1)

    if args.ckpt is None:
        args.ckpt = str(config.CHECKPOINT_DIR /
                        ("best.pth" if args.mode == "fusion" else "p1_best.pth"))

    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print(f"  Device     : {device}")
    print(f"  Mode       : {args.mode}")
    print(f"  Checkpoint : {args.ckpt}")
    print(f"  Folder     : {folder}")

    by_class, unlabeled = collect_images(folder)
    total = sum(len(v) for v in by_class.values()) + len(unlabeled)
    print(f"  Images     : {total} ({len(by_class)} labeled classes, "
          f"{len(unlabeled)} unlabeled)")
    if total == 0:
        print("  [ERROR] no images found")
        sys.exit(1)

    print(f"\n  Loading model...")
    t0 = time.time()
    model, _, _, _ = load_checkpoint(args.ckpt, device)
    model.eval()
    print(f"  Loaded in {(time.time()-t0):.1f}s")

    # ── Inference ─────────────────────────────────────────────────
    correct = 0
    seen = 0
    failed = []
    per_class = defaultdict(lambda: [0, 0])   # cls -> [correct, total]
    t0 = time.time()

    for cls_name, paths in sorted(by_class.items()):
        true_idx = config.CLASSES.index(cls_name)
        sample = paths if args.limit == 0 else paths[: args.limit]
        for p in sample:
            try:
                img = load_image_rgb(str(p))
                pred_idx, conf = predict_one(model, img, args.mode, device)
            except Exception as e:
                failed.append((p, f"{type(e).__name__}: {e}"))
                continue
            seen += 1
            ok = pred_idx == true_idx
            correct += ok
            per_class[cls_name][0] += ok
            per_class[cls_name][1] += 1

    # unlabeled: just confirm we can run inference without crashing
    for p in unlabeled[: args.limit or None]:
        try:
            img = load_image_rgb(str(p))
            predict_one(model, img, args.mode, device)
            seen += 1
        except Exception as e:
            failed.append((p, f"{type(e).__name__}: {e}"))

    dt = time.time() - t0

    # ── Report ────────────────────────────────────────────────────
    print(f"\n  {'='*60}")
    print(f"  Per-class accuracy:")
    print(f"  {'class':<18s} {'correct':>10s} {'total':>8s} {'acc':>8s}")
    print(f"  {'-'*46}")
    for cls in config.CLASSES:
        c, t = per_class[cls]
        if t == 0:
            continue
        print(f"  {cls:<18s} {c:>10d} {t:>8d} {c/t*100:>7.2f}%")
    print(f"  {'-'*46}")
    if seen > 0:
        labeled_seen = sum(t for _, t in per_class.values())
        if labeled_seen > 0:
            print(f"  {'OVERALL':<18s} {correct:>10d} {labeled_seen:>8d} "
                  f"{correct/labeled_seen*100:>7.2f}%")
        print(f"\n  Throughput : {seen/dt:.1f} img/s  ({dt:.1f}s total)")

    print(f"\n  Decode/inference failures: {len(failed)}")
    for p, err in failed[:10]:
        print(f"    - {p.name}: {err}")
    if len(failed) > 10:
        print(f"    ... and {len(failed) - 10} more")

    sys.exit(0 if len(failed) == 0 else 2)


if __name__ == "__main__":
    main()