diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7517c235..c44f7349 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,6 +23,13 @@ repos: # Run the formatter - id: ruff-format + # python static type checking + - repo: https://github.com/pre-commit/mirrors-mypy + rev: "v1.11.2" + hooks: + - id: mypy + additional_dependencies: [types-PyYAML, types-setuptools] + - repo: https://github.com/pre-commit/mirrors-prettier rev: v4.0.0-alpha.8 hooks: diff --git a/docs/source/conf.py b/docs/source/conf.py index a85354ae..fd532d3f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -42,7 +42,7 @@ ] templates_path = ["_templates"] -exclude_patterns = [] +exclude_patterns: list[str] = [] # Automatic exclusion of prompts from the copies # https://sphinx-copybutton.readthedocs.io/en/latest/use.html#automatic-exclusion-of-prompts-from-the-copies diff --git a/examples/python/__init__.py b/examples/python/__init__.py new file mode 100644 index 00000000..916f3a44 --- /dev/null +++ b/examples/python/__init__.py @@ -0,0 +1,2 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/model_api/python/model_api/adapters/inference_adapter.py b/model_api/python/model_api/adapters/inference_adapter.py index c8b93a0d..42b25649 100644 --- a/model_api/python/model_api/adapters/inference_adapter.py +++ b/model_api/python/model_api/adapters/inference_adapter.py @@ -3,8 +3,9 @@ # SPDX-License-Identifier: Apache-2.0 # -import abc +from abc import ABC, abstractmethod from dataclasses import dataclass, field +from typing import Any, Dict, List, Set, Tuple @dataclass @@ -17,30 +18,37 @@ class Metadata: meta: dict = field(default_factory=dict) -class InferenceAdapter(abc.ABC): - """An abstract Model Adapter with the following interface: - - - Reading the model from disk or other place - - Loading the model to the device - - Accessing the information about inputs/outputs - - The model reshaping - - Synchronous model inference - - Asynchronous model inference +class InferenceAdapter(ABC): + """ + An abstract Model Adapter with the following interface: + + - Reading the model from disk or other place + - Loading the model to the device + - Accessing the information about inputs/outputs + - The model reshaping + - Synchronous model inference + - Asynchronous model inference """ precisions = ("FP32", "I32", "FP16", "I16", "I8", "U8") - @abc.abstractmethod - def __init__(self): - """An abstract Model Adapter constructor. + @abstractmethod + def __init__(self) -> None: + """ + An abstract Model Adapter constructor. Reads the model from disk or other place. """ + self.model: Any - @abc.abstractmethod + @abstractmethod def load_model(self): """Loads the model on the device.""" - @abc.abstractmethod + @abstractmethod + def get_model(self): + """Get the model.""" + + @abstractmethod def get_input_layers(self): """Gets the names of model inputs and for each one creates the Metadata structure, which contains the information about the input shape, layout, precision @@ -50,7 +58,7 @@ def get_input_layers(self): - the dict containing Metadata for all inputs """ - @abc.abstractmethod + @abstractmethod def get_output_layers(self): """Gets the names of model outputs and for each one creates the Metadata structure, which contains the information about the output shape, layout, precision @@ -60,7 +68,7 @@ def get_output_layers(self): - the dict containing Metadata for all outputs """ - @abc.abstractmethod + @abstractmethod def reshape_model(self, new_shape): """Reshapes the model inputs to fit the new input shape. @@ -74,7 +82,7 @@ def reshape_model(self, new_shape): } """ - @abc.abstractmethod + @abstractmethod def infer_sync(self, dict_data): """Performs the synchronous model inference. The infer is a blocking method. @@ -95,9 +103,10 @@ def infer_sync(self, dict_data): } """ - @abc.abstractmethod - def infer_async(self, dict_data, callback_fn, callback_data): - """Performs the asynchronous model inference and sets + @abstractmethod + def infer_async(self, dict_data, callback_data): + """ + Performs the asynchronous model inference and sets the callback for inference completion. Also, it should define get_raw_result() function, which handles the result of inference from the model. @@ -109,11 +118,10 @@ def infer_async(self, dict_data, callback_fn, callback_data): 'input_layer_name_2': data_2, ... } - - callback_fn: the callback function, which is defined outside the adapter - callback_data: the data for callback, that will be taken after the model inference is ended """ - @abc.abstractmethod + @abstractmethod def is_ready(self): """In case of asynchronous execution checks if one can submit input data to the model for inference, or all infer requests are busy. @@ -123,23 +131,23 @@ def is_ready(self): submitted to the model for inference or not """ - @abc.abstractmethod + @abstractmethod def await_all(self): """In case of asynchronous execution waits the completion of all busy infer requests. """ - @abc.abstractmethod + @abstractmethod def await_any(self): """In case of asynchronous execution waits the completion of any busy infer request until it becomes available for the data submission. """ - @abc.abstractmethod + @abstractmethod def get_rt_info(self, path): """Forwards to openvino.Model.get_rt_info(path)""" - @abc.abstractmethod + @abstractmethod def embed_preprocessing( self, layout, diff --git a/model_api/python/model_api/adapters/onnx_adapter.py b/model_api/python/model_api/adapters/onnx_adapter.py index 08af61e6..6d03380b 100644 --- a/model_api/python/model_api/adapters/onnx_adapter.py +++ b/model_api/python/model_api/adapters/onnx_adapter.py @@ -162,6 +162,10 @@ def embed_preprocessing( reversed(preproc_funcs), ) + def get_model(self): + """Return the reference to the ONNXRuntime session.""" + return self.session + def reshape_model(self, new_shape): raise NotImplementedError diff --git a/model_api/python/model_api/adapters/ovms_adapter.py b/model_api/python/model_api/adapters/ovms_adapter.py index 7548264a..2056b45b 100644 --- a/model_api/python/model_api/adapters/ovms_adapter.py +++ b/model_api/python/model_api/adapters/ovms_adapter.py @@ -87,6 +87,10 @@ def is_ready(self): def load_model(self): pass + def get_model(self): + """Return the reference to the GrpcClient.""" + return self.client + def await_all(self): pass diff --git a/model_api/python/model_api/adapters/utils.py b/model_api/python/model_api/adapters/utils.py index 9b2850c9..96f92af2 100644 --- a/model_api/python/model_api/adapters/utils.py +++ b/model_api/python/model_api/adapters/utils.py @@ -7,6 +7,7 @@ import math from functools import partial +from typing import Callable, Optional import cv2 import numpy as np @@ -509,7 +510,7 @@ def crop_resize_ocv(image, size): return cv2.resize(cropped_frame, size) -RESIZE_TYPES = { +RESIZE_TYPES: dict[str, Callable] = { "crop": crop_resize_ocv, "standard": resize_image_ocv, "fit_to_window": resize_image_with_aspect_ocv, diff --git a/model_api/python/model_api/models/action_classification.py b/model_api/python/model_api/models/action_classification.py index 2ac6d975..e757aa9b 100644 --- a/model_api/python/model_api/models/action_classification.py +++ b/model_api/python/model_api/models/action_classification.py @@ -64,6 +64,14 @@ def __init__( self.image_blob_names = self._get_inputs() self.image_blob_name = self.image_blob_names[0] self.nscthw_layout = "NSCTHW" in self.inputs[self.image_blob_name].layout + self.labels: list[str] + self.path_to_labels: str + self.mean_values: list[int | float] + self.pad_value: int + self.resize_type: str + self.reverse_input_channels: bool + self.scale_values: list[int | float] + if self.nscthw_layout: self.n, self.s, self.c, self.t, self.h, self.w = self.inputs[self.image_blob_name].shape else: @@ -118,7 +126,7 @@ def parameters(cls) -> dict[str, Any]: ) return parameters - def _get_inputs(self) -> tuple[list[str], list[str]]: + def _get_inputs(self) -> list[str]: """Defines the model inputs for images and additional info. Raises: diff --git a/model_api/python/model_api/models/anomaly.py b/model_api/python/model_api/models/anomaly.py index 812bef8d..596a3acd 100644 --- a/model_api/python/model_api/models/anomaly.py +++ b/model_api/python/model_api/models/anomaly.py @@ -14,6 +14,8 @@ import cv2 import numpy as np +from model_api.adapters.inference_adapter import InferenceAdapter + from .image_model import ImageModel from .types import ListValue, NumericalValue, StringValue from .utils import AnomalyResult @@ -22,7 +24,9 @@ class AnomalyDetection(ImageModel): __model__ = "AnomalyDetection" - def __init__(self, inference_adapter, configuration=dict(), preload=False): + def __init__( + self, inference_adapter: InferenceAdapter, configuration: dict = dict(), preload: bool = False + ) -> None: super().__init__(inference_adapter, configuration, preload) self._check_io_number(1, 1) self.normalization_scale: float @@ -31,7 +35,7 @@ def __init__(self, inference_adapter, configuration=dict(), preload=False): self.task: str self.labels: list[str] - def postprocess(self, outputs: dict[str, np.ndarray], meta: dict[str, Any]): + def postprocess(self, outputs: dict[str, np.ndarray], meta: dict[str, Any]) -> AnomalyResult: """Post-processes the outputs and returns the results. Args: diff --git a/model_api/python/model_api/models/classification.py b/model_api/python/model_api/models/classification.py index 6e065615..77bd56a7 100644 --- a/model_api/python/model_api/models/classification.py +++ b/model_api/python/model_api/models/classification.py @@ -14,6 +14,8 @@ from openvino.runtime import Model, Type from openvino.runtime import opset10 as opset +from model_api.adapters.inference_adapter import InferenceAdapter + from .image_model import ImageModel from .types import BooleanValue, ListValue, NumericalValue, StringValue from .utils import ClassificationResult @@ -22,13 +24,24 @@ class ClassificationModel(ImageModel): __model__ = "Classification" - def __init__(self, inference_adapter, configuration=dict(), preload=False): + def __init__(self, inference_adapter: InferenceAdapter, configuration: dict = dict(), preload: bool = False): super().__init__(inference_adapter, configuration, preload=False) + self.topk: int + self.labels: list[str] + self.path_to_labels: str + self.multilabel: bool + self.hierarchical: bool + self.hierarchical_config: str + self.confidence_threshold: float + self.output_raw_scores: bool + self.hierarchical_postproc: str + self.labels_resolver: GreedyLabelsResolver | ProbabilisticLabelsResolver + self._check_io_number(1, (1, 2, 3, 4, 5)) if self.path_to_labels: self.labels = self._load_labels(self.path_to_labels) if len(self.outputs) == 1: - self._verify_signle_output() + self._verify_single_output() self.raw_scores_name = _raw_scores_name if self.hierarchical: @@ -99,7 +112,7 @@ def _load_labels(self, labels_file): labels.append(s[(begin_idx + 1) : end_idx]) return labels - def _verify_signle_output(self): + def _verify_single_output(self): layer_name = next(iter(self.outputs)) layer_shape = self.outputs[layer_name].shape @@ -197,7 +210,7 @@ def get_saliency_maps(self, outputs: dict) -> np.ndarray: if not self.hierarchical: return saliency_maps - reordered_saliency_maps = [[] for _ in range(len(saliency_maps))] + reordered_saliency_maps: list[list[np.ndarray]] = [[] for _ in range(len(saliency_maps))] model_classes = self.hierarchical_info["cls_heads_info"]["class_to_group_idx"] label_to_model_out_idx = {lbl: i for i, lbl in enumerate(model_classes.keys())} for batch in range(len(saliency_maps)): @@ -279,7 +292,7 @@ def get_multiclass_predictions(self, outputs): return list(zip(indicesTensor, labels, scoresTensor)) -def addOrFindSoftmaxAndTopkOutputs(inference_adapter, topk, output_raw_scores): +def addOrFindSoftmaxAndTopkOutputs(inference_adapter: InferenceAdapter, topk: int, output_raw_scores: bool): softmaxNode = None for i in range(len(inference_adapter.model.outputs)): output_node = inference_adapter.model.get_output_op(i).input(0).get_source_output().get_node() diff --git a/model_api/python/model_api/models/detection_model.py b/model_api/python/model_api/models/detection_model.py index 40c19753..3e72de4d 100644 --- a/model_api/python/model_api/models/detection_model.py +++ b/model_api/python/model_api/models/detection_model.py @@ -18,6 +18,8 @@ class DetectionModel(ImageModel): The `postprocess` method must be implemented in a specific inherited wrapper. """ + __model__ = "DetectionModel" + def __init__(self, inference_adapter, configuration=dict(), preload=False): """Detection Model constructor diff --git a/model_api/python/model_api/models/image_model.py b/model_api/python/model_api/models/image_model.py index 4e6407a9..25780c3d 100644 --- a/model_api/python/model_api/models/image_model.py +++ b/model_api/python/model_api/models/image_model.py @@ -30,6 +30,8 @@ class ImageModel(Model): input_transform (InputTransform): instance of the `InputTransform` for image normalization """ + __model__ = "ImageModel" + def __init__(self, inference_adapter, configuration=dict(), preload=False): """Image model constructor diff --git a/model_api/python/model_api/models/model.py b/model_api/python/model_api/models/model.py index 72fad593..3d2f4a1e 100644 --- a/model_api/python/model_api/models/model.py +++ b/model_api/python/model_api/models/model.py @@ -5,6 +5,7 @@ import logging as log import re +from abc import ABC from contextlib import contextmanager from model_api.adapters.inference_adapter import InferenceAdapter @@ -50,7 +51,7 @@ class Model: model_loaded (bool): a flag whether the model is loaded to device """ - __model__ = None # Abstract wrapper has no name + __model__: str = "Model" def __init__(self, inference_adapter, configuration=dict(), preload=False): """Model constructor @@ -91,19 +92,11 @@ def __init__(self, inference_adapter, configuration=dict(), preload=False): self.callback_fn = lambda _: None def get_model(self): - """Returns the ov.Model object stored in the InferenceAdapter. - - Note: valid only for local inference - - Returns: - ov.Model object - Raises: - RuntimeError: in case of remote inference (serving) - """ - if isinstance(self.inference_adapter, OpenvinoAdapter): - return self.inference_adapter.get_model() - - raise RuntimeError("get_model() is not supported for remote inference") + model = self.inference_adapter.get_model() + model.set_rt_info(self.__model__, ["model_info", "model_type"]) + for name in self.parameters(): + model.set_rt_info(getattr(self, name), ["model_info", name]) + return model @classmethod def get_model_class(cls, name): @@ -157,7 +150,7 @@ def create_model( cache_dir (:obj:`str`, optional): directory where to store compiled models to reduce the load time before the inference Returns: - Model objcet + Model object """ if isinstance(model, InferenceAdapter): inference_adapter = model @@ -262,8 +255,8 @@ def _load_config(self, config): errors = parameters[name].validate(value) if errors: self.logger.error(f'Error with "{name}" parameter:') - for error in errors: - self.logger.error(f"\t{error}") + for _error in errors: + self.logger.error(f"\t{_error}") self.raise_error("Incorrect user configuration") value = parameters[name].get_value(value) self.__setattr__(name, value) @@ -496,13 +489,6 @@ def log_layers_info(self): f"\tOutput layer: {name}, shape: {metadata.shape}, precision: {metadata.precision}, layout: {metadata.layout}", ) - def get_model(self): - model = self.inference_adapter.get_model() - model.set_rt_info(self.__model__, ["model_info", "model_type"]) - for name in self.parameters(): - model.set_rt_info(getattr(self, name), ["model_info", name]) - return model - def save(self, xml_path, bin_path="", version="UNSPECIFIED"): import openvino diff --git a/model_api/python/model_api/models/sam_models.py b/model_api/python/model_api/models/sam_models.py index 80485c65..b1d6f5af 100644 --- a/model_api/python/model_api/models/sam_models.py +++ b/model_api/python/model_api/models/sam_models.py @@ -30,6 +30,8 @@ def __init__( ): super().__init__(inference_adapter, configuration, preload) self.output_name: str = list(self.outputs.keys())[0] + self.resize_type: str + self.image_size: int @classmethod def parameters(cls) -> dict[str, Any]: @@ -78,6 +80,9 @@ def __init__( self.mask_input = np.zeros((1, 1, 256, 256), dtype=np.float32) self.has_mask_input = np.zeros((1, 1), dtype=np.float32) + self.image_size: int + self.mask_threshold: float + self.embed_dim: int @classmethod def parameters(cls) -> dict[str, Any]: diff --git a/model_api/python/model_api/models/types.py b/model_api/python/model_api/models/types.py index 5fa8794a..77e40d85 100644 --- a/model_api/python/model_api/models/types.py +++ b/model_api/python/model_api/models/types.py @@ -30,7 +30,7 @@ def get_value(self, value): if len(errors) == 0: return value if value is not None else self.default_value - def build_error(): + def build_error(self) -> None: pass def __str__(self) -> str: diff --git a/model_api/python/model_api/models/visual_prompting.py b/model_api/python/model_api/models/visual_prompting.py index 65c636f7..a9b83b9b 100644 --- a/model_api/python/model_api/models/visual_prompting.py +++ b/model_api/python/model_api/models/visual_prompting.py @@ -149,13 +149,12 @@ def __init__( """ self.encoder = encoder_model self.decoder = decoder_model + self._used_indices: np.ndarray | None = None + self._reference_features: np.ndarray | None = None if reference_features is not None: self._reference_features = reference_features.feature_vectors self._used_indices = reference_features.used_indices - else: - self._reference_features = None - self._used_indices = None self._point_labels_box = np.array([[2, 3]], dtype=np.float32) self._has_mask_inputs = [np.array([[0.0]]), np.array([[1.0]])] @@ -287,10 +286,9 @@ def learn( ) cur_default_threshold_reference -= 0.05 - self._reference_features[label] = ref_feat - self._used_indices: np.ndarray = np.concatenate( - (self._used_indices, [label]), - ) + if self._reference_features is not None: + self._reference_features[label] = ref_feat + self._used_indices = np.concatenate((self._used_indices, [label])) ref_masks[label] = ref_mask self._used_indices = np.unique(self._used_indices) @@ -359,7 +357,7 @@ def infer( downsizing=self._downsizing, ) - predicted_masks: defaultdict[int, list] = defaultdict(list) + predicted_masks: dict[int, list] = defaultdict(list) used_points: defaultdict[int, list] = defaultdict(list) for label in total_points_scores: points_scores = total_points_scores[label] @@ -392,20 +390,18 @@ def infer( } inputs_decoder["image_embeddings"] = image_embeddings - prediction = self._predict_masks( - inputs_decoder, - original_shape, - apply_masks_refinement, + _prediction: dict[str, np.ndarray] = self._predict_masks( + inputs_decoder, original_shape, apply_masks_refinement ) - prediction.update({"scores": points_score[-1]}) + _prediction.update({"scores": points_score[-1]}) - predicted_masks[label].append(prediction[self.decoder.output_blob_name]) + predicted_masks[label].append(_prediction[self.decoder.output_blob_name]) used_points[label].append(points_score) # check overlapping area between different label masks _inspect_overlapping_areas(predicted_masks, used_points) - prediction = {} + prediction: dict[int, PredictedMask] = {} for k in used_points: processed_points = [] scores = [] diff --git a/model_api/python/model_api/tilers/tiler.py b/model_api/python/model_api/tilers/tiler.py index 8d99b996..74da28fe 100644 --- a/model_api/python/model_api/tilers/tiler.py +++ b/model_api/python/model_api/tilers/tiler.py @@ -131,8 +131,8 @@ def _load_config(self, config): errors = parameters[name].validate(value) if errors: self.logger.error(f'Error with "{name}" parameter:') - for error in errors: - self.logger.error(f"\t{error}") + for _error in errors: + self.logger.error(f"\t{_error}") raise RuntimeError("Incorrect user configuration") value = parameters[name].get_value(value) self.__setattr__(name, value) diff --git a/model_api/python/pyproject.toml b/model_api/python/pyproject.toml index 6193d2e5..d6a1377e 100644 --- a/model_api/python/pyproject.toml +++ b/model_api/python/pyproject.toml @@ -69,6 +69,17 @@ include = ["model_api*"] # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# MYPY CONFIGURATION. # +[tool.mypy] +ignore_missing_imports = true +show_error_codes = true + + +[[tool.mypy.overrides]] +follow_imports = "skip" +follow_imports_for_stubs = true + + # RUFF CONFIGURATION # [tool.ruff] # Enable preview features diff --git a/tests/python/accuracy/__init__.py b/tests/python/accuracy/__init__.py new file mode 100644 index 00000000..916f3a44 --- /dev/null +++ b/tests/python/accuracy/__init__.py @@ -0,0 +1,2 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/python/accuracy/test_accuracy.py b/tests/python/accuracy/test_accuracy.py index 8a9cbfbc..61b6800a 100644 --- a/tests/python/accuracy/test_accuracy.py +++ b/tests/python/accuracy/test_accuracy.py @@ -73,21 +73,17 @@ def create_models(model_type, model_path, download_dir, force_onnx_adapter=False ] if model_path.endswith(".xml"): wrapper_type = model_type.get_model_class( - create_models.core.read_model(model_path) + create_core() + .read_model(model_path) .get_rt_info(["model_info", "model_type"]) .astype(str) ) - model = wrapper_type( - OpenvinoAdapter(create_models.core, model_path, device="CPU") - ) + model = wrapper_type(OpenvinoAdapter(create_core(), model_path, device="CPU")) model.load() models.append(model) return models -create_models.core = create_core() - - @pytest.fixture(scope="session") def data(pytestconfig): return pytestconfig.getoption("data") @@ -270,7 +266,8 @@ def test_image_models(data, dump, result, model_data): model.save(data + "/serialized/" + save_name) if model_data.get("check_extra_rt_info", False): assert ( - create_models.core.read_model(data + "/serialized/" + save_name) + create_core() + .read_model(data + "/serialized/" + save_name) .get_rt_info(["model_info", "label_ids"]) .astype(str) ) diff --git a/tests/python/precommit/__init__.py b/tests/python/precommit/__init__.py new file mode 100644 index 00000000..916f3a44 --- /dev/null +++ b/tests/python/precommit/__init__.py @@ -0,0 +1,2 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0