# coding: utf-8
from __future__ import annotations
import warnings
from copy import deepcopy
from math import ceil, floor
from typing import TYPE_CHECKING, Dict, List, Tuple
import numpy as np
from supervisely.sly_logger import logger
from supervisely.geometry.constants import (
ANY_SHAPE,
BITMAP,
CLASS_ID,
CREATED_AT,
EXTERIOR,
ID,
INTERIOR,
LABELER_LOGIN,
LOC,
NODES,
ORIGIN,
POINTS,
UPDATED_AT,
)
from supervisely.io.json import JsonSerializable
warnings.simplefilter(action="ignore", category=FutureWarning)
if not hasattr(np, "bool"):
np.bool = np.bool_
if TYPE_CHECKING:
from supervisely.geometry.rectangle import Rectangle
from supervisely.geometry.image_rotator import ImageRotator
# @TODO: use properties instead of field if it makes sense
[docs]
class Geometry(JsonSerializable):
"""Base class for all geometry classes."""
def __init__(
self,
sly_id=None,
class_id=None,
labeler_login=None,
updated_at=None,
created_at=None,
):
"""
:param sly_id: Geometry ID on Supervisely server.
:type sly_id: int, optional
:param class_id: ObjClass ID.
:type class_id: int, optional
:param labeler_login: Labeler user login.
:type labeler_login: str, optional
:param updated_at: Last update timestamp.
:type updated_at: int, optional
:param created_at: Creation timestamp.
:type created_at: int, optional
"""
self.sly_id = sly_id
self.labeler_login = labeler_login
self.updated_at = updated_at
self.created_at = created_at
self.class_id = class_id
def _add_creation_info(self, d):
"""Add creation information to the geometry."""
if self.labeler_login is not None:
d[LABELER_LOGIN] = self.labeler_login
if self.updated_at is not None:
d[UPDATED_AT] = self.updated_at
if self.created_at is not None:
d[CREATED_AT] = self.created_at
if self.sly_id is not None:
d[ID] = self.sly_id
if self.class_id is not None:
d[CLASS_ID] = self.class_id
def _copy_creation_info_inplace(self, g):
"""Copy creation information to the geometry."""
self.labeler_login = g.labeler_login
self.updated_at = g.updated_at
self.created_at = g.created_at
self.sly_id = g.sly_id
[docs]
@staticmethod
def geometry_name():
"""
Get the name of the geometry.
:returns: name of the geometry
:rtype: str
:returns: string with name of geometry
"""
raise NotImplementedError()
[docs]
@classmethod
def name(cls):
"""
Get the name of the geometry.
Same as :meth:`~supervisely.geometry.geometry.Geometry.geometry_name`, but shorter. In order to make the code more concise.
:returns: string with name of geometry
"""
return cls.geometry_name()
[docs]
def crop(self, rect: Rectangle) -> List[Geometry]:
"""
Crop the geometry with given rectangle.
:param rect: Rectangle for crop.
:type rect: :class:`~supervisely.geometry.rectangle.Rectangle`
:returns: List of Geometry after crop.
:rtype: List[:class:`~supervisely.geometry.geometry.Geometry`]`
"""
raise NotImplementedError()
[docs]
def relative_crop(self, rect):
"""Crops object like "crop" method, but return results with coordinates relative to rect
:param rect: Rectangle for crop.
:type rect: :class:`~supervisely.geometry.rectangle.Rectangle`
:returns: List of Geometry after relative crop.
:rtype: List[:class:`~supervisely.geometry.geometry.Geometry`]`
:raises NotImplementedError: if method is not implemented in subclass
"""
return [geom.translate(drow=-rect.top, dcol=-rect.left) for geom in self.crop(rect)]
[docs]
def rotate(self, rotator: ImageRotator) -> Geometry:
"""Rotates around image center -> New Geometry
:param rotator: Class for image rotation.
:type rotator: :class:`~supervisely.geometry.image_rotator.ImageRotator`
:returns: Geometry after rotation.
:rtype: :class:`~supervisely.geometry.geometry.Geometry`
"""
raise NotImplementedError()
[docs]
def resize(self, in_size: Tuple[int, int], out_size: Tuple[int, int]) -> Geometry:
"""
:param in_size: Input image size (height, width).
:type in_size: Tuple[int, int]
:param out_size: Desired output image size (height, width) of the Annotation to which Label belongs.
:type out_size: Tuple[int, int]
:returns: Geometry after resize.
:rtype: :class:`~supervisely.geometry.geometry.Geometry`
:raises NotImplementedError: if method is not implemented in subclass
"""
raise NotImplementedError()
[docs]
def scale(self, factor):
"""Scales around origin with a given factor.
:param: factor: Scale factor.
:type factor: float
:returns: :class:`~supervisely.geometry.geometry.Geometry`
:rtype: :class:`~supervisely.geometry.geometry.Geometry`
:raises NotImplementedError: if method is not implemented in subclass
"""
raise NotImplementedError()
[docs]
def translate(self, drow, dcol):
"""
:param drow: Vertical shift.
:type drow: int
:param dcol: Horizontal shift.
:type dcol: int
:returns: Geometry after translate.
:rtype: :class:`~supervisely.geometry.geometry.Geometry`
:raises NotImplementedError: if method is not implemented in subclass
"""
raise NotImplementedError()
[docs]
def fliplr(self, img_size):
"""
:param img_size: Image size (height, width) to which belongs Geometry.
:type img_size: Tuple[int, int]
:returns: Geometry after flip in horizontal.
:rtype: :class:`~supervisely.geometry.geometry.Geometry`
:raises NotImplementedError: if method is not implemented in subclass
"""
raise NotImplementedError()
[docs]
def flipud(self, img_size):
"""
:param img_size: Image size (height, width) to which belongs Geometry.
:type img_size: Tuple[int, int]
:returns: Geometry after flip in vertical.
:rtype: :class:`~supervisely.geometry.geometry.Geometry`
:raises NotImplementedError: if method is not implemented in subclass
"""
raise NotImplementedError()
def _draw_bool_compatible(self, draw_fn, bitmap, color, thickness, config=None):
""" """
if bitmap.dtype == np.bool:
# Cannot draw on the canvas directly, create a temporary with different type.
temp_bitmap = np.zeros(bitmap.shape[:2], dtype=np.uint8)
draw_fn(temp_bitmap, 1, thickness=thickness, config=config)
bitmap[temp_bitmap == 1] = color
else:
# Pass through the canvas without temp bitmap for efficiency.
draw_fn(bitmap, color, thickness=thickness, config=config)
[docs]
def draw(self, bitmap, color, thickness=1, config=None):
"""
:param bitmap: np.ndarray
:param color: Color [R, G, B]
:param thickness: used only in Polyline and :class:`~supervisely.geometry.point.Point`
:param config: Drawing config specific to a concrete subclass, e.g. per edge colors
:type config: dict
:raises NotImplementedError: if method is not implemented in subclass
"""
self._draw_bool_compatible(self._draw_impl, bitmap, color, thickness, config)
[docs]
def get_mask(self, img_size: Tuple[int, int]):
"""Returns 2D boolean mask of the geometry.
With shape as img_size (height, width) and filled
with True values inside the geometry and False values outside.
dtype = np.bool
shape = img_size
:param img_size: size of the image (height, width)
:type img_size: Tuple[int, int]
:returns: 2D boolean mask of the geometry
:rtype: np.ndarray
"""
bitmap = np.zeros(img_size + (3,), dtype=np.uint8)
self.draw(bitmap, color=[255, 255, 255], thickness=-1)
return np.any(bitmap != 0, axis=-1)
def _draw_impl(self, bitmap, color, thickness=1, config=None):
"""
:param bitmap: np.ndarray
:param color: [R, G, B]
:param thickness: used only in Polyline and :class:`~supervisely.geometry.point.Point`
:type thickness: int
:param config: Drawing config specific to a concrete subclass, e.g. per edge colors
:type config: dict
:raises NotImplementedError: if method is not implemented in subclass
"""
raise NotImplementedError()
[docs]
def draw_contour(self, bitmap, color, thickness=1, config=None):
"""Draws the figure contour on a given bitmap canvas
:param bitmap: np.ndarray
:param color: [R, G, B]
:param thickness: (int)
:param config: drawing config specific to a concrete subclass, e.g. per edge colors
"""
self._draw_bool_compatible(self._draw_contour_impl, bitmap, color, thickness, config)
def _draw_contour_impl(self, bitmap, color, thickness=1, config=None):
"""Draws the figure contour on a given bitmap canvas
:param bitmap: np.ndarray
:param color: [R, G, B]
:param thickness: (int)
"""
raise NotImplementedError()
@property
def area(self):
"""
:returns: float
"""
raise NotImplementedError()
[docs]
def to_bbox(self) -> Rectangle:
"""
:returns: Rectangle from geometry.
:rtype: :class:`~supervisely.geometry.rectangle.Rectangle`
:raises NotImplementedError: if method is not implemented in subclass
"""
raise NotImplementedError()
[docs]
def clone(self):
"""Clone from GEOMETRYYY"""
return deepcopy(self)
[docs]
def validate(self, obj_class_shape, settings):
"""
Validate geometry.
:param obj_class_shape: Object class shape.
:type obj_class_shape: str
:param settings: Settings.
:type settings: dict
:raises ValueError: if geometry validation error
"""
if obj_class_shape != ANY_SHAPE:
if self.geometry_name() != obj_class_shape:
raise ValueError("Geometry validation error: shape names are mismatched!")
[docs]
@staticmethod
def config_from_json(config):
"""
Convert geometry config from json format
:param config: dictionary(geometry config) in json format
:type config: dict
:returns: dictionary(geometry config) in json format
:rtype: dict
"""
return config
[docs]
@staticmethod
def config_to_json(config):
"""
Convert geometry config to json format
:param config: dictionary(geometry config)
:type config: dict
:returns: dictionary(geometry config) in json format
:rtype: dict
"""
return config
[docs]
def convert(self, new_geometry, contour_radius=0, approx_epsilon=None):
"""
Convert geometry to another geometry shape.
:param new_geometry: New geometry shape.
:type new_geometry: :class:`~supervisely.geometry.geometry.Geometry`
:param contour_radius: Radius of the contour.
:type contour_radius: int
:param approx_epsilon: Approximation epsilon.
:type approx_epsilon: float
:returns: List of geometries.
:rtype: List[:class:`~supervisely.geometry.geometry.Geometry`]
"""
from supervisely.geometry.any_geometry import AnyGeometry
if type(self) == new_geometry or new_geometry == AnyGeometry:
return [self]
allowed_transforms = self.allowed_transforms()
if new_geometry not in allowed_transforms:
raise NotImplementedError(
"from {!r} to {!r}".format(self.geometry_name(), new_geometry.geometry_name())
)
from supervisely.geometry.alpha_mask import AlphaMask
from supervisely.geometry.bitmap import Bitmap
from supervisely.geometry.helpers import (
geometry_to_alpha_mask,
geometry_to_bitmap,
geometry_to_polygon,
)
from supervisely.geometry.polygon import Polygon
from supervisely.geometry.rectangle import Rectangle
from supervisely.geometry.oriented_bbox import OrientedBBox
res = []
if new_geometry == Bitmap:
res = geometry_to_bitmap(self, radius=contour_radius)
elif new_geometry == AlphaMask:
res = geometry_to_alpha_mask(self, radius=contour_radius)
elif new_geometry == Rectangle:
res = [self.to_bbox()]
elif new_geometry == Polygon:
res = geometry_to_polygon(self, approx_epsilon=approx_epsilon)
elif new_geometry == OrientedBBox:
bbox = self.to_bbox()
res = [OrientedBBox.from_bbox(bbox)]
if len(res) == 0:
logger.warning(
"Can not convert geometry {} to {} because geometry to convert is very small".format(
self.geometry_name(), new_geometry.geometry_name()
)
)
return res
@classmethod
def _to_pixel_coordinate_system_json(cls, data: Dict, image_size: List[int]) -> Dict:
"""
Convert geometry from subpixel precision to pixel precision by subtracting a subpixel offset from the coordinates.
This method should be reimplemented in subclasses if needed.
Point order: [x, y]
In the labeling tool, labels are created with subpixel precision,
which means that the coordinates of the geometry can have decimal values representing fractions of a pixel.
However, in Supervisely SDK, geometry coordinates are represented using pixel precision, where the coordinates are integers representing whole pixels.
:param data: Json data with geometry config.
:type data: dict
:param image_size: Image size in pixels (height, width).
:type image_size: List[int]
:returns: Json data with coordinates converted to pixel coordinate system.
:rtype: dict
"""
data = deepcopy(data) # Avoid modifying the original data
height, width = image_size[:2]
# Point, Polygon, Polyline. Rectangle have its own implementation
if data.get(POINTS) is not None:
exterior = data[POINTS][EXTERIOR]
interior = data[POINTS][INTERIOR]
for point in exterior:
point[0] = floor(point[0]) - 1 if point[0] == width else floor(point[0])
point[1] = floor(point[1]) - 1 if point[1] == height else floor(point[1])
for coords in interior:
for point in coords:
point[0] = floor(point[0]) - 1 if point[0] == width else floor(point[0])
point[1] = floor(point[1]) - 1 if point[1] == height else floor(point[1])
data[POINTS][EXTERIOR] = exterior
data[POINTS][INTERIOR] = interior
# Bitmap and AlphaMask
if data.get(BITMAP) is not None:
origin = data[BITMAP][ORIGIN]
data[BITMAP][ORIGIN] = [floor(origin[0]), floor(origin[1])]
# GraphNodes and Cuboid
if data.get(NODES) is not None:
nodes = data[NODES]
for node_key in nodes:
nodes[node_key][LOC] = [
(
floor(nodes[node_key][LOC][0]) - 1
if nodes[node_key][LOC][0] == width
else floor(nodes[node_key][LOC][0])
),
(
floor(nodes[node_key][LOC][1]) - 1
if nodes[node_key][LOC][1] == height
else floor(nodes[node_key][LOC][1])
),
]
data[NODES] = nodes
return data
@classmethod
def _to_subpixel_coordinate_system_json(cls, data: Dict) -> Dict:
"""
Convert geometry from pixel precision to subpixel precision by adding a subpixel offset to the coordinates.
This method should be reimplemented in subclasses if needed.
Point order: [x, y]
In the labeling tool, labels are created with subpixel precision,
which means that the coordinates of the geometry can have decimal values representing fractions of a pixel.
However, in Supervisely SDK, geometry coordinates are represented using pixel precision, where the coordinates are integers representing whole pixels.
:param data: Json data with geometry config.
:type data: dict
:returns: Json data with coordinates converted to subpixel coordinate system.
:rtype: dict
"""
data = deepcopy(data) # Avoid modifying the original data
# Point, Polygon, Polyline. Rectangle have its own implementation
if data.get(POINTS) is not None:
exterior = data[POINTS][EXTERIOR]
interior = data[POINTS][INTERIOR]
for point in exterior:
point[0] = point[0] + 0.5
point[1] = point[1] + 0.5
for coords in interior:
for point in coords:
point[0] = point[0] + 0.5
point[1] = point[1] + 0.5
data[POINTS][EXTERIOR] = exterior
data[POINTS][INTERIOR] = interior
# Bitmap and AlphaMask
if data.get(BITMAP) is not None:
origin = data[BITMAP][ORIGIN]
data[BITMAP][ORIGIN] = [floor(origin[0]), floor(origin[1])]
# GraphNodes and Cuboid
if data.get(NODES) is not None:
nodes = data[NODES]
for node_key in nodes:
nodes[node_key][LOC] = [
floor(nodes[node_key][LOC][0]) + 0.5,
floor(nodes[node_key][LOC][1]) + 0.5,
]
data[NODES] = nodes
return data
# def _to_pixel_coordinate_system(self):
# """
# This method should be implemented in subclasses.
# Convert geometry from subpixel precision to pixel precision by subtracting a subpixel offset from the coordinates.
# In the labeling tool, labels are created with subpixel precision,
# which means that the coordinates of the geometry can have decimal values representing fractions of a pixel.
# However, in Supervisely SDK, geometry coordinates are represented using pixel precision, where the coordinates are integers representing whole pixels.
# :returns: New instance of Geometry object in pixel coordinates system
# :rtype: :class:`~supervisely.geometry.geometry.Geometry`
# """
# return self
# def _to_subpixel_coordinate_system(self):
# """
# This method should be implemented in subclasses.
# Convert geometry from pixel precision to subpixel precision by adding a subpixel offset to the coordinates.
# In the labeling tool, labels are created with subpixel precision,
# which means that the coordinates of the geometry can have decimal values representing fractions of a pixel.
# However, in Supervisely SDK, geometry coordinates are represented using pixel precision, where the coordinates are integers representing whole pixels.
# :returns: New instance of Geometry object in subpixel coordinates system
# :rtype: :class:`~supervisely.geometry.geometry.Geometry`
# """
# return self