nayana-ocr-env / server /layout.py
AdithyaSK's picture
AdithyaSK HF Staff
Serve complete Nayana corpus 867c992ae910 from mounted bucket
e4278be verified
Raw History Blame Contribute Delete
3.6 kB
"""Class-aware maximum-cardinality matching; mean F1 over IoU .50:.05:.95."""
import json
import math
from ..data.schema import LAYOUT_LABELS
POLICY = "layout-mean-f1-iou-50-95-v1"
def parse_regions(text, width=None, height=None):
if len(text) > 65536:
raise ValueError("Layout answer exceeds 65536 characters")
def unique_object(pairs):
if len(dict(pairs)) != len(pairs):
raise ValueError("Duplicate JSON keys")
return dict(pairs)
value = json.loads(text, object_pairs_hook=unique_object)
if not isinstance(value, list) or len(value) > 512:
raise ValueError("Expected at most 512 regions")
for region in value:
if not isinstance(region, dict) or set(region) != {"label", "bbox"}:
raise ValueError("Each region needs exactly label and bbox")
if region["label"] not in LAYOUT_LABELS:
raise ValueError("Unknown layout label")
box = region["bbox"]
if (
not isinstance(box, list)
or len(box) != 4
or not all(type(v) in (int, float) and math.isfinite(v) for v in box)
):
raise ValueError("Expected four finite numeric coordinates")
x0, y0, x1, y1 = box
if not (0 <= x0 < x1 and 0 <= y0 < y1):
raise ValueError("Invalid box bounds")
if width is not None and (x1 > width or y1 > height):
raise ValueError("Box outside image")
return value
def iou(a, b):
intersection = max(0, min(a[2], b[2]) - max(a[0], b[0])) * max(
0, min(a[3], b[3]) - max(a[1], b[1])
)
union = (a[2] - a[0]) * (a[3] - a[1]) + (b[2] - b[0]) * (b[3] - b[1]) - intersection
return intersection / union
def matched_count(overlaps, threshold):
"""Augmenting paths find the maximum number of one-to-one qualifying pairs."""
matches = {}
def visit(predicted, seen):
for target, overlap in enumerate(overlaps[predicted]):
if overlap < threshold or target in seen:
continue
seen.add(target)
if target not in matches or visit(matches[target], seen):
matches[target] = predicted
return True
return False
return sum(visit(p, set()) for p in range(len(overlaps)))
def score_layout(prediction, reference, width=None, height=None):
targets = parse_regions(reference, width, height)
if not targets:
raise ValueError("Layout reference is empty")
try:
predicted = parse_regions(prediction, width, height)
except (ValueError, TypeError, RecursionError):
return 0.0, {"valid_format": False, "mean_f1": 0.0, "exact_match": False}
overlaps = [
[
iou(p["bbox"], t["bbox"]) if p["label"] == t["label"] else 0.0
for t in targets
]
for p in predicted
]
matches = [matched_count(overlaps, (50 + 5 * i) / 100) for i in range(10)]
denominator = len(predicted) + len(targets)
reward = sum(2 * count / denominator for count in matches) / 10
return reward, {
"valid_format": True,
"exact_match": sorted((r["label"], *r["bbox"]) for r in predicted)
== sorted((r["label"], *r["bbox"]) for r in targets),
"mean_f1": reward,
"precision_at_50": matches[0] / len(predicted) if predicted else 0.0,
"recall_at_50": matches[0] / len(targets),
"f1_at_50": 2 * matches[0] / denominator,
"f1_at_75": 2 * matches[5] / denominator,
"predicted_regions": len(predicted),
"reference_regions": len(targets),
}