From ebcb43d2abeef4bbe0d5cfc8ff1f76751483cbe6 Mon Sep 17 00:00:00 2001 From: Igor Davidyuk Date: Thu, 25 Apr 2024 12:50:00 +0300 Subject: [PATCH] Remove dependency on OTX (#393) * remove otx from code Signed-off-by: Igor Davidyuk * remove otx from comments Signed-off-by: Igor Davidyuk * remove otx from requirements Signed-off-by: Igor Davidyuk * deprecate model wrappers mechanism Signed-off-by: Igor Davidyuk * add check for otx requirement in deployed model Signed-off-by: Igor Davidyuk * fix OVMS model loading Signed-off-by: Igor Davidyuk --------- Signed-off-by: Igor Davidyuk --- geti_sdk/data_models/annotation_scene.py | 47 ---- geti_sdk/data_models/annotations.py | 43 +--- geti_sdk/data_models/enums/task_type.py | 17 -- geti_sdk/data_models/label.py | 57 ----- geti_sdk/data_models/shapes.py | 188 +--------------- geti_sdk/deployment/deployed_model.py | 69 ++---- geti_sdk/deployment/deployment.py | 39 +--- .../results_converter/__init__.py | 2 - .../results_converter/legacy_converter.py | 200 ------------------ .../legacy_converters/__init__.py | 30 --- .../legacy_anomaly_converter.py | 171 --------------- .../results_to_prediction_converter.py | 17 +- requirements/requirements.txt | 1 - 13 files changed, 37 insertions(+), 844 deletions(-) delete mode 100644 geti_sdk/deployment/predictions_postprocessing/results_converter/legacy_converter.py delete mode 100644 geti_sdk/deployment/predictions_postprocessing/results_converter/legacy_converters/__init__.py delete mode 100644 geti_sdk/deployment/predictions_postprocessing/results_converter/legacy_converters/legacy_anomaly_converter.py diff --git a/geti_sdk/data_models/annotation_scene.py b/geti_sdk/data_models/annotation_scene.py index d88eee65..db84f250 100644 --- a/geti_sdk/data_models/annotation_scene.py +++ b/geti_sdk/data_models/annotation_scene.py @@ -20,7 +20,6 @@ import attr import cv2 import numpy as np -from otx.api.entities.annotation import AnnotationSceneEntity, AnnotationSceneKind from geti_sdk.data_models.annotations import Annotation from geti_sdk.data_models.enums import AnnotationKind @@ -336,52 +335,6 @@ def apply_identifier( annotation.modified = "" return new_annotation - @classmethod - def from_ote( - cls, - ote_annotation_scene: AnnotationSceneEntity, - image_width: int, - image_height: int, - ) -> "AnnotationScene": - """ - Create a :py:class:`~geti_sdk.data_models.annotation_scene.AnnotationScene` - instance from a given OTE SDK AnnotationSceneEntity object. - - :param ote_annotation_scene: OTE AnnotationSceneEntity object to create the - instance from - :param image_width: Width of the image to which the annotation scene applies - :param image_height: Height of the image to which the annotation scene applies - :return: AnnotationScene instance - """ - annotations = [ - Annotation.from_ote( - annotation, image_width=image_width, image_height=image_height - ) - for annotation in ote_annotation_scene.annotations - ] - return cls( - annotations=annotations, - id=ote_annotation_scene.id, - ) - - def to_ote(self, image_width: int, image_height: int) -> AnnotationSceneEntity: - """ - Create an AnnotationSceneEntity object from OTE SDK from the Geti SDK - AnnotationScene instance - - :param image_width: Width of the image to which the annotation scene applies - :param image_height: Height of the image to which the annotation scene applies - :return: OTE SDK AnnotationSceneEntity instance, corresponding to the current - AnnotationScene - """ - annotations = [ - annotation.to_ote(image_width=image_width, image_height=image_height) - for annotation in self.annotations - ] - return AnnotationSceneEntity( - annotations=annotations, kind=AnnotationSceneKind[self.kind.name] - ) - def map_labels( self, labels: Sequence[Union[Label, ScoredLabel]] ) -> "AnnotationScene": diff --git a/geti_sdk/data_models/annotations.py b/geti_sdk/data_models/annotations.py index bc85f6b3..9a85ba7c 100644 --- a/geti_sdk/data_models/annotations.py +++ b/geti_sdk/data_models/annotations.py @@ -15,16 +15,9 @@ from typing import ClassVar, List, Optional, Sequence, Union import attr -from otx.api.entities.annotation import Annotation as OteAnnotation from geti_sdk.data_models.label import Label, ScoredLabel -from geti_sdk.data_models.shapes import ( - Ellipse, - Polygon, - Rectangle, - RotatedRectangle, - Shape, -) +from geti_sdk.data_models.shapes import Ellipse, Polygon, Rectangle, RotatedRectangle from geti_sdk.data_models.utils import deidentify, str_to_datetime @@ -97,40 +90,6 @@ def pop_label_by_name(self, label_name: str) -> None: if index is not None: self.labels.pop(index) - @classmethod - def from_ote( - cls, ote_annotation: OteAnnotation, image_width: int, image_height: int - ) -> "Annotation": - """ - Create a :py:class:`~geti_sdk.data_models.annotations.Annotation` instance - from a given OTE SDK Annotation object. - - :param ote_annotation: OTE Annotation object to create the instance from - :param image_width: Width of the image to which the annotation applies - :param image_height: Height of the image to which the annotation applies - :return: Annotation instance - """ - shape = Shape.from_ote( - ote_annotation.shape, image_width=image_width, image_height=image_height - ) - labels = [ - ScoredLabel.from_ote(ote_label) - for ote_label in ote_annotation.get_labels(include_empty=True) - ] - return Annotation(shape=shape, labels=labels, id=ote_annotation.id) - - def to_ote(self, image_width: int, image_height: int) -> OteAnnotation: - """ - Create an OTE SDK Annotation object corresponding to this - :py:class:`~geti_sdk.data_models.annotations.Annotation` instance - - :param image_width: Width of the image to which the annotation applies - :param image_height: Height of the image to which the annotation applies - """ - shape = self.shape.to_ote(image_width=image_width, image_height=image_height) - labels = [label.to_ote() for label in self.labels] - return OteAnnotation(shape=shape, labels=labels) - def map_labels(self, labels: Sequence[Union[ScoredLabel, Label]]) -> "Annotation": """ Attempt to map the labels found in `labels` to those in the Annotation diff --git a/geti_sdk/data_models/enums/task_type.py b/geti_sdk/data_models/enums/task_type.py index c0bc5332..e4197e0c 100644 --- a/geti_sdk/data_models/enums/task_type.py +++ b/geti_sdk/data_models/enums/task_type.py @@ -14,8 +14,6 @@ from enum import Enum -from otx.api.entities.model_template import Domain as OteDomain - class TaskType(Enum): """ @@ -106,21 +104,6 @@ def from_domain(cls, domain): """ return cls[domain.name] - def to_ote_domain(self) -> OteDomain: - """ - Convert a TaskType instance to an OTE SDK Domain object. - - NOTE: Not all TaskTypes have a counterpart in the OTE SDK Domain Enum, for - example TaskType.DATASET and TaskType.CROP cannot be converted to a Domain. For - those TaskTypes, a `Domain.NULL` instance will be returned. - - :return: Domain instance corresponding to the TaskType instance - """ - if self in NON_TRAINABLE_TASK_TYPES: - return OteDomain.NULL - else: - return OteDomain[self.name] - NON_TRAINABLE_TASK_TYPES = [TaskType.DATASET, TaskType.CROP] diff --git a/geti_sdk/data_models/label.py b/geti_sdk/data_models/label.py index fc959494..52c6b8ed 100644 --- a/geti_sdk/data_models/label.py +++ b/geti_sdk/data_models/label.py @@ -16,13 +16,7 @@ from typing import ClassVar, List, Optional, Tuple import attr -from otx.api.entities.color import Color -from otx.api.entities.color import Color as OteColor -from otx.api.entities.label import Domain as OteLabelDomain -from otx.api.entities.label import LabelEntity -from otx.api.entities.scored_label import ScoredLabel as OteScoredLabel -from geti_sdk.data_models.enums import TaskType from geti_sdk.data_models.enums.domain import Domain @@ -88,22 +82,6 @@ def __hash__(self) -> int: """ return hash(self.__key()) - def to_ote(self, task_type: TaskType) -> LabelEntity: - """ - Convert the `Label` instance to an OTE SDK LabelEntity object. - - :return: OTE SDK LabelEntity instance corresponding to the label - """ - return LabelEntity( - name=self.name, - domain=task_type.to_ote_domain(), - id=self.id, - hotkey=self.hotkey, - is_empty=self.is_empty, - color=Color.from_hex_str(self.color), - is_anomalous=self.is_anomalous, - ) - def prepare_for_post(self) -> None: """ Set all fields to None that are not valid for making a POST request to the @@ -167,38 +145,3 @@ def from_label(cls, label: Label, probability: float) -> "ScoredLabel": return ScoredLabel( name=label.name, probability=probability, color=label.color, id=label.id ) - - @classmethod - def from_ote(cls, ote_label: OteScoredLabel) -> "ScoredLabel": - """ - Create a :py:class`~geti_sdk.data_models.label.ScoredLabel` from - the OTE SDK ScoredLabel entity passed. - - :param ote_label: OTE SDK ScoredLabel entity to convert from - :return: ScoredLabel instance created according to the ote_label - """ - return cls( - name=ote_label.name, - id=ote_label.id, - probability=ote_label.probability, - color=( - ote_label.color - if isinstance(ote_label.color, str) - else ote_label.color.hex_str - ), - ) - - def to_ote(self) -> OteScoredLabel: - """ - Create a ScoredLabel object from OTE SDK corresponding to this - :py:class`~geti_sdk.data_models.label.ScoredLabel` instance. - """ - return OteScoredLabel( - label=LabelEntity( - name=self.name, - color=OteColor(*self.color_tuple), - id=self.id, - domain=OteLabelDomain.NULL, - ), - probability=self.probability, - ) diff --git a/geti_sdk/data_models/shapes.py b/geti_sdk/data_models/shapes.py index f7e16533..f74c728a 100644 --- a/geti_sdk/data_models/shapes.py +++ b/geti_sdk/data_models/shapes.py @@ -14,24 +14,15 @@ import abc import math -from typing import Any, Dict, List, Optional, Tuple, TypeVar, Union +from typing import Any, Dict, List, Optional, Tuple, Union import attr import cv2 import numpy as np -from otx.api.entities.shapes.ellipse import Ellipse as OteEllipse -from otx.api.entities.shapes.polygon import Point as OtePoint -from otx.api.entities.shapes.polygon import Polygon as OtePolygon -from otx.api.entities.shapes.rectangle import Rectangle as OteRectangle -from otx.api.entities.shapes.shape import ShapeType as OteShapeType from geti_sdk.data_models.enums import ShapeType from geti_sdk.data_models.utils import round_to_n_digits, str_to_shape_type -# OteShapeTypeVar is a type variable that represents the possible different shapes -# that can be converted from the OTE SDK -OteShapeTypeVar = TypeVar("OteShapeTypeVar", OtePolygon, OteEllipse, OteRectangle) - # N_DIGITS_TO_ROUND_TO determines how pixel coordinates will be rounded when they are # passed from the Intel® Geti™ REST API. The Intel® Geti™ server itself rounds some # coordinates to 4 digits, but not all. Here we round all coordinates for internal @@ -88,41 +79,6 @@ def to_normalized_coordinates( """ raise NotImplementedError - @classmethod - def from_ote( - cls, ote_shape: OteShapeTypeVar, image_width: int, image_height: int - ) -> Union["Rectangle", "Ellipse", "Polygon", "RotatedRectangle"]: - """ - Create a Shape entity from a corresponding shape in the OTE SDK. - - :param ote_shape: OTE SDK shape to convert from - :param image_width: Width of the image to which the shape applies (in pixels) - :param image_height: Height of the image to which the shape applies (in pixels) - :return: Shape entity created from the ote_shape - """ - shape_mapping = { - OteShapeType.RECTANGLE: Rectangle, - OteShapeType.ELLIPSE: Ellipse, - OteShapeType.POLYGON: Polygon, - } - return shape_mapping[ote_shape.type].from_ote( - ote_shape, image_width=image_width, image_height=image_height - ) - - def to_ote( - self, image_width: int, image_height: int - ) -> Union[OtePolygon, OteEllipse, OteRectangle]: - """ - Convert the Shape instance to the corresponding shape in the OTE SDK. - - :param image_width: Width of the image with respect to which the shape is - defined - :param image_height: Height of the image with respect to which the shape is - defined - :return: OTE SDK Rectangle, Ellipse or Polygon shape - """ - raise NotImplementedError - @property @abc.abstractmethod def area(self) -> float: @@ -214,41 +170,6 @@ def to_absolute_coordinates(self, parent_roi: "Rectangle") -> "Rectangle": y_min = parent_roi.y + self.y return Rectangle(x=x_min, y=y_min, width=self.width, height=self.height) - @classmethod - def from_ote( - cls, ote_shape: OteRectangle, image_width: int, image_height: int - ) -> "Rectangle": - """ - Create a :py:class`~geti_sdk.data_models.shapes.Rectangle` from - the OTE SDK Rectangle entity passed. - - :param ote_shape: OTE SDK Rectangle entity to convert from - :param image_width: Width of the image to which the rectangle applies (in pixels) - :param image_height: Height of the image to which the rectangle applies (in pixels) - :return: Rectangle instance created according to the ote_shape - """ - return cls( - x=ote_shape.x1 * image_width, - y=ote_shape.y1 * image_height, - width=ote_shape.width * image_width, - height=ote_shape.height * image_height, - ) - - def to_ote(self, image_width: int, image_height: int) -> OteRectangle: - """ - Convert the Rectangle to a Rectangle instance from OTE SDK - - :param image_width: Width of the image to which the rectangle applies (in pixels) - :param image_height: Height of the image to which the rectangle applies (in pixels) - :return: OTE SDK Rectangle instance - """ - return OteRectangle( - x1=self.x / image_width, - y1=self.y / image_height, - x2=(self.x + self.width) / image_width, - y2=(self.y + self.height) / image_height, - ) - @property def area(self) -> float: """ @@ -363,42 +284,6 @@ def get_center_point(self) -> Tuple[int, int]: """ return self.x + self.width // 2, self.y + self.height // 2 - @classmethod - def from_ote( - cls, ote_shape: OteEllipse, image_width: int, image_height: int - ) -> "Ellipse": - """ - Create a :py:class`~geti_sdk.data_models.shapes.Ellipse` from - the OTE SDK Ellipse entity passed. - - :param ote_shape: OTE SDK Ellipse entity to convert from - :param image_width: Width of the image to which the ellipse applies (in pixels) - :param image_height: Height of the image to which the ellipse applies (in pixels) - :return: Ellipse instance created according to the ote_shape - """ - return cls( - x=int(ote_shape.x1 * image_width), - y=int(ote_shape.y1 * image_height), - width=int(ote_shape.width * image_width), - height=int(ote_shape.height * image_height), - ) - - def to_ote(self, image_width: int, image_height: int) -> OteEllipse: - """ - Convert the Ellipse to an Ellipse instance from OTE SDK - - :param image_width: Width of the image to which the ellipse applies (in pixels) - :param image_height: Height of the image to which the ellipse applies (in - pixels) - :return: OTE SDK Ellipse instance - """ - return OteEllipse( - x1=self.x / image_width, - y1=self.y / image_height, - x2=(self.x + self.width) / image_width, - y2=(self.y + self.height) / image_height, - ) - @property def area(self) -> float: """ @@ -545,40 +430,6 @@ def to_normalized_coordinates( ) return dict(points=normalized_points, type=str(self.type)) - @classmethod - def from_ote( - cls, ote_shape: OtePolygon, image_width: int, image_height: int - ) -> "Polygon": - """ - Create a :py:class`~geti_sdk.data_models.shapes.Polygon` from - the OTE SDK Polygon entity passed. - - :param ote_shape: OTE SDK Polygon entity to convert from - :param image_width: Width of the image to which the polygon applies (in pixels) - :param image_height: Height of the image to which the polygon applies (in pixels) - :return: Polygon instance created according to the ote_shape - """ - points = [ - Point(x=int(ote_point.x * image_width), y=int(ote_point.y * image_height)) - for ote_point in ote_shape.points - ] - return cls(points=points) - - def to_ote(self, image_width: int, image_height: int) -> OtePolygon: - """ - Convert the Polygon to a Polygon instance from OTE SDK - - :param image_width: Width of the image to which the polygon applies (in pixels) - :param image_height: Height of the image to which the polygon applies (in - pixels) - :return: OTE SDK Polygon instance - """ - ote_points = [ - OtePoint(x=point.x / image_width, y=point.y / image_height) - for point in self.points - ] - return OtePolygon(points=ote_points) - @property def area(self) -> float: """ @@ -775,43 +626,6 @@ def from_polygon(cls, polygon: Polygon) -> "RotatedRectangle": angle=alpha, ) - @classmethod - def from_ote( - cls, ote_shape: OtePolygon, image_width: int, image_height: int - ) -> "RotatedRectangle": - """ - Create a :py:class`~geti_sdk.data_models.shapes.RotatedRectangle` from - the OTE SDK Polygon entity passed. - - NOTE: The Polygon MUST consist of 4 points, otherwise a ValueError is raised - - :param ote_shape: OTE SDK Rectangle entity to convert from - :param image_width: Width of the image to which the shape applies (in pixels) - :param image_height: Height of the image to which the shape applies (in pixels) - :return: RotatedRectangle instance created according to the ote_shape - """ - polygon = Polygon.from_ote( - ote_shape, image_width=image_width, image_height=image_height - ) - return cls.from_polygon(polygon) - - def to_ote(self, image_width: int, image_height: int) -> OtePolygon: - """ - Convert the RotatedRectangle to a Polygon instance from OTE SDK - - :param image_width: Width of the image to which the rotated rectangle applies - (in pixels) - :param image_height: Height of the image to which the rotated rectangle - applies (in pixels) - :return: OTE SDK Polygon instance corresponding to the RotatedRectangle - """ - polygon_representation = self.to_polygon() - ote_points = [ - OtePoint(x=point.x / image_width, y=point.y / image_height) - for point in polygon_representation.points - ] - return OtePolygon(points=ote_points) - def to_roi(self) -> Rectangle: """ Return the bounding box containing the RotatedRectangle, as an instance of diff --git a/geti_sdk/deployment/deployed_model.py b/geti_sdk/deployment/deployed_model.py index 4879cff2..cb2a0122 100644 --- a/geti_sdk/deployment/deployed_model.py +++ b/geti_sdk/deployment/deployed_model.py @@ -12,12 +12,10 @@ # See the License for the specific language governing permissions # and limitations under the License. -import importlib.util import json import logging import os import shutil -import sys import tempfile import time import zipfile @@ -52,7 +50,6 @@ MODEL_DIR_NAME = "model" PYTHON_DIR_NAME = "python" -WRAPPER_DIR_NAME = "model_wrappers" REQUIREMENTS_FILE_NAME = "requirements.txt" LABELS_CONFIG_KEY = "labels" @@ -88,7 +85,6 @@ def __attrs_post_init__(self): self._model_python_path: Optional[str] = None self._needs_tempdir_deletion: bool = False self._tempdir_path: Optional[str] = None - self._has_custom_model_wrappers: bool = False self._label_schema: Optional[LabelSchema] = None # Attributes related to model explainability @@ -144,9 +140,6 @@ def get_data(self, source: Union[str, os.PathLike, GetiSession]): self._model_data_path = os.path.join(temp_dir, MODEL_DIR_NAME) self._model_python_path = os.path.join(temp_dir, PYTHON_DIR_NAME) - # Check if the model includes custom model wrappers - if WRAPPER_DIR_NAME in os.listdir(self._model_python_path): - self._has_custom_model_wrappers = True self.get_data(temp_dir) elif os.path.isdir(source): @@ -167,21 +160,23 @@ def get_data(self, source: Union[str, os.PathLike, GetiSession]): f"file 'model.xml' and weights file 'model.bin' were not found " f"at the path specified. " ) - if PYTHON_DIR_NAME in source_contents: - model_python_path = os.path.join(source, PYTHON_DIR_NAME) - else: - model_python_path = os.path.join( - os.path.dirname(source), PYTHON_DIR_NAME - ) - python_dir_contents = ( - os.listdir(model_python_path) - if os.path.exists(model_python_path) - else [] - ) - if WRAPPER_DIR_NAME in python_dir_contents: - self._has_custom_model_wrappers = True self._model_python_path = os.path.join(source, PYTHON_DIR_NAME) + # A model is being loaded from disk, check if it is a legacy model + # We support OTX models starting from version 1.5.0 + otx_version = get_package_version_from_requirements( + requirements_path=os.path.join( + self._model_python_path, REQUIREMENTS_FILE_NAME + ), + package_name="otx", + ) + if otx_version: # Empty string if package not found + otx_version = otx_version.split(".") + if int(otx_version[0]) <= 1 and int(otx_version[1]) < 5: + raise ValueError( + "Model version is not supported. Please use a model trained with " + "OTX version 1.5.0 or higher." + ) elif isinstance(source, GetiSession): if self.base_url is None: @@ -289,27 +284,6 @@ def load_inference_model( ) self._parse_label_schema_from_dict(label_dictionary) - # Create model wrapper with the loaded configuration - # First, add custom wrapper (if any) to path so that we can find it - if self._has_custom_model_wrappers: - wrapper_module_path = os.path.join( - self._model_python_path, WRAPPER_DIR_NAME - ) - module_name = WRAPPER_DIR_NAME + "." + model_type.lower().replace(" ", "-") - try: - spec = importlib.util.spec_from_file_location( - module_name, os.path.join(wrapper_module_path, "__init__.py") - ) - module = importlib.util.module_from_spec(spec) - sys.modules[module_name] = module - spec.loader.exec_module(module) - except ImportError as ex: - raise ImportError( - f"Unable to load inference model for {self}. A custom model wrapper" - f"is required, but could not be found at path " - f"{wrapper_module_path}." - ) from ex - model = model_api_Model.create_model( model=model_adapter, model_type=model_type, @@ -318,20 +292,13 @@ def load_inference_model( ) self._inference_model = model - # Load results to Prediction converter - otx_version = get_package_version_from_requirements( - requirements_path=os.path.join( - self._model_python_path, REQUIREMENTS_FILE_NAME - ), - package_name="otx", - ) - use_legacy_converter = not otx_version.startswith("1.5") + # Load a Results-to-Prediction converter self._converter = ConverterFactory.create_converter( - self.label_schema, configuration, use_legacy_converter=use_legacy_converter + self.label_schema, configuration ) # TODO: This is a workaround to fix the issue that causes the output blob name - # to be unset. Remove this once it has been fixed on OTX/ModelAPI side + # to be unset. Remove this once it has been fixed on ModelAPI side output_names = list(self._inference_model.outputs.keys()) if hasattr(self._inference_model, "output_blob_name"): if not self._inference_model.output_blob_name: diff --git a/geti_sdk/deployment/deployment.py b/geti_sdk/deployment/deployment.py index 241fabd4..6c502325 100644 --- a/geti_sdk/deployment/deployment.py +++ b/geti_sdk/deployment/deployment.py @@ -20,7 +20,7 @@ import attr import numpy as np -import otx +from openvino.model_api.models import Model as OMZModel from geti_sdk.data_models import ( Annotation, @@ -424,33 +424,16 @@ def generate_ovms_config(self, output_folder: Union[str, os.PathLike]) -> None: model_version = "1" ovms_model_dir = os.path.join(ovms_models_dir, model_name, model_version) - source_model_dir = model.model_data_path - - if otx.__version__ >= "1.4.0": - # Load the model to embed preprocessing for inference with OVMS adapter - try: - from openvino.model_api.models import Model as OMZModel - except ImportError as error: - raise ValueError( - f"Unable to load inference model for {model.name}. Relevant " - f"OpenVINO packages were not found. Please make sure that " - f"OpenVINO is installed correctly." - ) from error - embedded_model = OMZModel.create_model( - model=os.path.join(model.model_data_path, "model.xml") - ) - embedded_model.save( - xml_path=os.path.join(ovms_model_dir, "model.xml"), - bin_path=os.path.join(ovms_model_dir, "model.bin"), - ) - logging.info(f"Model `{model.name}` prepared for OVMS inference.") - else: - os.makedirs(ovms_model_dir, exist_ok=True) - for model_file in os.listdir(source_model_dir): - shutil.copy2( - src=os.path.join(source_model_dir, model_file), - dst=os.path.join(ovms_model_dir, model_file), - ) + + # Load the model to embed preprocessing for inference with OVMS adapter + embedded_model = OMZModel.create_model( + model=os.path.join(model.model_data_path, "model.xml") + ) + embedded_model.save( + xml_path=os.path.join(ovms_model_dir, "model.xml"), + bin_path=os.path.join(ovms_model_dir, "model.bin"), + ) + logging.info(f"Model `{model.name}` prepared for OVMS inference.") # Save model configurations ovms_config_list = {"model_config_list": model_configs} diff --git a/geti_sdk/deployment/predictions_postprocessing/results_converter/__init__.py b/geti_sdk/deployment/predictions_postprocessing/results_converter/__init__.py index d4fa29fc..83d245b1 100644 --- a/geti_sdk/deployment/predictions_postprocessing/results_converter/__init__.py +++ b/geti_sdk/deployment/predictions_postprocessing/results_converter/__init__.py @@ -23,8 +23,6 @@ - `RotatedRectToPredictionConverter` - class for converting rotated detection results to internal Prediction entities - `SegmentationToPredictionConverter` - class for converting segmentation results to internal Prediction entities - - `LegacyConverter` - OTX based universal converter for models generated with Geti v1.8 and OTX 1.4 - - `ConverterFactory` - factory class for creating the appropriate converter based on the domain of the inference results """ diff --git a/geti_sdk/deployment/predictions_postprocessing/results_converter/legacy_converter.py b/geti_sdk/deployment/predictions_postprocessing/results_converter/legacy_converter.py deleted file mode 100644 index 6f089c44..00000000 --- a/geti_sdk/deployment/predictions_postprocessing/results_converter/legacy_converter.py +++ /dev/null @@ -1,200 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. - -"""Module implements the Postprocessor class.""" - -from typing import List, Tuple - -import numpy as np -import otx -from otx.api.entities.label_schema import LabelGroup, LabelGroupType, LabelSchemaEntity -from otx.api.entities.model_template import Domain as OteDomain -from otx.api.usecases.exportable_code.prediction_to_annotation_converter import ( - IPredictionToAnnotationConverter, - create_converter, -) - -from geti_sdk.data_models.annotations import Annotation -from geti_sdk.data_models.enums.domain import Domain -from geti_sdk.data_models.enums.task_type import TaskType -from geti_sdk.data_models.label import Label, ScoredLabel -from geti_sdk.data_models.label_schema import LabelSchema -from geti_sdk.data_models.predictions import Prediction -from geti_sdk.data_models.shapes import Polygon, Rectangle, RotatedRectangle -from geti_sdk.deployment.predictions_postprocessing.utils.detection_utils import ( - detection2array, -) - -from .legacy_converters import ( - AnomalyClassificationToAnnotationConverter, - AnomalyDetectionToAnnotationConverter, - AnomalySegmentationToAnnotationConverter, -) - - -class LegacyConverter: - """ - LegacyConverter class responsible for converting the output of the model to a Prediction object. - For models generated with Geti v1.8 and below. - - :param labels: Label schema to be used for the conversion. - :param configuration: Configuration to be used for the conversion. - :param task: Task object containing the task metadata. - """ - - def __init__( - self, label_schema: LabelSchema, configuration, domain: Domain - ) -> None: - self.domain = domain - self.task_type = TaskType[self.domain.name] - self.label_schema = LabelSchemaEntity( - label_groups=[ - LabelGroup( - name=group.name, - labels=[label.to_ote(self.task_type) for label in group.labels], - group_type=LabelGroupType[group.group_type.name], - id=group.id, - ) - for group in label_schema.get_groups(include_empty=True) - ] - ) - - # Create OTX converter - converter_args = {"labels": self.label_schema} - if otx.__version__ > "1.2.0": - if "use_ellipse_shapes" not in configuration.keys(): - configuration.update({"use_ellipse_shapes": False}) - converter_args["configuration"] = configuration - - self.converter: IPredictionToAnnotationConverter = create_converter( - converter_type=OteDomain[self.domain.name], **converter_args - ) - - def convert_to_prediction( - self, postprocessing_results: List, image_shape: Tuple[int], **kwargs - ) -> Prediction: - """ - Convert the postprocessing results to a Prediction object. - """ - # Handle empty annotations - if isinstance(postprocessing_results, (np.ndarray, list)): - try: - n_outputs = len(postprocessing_results) - except TypeError: - n_outputs = 1 - else: - # Handle the new modelAPI output formats for detection and instance - # segmentation models - if ( - hasattr(postprocessing_results, "objects") - and self.domain == Domain.DETECTION - ): - n_outputs = len(postprocessing_results.objects) - postprocessing_results = detection2array(postprocessing_results.objects) - elif hasattr( - postprocessing_results, "segmentedObjects" - ) and self.domain in [ - Domain.INSTANCE_SEGMENTATION, - Domain.ROTATED_DETECTION, - ]: - n_outputs = len(postprocessing_results.segmentedObjects) - postprocessing_results = postprocessing_results.segmentedObjects - elif isinstance(postprocessing_results, tuple): - try: - n_outputs = len(postprocessing_results) - except TypeError: - n_outputs = 1 - else: - raise ValueError( - f"Unknown postprocessing output of type " - f"`{type(postprocessing_results)}` for task `{self.task.title}`." - ) - - # Proceed with postprocessing - width: int = image_shape[1] - height: int = image_shape[0] - - if n_outputs != 0: - try: - annotation_scene_entity = self.converter.convert_to_annotation( - predictions=postprocessing_results, - metadata={"original_shape": image_shape}, - ) - except AttributeError as e: - # Add backwards compatibility for anomaly models created in Geti v1.8 and below - if self.domain == Domain.ANOMALY_CLASSIFICATION: - legacy_converter = AnomalyClassificationToAnnotationConverter( - label_schema=self.label_schema - ) - elif self.domain == Domain.ANOMALY_DETECTION: - legacy_converter = AnomalyDetectionToAnnotationConverter( - label_schema=self.label_schema - ) - elif self.domain == Domain.ANOMALY_SEGMENTATION: - legacy_converter = AnomalySegmentationToAnnotationConverter( - label_schema=self.label_schema - ) - else: - raise e - - annotation_scene_entity = legacy_converter.convert_to_annotation( - predictions=postprocessing_results, - metadata={"original_shape": image_shape}, - ) - self.converter = legacy_converter - - prediction = Prediction.from_ote( - annotation_scene_entity, image_width=width, image_height=height - ) - else: - prediction = Prediction(annotations=[]) - - # Empty label is not generated by OTE correctly, append it here if there are - # no other predictions - if len(prediction.annotations) == 0: - empty_label = next( - ( - label - for label in self.label_schema.get_labels(include_empty=True) - if label.is_empty - ), - None, - ) - if empty_label is not None: - empty_label_sdk = Label( - name=empty_label.name, - color=( - empty_label.color - if isinstance(empty_label.color, str) - else empty_label.color.hex_str - ), - id=empty_label.id_, - is_empty=True, - group="", - ) - prediction.append( - Annotation( - shape=Rectangle(x=0, y=0, width=width, height=height), - labels=[ScoredLabel.from_label(empty_label_sdk, probability=1)], - ) - ) - - # Rotated detection models produce Polygons, convert them here to - # RotatedRectangles - if self.domain == Domain.ROTATED_DETECTION: - for annotation in prediction.annotations: - if isinstance(annotation.shape, Polygon): - annotation.shape = RotatedRectangle.from_polygon(annotation.shape) - - return prediction diff --git a/geti_sdk/deployment/predictions_postprocessing/results_converter/legacy_converters/__init__.py b/geti_sdk/deployment/predictions_postprocessing/results_converter/legacy_converters/__init__.py deleted file mode 100644 index b0726cde..00000000 --- a/geti_sdk/deployment/predictions_postprocessing/results_converter/legacy_converters/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. - -""" -Prediction converters for use with inference models created in older -versions of the Intel® Geti™ platform, i.e. v1.8 and below. -""" - -from .legacy_anomaly_converter import ( - AnomalyClassificationToAnnotationConverter, - AnomalyDetectionToAnnotationConverter, - AnomalySegmentationToAnnotationConverter, -) - -__all__ = [ - "AnomalyClassificationToAnnotationConverter", - "AnomalyDetectionToAnnotationConverter", - "AnomalySegmentationToAnnotationConverter", -] diff --git a/geti_sdk/deployment/predictions_postprocessing/results_converter/legacy_converters/legacy_anomaly_converter.py b/geti_sdk/deployment/predictions_postprocessing/results_converter/legacy_converters/legacy_anomaly_converter.py deleted file mode 100644 index e5974f6a..00000000 --- a/geti_sdk/deployment/predictions_postprocessing/results_converter/legacy_converters/legacy_anomaly_converter.py +++ /dev/null @@ -1,171 +0,0 @@ -# Copyright (C) 2024 Intel Corporation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions -# and limitations under the License. -from typing import Any, Dict - -import numpy as np -from openvino.model_api.models.utils import AnomalyResult -from otx.api.entities.annotation import ( - Annotation, - AnnotationSceneEntity, - AnnotationSceneKind, -) -from otx.api.entities.label_schema import LabelSchemaEntity -from otx.api.entities.scored_label import ScoredLabel -from otx.api.entities.shapes.rectangle import Rectangle -from otx.api.usecases.exportable_code.prediction_to_annotation_converter import ( - IPredictionToAnnotationConverter, -) -from otx.api.utils.segmentation_utils import create_annotation_from_segmentation_map - - -class AnomalyClassificationToAnnotationConverter(IPredictionToAnnotationConverter): - """ - Convert AnomalyClassification Predictions ModelAPI to Annotations. - - :param labels: Label Schema containing the label info of the task - """ - - def __init__(self, label_schema: LabelSchemaEntity): - labels = label_schema.get_labels(include_empty=False) - self.normal_label = [label for label in labels if not label.is_anomalous][0] - self.anomalous_label = [label for label in labels if label.is_anomalous][0] - - def convert_to_annotation( - self, predictions: np.ndarray, metadata: Dict[str, Any] - ) -> AnnotationSceneEntity: - """ - Convert predictions to OTX Annotation Scene using the metadata. - - :param predictions: Raw predictions from the model. - :param metadata: Variable containing metadata information. - :return: OTX annotation scene entity object. - """ - pred_label = predictions >= metadata.get("threshold", 0.5) - - label = self.anomalous_label if pred_label else self.normal_label - probability = (1 - predictions) if predictions < 0.5 else predictions - - annotations = [ - Annotation( - Rectangle.generate_full_box(), - labels=[ScoredLabel(label=label, probability=float(probability))], - ) - ] - return AnnotationSceneEntity( - kind=AnnotationSceneKind.PREDICTION, annotations=annotations - ) - - -class AnomalySegmentationToAnnotationConverter(IPredictionToAnnotationConverter): - """ - Convert AnomalySegmentation Predictions ModelAPI to Annotations. - - :param labels: Label Schema containing the label info of the task - """ - - def __init__(self, label_schema: LabelSchemaEntity): - labels = label_schema.get_labels(include_empty=False) - self.normal_label = [label for label in labels if not label.is_anomalous][0] - self.anomalous_label = [label for label in labels if label.is_anomalous][0] - self.label_map = {0: self.normal_label, 1: self.anomalous_label} - - def convert_to_annotation( - self, predictions: AnomalyResult, metadata: Dict[str, Any] - ) -> AnnotationSceneEntity: - """ - Convert predictions to OTX Annotation Scene using the metadata. - - :param predictions: Raw predictions from the model. - :param metadata: Variable containing metadata information. - :return: OTX annotation scene entity object. - """ - if predictions.pred_mask is None: - raise ValueError("No prediction mask found in model output") - if predictions.anomaly_map is None: - raise ValueError("No anomaly map found in model output") - annotations = create_annotation_from_segmentation_map( - predictions.pred_mask, predictions.anomaly_map / 255.0, self.label_map - ) - if len(annotations) == 0: - # TODO: add confidence to this label - annotations = [ - Annotation( - Rectangle.generate_full_box(), - labels=[ScoredLabel(label=self.normal_label, probability=1.0)], - ) - ] - return AnnotationSceneEntity( - kind=AnnotationSceneKind.PREDICTION, annotations=annotations - ) - - -class AnomalyDetectionToAnnotationConverter(IPredictionToAnnotationConverter): - """ - Convert Anomaly Detection Predictions ModelAPI to Annotations. - - :param labels: Label Schema containing the label info of the task - """ - - def __init__(self, label_schema: LabelSchemaEntity): - labels = label_schema.get_labels(include_empty=False) - self.normal_label = [label for label in labels if not label.is_anomalous][0] - self.anomalous_label = [label for label in labels if label.is_anomalous][0] - self.label_map = {0: self.normal_label, 1: self.anomalous_label} - - def convert_to_annotation( - self, predictions: AnomalyResult, metadata: Dict[str, Any] - ) -> AnnotationSceneEntity: - """ - Convert predictions to OTX Annotation Scene using the metadata. - - :param predictions: Raw predictions from the model. - :param metadata: Variable containing metadata information. - :return: OTX annotation scene entity object. - """ - if predictions.pred_mask is None: - raise ValueError("No prediction mask found in model output") - if predictions.pred_boxes is None: - raise ValueError("No bounding boxes found in model output") - if predictions.pred_score is None: - raise ValueError("No prediction score found in model output") - annotations = [] - image_h, image_w = predictions.pred_mask.shape - for box in predictions.pred_boxes: - annotations.append( - Annotation( - Rectangle( - box[0] / image_w, - box[1] / image_h, - box[2] / image_w, - box[3] / image_h, - ), - labels=[ - ScoredLabel( - label=self.anomalous_label, - probability=predictions.pred_score, - ) - ], - ) - ) - if len(annotations) == 0: - # TODO: add confidence to this label - annotations = [ - Annotation( - Rectangle.generate_full_box(), - labels=[ScoredLabel(label=self.normal_label, probability=1.0)], - ) - ] - return AnnotationSceneEntity( - kind=AnnotationSceneKind.PREDICTION, annotations=annotations - ) diff --git a/geti_sdk/deployment/predictions_postprocessing/results_converter/results_to_prediction_converter.py b/geti_sdk/deployment/predictions_postprocessing/results_converter/results_to_prediction_converter.py index fbc56518..27d79f5d 100644 --- a/geti_sdk/deployment/predictions_postprocessing/results_converter/results_to_prediction_converter.py +++ b/geti_sdk/deployment/predictions_postprocessing/results_converter/results_to_prediction_converter.py @@ -39,9 +39,6 @@ Rectangle, RotatedRectangle, ) -from geti_sdk.deployment.predictions_postprocessing.results_converter.legacy_converter import ( - LegacyConverter, -) from geti_sdk.deployment.predictions_postprocessing.utils.detection_utils import ( detection2array, ) @@ -87,7 +84,10 @@ def __init__(self, label_schema: LabelSchema): self.label_schema = label_schema def convert_to_prediction( - self, predictions: ClassificationResult, image_shape: Tuple[int], **kwargs + self, + predictions: ClassificationResult, + image_shape: Tuple[int, int, int], + **kwargs, ) -> Prediction: # noqa: ARG003 """ Convert ModelAPI ClassificationResult predictions to Prediction object. @@ -269,7 +269,7 @@ def __init__( self.confidence_threshold = configuration["confidence_threshold"] def convert_to_prediction( - self, predictions: Tuple, **kwargs: Dict[str, Any] + self, predictions: Any, **kwargs: Dict[str, Any] ) -> Prediction: """ Convert predictions to Prediction object. @@ -436,8 +436,8 @@ def convert_to_prediction( ) annotations = [ Annotation( - Rectangle.generate_full_box(*image_shape[1::-1]), labels=[scored_label], + shape=Rectangle.generate_full_box(*image_shape[1::-1]), ) ] return Prediction(annotations) @@ -452,22 +452,17 @@ class ConverterFactory: def create_converter( label_schema: LabelSchema, configuration: Optional[Dict[str, Any]] = None, - use_legacy_converter: bool = False, ) -> InferenceResultsToPredictionConverter: """ Create the appropriate inferencer object according to the model's task. :param label_schema: The label schema containing the label info of the task. :param configuration: Optional configuration for the converter. Defaults to None. - :param use_legacy_converter: Load a legacy converter for models generated by OTX version 1.4. :return: The created inference result to prediction converter. :raises ValueError: If the task type cannot be determined from the label schema. """ domain = ConverterFactory._get_labels_domain(label_schema) - if use_legacy_converter: - return LegacyConverter(label_schema, configuration, domain) - if domain == Domain.CLASSIFICATION: return ClassificationToPredictionConverter(label_schema) if domain == Domain.DETECTION: diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 3ae14363..fe648078 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -10,7 +10,6 @@ Pillow==10.3.* pathvalidate>=2.5.0 simplejson==3.19.* ipython==8.12.* -otx==1.4.4 openvino==2023.0.* openvino-model-api==0.1.5 certifi>=2022.12.7