omnial / scripts /evaluate.py
erfanasgari21's picture
NaN in metrics
9dd49be
Raw History Blame Contribute Delete
4.65 kB
"""
evaluate.py
============
توابع ماژولار ارزیابی روی مجموعه تست (Image-AUROC, Pixel-AUROC, Pixel-AP).
"""
import os
import sys
import numpy as np
import torch
import torch.nn.functional as F
from tqdm.auto import tqdm
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
from data.data_loader import list_test_images, load_and_resize, load_mask
from utils.metrics import anomaly_score_from_seg, compute_image_auroc, compute_pixel_auroc, compute_pixel_ap
@torch.no_grad()
def evaluate_category(model, root: str, category: str, size, device: str, dataset_name: str = "mvtec",
smooth_kernel: int = 7, smooth_sigma: float = 1.5):
"""
Run model on one class test set. Returns image/pixel scores and labels.
"""
model.eval()
test_items = list_test_images(root, category, dataset_name)
image_scores, image_labels = [], []
pixel_scores, pixel_labels = [], []
for img_path, defect_type, mask_path in test_items:
img = load_and_resize(img_path, size)
img_t = torch.from_numpy(img.transpose(2, 0, 1)).float().unsqueeze(0).to(device)
# Eval in FP32 — AMP leftovers in weights can still NaN; sanitize scores below
with torch.amp.autocast("cuda", enabled=False):
out = model(img_t)
seg_map = anomaly_score_from_seg(
out["seg_logits"].float(), smooth_kernel=smooth_kernel, smooth_sigma=smooth_sigma
)[0].cpu().numpy() # (H,W)
seg_map = np.nan_to_num(seg_map, nan=0.0, posinf=1.0, neginf=0.0)
label = 0 if defect_type == "good" else 1
image_scores.append(float(np.max(seg_map)))
image_labels.append(label)
if mask_path is not None:
gt = load_mask(mask_path, size)
else:
gt = np.zeros(size, dtype=np.float32)
pixel_scores.append(seg_map)
pixel_labels.append(gt)
return {
"image_scores": np.asarray(image_scores, dtype=np.float64),
"image_labels": np.asarray(image_labels),
"pixel_scores": np.stack(pixel_scores, axis=0),
"pixel_labels": np.stack(pixel_labels, axis=0),
}
def evaluate_dataset(model, cfg: dict, dataset_name: str = "mvtec", device: str = "cuda",
verbose: bool = True):
"""
Full eval over all classes. Returns dict with per-category metrics + "average".
Set verbose=False to silence per-category prints (training loop uses this).
"""
if dataset_name == "mvtec":
root = cfg["data"]["mvtec_root"]
categories = cfg["data"]["mvtec_categories"]
size = tuple(cfg["data"]["mvtec_size"])
else:
root = cfg["data"]["visa_root"]
categories = cfg["data"]["visa_categories"]
size = tuple(cfg["data"]["visa_size"])
smooth_k = cfg["model"].get("anomaly_smooth_kernel", 7)
smooth_s = cfg["model"].get("anomaly_smooth_sigma", 1.5)
results = {}
cat_iter = categories if verbose else tqdm(categories, desc="eval", leave=False, dynamic_ncols=True)
for cat in cat_iter:
try:
res = evaluate_category(
model, root, cat, size, device, dataset_name,
smooth_kernel=smooth_k, smooth_sigma=smooth_s,
)
except Exception as e:
print(f"[WARN] {cat}: eval failed ({e}); skipping.")
continue
if len(np.unique(res["image_labels"])) < 2:
if verbose:
print(f"[WARN] {cat}: need both normal/anomaly labels for AUROC.")
continue
i_auroc = compute_image_auroc(res["image_scores"], res["image_labels"])
p_auroc = compute_pixel_auroc(res["pixel_scores"], res["pixel_labels"])
p_ap = compute_pixel_ap(res["pixel_scores"], res["pixel_labels"])
if not np.isfinite(i_auroc) or not np.isfinite(p_auroc):
print(f"[WARN] {cat}: non-finite metrics (I={i_auroc}, P={p_auroc}); skipping.")
continue
results[cat] = {"I-AUROC": i_auroc, "P-AUROC": p_auroc, "P-AP": p_ap}
if verbose:
print(f"{cat:15s} | I-AUROC={i_auroc:.3f} P-AUROC={p_auroc:.3f} P-AP={p_ap:.3f}")
if results:
avg = {k: float(np.mean([v[k] for v in results.values()])) for k in ["I-AUROC", "P-AUROC", "P-AP"]}
results["average"] = avg
if verbose:
print(f"{'AVERAGE':15s} | I-AUROC={avg['I-AUROC']:.3f} P-AUROC={avg['P-AUROC']:.3f} P-AP={avg['P-AP']:.3f}")
else:
results["average"] = {"I-AUROC": 0.0, "P-AUROC": 0.0, "P-AP": 0.0}
print("[WARN] No valid categories in eval — returning zero averages.")
return results