File size: 10,289 Bytes
74e3c01 | 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 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | """COCO val2017 mAP eval for split-tower detection heads on frozen EUPE-ViT-B.
Default output is hard NMS (the standard reported in the FCOS / YOLO / DETR
literature). Pass `--soft-nms` to additionally report a soft-NMS comparison
pass on the same forward outputs.
Auto-detects architecture flags from the checkpoint's state_dict:
hidden width, n_std / n_dw tower depths, per_scale_bias, reg_on_raw.
Usage:
python eval_coco_map.py --picker <ckpt.pth>
python eval_coco_map.py --picker <ckpt.pth> --soft-nms
"""
import argparse
import os
import re
import sys
import time
os.environ.setdefault("COCO_TEXT_EMBED_PATH", "/mnt/d/detection-heads/_diag_text_embed_vitb32.pt")
import torch
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from train_split_tower_5scale import SplitTowerHead, make_locations, STRIDES
DEVICE = "cuda"
COCO_ROOT = os.environ["ARENA_COCO_ROOT"]
VAL_CACHE = os.environ["ARENA_VAL_CACHE"]
RESOLUTION = 640
H = RESOLUTION // 16
NUM_CLASSES = 80
SCORE_THRESH = 0.05
MAX_PER_IMAGE = 100
def hard_nms_per_class(boxes, scores, labels, iou_thresh=0.5):
"""Per-class hard NMS via torchvision.ops.nms. Returns indices into input."""
from torchvision.ops import nms
keep = []
for c in labels.unique():
m = labels == c
idx = m.nonzero(as_tuple=True)[0]
k = nms(boxes[m], scores[m], iou_thresh)
keep.append(idx[k])
return torch.cat(keep) if keep else torch.tensor([], dtype=torch.long, device=boxes.device)
def soft_nms_linear(boxes, scores, iou_thresh=0.5, min_score=0.001):
"""GPU-vectorized linear-decay soft NMS (Bodla et al. 2017).
Iteratively picks the max-score box, multiplicatively decays overlapping
scores by (1 - IoU), repeats until the max falls below min_score."""
if boxes.numel() == 0:
return torch.tensor([], dtype=torch.long, device=boxes.device), scores.clone()
N = boxes.shape[0]
scores = scores.clone()
areas = (boxes[:, 2] - boxes[:, 0]).clamp(min=0) * (boxes[:, 3] - boxes[:, 1]).clamp(min=0)
active = torch.ones(N, dtype=torch.bool, device=boxes.device)
kept_idx, kept_scores = [], []
while active.any():
masked = torch.where(active, scores, torch.full_like(scores, -float("inf")))
i = int(masked.argmax())
if scores[i] < min_score:
break
kept_idx.append(i); kept_scores.append(scores[i].item())
active[i] = False
box_i = boxes[i]
x1 = torch.maximum(box_i[0], boxes[:, 0]); y1 = torch.maximum(box_i[1], boxes[:, 1])
x2 = torch.minimum(box_i[2], boxes[:, 2]); y2 = torch.minimum(box_i[3], boxes[:, 3])
inter = (x2 - x1).clamp(min=0) * (y2 - y1).clamp(min=0)
iou = inter / (areas[i] + areas - inter).clamp(min=1e-6)
decay = torch.where(iou > iou_thresh, 1 - iou, torch.ones_like(iou))
scores = scores * decay
if not kept_idx:
return (torch.tensor([], dtype=torch.long, device=boxes.device),
torch.tensor([], device=boxes.device))
return (torch.tensor(kept_idx, dtype=torch.long, device=boxes.device),
torch.tensor(kept_scores, device=boxes.device))
def soft_nms_per_class(boxes, scores, labels, iou_thresh=0.5):
keep_idx, keep_scores = [], []
for c in labels.unique():
m = labels == c
idx = m.nonzero(as_tuple=True)[0]
k, rescored = soft_nms_linear(boxes[m], scores[m], iou_thresh)
keep_idx.append(idx[k]); keep_scores.append(rescored)
if not keep_idx:
return (torch.tensor([], dtype=torch.long, device=boxes.device),
torch.tensor([], device=boxes.device))
return torch.cat(keep_idx), torch.cat(keep_scores)
def autodetect_and_load(ckpt_path):
ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=False)
sd = ckpt["head"] if isinstance(ckpt, dict) and "head" in ckpt else ckpt
per_scale_bias = (sd["cls_bias"].shape == (5, 80))
reg_on_raw = any("reg_stem" in k for k in sd.keys())
hidden = sd["stem.weight"].shape[0]
tower_idxs = sorted({int(m.group(1)) for k in sd if (m := re.match(r"cls_tower\.(\d+)\.", k))})
n_std = sum(1 for i in tower_idxs if f"cls_tower.{i}.conv.weight" in sd)
n_dw = len(tower_idxs) - n_std
return sd, hidden, n_std, n_dw, per_scale_bias, reg_on_raw
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--picker", required=True, help="Path to picker .pth checkpoint")
parser.add_argument("--soft-nms", action="store_true",
help="Also run a soft-NMS pass and report both. Default is hard NMS only.")
args = parser.parse_args()
t_start = time.time()
print(f"[{time.time()-t_start:6.1f}s] START pid={os.getpid()}", flush=True)
print(f"[{time.time()-t_start:6.1f}s] picker={args.picker} soft_nms={args.soft_nms}", flush=True)
print(f"[{time.time()-t_start:6.1f}s] Inspecting checkpoint ...", flush=True)
sd, hidden, n_std, n_dw, per_scale_bias, reg_on_raw = autodetect_and_load(args.picker)
print(f"[{time.time()-t_start:6.1f}s] Arch: hidden={hidden} n_std={n_std} n_dw={n_dw} "
f"per_scale_bias={per_scale_bias} reg_on_raw={reg_on_raw}", flush=True)
print(f"[{time.time()-t_start:6.1f}s] Constructing head ...", flush=True)
head = SplitTowerHead(hidden=hidden, n_std_layers=n_std, n_dw_layers=n_dw, n_scales=4,
reg_on_raw=reg_on_raw, per_scale_bias=per_scale_bias).to(DEVICE)
head.load_state_dict(sd); head.eval()
print(f"[{time.time()-t_start:6.1f}s] Head loaded: {sum(p.numel() for p in head.parameters()):,} params", flush=True)
print(f"[{time.time()-t_start:6.1f}s] Loading val cache ...", flush=True)
val = torch.load(VAL_CACHE, map_location="cpu", weights_only=False)
from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval
coco_gt = COCO(os.path.join(COCO_ROOT, "annotations", "instances_val2017.json"))
cat_ids = sorted(coco_gt.getCatIds())
idx_to_cat = {i: c for i, c in enumerate(cat_ids)}
print(f"[{time.time()-t_start:6.1f}s] Val: {len(val)} items, {len(cat_ids)} categories", flush=True)
feat_sizes = [(H*2, H*2), (H, H), (H//2, H//2), (H//4, H//4), (H//8, H//8)]
all_locs = torch.cat(make_locations(feat_sizes, STRIDES, torch.device(DEVICE)))
print(f"[{time.time()-t_start:6.1f}s] ==================== INFERENCE START ====================", flush=True)
results_hard = []
results_soft = [] if args.soft_nms else None
t0 = time.time()
PRINT_EVERY = 500
with torch.no_grad():
for idx, item in enumerate(val):
spatial = item["spatial"].unsqueeze(0).float().to(DEVICE)
img_id = int(item["img_id"]); scale = item["scale"]
cls_l, reg_l, ctr_l = head(spatial)
cls_s = torch.cat([c.permute(0,2,3,1).reshape(-1, NUM_CLASSES) for c in cls_l]).sigmoid()
reg_s = torch.cat([r.permute(0,2,3,1).reshape(-1, 4) for r in reg_l])
ctr_s = torch.cat([c.permute(0,2,3,1).reshape(-1) for c in ctr_l]).sigmoid()
scores_all = cls_s * ctr_s.unsqueeze(1)
mask = scores_all > SCORE_THRESH
if not mask.any():
if (idx+1) % PRINT_EVERY == 0:
elapsed = time.time()-t0; rate = (idx+1)/elapsed
print(f"[{time.time()-t_start:6.1f}s] img {idx+1}/{len(val)} | "
f"hard={len(results_hard):,}"
+ (f" soft={len(results_soft):,}" if args.soft_nms else "")
+ f" | {rate:.1f} img/s | ETA {(len(val)-idx-1)/rate:.0f}s", flush=True)
continue
loc_idx, cls_idx = mask.nonzero(as_tuple=True)
sc = scores_all[loc_idx, cls_idx]
xy = all_locs[loc_idx]; rr = reg_s[loc_idx]
x1 = (xy[:,0]-rr[:,0]).clamp(0, RESOLUTION); y1 = (xy[:,1]-rr[:,1]).clamp(0, RESOLUTION)
x2 = (xy[:,0]+rr[:,2]).clamp(0, RESOLUTION); y2 = (xy[:,1]+rr[:,3]).clamp(0, RESOLUTION)
boxes = torch.stack([x1, y1, x2, y2], dim=-1)
keep_hard = hard_nms_per_class(boxes, sc, cls_idx, iou_thresh=0.5)
_append_dets(results_hard, boxes, sc[keep_hard], keep_hard, cls_idx, scale, idx_to_cat, img_id)
if args.soft_nms:
keep_soft, sc_soft = soft_nms_per_class(boxes, sc, cls_idx, iou_thresh=0.5)
_append_dets(results_soft, boxes, sc_soft, keep_soft, cls_idx, scale, idx_to_cat, img_id)
if (idx+1) % PRINT_EVERY == 0:
elapsed = time.time()-t0; rate = (idx+1)/elapsed
msg = (f"[{time.time()-t_start:6.1f}s] img {idx+1}/{len(val)} | "
f"hard={len(results_hard):,}")
if args.soft_nms:
msg += f" soft={len(results_soft):,}"
msg += f" | n_pos={mask.sum().item()} | {rate:.1f} img/s | ETA {(len(val)-idx-1)/rate:.0f}s"
print(msg, flush=True)
print(f"\n[{time.time()-t_start:6.1f}s] Inference done in {time.time()-t0:.0f}s", flush=True)
passes = [("HARD NMS", results_hard)]
if args.soft_nms:
passes.append(("SOFT NMS", results_soft))
for label, results in passes:
print(f"\n{'='*40}\n{label}\n{'='*40}", flush=True)
coco_dt = coco_gt.loadRes(results)
ev = COCOeval(coco_gt, coco_dt, "bbox")
ev.params.imgIds = sorted(coco_gt.getImgIds())[:len(val)]
ev.evaluate(); ev.accumulate(); ev.summarize()
def _append_dets(sink, boxes, scores_kept, keep, cls_idx, scale, idx_to_cat, img_id):
if keep.numel() == 0:
return
if scores_kept.numel() > MAX_PER_IMAGE:
top = scores_kept.topk(MAX_PER_IMAGE)
scores_kept = top.values; keep = keep[top.indices]
bx = boxes[keep] / scale
w = (bx[:,2]-bx[:,0]).clamp(min=0); h = (bx[:,3]-bx[:,1]).clamp(min=0)
for i in range(keep.numel()):
s = scores_kept[i].item()
if s < SCORE_THRESH:
continue
sink.append({"image_id": img_id,
"category_id": idx_to_cat[cls_idx[keep[i]].item()],
"bbox": [bx[i,0].item(), bx[i,1].item(), w[i].item(), h[i].item()],
"score": s})
if __name__ == "__main__":
main()
|