Module datatap.geometry
This module provides geometric primitives for storing or manipulating ML annotations.
Generally speaking, a geometric object is considered "valid" in the droplet format when it lies entirely within the unit
plane. This is because annotations in the droplet format are scaled to 0-to-1 along both axes so that they are
resolution-independent. This can be checked by invoking assert_valid
on any of the geometric objects (though this is
done automatically when geometric constructs are used to create droplets).
Expand source code
"""
This module provides geometric primitives for storing or manipulating ML annotations.
Generally speaking, a geometric object is considered "valid" in the droplet format when it lies entirely within the unit
plane. This is because annotations in the droplet format are scaled to 0-to-1 along both axes so that they are
resolution-independent. This can be checked by invoking `assert_valid` on any of the geometric objects (though this is
done automatically when geometric constructs are used to create droplets).
"""
from .mask import Mask, MaskJson
from .point import Point, PointJson
from .polygon import Polygon, PolygonJson
from .rectangle import Rectangle, RectangleJson
__all__ = [
"Mask",
"MaskJson",
"Point",
"PointJson",
"Polygon",
"PolygonJson",
"Rectangle",
"RectangleJson"
]
Sub-modules
datatap.geometry.mask
datatap.geometry.point
datatap.geometry.polygon
datatap.geometry.rectangle
Classes
class Mask (polygons: Sequence[Polygon])
-
The shape resulting from XORing a set of polygons in 2D space.
Generally, the expectation is that the polygons have no edge itersections; specifically, that for any pair of polygons in the mask, either they have no intersection or one completely contains the other. However, there is no assertion that this is the case, and generally speaking, the even-odd rule is used to determine if a particular point is contained by the mask.
Expand source code
class Mask: """ The shape resulting from XORing a set of polygons in 2D space. Generally, the expectation is that the polygons have no edge itersections; specifically, that for any pair of polygons in the mask, either they have no intersection or one completely contains the other. However, there is no assertion that this is the case, and generally speaking, the even-odd rule is used to determine if a particular point is contained by the mask. """ polygons: Sequence[Polygon] """ The constituent polygons of this `Mask`. """ @staticmethod def from_json(json: MaskJson) -> Mask: """ Creates a `Mask` from a `MaskJson`. """ return Mask([Polygon.from_json(poly) for poly in json]) def __init__(self, polygons: Sequence[Polygon]): self.polygons = polygons if len(self.polygons) < 1: raise ValueError(f"A mask must have at least one polygon; failed on mask {repr(self)}") def scale(self, factor: Union[float, int, Tuple[float, float], Point]) -> Mask: """ Resizes the mask according to `factor`. The scaling factor can either be a scalar (`int` or `float`), in which case the mask will be scaled by the same factor on both axes, or a point-like (`Tuple[float, float]` or `Point`), in which case the mask will be scaled independently on each axis. """ return Mask([p.scale(factor) for p in self.polygons]) def to_json(self) -> MaskJson: """ Serializes this object as a `MaskJson`. """ return [polygon.to_json() for polygon in self.polygons] def assert_valid(self) -> None: """ Asserts that this mask is valid on the unit plane. """ for polygon in self.polygons: polygon.assert_valid() # TODO(mdsavage): check for invalid polygon intersections? def __repr__(self) -> str: return basic_repr("Mask", self.polygons) def __eq__(self, other: object) -> bool: # TODO(mdsavage): currently, this requires the polygons to be in the same order, not just represent the same mask if not isinstance(other, Mask): return NotImplemented return self.polygons == other.polygons def __iter__(self) -> Generator[Polygon, None, None]: yield from self.polygons
Class variables
var polygons : Sequence[Polygon]
-
The constituent polygons of this
Mask
.
Static methods
def from_json(json: MaskJson) ‑> Mask
-
Creates a
Mask
from aMaskJson
.Expand source code
@staticmethod def from_json(json: MaskJson) -> Mask: """ Creates a `Mask` from a `MaskJson`. """ return Mask([Polygon.from_json(poly) for poly in json])
Methods
def assert_valid(self) ‑> None
-
Asserts that this mask is valid on the unit plane.
Expand source code
def assert_valid(self) -> None: """ Asserts that this mask is valid on the unit plane. """ for polygon in self.polygons: polygon.assert_valid() # TODO(mdsavage): check for invalid polygon intersections?
def scale(self, factor: Union[float, int, Tuple[float, float], Point]) ‑> Mask
-
Resizes the mask according to
factor
. The scaling factor can either be a scalar (int
orfloat
), in which case the mask will be scaled by the same factor on both axes, or a point-like (Tuple[float, float]
orPoint
), in which case the mask will be scaled independently on each axis.Expand source code
def scale(self, factor: Union[float, int, Tuple[float, float], Point]) -> Mask: """ Resizes the mask according to `factor`. The scaling factor can either be a scalar (`int` or `float`), in which case the mask will be scaled by the same factor on both axes, or a point-like (`Tuple[float, float]` or `Point`), in which case the mask will be scaled independently on each axis. """ return Mask([p.scale(factor) for p in self.polygons])
def to_json(self) ‑> Sequence[Sequence[Tuple[float, float]]]
-
Serializes this object as a
MaskJson
.Expand source code
def to_json(self) -> MaskJson: """ Serializes this object as a `MaskJson`. """ return [polygon.to_json() for polygon in self.polygons]
class Point (x: float, y: float, clip: bool = False)
-
A point in 2D space. Also often used to represent a 2D vector.
Expand source code
class Point: """ A point in 2D space. Also often used to represent a 2D vector. """ x: float """ The x-coordinate of the point. """ y: float """ The y-coordinate of the point. """ @staticmethod def from_json(json: PointJson) -> Point: """ Creates a `Point` from a `PointJson`. """ return Point(json[0], json[1]) def __init__(self, x: float, y: float, clip: bool = False): self.x = min(max(x, 0), 1) if clip else x self.y = min(max(y, 0), 1) if clip else y def to_json(self) -> PointJson: """ Serializes this object as a `PointJson`. """ return (self.x, self.y) def distance(self, other: Point) -> float: """ Computes the scalar distance to another point. """ return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5 def assert_valid(self) -> None: """ Asserts that this polygon is valid on the unit plane. """ assert 0 <= self.x <= 1 and 0 <= self.y <= 1, f"Point coordinates must be between 0 and 1; failed on point {repr(self)}" def clip(self) -> Point: """ Clips both coordinates of this point to the range [0, 1]. """ return Point(self.x, self.y, clip = True) def scale(self, factor: Union[float, int, Tuple[float, float], Point]) -> Point: """ Resizes the point according to `factor`. The scaling factor can either be a scalar (`int` or `float`), in which case the point will be scaled by the same factor on both axes, or a point-like (`Tuple[float, float]` or `Point`), in which case the point will be scaled independently on each axis. """ if isinstance(factor, (float, int)): return self * factor if isinstance(factor, tuple): return Point(self.x * factor[0], self.y * factor[1]) return Point(self.x * factor.x, self.x * factor.y) def __add__(self, o: Point) -> Point: if isinstance(o, Point): # type: ignore - pyright complains about the isinstance check being redundant return Point(self.x + o.x, self.y + o.y) return NotImplemented def __sub__(self, o: Point) -> Point: if isinstance(o, Point): # type: ignore - pyright complains about the isinstance check being redundant return Point(self.x - o.x, self.y - o.y) return NotImplemented def __mul__(self, o: Union[int, float]) -> Point: if isinstance(o, (int, float)): # type: ignore - pyright complains about the isinstance check being redundant return Point(self.x * o, self.y * o) return NotImplemented def __truediv__(self, o: Union[int, float]) -> Point: if isinstance(o, (int, float)): # type: ignore - pyright complains about the isinstance check being redundant return Point(self.x / o, self.y / o) return NotImplemented def __repr__(self) -> str: return basic_repr("Point", self.x, self.y) def __hash__(self) -> int: return hash((self.x, self.y)) def __eq__(self, other: object) -> bool: if isinstance(other, Point): return self.x == other.x and self.y == other.y return NotImplemented
Class variables
var x : float
-
The x-coordinate of the point.
var y : float
-
The y-coordinate of the point.
Static methods
def from_json(json: PointJson) ‑> Point
-
Creates a
Point
from aPointJson
.Expand source code
@staticmethod def from_json(json: PointJson) -> Point: """ Creates a `Point` from a `PointJson`. """ return Point(json[0], json[1])
Methods
def assert_valid(self) ‑> None
-
Asserts that this polygon is valid on the unit plane.
Expand source code
def assert_valid(self) -> None: """ Asserts that this polygon is valid on the unit plane. """ assert 0 <= self.x <= 1 and 0 <= self.y <= 1, f"Point coordinates must be between 0 and 1; failed on point {repr(self)}"
def clip(self) ‑> Point
-
Clips both coordinates of this point to the range [0, 1].
Expand source code
def clip(self) -> Point: """ Clips both coordinates of this point to the range [0, 1]. """ return Point(self.x, self.y, clip = True)
def distance(self, other: Point) ‑> float
-
Computes the scalar distance to another point.
Expand source code
def distance(self, other: Point) -> float: """ Computes the scalar distance to another point. """ return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
def scale(self, factor: Union[float, int, Tuple[float, float], Point]) ‑> Point
-
Resizes the point according to
factor
. The scaling factor can either be a scalar (int
orfloat
), in which case the point will be scaled by the same factor on both axes, or a point-like (Tuple[float, float]
orPoint
), in which case the point will be scaled independently on each axis.Expand source code
def scale(self, factor: Union[float, int, Tuple[float, float], Point]) -> Point: """ Resizes the point according to `factor`. The scaling factor can either be a scalar (`int` or `float`), in which case the point will be scaled by the same factor on both axes, or a point-like (`Tuple[float, float]` or `Point`), in which case the point will be scaled independently on each axis. """ if isinstance(factor, (float, int)): return self * factor if isinstance(factor, tuple): return Point(self.x * factor[0], self.y * factor[1]) return Point(self.x * factor.x, self.x * factor.y)
def to_json(self) ‑> Tuple[float, float]
-
Serializes this object as a
PointJson
.Expand source code
def to_json(self) -> PointJson: """ Serializes this object as a `PointJson`. """ return (self.x, self.y)
class Polygon (points: Sequence[Point])
-
A polygon in 2D space.
Expand source code
class Polygon: """ A polygon in 2D space. """ points: Sequence[Point] """ The vertices of this polygon. """ @staticmethod def from_json(json: PolygonJson) -> Polygon: """ Creates a `Polygon` from a `PolygonJson`. """ return Polygon([Point.from_json(pt) for pt in json]) def __init__(self, points: Sequence[Point]): self.points = points if len(self.points) < 3: raise ValueError(f"A polygon must have at least three points; failed on polygon {repr(self)}") def scale(self, factor: Union[float, int, Tuple[float, float], Point]) -> Polygon: """ Resizes the polygon according to `factor`. The scaling factor can either be a scalar (`int` or `float`), in which case the polygon will be scaled by the same factor on both axes, or a point-like (`Tuple[float, float]` or `Point`), in which case the polygon will be scaled independently on each axis. """ return Polygon([p.scale(factor) for p in self.points]) def to_json(self) -> PolygonJson: """ Serializes this object as a `PolygonJson`. """ return [point.to_json() for point in self.points] def assert_valid(self) -> None: """ Ensures that this polygon is valid on the unit plane. """ for point in self.points: point.assert_valid() # TODO(mdsavage): check for self-intersection? def __repr__(self) -> str: return basic_repr("Polygon", self.points) def __eq__(self, other: object) -> bool: # TODO(mdsavage): currently, this requires the points to be in the same order, not just represent the same polygon if not isinstance(other, Polygon): return NotImplemented return self.points == other.points def __mul__(self, o: Union[int, float]) -> Polygon: if not isinstance(o, (int, float)): # type: ignore - pyright complains about the isinstance check being redundant return NotImplemented return Polygon([p * o for p in self.points]) def __iter__(self) -> Generator[Point, None, None]: yield from self.points
Class variables
var points : Sequence[Point]
-
The vertices of this polygon.
Static methods
def from_json(json: PolygonJson) ‑> Polygon
-
Creates a
Polygon
from aPolygonJson
.Expand source code
@staticmethod def from_json(json: PolygonJson) -> Polygon: """ Creates a `Polygon` from a `PolygonJson`. """ return Polygon([Point.from_json(pt) for pt in json])
Methods
def assert_valid(self) ‑> None
-
Ensures that this polygon is valid on the unit plane.
Expand source code
def assert_valid(self) -> None: """ Ensures that this polygon is valid on the unit plane. """ for point in self.points: point.assert_valid() # TODO(mdsavage): check for self-intersection?
def scale(self, factor: Union[float, int, Tuple[float, float], Point]) ‑> Polygon
-
Resizes the polygon according to
factor
. The scaling factor can either be a scalar (int
orfloat
), in which case the polygon will be scaled by the same factor on both axes, or a point-like (Tuple[float, float]
orPoint
), in which case the polygon will be scaled independently on each axis.Expand source code
def scale(self, factor: Union[float, int, Tuple[float, float], Point]) -> Polygon: """ Resizes the polygon according to `factor`. The scaling factor can either be a scalar (`int` or `float`), in which case the polygon will be scaled by the same factor on both axes, or a point-like (`Tuple[float, float]` or `Point`), in which case the polygon will be scaled independently on each axis. """ return Polygon([p.scale(factor) for p in self.points])
def to_json(self) ‑> Sequence[Tuple[float, float]]
-
Serializes this object as a
PolygonJson
.Expand source code
def to_json(self) -> PolygonJson: """ Serializes this object as a `PolygonJson`. """ return [point.to_json() for point in self.points]
class Rectangle (p1: Point, p2: Point, normalize: bool = False)
-
An axis-aligned rectangle in 2D space.
Expand source code
class Rectangle: """ An axis-aligned rectangle in 2D space. """ p1: Point """ The top-left corner of the rectangle. """ p2: Point """ The bottom-right corner of the rectangle. """ @staticmethod def from_json(json: RectangleJson) -> Rectangle: """ Creates a `Rectangle` from a `RectangleJson`. """ return Rectangle(Point.from_json(json[0]), Point.from_json(json[1])) @staticmethod def from_point_set(points: Sequence[Point]) -> Rectangle: """ Creates the bounding rectangle of a set of points. Note, it is possible for this to create an invalid rectangle if all points are colinear and axis-aligned. """ return Rectangle( Point(min(p.x for p in points), min(p.y for p in points)), Point(max(p.x for p in points), max(p.y for p in points)) ) def __init__(self, p1: Point, p2: Point, normalize: bool = False): if normalize: self.p1 = Point(min(p1.x, p2.x), min(p1.y, p2.y)) self.p2 = Point(max(p1.x, p2.x), max(p1.y, p2.y)) else: self.p1 = p1 self.p2 = p2 def assert_valid(self) -> None: """ Ensures that this rectangle is valid on the unit plane. """ self.p1.assert_valid() self.p2.assert_valid() assert self.p1.x < self.p2.x and self.p1.y < self.p2.y, f"Rectangle has non-positive area; failed on rectangle {repr(self)}" def to_json(self) -> RectangleJson: """ Serializes this object as a `RectangleJson`. """ return (self.p1.to_json(), self.p2.to_json()) def to_shapely(self) -> ShapelyPolygon: """ Converts this rectangle into a `Shapely.Polygon`. """ return box(self.p1.x, self.p1.y, self.p2.x, self.p2.y) def to_xywh_tuple(self) -> Tuple[float, float, float, float]: """ Converts this rectangle into a tuple of `(x_coordinate, y_coordinate, width, height)`. """ w = self.p2.x - self.p1.x h = self.p2.y - self.p1.y return (self.p1.x, self.p1.y, w, h) def to_xyxy_tuple(self) -> Tuple[float, float, float, float]: """ Converts this rectangle into a tuple of `(x_min, y_min, x_max, y_max)`. """ return (self.p1.x, self.p1.y, self.p2.x, self.p2.y) def area(self) -> float: """ Computes the area of this rectangle. """ return abs(self.p1.x - self.p2.x) * abs(self.p1.y - self.p2.y) def iou(self, other: Rectangle) -> float: """ Computes the iou (intersection-over-union) of this rectangle with another. """ x1 = max(self.p1.x, other.p1.x) y1 = max(self.p1.y, other.p1.y) x2 = min(self.p2.x, other.p2.x) y2 = min(self.p2.y, other.p2.y) intersection_area = max(x2 - x1, 0) * max(y2 - y1, 0) union_area = self.area() + other.area() - intersection_area return intersection_area / union_area def diagonal(self) -> float: """ Computes the diagonal length of this rectangle. """ return self.p1.distance(self.p2) def scale(self, factor: Union[float, int, Tuple[float, float], Point]): """ Resizes the rectangle according to `factor`. The scaling factor can either be a scalar (`int` or `float`), in which case the rectangle will be scaled by the same factor on both axes, or a point-like (`Tuple[float, float]` or `Point`), in which case the rectangle will be scaled independently on each axis. """ return Rectangle(self.p1.scale(factor), self.p2.scale(factor)) def center(self) -> Point: """ Computes the center of this rectangle. """ return Point((self.p1.x + self.p2.x) / 2, (self.p1.y + self.p2.y) / 2) def scale_from_center(self, factor: Union[float, int, Tuple[float, float], Point]) -> Rectangle: """ Resizes the rectangle according to `factor`, though translates it so that its center does not move. The scaling factor can either be a scalar (`int` or `float`), in which case the rectangle will be scaled by the same factor on both axes, or a point-like (`Tuple[float, float]` or `Point`), in which case the rectangle will be scaled independently on each axis. """ center = self.center() return Rectangle( (self.p1 - center).scale(factor) + center, (self.p2 - center).scale(factor) + center ) def clip(self) -> Rectangle: """ Clips the rectangle the unit-plane. """ return Rectangle(self.p1.clip(), self.p2.clip()) def normalize(self) -> Rectangle: """ Returns a new rectangle that is guaranteed to have `p1` be the top left corner and `p2` be the bottom right corner. """ return Rectangle(self.p1, self.p2, True) def __repr__(self) -> str: return basic_repr("Rectangle", self.p1, self.p2) def __hash__(self) -> int: return hash((self.p1, self.p2)) def __eq__(self, other: object) -> bool: if not isinstance(other, Rectangle): return NotImplemented return self.p1 == other.p1 and self.p2 == other.p2 def __mul__(self, o: Union[int, float]) -> Rectangle: if isinstance(o, (int, float)): # type: ignore - pyright complains about the isinstance check being redundant return Rectangle(self.p1 * o, self.p2 * o) return NotImplemented
Class variables
var p1 : Point
-
The top-left corner of the rectangle.
var p2 : Point
-
The bottom-right corner of the rectangle.
Static methods
def from_json(json: RectangleJson) ‑> Rectangle
-
Creates a
Rectangle
from aRectangleJson
.Expand source code
@staticmethod def from_json(json: RectangleJson) -> Rectangle: """ Creates a `Rectangle` from a `RectangleJson`. """ return Rectangle(Point.from_json(json[0]), Point.from_json(json[1]))
def from_point_set(points: Sequence[Point]) ‑> Rectangle
-
Creates the bounding rectangle of a set of points.
Note, it is possible for this to create an invalid rectangle if all points are colinear and axis-aligned.
Expand source code
@staticmethod def from_point_set(points: Sequence[Point]) -> Rectangle: """ Creates the bounding rectangle of a set of points. Note, it is possible for this to create an invalid rectangle if all points are colinear and axis-aligned. """ return Rectangle( Point(min(p.x for p in points), min(p.y for p in points)), Point(max(p.x for p in points), max(p.y for p in points)) )
Methods
def area(self) ‑> float
-
Computes the area of this rectangle.
Expand source code
def area(self) -> float: """ Computes the area of this rectangle. """ return abs(self.p1.x - self.p2.x) * abs(self.p1.y - self.p2.y)
def assert_valid(self) ‑> None
-
Ensures that this rectangle is valid on the unit plane.
Expand source code
def assert_valid(self) -> None: """ Ensures that this rectangle is valid on the unit plane. """ self.p1.assert_valid() self.p2.assert_valid() assert self.p1.x < self.p2.x and self.p1.y < self.p2.y, f"Rectangle has non-positive area; failed on rectangle {repr(self)}"
def center(self) ‑> Point
-
Computes the center of this rectangle.
Expand source code
def center(self) -> Point: """ Computes the center of this rectangle. """ return Point((self.p1.x + self.p2.x) / 2, (self.p1.y + self.p2.y) / 2)
def clip(self) ‑> Rectangle
-
Clips the rectangle the unit-plane.
Expand source code
def clip(self) -> Rectangle: """ Clips the rectangle the unit-plane. """ return Rectangle(self.p1.clip(), self.p2.clip())
def diagonal(self) ‑> float
-
Computes the diagonal length of this rectangle.
Expand source code
def diagonal(self) -> float: """ Computes the diagonal length of this rectangle. """ return self.p1.distance(self.p2)
def iou(self, other: Rectangle) ‑> float
-
Computes the iou (intersection-over-union) of this rectangle with another.
Expand source code
def iou(self, other: Rectangle) -> float: """ Computes the iou (intersection-over-union) of this rectangle with another. """ x1 = max(self.p1.x, other.p1.x) y1 = max(self.p1.y, other.p1.y) x2 = min(self.p2.x, other.p2.x) y2 = min(self.p2.y, other.p2.y) intersection_area = max(x2 - x1, 0) * max(y2 - y1, 0) union_area = self.area() + other.area() - intersection_area return intersection_area / union_area
def normalize(self) ‑> Rectangle
-
Returns a new rectangle that is guaranteed to have
p1
be the top left corner andp2
be the bottom right corner.Expand source code
def normalize(self) -> Rectangle: """ Returns a new rectangle that is guaranteed to have `p1` be the top left corner and `p2` be the bottom right corner. """ return Rectangle(self.p1, self.p2, True)
def scale(self, factor: Union[float, int, Tuple[float, float], Point])
-
Resizes the rectangle according to
factor
. The scaling factor can either be a scalar (int
orfloat
), in which case the rectangle will be scaled by the same factor on both axes, or a point-like (Tuple[float, float]
orPoint
), in which case the rectangle will be scaled independently on each axis.Expand source code
def scale(self, factor: Union[float, int, Tuple[float, float], Point]): """ Resizes the rectangle according to `factor`. The scaling factor can either be a scalar (`int` or `float`), in which case the rectangle will be scaled by the same factor on both axes, or a point-like (`Tuple[float, float]` or `Point`), in which case the rectangle will be scaled independently on each axis. """ return Rectangle(self.p1.scale(factor), self.p2.scale(factor))
def scale_from_center(self, factor: Union[float, int, Tuple[float, float], Point]) ‑> Rectangle
-
Resizes the rectangle according to
factor
, though translates it so that its center does not move. The scaling factor can either be a scalar (int
orfloat
), in which case the rectangle will be scaled by the same factor on both axes, or a point-like (Tuple[float, float]
orPoint
), in which case the rectangle will be scaled independently on each axis.Expand source code
def scale_from_center(self, factor: Union[float, int, Tuple[float, float], Point]) -> Rectangle: """ Resizes the rectangle according to `factor`, though translates it so that its center does not move. The scaling factor can either be a scalar (`int` or `float`), in which case the rectangle will be scaled by the same factor on both axes, or a point-like (`Tuple[float, float]` or `Point`), in which case the rectangle will be scaled independently on each axis. """ center = self.center() return Rectangle( (self.p1 - center).scale(factor) + center, (self.p2 - center).scale(factor) + center )
def to_json(self) ‑> Tuple[Tuple[float, float], Tuple[float, float]]
-
Serializes this object as a
RectangleJson
.Expand source code
def to_json(self) -> RectangleJson: """ Serializes this object as a `RectangleJson`. """ return (self.p1.to_json(), self.p2.to_json())
def to_shapely(self) ‑> shapely.geometry.polygon.Polygon
-
Converts this rectangle into a
Shapely.Polygon
.Expand source code
def to_shapely(self) -> ShapelyPolygon: """ Converts this rectangle into a `Shapely.Polygon`. """ return box(self.p1.x, self.p1.y, self.p2.x, self.p2.y)
def to_xywh_tuple(self) ‑> Tuple[float, float, float, float]
-
Converts this rectangle into a tuple of
(x_coordinate, y_coordinate, width, height)
.Expand source code
def to_xywh_tuple(self) -> Tuple[float, float, float, float]: """ Converts this rectangle into a tuple of `(x_coordinate, y_coordinate, width, height)`. """ w = self.p2.x - self.p1.x h = self.p2.y - self.p1.y return (self.p1.x, self.p1.y, w, h)
def to_xyxy_tuple(self) ‑> Tuple[float, float, float, float]
-
Converts this rectangle into a tuple of
(x_min, y_min, x_max, y_max)
.Expand source code
def to_xyxy_tuple(self) -> Tuple[float, float, float, float]: """ Converts this rectangle into a tuple of `(x_min, y_min, x_max, y_max)`. """ return (self.p1.x, self.p1.y, self.p2.x, self.p2.y)