"""Greedy bbox IoU matching between reference and candidate page boxes.""" from __future__ import annotations import json from pathlib import Path from typing import Dict, List, Sequence Box = Sequence[float] def iou(a: Box, b: Box) -> float: ax0, ay0, ax1, ay1 = a bx0, by0, bx1, by1 = b ix0, iy0 = max(ax0, bx0), max(ay0, by0) ix1, iy1 = min(ax1, bx1), min(ay1, by1) iw, ih = max(0.0, ix1 - ix0), max(0.0, iy1 - iy0) inter = iw * ih if inter <= 0: return 0.0 area_a = max(0.0, ax1 - ax0) * max(0.0, ay1 - ay0) area_b = max(0.0, bx1 - bx0) * max(0.0, by1 - by0) union = area_a + area_b - inter return inter / union if union > 0 else 0.0 def match_boxes(ref: List[Box], cand: List[Box], iou_threshold: float = 0.5) -> Dict[str, float]: used = [False] * len(cand) matched_ious: List[float] = [] for r in ref: best_j, best_iou = -1, 0.0 for j, c in enumerate(cand): if used[j]: continue cur = iou(r, c) if cur > best_iou: best_iou, best_j = cur, j if best_j >= 0 and best_iou >= iou_threshold: used[best_j] = True matched_ious.append(best_iou) matched = len(matched_ious) return { "matched": matched, "missed": len(ref) - matched, "extra": len(cand) - matched, "mean_matched_iou": sum(matched_ious) / matched if matched else 0.0, } def bbox_iou_over_dirs(ref_dir: Path, cand_dir: Path, iou_threshold: float = 0.5) -> Dict: per_file: Dict[str, Dict[str, float]] = {} for ref_path in sorted(ref_dir.glob("*.json")): ref_boxes = json.loads(ref_path.read_text(encoding="utf-8")).get("boxes", []) cand_path = cand_dir / ref_path.name cand_boxes = ( json.loads(cand_path.read_text(encoding="utf-8")).get("boxes", []) if cand_path.exists() else [] ) per_file[ref_path.name] = match_boxes(ref_boxes, cand_boxes, iou_threshold) n = len(per_file) or 1 return { "mean_bbox_iou": sum(v["mean_matched_iou"] for v in per_file.values()) / n, "mean_missed_lines": sum(v["missed"] for v in per_file.values()) / n, "mean_extra_lines": sum(v["extra"] for v in per_file.values()) / n, "per_file": per_file, }