diff --git a/.github/workflows/test_precommit.yml b/.github/workflows/test_precommit.yml index 208e722a..0d42c991 100644 --- a/.github/workflows/test_precommit.yml +++ b/.github/workflows/test_precommit.yml @@ -5,31 +5,6 @@ concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: - Python-Code-Quality: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: 3.9 - cache: pip - - name: Create and start a virtual environment - run: | - python -m venv venv - source venv/bin/activate - - name: Install dependencies - run: | - source venv/bin/activate - pip install --upgrade pip - pip install isort black - - name: Check style with black - run: | - source venv/bin/activate - black --check . - - name: Check style with isort - run: | - source venv/bin/activate - isort --check . Python-Precommit: runs-on: ubuntu-latest steps: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 073226ab..7517c235 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,6 +13,16 @@ repos: - id: debug-statements - id: detect-private-key + # Ruff version. + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: "v0.6.2" + hooks: + # Run the linter. + - id: ruff + args: ["--fix"] + # Run the formatter + - id: ruff-format + - 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 c5d8d49f..a85354ae 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -15,10 +15,10 @@ # Insert the path to sys.path sys.path.insert(0, str(module_path.resolve())) -project = 'InferenceSDK' -copyright = '2024, Intel OpenVINO' -author = 'Intel OpenVINO' -release = '2024' +project = "InferenceSDK" +copyright = "2024, Intel OpenVINO" +author = "Intel OpenVINO" +release = "2024" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration @@ -33,7 +33,7 @@ "sphinx.ext.napoleon", "sphinx_autodoc_typehints", "sphinx_copybutton", - "sphinx.ext.graphviz" + "sphinx.ext.graphviz", ] myst_enable_extensions = [ @@ -41,7 +41,7 @@ # other MyST extensions... ] -templates_path = ['_templates'] +templates_path = ["_templates"] exclude_patterns = [] # Automatic exclusion of prompts from the copies @@ -52,11 +52,11 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "pydata_sphinx_theme" -html_static_path = ['_static'] +html_static_path = ["_static"] -breathe_projects = {"InferenceSDK": Path(__file__).parent.parent/"build_cpp"/ "xml"} +breathe_projects = {"InferenceSDK": Path(__file__).parent.parent / "build_cpp" / "xml"} breathe_default_project = "InferenceSDK" -breathe_default_members = ('members', 'undoc-members', 'private-members') +breathe_default_members = ("members", "undoc-members", "private-members") autodoc_docstring_signature = True autodoc_member_order = "bysource" diff --git a/examples/python/asynchronous_api/run.py b/examples/python/asynchronous_api/run.py index e9a1d9a1..6fb1d8cf 100644 --- a/examples/python/asynchronous_api/run.py +++ b/examples/python/asynchronous_api/run.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 """ - Copyright (C) 2018-2022 Intel Corporation +Copyright (C) 2018-2022 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 +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 + 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. +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. """ import sys diff --git a/examples/python/serving_api/run.py b/examples/python/serving_api/run.py index 3d4aa94b..c2c35437 100755 --- a/examples/python/serving_api/run.py +++ b/examples/python/serving_api/run.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 """ - Copyright (C) 2018-2022 Intel Corporation +Copyright (C) 2018-2022 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 +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 + 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. +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. """ import sys diff --git a/examples/python/synchronous_api/run.py b/examples/python/synchronous_api/run.py index a4cf7f65..7a9bdd8e 100755 --- a/examples/python/synchronous_api/run.py +++ b/examples/python/synchronous_api/run.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 """ - Copyright (C) 2018-2022 Intel Corporation +Copyright (C) 2018-2022 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 +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 + 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. +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. """ import sys diff --git a/examples/python/visual_prompting/run.py b/examples/python/visual_prompting/run.py index 8030d826..20f2bc04 100644 --- a/examples/python/visual_prompting/run.py +++ b/examples/python/visual_prompting/run.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 """ - Copyright (C) 2024 Intel Corporation +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 +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 + 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. +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. """ import argparse diff --git a/examples/python/zsl_visual_prompting/run.py b/examples/python/zsl_visual_prompting/run.py index 5f8e5f6e..6bfcf09b 100644 --- a/examples/python/zsl_visual_prompting/run.py +++ b/examples/python/zsl_visual_prompting/run.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 """ - Copyright (C) 2024 Intel Corporation +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 +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 + 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. +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. """ import argparse diff --git a/model_api/python/model_api/adapters/__init__.py b/model_api/python/model_api/adapters/__init__.py index ee7be966..7c7a1f31 100644 --- a/model_api/python/model_api/adapters/__init__.py +++ b/model_api/python/model_api/adapters/__init__.py @@ -1,17 +1,16 @@ -""" - Copyright (C) 2021-2024 Intel Corporation +"""Copyright (C) 2021-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 +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 +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. +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 .onnx_adapter import ONNXRuntimeAdapter diff --git a/model_api/python/model_api/adapters/inference_adapter.py b/model_api/python/model_api/adapters/inference_adapter.py index 40112a0c..c9421537 100644 --- a/model_api/python/model_api/adapters/inference_adapter.py +++ b/model_api/python/model_api/adapters/inference_adapter.py @@ -1,65 +1,58 @@ -""" - Copyright (c) 2021-2024 Intel Corporation +"""Copyright (c) 2021-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 +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 +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. +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. """ import abc from dataclasses import dataclass, field -from typing import Dict, List, Set, Tuple @dataclass class Metadata: - names: Set[str] = field(default_factory=set) - shape: List[int] = field(default_factory=list) + names: set[str] = field(default_factory=set) + shape: list[int] = field(default_factory=list) layout: str = "" precision: str = "" type: str = "" - meta: Dict = field(default_factory=dict) + meta: dict = field(default_factory=dict) -class InferenceAdapter(metaclass=abc.ABCMeta): - """ - 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.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. + """An abstract Model Adapter constructor. Reads the model from disk or other place. """ @abc.abstractmethod def load_model(self): - """ - Loads the model on the device. - """ + """Loads the model on the device.""" @abc.abstractmethod def get_input_layers(self): - """ - Gets the names of model inputs and for each one creates the Metadata structure, + """Gets the names of model inputs and for each one creates the Metadata structure, which contains the information about the input shape, layout, precision in OpenVINO format, meta (optional) @@ -69,8 +62,7 @@ def get_input_layers(self): @abc.abstractmethod def get_output_layers(self): - """ - Gets the names of model outputs and for each one creates the Metadata structure, + """Gets the names of model outputs and for each one creates the Metadata structure, which contains the information about the output shape, layout, precision in OpenVINO format, meta (optional) @@ -80,8 +72,7 @@ def get_output_layers(self): @abc.abstractmethod def reshape_model(self, new_shape): - """ - Reshapes the model inputs to fit the new input shape. + """Reshapes the model inputs to fit the new input shape. Args: - new_shape (dict): the dictionary with inputs names as keys and @@ -95,8 +86,7 @@ def reshape_model(self, new_shape): @abc.abstractmethod def infer_sync(self, dict_data): - """ - Performs the synchronous model inference. The infer is a blocking method. + """Performs the synchronous model inference. The infer is a blocking method. Args: - dict_data: it's submitted to the model for inference and has the following format: @@ -117,8 +107,7 @@ 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 + """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. @@ -136,8 +125,7 @@ def infer_async(self, dict_data, callback_fn, callback_data): @abc.abstractmethod def is_ready(self): - """ - In case of asynchronous execution checks if one can submit input data + """In case of asynchronous execution checks if one can submit input data to the model for inference, or all infer requests are busy. Returns: @@ -147,23 +135,19 @@ def is_ready(self): @abc.abstractmethod def await_all(self): - """ - In case of asynchronous execution waits the completion of all + """In case of asynchronous execution waits the completion of all busy infer requests. """ @abc.abstractmethod def await_any(self): - """ - In case of asynchronous execution waits the completion of any + """In case of asynchronous execution waits the completion of any busy infer request until it becomes available for the data submission. """ @abc.abstractmethod def get_rt_info(self, path): - """ - Forwards to openvino.Model.get_rt_info(path) - """ + """Forwards to openvino.Model.get_rt_info(path)""" @abc.abstractmethod def embed_preprocessing( @@ -171,7 +155,7 @@ def embed_preprocessing( layout, resize_mode: str, interpolation_mode, - target_shape: Tuple[int], + target_shape: tuple[int], pad_value, dtype: type = int, brg2rgb=False, @@ -179,6 +163,4 @@ def embed_preprocessing( scale=None, input_idx=0, ): - """ - Embeds preprocessing into the model using OpenVINO preprocessing API - """ + """Embeds preprocessing into the model using OpenVINO preprocessing API""" diff --git a/model_api/python/model_api/adapters/onnx_adapter.py b/model_api/python/model_api/adapters/onnx_adapter.py index a8784660..2a75c4b5 100644 --- a/model_api/python/model_api/adapters/onnx_adapter.py +++ b/model_api/python/model_api/adapters/onnx_adapter.py @@ -1,17 +1,16 @@ -""" - Copyright (c) 2023-2024 Intel Corporation +"""Copyright (c) 2023-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 +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 +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. +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. """ import sys @@ -35,8 +34,7 @@ class ONNXRuntimeAdapter(InferenceAdapter): - """ - This inference adapter allows running ONNX models via ONNXRuntime. + """This inference adapter allows running ONNX models via ONNXRuntime. The adapter has limited functionality: it supports only image models generated by OpenVINO training extensions (OTX: https://github.com/openvinotoolkit/training_extensions/). Each onnx file generated by OTX contains ModelAPI-style metadata, which is used for @@ -47,19 +45,23 @@ class ONNXRuntimeAdapter(InferenceAdapter): """ def __init__(self, model: str, ort_options: dict = {}): - """ - Args: - model (str): Filename or serialized ONNX model in a byte string. - ort_options (dict): parameters that will be forwarded to onnxruntime.InferenceSession + """Args: + model (str): Filename or serialized ONNX model in a byte string. + ort_options (dict): parameters that will be forwarded to onnxruntime.InferenceSession """ loaded_model = onnx.load(model) inferred_model = SymbolicShapeInference.infer_shapes( - loaded_model, int(sys.maxsize / 2), False, False, False + loaded_model, + int(sys.maxsize / 2), + False, + False, + False, ) self.session = ort.InferenceSession( - inferred_model.SerializeToString(), **ort_options + inferred_model.SerializeToString(), + **ort_options, ) self.output_names = [o.name for o in self.session.get_outputs()] self.onnx_metadata = load_parameters_from_onnx(inferred_model) @@ -98,11 +100,11 @@ def infer_sync(self, dict_data): preprocessed_input = self.preprocessor(dict_data[input.name]) if dict_data[input.name].dtype != _onnx2np_precision[input.type]: inputs[input.name] = ort.OrtValue.ortvalue_from_numpy( - preprocessed_input.astype(_onnx2np_precision[input.type]) + preprocessed_input.astype(_onnx2np_precision[input.type]), ) else: inputs[input.name] = ort.OrtValue.ortvalue_from_numpy( - preprocessed_input + preprocessed_input, ) raw_result = self.session.run(self.output_names, inputs) @@ -166,7 +168,8 @@ def embed_preprocessing( preproc_funcs.append(partial(change_layout, layout=layout)) self.preprocessor = reduce( - lambda f, g: lambda x: f(g(x)), reversed(preproc_funcs) + lambda f, g: lambda x: f(g(x)), + reversed(preproc_funcs), ) def reshape_model(self, new_shape): diff --git a/model_api/python/model_api/adapters/openvino_adapter.py b/model_api/python/model_api/adapters/openvino_adapter.py index 4d096558..5ea897a4 100644 --- a/model_api/python/model_api/adapters/openvino_adapter.py +++ b/model_api/python/model_api/adapters/openvino_adapter.py @@ -1,22 +1,20 @@ -""" - Copyright (c) 2021-2024 Intel Corporation +"""Copyright (c) 2021-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 +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 +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. +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. """ import logging as log from pathlib import Path -from typing import Dict, Set, Tuple try: import openvino.runtime as ov @@ -52,7 +50,7 @@ def create_core(): raise ImportError("The OpenVINO package is not installed") log.info("OpenVINO Runtime") - log.info("\tbuild: {}".format(get_version())) + log.info(f"\tbuild: {get_version()}") return Core() @@ -71,7 +69,7 @@ def parse_devices(device_string): return (device_string,) -def parse_value_per_device(devices: Set[str], values_string: str) -> Dict[str, int]: +def parse_value_per_device(devices: set[str], values_string: str) -> dict[str, int]: """Format: :,: or just """ values_string_upper = values_string.upper() result = {} @@ -90,8 +88,10 @@ def parse_value_per_device(devices: Set[str], values_string: str) -> Dict[str, i def get_user_config( - flags_d: str, flags_nstreams: str, flags_nthreads: int -) -> Dict[str, str]: + flags_d: str, + flags_nstreams: str, + flags_nthreads: int, +) -> dict[str, str]: config = {} devices = set(parse_devices(flags_d)) @@ -104,17 +104,9 @@ def get_user_config( config["INFERENCE_NUM_THREADS"] = str(flags_nthreads) # for CPU execution, more throughput-oriented execution via streams - config["NUM_STREAMS"] = ( - str(device_nstreams[device]) - if device in device_nstreams - else "NUM_STREAMS_AUTO" - ) + config["NUM_STREAMS"] = str(device_nstreams[device]) if device in device_nstreams else "NUM_STREAMS_AUTO" elif device == "GPU": - config["NUM_STREAMS"] = ( - str(device_nstreams[device]) - if device in device_nstreams - else "NUM_STREAMS_AUTO" - ) + config["NUM_STREAMS"] = str(device_nstreams[device]) if device in device_nstreams else "NUM_STREAMS_AUTO" if "MULTI" in flags_d and "CPU" in devices: # multi-device execution with the CPU + GPU performs best with GPU throttling hint, # which releases another CPU thread (that is otherwise used by the GPU driver for active polling) @@ -123,9 +115,7 @@ def get_user_config( class OpenvinoAdapter(InferenceAdapter): - """ - Works with OpenVINO model - """ + """Works with OpenVINO model""" def __init__( self, @@ -140,9 +130,7 @@ def __init__( download_dir=None, cache_dir=None, ): - """ - precision, download_dir and cache_dir are ignored if model is a path to a file - """ + """precision, download_dir and cache_dir are ignored if model is a path to a file""" self.core = core self.model_path = model self.device = device @@ -150,7 +138,7 @@ def __init__( self.max_num_requests = max_num_requests self.model_parameters = model_parameters self.model_parameters["input_layouts"] = Layout.parse_layouts( - self.model_parameters.get("input_layouts", None) + self.model_parameters.get("input_layouts", None), ) self.is_onnx_file = False self.onnx_metadata = {} @@ -159,25 +147,26 @@ def __init__( if Path(self.model_path).suffix == ".onnx" and weights_path: log.warning( 'For model in ONNX format should set only "model_path" parameter.' - 'The "weights_path" will be omitted' + 'The "weights_path" will be omitted', ) if Path(self.model_path).suffix == ".onnx" and not weights_path: import onnx self.is_onnx_file = True self.onnx_metadata = load_parameters_from_onnx( - onnx.load(self.model_path) + onnx.load(self.model_path), ) self.model_from_buffer = isinstance(self.model_path, bytes) and isinstance( - weights_path, bytes + weights_path, + bytes, ) model_from_file = not self.model_from_buffer and Path(self.model_path).is_file() if model_from_file or self.model_from_buffer: log.info( "Reading model {}".format( - "from buffer" if self.model_from_buffer else self.model_path - ) + "from buffer" if self.model_from_buffer else self.model_path, + ), ) self.model = core.read_model(self.model_path, weights_path) return @@ -199,20 +188,23 @@ def __init__( def load_model(self): self.compiled_model = self.core.compile_model( - self.model, self.device, self.plugin_config + self.model, + self.device, + self.plugin_config, ) self.async_queue = AsyncInferQueue(self.compiled_model, self.max_num_requests) if self.max_num_requests == 0: # +1 to use it as a buffer of the pipeline self.async_queue = AsyncInferQueue( - self.compiled_model, len(self.async_queue) + 1 + self.compiled_model, + len(self.async_queue) + 1, ) log.info( "The model {} is loaded to {}".format( "from buffer" if self.model_from_buffer else self.model_path, self.device, - ) + ), ) self.log_runtime_settings() @@ -222,20 +214,20 @@ def log_runtime_settings(self): for device in devices: try: nstreams = self.compiled_model.get_property( - device + "_THROUGHPUT_STREAMS" + device + "_THROUGHPUT_STREAMS", ) - log.info("\tDevice: {}".format(device)) - log.info("\t\tNumber of streams: {}".format(nstreams)) + log.info(f"\tDevice: {device}") + log.info(f"\t\tNumber of streams: {nstreams}") if device == "CPU": nthreads = self.compiled_model.get_property("CPU_THREADS_NUM") log.info( "\t\tNumber of threads: {}".format( - nthreads if int(nthreads) else "AUTO" - ) + nthreads if int(nthreads) else "AUTO", + ), ) except RuntimeError: pass - log.info("\tNumber of model infer requests: {}".format(len(self.async_queue))) + log.info(f"\tNumber of model infer requests: {len(self.async_queue)}") def get_input_layers(self): inputs = {} @@ -255,25 +247,22 @@ def get_layout_for_input(self, input, shape=None) -> str: input_layout = "" if self.model_parameters["input_layouts"]: input_layout = Layout.from_user_layouts( - input.get_names(), self.model_parameters["input_layouts"] + input.get_names(), + self.model_parameters["input_layouts"], ) if not input_layout: if not layout_helpers.get_layout(input).empty: input_layout = Layout.from_openvino(input) else: input_layout = Layout.from_shape( - shape if shape is not None else input.shape + shape if shape is not None else input.shape, ) return input_layout def get_output_layers(self): outputs = {} for i, output in enumerate(self.model.outputs): - output_shape = ( - output.partial_shape.get_min_shape() - if self.model.is_dynamic() - else output.shape - ) + output_shape = output.partial_shape.get_min_shape() if self.model.is_dynamic() else output.shape output_name = output.get_any_name() if output.get_names() else output outputs[output_name] = Metadata( @@ -287,14 +276,7 @@ def get_output_layers(self): def reshape_model(self, new_shape): new_shape = { name: PartialShape( - [ - ( - Dimension(dim) - if not isinstance(dim, tuple) - else Dimension(dim[0], dim[1]) - ) - for dim in shape - ] + [(Dimension(dim) if not isinstance(dim, tuple) else Dimension(dim[0], dim[1])) for dim in shape], ) for name, shape in new_shape.items() } @@ -304,9 +286,7 @@ def get_raw_result(self, request): return {key: request.get_tensor(key).data for key in self.get_output_layers()} def copy_raw_result(self, request): - return { - key: request.get_tensor(key).data.copy() for key in self.get_output_layers() - } + return {key: request.get_tensor(key).data.copy() for key in self.get_output_layers()} def infer_sync(self, dict_data): self.infer_request = self.async_queue[self.async_queue.get_idle_request_id()] @@ -343,7 +323,8 @@ def operations_by_type(self, operation_type): if node.get_type_name() == operation_type: layer_name = node.get_friendly_name() layers_info[layer_name] = Metadata( - type=node.get_type_name(), meta=node.get_attributes() + type=node.get_type_name(), + meta=node.get_attributes(), ) return layers_info @@ -357,7 +338,7 @@ def embed_preprocessing( layout, resize_mode: str, interpolation_mode, - target_shape: Tuple[int], + target_shape: tuple[int], pad_value, dtype: type = int, brg2rgb=False, @@ -374,7 +355,7 @@ def embed_preprocessing( ppp.input(input_idx).tensor().set_element_type(Type.f32) ppp.input(input_idx).tensor().set_layout(ov.Layout("NHWC")).set_color_format( - ColorFormat.BGR + ColorFormat.BGR, ) INTERPOLATION_MODE_MAP = { @@ -402,12 +383,12 @@ def embed_preprocessing( target_shape, INTERPOLATION_MODE_MAP[interpolation_mode], pad_value, - ) + ), ) else: raise ValueError( - f"Upsupported resize type in model preprocessing: {resize_mode}" + f"Upsupported resize type in model preprocessing: {resize_mode}", ) # Handle layout diff --git a/model_api/python/model_api/adapters/ovms_adapter.py b/model_api/python/model_api/adapters/ovms_adapter.py index 509b2ba9..b18ef372 100644 --- a/model_api/python/model_api/adapters/ovms_adapter.py +++ b/model_api/python/model_api/adapters/ovms_adapter.py @@ -1,17 +1,16 @@ -""" - Copyright (c) 2021-2024 Intel Corporation +"""Copyright (c) 2021-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 +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 +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. +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. """ import re @@ -23,22 +22,21 @@ class OVMSAdapter(InferenceAdapter): - """ - Class that allows working with models served by the OpenVINO Model Server - """ + """Class that allows working with models served by the OpenVINO Model Server""" def __init__(self, target_model: str): """Expected format:
:/models/[:]""" import ovmsclient service_url, self.model_name, self.model_version = _parse_model_arg( - target_model + target_model, ) self.client = ovmsclient.make_grpc_client(url=service_url) _verify_model_available(self.client, self.model_name, self.model_version) self.metadata = self.client.get_model_metadata( - model_name=self.model_name, model_version=self.model_version + model_name=self.model_name, + model_version=self.model_version, ) def get_input_layers(self): @@ -65,19 +63,23 @@ def get_output_layers(self): def infer_sync(self, dict_data): inputs = _prepare_inputs(dict_data, self.metadata["inputs"]) raw_result = self.client.predict( - inputs, model_name=self.model_name, model_version=self.model_version + inputs, + model_name=self.model_name, + model_version=self.model_version, ) # For models with single output ovmsclient returns ndarray with results, # so the dict must be created to correctly implement interface. if isinstance(raw_result, np.ndarray): - output_name = next(iter((self.metadata["outputs"].keys()))) + output_name = next(iter(self.metadata["outputs"].keys())) return {output_name: raw_result} return raw_result def infer_async(self, dict_data, callback_data): inputs = _prepare_inputs(dict_data, self.metadata["inputs"]) raw_result = self.client.predict( - inputs, model_name=self.model_name, model_version=self.model_version + inputs, + model_name=self.model_name, + model_version=self.model_version, ) # For models with single output ovmsclient returns ndarray with results, # so the dict must be created to correctly implement interface. @@ -154,7 +156,8 @@ def _parse_model_arg(target_model: str): raise TypeError("target_model must be str") # Expected format:
:/models/[:] if not re.fullmatch( - r"(\w+\.*\-*)*\w+:\d+\/models\/[a-zA-Z0-9._-]+(\:\d+)*", target_model + r"(\w+\.*\-*)*\w+:\d+\/models\/[a-zA-Z0-9._-]+(\:\d+)*", + target_model, ): raise ValueError("invalid --model option format") service_url, _, model = target_model.split("/") @@ -175,13 +178,13 @@ def _verify_model_available(client, model_name, model_version): model_status = client.get_model_status(model_name, model_version) except ovmsclient.ModelNotFoundError as e: raise RuntimeError( - f"Requested model: {model_name}, version: {version} has not been found" + f"Requested model: {model_name}, version: {version} has not been found", ) from e target_version = max(model_status.keys()) version_status = model_status[target_version] if version_status["state"] != "AVAILABLE" or version_status["error_code"] != 0: raise RuntimeError( - f"Requested model: {model_name}, version: {version} is not in available state" + f"Requested model: {model_name}, version: {version} is not in available state", ) diff --git a/model_api/python/model_api/adapters/utils.py b/model_api/python/model_api/adapters/utils.py index cba16073..32ceecc8 100644 --- a/model_api/python/model_api/adapters/utils.py +++ b/model_api/python/model_api/adapters/utils.py @@ -1,22 +1,22 @@ -""" - Copyright (C) 2022-2024 Intel Corporation +"""Copyright (C) 2022-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 +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 +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. +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 __future__ import annotations # TODO: remove when Python3.9 support is dropped + import math from functools import partial -from typing import Optional import cv2 import numpy as np @@ -32,9 +32,7 @@ def __init__(self, layout="") -> None: @staticmethod def from_shape(shape): - """ - Create Layout from given shape - """ + """Create Layout from given shape""" if len(shape) == 2: return "NC" if len(shape) == 3: @@ -45,36 +43,28 @@ def from_shape(shape): return "NSTHWC" if shape[5] in range(1, 5) else "NSCTHW" raise RuntimeError( - "Get layout from shape method doesn't support {}D shape".format(len(shape)) + f"Get layout from shape method doesn't support {len(shape)}D shape", ) @staticmethod def from_openvino(input): - """ - Create Layout from openvino input - """ + """Create Layout from openvino input""" return layout_helpers.get_layout(input).to_string().strip("[]").replace(",", "") @staticmethod def from_user_layouts(input_names: set, user_layouts: dict): - """ - Create Layout for input based on user info - """ + """Create Layout for input based on user info""" for input_name in input_names: if input_name in user_layouts: return user_layouts[input_name] return user_layouts.get("", "") @staticmethod - def parse_layouts(layout_string: str) -> Optional[dict]: - """ - Parse layout parameter in format "input0:NCHW,input1:NC" or "NCHW" (applied to all inputs) - """ + def parse_layouts(layout_string: str) -> dict | None: + """Parse layout parameter in format "input0:NCHW,input1:NC" or "NCHW" (applied to all inputs)""" if not layout_string: return None - search_string = ( - layout_string if layout_string.rfind(":") != -1 else ":" + layout_string - ) + search_string = layout_string if layout_string.rfind(":") != -1 else ":" + layout_string colon_pos = search_string.rfind(":") user_layouts = {} while colon_pos != -1: @@ -113,10 +103,12 @@ def resize_image_letterbox_graph(input: Output, size, interpolation, pad_value): h_ratio = opset.divide(opset.constant(h, dtype=Type.f32), ih) scale = opset.minimum(w_ratio, h_ratio) nw = opset.convert( - opset.round(opset.multiply(iw, scale), "half_to_even"), destination_type="i32" + opset.round(opset.multiply(iw, scale), "half_to_even"), + destination_type="i32", ) nh = opset.convert( - opset.round(opset.multiply(ih, scale), "half_to_even"), destination_type="i32" + opset.round(opset.multiply(ih, scale), "half_to_even"), + destination_type="i32", ) new_size = opset.concat([opset.unsqueeze(nh, 0), opset.unsqueeze(nw, 0)], axis=0) image = opset.interpolate( @@ -136,10 +128,12 @@ def resize_image_letterbox_graph(input: Output, size, interpolation, pad_value): opset.constant(2, dtype=np.int32), ) dx_border = opset.subtract( - opset.subtract(opset.constant(w, dtype=np.int32), nw), dx + opset.subtract(opset.constant(w, dtype=np.int32), nw), + dx, ) dy_border = opset.subtract( - opset.subtract(opset.constant(h, dtype=np.int32), nh), dy + opset.subtract(opset.constant(h, dtype=np.int32), nh), + dy, ) pads_begin = opset.concat( [ @@ -194,11 +188,17 @@ def crop_resize_graph(input: Output, size): ) then_stop = opset.add(then_offset, iw_t) then_cropped_frame = opset.slice( - image_t, start=then_offset, stop=then_stop, step=[1], axes=[h_axis] + image_t, + start=then_offset, + stop=then_stop, + step=[1], + axes=[h_axis], ) then_body_res_1 = opset.result(then_cropped_frame) then_body = Model( - [then_body_res_1], [image_t, iw_t, ih_t], "then_body_function" + [then_body_res_1], + [image_t, iw_t, ih_t], + "then_body_function", ) # else_body @@ -211,11 +211,17 @@ def crop_resize_graph(input: Output, size): ) else_stop = opset.add(else_offset, ih_e) else_cropped_frame = opset.slice( - image_e, start=else_offset, stop=else_stop, step=[1], axes=[w_axis] + image_e, + start=else_offset, + stop=else_stop, + step=[1], + axes=[w_axis], ) else_body_res_1 = opset.result(else_cropped_frame) else_body = Model( - [else_body_res_1], [image_e, iw_e, ih_e], "else_body_function" + [else_body_res_1], + [image_e, iw_e, ih_e], + "else_body_function", ) # if @@ -231,34 +237,46 @@ def crop_resize_graph(input: Output, size): elif desired_aspect_ratio < 1: new_width = opset.floor( opset.multiply( - opset.convert(ih, destination_type="f32"), desired_aspect_ratio - ) + opset.convert(ih, destination_type="f32"), + desired_aspect_ratio, + ), ) offset = opset.unsqueeze( opset.divide( - opset.subtract(iw, new_width), opset.constant(2, dtype=np.int32) + opset.subtract(iw, new_width), + opset.constant(2, dtype=np.int32), ), 0, ) stop = opset.add(offset, new_width) cropped_frame = opset.slice( - input, start=offset, stop=stop, step=[1], axes=[w_axis] + input, + start=offset, + stop=stop, + step=[1], + axes=[w_axis], ) elif desired_aspect_ratio > 1: new_hight = opset.floor( opset.multiply( - opset.convert(iw, destination_type="f32"), desired_aspect_ratio - ) + opset.convert(iw, destination_type="f32"), + desired_aspect_ratio, + ), ) offset = opset.unsqueeze( opset.divide( - opset.subtract(ih, new_hight), opset.constant(2, dtype=np.int32) + opset.subtract(ih, new_hight), + opset.constant(2, dtype=np.int32), ), 0, ) stop = opset.add(offset, new_hight) cropped_frame = opset.slice( - input, start=offset, stop=stop, step=[1], axes=[h_axis] + input, + start=offset, + stop=stop, + step=[1], + axes=[h_axis], ) target_size = list(size) @@ -275,7 +293,11 @@ def crop_resize_graph(input: Output, size): def resize_image_graph( - input: Output, size, keep_aspect_ratio, interpolation, pad_value + input: Output, + size, + keep_aspect_ratio, + interpolation, + pad_value, ): if not isinstance(pad_value, int): raise RuntimeError("pad_value must be int") @@ -310,10 +332,12 @@ def resize_image_graph( h_ratio = opset.divide(np.float32(h), ih) scale = opset.minimum(w_ratio, h_ratio) nw = opset.convert( - opset.round(opset.multiply(iw, scale), "half_to_even"), destination_type="i32" + opset.round(opset.multiply(iw, scale), "half_to_even"), + destination_type="i32", ) nh = opset.convert( - opset.round(opset.multiply(ih, scale), "half_to_even"), destination_type="i32" + opset.round(opset.multiply(ih, scale), "half_to_even"), + destination_type="i32", ) new_size = opset.concat([opset.unsqueeze(nh, 0), opset.unsqueeze(nw, 0)], axis=0) image = opset.interpolate( @@ -353,7 +377,7 @@ def resize_image(size, interpolation, pad_value): keep_aspect_ratio=False, interpolation=interpolation, pad_value=pad_value, - ) + ), ) @@ -365,7 +389,7 @@ def resize_image_with_aspect(size, interpolation, pad_value): keep_aspect_ratio=True, interpolation=interpolation, pad_value=pad_value, - ) + ), ) @@ -380,7 +404,7 @@ def resize_image_letterbox(size, interpolation, pad_value): size=size, interpolation=interpolation, pad_value=pad_value, - ) + ), ) @@ -412,7 +436,7 @@ def get_rt_info_from_dict(rt_info_dict, path): return OVAny(value) except KeyError: raise RuntimeError( - "Cannot get runtime attribute. Path to runtime attribute is incorrect." + "Cannot get runtime attribute. Path to runtime attribute is incorrect.", ) @@ -453,7 +477,10 @@ def resize_image_with_aspect_ocv(image, size, interpolation=cv2.INTER_LINEAR): def resize_image_letterbox_ocv( - image, size, interpolation=cv2.INTER_LINEAR, pad_value=0 + image, + size, + interpolation=cv2.INTER_LINEAR, + pad_value=0, ): ih, iw = image.shape[0:2] w, h = size @@ -510,20 +537,15 @@ def crop_resize_ocv(image, size): class InputTransform: def __init__( - self, reverse_input_channels=False, mean_values=None, scale_values=None + self, + reverse_input_channels=False, + mean_values=None, + scale_values=None, ): self.reverse_input_channels = reverse_input_channels self.is_trivial = not (reverse_input_channels or mean_values or scale_values) - self.means = ( - np.array(mean_values, dtype=np.float32) - if mean_values - else np.array([0.0, 0.0, 0.0]) - ) - self.std_scales = ( - np.array(scale_values, dtype=np.float32) - if scale_values - else np.array([1.0, 1.0, 1.0]) - ) + self.means = np.array(mean_values, dtype=np.float32) if mean_values else np.array([0.0, 0.0, 0.0]) + self.std_scales = np.array(scale_values, dtype=np.float32) if scale_values else np.array([1.0, 1.0, 1.0]) def __call__(self, inputs): if self.is_trivial: diff --git a/model_api/python/model_api/models/__init__.py b/model_api/python/model_api/models/__init__.py index 86a4a61c..a2d4a54e 100644 --- a/model_api/python/model_api/models/__init__.py +++ b/model_api/python/model_api/models/__init__.py @@ -1,17 +1,16 @@ -""" - Copyright (C) 2021-2024 Intel Corporation +"""Copyright (C) 2021-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 +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 +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. +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 .action_classification import ActionClassificationModel diff --git a/model_api/python/model_api/models/action_classification.py b/model_api/python/model_api/models/action_classification.py index f887642c..89a37637 100644 --- a/model_api/python/model_api/models/action_classification.py +++ b/model_api/python/model_api/models/action_classification.py @@ -1,17 +1,16 @@ -""" - Copyright (C) 2024 Intel Corporation +"""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 +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 +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. +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 __future__ import annotations @@ -19,6 +18,7 @@ from typing import TYPE_CHECKING, Any import numpy as np + from model_api.adapters.utils import RESIZE_TYPES, InputTransform from .model import Model @@ -75,16 +75,14 @@ def __init__( self.image_blob_name = self.image_blob_names[0] self.nscthw_layout = "NSCTHW" in self.inputs[self.image_blob_name].layout if self.nscthw_layout: - self.n, self.s, self.c, self.t, self.h, self.w = self.inputs[ - self.image_blob_name - ].shape + self.n, self.s, self.c, self.t, self.h, self.w = self.inputs[self.image_blob_name].shape else: - self.n, self.s, self.t, self.h, self.w, self.c = self.inputs[ - self.image_blob_name - ].shape + self.n, self.s, self.t, self.h, self.w, self.c = self.inputs[self.image_blob_name].shape self.resize = RESIZE_TYPES[self.resize_type] self.input_transform = InputTransform( - self.reverse_input_channels, self.mean_values, self.scale_values + self.reverse_input_channels, + self.mean_values, + self.scale_values, ) if self.path_to_labels: self.labels = load_labels(self.path_to_labels) @@ -100,7 +98,7 @@ def parameters(cls) -> dict[str, Any]: { "labels": ListValue(description="List of class labels"), "path_to_labels": StringValue( - description="Path to file with labels. Overrides the labels, if they sets via 'labels' parameter" + description="Path to file with labels. Overrides the labels, if they sets via 'labels' parameter", ), "mean_values": ListValue( description="Normalization values, which will be subtracted from image channels for image-input layer during preprocessing", @@ -119,13 +117,14 @@ def parameters(cls) -> dict[str, Any]: description="Type of input image resizing", ), "reverse_input_channels": BooleanValue( - default_value=False, description="Reverse the input channel order" + default_value=False, + description="Reverse the input channel order", ), "scale_values": ListValue( default_value=[], description="Normalization values, which will divide the image channels for image-input layer", ), - } + }, ) return parameters @@ -145,16 +144,17 @@ def _get_inputs(self) -> tuple[list[str], list[str]]: image_blob_names.append(name) else: self.raise_error( - "Failed to identify the input for ImageModel: only 4D and 6D input layer supported" + "Failed to identify the input for ImageModel: only 4D and 6D input layer supported", ) if not image_blob_names: self.raise_error( - "Failed to identify the input for the image: no 6D input layer found" + "Failed to identify the input for the image: no 6D input layer found", ) return image_blob_names def preprocess( - self, inputs: np.ndarray + self, + inputs: np.ndarray, ) -> tuple[dict[str, np.ndarray], dict[str, tuple[int, ...]]]: """Data preprocess method @@ -191,12 +191,9 @@ def preprocess( "original_shape": inputs.shape, "resized_shape": (self.n, self.s, self.c, self.t, self.h, self.w), } - resized_inputs = [ - self.resize(frame, (self.w, self.h), pad_value=self.pad_value) - for frame in inputs - ] + resized_inputs = [self.resize(frame, (self.w, self.h), pad_value=self.pad_value) for frame in inputs] np_frames = self._change_layout( - [self.input_transform(inputs) for inputs in resized_inputs] + [self.input_transform(inputs) for inputs in resized_inputs], ) dict_inputs = {self.image_blob_name: np_frames} return dict_inputs, meta @@ -216,7 +213,9 @@ def _change_layout(self, inputs: list[np.ndarray]) -> np.ndarray: return np_inputs def postprocess( - self, outputs: dict[str, np.ndarray], meta: dict[str, Any] + self, + outputs: dict[str, np.ndarray], + meta: dict[str, Any], ) -> ClassificationResult: """Post-process.""" logits = next(iter(outputs.values())).squeeze() diff --git a/model_api/python/model_api/models/anomaly.py b/model_api/python/model_api/models/anomaly.py index 717a5406..e0bb36cf 100644 --- a/model_api/python/model_api/models/anomaly.py +++ b/model_api/python/model_api/models/anomaly.py @@ -63,9 +63,7 @@ def postprocess(self, outputs: dict[str, np.ndarray], meta: dict[str, Any]): anomaly_map = predictions.squeeze() pred_score = anomaly_map.reshape(-1).max() - pred_label = ( - self.labels[1] if pred_score > self.image_threshold else self.labels[0] - ) + pred_label = self.labels[1] if pred_score > self.image_threshold else self.labels[0] assert anomaly_map is not None pred_mask = (anomaly_map >= self.pixel_threshold).astype(np.uint8) @@ -73,7 +71,8 @@ def postprocess(self, outputs: dict[str, np.ndarray], meta: dict[str, Any]): anomaly_map *= 255 anomaly_map = np.round(anomaly_map).astype(np.uint8) pred_mask = cv2.resize( - pred_mask, (meta["original_shape"][1], meta["original_shape"][0]) + pred_mask, + (meta["original_shape"][1], meta["original_shape"][0]), ) # normalize @@ -85,7 +84,8 @@ def postprocess(self, outputs: dict[str, np.ndarray], meta: dict[str, Any]): # resize outputs if anomaly_map is not None: anomaly_map = cv2.resize( - anomaly_map, (meta["original_shape"][1], meta["original_shape"][0]) + anomaly_map, + (meta["original_shape"][1], meta["original_shape"][0]), ) if self.task == "detection": @@ -105,19 +105,24 @@ def parameters(cls) -> dict: parameters.update( { "image_threshold": NumericalValue( - description="Image threshold", min=0.0, default_value=0.5 + description="Image threshold", + min=0.0, + default_value=0.5, ), "pixel_threshold": NumericalValue( - description="Pixel threshold", min=0.0, default_value=0.5 + description="Pixel threshold", + min=0.0, + default_value=0.5, ), "normalization_scale": NumericalValue( description="Value used for normalization", ), "task": StringValue( - description="Task type", default_value="segmentation" + description="Task type", + default_value="segmentation", ), "labels": ListValue(description="List of class labels"), - } + }, ) return parameters diff --git a/model_api/python/model_api/models/classification.py b/model_api/python/model_api/models/classification.py index b0470c7d..d952692b 100644 --- a/model_api/python/model_api/models/classification.py +++ b/model_api/python/model_api/models/classification.py @@ -1,17 +1,16 @@ -""" - Copyright (c) 2021-2024 Intel Corporation +"""Copyright (c) 2021-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 +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 +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. +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 __future__ import annotations # TODO: remove when Python3.9 support is dropped @@ -38,7 +37,7 @@ def __init__(self, inference_adapter, configuration=dict(), preload=False): 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 1 == len(self.outputs): + if len(self.outputs) == 1: self._verify_signle_output() self.raw_scores_name = _raw_scores_name @@ -53,7 +52,7 @@ def __init__(self, inference_adapter, configuration=dict(), preload=False): if self.hierarchical_postproc == "probabilistic": self.labels_resolver = ProbabilisticLabelsResolver( - self.hierarchical_info + self.hierarchical_info, ) else: self.labels_resolver = GreedyLabelsResolver(self.hierarchical_info) @@ -73,7 +72,9 @@ def __init__(self, inference_adapter, configuration=dict(), preload=False): try: addOrFindSoftmaxAndTopkOutputs( - self.inference_adapter, self.topk, self.output_raw_scores + self.inference_adapter, + self.topk, + self.output_raw_scores, ) self.embedded_topk = True self.out_layer_names = ["indices", "scores"] @@ -98,7 +99,7 @@ def __init__(self, inference_adapter, configuration=dict(), preload=False): self.load() def _load_labels(self, labels_file): - with open(labels_file, "r") as f: + with open(labels_file) as f: labels = [] for s in f: begin_idx = s.find(" ") @@ -114,12 +115,12 @@ def _verify_signle_output(self): if len(layer_shape) != 2 and len(layer_shape) != 4: self.raise_error( - "The Classification model wrapper supports topologies only with 2D or 4D output" + "The Classification model wrapper supports topologies only with 2D or 4D output", ) if len(layer_shape) == 4 and (layer_shape[2] != 1 or layer_shape[3] != 1): self.raise_error( "The Classification model wrapper supports topologies only with 4D " - "output which has last two dimensions of size 1" + "output which has last two dimensions of size 1", ) if self.labels: if layer_shape[1] == len(self.labels) + 1: @@ -128,9 +129,7 @@ def _verify_signle_output(self): if layer_shape[1] > len(self.labels): self.raise_error( "Model's number of classes must be greater then " - "number of parsed labels ({}, {})".format( - layer_shape[1], len(self.labels) - ) + f"number of parsed labels ({layer_shape[1]}, {len(self.labels)})", ) @classmethod @@ -146,10 +145,11 @@ def parameters(cls): ), "labels": ListValue(description="List of class labels"), "path_to_labels": StringValue( - description="Path to file with labels. Overrides the labels, if they sets via 'labels' parameter" + description="Path to file with labels. Overrides the labels, if they sets via 'labels' parameter", ), "multilabel": BooleanValue( - default_value=False, description="Predict a set of labels per image" + default_value=False, + description="Predict a set of labels per image", ), "hierarchical": BooleanValue( default_value=False, @@ -160,7 +160,8 @@ def parameters(cls): description="Extra config for decoding hierarchical predictions", ), "confidence_threshold": NumericalValue( - default_value=0.5, description="Predict a set of labels per image" + default_value=0.5, + description="Predict a set of labels per image", ), "output_raw_scores": BooleanValue( default_value=False, @@ -171,18 +172,18 @@ def parameters(cls): choices=("probabilistic", "greedy"), description="Type of hierarchical postprocessing", ), - } + }, ) return parameters def postprocess(self, outputs, meta): if self.multilabel: result = self.get_multilabel_predictions( - outputs[self.out_layer_names[0]].squeeze() + outputs[self.out_layer_names[0]].squeeze(), ) elif self.hierarchical: result = self.get_hierarchical_predictions( - outputs[self.out_layer_names[0]].squeeze() + outputs[self.out_layer_names[0]].squeeze(), ) else: result = self.get_multiclass_predictions(outputs) @@ -199,8 +200,7 @@ def postprocess(self, outputs, meta): ) def get_saliency_maps(self, outputs: dict) -> np.ndarray: - """ - Returns saliency map model output. In hierarchical case reorders saliency maps + """Returns saliency map model output. In hierarchical case reorders saliency maps to match the order of labels in .XML meta. """ saliency_maps = outputs.get(_saliency_map_name, np.ndarray(0)) @@ -224,21 +224,18 @@ def get_all_probs(self, logits: np.ndarray): probs = np.copy(logits) cls_heads_info = self.hierarchical_info["cls_heads_info"] for i in range(cls_heads_info["num_multiclass_heads"]): - logits_begin, logits_end = cls_heads_info["head_idx_to_logits_range"][ - str(i) - ] + logits_begin, logits_end = cls_heads_info["head_idx_to_logits_range"][str(i)] probs[logits_begin:logits_end] = softmax_numpy( - logits[logits_begin:logits_end] + logits[logits_begin:logits_end], ) if cls_heads_info["num_multilabel_classes"]: logits_begin = cls_heads_info["num_single_label_classes"] probs[logits_begin:] = sigmoid_numpy(logits[logits_begin:]) + elif self.embedded_topk: + probs = logits.reshape(-1) else: - if self.embedded_topk: - probs = logits.reshape(-1) - else: - probs = softmax_numpy(logits.reshape(-1)) + probs = softmax_numpy(logits.reshape(-1)) return probs def get_hierarchical_predictions(self, logits: np.ndarray): @@ -246,9 +243,7 @@ def get_hierarchical_predictions(self, logits: np.ndarray): predicted_scores = [] cls_heads_info = self.hierarchical_info["cls_heads_info"] for i in range(cls_heads_info["num_multiclass_heads"]): - logits_begin, logits_end = cls_heads_info["head_idx_to_logits_range"][ - str(i) - ] + logits_begin, logits_end = cls_heads_info["head_idx_to_logits_range"][str(i)] head_logits = logits[logits_begin:logits_end] head_logits = softmax_numpy(head_logits) j = np.argmax(head_logits) @@ -263,9 +258,7 @@ def get_hierarchical_predictions(self, logits: np.ndarray): for i in range(head_logits.shape[0]): if head_logits[i] > self.confidence_threshold: - label_str = cls_heads_info["all_groups"][ - cls_heads_info["num_multiclass_heads"] + i - ][0] + label_str = cls_heads_info["all_groups"][cls_heads_info["num_multiclass_heads"] + i][0] predicted_labels.append(label_str) predicted_scores.append(head_logits[i]) @@ -299,24 +292,14 @@ def get_multiclass_predictions(self, outputs): def addOrFindSoftmaxAndTopkOutputs(inference_adapter, topk, output_raw_scores): 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() - ) - if "Softmax" == output_node.get_type_name(): + output_node = inference_adapter.model.get_output_op(i).input(0).get_source_output().get_node() + if output_node.get_type_name() == "Softmax": softmaxNode = output_node - elif "TopK" == output_node.get_type_name(): + elif output_node.get_type_name() == "TopK": return if softmaxNode is None: - logitsNode = ( - inference_adapter.model.get_output_op(0) - .input(0) - .get_source_output() - .get_node() - ) + logitsNode = inference_adapter.model.get_output_op(0).input(0).get_source_output().get_node() softmaxNode = opset.softmax(logitsNode.output(0), 1) k = opset.constant(topk, np.int32) topkNode = opset.topk(softmaxNode, k, 1, "max", "value") @@ -328,10 +311,7 @@ def addOrFindSoftmaxAndTopkOutputs(inference_adapter, topk, output_raw_scores): raw_scores = softmaxNode.output(0) results_descr.append(raw_scores) for output in inference_adapter.model.outputs: - if ( - _saliency_map_name in output.get_names() - or _feature_vector_name in output.get_names() - ): + if _saliency_map_name in output.get_names() or _feature_vector_name in output.get_names(): results_descr.append(output) source_rt_info = inference_adapter.get_model().get_rt_info() @@ -381,8 +361,7 @@ def __init__(self, hierarchical_config) -> None: self.label_tree.add_edge(parent, child) def resolve_labels(self, predictions): - """ - Resolves hierarchical labels and exclusivity based on a list of ScoredLabels (labels with probability). + """Resolves hierarchical labels and exclusivity based on a list of ScoredLabels (labels with probability). The following two steps are taken: - select the most likely label from each label group - add it and it's predecessors if they are also most likely labels (greedy approach). @@ -404,7 +383,7 @@ def get_predecessors(lbl, candidates): predecessors.append(lbl) return predecessors - label_to_prob = {lbl: 0.0 for lbl in self.label_to_idx.keys()} + label_to_prob = dict.fromkeys(self.label_to_idx.keys(), 0.0) for lbl, score in predictions: label_to_prob[lbl] = score @@ -431,10 +410,7 @@ def get_predecessors(lbl, candidates): if new_lbl not in output_labels: output_labels.append(new_lbl) - output_predictions = [ - (self.label_to_idx[lbl], lbl, label_to_prob[lbl]) - for lbl in sorted(output_labels) - ] + output_predictions = [(self.label_to_idx[lbl], lbl, label_to_prob[lbl]) for lbl in sorted(output_labels)] return output_predictions @@ -497,18 +473,18 @@ def __resolve_labels_probabilistic( lbl, # retain the original probability in the output probability * label_to_probability.get(lbl, 1.0), - ) + ), ) return result def _suppress_descendant_output( - self, hard_classification: dict[str, float] + self, + hard_classification: dict[str, float], ) -> dict[str, float]: """Suppresses outputs in `label_to_probability`. Sets probability to 0.0 for descendants of parents that have 0 probability in `hard_classification`. """ - # Input: Conditional probability of each label given its parent label # Output: Marginal probability of each label @@ -530,7 +506,8 @@ def _suppress_descendant_output( return hard_classification def _resolve_exclusive_labels( - self, label_to_probability: dict[str, float] + self, + label_to_probability: dict[str, float], ) -> dict[str, float]: """Resolve exclusive labels. @@ -552,15 +529,12 @@ def _add_missing_ancestors( for label in label_to_probability: for ancestor in self.label_tree.get_ancestors(label): if ancestor not in updated_label_to_probability: - updated_label_to_probability[ancestor] = ( - 0.0 # by default missing ancestors get probability 0.0 - ) + updated_label_to_probability[ancestor] = 0.0 # by default missing ancestors get probability 0.0 return updated_label_to_probability class SimpleLabelsGraph: - """ - Class representing a tree. It implements basic operations + """Class representing a tree. It implements basic operations like adding edges, getting children and parents. """ @@ -583,7 +557,6 @@ def get_parent(self, label): def get_ancestors(self, label): """Returns all the ancestors of the input label, including self.""" - predecessors = [label] last_parent = self.get_parent(label) if last_parent is None: @@ -625,8 +598,7 @@ def topological_sort(self): if len(ordered) != len(self._v): raise RuntimeError( - "Topological sort failed: input graph has been" - "changed during the sorting or contains a cycle" + "Topological sort failed: input graph has been" "changed during the sorting or contains a cycle", ) return ordered diff --git a/model_api/python/model_api/models/detection_model.py b/model_api/python/model_api/models/detection_model.py index b8092133..0e833af9 100644 --- a/model_api/python/model_api/models/detection_model.py +++ b/model_api/python/model_api/models/detection_model.py @@ -1,17 +1,16 @@ -""" - Copyright (c) 2021-2024 Intel Corporation +"""Copyright (c) 2021-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 +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 +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. +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 .image_model import ImageModel @@ -44,14 +43,11 @@ def __init__(self, inference_adapter, configuration=dict(), preload=False): Raises: WrapperError: if the model has more than 1 image inputs """ - super().__init__(inference_adapter, configuration, preload) if not self.image_blob_name: self.raise_error( - "The Wrapper supports only one image input, but {} found".format( - len(self.image_blob_names) - ) + f"The Wrapper supports only one image input, but {len(self.image_blob_names)} found", ) if self.path_to_labels: @@ -68,9 +64,9 @@ def parameters(cls): ), "labels": ListValue(description="List of class labels"), "path_to_labels": StringValue( - description="Path to file with labels. Overrides the labels, if they sets via 'labels' parameter" + description="Path to file with labels. Overrides the labels, if they sets via 'labels' parameter", ), - } + }, ) return parameters @@ -93,14 +89,12 @@ def _resize_detections(self, detections, meta): inverted_scale_y = input_img_height / self.h pad_left = 0 pad_top = 0 - if ( - "fit_to_window" == self.resize_type - or "fit_to_window_letterbox" == self.resize_type - ): + if self.resize_type == "fit_to_window" or self.resize_type == "fit_to_window_letterbox": inverted_scale_x = inverted_scale_y = max( - inverted_scale_x, inverted_scale_y + inverted_scale_x, + inverted_scale_y, ) - if "fit_to_window_letterbox" == self.resize_type: + if self.resize_type == "fit_to_window_letterbox": pad_left = (self.w - round(input_img_widht / inverted_scale_x)) // 2 pad_top = (self.h - round(input_img_height / inverted_scale_y)) // 2 @@ -145,8 +139,7 @@ def _filter_detections(self, detections, box_area_threshold=0.0): for detection in detections: if ( detection.score < self.confidence_threshold - or (detection.xmax - detection.xmin) * (detection.ymax - detection.ymin) - < box_area_threshold + or (detection.xmax - detection.xmin) * (detection.ymax - detection.ymin) < box_area_threshold ): continue filtered_detections.append(detection) diff --git a/model_api/python/model_api/models/image_model.py b/model_api/python/model_api/models/image_model.py index 10aaf939..ffa7511c 100644 --- a/model_api/python/model_api/models/image_model.py +++ b/model_api/python/model_api/models/image_model.py @@ -1,17 +1,16 @@ -""" - Copyright (c) 2021-2024 Intel Corporation +"""Copyright (c) 2021-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 +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 +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. +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 model_api.adapters.utils import RESIZE_TYPES, InputTransform @@ -67,7 +66,9 @@ def __init__(self, inference_adapter, configuration=dict(), preload=False): self.n, self.h, self.w, self.c = self.inputs[self.image_blob_name].shape self.resize = RESIZE_TYPES[self.resize_type] self.input_transform = InputTransform( - self.reverse_input_channels, self.mean_values, self.scale_values + self.reverse_input_channels, + self.mean_values, + self.scale_values, ) layout = self.inputs[self.image_blob_name].layout @@ -101,10 +102,12 @@ def parameters(cls): default_value=[], ), "orig_height": NumericalValue( - int, description="Model input height before embedding processing" + int, + description="Model input height before embedding processing", ), "orig_width": NumericalValue( - int, description="Model input width before embedding processing" + int, + description="Model input width before embedding processing", ), "pad_value": NumericalValue( int, @@ -119,13 +122,14 @@ def parameters(cls): description="Type of input image resizing", ), "reverse_input_channels": BooleanValue( - default_value=False, description="Reverse the input channel order" + default_value=False, + description="Reverse the input channel order", ), "scale_values": ListValue( default_value=[], description="Normalization values, which will divide the image channels for image-input layer", ), - } + }, ) return parameters @@ -154,11 +158,11 @@ def _get_inputs(self): image_info_blob_names.append(name) else: self.raise_error( - "Failed to identify the input for ImageModel: only 2D and 4D input layer supported" + "Failed to identify the input for ImageModel: only 2D and 4D input layer supported", ) if not image_blob_names: self.raise_error( - "Failed to identify the input for the image: no 4D input layer found" + "Failed to identify the input for the image: no 4D input layer found", ) return image_blob_names, image_info_blob_names diff --git a/model_api/python/model_api/models/instance_segmentation.py b/model_api/python/model_api/models/instance_segmentation.py index 821b961f..fb173cf0 100644 --- a/model_api/python/model_api/models/instance_segmentation.py +++ b/model_api/python/model_api/models/instance_segmentation.py @@ -1,17 +1,16 @@ -""" - Copyright (c) 2020-2024 Intel Corporation +"""Copyright (c) 2020-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 +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 +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. +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. """ import cv2 @@ -19,7 +18,7 @@ from .image_model import ImageModel from .types import BooleanValue, ListValue, NumericalValue, StringValue -from .utils import InstanceSegmentationResult, SegmentedObject, load_labels, nms +from .utils import InstanceSegmentationResult, SegmentedObject, load_labels class MaskRCNNModel(ImageModel): @@ -44,13 +43,13 @@ def parameters(cls): ), "labels": ListValue(description="List of class labels"), "path_to_labels": StringValue( - description="Path to file with labels. Overrides the labels, if they sets via `labels` parameter" + description="Path to file with labels. Overrides the labels, if they sets via `labels` parameter", ), "postprocess_semantic_masks": BooleanValue( description="Resize and apply 0.5 threshold to instance segmentation masks", default_value=True, ), - } + }, ) return parameters @@ -59,10 +58,7 @@ def _get_outputs(self): return self._get_segmentoly_outputs() filtered_names = [] for name, output in self.outputs.items(): - if ( - _saliency_map_name not in output.names - and _feature_vector_name not in output.names - ): + if _saliency_map_name not in output.names and _feature_vector_name not in output.names: filtered_names.append(name) outputs = {} for layer_name in filtered_names: @@ -110,9 +106,7 @@ def _get_segmentoly_outputs(self): outputs["masks"] = layer_name else: self.raise_error( - "Unexpected output layer shape {} with name {}".format( - layer_shape, layer_name - ) + f"Unexpected output layer shape {layer_shape} with name {layer_name}", ) return outputs @@ -122,7 +116,8 @@ def preprocess(self, inputs): if self.is_segmentoly: assert len(self.image_info_blob_names) == 1 input_image_info = np.asarray( - [[input_image_size[0], input_image_size[1], 1]], dtype=np.float32 + [[input_image_size[0], input_image_size[1], 1]], + dtype=np.float32, ) dict_inputs[self.image_info_blob_names[0]] = input_image_info return dict_inputs, meta @@ -166,16 +161,11 @@ def postprocess(self, outputs, meta): inputImgHeight / self.orig_height, ) padLeft, padTop = 0, 0 - if ( - "fit_to_window" == self.resize_type - or "fit_to_window_letterbox" == self.resize_type - ): + if self.resize_type == "fit_to_window" or self.resize_type == "fit_to_window_letterbox": invertedScaleX = invertedScaleY = max(invertedScaleX, invertedScaleY) - if "fit_to_window_letterbox" == self.resize_type: + if self.resize_type == "fit_to_window_letterbox": padLeft = (self.orig_width - round(inputImgWidth / invertedScaleX)) // 2 - padTop = ( - self.orig_height - round(inputImgHeight / invertedScaleY) - ) // 2 + padTop = (self.orig_height - round(inputImgHeight / invertedScaleY)) // 2 boxes -= (padLeft, padTop, padLeft, padTop) boxes *= (invertedScaleX, invertedScaleY, invertedScaleX, invertedScaleY) @@ -197,9 +187,7 @@ def postprocess(self, outputs, meta): saliency_maps = [] for box, confidence, cls, raw_mask in zip(boxes, scores, labels, masks): x1, y1, x2, y2 = box - if (x2 - x1) * (y2 - y1) < 1 or ( - confidence <= self.confidence_threshold and not has_feature_vector_name - ): + if (x2 - x1) * (y2 - y1) < 1 or (confidence <= self.confidence_threshold and not has_feature_vector_name): continue # Skip if label index is out of bounds @@ -212,18 +200,22 @@ def postprocess(self, outputs, meta): raw_cls_mask = raw_mask[cls, ...] if self.is_segmentoly else raw_mask if self.postprocess_semantic_masks or has_feature_vector_name: resized_mask = _segm_postprocess( - box, raw_cls_mask, *meta["original_shape"][:-1] + box, + raw_cls_mask, + *meta["original_shape"][:-1], ) else: resized_mask = raw_cls_mask if confidence > self.confidence_threshold: - output_mask = ( - resized_mask if self.postprocess_semantic_masks else raw_cls_mask - ) + output_mask = resized_mask if self.postprocess_semantic_masks else raw_cls_mask objects.append( SegmentedObject( - *box.astype(int), confidence, cls, str_label, output_mask - ) + *box.astype(int), + confidence, + cls, + str_label, + output_mask, + ), ) if has_feature_vector_name: if confidence > self.confidence_threshold: @@ -265,7 +257,8 @@ def _segm_postprocess(box, raw_cls_mask, im_h, im_w): # Add zero border to prevent upsampling artifacts on segment borders. raw_cls_mask = np.pad(raw_cls_mask, ((1, 1), (1, 1)), "constant", constant_values=0) extended_box = _expand_box( - box, raw_cls_mask.shape[0] / (raw_cls_mask.shape[0] - 2.0) + box, + raw_cls_mask.shape[0] / (raw_cls_mask.shape[0] - 2.0), ).astype(int) w, h = np.maximum(extended_box[2:] - extended_box[:2] + 1, 1) x0, y0 = np.clip(extended_box[:2], a_min=0, a_max=[im_w, im_h]) diff --git a/model_api/python/model_api/models/keypoint_detection.py b/model_api/python/model_api/models/keypoint_detection.py index ebe70753..120ec150 100644 --- a/model_api/python/model_api/models/keypoint_detection.py +++ b/model_api/python/model_api/models/keypoint_detection.py @@ -1,17 +1,16 @@ -""" - Copyright (c) 2024 Intel Corporation +"""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 +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 +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. +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 __future__ import annotations @@ -26,15 +25,12 @@ class KeypointDetectionModel(ImageModel): - """ - A wrapper that implements a basic keypoint regression model. - """ + """A wrapper that implements a basic keypoint regression model.""" __model__ = "keypoint_detection" def __init__(self, inference_adapter, configuration=dict(), preload=False): - """ - Initializes the keypoint detection model. + """Initializes the keypoint detection model. Args: inference_adapter (InferenceAdapter): inference adapter containing the underlying model. @@ -46,10 +42,11 @@ def __init__(self, inference_adapter, configuration=dict(), preload=False): self._check_io_number(1, 2) def postprocess( - self, outputs: dict[str, np.ndarray], meta: dict[str, Any] + self, + outputs: dict[str, np.ndarray], + meta: dict[str, Any], ) -> DetectedKeypoints: - """ - Applies SCC decoded to the model outputs. + """Applies SCC decoded to the model outputs. Args: outputs (dict[str, np.ndarray]): raw outputs of the model @@ -72,26 +69,27 @@ def parameters(cls) -> dict: parameters.update( { "labels": ListValue( - description="List of class labels", value_type=str, default_value=[] + description="List of class labels", + value_type=str, + default_value=[], ), - } + }, ) return parameters class TopDownKeypointDetectionPipeline: - """ - Pipeline implementing top down keypoint detection approach. - """ + """Pipeline implementing top down keypoint detection approach.""" def __init__(self, base_model: KeypointDetectionModel) -> None: self.base_model = base_model def predict( - self, image: np.ndarray, detections: list[Detection] + self, + image: np.ndarray, + detections: list[Detection], ) -> list[DetectedKeypoints]: - """ - Predicts keypoints for the given image and detections. + """Predicts keypoints for the given image and detections. Args: image (np.ndarray): input full-size image @@ -114,8 +112,7 @@ def predict( return crops_results def predict_crops(self, crops: list[np.ndarray]) -> list[DetectedKeypoints]: - """ - Predicts keypoints for the given crops. + """Predicts keypoints for the given crops. Args: crops (list[np.ndarray]): list of cropped object images @@ -127,7 +124,9 @@ def predict_crops(self, crops: list[np.ndarray]) -> list[DetectedKeypoints]: def _decode_simcc( - simcc_x: np.ndarray, simcc_y: np.ndarray, simcc_split_ratio: float = 2.0 + simcc_x: np.ndarray, + simcc_y: np.ndarray, + simcc_split_ratio: float = 2.0, ) -> tuple[np.ndarray, np.ndarray]: """Decodes keypoint coordinates from SimCC representations. The decoded coordinates are in the input image space. @@ -198,10 +197,14 @@ def _get_simcc_maximum( y_locs = np.argmax(simcc_y, axis=1) locs = np.stack((x_locs, y_locs), axis=-1).astype(np.float32) max_val_x = np.take_along_axis( - simcc_x, np.expand_dims(x_locs, axis=-1), axis=-1 + simcc_x, + np.expand_dims(x_locs, axis=-1), + axis=-1, ).squeeze(axis=-1) max_val_y = np.take_along_axis( - simcc_y, np.expand_dims(y_locs, axis=-1), axis=-1 + simcc_y, + np.expand_dims(y_locs, axis=-1), + axis=-1, ).squeeze(axis=-1) mask = max_val_x > max_val_y diff --git a/model_api/python/model_api/models/model.py b/model_api/python/model_api/models/model.py index 78f7ce97..ab43efbd 100644 --- a/model_api/python/model_api/models/model.py +++ b/model_api/python/model_api/models/model.py @@ -1,17 +1,16 @@ -""" - Copyright (C) 2020-2024 Intel Corporation +"""Copyright (C) 2020-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 +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 +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. +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. """ import logging as log @@ -79,7 +78,8 @@ def __init__(self, inference_adapter, configuration=dict(), preload=False): self.logger = log.getLogger() self.inference_adapter = inference_adapter if isinstance( - self.inference_adapter, ONNXRuntimeAdapter + self.inference_adapter, + ONNXRuntimeAdapter, ) and self.__model__ not in { "Classification", "MaskRCNN", @@ -87,7 +87,7 @@ def __init__(self, inference_adapter, configuration=dict(), preload=False): "Segmentation", }: self.raise_error( - "this type of wrapper only supports OpenVINO and OVMS inference adapters" + "this type of wrapper only supports OpenVINO and OVMS inference adapters", ) self.inputs = self.inference_adapter.get_input_layers() @@ -117,9 +117,7 @@ def get_model(self): @classmethod def get_model_class(cls, name): - subclasses = [ - subclass for subclass in cls.get_subclasses() if subclass.__model__ - ] + subclasses = [subclass for subclass in cls.get_subclasses() if subclass.__model__] if cls.__model__: subclasses.append(cls) for subclass in subclasses: @@ -127,8 +125,9 @@ def get_model_class(cls, name): return subclass cls.raise_error( 'There is no model with name "{}" in list: {}'.format( - name, ", ".join([subclass.__model__ for subclass in subclasses]) - ) + name, + ", ".join([subclass.__model__ for subclass in subclasses]), + ), ) @classmethod @@ -149,8 +148,7 @@ def create_model( download_dir=None, cache_dir=None, ): - """ - Create an instance of the Model API model + """Create an instance of the Model API model Args: model (str): model name from OpenVINO Model Zoo, path to model, OVMS URL @@ -174,7 +172,7 @@ def create_model( if isinstance(model, InferenceAdapter): inference_adapter = model elif isinstance(model, str) and re.compile( - r"(\w+\.*\-*)*\w+:\d+\/models\/[a-zA-Z0-9._-]+(\:\d+)*" + r"(\w+\.*\-*)*\w+:\d+\/models\/[a-zA-Z0-9._-]+(\:\d+)*", ).fullmatch(model): inference_adapter = OVMSAdapter(model) else: @@ -195,7 +193,7 @@ def create_model( ) if model_type is None: model_type = inference_adapter.get_rt_info( - ["model_info", "model_type"] + ["model_info", "model_type"], ).astype(str) Model = cls.get_model_class(model_type) return Model(inference_adapter, configuration, preload) @@ -212,9 +210,7 @@ def get_subclasses(cls): def available_wrappers(cls): available_classes = [cls] if cls.__model__ else [] available_classes.extend(cls.get_subclasses()) - return [ - subclass.__model__ for subclass in available_classes if subclass.__model__ - ] + return [subclass.__model__ for subclass in available_classes if subclass.__model__] @classmethod def parameters(cls): @@ -253,24 +249,19 @@ def _load_config(self, config): then the default value of the parameter will be updated. If some key presented in the config is not introduced in `parameters`, it will be omitted. - Raises: + Raises: WrapperError: if the configuration is incorrect """ parameters = self.parameters() for name, param in parameters.items(): try: value = param.from_str( - self.inference_adapter.get_rt_info(["model_info", name]).astype(str) + self.inference_adapter.get_rt_info(["model_info", name]).astype(str), ) self.__setattr__(name, value) except RuntimeError as error: - missing_rt_info = ( - "Cannot get runtime attribute. Path to runtime attribute is incorrect." - in str(error) - ) - is_OVMSAdapter = ( - str(error) == "OVMSAdapter does not support RT info getting" - ) + missing_rt_info = "Cannot get runtime attribute. Path to runtime attribute is incorrect." in str(error) + is_OVMSAdapter = str(error) == "OVMSAdapter does not support RT info getting" if not missing_rt_info and not is_OVMSAdapter: raise @@ -288,7 +279,7 @@ def _load_config(self, config): self.__setattr__(name, value) else: self.logger.warning( - f'The parameter "{name}" not found in {self.__model__} wrapper, will be omitted' + f'The parameter "{name}" not found in {self.__model__} wrapper, will be omitted', ) @classmethod @@ -356,18 +347,17 @@ def _check_io_number(self, number_of_inputs, number_of_outputs): "s" if number_of_inputs != 1 else "", len(self.inputs), ", ".join(self.inputs), - ) - ) - else: - if not len(self.inputs) in number_of_inputs: - self.raise_error( - "Expected {} or {} input blobs, but {} found: {}".format( - ", ".join(str(n) for n in number_of_inputs[:-1]), - int(number_of_inputs[-1]), - len(self.inputs), - ", ".join(self.inputs), - ) + ), ) + elif len(self.inputs) not in number_of_inputs: + self.raise_error( + "Expected {} or {} input blobs, but {} found: {}".format( + ", ".join(str(n) for n in number_of_inputs[:-1]), + int(number_of_inputs[-1]), + len(self.inputs), + ", ".join(self.inputs), + ), + ) if not isinstance(number_of_outputs, tuple): if len(self.outputs) != number_of_outputs and number_of_outputs != -1: @@ -377,22 +367,20 @@ def _check_io_number(self, number_of_inputs, number_of_outputs): "s" if number_of_outputs != 1 else "", len(self.outputs), ", ".join(self.outputs), - ) - ) - else: - if not len(self.outputs) in number_of_outputs: - self.raise_error( - "Expected {} or {} output blobs, but {} found: {}".format( - ", ".join(str(n) for n in number_of_outputs[:-1]), - int(number_of_outputs[-1]), - len(self.outputs), - ", ".join(self.outputs), - ) + ), ) + elif len(self.outputs) not in number_of_outputs: + self.raise_error( + "Expected {} or {} output blobs, but {} found: {}".format( + ", ".join(str(n) for n in number_of_outputs[:-1]), + int(number_of_outputs[-1]), + len(self.outputs), + ", ".join(self.outputs), + ), + ) def __call__(self, inputs): - """ - Applies preprocessing, synchronous inference, postprocessing routines while one call. + """Applies preprocessing, synchronous inference, postprocessing routines while one call. Args: inputs: raw input data, the data type is defined by wrapper @@ -405,8 +393,7 @@ def __call__(self, inputs): return self.postprocess(raw_result, input_meta) def infer_batch(self, inputs): - """ - Applies preprocessing, asynchronous inference, postprocessing routines to a collection of inputs. + """Applies preprocessing, asynchronous inference, postprocessing routines to a collection of inputs. Args: inputs (list): a list of inputs for inference @@ -458,7 +445,7 @@ def infer_sync(self, dict_data): if not self.model_loaded: self.raise_error( "The model is not loaded to the device. Please, create the wrapper " - "with preload=True option or call load() method before infer_sync()" + "with preload=True option or call load() method before infer_sync()", ) return self.inference_adapter.infer_sync(dict_data) @@ -466,7 +453,7 @@ def infer_async_raw(self, dict_data, callback_data): if not self.model_loaded: self.raise_error( "The model is not loaded to the device. Please, create the wrapper " - "with preload=True option or call load() method before infer_async()" + "with preload=True option or call load() method before infer_async()", ) self.inference_adapter.infer_async(dict_data, callback_data) @@ -474,7 +461,7 @@ def infer_async(self, input_data, user_data): if not self.model_loaded: self.raise_error( "The model is not loaded to the device. Please, create the wrapper " - "with preload=True option or call load() method before infer_async()" + "with preload=True option or call load() method before infer_async()", ) dict_data, meta = self.preprocess(input_data) self.inference_adapter.infer_async( @@ -512,15 +499,11 @@ def log_layers_info(self): """Prints the shape, precision and layout for all model inputs/outputs.""" for name, metadata in self.inputs.items(): self.logger.info( - "\tInput layer: {}, shape: {}, precision: {}, layout: {}".format( - name, metadata.shape, metadata.precision, metadata.layout - ) + f"\tInput layer: {name}, shape: {metadata.shape}, precision: {metadata.precision}, layout: {metadata.layout}", ) for name, metadata in self.outputs.items(): self.logger.info( - "\tOutput layer: {}, shape: {}, precision: {}, layout: {}".format( - name, metadata.shape, metadata.precision, metadata.layout - ) + f"\tOutput layer: {name}, shape: {metadata.shape}, precision: {metadata.precision}, layout: {metadata.layout}", ) def get_model(self): diff --git a/model_api/python/model_api/models/sam_models.py b/model_api/python/model_api/models/sam_models.py index e400fc7f..f73d1684 100644 --- a/model_api/python/model_api/models/sam_models.py +++ b/model_api/python/model_api/models/sam_models.py @@ -1,25 +1,25 @@ -""" - Copyright (C) 2024 Intel Corporation +"""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 +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 +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. +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 __future__ import annotations # TODO: remove when Python3.9 support is dropped from copy import deepcopy -from typing import Any, Dict +from typing import Any import numpy as np + from model_api.adapters.inference_adapter import InferenceAdapter from model_api.models.types import BooleanValue, NumericalValue @@ -35,7 +35,7 @@ class SAMImageEncoder(ImageModel): def __init__( self, inference_adapter: InferenceAdapter, - configuration: Dict[str, Any] = dict(), + configuration: dict[str, Any] = dict(), preload: bool = False, ): super().__init__(inference_adapter, configuration, preload) @@ -47,14 +47,18 @@ def parameters(cls) -> dict[str, Any]: parameters.update( { "image_size": NumericalValue( - value_type=int, default_value=1024, min=0, max=2048 + value_type=int, + default_value=1024, + min=0, + max=2048, ), }, ) return parameters def preprocess( - self, inputs: np.ndarray + self, + inputs: np.ndarray, ) -> tuple[dict[str, np.ndarray], dict[str, Any]]: """Update meta for image encoder.""" dict_inputs, meta = super().preprocess(inputs) @@ -62,7 +66,9 @@ def preprocess( return dict_inputs, meta def postprocess( - self, outputs: dict[str, np.ndarray], meta: dict[str, Any] + self, + outputs: dict[str, np.ndarray], + meta: dict[str, Any], ) -> np.ndarray: return outputs[self.output_name] @@ -75,7 +81,7 @@ class SAMDecoder(SegmentationModel): def __init__( self, model_adapter: InferenceAdapter, - configuration: Dict[str, Any] = dict(), + configuration: dict[str, Any] = dict(), preload: bool = False, ): super().__init__(model_adapter, configuration, preload) @@ -89,23 +95,32 @@ def parameters(cls) -> dict[str, Any]: parameters.update( { "image_size": NumericalValue( - value_type=int, default_value=1024, min=0, max=2048 - ) - } + value_type=int, + default_value=1024, + min=0, + max=2048, + ), + }, ) parameters.update( { "mask_threshold": NumericalValue( - value_type=float, default_value=0.0, min=0, max=1 - ) - } + value_type=float, + default_value=0.0, + min=0, + max=1, + ), + }, ) parameters.update( { "embed_dim": NumericalValue( - value_type=int, default_value=256, min=0, max=512 - ) - } + value_type=int, + default_value=256, + min=0, + max=512, + ), + }, ) parameters.update({"embedded_processing": BooleanValue(default_value=True)}) return parameters @@ -117,7 +132,7 @@ def preprocess(self, inputs: dict[str, Any]) -> list[dict[str, Any]]: """Preprocess prompts.""" processed_prompts: list[dict[str, Any]] = [] for prompt_name in ["bboxes", "points"]: - if (prompts := inputs.get(prompt_name, None)) is None or ( + if (prompts := inputs.get(prompt_name)) is None or ( labels := inputs["labels"].get(prompt_name, None) ) is None: continue @@ -125,12 +140,14 @@ def preprocess(self, inputs: dict[str, Any]) -> list[dict[str, Any]]: for prompt, label in zip(prompts, labels): if prompt_name == "bboxes": point_coords = self.apply_coords( - prompt.reshape(-1, 2, 2), inputs["orig_size"] + prompt.reshape(-1, 2, 2), + inputs["orig_size"], ) point_labels = np.array([2, 3], dtype=np.float32).reshape(-1, 2) else: point_coords = self.apply_coords( - prompt.reshape(-1, 1, 2), inputs["orig_size"] + prompt.reshape(-1, 1, 2), + inputs["orig_size"], ) point_labels = np.array([1], dtype=np.float32).reshape(-1, 1) @@ -141,7 +158,8 @@ def preprocess(self, inputs: dict[str, Any]) -> list[dict[str, Any]]: "mask_input": self.mask_input, "has_mask_input": self.has_mask_input, "orig_size": np.array( - inputs["orig_size"], dtype=np.int64 + inputs["orig_size"], + dtype=np.int64, ).reshape(-1, 2), "label": label, }, @@ -149,7 +167,9 @@ def preprocess(self, inputs: dict[str, Any]) -> list[dict[str, Any]]: return processed_prompts def apply_coords( - self, coords: np.ndarray, orig_size: np.ndarray | list[int] | tuple[int, int] + self, + coords: np.ndarray, + orig_size: np.ndarray | list[int] | tuple[int, int], ) -> np.ndarray: """Process coords according to preprocessed image size using image meta.""" old_h, old_w = orig_size @@ -160,7 +180,10 @@ def apply_coords( return coords def _get_preprocess_shape( - self, old_h: int, old_w: int, image_size: int + self, + old_h: int, + old_w: int, + image_size: int, ) -> tuple[int, int]: """Compute the output size given input size and target image size.""" scale = image_size / max(old_h, old_w) @@ -170,7 +193,9 @@ def _get_preprocess_shape( return (new_h, new_w) def _check_io_number( - self, number_of_inputs: int | tuple[int], number_of_outputs: int | tuple[int] + self, + number_of_inputs: int | tuple[int], + number_of_outputs: int | tuple[int], ) -> None: pass @@ -181,7 +206,9 @@ def _get_inputs(self) -> tuple[list[str], list[str]]: return image_blob_names, image_info_blob_names def postprocess( - self, outputs: dict[str, np.ndarray], meta: dict[str, Any] + self, + outputs: dict[str, np.ndarray], + meta: dict[str, Any], ) -> dict[str, np.ndarray]: """Postprocess to convert soft prediction to hard prediction. @@ -194,9 +221,7 @@ def postprocess( """ outputs = deepcopy(outputs) probability = np.clip(outputs["scores"], 0.0, 1.0) - hard_prediction = ( - outputs[self.output_blob_name].squeeze(0) > self.mask_threshold - ) + hard_prediction = outputs[self.output_blob_name].squeeze(0) > self.mask_threshold soft_prediction = hard_prediction * probability.reshape(-1, 1, 1) outputs["hard_prediction"] = hard_prediction diff --git a/model_api/python/model_api/models/segmentation.py b/model_api/python/model_api/models/segmentation.py index ae95c436..e3937148 100644 --- a/model_api/python/model_api/models/segmentation.py +++ b/model_api/python/model_api/models/segmentation.py @@ -1,20 +1,21 @@ -""" - Copyright (c) 2020-2024 Intel Corporation +"""Copyright (c) 2020-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 +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 +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. +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 Iterable, Union +from __future__ import annotations # TODO: remove when Python3.9 support is dropped + +from collections.abc import Iterable import cv2 import numpy as np @@ -25,7 +26,9 @@ def create_hard_prediction_from_soft_prediction( - soft_prediction: np.ndarray, soft_threshold: float, blur_strength: int + soft_prediction: np.ndarray, + soft_threshold: float, + blur_strength: int, ) -> np.ndarray: """Creates a hard prediction containing the final label index per pixel. @@ -45,13 +48,13 @@ def create_hard_prediction_from_soft_prediction( """ if blur_strength == -1 or soft_threshold == float("inf"): return np.argmax(soft_prediction, axis=2) - else: - soft_prediction_blurred = cv2.blur( - soft_prediction, (blur_strength, blur_strength) - ) - assert len(soft_prediction.shape) == 3 - soft_prediction_blurred[soft_prediction_blurred < soft_threshold] = 0 - return np.argmax(soft_prediction_blurred, axis=2) + soft_prediction_blurred = cv2.blur( + soft_prediction, + (blur_strength, blur_strength), + ) + assert len(soft_prediction.shape) == 3 + soft_prediction_blurred[soft_prediction_blurred < soft_threshold] = 0 + return np.argmax(soft_prediction_blurred, axis=2) class SegmentationModel(ImageModel): @@ -71,7 +74,7 @@ def _get_outputs(self): if _feature_vector_name not in output.names: if out_name: self.raise_error( - f"only {_feature_vector_name} and 1 other output are allowed" + f"only {_feature_vector_name} and 1 other output are allowed", ) else: out_name = name @@ -85,9 +88,7 @@ def _get_outputs(self): self.out_channels = layer_shape[1] else: self.raise_error( - "Unexpected output layer shape {}. Only 4D and 3D output layers are supported".format( - layer_shape - ) + f"Unexpected output layer shape {layer_shape}. Only 4D and 3D output layers are supported", ) return out_name @@ -99,7 +100,7 @@ def parameters(cls): { "labels": ListValue(description="List of class labels"), "path_to_labels": StringValue( - description="Path to file with labels. Overrides the labels, if they sets via 'labels' parameter" + description="Path to file with labels. Overrides the labels, if they sets via 'labels' parameter", ), "blur_strength": NumericalValue( value_type=int, @@ -115,7 +116,7 @@ def parameters(cls): description="Return raw resized model prediction in addition to processed one", default_value=True, ), - } + }, ) return parameters @@ -155,11 +156,7 @@ def postprocess(self, outputs, meta): return ImageResultWithSoftPrediction( hard_prediction, soft_prediction, - ( - _get_activation_map(soft_prediction) - if _feature_vector_name in outputs - else np.ndarray(0) - ), + (_get_activation_map(soft_prediction) if _feature_vector_name in outputs else np.ndarray(0)), outputs.get(_feature_vector_name, np.ndarray(0)), ) return hard_prediction @@ -177,7 +174,9 @@ def get_contours( label = self.get_label_name(layer_index - 1) if len(prediction.soft_prediction.shape) == 3: current_label_soft_prediction = prediction.soft_prediction[ - :, :, layer_index + :, + :, + layer_index, ] else: current_label_soft_prediction = prediction.soft_prediction @@ -186,7 +185,9 @@ def get_contours( label_index_map = obj_group.astype(np.uint8) * 255 contours, _hierarchy = cv2.findContours( - label_index_map, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE + label_index_map, + cv2.RETR_EXTERNAL, + cv2.CHAIN_APPROX_NONE, ) for contour in contours: @@ -225,7 +226,7 @@ def postprocess(self, outputs, meta): _feature_vector_name = "feature_vector" -def _get_activation_map(features: Union[np.ndarray, Iterable, int, float]): +def _get_activation_map(features: np.ndarray | Iterable | int | float): """Getter activation_map functions.""" min_soft_score = np.min(features) max_soft_score = np.max(features) diff --git a/model_api/python/model_api/models/ssd.py b/model_api/python/model_api/models/ssd.py index 63e8c559..9d197cfb 100644 --- a/model_api/python/model_api/models/ssd.py +++ b/model_api/python/model_api/models/ssd.py @@ -1,17 +1,16 @@ -""" - Copyright (C) 2020-2024 Intel Corporation +"""Copyright (C) 2020-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 +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 +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. +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. """ import numpy as np @@ -25,11 +24,7 @@ class SSD(DetectionModel): def __init__(self, inference_adapter, configuration=dict(), preload=False): super().__init__(inference_adapter, configuration, preload) - self.image_info_blob_name = ( - self.image_info_blob_names[0] - if len(self.image_info_blob_names) == 1 - else None - ) + self.image_info_blob_name = self.image_info_blob_names[0] if len(self.image_info_blob_names) == 1 else None self.output_parser = self._get_output_parser(self.image_blob_name) def preprocess(self, inputs): @@ -50,7 +45,11 @@ def postprocess(self, outputs, meta): ) def _get_output_parser( - self, image_blob_name, bboxes="bboxes", labels="labels", scores="scores" + self, + image_blob_name, + bboxes="bboxes", + labels="labels", + scores="scores", ): try: parser = SingleOutputParser(self.outputs) @@ -81,10 +80,10 @@ def _parse_outputs(self, outputs): def find_layer_by_name(name, layers): suitable_layers = [layer_name for layer_name in layers if name in layer_name] if not suitable_layers: - raise ValueError('Suitable layer for "{}" output is not found'.format(name)) + raise ValueError(f'Suitable layer for "{name}" output is not found') if len(suitable_layers) > 1: - raise ValueError('More than 1 layer matched to "{}" output'.format(name)) + raise ValueError(f'More than 1 layer matched to "{name}" output') return suitable_layers[0] @@ -97,16 +96,13 @@ def __init__(self, all_outputs): last_dim = output_data.shape[-1] if last_dim != 7: raise ValueError( - "The last dimension of the output blob must be equal to 7, " - "got {} instead.".format(last_dim) + "The last dimension of the output blob must be equal to 7, " f"got {last_dim} instead.", ) def __call__(self, outputs): return [ Detection(xmin, ymin, xmax, ymax, score, label) - for _, label, score, xmin, ymin, xmax, ymax in outputs[self.output_name][0][ - 0 - ] + for _, label, score, xmin, ymin, xmax, ymax in outputs[self.output_name][0][0] ] @@ -126,10 +122,7 @@ def __call__(self, outputs): bboxes = outputs[self.bboxes_layer][0] scores = outputs[self.scores_layer][0] labels = outputs[self.labels_layer][0] - return [ - Detection(*bbox, score, label) - for label, score, bbox in zip(labels, scores, bboxes) - ] + return [Detection(*bbox, score, label) for label, score, bbox in zip(labels, scores, bboxes)] class BoxesLabelsParser: @@ -169,10 +162,7 @@ def __call__(self, outputs): labels = np.full(len(bboxes), self.default_label, dtype=bboxes.dtype) labels = labels.squeeze(0) - detections = [ - Detection(*bbox, score, label) - for label, score, bbox in zip(labels, scores, bboxes) - ] + detections = [Detection(*bbox, score, label) for label, score, bbox in zip(labels, scores, bboxes)] return detections diff --git a/model_api/python/model_api/models/types.py b/model_api/python/model_api/models/types.py index 1ab8faaa..7a451a8e 100644 --- a/model_api/python/model_api/models/types.py +++ b/model_api/python/model_api/models/types.py @@ -1,17 +1,16 @@ -""" - Copyright (C) 2021-2024 Intel Corporation +"""Copyright (C) 2021-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 +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 +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. +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. """ @@ -23,7 +22,9 @@ def __init__(self, message, prefix=None): class BaseValue: def __init__( - self, description="No description available", default_value=None + self, + description="No description available", + default_value=None, ) -> None: self.default_value = default_value self.description = description @@ -51,7 +52,12 @@ def __str__(self) -> str: class NumericalValue(BaseValue): def __init__( - self, value_type=float, choices=(), min=None, max=None, **kwargs + self, + value_type=float, + choices=(), + min=None, + max=None, + **kwargs, ) -> None: super().__init__(**kwargs) self.choices = choices @@ -69,28 +75,28 @@ def validate(self, value): if not isinstance(value, self.value_type): errors.append( ConfigurableValueError( - f"Incorrect value type {type(value)}: should be {self.value_type}" - ) + f"Incorrect value type {type(value)}: should be {self.value_type}", + ), ) return errors if len(self.choices): if value not in self.choices: errors.append( ConfigurableValueError( - f"Incorrect value {value}: out of allowable list - {self.choices}" - ) + f"Incorrect value {value}: out of allowable list - {self.choices}", + ), ) if self.min is not None and value < self.min: errors.append( ConfigurableValueError( - f"Incorrect value {value}: less than minimum allowable {self.min}" - ) + f"Incorrect value {value}: less than minimum allowable {self.min}", + ), ) if self.max is not None and value > self.max: errors.append( ConfigurableValueError( - f"Incorrect value {value}: bigger than maximum allowable {self.min}" - ) + f"Incorrect value {value}: bigger than maximum allowable {self.min}", + ), ) return errors @@ -104,13 +110,16 @@ def __str__(self) -> str: class StringValue(BaseValue): def __init__( - self, choices=(), description="No description available", default_value="" + self, + choices=(), + description="No description available", + default_value="", ): super().__init__(description, default_value) self.choices = choices for choice in self.choices: if not isinstance(choice, str): - raise ValueError("Incorrect option in choice list - {}.".format(choice)) + raise ValueError(f"Incorrect option in choice list - {choice}.") def from_str(self, value): return value @@ -122,14 +131,14 @@ def validate(self, value): if not isinstance(value, str): errors.append( ConfigurableValueError( - f'Incorrect value type {type(value)}: should be "str"' - ) + f'Incorrect value type {type(value)}: should be "str"', + ), ) if len(self.choices) > 0 and value not in self.choices: errors.append( ConfigurableValueError( - f"Incorrect value {value}: out of allowable list - {self.choices}" - ) + f"Incorrect value {value}: out of allowable list - {self.choices}", + ), ) return errors @@ -147,7 +156,7 @@ def __init__(self, **kwargs) -> None: super().__init__(**kwargs) def from_str(self, value): - return "YES" == value or "True" == value + return value == "YES" or value == "True" def validate(self, value): errors = super().validate(value) @@ -156,15 +165,18 @@ def validate(self, value): if not isinstance(value, bool): errors.append( ConfigurableValueError( - f'Incorrect value type - {type(value)}: should be "bool"' - ) + f'Incorrect value type - {type(value)}: should be "bool"', + ), ) return errors class ListValue(BaseValue): def __init__( - self, value_type=None, description="No description available", default_value=[] + self, + value_type=None, + description="No description available", + default_value=[], ) -> None: super().__init__(description, default_value) self.value_type = value_type @@ -189,8 +201,8 @@ def validate(self, value): if not isinstance(value, (tuple, list)): errors.append( ConfigurableValueError( - f"Incorrect value type - {type(value)}: should be list or tuple" - ) + f"Incorrect value type - {type(value)}: should be list or tuple", + ), ) if self.value_type: if isinstance(self.value_type, BaseValue): @@ -200,18 +212,18 @@ def validate(self, value): errors.extend( [ ConfigurableValueError( - f"Incorrect #{i} element of the list" + f"Incorrect #{i} element of the list", ), *temp_errors, - ] + ], ) else: for i, element in enumerate(value): if not isinstance(element, self.value_type): errors.append( ConfigurableValueError( - f"Incorrect #{i} element type - {type(element)}: should be {self.value_type}" - ) + f"Incorrect #{i} element type - {type(element)}: should be {self.value_type}", + ), ) return errors @@ -230,7 +242,7 @@ def validate(self, value): if not isinstance(value, dict): errors.append( ConfigurableValueError( - f'Incorrect value type - {type(value)}: should be "dict"' - ) + f'Incorrect value type - {type(value)}: should be "dict"', + ), ) return errors diff --git a/model_api/python/model_api/models/utils.py b/model_api/python/model_api/models/utils.py index f78d958c..86a5714d 100644 --- a/model_api/python/model_api/models/utils.py +++ b/model_api/python/model_api/models/utils.py @@ -1,23 +1,22 @@ -""" - Copyright (C) 2020-2024 Intel Corporation +"""Copyright (C) 2020-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 +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 +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. +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 __future__ import annotations # TODO: remove when Python3.9 support is dropped from collections import namedtuple -from typing import List, NamedTuple, Tuple, Union +from typing import NamedTuple import cv2 import numpy as np @@ -51,14 +50,12 @@ def __str__(self) -> str: class ClassificationResult( namedtuple( - "ClassificationResult", "top_labels saliency_map feature_vector raw_scores" - ) # Contains "raw_scores", "saliency_map" and "feature_vector" model outputs if such exist + "ClassificationResult", + "top_labels saliency_map feature_vector raw_scores", + ), # Contains "raw_scores", "saliency_map" and "feature_vector" model outputs if such exist ): def __str__(self): - labels = ", ".join( - f"{idx} ({label}): {confidence:.3f}" - for idx, label, confidence in self.top_labels - ) + labels = ", ".join(f"{idx} ({label}): {confidence:.3f}" for idx, label, confidence in self.top_labels) return ( f"{labels}, [{','.join(str(i) for i in self.saliency_map.shape)}], [{','.join(str(i) for i in self.feature_vector.shape)}], " f"[{','.join(str(i) for i in self.raw_scores.shape)}]" @@ -81,8 +78,9 @@ def __str__(self): class DetectionResult( namedtuple( - "DetectionResult", "objects saliency_map feature_vector" - ) # Contan "saliency_map" and "feature_vector" model outputs if such exist + "DetectionResult", + "objects saliency_map feature_vector", + ), # Contan "saliency_map" and "feature_vector" model outputs if such exist ): def __str__(self): obj_str = "; ".join(str(obj) for obj in self.objects) @@ -122,9 +120,9 @@ def __str__(self): class InstanceSegmentationResult(NamedTuple): - segmentedObjects: List[Union[SegmentedObject, SegmentedObjectWithRects]] + segmentedObjects: list[SegmentedObject | SegmentedObjectWithRects] # Contain per class saliency_maps and "feature_vector" model output if feature_vector exists - saliency_map: List[np.ndarray] + saliency_map: list[np.ndarray] feature_vector: np.ndarray def __str__(self): @@ -134,22 +132,19 @@ def __str__(self): if cls_map.size: filled += 1 prefix = f"{obj_str}; " if len(obj_str) else "" - return ( - prefix - + f"{filled}; [{','.join(str(i) for i in self.feature_vector.shape)}]" - ) + return prefix + f"{filled}; [{','.join(str(i) for i in self.feature_vector.shape)}]" class VisualPromptingResult(NamedTuple): - upscaled_masks: List[np.ndarray] | None = None - processed_mask: List[np.ndarray] | None = None - low_res_masks: List[np.ndarray] | None = None - iou_predictions: List[np.ndarray] | None = None - scores: List[np.ndarray] | None = None - labels: List[np.ndarray] | None = None - hard_predictions: List[np.ndarray] | None = None - soft_predictions: List[np.ndarray] | None = None - best_iou: List[float] | None = None + upscaled_masks: list[np.ndarray] | None = None + processed_mask: list[np.ndarray] | None = None + low_res_masks: list[np.ndarray] | None = None + iou_predictions: list[np.ndarray] | None = None + scores: list[np.ndarray] | None = None + labels: list[np.ndarray] | None = None + hard_predictions: list[np.ndarray] | None = None + soft_predictions: list[np.ndarray] | None = None + best_iou: list[float] | None = None def _compute_min_max(self, tensor: np.ndarray) -> tuple[np.ndarray, np.ndarray]: return tensor.min(), tensor.max() @@ -158,7 +153,7 @@ def __str__(self) -> str: assert self.hard_predictions is not None assert self.upscaled_masks is not None upscaled_masks_min, upscaled_masks_max = self._compute_min_max( - self.upscaled_masks[0] + self.upscaled_masks[0], ) return ( @@ -209,7 +204,7 @@ class DetectedKeypoints(NamedTuple): scores: np.ndarray def __str__(self): - return f"keypoints: {self.keypoints.shape}, keypoints_x_sum: {np.sum(self.keypoints[:,:1]):.3f}, scores: {self.scores.shape}" + return f"keypoints: {self.keypoints.shape}, keypoints_x_sum: {np.sum(self.keypoints[:, :1]):.3f}, scores: {self.scores.shape}" def add_rotated_rects(segmented_objects): @@ -217,23 +212,27 @@ def add_rotated_rects(segmented_objects): for segmented_object in segmented_objects: mask = segmented_object.mask.astype(np.uint8) contours, hierarchies = cv2.findContours( - mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + mask, + cv2.RETR_EXTERNAL, + cv2.CHAIN_APPROX_SIMPLE, ) contour = np.vstack(contours) objects_with_rects.append( - SegmentedObjectWithRects(segmented_object, cv2.minAreaRect(contour)) + SegmentedObjectWithRects(segmented_object, cv2.minAreaRect(contour)), ) return objects_with_rects def get_contours( - segmentedObjects: List[Union[SegmentedObject, SegmentedObjectWithRects]] + segmentedObjects: list[SegmentedObject | SegmentedObjectWithRects], ): combined_contours = [] for obj in segmentedObjects: contours, _ = cv2.findContours( - obj.mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE + obj.mask, + cv2.RETR_EXTERNAL, + cv2.CHAIN_APPROX_NONE, ) # Assuming one contour output for findContours. Based on OTX this is a safe # assumption @@ -255,7 +254,7 @@ def clip_detections(detections, size): class Contour(NamedTuple): label: str probability: float - shape: List[Tuple[int, int]] + shape: list[tuple[int, int]] def __str__(self): return f"{self.label}: {self.probability:.3f}, {len(self.shape)}" @@ -301,7 +300,8 @@ def compute_resolution(self, input_size): self.input_size = input_size size = self.input_size[::-1] self.scale_factor = min( - self.output_resolution[0] / size[0], self.output_resolution[1] / size[1] + self.output_resolution[0] / size[0], + self.output_resolution[1] / size[1], ) return self.scale(size) @@ -322,7 +322,7 @@ def scale(self, inputs): def load_labels(label_file): - with open(label_file, "r") as f: + with open(label_file) as f: return [x.strip() for x in f] diff --git a/model_api/python/model_api/models/visual_prompting.py b/model_api/python/model_api/models/visual_prompting.py index 40c1cf89..60c3f42e 100644 --- a/model_api/python/model_api/models/visual_prompting.py +++ b/model_api/python/model_api/models/visual_prompting.py @@ -1,14 +1,13 @@ -""" - 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. +"""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 __future__ import annotations # TODO: remove when Python3.9 support is dropped @@ -19,6 +18,7 @@ import cv2 import numpy as np + from model_api.models import SAMDecoder, SAMImageEncoder from model_api.models.utils import ( PredictedMask, @@ -38,8 +38,7 @@ class Prompt(NamedTuple): class SAMVisualPrompter: - """ - A wrapper that implements SAM Visual Prompter. + """A wrapper that implements SAM Visual Prompter. Segmentation results can be obtained by calling infer() method with corresponding parameters. @@ -59,8 +58,7 @@ def infer( boxes: list[Prompt] | None = None, points: list[Prompt] | None = None, ) -> VisualPromptingResult: - """ - Obtains segmentation masks using given prompts. + """Obtains segmentation masks using given prompts. Args: image (np.ndarray): HWC-shaped image @@ -134,8 +132,7 @@ def __call__( class SAMLearnableVisualPrompter: - """ - A wrapper that provides ZSL Visual Prompting workflow. + """A wrapper that provides ZSL Visual Prompting workflow. To obtain segmentation results, one should run learn() first to obtain the reference features, or use previously generated ones. """ @@ -147,8 +144,7 @@ def __init__( reference_features: VisualPromptingFeatures | None = None, threshold: float = 0.65, ): - """ - Initializes ZSL pipeline. + """Initializes ZSL pipeline. Args: encoder_model (SAMImageEncoder): initialized decoder wrapper @@ -183,20 +179,18 @@ def __init__( self._default_threshold_reference: float = 0.3 def has_reference_features(self) -> bool: - """ - Checks if reference features are stored in the object state. - """ + """Checks if reference features are stored in the object state.""" return self._reference_features is not None and self._used_indices is not None @property def reference_features(self) -> VisualPromptingFeatures: - """ - Property represents reference features. An exception is thrown if called when + """Property represents reference features. An exception is thrown if called when the features are not presented in the internal object state. """ if self.has_reference_features(): return VisualPromptingFeatures( - np.copy(self._reference_features), np.copy(self._used_indices) + np.copy(self._reference_features), + np.copy(self._used_indices), ) raise RuntimeError("Reference features are not generated") @@ -209,8 +203,7 @@ def learn( polygons: list[Prompt] | None = None, reset_features: bool = False, ) -> tuple[VisualPromptingFeatures, np.ndarray]: - """ - Executes `learn` stage of SAM ZSL pipeline. + """Executes `learn` stage of SAM ZSL pipeline. Reference features are updated according to newly arrived prompts. Features corresponding to the same labels are overridden during @@ -231,10 +224,9 @@ def learn( tuple[VisualPromptingFeatures, np.ndarray]: return values are the updated VPT reference features and reference masks. The shape of the reference mask is N_labels x H x W, where H and W are the same as in the input image. """ - if boxes is None and points is None and polygons is None: raise RuntimeError( - "boxes, polygons or points prompts are required for learning" + "boxes, polygons or points prompts are required for learning", ) if reset_features or not self.has_reference_features(): @@ -269,7 +261,8 @@ def learn( # get reference masks ref_masks: np.ndarray = np.zeros( - (largest_label + 1, *original_shape), dtype=np.uint8 + (largest_label + 1, *original_shape), + dtype=np.uint8, ) for label, input_prompts in processed_prompts_w_labels.items(): ref_mask: np.ndarray = np.zeros(original_shape, dtype=np.uint8) @@ -279,7 +272,9 @@ def learn( # bboxes and points inputs_decoder["image_embeddings"] = image_embeddings prediction = self._predict_masks( - inputs_decoder, original_shape, is_cascade=self._is_cascade + inputs_decoder, + original_shape, + is_cascade=self._is_cascade, ) masks = prediction["upscaled_masks"] elif "polygon" in inputs_decoder: @@ -301,7 +296,7 @@ def learn( self._reference_features[label] = ref_feat self._used_indices: np.ndarray = np.concatenate( - (self._used_indices, [label]) + (self._used_indices, [label]), ) ref_masks[label] = ref_mask @@ -324,8 +319,7 @@ def infer( reference_features: VisualPromptingFeatures | None = None, apply_masks_refinement: bool = True, ) -> ZSLVisualPromptingResult: - """ - Obtains masks by already prepared reference features. + """Obtains masks by already prepared reference features. Reference features can be obtained with SAMLearnableVisualPrompter.learn() and passed as an argument. If the features are not passed, instance internal state will be used as a source of the features. @@ -345,17 +339,15 @@ def infer( if reference_features is None: if self._reference_features is None: raise RuntimeError( - "Reference features are not defined. This parameter can be passed via SAMLearnableVisualPrompter constructor, or as an argument of infer() method" + "Reference features are not defined. This parameter can be passed via SAMLearnableVisualPrompter constructor, or as an argument of infer() method", ) - else: - reference_feats = self._reference_features + reference_feats = self._reference_features if self._used_indices is None: raise RuntimeError( - "Used indices are not defined. This parameter can be passed via SAMLearnableVisualPrompter constructor, or as an argument of infer() method" + "Used indices are not defined. This parameter can be passed via SAMLearnableVisualPrompter constructor, or as an argument of infer() method", ) - else: - used_idx = self._used_indices + used_idx = self._used_indices else: reference_feats, used_idx = reference_features @@ -394,7 +386,9 @@ def infer( continue point_coords = np.concatenate( - (np.array([[x, y]]), bg_coords), axis=0, dtype=np.float32 + (np.array([[x, y]]), bg_coords), + axis=0, + dtype=np.float32, ) point_coords = self.decoder.apply_coords(point_coords, original_shape) point_labels = np.array([1] + [0] * len(bg_coords), dtype=np.float32) @@ -406,7 +400,9 @@ def infer( inputs_decoder["image_embeddings"] = image_embeddings prediction = self._predict_masks( - inputs_decoder, original_shape, apply_masks_refinement + inputs_decoder, + original_shape, + apply_masks_refinement, ) prediction.update({"scores": points_score[-1]}) @@ -430,7 +426,8 @@ def infer( def reset_reference_info(self) -> None: """Initialize reference information.""" self._reference_features = np.zeros( - (0, 1, self.decoder.embed_dim), dtype=np.float32 + (0, 1, self.decoder.embed_dim), + dtype=np.float32, ) self._used_indices = np.array([], dtype=np.int64) @@ -439,9 +436,8 @@ def _gather_prompts_with_labels( image_prompts: list[dict[str, np.ndarray]], ) -> dict[int, list[dict[str, np.ndarray]]]: """Gather prompts according to labels.""" - processed_prompts: defaultdict[int, list[dict[str, np.ndarray]]] = defaultdict( - list + list, ) for prompt in image_prompts: processed_prompts[int(prompt["label"])].append(prompt) @@ -484,8 +480,11 @@ def _predict_masks( elif i == 1: # Cascaded Post-refinement-1 mask_input, masks, _ = _decide_masks( - masks, logits, scores, is_single=True - ) # noqa: F821 + masks, + logits, + scores, + is_single=True, + ) if masks.sum() == 0: return {"upscaled_masks": masks} @@ -494,8 +493,10 @@ def _predict_masks( elif i == 2: # Cascaded Post-refinement-2 mask_input, masks, _ = _decide_masks( - masks, logits, scores - ) # noqa: F821 + masks, + logits, + scores, + ) if masks.sum() == 0: return {"upscaled_masks": masks} @@ -503,7 +504,8 @@ def _predict_masks( y, x = np.nonzero(masks) box_coords = self.decoder.apply_coords( np.array( - [[x.min(), y.min()], [x.max(), y.max()]], dtype=np.float32 + [[x.min(), y.min()], [x.max(), y.max()]], + dtype=np.float32, ), original_size, ) @@ -511,10 +513,12 @@ def _predict_masks( inputs.update( { "point_coords": np.concatenate( - (inputs["point_coords"], box_coords), axis=1 + (inputs["point_coords"], box_coords), + axis=1, ), "point_labels": np.concatenate( - (inputs["point_labels"], self._point_labels_box), axis=1 + (inputs["point_labels"], self._point_labels_box), + axis=1, ), }, ) @@ -533,7 +537,9 @@ def _predict_masks( def _polygon_to_mask( - polygon: np.ndarray | list[np.ndarray], height: int, width: int + polygon: np.ndarray | list[np.ndarray], + height: int, + width: int, ) -> np.ndarray: """Converts a polygon represented as an array of 2D points into a mask""" if isinstance(polygon, np.ndarray) and np.issubdtype(polygon.dtype, np.integer): @@ -609,13 +615,9 @@ def _decide_masks( scores, masks, logits = (x[:, 1:] for x in (scores, masks, logits)) # filter zero masks - while ( - len(scores[0]) > 0 - and masks[0, (best_idx := np.argmax(scores[0]))].sum() == 0 - ): + while len(scores[0]) > 0 and masks[0, (best_idx := np.argmax(scores[0]))].sum() == 0: scores, masks, logits = ( - np.concatenate((x[:, :best_idx], x[:, best_idx + 1 :]), axis=1) - for x in (scores, masks, logits) + np.concatenate((x[:, :best_idx], x[:, best_idx + 1 :]), axis=1) for x in (scores, masks, logits) ) if len(scores[0]) == 0: @@ -688,7 +690,8 @@ def _point_selection( # Top-first point selection point_coords = np.where(mask_sim > threshold) fg_coords_scores = np.stack( - point_coords[::-1] + (mask_sim[point_coords],), axis=0 + point_coords[::-1] + (mask_sim[point_coords],), + axis=0, ).T ## skip if there is no point coords @@ -700,31 +703,26 @@ def _point_selection( n_w = width // downsizing ## get grid numbers - idx_grid = ( - fg_coords_scores[:, 1] * ratio // downsizing * n_w - + fg_coords_scores[:, 0] * ratio // downsizing - ) + idx_grid = fg_coords_scores[:, 1] * ratio // downsizing * n_w + fg_coords_scores[:, 0] * ratio // downsizing idx_grid_unique = np.unique(idx_grid.astype(np.int64)) ## get matched indices - matched_matrix = ( - np.expand_dims(idx_grid, axis=-1) == idx_grid_unique - ) # (totalN, uniqueN) + matched_matrix = np.expand_dims(idx_grid, axis=-1) == idx_grid_unique # (totalN, uniqueN) ## sample fg_coords_scores matched by matched_matrix matched_grid = np.expand_dims(fg_coords_scores, axis=1) * np.expand_dims( - matched_matrix, axis=-1 + matched_matrix, + axis=-1, ) ## sample the highest score one of the samples that are in the same grid - matched_indices = _topk_numpy(matched_grid[..., -1], k=1, axis=0, largest=True)[1][ - 0 - ].astype(np.int64) + matched_indices = _topk_numpy(matched_grid[..., -1], k=1, axis=0, largest=True)[1][0].astype(np.int64) points_scores = matched_grid[matched_indices].diagonal().T ## sort by the highest score sorted_points_scores_indices = np.flip( - np.argsort(points_scores[:, -1]), axis=-1 + np.argsort(points_scores[:, -1]), + axis=-1, ).astype(np.int64) points_scores = points_scores[sorted_points_scores_indices] @@ -739,7 +737,9 @@ def _point_selection( def _resize_to_original_shape( - masks: np.ndarray, image_size: int, original_shape: np.ndarray + masks: np.ndarray, + image_size: int, + original_shape: np.ndarray, ) -> np.ndarray: """Resize feature size to original shape.""" # resize feature size to input size @@ -763,7 +763,10 @@ def _get_prepadded_size(original_shape: np.ndarray, image_size: int) -> np.ndarr def _topk_numpy( - x: np.ndarray, k: int, axis: int = -1, largest: bool = True + x: np.ndarray, + k: int, + axis: int = -1, + largest: bool = True, ) -> tuple[np.ndarray, np.ndarray]: """Top-k function for numpy same with torch.topk.""" if largest: @@ -787,10 +790,11 @@ def _inspect_overlapping_areas( threshold_iou: float = 0.8, ) -> None: def _calculate_mask_iou( - mask1: np.ndarray, mask2: np.ndarray + mask1: np.ndarray, + mask2: np.ndarray, ) -> tuple[float, np.ndarray | None]: - assert mask1.ndim == 2 # noqa: S101 - assert mask2.ndim == 2 # noqa: S101 + assert mask1.ndim == 2 + assert mask2.ndim == 2 # Avoid division by zero if (union := np.logical_or(mask1, mask2).sum().item()) == 0: return 0.0, None @@ -798,7 +802,8 @@ def _calculate_mask_iou( return intersection.sum().item() / union, intersection for (label, masks), (other_label, other_masks) in product( - predicted_masks.items(), predicted_masks.items() + predicted_masks.items(), + predicted_masks.items(), ): if other_label <= label: continue @@ -806,7 +811,8 @@ def _calculate_mask_iou( overlapped_label = [] overlapped_other_label = [] for (im, mask), (jm, other_mask) in product( - enumerate(masks), enumerate(other_masks) + enumerate(masks), + enumerate(other_masks), ): _mask_iou, _intersection = _calculate_mask_iou(mask, other_mask) if _mask_iou > threshold_iou: diff --git a/model_api/python/model_api/models/yolo.py b/model_api/python/model_api/models/yolo.py index 82291947..02ffded7 100644 --- a/model_api/python/model_api/models/yolo.py +++ b/model_api/python/model_api/models/yolo.py @@ -1,19 +1,20 @@ -""" - Copyright (C) 2020-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. +"""Copyright (C) 2020-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 collections import namedtuple +from itertools import starmap import numpy as np + from model_api.adapters.utils import INTERPOLATION_TYPES, resize_image_ocv from .detection_model import DetectionModel @@ -95,9 +96,7 @@ def permute_to_N_HWA_K(tensor, K, output_layout): - """ - Transpose/reshape a tensor from (N, (A x K), H, W) to (N, (HxWxA), K) - """ + """Transpose/reshape a tensor from (N, (A x K), H, W) to (N, (HxWxA), K)""" assert tensor.ndim == 4, tensor.shape if output_layout == "NHWC": tensor = tensor.transpose(0, 3, 1, 2) @@ -154,9 +153,7 @@ def __init__(self, param, sides): def __init__(self, inference_adapter, configuration, preload=False): super().__init__(inference_adapter, configuration, preload) - self.is_tiny = ( - len(self.outputs) == 2 - ) # Weak way to distinguish between YOLOv4 and YOLOv4-tiny + self.is_tiny = len(self.outputs) == 2 # Weak way to distinguish between YOLOv4 and YOLOv4-tiny self._check_io_number(1, -1) @@ -194,7 +191,7 @@ def parameters(cls): default_value=0.5, description="Threshold for non-maximum suppression (NMS) intersection over union (IOU) filtering", ), - } + }, ) parameters["resize_type"].update_default_value("fit_to_window_letterbox") parameters["confidence_threshold"].update_default_value(0.5) @@ -211,7 +208,9 @@ def _parse_yolo_region(self, predictions, input_size, params): objects = [] size_normalizer = input_size if params.use_input_size else params.sides predictions = permute_to_N_HWA_K( - predictions, params.bbox_size, params.output_layout + predictions, + params.bbox_size, + params.output_layout, ) # ------------------------------------------- Parsing YOLO Region output --------------------------------------- for prediction in predictions: @@ -249,7 +248,7 @@ def _parse_yolo_region(self, predictions, input_size, params): predicted_box.y + predicted_box.h / 2, confidence.item(), label.item(), - ) + ), ) return objects @@ -274,7 +273,12 @@ def _get_raw_box(prediction, obj_ind): @staticmethod def _get_absolute_det_box( - box, row, col, anchors, coord_normalizer, size_normalizer + box, + row, + col, + anchors, + coord_normalizer, + size_normalizer, ): x = (col + box.x) / coord_normalizer[1] y = (row + box.y) / coord_normalizer[0] @@ -287,10 +291,12 @@ def _get_absolute_det_box( def _filter(detections, iou_threshold): def iou(box_1, box_2): width_of_overlap_area = min(box_1.xmax, box_2.xmax) - max( - box_1.xmin, box_2.xmin + box_1.xmin, + box_2.xmin, ) height_of_overlap_area = min(box_1.ymax, box_2.ymax) - max( - box_1.ymin, box_2.ymin + box_1.ymin, + box_2.ymin, ) if width_of_overlap_area < 0 or height_of_overlap_area < 0: area_of_overlap = 0 @@ -324,7 +330,9 @@ def _parse_outputs(self, outputs, meta): layer_params = self.yolo_layer_params[layer_name] out_blob.shape = layer_params[0] detections += self._parse_yolo_region( - out_blob, meta["resized_shape"], layer_params[1] + out_blob, + meta["resized_shape"], + layer_params[1], ) detections = self._filter(detections, self.iou_threshold) @@ -355,12 +363,12 @@ def _get_output_info(self): if not self.anchors: self.anchors = ANCHORS["YOLOV4-TINY"] if self.is_tiny else ANCHORS["YOLOV4"] if not self.masks: - self.masks = ( - [1, 2, 3, 3, 4, 5] if self.is_tiny else [0, 1, 2, 3, 4, 5, 6, 7, 8] - ) + self.masks = [1, 2, 3, 3, 4, 5] if self.is_tiny else [0, 1, 2, 3, 4, 5, 6, 7, 8] outputs = sorted( - self.outputs.items(), key=lambda x: x[1].shape[2], reverse=True + self.outputs.items(), + key=lambda x: x[1].shape[2], + reverse=True, ) output_info = {} @@ -374,7 +382,7 @@ def _get_output_info(self): classes = channels // num - 5 if channels % num != 0: self.raise_error( - "The output blob {} has wrong 2nd dimension".format(name) + f"The output blob {name} has wrong 2nd dimension", ) yolo_params = self.Params( classes, @@ -394,9 +402,9 @@ def parameters(cls): { "anchors": ListValue(description="List of custom anchor values"), "masks": ListValue( - description="List of mask, applied to anchors for each output layer" + description="List of mask, applied to anchors for each output layer", ), - } + }, ) return parameters @@ -456,7 +464,12 @@ def _get_probabilities(prediction, classes): @staticmethod def _get_absolute_det_box( - box, row, col, anchors, coord_normalizer, size_normalizer + box, + row, + col, + anchors, + coord_normalizer, + size_normalizer, ): anchor_x = anchors[0] / size_normalizer[0] anchor_y = anchors[1] / size_normalizer[1] @@ -489,7 +502,7 @@ def parameters(cls): default_value=0.65, description="Threshold for non-maximum suppression (NMS) intersection over union (IOU) filtering", ), - } + }, ) parameters["confidence_threshold"].update_default_value(0.5) return parameters @@ -497,7 +510,9 @@ def parameters(cls): def preprocess(self, inputs): image = inputs resized_image = resize_image_ocv( - image, (self.w, self.h), keep_aspect_ratio=True + image, + (self.w, self.h), + keep_aspect_ratio=True, ) padded_image = np.ones((self.h, self.w, 3), dtype=np.uint8) * 114 @@ -510,10 +525,10 @@ def preprocess(self, inputs): preprocessed_image = self.input_transform(padded_image) preprocessed_image = preprocessed_image.transpose( - (2, 0, 1) + (2, 0, 1), ) # Change data layout from HWC to CHW preprocessed_image = preprocessed_image.reshape( - (self.n, self.c, self.h, self.w) + (self.n, self.c, self.h, self.w), ) dict_inputs = {self.image_blob_name: preprocessed_image} @@ -544,17 +559,19 @@ def postprocess(self, outputs, meta): include_boundaries=True, ) - detections = [ - Detection(*det) - for det in zip( - x_mins[keep_nms], - y_mins[keep_nms], - x_maxs[keep_nms], - y_maxs[keep_nms], - scores[keep_nms], - j[keep_nms], + detections = list( + starmap( + Detection, + zip( + x_mins[keep_nms], + y_mins[keep_nms], + x_maxs[keep_nms], + y_maxs[keep_nms], + scores[keep_nms], + j[keep_nms], + ), ) - ] + ) return clip_detections(detections, meta["original_shape"]) def set_strides_grids(self): @@ -582,11 +599,7 @@ class YoloV3ONNX(DetectionModel): def __init__(self, inference_adapter, configuration=dict(), preload=False): super().__init__(inference_adapter, configuration, preload) - self.image_info_blob_name = ( - self.image_info_blob_names[0] - if len(self.image_info_blob_names) == 1 - else None - ) + self.image_info_blob_name = self.image_info_blob_names[0] if len(self.image_info_blob_names) == 1 else None self._check_io_number(2, 3) self.classes = 80 ( @@ -619,18 +632,12 @@ def _get_outputs(self): self.raise_error( "Expected shapes [:,:,4], [:,{},:] and [:,3] for outputs, but got {}, {} and {}".format( self.classes, - *[output.shape for output in self.outputs.values()] - ) + *[output.shape for output in self.outputs.values()], + ), ) - if ( - self.outputs[bboxes_blob_name].shape[1] - != self.outputs[scores_blob_name].shape[2] - ): + if self.outputs[bboxes_blob_name].shape[1] != self.outputs[scores_blob_name].shape[2]: self.raise_error( - "Expected the same dimension for boxes and scores, but got {} and {}".format( - self.outputs[bboxes_blob_name].shape[1], - self.outputs[scores_blob_name].shape[2], - ) + f"Expected the same dimension for boxes and scores, but got {self.outputs[bboxes_blob_name].shape[1]} and {self.outputs[scores_blob_name].shape[2]}", ) return bboxes_blob_name, scores_blob_name, indices_blob_name @@ -652,19 +659,23 @@ def preprocess(self, inputs): dict_inputs = { self.image_blob_name: np.expand_dims(image, axis=0), self.image_info_blob_name: np.array( - [[image.shape[0], image.shape[1]]], dtype=np.float32 + [[image.shape[0], image.shape[1]]], + dtype=np.float32, ), } else: resized_image = self.resize( - image, (self.w, self.h), interpolation=INTERPOLATION_TYPES["CUBIC"] + image, + (self.w, self.h), + interpolation=INTERPOLATION_TYPES["CUBIC"], ) meta.update({"resized_shape": resized_image.shape}) resized_image = self._change_layout(resized_image) dict_inputs = { self.image_blob_name: resized_image, self.image_info_blob_name: np.array( - [[image.shape[0], image.shape[1]]], dtype=np.float32 + [[image.shape[0], image.shape[1]]], + dtype=np.float32, ), } @@ -708,18 +719,13 @@ def _parse_outputs(self, outputs): x_maxs = transposed_boxes[3] y_maxs = transposed_boxes[2] - detections = [ - Detection(*det) - for det in zip(x_mins, y_mins, x_maxs, y_maxs, out_scores, out_classes) - ] + detections = list(starmap(Detection, zip(x_mins, y_mins, x_maxs, y_maxs, out_scores, out_classes))) return detections class YOLOv5(DetectionModel): - """ - Reimplementation of ultralytics.YOLO - """ + """Reimplementation of ultralytics.YOLO""" __model__ = "YOLOv5" @@ -727,10 +733,10 @@ def __init__(self, inference_adapter, configuration, preload=False): super().__init__(inference_adapter, configuration, preload) self._check_io_number(1, 1) output = next(iter(self.outputs.values())) - if "f32" != output.precision: + if output.precision != "f32": self.raise_error("the output must be of precision f32") out_shape = output.shape - if 3 != len(out_shape): + if len(out_shape) != 3: self.raise_error("the output must be of rank 3") if self.labels and len(self.labels) + 4 != out_shape[1]: self.raise_error("number of labels must be smaller than out_shape[1] by 4") @@ -756,25 +762,23 @@ def parameters(cls): default_value=0.7, description="Threshold for non-maximum suppression (NMS) intersection over union (IOU) filtering", ), - } + }, ) return parameters def postprocess(self, outputs, meta): - if 1 != len(outputs): + if len(outputs) != 1: self.raise_error("expect 1 output") prediction = next(iter(outputs.values())) if np.float32 != prediction.dtype: self.raise_error("the output must be of precision f32") out_shape = prediction.shape - if 3 != len(out_shape): + if len(out_shape) != 3: raise RuntimeError("the output must be of rank 3") - if 1 != out_shape[0]: + if out_shape[0] != 1: raise RuntimeError("the first dim of the output must be 1") LABELS_START = 4 - filtered = prediction[0].T[ - (prediction[:, LABELS_START:] > self.confidence_threshold).any(1)[0] - ] + filtered = prediction[0].T[(prediction[:, LABELS_START:] > self.confidence_threshold).any(1)[0]] confidences = filtered[:, LABELS_START:] labels = confidences.argmax(1, keepdims=True) confidences = np.take_along_axis(confidences, labels, 1) @@ -805,16 +809,11 @@ def postprocess(self, outputs, meta): inputImgHeight / self.orig_height, ) padLeft, padTop = 0, 0 - if ( - "fit_to_window" == self.resize_type - or "fit_to_window_letterbox" == self.resize_type - ): + if self.resize_type == "fit_to_window" or self.resize_type == "fit_to_window_letterbox": invertedScaleX = invertedScaleY = max(invertedScaleX, invertedScaleY) - if "fit_to_window_letterbox" == self.resize_type: + if self.resize_type == "fit_to_window_letterbox": padLeft = (self.orig_width - round(inputImgWidth / invertedScaleX)) // 2 - padTop = ( - self.orig_height - round(inputImgHeight / invertedScaleY) - ) // 2 + padTop = (self.orig_height - round(inputImgHeight / invertedScaleY)) // 2 coords = boxes[:, 2:] coords -= (padLeft, padTop, padLeft, padTop) coords *= (invertedScaleX, invertedScaleY, invertedScaleX, invertedScaleY) @@ -830,7 +829,10 @@ def postprocess(self, outputs, meta): return DetectionResult( [ Detection( - *intboxes[i], boxes[i, 1], intid[i], self.get_label_name(intid[i]) + *intboxes[i], + boxes[i, 1], + intid[i], + self.get_label_name(intid[i]), ) for i in range(len(boxes)) ], diff --git a/model_api/python/model_api/performance_metrics.py b/model_api/python/model_api/performance_metrics.py index 93b84966..fff1598c 100644 --- a/model_api/python/model_api/performance_metrics.py +++ b/model_api/python/model_api/performance_metrics.py @@ -1,17 +1,16 @@ -""" - Copyright (C) 2020-2024 Intel Corporation +"""Copyright (C) 2020-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 +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 +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. +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. """ import logging as log @@ -21,10 +20,22 @@ def put_highlighted_text( - frame, message, position, font_face, font_scale, color, thickness + frame, + message, + position, + font_face, + font_scale, + color, + thickness, ): cv2.putText( - frame, message, position, font_face, font_scale, (255, 255, 255), thickness + 1 + frame, + message, + position, + font_face, + font_scale, + (255, 255, 255), + thickness + 1, ) # white border cv2.putText(frame, message, position, font_face, font_scale, color, thickness) @@ -82,7 +93,7 @@ def paint_metrics( if current_latency is not None: put_highlighted_text( frame, - "Latency: {:.1f} ms".format(current_latency * 1e3), + f"Latency: {current_latency * 1e3:.1f} ms", position, cv2.FONT_HERSHEY_COMPLEX, font_scale, @@ -92,7 +103,7 @@ def paint_metrics( if current_fps is not None: put_highlighted_text( frame, - "FPS: {:.1f}".format(current_fps), + f"FPS: {current_fps:.1f}", (position[0], position[1] + 30), cv2.FONT_HERSHEY_COMPLEX, font_scale, @@ -103,43 +114,27 @@ def paint_metrics( def get_last(self): return ( ( - self.last_moving_statistic.latency - / self.last_moving_statistic.frame_count + self.last_moving_statistic.latency / self.last_moving_statistic.frame_count if self.last_moving_statistic.frame_count != 0 else None ), ( - self.last_moving_statistic.frame_count - / self.last_moving_statistic.period + self.last_moving_statistic.frame_count / self.last_moving_statistic.period if self.last_moving_statistic.period != 0.0 else None ), ) def get_total(self): - frame_count = ( - self.total_statistic.frame_count + self.current_moving_statistic.frame_count - ) + frame_count = self.total_statistic.frame_count + self.current_moving_statistic.frame_count return ( ( - ( - ( - self.total_statistic.latency - + self.current_moving_statistic.latency - ) - / frame_count - ) + ((self.total_statistic.latency + self.current_moving_statistic.latency) / frame_count) if frame_count != 0 else None ), ( - ( - frame_count - / ( - self.total_statistic.period - + self.current_moving_statistic.period - ) - ) + (frame_count / (self.total_statistic.period + self.current_moving_statistic.period)) if frame_count != 0 else None ), @@ -152,10 +147,8 @@ def log_total(self): total_latency, total_fps = self.get_total() log.info("Metrics report:") log.info( - "\tLatency: {:.1f} ms".format(total_latency * 1e3) - if total_latency is not None - else "\tLatency: N/A" + f"\tLatency: {total_latency * 1e3:.1f} ms" if total_latency is not None else "\tLatency: N/A", ) log.info( - "\tFPS: {:.1f}".format(total_fps) if total_fps is not None else "\tFPS: N/A" + f"\tFPS: {total_fps:.1f}" if total_fps is not None else "\tFPS: N/A", ) diff --git a/model_api/python/model_api/pipelines/async_pipeline.py b/model_api/python/model_api/pipelines/async_pipeline.py index 8798bc74..8d262216 100644 --- a/model_api/python/model_api/pipelines/async_pipeline.py +++ b/model_api/python/model_api/pipelines/async_pipeline.py @@ -1,17 +1,16 @@ -""" - Copyright (C) 2020-2024 Intel Corporation +"""Copyright (C) 2020-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 +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 +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. +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 time import perf_counter @@ -65,10 +64,13 @@ def get_result(self, id): self.inference_metrics.update(infer_start_time) postprocessing_start_time = perf_counter() - result = self.model.postprocess(raw_result, preprocess_meta), { - **meta, - **preprocess_meta, - } + result = ( + self.model.postprocess(raw_result, preprocess_meta), + { + **meta, + **preprocess_meta, + }, + ) self.postprocess_metrics.update(postprocessing_start_time) return result return None diff --git a/model_api/python/model_api/tilers/__init__.py b/model_api/python/model_api/tilers/__init__.py index e4a5f19f..879c1be6 100644 --- a/model_api/python/model_api/tilers/__init__.py +++ b/model_api/python/model_api/tilers/__init__.py @@ -1,17 +1,16 @@ -""" - Copyright (C) 2023-2024 Intel Corporation +"""Copyright (C) 2023-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 +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 +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. +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 .detection import DetectionTiler diff --git a/model_api/python/model_api/tilers/detection.py b/model_api/python/model_api/tilers/detection.py index 0e81c235..bd4a4159 100644 --- a/model_api/python/model_api/tilers/detection.py +++ b/model_api/python/model_api/tilers/detection.py @@ -1,21 +1,21 @@ -""" - Copyright (c) 2023-2024 Intel Corporation +"""Copyright (c) 2023-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 +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 +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. +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. """ import cv2 as cv import numpy as np + from model_api.models.types import NumericalValue from model_api.models.utils import Detection, DetectionResult, multiclass_nms @@ -23,8 +23,7 @@ class DetectionTiler(Tiler): - """ - Tiler for object detection models. + """Tiler for object detection models. This tiler expects model to output a lsit of `Detection` objects or one `DetectionResult` object. """ @@ -55,7 +54,7 @@ def parameters(cls): max=1.0, description="IoU threshold which is used to apply NMS to bounding boxes", ), - } + }, ) return parameters @@ -70,7 +69,6 @@ def _postprocess_tile(self, predictions, coord): Returns: a dict with postprocessed predictions in 6-items format: (label id, score, bbox) """ - output_dict = {} if hasattr(predictions, "objects"): detections = _detection2array(predictions.objects) @@ -104,7 +102,6 @@ def _merge_results(self, results, shape): Returns: merged prediction """ - detections_array = np.empty((0, 6), dtype=np.float32) feature_vectors = [] saliency_maps = [] @@ -123,14 +120,8 @@ def _merge_results(self, results, shape): iou_threshold=self.iou_threshold, ) - merged_vector = ( - np.mean(feature_vectors, axis=0) if feature_vectors else np.ndarray(0) - ) - saliency_map = ( - self._merge_saliency_maps(saliency_maps, shape, tiles_coords) - if saliency_maps - else np.ndarray(0) - ) + merged_vector = np.mean(feature_vectors, axis=0) if feature_vectors else np.ndarray(0) + saliency_map = self._merge_saliency_maps(saliency_maps, shape, tiles_coords) if saliency_maps else np.ndarray(0) detected_objects = [] for i in range(detections_array.shape[0]): @@ -138,7 +129,7 @@ def _merge_results(self, results, shape): score = float(detections_array[i][1]) bbox = list(detections_array[i][2:].astype(np.int32)) detected_objects.append( - Detection(*bbox, score, label, self.model.labels[label]) + Detection(*bbox, score, label, self.model.labels[label]), ) return DetectionResult( @@ -158,7 +149,6 @@ def _merge_saliency_maps(self, saliency_maps, shape, tiles_coords): Returns: Merged saliency map with shape (Nc, H, W) """ - if not saliency_maps: return None @@ -176,8 +166,13 @@ def _merge_saliency_maps(self, saliency_maps, shape, tiles_coords): map_h, map_w = image_saliency_map.shape[1:] image_h, image_w, _ = shape - ratio = map_h / min(image_h, self.tile_size), map_w / min( - image_w, self.tile_size + ratio = ( + map_h / min(image_h, self.tile_size), + map_w + / min( + image_w, + self.tile_size, + ), ) image_map_h = int(image_h * ratio[0]) @@ -207,9 +202,7 @@ def _merge_saliency_maps(self, saliency_maps, shape, tiles_coords): map_pixel = cls_map[hi, wi] merged_pixel = merged_map[class_idx][y_1 + hi, x_1 + wi] if merged_pixel != 0: - merged_map[class_idx][y_1 + hi, x_1 + wi] = 0.5 * ( - map_pixel + merged_pixel - ) + merged_map[class_idx][y_1 + hi, x_1 + wi] = 0.5 * (map_pixel + merged_pixel) else: merged_map[class_idx][y_1 + hi, x_1 + wi] = map_pixel @@ -229,7 +222,6 @@ def _merge_saliency_maps(self, saliency_maps, shape, tiles_coords): def _non_linear_normalization(saliency_map): """Use non-linear normalization y=x**1.5 for 2D saliency maps.""" - min_soft_score = np.min(saliency_map) # make merged_map distribution positive to perform non-linear normalization y=x**1.5 saliency_map = (saliency_map - min_soft_score) ** 1.5 diff --git a/model_api/python/model_api/tilers/instance_segmentation.py b/model_api/python/model_api/tilers/instance_segmentation.py index 6ad07f3b..f7b5df86 100644 --- a/model_api/python/model_api/tilers/instance_segmentation.py +++ b/model_api/python/model_api/tilers/instance_segmentation.py @@ -1,23 +1,23 @@ -""" - Copyright (c) 2023-2024 Intel Corporation +"""Copyright (c) 2023-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 +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 +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. +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 contextlib import contextmanager import cv2 as cv import numpy as np + from model_api.models.instance_segmentation import MaskRCNNModel, _segm_postprocess from model_api.models.utils import ( InstanceSegmentationResult, @@ -29,8 +29,7 @@ class InstanceSegmentationTiler(DetectionTiler): - """ - Tiler for object instance segmentation models. + """Tiler for object instance segmentation models. This tiler expects model to output a list of `SegmentedObject` objects. In addition, this tiler allows to use a tile classifier model, @@ -45,8 +44,7 @@ def __init__( execution_mode="async", tile_classifier_model=None, ): - """ - Constructor for creating a semantic segmentation tiling pipeline + """Constructor for creating a semantic segmentation tiling pipeline Args: model: underlying model @@ -90,7 +88,6 @@ def _postprocess_tile(self, predictions, coord): Returns: a dict with postprocessed detections in 6-items format: (label id, score, bbox) and masks """ - output_dict = super()._postprocess_tile(predictions, coord) output_dict["masks"] = [] for segm_res in predictions.segmentedObjects: @@ -109,7 +106,6 @@ def _merge_results(self, results, shape): Returns: merged prediction """ - detections_array = np.empty((0, 6), dtype=np.float32) feature_vectors = [] saliency_maps = [] @@ -133,14 +129,8 @@ def _merge_results(self, results, shape): ) masks = [masks[keep_idx] for keep_idx in keep_idxs] - merged_vector = ( - np.mean(feature_vectors, axis=0) if feature_vectors else np.ndarray(0) - ) - saliency_map = ( - self._merge_saliency_maps(saliency_maps, shape, tiles_coords) - if saliency_maps - else [] - ) + merged_vector = np.mean(feature_vectors, axis=0) if feature_vectors else np.ndarray(0) + saliency_map = self._merge_saliency_maps(saliency_maps, shape, tiles_coords) if saliency_maps else [] detected_objects = [] for i in range(detections_array.shape[0]): @@ -149,7 +139,7 @@ def _merge_results(self, results, shape): bbox = list(detections_array[i][2:].astype(np.int32)) masks[i] = _segm_postprocess(np.array(bbox), masks[i], *shape[:-1]) detected_objects.append( - SegmentedObject(*bbox, score, label, self.model.labels[label], masks[i]) + SegmentedObject(*bbox, score, label, self.model.labels[label], masks[i]), ) return InstanceSegmentationResult( @@ -169,7 +159,6 @@ def _merge_saliency_maps(self, saliency_maps, shape, tiles_coords): Returns: Merged saliency map with shape (Nc, H, W) """ - if not saliency_maps: return None @@ -197,11 +186,7 @@ def _merge_saliency_maps(self, saliency_maps, shape, tiles_coords): merged_map[class_idx][y_1:y_2, x_1:x_2] = np.maximum(tile_map, cls_map) for class_idx in range(num_classes): - image_map_cls = ( - image_saliency_map[class_idx] - if self.tile_with_full_img - else np.ndarray(0) - ) + image_map_cls = image_saliency_map[class_idx] if self.tile_with_full_img else np.ndarray(0) if len(image_map_cls.shape) < 2: merged_map[class_idx] = np.round(merged_map[class_idx]).astype(np.uint8) if np.sum(merged_map[class_idx]) == 0: diff --git a/model_api/python/model_api/tilers/semantic_segmentation.py b/model_api/python/model_api/tilers/semantic_segmentation.py index d125d3f3..c00c346e 100644 --- a/model_api/python/model_api/tilers/semantic_segmentation.py +++ b/model_api/python/model_api/tilers/semantic_segmentation.py @@ -1,17 +1,16 @@ -""" - Copyright (C) 2024 Intel Corporation +"""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 +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 +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. +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 __future__ import annotations @@ -19,6 +18,7 @@ from contextlib import contextmanager import numpy as np + from model_api.models import SegmentationModel from model_api.models.utils import ImageResultWithSoftPrediction @@ -26,9 +26,7 @@ class SemanticSegmentationTiler(Tiler): - """ - Tiler for segmentation models. - """ + """Tiler for segmentation models.""" def _postprocess_tile( self, @@ -50,7 +48,9 @@ def _postprocess_tile( return output_dict def _merge_results( - self, results: list[dict], shape: tuple[int, int, int] + self, + results: list[dict], + shape: tuple[int, int, int], ) -> ImageResultWithSoftPrediction: """Merge the results from all tiles. diff --git a/model_api/python/model_api/tilers/tiler.py b/model_api/python/model_api/tilers/tiler.py index 6bae39a9..99e48004 100644 --- a/model_api/python/model_api/tilers/tiler.py +++ b/model_api/python/model_api/tilers/tiler.py @@ -1,17 +1,16 @@ -""" - Copyright (c) 2023-2024 Intel Corporation +"""Copyright (c) 2023-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 +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 +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. +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. """ import abc @@ -22,7 +21,7 @@ from model_api.pipelines import AsyncPipeline -class Tiler(metaclass=abc.ABCMeta): +class Tiler(abc.ABC): EXECUTION_MODES = ["async", "sync"] """ An abstract tiler @@ -42,8 +41,7 @@ class Tiler(metaclass=abc.ABCMeta): """ def __init__(self, model, configuration=dict(), execution_mode="async"): - """ - Base constructor for creating a tiling pipeline + """Base constructor for creating a tiling pipeline Args: model: underlying model @@ -51,7 +49,6 @@ def __init__(self, model, configuration=dict(), execution_mode="async"): tiler (`tile_size`, `tiles_overlap` etc.) which are set as data attributes. execution_mode: Controls inference mode of the tiler (`async` or `sync`). """ - self.logger = log.getLogger() self.model = model for name, parameter in self.parameters().items(): @@ -60,7 +57,7 @@ def __init__(self, model, configuration=dict(), execution_mode="async"): self.async_pipeline = AsyncPipeline(self.model) if execution_mode not in Tiler.EXECUTION_MODES: raise ValueError( - f"Wrong execution mode. The following modes are supported {Tiler.EXECUTION_MODES}" + f"Wrong execution mode. The following modes are supported {Tiler.EXECUTION_MODES}", ) self.execution_mode = execution_mode @@ -97,7 +94,7 @@ def parameters(cls): default_value=True, description="Whether to include full image as a tile", ), - } + }, ) return parameters @@ -118,7 +115,7 @@ def _load_config(self, config): then the default value of the parameter will be updated. If some key presented in the config is not introduced in `parameters`, it will be omitted. - Raises: + Raises: RuntimeError: if the configuration is incorrect """ parameters = self.parameters() @@ -127,18 +124,13 @@ def _load_config(self, config): try: value = param.from_str( self.model.inference_adapter.get_rt_info( - ["model_info", name] - ).astype(str) + ["model_info", name], + ).astype(str), ) self.__setattr__(name, value) except RuntimeError as error: - missing_rt_info = ( - "Cannot get runtime attribute. Path to runtime attribute is incorrect." - in str(error) - ) - is_OVMSAdapter = ( - str(error) == "OVMSAdapter does not support RT info getting" - ) + missing_rt_info = "Cannot get runtime attribute. Path to runtime attribute is incorrect." in str(error) + is_OVMSAdapter = str(error) == "OVMSAdapter does not support RT info getting" if not missing_rt_info and not is_OVMSAdapter: raise @@ -156,12 +148,11 @@ def _load_config(self, config): self.__setattr__(name, value) else: self.logger.warning( - f'The parameter "{name}" not found in tiler, will be omitted' + f'The parameter "{name}" not found in tiler, will be omitted', ) def __call__(self, inputs): - """ - Applies full pipeline of tiling inference in one call. + """Applies full pipeline of tiling inference in one call. Args: inputs: raw input data, the data type is defined by underlying model wrapper @@ -169,7 +160,6 @@ def __call__(self, inputs): Returns: - postprocessed data in the format defined by underlying model wrapper """ - tile_coords = self._tile(inputs) tile_coords = self._filter_tiles(inputs, tile_coords) diff --git a/model_api/python/pyproject.toml b/model_api/python/pyproject.toml index c5bb769b..62285df7 100644 --- a/model_api/python/pyproject.toml +++ b/model_api/python/pyproject.toml @@ -65,3 +65,172 @@ Repository = "https://github.com/openvinotoolkit/model_api.git" [tool.setuptools.packages.find] include = ["model_api*"] + + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# RUFF CONFIGURATION # +[tool.ruff] +# Enable preview features +preview = true + + +# TODO: Slowly enable all the rules. + +# Enable rules +lint.select = [ + # "F", # Pyflakes (`F`) + # "E", # pycodestyle error (`E`) + # "W", # pycodestyle warning (`W`) + # "C90", # mccabe (`C90`) + "I", # isort (`I`) + # "N", # pep8-naming (`N`) + # "D", # pydocstyle (`D`) + # "UP", # pyupgrade (`UP`) + # "YTT", # flake8-2020 (`YTT`) + # "ANN", # flake8-annotations (`ANN`) + # "S", # flake8-bandit (`S`) + # "BLE", # flake8-blind-except (`BLE`) + # "FBT", # flake8-boolean-trap (`FBT`) + # "B", # flake8-bugbear (`B`) + # "A", # flake8-builtins (`A`) + # "COM", # flake8-commas (`COM`) + # "CPY", # flake8-copyright (`CPY`) + # "C4", # flake8-comprehensions (`C4`) + # "DTZ", # flake8-datatimez (`DTZ`) + # "T10", # flake8-debugger (`T10`) + # "EM", # flake8-errmsg (`EM`) + # "FA", # flake8-future-annotations (`FA`) + # "ISC", # flake8-implicit-str-concat (`ISC`) + # "ICN", # flake8-import-conventions (`ICN`) + # "PIE", # flake8-pie (`PIE`) + # "PT", # flake8-pytest-style (`PT`) + # "RSE", # flake8-raise (`RSE`) + # "RET", # flake8-return (`RET`) + # "SLF", # flake8-self (`SLF`) + # "SIM", # flake8-simplify (`SIM`) + # "TID", # flake8-tidy-imports (`TID`) + # "TCH", # flake8-type-checking (`TCH`) + # "INT", # flake8-gettext (`INT`) + # "ARG", # flake8-unsused-arguments (`ARG`) + # "PTH", # flake8-use-pathlib (`PTH`) + # "TD", # flake8-todos (`TD`) + # "FIX", # flake8-fixme (`FIX`) + # "ERA", # eradicate (`ERA`) + # "PD", # pandas-vet (`PD`) + # "PGH", # pygrep-hooks (`PGH`) + # "PL", # pylint (`PL`) + # "TRY", # tryceratos (`TRY`) + # "FLY", # flynt (`FLY`) + # "NPY", # NumPy-specific rules (`NPY`) + # "PERF", # Perflint (`PERF`) + # "RUF", # Ruff-specific rules (`RUF`) + # "FURB", # refurb (`FURB`) - ERROR: Unknown rule selector: `FURB` + # "LOG", # flake8-logging (`LOG`) - ERROR: Unknown rule selector: `LOG` +] + +lint.ignore = [ + # pydocstyle + "D107", # Missing docstring in __init__ + + # pylint + "PLR0913", # Too many arguments to function call + "PLR2004", # consider replacing with a constant variable + "PLR0912", # Too many branches + "PLR0915", # Too many statements + + # NOTE: Disable the following rules for now. + "A004", # import is shadowing a Python built-in + "A005", # Module is shadowing a Python built-in + "B909", # Mutation to loop iterable during iteration + "PLC2701", # Private name import + "PLC0415", # import should be at the top of the file + "PLR0917", # Too many positional arguments + "E226", # Missing whitespace around arithmetic operator + "E266", # Too many leading `#` before block comment + + "F822", # Undefined name `` in `__all__` + + "PGH004", # Use specific rule codes when using 'ruff: noqa' + "PT001", # Use @pytest.fixture over @pytest.fixture() + "PLR6104", # Use `*=` to perform an augmented assignment directly + "PLR0914", # Too many local variables + "PLC0206", # Extracting value from dictionary without calling `.items()` + "PLC1901", # can be simplified + + "RUF021", # Parenthesize the `and` subexpression + "RUF022", # Apply an isort-style sorting to '__all__' + "S404", # `subprocess` module is possibly insecure + # End of disable rules + + # flake8-annotations + "ANN101", # Missing-type-self + "ANN002", # Missing type annotation for *args + "ANN003", # Missing type annotation for **kwargs + + # flake8-bandit (`S`) + "S101", # Use of assert detected. + + # flake8-boolean-trap (`FBT`) + "FBT001", # Boolean positional arg in function definition + "FBT002", # Boolean default value in function definition + + # flake8-datatimez (`DTZ`) + "DTZ005", # The use of `datetime.datetime.now()` without `tz` argument is not allowed + + # flake8-fixme (`FIX`) + "FIX002", # Line contains TODO, consider resolving the issue +] + +# Allow autofix for all enabled rules (when `--fix`) is provided. +lint.fixable = ["ALL"] +lint.unfixable = [] + +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", +] + +# Same as Black. +line-length = 120 + +# Allow unused variables when underscore-prefixed. +lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +# Assume Python 3.10. +target-version = "py310" + +# Allow imports relative to the "src" and "tests" directories. +src = ["src", "tests"] + +[tool.ruff.lint.mccabe] +# Unlike Flake8, default to a complexity level of 10. +max-complexity = 15 + + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.flake8-copyright] +notice-rgx = """ +# Copyright \\(C\\) (\\d{4}(-\\d{4})?) Intel Corporation +# SPDX-License-Identifier: Apache-2\\.0 +""" diff --git a/tests/cpp/precommit/prepare_data.py b/tests/cpp/precommit/prepare_data.py index 96a14deb..22b82f46 100644 --- a/tests/cpp/precommit/prepare_data.py +++ b/tests/cpp/precommit/prepare_data.py @@ -22,6 +22,8 @@ def prepare_model( data_dir="./data", public_scope=Path(__file__).resolve().parent / "public_scope.json", ): + # TODO refactor this test so that it does not use eval + # flake8: noqa: F401 from model_api.models import ClassificationModel, DetectionModel, SegmentationModel with open(public_scope, "r") as f: diff --git a/tests/python/accuracy/test_accuracy.py b/tests/python/accuracy/test_accuracy.py index b9635c0a..e926d1a0 100644 --- a/tests/python/accuracy/test_accuracy.py +++ b/tests/python/accuracy/test_accuracy.py @@ -6,9 +6,13 @@ import numpy as np import onnx import pytest + from model_api.adapters.onnx_adapter import ONNXRuntimeAdapter from model_api.adapters.openvino_adapter import OpenvinoAdapter, create_core from model_api.adapters.utils import load_parameters_from_onnx + +# TODO refactor this test so that it does not use eval +# flake8: noqa: F401 from model_api.models import ( ActionClassificationModel, AnomalyDetection, diff --git a/tests/python/precommit/test_save.py b/tests/python/precommit/test_save.py index d201dab1..57775aa6 100644 --- a/tests/python/precommit/test_save.py +++ b/tests/python/precommit/test_save.py @@ -6,13 +6,15 @@ def test_detector_save(tmp_path): "ssd_mobilenet_v1_fpn_coco", configuration={"mean_values": [0, 0, 0], "confidence_threshold": 0.6}, ) - assert True == downloaded.get_model().get_rt_info( - ["model_info", "embedded_processing"] - ).astype(bool) + assert ( + downloaded.get_model() + .get_rt_info(["model_info", "embedded_processing"]) + .astype(bool) + ) xml_path = str(tmp_path / "a.xml") downloaded.save(xml_path) deserialized = Model.create_model(xml_path) - assert type(downloaded) == type(deserialized) + assert type(downloaded) is type(deserialized) for attr in downloaded.parameters(): assert getattr(downloaded, attr) == getattr(deserialized, attr) @@ -21,13 +23,15 @@ def test_classifier_save(tmp_path): downloaded = Model.create_model( "efficientnet-b0-pytorch", configuration={"scale_values": [1, 1, 1], "topk": 6} ) - assert True == downloaded.get_model().get_rt_info( - ["model_info", "embedded_processing"] - ).astype(bool) + assert ( + downloaded.get_model() + .get_rt_info(["model_info", "embedded_processing"]) + .astype(bool) + ) xml_path = str(tmp_path / "a.xml") downloaded.save(xml_path) deserialized = Model.create_model(xml_path) - assert type(downloaded) == type(deserialized) + assert type(downloaded) is type(deserialized) for attr in downloaded.parameters(): assert getattr(downloaded, attr) == getattr(deserialized, attr) @@ -37,12 +41,14 @@ def test_segmentor_save(tmp_path): "hrnet-v2-c1-segmentation", configuration={"reverse_input_channels": True, "labels": ["first", "second"]}, ) - assert True == downloaded.get_model().get_rt_info( - ["model_info", "embedded_processing"] - ).astype(bool) + assert ( + downloaded.get_model() + .get_rt_info(["model_info", "embedded_processing"]) + .astype(bool) + ) xml_path = str(tmp_path / "a.xml") downloaded.save(xml_path) deserialized = Model.create_model(xml_path) - assert type(downloaded) == type(deserialized) + assert type(downloaded) is type(deserialized) for attr in downloaded.parameters(): assert getattr(downloaded, attr) == getattr(deserialized, attr)