File size: 3,201 Bytes
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
"""Geometry normalization — convert provider formats to canonical format.

Providers return coordinates in various formats.  This module provides
explicit converters for each known convention.  The canonical format is
always (x, y, width, height) with origin at top_left, unit px.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from src.app.geometry.polygon import PolygonPoints, polygon_to_bbox

if TYPE_CHECKING:
    from src.app.geometry.bbox import BBoxTuple


def xyxy_to_xywh(xyxy: tuple[float, float, float, float]) -> BBoxTuple:
    """Convert (x1, y1, x2, y2) to canonical (x, y, width, height).

    Many providers (PaddleOCR rec_boxes, etc.) use this format.
    """
    x1, y1, x2, y2 = xyxy
    if x2 < x1:
        x1, x2 = x2, x1
    if y2 < y1:
        y1, y2 = y2, y1
    return (x1, y1, x2 - x1, y2 - y1)


def xywh_to_xyxy(bbox: BBoxTuple) -> tuple[float, float, float, float]:
    """Convert canonical (x, y, width, height) to (x1, y1, x2, y2)."""
    return (bbox[0], bbox[1], bbox[0] + bbox[2], bbox[1] + bbox[3])


def cxcywh_to_xywh(cxcywh: tuple[float, float, float, float]) -> BBoxTuple:
    """Convert center-based (cx, cy, width, height) to canonical (x, y, w, h)."""
    cx, cy, w, h = cxcywh
    return (cx - w / 2, cy - h / 2, w, h)


def xywh_to_cxcywh(bbox: BBoxTuple) -> tuple[float, float, float, float]:
    """Convert canonical (x, y, w, h) to center-based (cx, cy, w, h)."""
    return (bbox[0] + bbox[2] / 2, bbox[1] + bbox[3] / 2, bbox[2], bbox[3])


def four_point_to_xywh(points: list[list[float]] | list[tuple[float, float]]) -> BBoxTuple:
    """Convert a 4-point quadrilateral (e.g. PaddleOCR detection) to canonical bbox.

    PaddleOCR returns [[x1,y1],[x2,y2],[x3,y3],[x4,y4]] — 4 corners of
    a (possibly rotated) quadrilateral.  We compute the axis-aligned
    bounding box enclosing all 4 points.
    """
    if len(points) != 4:
        raise ValueError(f"Expected 4 points, got {len(points)}")
    polygon: PolygonPoints = [(float(p[0]), float(p[1])) for p in points]
    return polygon_to_bbox(polygon)


def four_point_to_polygon(points: list[list[float]] | list[tuple[float, float]]) -> PolygonPoints:
    """Convert a 4-point list to a PolygonPoints list."""
    if len(points) != 4:
        raise ValueError(f"Expected 4 points, got {len(points)}")
    return [(float(p[0]), float(p[1])) for p in points]


def normalize_bbox_to_pixels(
    bbox: BBoxTuple,
    image_width: int,
    image_height: int,
) -> BBoxTuple:
    """Convert a normalized [0,1] bbox to pixel coordinates."""
    return (
        bbox[0] * image_width,
        bbox[1] * image_height,
        bbox[2] * image_width,
        bbox[3] * image_height,
    )


def pixels_to_normalized_bbox(
    bbox: BBoxTuple,
    image_width: int,
    image_height: int,
) -> BBoxTuple:
    """Convert pixel bbox to normalized [0,1] coordinates."""
    if image_width <= 0 or image_height <= 0:
        raise ValueError(
            f"Image dimensions must be > 0, got {image_width}x{image_height}"
        )
    return (
        bbox[0] / image_width,
        bbox[1] / image_height,
        bbox[2] / image_width,
        bbox[3] / image_height,
    )