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 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


# @TODO: use properties instead of field if it makes sense
[docs]class Geometry(JsonSerializable): """ """ def __init__( self, sly_id=None, class_id=None, labeler_login=None, updated_at=None, created_at=None, ): 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): """ """ 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): """ """ 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(): """ :return: string with name of geometry """ raise NotImplementedError()
[docs] @classmethod def name(cls): """ Same as geometry_name(), but shorter. In order to make the code more concise. :return: string with name of geometry """ return cls.geometry_name()
[docs] def crop(self, rect): """ :param rect: Rectangle :return: list of Geometry """ raise NotImplementedError()
[docs] def relative_crop(self, rect): """Crops object like "crop" method, but return results with coordinates relative to rect :param rect: :return: list of Geometry """ return [geom.translate(drow=-rect.top, dcol=-rect.left) for geom in self.crop(rect)]
[docs] def rotate(self, rotator): """Rotates around image center -> New Geometry :param rotator: ImageRotator :return: Geometry """ raise NotImplementedError()
[docs] def resize(self, in_size, out_size): """ :param in_size: (rows, cols) :param out_size: (128, 256) (128, KEEP_ASPECT_RATIO) (KEEP_ASPECT_RATIO, 256) :return: Geometry """ raise NotImplementedError()
[docs] def scale(self, factor): """Scales around origin with a given factor. :param: factor (float): :return: Geometry """ raise NotImplementedError()
[docs] def translate(self, drow, dcol): """ :param drow: int rows shift :param dcol: int cols shift :return: Geometry """ raise NotImplementedError()
[docs] def fliplr(self, img_size): """ :param img_size: (rows, cols) :return: Geometry """ raise NotImplementedError()
[docs] def flipud(self, img_size): """ :param img_size: (rows, cols) :return: Geometry """ 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: [R, G, B] :param thickness: used only in Polyline and Point :param config: drawing config specific to a concrete subclass, e.g. per edge colors """ 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] :return: 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 Point """ 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): """ :return: float """ raise NotImplementedError()
[docs] def to_bbox(self) -> Rectangle: """ :return: Rectangle """ raise NotImplementedError()
[docs] def clone(self): """Clone from GEOMETRYYY""" return deepcopy(self)
def validate(self, obj_class_shape, settings): """ """ if obj_class_shape != ANY_SHAPE: if self.geometry_name() != obj_class_shape: raise ValueError("Geometry validation error: shape names are mismatched!") @staticmethod def config_from_json(config): """ """ return config @staticmethod def config_to_json(config): """ """ return config @classmethod def allowed_transforms(cls): """ """ # raise NotImplementedError("{!r}".format(cls.geometry_name())) return [] def convert(self, new_geometry, contour_radius=0, approx_epsilon=None): """ """ 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 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) if len(res) == 0: logger.warn( "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: :class:`dict` :param image_size: Image size in pixels (height, width). :type image_size: List[int] :return: Json data with coordinates converted to pixel coordinate system. :rtype: :class:`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: :class:`dict` :return: Json data with coordinates converted to subpixel coordinate system. :rtype: :class:`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. # :return: New instance of Geometry object in pixel coordinates system # :rtype: :class:`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. # :return: New instance of Geometry object in subpixel coordinates system # :rtype: :class:`Geometry<Geometry>` # """ # return self