Source code for otx.api.entities.shapes.polygon

"""This module implements the Polygon Shape entity."""

# Copyright (C) 2021-2022 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
#

# Conflict with Isort
# pylint: disable=wrong-import-order

import datetime
import warnings
from operator import attrgetter
from typing import List, Optional
import numpy as np

from shapely.geometry import Polygon as shapely_polygon

from otx.api.entities.shapes.rectangle import Rectangle
from otx.api.entities.shapes.shape import Shape, ShapeType
from otx.api.utils.time_utils import now


[docs] class Point: """This class defines a Point with an X and Y coordinate. Multiple points can be used to represent a Polygon """ __slots__ = ["x", "y"] def __init__(self, x: float, y: float): self.x = x self.y = y def __repr__(self): """String representation of the point.""" return f"Point({self.x}, {self.y})" def __eq__(self, other): """Checks if two points have the same x and y coordinates.""" if isinstance(other, Point): return self.x == other.x and self.y == other.y return False
[docs] def normalize_wrt_roi(self, roi_shape: Rectangle) -> "Point": """The inverse of denormalize_wrt_roi_shape. Transforming Polygon from the `roi` coordinate system to the normalized coordinate system. This is used when the tasks want to save the analysis results. For example in Detection -> Segmentation pipeline, the analysis results of segmentation needs to be normalized to the roi (bounding boxes) coming from the detection. Args: roi_shape (Point): the shape of the roi """ roi_shape = roi_shape.clip_to_visible_region() width = roi_shape.width height = roi_shape.height x1 = roi_shape.x1 y1 = roi_shape.y1 return Point(x=self.x * width + x1, y=self.y * height + y1)
[docs] def denormalize_wrt_roi_shape(self, roi_shape: Rectangle) -> "Point": """The inverse of normalize_wrt_roi_shape. Transforming Polygon from the normalized coordinate system to the `roi` coordinate system. This is used to pull ground truth during training process of the tasks. Examples given in the Shape implementations. Args: roi_shape (Rectangle): the shape of the roi """ roi_shape = roi_shape.clip_to_visible_region() return Point( x=(self.x - roi_shape.x1) / roi_shape.width, y=(self.y - roi_shape.y1) / roi_shape.height, )
[docs] class Polygon(Shape): """Represents a polygon formed by a list of coordinates. NB Freehand drawings are also stored as polygons. Args: points: list of Point's forming the polygon modification_date: last modified date """ # pylint: disable=too-many-arguments; Requires refactor def __init__( self, points: List[Point], modification_date: Optional[datetime.datetime] = None, ): modification_date = now() if modification_date is None else modification_date super().__init__( shape_type=ShapeType.POLYGON, modification_date=modification_date, ) if len(points) == 0: raise ValueError("Cannot create polygon with no points") self.points = points self.min_x = min(points, key=attrgetter("x")).x self.max_x = max(points, key=attrgetter("x")).x self.min_y = min(points, key=attrgetter("y")).y self.max_y = max(points, key=attrgetter("y")).y is_valid = True for (x, y) in [(self.min_x, self.min_y), (self.max_x, self.max_y)]: is_valid = is_valid and self._validate_coordinates(x, y) if not is_valid: points_str = "; ".join(str(p) for p in self.points) warnings.warn( f"{type(self).__name__} coordinates are invalid : {points_str}", UserWarning, ) def __repr__(self): """String representation of the polygon.""" return ( f"Polygon(len(points)={len(self.points)}," f" min_x={self.min_x}, max_x={self.max_x}, min_y={self.min_y}, max_y={self.max_y})" ) def __eq__(self, other): """Compares if the polygon has the same points and modification date.""" if isinstance(other, Polygon): return self.points == other.points and self.modification_date == other.modification_date return False def __hash__(self): """Returns hash of the Polygon object.""" return hash(str(self))
[docs] def normalize_wrt_roi_shape(self, roi_shape: Rectangle) -> "Polygon": """Transforms from the `roi` coordinate system to the normalized coordinate system. This function is the inverse of ``denormalize_wrt_roi_shape``. Example: Assume we have Polygon `p1` which lives in the top-right quarter of a 2D space. The 2D space where `p1` lives in is an `roi` living in the top-left quarter of the normalized coordinate space. This function returns Polygon `p1` expressed in the normalized coordinate space. >>> from otx.api.entities.annotation import Annotation >>> from otx.api.entities.shapes.rectangle import Rectangle >>> p1 = Polygon(points=[Point(x=0.5, y=0.0), Point(x=0.75, y=0.2), Point(x=0.6, y=0.1)]) >>> roi = Rectangle(x1=0.0, x2=0.5, y1=0.0, y2=0.5) >>> normalized = p1.normalize_wrt_roi_shape(roi_shape) >>> normalized Polygon(, len(points)=3) Args: roi_shape: Region of Interest Returns: New polygon in the image coordinate system """ if not isinstance(roi_shape, Rectangle): raise ValueError("roi_shape has to be a Rectangle.") roi_shape = roi_shape.clip_to_visible_region() points = [p.normalize_wrt_roi(roi_shape) for p in self.points] return Polygon(points=points)
[docs] def denormalize_wrt_roi_shape(self, roi_shape: Rectangle) -> "Polygon": """Transforming shape from the normalized coordinate system to the `roi` coordinate system. This function is the inverse of ``normalize_wrt_roi_shape`` Example: Assume we have Polygon `p1` which lives in the top-right quarter of the normalized coordinate space. The `roi` is a rectangle living in the half right of the normalized coordinate space. This function returns Polygon `p1` expressed in the coordinate space of `roi`. (should return top-half) Polygon denormalized to a rectangle as ROI >>> from otx.api.entities.shapes.rectangle import Rectangle >>> from otx.api.entities.annotation import Annotation >>> p1 = Polygon(points=[Point(x=0.5, y=0.0), Point(x=0.75, y=0.2), Point(x=0.6, y=0.1)]) >>> roi = Rectangle(x1=0.5, x2=1.0, y1=0.0, y2=1.0) # the half-right >>> normalized = p1.denormalize_wrt_roi_shape(roi_shape) >>> normalized Polygon(, len(points)=3) Args: roi_shape: Region of Interest Returns: New polygon in the ROI coordinate system """ if not isinstance(roi_shape, Rectangle): raise ValueError("roi_shape has to be a Rectangle.") roi_shape = roi_shape.clip_to_visible_region() points = [p.denormalize_wrt_roi_shape(roi_shape) for p in self.points] return Polygon(points=points)
def _as_shapely_polygon(self) -> shapely_polygon: """Returns the Polygon object as a shapely polygon which is used for calculating intersection between shapes.""" return shapely_polygon([(point.x, point.y) for point in self.points])
[docs] def get_area(self) -> float: """Returns the approximate area of the shape. Area is a value between 0 and 1, computed by converting the Polygon to a shapely polygon and reading the `.area` property. NOTE: This method should not be relied on for exact area computation. The area is approximate, because shapes are continuous, but pixels are discrete. Example: >>> Polygon(points=[Point(x=0.0, y=0.5), Point(x=0.5, y=0.5), Point(x=0.75, y=0.75)]).get_area() 0.0625 Returns: area of the shape """ return self._as_shapely_polygon().area