Spaces:
Sleeping
Sleeping
File size: 3,473 Bytes
41fae03 | 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 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | """Geometry primitives and context for the canonical model.
Convention (non-negotiable, see AGENTS.md):
- bbox: [x, y, width, height] — x = left edge, y = top edge
- coordinate origin: top_left
- unit: px
- polygon: list of (x, y) pairs, or None
"""
from __future__ import annotations
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field, model_validator
from src.app.domain.models.status import CoordinateOrigin, GeometryStatus, Unit
# -- Primitives --------------------------------------------------------------
class Point(BaseModel):
"""A 2D point in image space."""
model_config = ConfigDict(frozen=True)
x: float = Field(ge=0)
y: float = Field(ge=0)
class BBox(BaseModel):
"""Axis-aligned bounding box: [x, y, width, height].
x, y = top-left corner. width, height > 0.
"""
model_config = ConfigDict(frozen=True)
x: float = Field(ge=0)
y: float = Field(ge=0)
width: float = Field(gt=0)
height: float = Field(gt=0)
@property
def x2(self) -> float:
return self.x + self.width
@property
def y2(self) -> float:
return self.y + self.height
def as_tuple(self) -> tuple[float, float, float, float]:
return (self.x, self.y, self.width, self.height)
PolygonPoints = list[tuple[float, float]]
"""Ordered list of (x, y) vertex pairs forming a closed polygon."""
class Baseline(BaseModel):
"""A baseline defined by two endpoints."""
model_config = ConfigDict(frozen=True)
start: Point
end: Point
class ClipRect(BaseModel):
"""Clipping rectangle — same semantics as BBox but used for clipping ops."""
model_config = ConfigDict(frozen=True)
x: float = Field(ge=0)
y: float = Field(ge=0)
width: float = Field(gt=0)
height: float = Field(gt=0)
# -- Geometry (the canonical geometry object on every node) ------------------
class Geometry(BaseModel):
"""Standardised geometry attached to every canonical node.
bbox is always required (at least for ALTO export).
polygon is optional — preserved when the provider supplies it.
status indicates how the geometry was obtained.
"""
model_config = ConfigDict(frozen=True)
bbox: Annotated[
tuple[float, float, float, float],
Field(description="(x, y, width, height)"),
]
polygon: PolygonPoints | None = None
status: GeometryStatus
@model_validator(mode="after")
def _validate_bbox_dimensions(self) -> Geometry:
_x, _y, w, h = self.bbox
if w <= 0 or h <= 0:
raise ValueError(f"bbox width and height must be > 0, got w={w}, h={h}")
if _x < 0 or _y < 0:
raise ValueError(f"bbox x and y must be >= 0, got x={_x}, y={_y}")
return self
# -- GeometryContext (describes the coordinate space of a provider output) ---
class GeometryContext(BaseModel):
"""Describes the geometric context of a provider's output.
Used by adapters to normalise provider coordinates into canonical space.
"""
model_config = ConfigDict(frozen=True)
source_width: int = Field(gt=0)
source_height: int = Field(gt=0)
provided_width: int | None = Field(default=None, gt=0)
provided_height: int | None = Field(default=None, gt=0)
resize_factor: float | None = Field(default=None, gt=0)
rotation: float = 0.0
coordinate_origin: CoordinateOrigin = CoordinateOrigin.TOP_LEFT
unit: Unit = Unit.PX
|