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()
|