Source code for supervisely.volume_annotation.volume_figure

# coding: utf-8
from __future__ import annotations
import uuid
from typing import Union, Optional, Literal
from numpy import ndarray
from uuid import UUID
from supervisely.video_annotation.video_figure import VideoFigure
from supervisely.video_annotation.key_id_map import KeyIdMap
from supervisely.geometry.closed_surface_mesh import ClosedSurfaceMesh
from supervisely.geometry.mask_3d import Mask3D
from supervisely.api.module_api import ApiField
from supervisely.geometry.any_geometry import AnyGeometry
from supervisely.annotation.json_geometries_map import GET_GEOMETRY_FROM_STR
from supervisely._utils import take_with_default
from supervisely.volume_annotation.volume_object import VolumeObject
from supervisely.geometry.geometry import Geometry
import supervisely.volume_annotation.constants as constants
from supervisely.volume_annotation.constants import ID, KEY, OBJECT_ID, OBJECT_KEY, META
from supervisely.geometry.constants import (
    LABELER_LOGIN,
    UPDATED_AT,
    CREATED_AT,
    CLASS_ID,
)

from supervisely.volume_annotation.volume_object_collection import (
    VolumeObjectCollection,
)


[docs]class VolumeFigure(VideoFigure): """ VolumeFigure object for :class:`VolumeAnnotation<supervisely.volume_annotation.volume_annotation.VolumeAnnotation>`. :class:`VolumeFigure<VolumeFigure>` object is immutable. :param volume_object: VolumeObject object. :type volume_object: VolumeObject :param geometry: Geometry object :type geometry: :class:`Geometry<supervisely.geometry>` :param plane_name: Name of the volume plane. :type plane_name: str :param slice_index: Index of slice to which VolumeFigure belongs. :type slice_index: int :param key: The UUID key associated with the VolumeFigure. :type key: UUID, optional :param class_id: ID of :class:`VolumeObject<VolumeObject>` to which VolumeFigure belongs. :type class_id: int, optional :param labeler_login: Login of the user who created VolumeFigure. :type labeler_login: str, optional :param updated_at: Date and Time when VolumeFigure was modified last. Date Format: Year:Month:Day:Hour:Minute:Seconds. Example: '2021-01-22T19:37:50.158Z'. :type updated_at: str, optional :param created_at: Date and Time when VolumeFigure was created. Date Format is the same as in "updated_at" parameter. :type created_at: str, optional :Usage example: .. code-block:: python import supervisely as sly obj_class_heart = sly.ObjClass('heart', sly.Rectangle) volume_obj_heart = sly.VolumeObject(obj_class_heart) slice_index = 7 plane_name = "axial" geometry = sly.Rectangle(0, 0, 100, 100) volume_figure_heart = sly.VolumeFigure(volume_obj_heart, geometry, plane_name, slice_index) volume_figure_heart_json = volume_figure_heart.to_json() print(volume_figure_heart_json) # Output: { # "geometry": { # "points": { # "exterior": [ # [0, 0], # [100, 100] # ], # "interior": [] # } # }, # "geometryType": "rectangle", # "key": "158e6cf4f4ac4c639fc6994aad127c16", # "meta": { # "normal": { "x": 0, "y": 0, "z": 1 }, # "planeName": "axial", # "sliceIndex": 7 # }, # "objectKey": "bf63ffe342e949899d3ddcb6b0f73f54" # } """ def __init__( self, volume_object: VolumeObject, geometry: Geometry, plane_name: Literal["axial", "sagittal", "coronal"] = None, slice_index: int = None, key: Optional[UUID] = None, class_id: Optional[int] = None, labeler_login: Optional[str] = None, updated_at: Optional[str] = None, created_at: Optional[str] = None, ): # only Mask3D can be created without 'plane_name' and 'slice_index' if not isinstance(geometry, (Mask3D, ClosedSurfaceMesh)): if plane_name is None and slice_index is None: raise TypeError( "VolumeFigure.__init__() missing 2 required positional arguments: 'plane_name' and 'slice_index'" ) if plane_name is None: raise TypeError( f"Argument 'plane_name' must be set as one of 'axial', 'sagittal', 'coronal' str" ) elif slice_index is None: raise TypeError(f"Argument 'slice_index' must be set as int number") super().__init__( video_object=volume_object, geometry=geometry, frame_index=slice_index, key=key, class_id=class_id, labeler_login=labeler_login, updated_at=updated_at, created_at=created_at, ) from supervisely.volume_annotation.plane import Plane Plane.validate_name(plane_name) self._plane_name = plane_name self._slice_index = slice_index @property def volume_object(self) -> VolumeObject: """ Get a parent VolumeObject object of volume figure. :return: Parent VolumeObject object of volume figure. :rtype: VolumeObject :Usage example: .. code-block:: python import supervisely as sly volume_obj_heart = sly.VolumeObject(obj_class_heart) volume_figure_heart = sly.VolumeFigure( volume_obj_heart, geometry=sly.Rectangle(0, 0, 100, 100), plane_name="axial", slice_index=7 ) print(volume_figure_heart.parent_object) # Output: # <supervisely.volume_annotation.volume_object.VolumeObject object at 0x7f95f0950b50> """ return self._video_object @property def video_object(self): """Property "video_object" is only available for videos.""" raise NotImplementedError('Property "video_object" is only available for videos') @property def parent_object(self) -> VolumeObject: """ Get a parent VolumeObject object of volume figure. :return: VolumeObject object :rtype: VolumeObject :Usage example: .. code-block:: python import supervisely as sly obj_class_heart = sly.ObjClass('heart', sly.Rectangle) volume_obj_heart = sly.VolumeObject(obj_class_heart) volume_figure_heart = sly.VolumeFigure( volume_obj_heart, geometry=sly.Rectangle(0, 0, 100, 100), plane_name="axial", slice_index=7 ) print(volume_figure_heart.parent_object) # Output: # <supervisely.volume_annotation.volume_object.VolumeObject object at 0x7f786a3f8bd0> """ return self.volume_object @property def frame_index(self): """Property "frame_index" is only available for videos.""" raise NotImplementedError('Property "frame_index" is only available for videos') @property def slice_index(self): """ Get a slice index of volume figure. :return: :py:class:`Slice<supervisely.volume_annotation.slice.Slice>` index of volume figure. :rtype: int :Usage example: .. code-block:: python import supervisely as sly obj_class_heart = sly.ObjClass('heart', sly.Rectangle) volume_obj_heart = sly.VolumeObject(obj_class_heart) volume_figure_heart = sly.VolumeFigure( volume_obj_heart, geometry=sly.Rectangle(0, 0, 100, 100), plane_name="axial", slice_index=7 ) print(volume_figure_heart.slice_index) # Output: 7 """ return self._slice_index @property def plane_name(self): """ Get a plane name of volume figure. :return: Plane name of volume figure. :rtype: str :Usage example: .. code-block:: python import supervisely as sly obj_class_heart = sly.ObjClass('heart', sly.Rectangle) volume_obj_heart = sly.VolumeObject(obj_class_heart) volume_figure_heart = sly.VolumeFigure( volume_obj_heart, geometry=sly.Rectangle(0, 0, 100, 100), plane_name="axial", slice_index=7 ) print(volume_figure_heart.plane_name) # Output: axial """ return self._plane_name @property def normal(self): """ Get a normal vector associated with a plane name. :return: Dictionary with normal vector associated with a plane name. :rtype: dict :Usage example: .. code-block:: python import supervisely as sly obj_class_heart = sly.ObjClass('heart', sly.Rectangle) volume_obj_heart = sly.VolumeObject(obj_class_heart) volume_figure_heart = sly.VolumeFigure( volume_obj_heart, geometry=sly.Rectangle(0, 0, 100, 100), plane_name="axial", slice_index=7 ) print(volume_figure_heart.normal) # Output: {'x': 0, 'y': 0, 'z': 1} """ from supervisely.volume_annotation.plane import Plane return Plane.get_normal(self.plane_name) def _validate_geometry_type(self): if ( self.parent_object.obj_class.geometry_type != AnyGeometry and type(self._geometry) != ClosedSurfaceMesh ): if type(self._geometry) is not self.parent_object.obj_class.geometry_type: raise RuntimeError( "Input geometry type {!r} != geometry type of ObjClass {}".format( type(self._geometry), self.parent_object.obj_class.geometry_type ) ) def _validate_geometry(self): if type(self._geometry) == ClosedSurfaceMesh: return super()._validate_geometry()
[docs] def validate_bounds(self, img_size, _auto_correct=False): """ Checks if given image with given size contains a figure. :param img_size: Size of the image (height, width). :type img_size: Tuple[int, int] :param _auto_correct: Correct the geometry of a shape if it is out of bounds or not. :type _auto_correct: bool, optional :raises: :class:`OutOfImageBoundsException<supervisely.video_annotation.video_figure.OutOfImageBoundsException>`, if figure is out of image bounds :return: None :rtype: :class:`NoneType` :Usage Example: .. code-block:: python import supervisely as sly obj_class_heart = sly.ObjClass('heart', sly.Rectangle) volume_obj_heart = sly.VolumeObject(obj_class_heart) slice_index = 7 plane_name = "axial" geometry = sly.Rectangle(0, 0, 100, 100) volume_figure_heart = sly.VolumeFigure(volume_obj_heart, geometry, plane_name, slice_index) im_size = (50, 200) volume_figure_heart.validate_bounds(im_size) # raise OutOfImageBoundsException("Figure is out of image bounds") """ if type(self._geometry) == ClosedSurfaceMesh: return super().validate_bounds(img_size, _auto_correct)
[docs] def clone( self, volume_object=None, geometry=None, plane_name=None, slice_index=None, key=None, class_id=None, labeler_login=None, updated_at=None, created_at=None, ): """ Makes a copy of VolumeFigure with new fields, if fields are given, otherwise it will use fields of the original VolumeFigure. :param volume_object: VolumeObject object. :type volume_object: VolumeObject, optional :param geometry: Geometry object. :type geometry: :class:`Geometry<supervisely.geometry>` :param plane_name: Name of the volume plane. :type plane_name: str, optional :param slice_index: Index of slice to which VolumeFigure belongs. :type slice_index: int, optional :param key: KeyIdMap object. :type key: KeyIdMap, optional :param class_id: ID of :class:`ObjClass<supervisely.annotation.obj_class.ObjClass>` to which VolumeFigure belongs. :type class_id: int, optional :param labeler_login: Login of the user who created VolumeFigure. :type labeler_login: str, optional :param updated_at: Date and Time when VolumeFigure was modified last. Date Format: Year:Month:Day:Hour:Minute:Seconds. Example: '2021-01-22T19:37:50.158Z'. :type updated_at: str, optional :param created_at: Date and Time when VolumeFigure was created. Date Format is the same as in "updated_at" parameter. :type created_at: str, optional :return: VolumeFigure object :rtype: :class:`VolumeFigure` :Usage example: .. code-block:: python import supervisely as sly obj_class_heart = sly.ObjClass('heart', sly.Rectangle) volume_obj_heart = sly.VolumeObject(obj_class_heart) slice_index = 7 plane_name = "axial" geometry = sly.Rectangle(0, 0, 100, 100) volume_figure_heart = sly.VolumeFigure(volume_obj_heart, geometry, plane_name, slice_index) obj_class_lang = sly.ObjClass('lang', sly.Rectangle) volume_obj_lang = sly.VolumeObject(obj_class_lang) slice_index_lang = 15 geometry_lang = sly.Rectangle(0, 0, 500, 600) # Remember that VolumeFigure object is immutable, and we need to assign new instance of VolumeFigure to a new variable volume_figure_lang = volume_figure_heart.clone(volume_object=volume_obj_lang, geometry=geometry_lang, slice_index=slice_index_lang) print(volume_figure_lang.to_json()) # Output: { # "geometry": { # "points": { # "exterior": [ # [0, 0], # [600, 500] # ], # "interior": [] # } # }, # "geometryType": "rectangle", # "key": "2974165267224bf6b677e17ca2304b04", # "meta": { # "normal": { "x": 0, "y": 0, "z": 1 }, # "planeName": "axial", # "sliceIndex": 15 # }, # "objectKey": "dafe3adaacad474ba5163ecebcc57cd0" # } """ return self.__class__( volume_object=take_with_default(volume_object, self.parent_object), geometry=take_with_default(geometry, self.geometry), plane_name=take_with_default(plane_name, self.plane_name), slice_index=take_with_default(slice_index, self.slice_index), key=take_with_default(key, self._key), class_id=take_with_default(class_id, self.class_id), labeler_login=take_with_default(labeler_login, self.labeler_login), updated_at=take_with_default(updated_at, self.updated_at), created_at=take_with_default(created_at, self.created_at), )
[docs] def get_meta(self): """ Get a dictionary with metadata associated with volume figure. :return: Dictionary with metadata associated with volume figure. :rtype: dict :Usage example: .. code-block:: python import supervisely as sly obj_class_heart = sly.ObjClass('heart', sly.Rectangle) volume_obj_heart = sly.VolumeObject(obj_class_heart) volume_figure_heart = sly.VolumeFigure( volume_obj_heart, geometry=sly.Rectangle(0, 0, 100, 100), plane_name="axial", slice_index=7 ) print(volume_figure_heart.get_meta()) # {'sliceIndex': 7, 'planeName': 'axial', 'normal': {'x': 0, 'y': 0, 'z': 1}} """ return { constants.SLICE_INDEX: self.slice_index, constants.PLANE_NAME: self.plane_name, constants.NORMAL: self.normal, }
[docs] @classmethod def from_json( cls, data, objects: VolumeObjectCollection, plane_name, slice_index, key_id_map: KeyIdMap = None, ): """ Convert a json dict to VolumeFigure. Read more about `Supervisely format <https://docs.supervise.ly/data-organization/00_ann_format_navi>`_. :param data: Dict in json format. :type data: dict :param objects: VolumeObjectCollection object. :type objects: VolumeObjectCollection :param plane_name: Name of the volume plane. :type plane_name: str :param slice_index: Index of slice to which VolumeFigure belongs. :type slice_index: int :param key_id_map: KeyIdMap object. :type key_id_map: KeyIdMap, optional :raises: :class:`RuntimeError`, if volume object ID and volume object key are None, if volume object key and key_id_map are None, if volume object with given id not found in key_id_map :return: VolumeFigure object :rtype: :class:`VolumeFigure` :Usage example: .. code-block:: python import supervisely as sly # Create VolumeFigure from json we use data from example to_json(see above) new_volume_figure = sly.VolumeFigure.from_json( data=volume_figure_json, objects=sly.VolumeObjectCollection([volume_obj_heart]), plane_name="axial", slice_index=7 ) """ # @#TODO: copypaste from video figure, add base class and refactor copypaste later # _video_figure = super().from_json(data, objects, slice_index, key_id_map) object_id = data.get(OBJECT_ID, None) object_key = None if OBJECT_KEY in data: object_key = uuid.UUID(data[OBJECT_KEY]) if object_id is None and object_key is None: raise RuntimeError( "Figure can not be deserialized from json: object_id or object_key are not found" ) if object_key is None: if key_id_map is None: raise RuntimeError("Figure can not be deserialized: key_id_map is None") object_key = key_id_map.get_object_key(object_id) if object_key is None: raise RuntimeError("Object with id={!r} not found in key_id_map".format(object_id)) volume_object = objects.get(object_key) if volume_object is None: raise RuntimeError( "Figure can not be deserialized: corresponding object {!r} not found in ObjectsCollection".format( object_key.hex ) ) shape_str = data[ApiField.GEOMETRY_TYPE] shape = GET_GEOMETRY_FROM_STR(shape_str) if shape == ClosedSurfaceMesh: geometry_json = data else: geometry_json = data[ApiField.GEOMETRY] geometry = shape.from_json(geometry_json) key = uuid.UUID(data[KEY]) if KEY in data else uuid.uuid4() if key_id_map is not None: key_id_map.add_figure(key, data.get(ID, None)) class_id = data.get(CLASS_ID, None) labeler_login = data.get(LABELER_LOGIN, None) updated_at = data.get(UPDATED_AT, None) created_at = data.get(CREATED_AT, None) return cls( volume_object=volume_object, geometry=geometry, plane_name=plane_name, slice_index=slice_index, key=key, class_id=class_id, labeler_login=labeler_login, updated_at=updated_at, created_at=created_at, )
[docs] def to_json(self, key_id_map=None, save_meta=True): """ Convert the VolumeFigure to a json dict. Read more about `Supervisely format <https://docs.supervise.ly/data-organization/00_ann_format_navi>`_. :param key_id_map: KeyIdMap object. :type key_id_map: KeyIdMap, optional :param save_meta: Save frame index or not. :type save_meta: bool, optional :return: Json format as a dict :rtype: :class:`dict` :Usage example: .. code-block:: python import supervisely as sly obj_class_heart = sly.ObjClass('heart', sly.Rectangle) volume_obj_heart = sly.VolumeObject(obj_class_heart) fr_index = 7 geometry = sly.Rectangle(0, 0, 100, 100) volume_figure_heart = sly.VolumeFigure(volume_obj_heart, geometry, fr_index) volume_figure_json = volume_figure_heart.to_json(save_meta=True) print(volume_figure_json) # Output: { # "geometry": { # "points": { # "exterior": [ # [0, 0], # [100, 100] # ], # "interior": [] # } # }, # "geometryType": "rectangle", # "key": "158e6cf4f4ac4c639fc6994aad127c16", # "meta": { # "normal": { "x": 0, "y": 0, "z": 1 }, # "planeName": "axial", # "sliceIndex": 7 # }, # "objectKey": "bf63ffe342e949899d3ddcb6b0f73f54" # } """ json_data = super().to_json(key_id_map, save_meta) if type(self._geometry) == ClosedSurfaceMesh: json_data.pop(ApiField.GEOMETRY) json_data.pop(ApiField.META) return json_data
[docs] @classmethod def from_mask3d( cls, volume_object: VolumeObject, geometry_data: Union[str, ndarray, bytes], key: Optional[UUID] = None, class_id: Optional[int] = None, labeler_login: Optional[str] = None, updated_at: Optional[str] = None, created_at: Optional[str] = None, ) -> VolumeFigure: """ Create a VolumeFigure from Mask 3D geometry. :param volume_object: The VolumeObject to which the VolumeFigure belongs. :type volume_object: VolumeObject :param geometry_data: Geometry data represented as a path, NumPy array, or bytes. :type geometry_data: str or ndarray or bytes :param key: The UUID key associated with the VolumeFigure. :type key: UUID, optional :param class_id: The ID of the VolumeObject class to which the VolumeFigure belongs. :type class_id: int, optional :param labeler_login: The login of the user who created the VolumeFigure. :type labeler_login: str, optional :param updated_at: The date and time when the VolumeFigure was last modified (ISO 8601 format, e.g., '2021-01-22T19:37:50.158Z'). :type updated_at: str, optional :param created_at: The date and time when the VolumeFigure was created (ISO 8601 format, e.g., '2021-01-22T19:37:50.158Z'). :type created_at: str, optional :return: A VolumeFigure object created from Mask3D geometry. :rtype: VolumeFigure """ if isinstance(geometry_data, str): mask_3d = Mask3D.create_from_file(geometry_data) if isinstance(geometry_data, ndarray): mask_3d = Mask3D(geometry_data) if isinstance(geometry_data, bytes): mask_3d = Mask3D.from_bytes(geometry_data) return cls( volume_object, mask_3d, key=key, class_id=class_id, labeler_login=labeler_login, updated_at=updated_at, created_at=created_at, )
def _set_3d_geometry(self, new_geometry: Mask3D) -> None: """ Change the Mask3D geometry data in the figure. :param new_geometry: The new Mask3D geometry object. :type new_geometry: Mask3D """ if not isinstance(new_geometry, Mask3D): raise TypeError( f"You trying to add '{new_geometry.name()}' data, accepted only '{Mask3D.name()}' geometry data" ) if not isinstance(self.geometry, Mask3D): raise TypeError( f"You trying to change '{self.geometry.name()}' geometry data with '{new_geometry.name()}' data" ) self.geometry.data = new_geometry.data self.geometry._space = new_geometry._space self.geometry._space_origin = new_geometry._space_origin self.geometry._space_directions = new_geometry._space_directions