diff --git a/.github/workflows/test_accuracy.yml b/.github/workflows/test_accuracy.yml index d9f0a7bd..e4584c1c 100644 --- a/.github/workflows/test_accuracy.yml +++ b/.github/workflows/test_accuracy.yml @@ -10,12 +10,12 @@ concurrency: cancel-in-progress: true jobs: test_accuracy: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: "3.10" cache: pip - name: Create and start a virtual environment run: | @@ -25,7 +25,7 @@ jobs: run: | source venv/bin/activate python -m pip install --upgrade pip - pip install model_api/python/[tests] --extra-index-url https://download.pytorch.org/whl/cpu + pip install model_api/python/[tests,build] --extra-index-url https://download.pytorch.org/whl/cpu - name: Prepare test data run: | source venv/bin/activate @@ -35,13 +35,15 @@ jobs: source venv/bin/activate pytest --data=./data tests/python/accuracy/test_accuracy.py DATA=data pytest --data=./data tests/python/accuracy/test_YOLOv8.py - - name: Install CPP ependencies + - name: Install CPP dependencies run: | sudo bash model_api/cpp/install_dependencies.sh - name: Build CPP Test run: | + pip install nanobind==2.4.0 + pip install typing_extensions==4.12.2 mkdir build && cd build - cmake ../tests/cpp/accuracy/ -DCMAKE_CXX_FLAGS=-Werror + cmake ../tests/cpp/accuracy/ make -j - name: Run CPP Test run: | diff --git a/.github/workflows/test_precommit.yml b/.github/workflows/test_precommit.yml index 1e3fc906..6084a799 100644 --- a/.github/workflows/test_precommit.yml +++ b/.github/workflows/test_precommit.yml @@ -48,12 +48,12 @@ jobs: # missingInclude: cppcheck can't find stl, openvino, opencv other_options: --suppress=missingInclude -Imodel_api/cpp/models/include -Imodel_api/cpp/utils/include -Imodel_api/cpp/pipelines/include --check-config CPP-Precommit: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: "3.10" cache: pip - name: Create and start a virtual environment run: | @@ -63,7 +63,7 @@ jobs: run: | source venv/bin/activate python -m pip install --upgrade pip - pip install model_api/python/[tests] --extra-index-url https://download.pytorch.org/whl/cpu + pip install model_api/python/[tests,build] --extra-index-url https://download.pytorch.org/whl/cpu sudo bash model_api/cpp/install_dependencies.sh - name: Prepare test data @@ -73,7 +73,9 @@ jobs: - name: Build run: | mkdir build && cd build - cmake ../tests/cpp/precommit/ -DCMAKE_CXX_FLAGS=-Werror + pip install nanobind==2.4.0 + pip install typing_extensions==4.12.2 + cmake ../tests/cpp/precommit/ cmake --build . -j $((`nproc`*2+2)) - name: Run test run: | @@ -96,7 +98,7 @@ jobs: run: | source venv/Scripts/activate python -m pip install --upgrade pip - pip install model_api/python/[tests] --extra-index-url https://download.pytorch.org/whl/cpu + pip install model_api/python/[tests,build] --extra-index-url https://download.pytorch.org/whl/cpu curl https://storage.openvinotoolkit.org/repositories/openvino/packages/2024.6/windows/w_openvino_toolkit_windows_2024.6.0.17404.4c0f47d2335_x86_64.zip --output w_openvino_toolkit_windows.zip unzip w_openvino_toolkit_windows.zip rm w_openvino_toolkit_windows.zip @@ -112,7 +114,7 @@ jobs: shell: bash run: | mkdir build && cd build - MSYS_NO_PATHCONV=1 cmake ../examples/cpp/ -DOpenVINO_DIR=$GITHUB_WORKSPACE/w_openvino_toolkit_windows_2024.6.0.17404.4c0f47d2335_x86_64/runtime/cmake -DOpenCV_DIR=$GITHUB_WORKSPACE/opencv/opencv/build -DCMAKE_CXX_FLAGS=/WX + MSYS_NO_PATHCONV=1 cmake ../examples/cpp/ -DOpenVINO_DIR=$GITHUB_WORKSPACE/w_openvino_toolkit_windows_2024.6.0.17404.4c0f47d2335_x86_64/runtime/cmake -DOpenCV_DIR=$GITHUB_WORKSPACE/opencv/opencv/build -DCMAKE_CXX_FLAGS=/WX -DENABLE_PY_BINDINGS=OFF cmake --build . --config Release -j $((`nproc`*2+2)) - name: Run sync sample shell: cmd diff --git a/model_api/cpp/CMakeLists.txt b/model_api/cpp/CMakeLists.txt index 4bcb9bbf..44e9b057 100644 --- a/model_api/cpp/CMakeLists.txt +++ b/model_api/cpp/CMakeLists.txt @@ -1,9 +1,11 @@ -# Copyright (C) 2018-2024 Intel Corporation +# Copyright (C) 2018-2025 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # cmake_minimum_required(VERSION 3.26) +option(ENABLE_PY_BINDINGS "Enables building python bindings package" ON) + # Multi config generators such as Visual Studio ignore CMAKE_BUILD_TYPE. Multi config generators are configured with # CMAKE_CONFIGURATION_TYPES, but limiting options in it completely removes such build options get_property(GENERATOR_IS_MULTI_CONFIG_VAR GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) @@ -81,6 +83,11 @@ if(MSVC) /EHsc) # Enable standard C++ stack unwinding, assume functions with extern "C" never throw elseif(CMAKE_CXX_COMPILER_ID MATCHES "^GNU|(Apple)?Clang$") target_compile_options(model_api PRIVATE -Wall -Wextra -Wpedantic) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") +endif() + +if (ENABLE_PY_BINDINGS) + add_subdirectory(py_bindings) endif() include(GenerateExportHeader) diff --git a/model_api/cpp/cmake/model_apiConfig.cmake b/model_api/cpp/cmake/model_apiConfig.cmake index 84f6272b..570679ce 100644 --- a/model_api/cpp/cmake/model_apiConfig.cmake +++ b/model_api/cpp/cmake/model_apiConfig.cmake @@ -3,3 +3,5 @@ find_dependency(OpenCV COMPONENTS core imgproc) find_dependency(OpenVINO COMPONENTS Runtime) include("${CMAKE_CURRENT_LIST_DIR}/model_apiTargets.cmake") + +check_required_components() diff --git a/model_api/cpp/py_bindings/CMakeLists.txt b/model_api/cpp/py_bindings/CMakeLists.txt new file mode 100644 index 00000000..91eb70ce --- /dev/null +++ b/model_api/cpp/py_bindings/CMakeLists.txt @@ -0,0 +1,28 @@ +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +set(DEV_MODULE Development.Module) + +find_package(Python COMPONENTS Interpreter ${DEV_MODULE} REQUIRED) + +execute_process( + COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE nanobind_ROOT) +find_package(nanobind CONFIG REQUIRED) + + +file(GLOB BINDINGS_SOURCES ./*.cpp) +file(GLOB BINDINGS_HEADERS ./*.hpp) + +nanobind_add_module(py_model_api NB_STATIC STABLE_ABI LTO ${BINDINGS_SOURCES} ${BINDINGS_HEADERS}) + +target_link_libraries(py_model_api PRIVATE model_api) + +nanobind_add_stub( + py_model_api_stub + MODULE py_model_api + OUTPUT py_model_api.pyi + PYTHON_PATH $ + DEPENDS py_model_api +) diff --git a/model_api/cpp/py_bindings/py_base.cpp b/model_api/cpp/py_bindings/py_base.cpp new file mode 100644 index 00000000..e257991d --- /dev/null +++ b/model_api/cpp/py_bindings/py_base.cpp @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2025 Intel Corporation + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include + +#include "models/image_model.h" +#include "models/results.h" + +namespace nb = nanobind; + +void init_base_modules(nb::module_& m) { + nb::class_(m, "ResultBase").def(nb::init<>()); + + nb::class_(m, "ModelBase") + .def("load", [](ModelBase& self, const std::string& device, size_t num_infer_requests) { + auto core = ov::Core(); + self.load(core, device, num_infer_requests); + }); + + nb::class_(m, "ImageModel"); +} diff --git a/model_api/cpp/py_bindings/py_classificaiton.cpp b/model_api/cpp/py_bindings/py_classificaiton.cpp new file mode 100644 index 00000000..19e2b4eb --- /dev/null +++ b/model_api/cpp/py_bindings/py_classificaiton.cpp @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2025 Intel Corporation + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include + +#include "models/classification_model.h" +#include "models/results.h" +#include "py_utils.hpp" + +namespace pyutils = vision::nanobind::utils; + +void init_classification(nb::module_& m) { + nb::class_(m, "Classification") + .def(nb::init()) + .def_rw("id", &ClassificationResult::Classification::id) + .def_rw("label", &ClassificationResult::Classification::label) + .def_rw("score", &ClassificationResult::Classification::score); + + nb::class_(m, "ClassificationResult") + .def(nb::init<>()) + .def_ro("topLabels", &ClassificationResult::topLabels) + .def("__repr__", &ClassificationResult::operator std::string) + .def_prop_ro( + "feature_vector", + [](ClassificationResult& r) { + if (!r.feature_vector) { + return nb::ndarray(); + } + + return nb::ndarray(r.feature_vector.data(), + r.feature_vector.get_shape().size(), + r.feature_vector.get_shape().data()); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "saliency_map", + [](ClassificationResult& r) { + if (!r.saliency_map) { + return nb::ndarray(); + } + + return nb::ndarray(r.saliency_map.data(), + r.saliency_map.get_shape().size(), + r.saliency_map.get_shape().data()); + }, + nb::rv_policy::reference_internal); + + nb::class_(m, "ClassificationModel") + .def_static( + "create_model", + [](const std::string& model_path, + const std::map& configuration, + bool preload, + const std::string& device) { + auto ov_any_config = ov::AnyMap(); + for (const auto& item : configuration) { + ov_any_config[item.first] = pyutils::py_object_to_any(item.second, item.first); + } + + return ClassificationModel::create_model(model_path, ov_any_config, preload, device); + }, + nb::arg("model_path"), + nb::arg("configuration") = ov::AnyMap({}), + nb::arg("preload") = true, + nb::arg("device") = "AUTO") + + .def("__call__", + [](ClassificationModel& self, const nb::ndarray<>& input) { + return self.infer(pyutils::wrap_np_mat(input)); + }) + .def("infer_batch", [](ClassificationModel& self, const std::vector> inputs) { + std::vector input_mats; + input_mats.reserve(inputs.size()); + + for (const auto& input : inputs) { + input_mats.push_back(pyutils::wrap_np_mat(input)); + } + + return self.inferBatch(input_mats); + }); +} diff --git a/model_api/cpp/py_bindings/py_utils.cpp b/model_api/cpp/py_bindings/py_utils.cpp new file mode 100644 index 00000000..d08881c5 --- /dev/null +++ b/model_api/cpp/py_bindings/py_utils.cpp @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 Intel Corporation + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "py_utils.hpp" + +namespace vision::nanobind::utils { + +cv::Mat wrap_np_mat(const nb::ndarray<>& input) { + if (input.ndim() != 3 || input.shape(2) != 3 || input.dtype() != nb::dtype()) { + throw std::runtime_error("Input image should have HWC_8U layout"); + } + + int height = input.shape(0); + int width = input.shape(1); + + return cv::Mat(height, width, CV_8UC3, input.data()); +} + +ov::Any py_object_to_any(const nb::object& py_obj, const std::string& property_name) { + if (nb::isinstance(py_obj)) { + return ov::Any(std::string(static_cast(py_obj).c_str())); + } else if (nb::isinstance(py_obj)) { + return ov::Any(static_cast(static_cast(py_obj))); + } else if (nb::isinstance(py_obj)) { + return ov::Any(static_cast(static_cast(py_obj))); + } else { + OPENVINO_THROW("Property \"" + property_name + "\" has unsupported type."); + } +} + +} // namespace vision::nanobind::utils diff --git a/model_api/cpp/py_bindings/py_utils.hpp b/model_api/cpp/py_bindings/py_utils.hpp new file mode 100644 index 00000000..955dbfe3 --- /dev/null +++ b/model_api/cpp/py_bindings/py_utils.hpp @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2025 Intel Corporation + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once +#include + +#include +#include + +namespace nb = nanobind; + +namespace vision::nanobind::utils { +cv::Mat wrap_np_mat(const nb::ndarray<>& input); +ov::Any py_object_to_any(const nb::object& py_obj, const std::string& property_name); +} // namespace vision::nanobind::utils diff --git a/model_api/cpp/py_bindings/py_vision_api.cpp b/model_api/cpp/py_bindings/py_vision_api.cpp new file mode 100644 index 00000000..c10e6486 --- /dev/null +++ b/model_api/cpp/py_bindings/py_vision_api.cpp @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2025 Intel Corporation + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +namespace nb = nanobind; + +void init_classification(nb::module_& m); +void init_base_modules(nb::module_& m); + +NB_MODULE(py_model_api, m) { + m.doc() = "Nanobind binding for OpenVINO Vision API library"; + init_base_modules(m); + init_classification(m); +} diff --git a/model_api/python/pyproject.toml b/model_api/python/pyproject.toml index 468d2752..316aece3 100644 --- a/model_api/python/pyproject.toml +++ b/model_api/python/pyproject.toml @@ -59,7 +59,10 @@ docs = [ "breathe", "graphviz", ] -full = ["openvino_model_api[dependencies, ovms, tests, docs]"] +build = [ + "nanobind==2.4.0", +] +full = ["openvino_model_api[dependencies, ovms, tests, docs, build]"] [project.urls] Homepage = "https://github.com/openvinotoolkit/model_api" diff --git a/tests/cpp/accuracy/CMakeLists.txt b/tests/cpp/accuracy/CMakeLists.txt index 7bac347c..28e2917c 100644 --- a/tests/cpp/accuracy/CMakeLists.txt +++ b/tests/cpp/accuracy/CMakeLists.txt @@ -30,7 +30,7 @@ if(MSVC) endif() if(CMAKE_CXX_COMPILER_ID MATCHES "^GNU|(Apple)?Clang$") - add_compile_options(-Wall -Wextra -Wpedantic) + add_compile_options(-Wall -Wextra) endif() if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64.*|aarch64.*|AARCH64.*)")