FastAPI service wrapping the Surya-OCR-2 model (datalab-to) served through vLLM: legacy /v1/api/ai/* endpoints, an OpenAI-compatible /v1/chat/completions endpoint, a coalescing request batcher, a local OCR CLI, Docker packaging, multilingual example outputs, and quantization/concurrency benchmarks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
67 lines
2.3 KiB
Python
67 lines
2.3 KiB
Python
"""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,
|
|
}
|