diff --git a/geti_sdk/deployment/deployed_model.py b/geti_sdk/deployment/deployed_model.py index 497a2101..42926bc3 100644 --- a/geti_sdk/deployment/deployed_model.py +++ b/geti_sdk/deployment/deployed_model.py @@ -164,10 +164,15 @@ def get_data(self, source: Union[str, os.PathLike, GetiSession]): model_python_path = os.path.join( os.path.dirname(source), PYTHON_DIR_NAME ) - python_dir_contents = os.listdir(model_python_path) + 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) + + self._model_python_path = os.path.join(source, PYTHON_DIR_NAME) elif isinstance(source, GetiSession): if self.base_url is None: @@ -411,14 +416,15 @@ def save(self, path_to_folder: Union[str, os.PathLike]) -> bool: dst=new_model_data_path, dirs_exist_ok=True, ) - shutil.copytree( - src=self._model_python_path, - dst=new_model_python_path, - dirs_exist_ok=True, - ) + if self._model_python_path is not None: + shutil.copytree( + src=self._model_python_path, + dst=new_model_python_path, + dirs_exist_ok=True, + ) + self._model_python_path = new_model_python_path self._model_data_path = new_model_data_path - self._model_python_path = new_model_python_path config_dict = ConfigurationRESTConverter.configuration_to_minimal_dict( self.hyper_parameters diff --git a/geti_sdk/deployment/deployment.py b/geti_sdk/deployment/deployment.py index 1f76c241..c5665465 100644 --- a/geti_sdk/deployment/deployment.py +++ b/geti_sdk/deployment/deployment.py @@ -35,6 +35,9 @@ from geti_sdk.data_models.predictions import ResultMedium from geti_sdk.data_models.shapes import Polygon, Rectangle, RotatedRectangle from geti_sdk.deployment.data_models import ROI, IntermediateInferenceResult +from geti_sdk.deployment.legacy_converters import ( + AnomalyClassificationToAnnotationConverter, +) from geti_sdk.rest_converters import ProjectRESTConverter from .deployed_model import DeployedModel @@ -327,9 +330,21 @@ def _infer_task( ) if n_outputs != 0: - annotation_scene_entity = converter.convert_to_annotation( - predictions=postprocessing_results, metadata=metadata - ) + try: + annotation_scene_entity = converter.convert_to_annotation( + predictions=postprocessing_results, metadata=metadata + ) + except AttributeError: + # Add backwards compatibility for anomaly models created in Geti v1.8 and below + if task.type.is_anomaly: + legacy_converter = AnomalyClassificationToAnnotationConverter( + label_schema=model.ote_label_schema + ) + annotation_scene_entity = legacy_converter.convert_to_annotation( + predictions=postprocessing_results, metadata=metadata + ) + self._inference_converters[task.type] = legacy_converter + prediction = Prediction.from_ote( annotation_scene_entity, image_width=width, image_height=height ) @@ -465,20 +480,32 @@ def _get_model_for_task(self, task: Task) -> DeployedModel: ) from error return self.models[task_index] - def _remove_temporary_resources(self) -> None: + def _remove_temporary_resources(self) -> bool: """ If necessary, clean up any temporary resources associated with the deployment. + + :return: True if temp files have been deleted successfully """ if self._path_to_temp_resources is not None and os.path.isdir( self._path_to_temp_resources ): - shutil.rmtree(self._path_to_temp_resources) + try: + shutil.rmtree(self._path_to_temp_resources) + except PermissionError: + logging.warning( + f"Unable to remove temporary files for deployment at path " + f"`{self._path_to_temp_resources}` because the files are in " + f"use by another process. " + ) + return False else: logging.debug( f"Unable to clean up temporary resources for deployment {self}, " f"because the resources were not found on the system. Possibly " f"they were already deleted." ) + return False + return True def __del__(self): """ diff --git a/geti_sdk/deployment/legacy_converters/__init__.py b/geti_sdk/deployment/legacy_converters/__init__.py new file mode 100644 index 00000000..b815b578 --- /dev/null +++ b/geti_sdk/deployment/legacy_converters/__init__.py @@ -0,0 +1,22 @@ +# 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 + +__all__ = ["AnomalyClassificationToAnnotationConverter"] diff --git a/geti_sdk/deployment/legacy_converters/legacy_anomaly_converter.py b/geti_sdk/deployment/legacy_converters/legacy_anomaly_converter.py new file mode 100644 index 00000000..74ec230e --- /dev/null +++ b/geti_sdk/deployment/legacy_converters/legacy_anomaly_converter.py @@ -0,0 +1,65 @@ +# 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 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, +) + + +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 + ) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 619303fb..2aa02f54 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -10,7 +10,7 @@ Pillow==10.2.* pathvalidate>=2.5.0 simplejson==3.19.* ipython==8.12.* -otx==1.4.3 +otx==1.4.4 openvino==2023.0.* openvino-model-api==0.1.5 certifi>=2022.12.7