diff --git a/.github/workflows/test_accuracy.yml b/.github/workflows/test_accuracy.yml index 61c877ca..0ce9dc10 100644 --- a/.github/workflows/test_accuracy.yml +++ b/.github/workflows/test_accuracy.yml @@ -44,6 +44,15 @@ jobs: pip install typing_extensions==4.12.2 cmake ../tests/cpp/accuracy/ make -j + - name: Build CPP-PY Bindings + run: | + source venv/bin/activate + pip install src/cpp/py_bindings - name: Run CPP Test run: | build/test_accuracy -d data -p tests/python/accuracy/public_scope.json + - name: Run CPP-PY Bindings Test + run: | + source venv/bin/activate + pip list + pytest --data=./data --config=./tests/python/accuracy/public_scope.json tests/cpp/accuracy/test_bindings.py diff --git a/examples/cpp/CMakeLists.txt b/examples/cpp/CMakeLists.txt index 2fe5a7fd..fe22571f 100644 --- a/examples/cpp/CMakeLists.txt +++ b/examples/cpp/CMakeLists.txt @@ -92,7 +92,6 @@ endmacro() find_package(OpenCV REQUIRED COMPONENTS imgcodecs) -set (ENABLE_PY_BINDINGS OFF) add_subdirectory(../../src/cpp ${Samples_BINARY_DIR}/src/cpp) add_example(NAME asynchronous_api SOURCES ./asynchronous_api/main.cpp DEPENDENCIES model_api) diff --git a/src/cpp/CMakeLists.txt b/src/cpp/CMakeLists.txt index 192e145f..880f211a 100644 --- a/src/cpp/CMakeLists.txt +++ b/src/cpp/CMakeLists.txt @@ -4,8 +4,6 @@ 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) @@ -77,10 +75,6 @@ elseif(CMAKE_CXX_COMPILER_ID MATCHES "^GNU|(Apple)?Clang$") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") endif() -if (ENABLE_PY_BINDINGS) - add_subdirectory(py_bindings) -endif() - include(GenerateExportHeader) generate_export_header(model_api) diff --git a/src/cpp/py_bindings/.gitignore b/src/cpp/py_bindings/.gitignore new file mode 100644 index 00000000..20b07f5e --- /dev/null +++ b/src/cpp/py_bindings/.gitignore @@ -0,0 +1,3 @@ +*.whl +*.dll +*.so* diff --git a/src/cpp/py_bindings/CMakeLists.txt b/src/cpp/py_bindings/CMakeLists.txt index b955b8c4..b25da694 100644 --- a/src/cpp/py_bindings/CMakeLists.txt +++ b/src/cpp/py_bindings/CMakeLists.txt @@ -2,7 +2,17 @@ # SPDX-License-Identifier: Apache-2.0 # +cmake_minimum_required(VERSION 3.26) + +if(WIN32) + set(CMAKE_GENERATOR_TOOLSET "v142") +endif() + + +add_subdirectory(../ model_api/cpp) + set(Python_FIND_VIRTUALENV FIRST) +project(_vision_api LANGUAGES CXX) find_package(Python COMPONENTS Interpreter Development REQUIRED) execute_process( @@ -11,17 +21,17 @@ execute_process( find_package(nanobind CONFIG REQUIRED) -file(GLOB BINDINGS_SOURCES ./*.cpp) -file(GLOB BINDINGS_HEADERS ./*.hpp) +file(GLOB BINDINGS_SOURCES src/vision_api/*.cpp) +file(GLOB BINDINGS_HEADERS src/vision_api/*.hpp) -nanobind_add_module(py_model_api NB_STATIC STABLE_ABI LTO ${BINDINGS_SOURCES} ${BINDINGS_HEADERS}) +message(INFO ${BINDINGS_SOURCES}) -target_link_libraries(py_model_api PRIVATE model_api) +nanobind_add_module(_vision_api NB_STATIC STABLE_ABI LTO ${BINDINGS_SOURCES} ${BINDINGS_HEADERS}) -nanobind_add_stub( - py_model_api_stub - MODULE py_model_api - OUTPUT py_model_api.pyi - PYTHON_PATH $ - DEPENDS py_model_api +set_target_properties(_vision_api PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/src/vision_api" ) + +target_link_libraries(_vision_api PRIVATE model_api) + +include(opencv.cmake) diff --git a/src/cpp/py_bindings/opencv.cmake b/src/cpp/py_bindings/opencv.cmake new file mode 100644 index 00000000..0e2b45ad --- /dev/null +++ b/src/cpp/py_bindings/opencv.cmake @@ -0,0 +1,28 @@ +find_package(OpenCV REQUIRED COMPONENTS core imgproc) + +if (MSVC) + set(DEPENDENCIES_TO_COPY + "${__location_release}" + ) +else() + find_package(PkgConfig REQUIRED) + find_package(TBB "2021.5.0" EXACT REQUIRED) + pkg_check_modules(TBB REQUIRED tbb) + + set(DEPENDENCIES_TO_COPY + ${OpenCV_DIR}/../../libopencv_core.so.4.5d + ${OpenCV_DIR}/../../libopencv_imgproc.so.4.5d + ${pkgcfg_lib_TBB_tbb}.2 # ubuntu system package uses tbb.so.2 + ) +endif() + +set(PYTHON_PACKAGE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/vision_api) +foreach(lib ${DEPENDENCIES_TO_COPY}) + add_custom_command( + TARGET _vision_api POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${lib} + ${PYTHON_PACKAGE_DIR} + COMMENT "Copying ${lib} to ${PYTHON_PACKAGE_DIR}" + ) +endforeach() diff --git a/src/cpp/py_bindings/pyproject.toml b/src/cpp/py_bindings/pyproject.toml new file mode 100644 index 00000000..1436f3e5 --- /dev/null +++ b/src/cpp/py_bindings/pyproject.toml @@ -0,0 +1,32 @@ +[build-system] +requires = ["scikit-build-core >=0.4.3", "nanobind >=1.3.2"] +build-backend = "scikit_build_core.build" + +[project] +name = "vision_api" +version = "0.3.0.2" +requires-python = ">=3.9" +authors = [ + {name = "Intel(R) Corporation"}, +] +maintainers = [ + {name = "Intel(R) Corporation"}, +] +description = "Model API: model wrappers and pipelines for inference with OpenVINO" +readme = "../../python/README.md" +classifiers = [ + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3.9" +] + +[project.urls] +Homepage = "https://github.com/open-edge-platform/model_api" + +[tool.scikit-build] +# Protect the configuration against future changes in scikit-build-core +minimum-version = "0.4" +# Setuptools-style build caching in a local directory +build-dir = "build/{wheel_tag}" +# Build stable ABI wheels for CPython 3.12+ +wheel.py-api = "cp312" +sdist.include = ["*.so*"] diff --git a/src/cpp/py_bindings/run.py b/src/cpp/py_bindings/run.py new file mode 100644 index 00000000..16141d5b --- /dev/null +++ b/src/cpp/py_bindings/run.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 + +from vision_api import ClassificationModel +import cv2 + +import sys + +if len(sys.argv) != 3: + raise RuntimeError(f"Usage: {sys.argv[0]} ") + +model_path = sys.argv[1] +image_path = sys.argv[2] + +model = ClassificationModel.create_model(model_path) +image = cv2.cvtColor(cv2.imread(image_path), cv2.COLOR_BGR2RGB) +model(image) diff --git a/src/cpp/py_bindings/src/vision_api/__init__.py b/src/cpp/py_bindings/src/vision_api/__init__.py new file mode 100644 index 00000000..0dbae48c --- /dev/null +++ b/src/cpp/py_bindings/src/vision_api/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +try: + from openvino import Core + + _ = Core() # Triggers loading of shared libs like libopenvino.so +except Exception as e: + raise ImportError(f"Failed to initialize OpenVINO runtime: {e}") + +from ._vision_api import ClassificationModel + +__all__ = [ClassificationModel] diff --git a/src/cpp/py_bindings/py_base.cpp b/src/cpp/py_bindings/src/vision_api/py_base.cpp similarity index 100% rename from src/cpp/py_bindings/py_base.cpp rename to src/cpp/py_bindings/src/vision_api/py_base.cpp diff --git a/src/cpp/py_bindings/py_classification.cpp b/src/cpp/py_bindings/src/vision_api/py_classification.cpp similarity index 100% rename from src/cpp/py_bindings/py_classification.cpp rename to src/cpp/py_bindings/src/vision_api/py_classification.cpp diff --git a/src/cpp/py_bindings/py_utils.cpp b/src/cpp/py_bindings/src/vision_api/py_utils.cpp similarity index 100% rename from src/cpp/py_bindings/py_utils.cpp rename to src/cpp/py_bindings/src/vision_api/py_utils.cpp diff --git a/src/cpp/py_bindings/py_utils.hpp b/src/cpp/py_bindings/src/vision_api/py_utils.hpp similarity index 100% rename from src/cpp/py_bindings/py_utils.hpp rename to src/cpp/py_bindings/src/vision_api/py_utils.hpp diff --git a/src/cpp/py_bindings/py_vision_api.cpp b/src/cpp/py_bindings/src/vision_api/py_vision_api.cpp similarity index 92% rename from src/cpp/py_bindings/py_vision_api.cpp rename to src/cpp/py_bindings/src/vision_api/py_vision_api.cpp index c10e6486..13605a01 100644 --- a/src/cpp/py_bindings/py_vision_api.cpp +++ b/src/cpp/py_bindings/src/vision_api/py_vision_api.cpp @@ -10,7 +10,7 @@ namespace nb = nanobind; void init_classification(nb::module_& m); void init_base_modules(nb::module_& m); -NB_MODULE(py_model_api, m) { +NB_MODULE(_vision_api, m) { m.doc() = "Nanobind binding for OpenVINO Vision API library"; init_base_modules(m); init_classification(m); diff --git a/tests/cpp/accuracy/CMakeLists.txt b/tests/cpp/accuracy/CMakeLists.txt index c1792701..e9648e91 100644 --- a/tests/cpp/accuracy/CMakeLists.txt +++ b/tests/cpp/accuracy/CMakeLists.txt @@ -67,7 +67,6 @@ include(../cmake/common.cmake) find_package(OpenCV REQUIRED COMPONENTS core highgui videoio imgproc imgcodecs) -set(ENABLE_PY_BINDINGS OFF) add_subdirectory(../../../src/cpp ${tests_BINARY_DIR}/model_api/cpp) add_test(NAME test_accuracy SOURCES test_accuracy.cpp DEPENDENCIES model_api) diff --git a/tests/cpp/accuracy/conftest.py b/tests/cpp/accuracy/conftest.py new file mode 100644 index 00000000..c05e5922 --- /dev/null +++ b/tests/cpp/accuracy/conftest.py @@ -0,0 +1,9 @@ +# +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + + +def pytest_addoption(parser): + parser.addoption("--data", action="store", help="data folder with dataset") + parser.addoption("--config", action="store", help="path to models config") diff --git a/tests/cpp/accuracy/test_bindings.py b/tests/cpp/accuracy/test_bindings.py new file mode 100644 index 00000000..00b4fb3d --- /dev/null +++ b/tests/cpp/accuracy/test_bindings.py @@ -0,0 +1,56 @@ +# +# Copyright (C) 2025 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest +import json +from pathlib import Path + +import cv2 + +from model_api.models import Model +from vision_api import ClassificationModel + + +def read_config(models_config: str, model_type: str): + with open(models_config, "r") as f: + data = json.load(f) + for item in data: + if item["type"] == model_type: + yield item + + +@pytest.fixture(scope="session") +def data(pytestconfig) -> str: + return pytestconfig.getoption("data") + + +@pytest.fixture(scope="session") +def models_config(pytestconfig) -> str: + return pytestconfig.getoption("config") + + +@pytest.fixture() +def classification_configs(models_config: str): + return read_config(models_config, "ClassificationModel") + + +def test_classification_models(data: str, classification_configs): + for model_data in classification_configs: + name = model_data["name"] + if ".xml" not in name: + continue + if name.endswith(".xml") or name.endswith(".onnx"): + name = f"{data}/{name}" + + model = Model.create_model(name, preload=True) + cpp_model = ClassificationModel.create_model(name, preload=True) + + image_path = Path(data) / next(iter(model_data["test_data"]))["image"] + image = cv2.cvtColor(cv2.imread(image_path), cv2.COLOR_BGR2RGB) + + py_result = model(image) + cpp_result = cpp_model(image) + + assert str(py_result) == str(cpp_result) diff --git a/tests/cpp/cmake/common.cmake b/tests/cpp/cmake/common.cmake index 0a88dca3..af7326df 100644 --- a/tests/cpp/cmake/common.cmake +++ b/tests/cpp/cmake/common.cmake @@ -41,7 +41,6 @@ macro(add_test) target_link_libraries(${TEST_NAME} PRIVATE pthread) endif() - target_link_libraries(${TEST_NAME} PRIVATE gtest gtest_main) - target_link_libraries(${TEST_NAME} PRIVATE nlohmann_json::nlohmann_json) + target_link_libraries(${TEST_NAME} PRIVATE gtest_main gmock_main nlohmann_json::nlohmann_json) endmacro() diff --git a/tests/cpp/precommit/CMakeLists.txt b/tests/cpp/precommit/CMakeLists.txt index ab8c77ff..0c56bf15 100644 --- a/tests/cpp/precommit/CMakeLists.txt +++ b/tests/cpp/precommit/CMakeLists.txt @@ -66,7 +66,6 @@ include(../cmake/common.cmake) find_package(OpenCV REQUIRED COMPONENTS core highgui videoio imgproc imgcodecs) -set(ENABLE_PY_BINDINGS OFF) add_subdirectory(../../../src/cpp ${tests_BINARY_DIR}/model_api/cpp) add_test(NAME test_sanity SOURCES test_sanity.cpp DEPENDENCIES model_api) diff --git a/tests/python/accuracy/public_scope.json b/tests/python/accuracy/public_scope.json index e244ece1..f198a02f 100644 --- a/tests/python/accuracy/public_scope.json +++ b/tests/python/accuracy/public_scope.json @@ -256,18 +256,6 @@ } ] }, - { - "name": "otx_models/mobilenet_v3_large_hc_cf.xml", - "type": "ClassificationModel", - "test_data": [ - { - "image": "coco128/images/train2017/000000000081.jpg", - "reference": [ - "3 (equilateral): 0.596, 1 (multi a): 0.922, 2 (multi b): 0.696, 5 (triangle): 0.993, [0], [0], [0]" - ] - } - ] - }, { "name": "otx_models/classification_model_with_xai_head.xml", "type": "ClassificationModel",