Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """ | |
| active_sampler.py | |
| Selects uncertainty-based samples for (re-)labeling. | |
| Input: val_predictions.json (or any predictions with fields path, boxes[{xyxy,cls,conf}]) | |
| Output: selection.json (list of samples with image path and rationale) | |
| Heuristic: | |
| uncertainty = (1 - conf_calibrated) * size_factor | |
| size_factor = 1.0 for medium/large boxes, >1.0 for very small | |
| Quota: | |
| max_per_class = K, global_max = N | |
| """ | |
| import argparse, json, math, sys | |
| from pathlib import Path | |
| from typing import Any, Dict, List, Tuple | |
| import numpy as np | |
| def read_json(p: Path): | |
| return json.loads(p.read_text(encoding="utf-8")) | |
| def write_json(p: Path, obj: Any): | |
| p.parent.mkdir(parents=True, exist_ok=True) | |
| p.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8") | |
| def temp_scale(p: float, T: float | None) -> float: | |
| if not T or T <= 0: | |
| return p | |
| p = min(max(p, 1e-8), 1 - 1e-8) | |
| z = math.log(p) - math.log(1 - p) | |
| return 1.0 / (1.0 + math.exp(-z / T)) | |
| def box_area_xyxy(box: List[float]) -> float: | |
| x1,y1,x2,y2 = box | |
| return max(0.0, x2-x1) * max(0.0, y2-y1) | |
| def sample_candidates(preds: List[Dict[str,Any]], | |
| names: List[str], | |
| temperature: float | None, | |
| small_area_px: float, | |
| max_per_class: int, | |
| global_max: int) -> List[Dict[str,Any]]: | |
| # Collect an uncertainty score per box | |
| candidates = [] | |
| for r in preds: | |
| path = r.get("path") | |
| h, w = r.get("orig_shape", [None, None]) | |
| boxes = r.get("boxes") or [] | |
| for b in boxes: | |
| cls = int(b["cls"]) | |
| conf = float(b["conf"]) | |
| conf_cal = temp_scale(conf, temperature) | |
| unc = 1.0 - conf_cal | |
| # weight small boxes more | |
| area = box_area_xyxy(b["xyxy"]) | |
| size_factor = 1.5 if area < small_area_px else 1.0 | |
| score = unc * size_factor | |
| candidates.append({ | |
| "path": path, | |
| "cls": cls, | |
| "label": names[cls] if 0 <= cls < len(names) else str(cls), | |
| "conf": conf, | |
| "conf_cal": conf_cal, | |
| "uncertainty": score, | |
| "area": area | |
| }) | |
| # sort by uncertainty descending | |
| candidates.sort(key=lambda x: x["uncertainty"], reverse=True) | |
| # Quotas | |
| picked = [] | |
| per_class = {i:0 for i in range(len(names))} | |
| for c in candidates: | |
| if len(picked) >= global_max: | |
| break | |
| ci = c["cls"] | |
| if per_class.get(ci, 0) >= max_per_class: | |
| continue | |
| picked.append(c) | |
| per_class[ci] = per_class.get(ci, 0) + 1 | |
| return picked | |
| def main(): | |
| ap = argparse.ArgumentParser(description="Active Learning Sampler") | |
| ap.add_argument("--predictions", required=True, type=Path, help="Path to val_predictions.json or similar JSON") | |
| ap.add_argument("--names", required=True, type=Path, help="names.json (list of classes)") | |
| ap.add_argument("--calibration", type=Path, default=None, help="calibration_temp.json (optional)") | |
| ap.add_argument("--out", required=True, type=Path, help="Output selection.json") | |
| ap.add_argument("--small_area_px", type=float, default=32*32, help="Threshold for 'small' boxes (px^2)") | |
| ap.add_argument("--max_per_class", type=int, default=50) | |
| ap.add_argument("--global_max", type=int, default=300) | |
| args = ap.parse_args() | |
| preds = read_json(args.predictions) | |
| names = read_json(args.names) | |
| T = None | |
| if args.calibration and args.calibration.exists(): | |
| try: | |
| T = float(read_json(args.calibration).get("temperature", None)) | |
| except Exception: | |
| T = None | |
| picked = sample_candidates(preds, names, T, args.small_area_px, args.max_per_class, args.global_max) | |
| write_json(args.out, { | |
| "total": len(picked), | |
| "max_per_class": args.max_per_class, | |
| "global_max": args.global_max, | |
| "items": picked[:args.global_max] | |
| }) | |
| print(f"selection -> {args.out} (n={len(picked)})") | |
| if __name__ == "__main__": | |
| main() | |