Spaces:
Sleeping
Sleeping
File size: 3,153 Bytes
9f084b2 bbbfba8 9f084b2 bbbfba8 9f084b2 bbbfba8 9f084b2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | """Quantization — float→int conversion strategies and tolerance checks.
ALTO XML expects integer coordinates. This module provides explicit
strategies for converting float geometry to int, with configurable
rounding and tolerance for containment checks.
"""
from __future__ import annotations
import math
from enum import StrEnum
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from src.app.geometry.bbox import BBoxTuple
class RoundingStrategy(StrEnum):
"""How to round float coordinates to integers."""
ROUND = "round"
FLOOR = "floor"
CEIL = "ceil"
EXPAND = "expand"
def quantize_value(value: float, strategy: RoundingStrategy = RoundingStrategy.ROUND) -> int:
"""Convert a single float to int using the given strategy."""
if strategy == RoundingStrategy.ROUND:
return round(value)
elif strategy == RoundingStrategy.FLOOR:
return math.floor(value)
elif strategy == RoundingStrategy.CEIL:
return math.ceil(value)
elif strategy == RoundingStrategy.EXPAND:
return round(value)
raise ValueError(f"Unknown rounding strategy: {strategy}")
def quantize_bbox(
bbox: BBoxTuple,
strategy: RoundingStrategy = RoundingStrategy.ROUND,
) -> tuple[int, int, int, int]:
"""Convert a float bbox to integer coordinates.
The EXPAND strategy rounds x,y down and width,height up to ensure
the integer bbox fully contains the float bbox.
"""
x, y, w, h = bbox
if strategy == RoundingStrategy.EXPAND:
ix = math.floor(x)
iy = math.floor(y)
ix2 = math.ceil(x + w)
iy2 = math.ceil(y + h)
return (ix, iy, ix2 - ix, iy2 - iy)
else:
return (
quantize_value(x, strategy),
quantize_value(y, strategy),
max(1, quantize_value(w, strategy)),
max(1, quantize_value(h, strategy)),
)
def bbox_contains_with_tolerance(
outer: BBoxTuple,
inner: BBoxTuple,
tolerance: float = 5.0,
) -> bool:
"""Check if outer contains inner with configurable pixel tolerance.
This is the standard containment check used by the structural validator.
The tolerance allows the inner bbox to exceed the outer bbox by up to
*tolerance* pixels on each side.
"""
from src.app.geometry.bbox import contains
return contains(outer, inner, tolerance)
def compute_overflow(outer: BBoxTuple, inner: BBoxTuple) -> dict[str, float]:
"""Compute how many pixels inner overflows outer on each side.
Returns a dict with keys 'left', 'top', 'right', 'bottom'.
Positive values mean overflow, negative/zero means contained.
"""
from src.app.geometry.bbox import x2, y2
return {
"left": outer[0] - inner[0],
"top": outer[1] - inner[1],
"right": x2(inner) - x2(outer),
"bottom": y2(inner) - y2(outer),
}
def max_overflow(outer: BBoxTuple, inner: BBoxTuple) -> float:
"""Return the maximum overflow in pixels of inner beyond outer.
Returns 0.0 if inner is fully contained.
"""
overflows = compute_overflow(outer, inner)
return max(0.0, max(overflows.values()))
|