Source code for supervisely.geometry.geometry

# 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] @classmethod def allowed_transforms(cls): """ Returns the allowed transforms for the Geometry. """ # raise NotImplementedError("{!r}".format(cls.geometry_name())) return []
[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