carprint / eval /evaluate_identity.py
Azamaka's picture
CarPrint: API + site, T4 image
b2d14a6 verified
Raw History Blame Contribute Delete
6.82 kB
"""Identity verification evaluation (ISO/IEC 19795-style counting).
Manifest (JSON list): {"path", "car_id", "session", "model", "slot"}.
Genuine comparisons: same car_id, different session. Impostor comparisons: different car_id (pairs by the same
photographer of the same model are excluded as unknown ground truth unless they were visually verified as distinct
cars of one "distinct_group"), split into
"hard" (same model) and "other". Error rates are reported per comparison AND bounded per vehicle, with
exact Clopper-Pearson 95% upper bounds; when there are 0 errors the bound is ~3/N (rule of three).
"""
from __future__ import annotations
import hashlib
import itertools
import json
import sys
import time
from collections import defaultdict
from pathlib import Path
import numpy as np
from scipy.stats import beta
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from carprint.core.image_io import load_image # noqa: E402
from carprint.identity.cohort import bank_paths # noqa: E402
from carprint.identity.pipeline import SUBJECT_STATS, USE_SUBJECT, make_frame # noqa: E402
from carprint.identity.verify import verify # noqa: E402
def zone_record(e) -> dict:
"""Everything needed to re-decide a pair offline under other thresholds (eval/calibrate.py)."""
return {"ref": e.ref_frame, "nfa": round(e.zone.log10_nfa, 2), "nfa_d": round(e.log10_nfa_distinct, 2),
"inl": e.zone.n_inliers, "dinl": round(e.distinct_inliers, 2), "marks": e.ref_marks_in_zone,
"votes": [[int(cx), int(cy), round(v, 3)] for (cx, cy), v in e.votes.items()]}
def upper95(errors: int, n: int) -> float | None:
return None if n == 0 else float(beta.ppf(0.95, errors + 1, n - errors)) if errors < n else 1.0
def rate(errors: int, n: int) -> dict:
return {"errors": errors, "n": n, "rate": (errors / n) if n else None, "upper95": upper95(errors, n)}
def main(manifest_path: str, cohort_dir: str, out_path: str, max_impostor_per_stratum: int = 200):
manifest_bytes = Path(manifest_path).read_bytes()
items = json.loads(manifest_bytes)
base = Path(manifest_path).parent
sessions: dict[tuple, list] = defaultdict(list)
for it in items:
sessions[(it["car_id"], it["session"])].append(it)
frames = {}
t = time.perf_counter()
for key, its in sessions.items():
frames[key] = [make_frame(f"{key[0]}|{key[1]}|{i}", it.get("slot", "extra"),
load_image((base / it["path"]).read_bytes()).rgb) for i, it in enumerate(its)]
print(f"features for {len(items)} images: {time.perf_counter() - t:.1f}s", flush=True)
cohort_cache = {}
def cohort(model: str):
if model not in cohort_cache:
paths = bank_paths(Path(cohort_dir), model)
cohort_cache[model] = [make_frame(f"c{model}{i}", "extra", load_image(p.read_bytes()).rgb) for i, p in enumerate(paths)]
return cohort_cache[model]
model_of = {k: v[0]["model"] for k, v in sessions.items()}
author_of = {k: {w.lower() for w in v[0].get("author", "").replace(",", " ").split() if len(w) >= 4}
for k, v in sessions.items()}
group_of = {k: v[0].get("distinct_group") for k, v in sessions.items()}
keys = sorted(sessions)
genuine = [(a, b) for a, b in itertools.combinations(keys, 2) if a[0] == b[0]]
# Same photographer + same model is often the same car shot twice: unknown ground truth, excluded.
impostor = [(a, b) for a, b in itertools.combinations(keys, 2) if a[0] != b[0]
and (not (author_of[a] & author_of[b] and model_of[a] == model_of[b])
or (group_of[a] is not None and group_of[a] == group_of[b]))]
rng = np.random.default_rng(0)
strata = {"hard": [p for p in impostor if model_of[p[0]] == model_of[p[1]]],
"other": [p for p in impostor if model_of[p[0]] != model_of[p[1]]]}
for s in strata:
if len(strata[s]) > max_impostor_per_stratum:
idx = rng.choice(len(strata[s]), max_impostor_per_stratum, replace=False)
strata[s] = [strata[s][i] for i in idx]
records, lat = [], []
for label, pairs in [("genuine", genuine), ("hard", strata["hard"]), ("other", strata["other"])]:
for a, b in pairs:
t = time.perf_counter()
coh = cohort(model_of[a])
v = verify(frames[a], frames[b], {f.frame_id: coh for f in frames[a]})
lat.append((time.perf_counter() - t) * 1000)
records.append({"label": label, "ref": list(a), "cur": list(b), "decision": v.decision, "score": v.score_raw,
"cohort": v.cohort_size, "zones": [zone_record(e) for e in v.zones]})
def summarize(label):
rs = [r for r in records if r["label"] == label]
dec = defaultdict(int)
for r in rs:
dec[r["decision"]] += 1
return rs, dict(dec)
out = {"test_manifest_sha256": hashlib.sha256(manifest_bytes).hexdigest(), "generated_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
"n_images": len(items), "n_cars": len({k[0] for k in keys}), "n_sessions": len(keys),
"vehicle_crop": USE_SUBJECT, "subject_stats": dict(SUBJECT_STATS), "identity": {}}
if USE_SUBJECT:
from carprint.identity.subject import detector_name
out["detector"] = detector_name()
g, gd = summarize("genuine")
out["identity"]["genuine"] = {"decisions": gd, "fnmr": rate(gd.get("DIFFERENT", 0), len(g)),
"review_rate": rate(gd.get("REVIEW", 0) + gd.get("RECAPTURE_REQUIRED", 0), len(g)),
"tar": rate(gd.get("SAME", 0), len(g))}
for s in ("hard", "other"):
rs, d = summarize(s)
cars = {c for r in rs for c in (r["ref"][0], r["cur"][0])}
fm_cars = {c for r in rs if r["decision"] == "SAME" for c in (r["ref"][0], r["cur"][0])}
out["identity"][f"impostor_{s}"] = {"decisions": d, "fmr": rate(d.get("SAME", 0), len(rs)),
"fmr_vehicle_bound": rate(len(fm_cars), len(cars)),
"review_rate": rate(d.get("REVIEW", 0) + d.get("RECAPTURE_REQUIRED", 0), len(rs))}
out["latency_ms"] = {"p50": float(np.percentile(lat, 50)) if lat else None,
"p95": float(np.percentile(lat, 95)) if lat else None, "n": len(lat)}
out["records"] = records
Path(out_path).write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
print(json.dumps({k: v for k, v in out.items() if k != "records"}, ensure_ascii=False, indent=1))
if __name__ == "__main__":
main(*sys.argv[1:4])