Skip to content

Commit e97b12c

Browse files
authoredJan 15, 2025
Add initial version of python bindings (#252)
* Add initial version of python bindings * Update python deps * Update linkage flags for GCC * Classification bindings without config * Wrap batch inference * Wrap load() to enable setting correct number of infer requests * Add a simple any map converter * Add license * Wrap tensor results * Update bindings directory structure * Add cmake option to anable/disable bindings * Fix linter * Update copyrights * Update CI scripts * Fix more issues in CI scripts * Add missing requirements * Dismiss pre-defined gha-specific ython paath * Force cmake to use python from venv * Workaround missing nanobind with global package installation * Try python 3.10 * Try to reduce the amount of warning in tests * Limit ubuntu version in cpp build * Ignore warnings on cpp pre-commit * Update installation script * Update cpp accuracy build settings
1 parent ee40e7a commit e97b12c

12 files changed

+239
-14
lines changed
 

‎.github/workflows/test_accuracy.yml

+7-5
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ concurrency:
1010
cancel-in-progress: true
1111
jobs:
1212
test_accuracy:
13-
runs-on: ubuntu-latest
13+
runs-on: ubuntu-22.04
1414
steps:
1515
- uses: actions/checkout@v3
1616
- uses: actions/setup-python@v4
1717
with:
18-
python-version: 3.9
18+
python-version: "3.10"
1919
cache: pip
2020
- name: Create and start a virtual environment
2121
run: |
@@ -25,7 +25,7 @@ jobs:
2525
run: |
2626
source venv/bin/activate
2727
python -m pip install --upgrade pip
28-
pip install model_api/python/[tests] --extra-index-url https://download.pytorch.org/whl/cpu
28+
pip install model_api/python/[tests,build] --extra-index-url https://download.pytorch.org/whl/cpu
2929
- name: Prepare test data
3030
run: |
3131
source venv/bin/activate
@@ -35,13 +35,15 @@ jobs:
3535
source venv/bin/activate
3636
pytest --data=./data tests/python/accuracy/test_accuracy.py
3737
DATA=data pytest --data=./data tests/python/accuracy/test_YOLOv8.py
38-
- name: Install CPP ependencies
38+
- name: Install CPP dependencies
3939
run: |
4040
sudo bash model_api/cpp/install_dependencies.sh
4141
- name: Build CPP Test
4242
run: |
43+
pip install nanobind==2.4.0
44+
pip install typing_extensions==4.12.2
4345
mkdir build && cd build
44-
cmake ../tests/cpp/accuracy/ -DCMAKE_CXX_FLAGS=-Werror
46+
cmake ../tests/cpp/accuracy/
4547
make -j
4648
- name: Run CPP Test
4749
run: |

‎.github/workflows/test_precommit.yml

+8-6
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,12 @@ jobs:
4848
# missingInclude: cppcheck can't find stl, openvino, opencv
4949
other_options: --suppress=missingInclude -Imodel_api/cpp/models/include -Imodel_api/cpp/utils/include -Imodel_api/cpp/pipelines/include --check-config
5050
CPP-Precommit:
51-
runs-on: ubuntu-latest
51+
runs-on: ubuntu-22.04
5252
steps:
5353
- uses: actions/checkout@v3
5454
- uses: actions/setup-python@v4
5555
with:
56-
python-version: 3.9
56+
python-version: "3.10"
5757
cache: pip
5858
- name: Create and start a virtual environment
5959
run: |
@@ -63,7 +63,7 @@ jobs:
6363
run: |
6464
source venv/bin/activate
6565
python -m pip install --upgrade pip
66-
pip install model_api/python/[tests] --extra-index-url https://download.pytorch.org/whl/cpu
66+
pip install model_api/python/[tests,build] --extra-index-url https://download.pytorch.org/whl/cpu
6767
6868
sudo bash model_api/cpp/install_dependencies.sh
6969
- name: Prepare test data
@@ -73,7 +73,9 @@ jobs:
7373
- name: Build
7474
run: |
7575
mkdir build && cd build
76-
cmake ../tests/cpp/precommit/ -DCMAKE_CXX_FLAGS=-Werror
76+
pip install nanobind==2.4.0
77+
pip install typing_extensions==4.12.2
78+
cmake ../tests/cpp/precommit/
7779
cmake --build . -j $((`nproc`*2+2))
7880
- name: Run test
7981
run: |
@@ -96,7 +98,7 @@ jobs:
9698
run: |
9799
source venv/Scripts/activate
98100
python -m pip install --upgrade pip
99-
pip install model_api/python/[tests] --extra-index-url https://download.pytorch.org/whl/cpu
101+
pip install model_api/python/[tests,build] --extra-index-url https://download.pytorch.org/whl/cpu
100102
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
101103
unzip w_openvino_toolkit_windows.zip
102104
rm w_openvino_toolkit_windows.zip
@@ -112,7 +114,7 @@ jobs:
112114
shell: bash
113115
run: |
114116
mkdir build && cd build
115-
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
117+
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
116118
cmake --build . --config Release -j $((`nproc`*2+2))
117119
- name: Run sync sample
118120
shell: cmd

‎model_api/cpp/CMakeLists.txt

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
# Copyright (C) 2018-2024 Intel Corporation
1+
# Copyright (C) 2018-2025 Intel Corporation
22
# SPDX-License-Identifier: Apache-2.0
33
#
44

55
cmake_minimum_required(VERSION 3.26)
66

7+
option(ENABLE_PY_BINDINGS "Enables building python bindings package" ON)
8+
79
# Multi config generators such as Visual Studio ignore CMAKE_BUILD_TYPE. Multi config generators are configured with
810
# CMAKE_CONFIGURATION_TYPES, but limiting options in it completely removes such build options
911
get_property(GENERATOR_IS_MULTI_CONFIG_VAR GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
@@ -81,6 +83,11 @@ if(MSVC)
8183
/EHsc) # Enable standard C++ stack unwinding, assume functions with extern "C" never throw
8284
elseif(CMAKE_CXX_COMPILER_ID MATCHES "^GNU|(Apple)?Clang$")
8385
target_compile_options(model_api PRIVATE -Wall -Wextra -Wpedantic)
86+
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC")
87+
endif()
88+
89+
if (ENABLE_PY_BINDINGS)
90+
add_subdirectory(py_bindings)
8491
endif()
8592

8693
include(GenerateExportHeader)

‎model_api/cpp/cmake/model_apiConfig.cmake

+2
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ find_dependency(OpenCV COMPONENTS core imgproc)
33
find_dependency(OpenVINO COMPONENTS Runtime)
44

55
include("${CMAKE_CURRENT_LIST_DIR}/model_apiTargets.cmake")
6+
7+
check_required_components()
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright (C) 2025 Intel Corporation
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
5+
set(DEV_MODULE Development.Module)
6+
7+
find_package(Python COMPONENTS Interpreter ${DEV_MODULE} REQUIRED)
8+
9+
execute_process(
10+
COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir
11+
OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE nanobind_ROOT)
12+
find_package(nanobind CONFIG REQUIRED)
13+
14+
15+
file(GLOB BINDINGS_SOURCES ./*.cpp)
16+
file(GLOB BINDINGS_HEADERS ./*.hpp)
17+
18+
nanobind_add_module(py_model_api NB_STATIC STABLE_ABI LTO ${BINDINGS_SOURCES} ${BINDINGS_HEADERS})
19+
20+
target_link_libraries(py_model_api PRIVATE model_api)
21+
22+
nanobind_add_stub(
23+
py_model_api_stub
24+
MODULE py_model_api
25+
OUTPUT py_model_api.pyi
26+
PYTHON_PATH $<TARGET_FILE_DIR:py_model_api>
27+
DEPENDS py_model_api
28+
)

‎model_api/cpp/py_bindings/py_base.cpp

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright (C) 2025 Intel Corporation
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
#include <nanobind/nanobind.h>
7+
#include <nanobind/stl/string.h>
8+
9+
#include <openvino/openvino.hpp>
10+
11+
#include "models/image_model.h"
12+
#include "models/results.h"
13+
14+
namespace nb = nanobind;
15+
16+
void init_base_modules(nb::module_& m) {
17+
nb::class_<ResultBase>(m, "ResultBase").def(nb::init<>());
18+
19+
nb::class_<ModelBase>(m, "ModelBase")
20+
.def("load", [](ModelBase& self, const std::string& device, size_t num_infer_requests) {
21+
auto core = ov::Core();
22+
self.load(core, device, num_infer_requests);
23+
});
24+
25+
nb::class_<ImageModel, ModelBase>(m, "ImageModel");
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright (C) 2025 Intel Corporation
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
#include <nanobind/ndarray.h>
7+
#include <nanobind/operators.h>
8+
#include <nanobind/stl/map.h>
9+
#include <nanobind/stl/string.h>
10+
#include <nanobind/stl/unique_ptr.h>
11+
#include <nanobind/stl/vector.h>
12+
13+
#include "models/classification_model.h"
14+
#include "models/results.h"
15+
#include "py_utils.hpp"
16+
17+
namespace pyutils = vision::nanobind::utils;
18+
19+
void init_classification(nb::module_& m) {
20+
nb::class_<ClassificationResult::Classification>(m, "Classification")
21+
.def(nb::init<unsigned int, const std::string, float>())
22+
.def_rw("id", &ClassificationResult::Classification::id)
23+
.def_rw("label", &ClassificationResult::Classification::label)
24+
.def_rw("score", &ClassificationResult::Classification::score);
25+
26+
nb::class_<ClassificationResult, ResultBase>(m, "ClassificationResult")
27+
.def(nb::init<>())
28+
.def_ro("topLabels", &ClassificationResult::topLabels)
29+
.def("__repr__", &ClassificationResult::operator std::string)
30+
.def_prop_ro(
31+
"feature_vector",
32+
[](ClassificationResult& r) {
33+
if (!r.feature_vector) {
34+
return nb::ndarray<float, nb::numpy, nb::c_contig>();
35+
}
36+
37+
return nb::ndarray<float, nb::numpy, nb::c_contig>(r.feature_vector.data(),
38+
r.feature_vector.get_shape().size(),
39+
r.feature_vector.get_shape().data());
40+
},
41+
nb::rv_policy::reference_internal)
42+
.def_prop_ro(
43+
"saliency_map",
44+
[](ClassificationResult& r) {
45+
if (!r.saliency_map) {
46+
return nb::ndarray<float, nb::numpy, nb::c_contig>();
47+
}
48+
49+
return nb::ndarray<float, nb::numpy, nb::c_contig>(r.saliency_map.data(),
50+
r.saliency_map.get_shape().size(),
51+
r.saliency_map.get_shape().data());
52+
},
53+
nb::rv_policy::reference_internal);
54+
55+
nb::class_<ClassificationModel, ImageModel>(m, "ClassificationModel")
56+
.def_static(
57+
"create_model",
58+
[](const std::string& model_path,
59+
const std::map<std::string, nb::object>& configuration,
60+
bool preload,
61+
const std::string& device) {
62+
auto ov_any_config = ov::AnyMap();
63+
for (const auto& item : configuration) {
64+
ov_any_config[item.first] = pyutils::py_object_to_any(item.second, item.first);
65+
}
66+
67+
return ClassificationModel::create_model(model_path, ov_any_config, preload, device);
68+
},
69+
nb::arg("model_path"),
70+
nb::arg("configuration") = ov::AnyMap({}),
71+
nb::arg("preload") = true,
72+
nb::arg("device") = "AUTO")
73+
74+
.def("__call__",
75+
[](ClassificationModel& self, const nb::ndarray<>& input) {
76+
return self.infer(pyutils::wrap_np_mat(input));
77+
})
78+
.def("infer_batch", [](ClassificationModel& self, const std::vector<nb::ndarray<>> inputs) {
79+
std::vector<ImageInputData> input_mats;
80+
input_mats.reserve(inputs.size());
81+
82+
for (const auto& input : inputs) {
83+
input_mats.push_back(pyutils::wrap_np_mat(input));
84+
}
85+
86+
return self.inferBatch(input_mats);
87+
});
88+
}
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (C) 2025 Intel Corporation
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
#include "py_utils.hpp"
7+
8+
namespace vision::nanobind::utils {
9+
10+
cv::Mat wrap_np_mat(const nb::ndarray<>& input) {
11+
if (input.ndim() != 3 || input.shape(2) != 3 || input.dtype() != nb::dtype<uint8_t>()) {
12+
throw std::runtime_error("Input image should have HWC_8U layout");
13+
}
14+
15+
int height = input.shape(0);
16+
int width = input.shape(1);
17+
18+
return cv::Mat(height, width, CV_8UC3, input.data());
19+
}
20+
21+
ov::Any py_object_to_any(const nb::object& py_obj, const std::string& property_name) {
22+
if (nb::isinstance<nb::str>(py_obj)) {
23+
return ov::Any(std::string(static_cast<nb::str>(py_obj).c_str()));
24+
} else if (nb::isinstance<nb::float_>(py_obj)) {
25+
return ov::Any(static_cast<double>(static_cast<nb::float_>(py_obj)));
26+
} else if (nb::isinstance<nb::int_>(py_obj)) {
27+
return ov::Any(static_cast<int>(static_cast<nb::int_>(py_obj)));
28+
} else {
29+
OPENVINO_THROW("Property \"" + property_name + "\" has unsupported type.");
30+
}
31+
}
32+
33+
} // namespace vision::nanobind::utils
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright (C) 2025 Intel Corporation
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
#pragma once
7+
#include <nanobind/ndarray.h>
8+
9+
#include <opencv2/core/core.hpp>
10+
#include <openvino/openvino.hpp>
11+
12+
namespace nb = nanobind;
13+
14+
namespace vision::nanobind::utils {
15+
cv::Mat wrap_np_mat(const nb::ndarray<>& input);
16+
ov::Any py_object_to_any(const nb::object& py_obj, const std::string& property_name);
17+
} // namespace vision::nanobind::utils
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright (C) 2025 Intel Corporation
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
#include <nanobind/nanobind.h>
7+
8+
namespace nb = nanobind;
9+
10+
void init_classification(nb::module_& m);
11+
void init_base_modules(nb::module_& m);
12+
13+
NB_MODULE(py_model_api, m) {
14+
m.doc() = "Nanobind binding for OpenVINO Vision API library";
15+
init_base_modules(m);
16+
init_classification(m);
17+
}

‎model_api/python/pyproject.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ docs = [
5959
"breathe",
6060
"graphviz",
6161
]
62-
full = ["openvino_model_api[dependencies, ovms, tests, docs]"]
62+
build = [
63+
"nanobind==2.4.0",
64+
]
65+
full = ["openvino_model_api[dependencies, ovms, tests, docs, build]"]
6366

6467
[project.urls]
6568
Homepage = "https://github.com/openvinotoolkit/model_api"

‎tests/cpp/accuracy/CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ if(MSVC)
3030
endif()
3131

3232
if(CMAKE_CXX_COMPILER_ID MATCHES "^GNU|(Apple)?Clang$")
33-
add_compile_options(-Wall -Wextra -Wpedantic)
33+
add_compile_options(-Wall -Wextra)
3434
endif()
3535

3636
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64.*|aarch64.*|AARCH64.*)")

0 commit comments

Comments
 (0)