Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """Measure detected card aspect ratio and scale_confidence across an image dir. | |
| Two metrics are captured per image (both come from the SAM-detected card's | |
| 4 corner points after order_corners): | |
| aspect_ratio = max(width_px, height_px) / min(width_px, height_px) | |
| Compared to the true credit-card aspect 1.586. | |
| scale_confidence = 1 - |px_per_cm_w - px_per_cm_h| / max(...) | |
| Consistency between the two independent scale estimates | |
| you get from the W and H sides; falls below 1.0 whenever | |
| the detected aspect deviates from 1.586 (perspective tilt, | |
| SAM mask error on one edge, non-standard card). | |
| Runs the same SAM hand + SAM card pipeline as the web demo. Prints per-image | |
| values and distribution summaries (n, min, p05/10/25/50/75/90/95, max, | |
| mean, std, coarse histograms, success/failure counts). | |
| Usage: | |
| source .venv/bin/activate | |
| python3 script/card_aspect_and_scale_survey.py input/kol_success | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import logging | |
| import statistics as stats | |
| import sys | |
| import time | |
| from pathlib import Path | |
| from typing import List, Optional, Tuple | |
| import cv2 | |
| import numpy as np | |
| ROOT = Path(__file__).resolve().parents[1] | |
| sys.path.insert(0, str(ROOT)) | |
| from src.card_detection import compute_scale_factor # noqa: E402 | |
| from src.finger_segmentation import segment_hand # noqa: E402 | |
| from src.sam_card_detection import ( # noqa: E402 | |
| detect_credit_card_sam_prompt, | |
| suggest_card_seeds, | |
| ) | |
| IMG_EXTS = {".jpg", ".jpeg", ".png"} | |
| TRUE_ASPECT = 1.586 # ISO/IEC 7810 ID-1: 85.60 / 53.98 | |
| def _process_image(path: Path) -> Tuple[Optional[float], Optional[float], str]: | |
| """Return (aspect_ratio, scale_confidence, status) for a single image.""" | |
| img = cv2.imread(str(path)) | |
| if img is None: | |
| return None, None, "load_failed" | |
| try: | |
| hand_data = segment_hand(img) | |
| except Exception as e: | |
| return None, None, f"hand_error:{type(e).__name__}" | |
| if hand_data is None: | |
| return None, None, "hand_not_detected" | |
| canonical = hand_data.get("canonical_image", img) | |
| hand_mask = hand_data.get("mask") | |
| landmarks = hand_data.get("landmarks") | |
| if hand_mask is None or landmarks is None or len(landmarks) <= 9: | |
| return None, None, "no_landmarks" | |
| y_limit = int(round(landmarks[9, 1])) | |
| seed_info = suggest_card_seeds(hand_mask, canonical.shape[:2], y_limit) | |
| seeds = seed_info["kept"] | |
| if not seeds: | |
| return None, None, "no_seeds" | |
| palm_c = np.mean(landmarks[[0, 5, 9, 13, 17], :2], axis=0) | |
| negatives = [(int(round(palm_c[0])), int(round(palm_c[1])))] | |
| try: | |
| card = detect_credit_card_sam_prompt( | |
| canonical, | |
| seed_points=seeds, | |
| negative_points=negatives, | |
| hand_mask=hand_mask, | |
| ) | |
| except Exception as e: | |
| return None, None, f"card_error:{type(e).__name__}" | |
| if card is None: | |
| return None, None, "card_not_detected" | |
| aspect = float(card["aspect_ratio"]) | |
| _, scale_conf = compute_scale_factor(card["corners"]) | |
| return aspect, float(scale_conf), "ok" | |
| def _percentiles(values_sorted: List[float], p: float) -> float: | |
| n = len(values_sorted) | |
| if n == 1: | |
| return values_sorted[0] | |
| k = (n - 1) * p | |
| lo = int(k) | |
| hi = min(lo + 1, n - 1) | |
| frac = k - lo | |
| return values_sorted[lo] * (1 - frac) + values_sorted[hi] * frac | |
| def _describe(values: List[float], label: str, *, fmt: str = "{:.3f}", | |
| hist_lo: float = 0.0, hist_hi: float = 1.0, | |
| hist_bin: float = 0.05) -> None: | |
| if not values: | |
| print(f"\n{label}: no successful measurements.") | |
| return | |
| vs = sorted(values) | |
| n = len(vs) | |
| print(f"\n=== {label} (n={n}) ===") | |
| for tag, p in [("min", 0.0), ("p05", 0.05), ("p10", 0.10), ("p25", 0.25), | |
| ("median", 0.50), ("p75", 0.75), ("p90", 0.90), ("p95", 0.95), | |
| ("max", 1.0)]: | |
| v = vs[0] if p == 0 else vs[-1] if p == 1 else _percentiles(vs, p) | |
| print(f" {tag:6s} = " + fmt.format(v)) | |
| print(f" mean = " + fmt.format(stats.mean(vs))) | |
| if n > 1: | |
| print(f" std = " + fmt.format(stats.stdev(vs))) | |
| # Coarse histogram | |
| edges = [] | |
| e = hist_lo | |
| while e <= hist_hi + 1e-9: | |
| edges.append(round(e, 4)) | |
| e += hist_bin | |
| counts = [0] * (len(edges) - 1) | |
| under = over = 0 | |
| for v in vs: | |
| if v < edges[0]: | |
| under += 1 | |
| continue | |
| placed = False | |
| for i in range(len(edges) - 1): | |
| if edges[i] <= v < edges[i + 1]: | |
| counts[i] += 1 | |
| placed = True | |
| break | |
| if not placed: | |
| over += 1 | |
| print(" histogram:") | |
| if under: | |
| print(f" <{fmt.format(edges[0])} : {under}") | |
| for i, c in enumerate(counts): | |
| if c == 0: | |
| continue | |
| bar = "#" * c | |
| print(f" [{fmt.format(edges[i])},{fmt.format(edges[i+1])}) : {c:2d} {bar}") | |
| if over: | |
| print(f" >={fmt.format(edges[-1])} : {over}") | |
| def main() -> int: | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("image_dir", type=Path) | |
| ap.add_argument("--limit", type=int, default=None, | |
| help="optional cap on number of images processed") | |
| args = ap.parse_args() | |
| logging.getLogger().setLevel(logging.ERROR) | |
| if not args.image_dir.is_dir(): | |
| print(f"Not a directory: {args.image_dir}") | |
| return 1 | |
| images = sorted( | |
| p for p in args.image_dir.iterdir() if p.suffix.lower() in IMG_EXTS | |
| ) | |
| if args.limit: | |
| images = images[: args.limit] | |
| if not images: | |
| print(f"No images found in {args.image_dir}") | |
| return 1 | |
| print(f"Processing {len(images)} images from {args.image_dir}") | |
| print(f"{'#':>3} {'file':<60} {'aspect':>7} {'sc':>5} status") | |
| aspects: List[float] = [] | |
| confs: List[float] = [] | |
| deltas: List[float] = [] # |aspect - 1.586| / 1.586 | |
| status_counts = {} | |
| t_start = time.time() | |
| for i, path in enumerate(images, 1): | |
| t0 = time.time() | |
| aspect, sc, status = _process_image(path) | |
| dt = time.time() - t0 | |
| status_counts[status] = status_counts.get(status, 0) + 1 | |
| a_str = f"{aspect:.3f}" if aspect is not None else " - " | |
| c_str = f"{sc:.3f}" if sc is not None else " - " | |
| print(f"{i:>3} {path.name:<60} {a_str:>7} {c_str:>5} {status} ({dt:.1f}s)") | |
| if aspect is not None: | |
| aspects.append(aspect) | |
| deltas.append(abs(aspect - TRUE_ASPECT) / TRUE_ASPECT) | |
| if sc is not None: | |
| confs.append(sc) | |
| total_dt = time.time() - t_start | |
| print(f"\nElapsed: {total_dt:.1f}s ({total_dt / max(1, len(images)):.1f}s/img)") | |
| print(f"Status summary: {status_counts}") | |
| _describe(aspects, label="aspect_ratio (true = 1.586)", | |
| hist_lo=1.30, hist_hi=1.85, hist_bin=0.05) | |
| _describe(deltas, label="|aspect - 1.586| / 1.586 (current gate ≤ 0.150)", | |
| fmt="{:.4f}", hist_lo=0.0, hist_hi=0.20, hist_bin=0.02) | |
| _describe(confs, label="scale_confidence (current gate > 0.90)", | |
| hist_lo=0.80, hist_hi=1.00, hist_bin=0.02) | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |