diff --git a/.gitignore b/.gitignore index 1f636f2..5ae33af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ include/libsgm_config.h -build/ \ No newline at end of file +build/ +.cache +dist diff --git a/CMakeLists.txt b/CMakeLists.txt index e6a6e0a..a730f07 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,7 @@ option(ENABLE_ZED_DEMO "Build a Demo using ZED Camera" OFF) option(ENABLE_SAMPLES "Build samples" OFF) option(ENABLE_TESTS "Test library" OFF) option(LIBSGM_SHARED "Build a shared library" OFF) +option(BUILD_PYTHON_WRAPPER "Build pybind11 wrappers" OFF) option(BUILD_OPENCV_WRAPPER "Make library compatible with cv::Mat and cv::cuda::GpuMat of OpenCV" OFF) set(CUDA_ARCHS "52;61;72;75;86" CACHE STRING "List of architectures to generate device code for") @@ -16,6 +17,14 @@ ${PROJECT_SOURCE_DIR}/include/libsgm_config.h add_subdirectory(src) +if(BUILD_PYTHON_WRAPPER) + if (LIBSGM_SHARED) + add_subdirectory(pysgm) + else() + message(WARNING "Python wrappers requires LIBSGM_SHARED=ON") + endif() +endif() + if(ENABLE_SAMPLES) add_subdirectory(sample) endif() diff --git a/README.md b/README.md index e77534e..9b79e79 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ The libSGM performance obtained from benchmark sample |OpenCV|version >= 3.4.8|for samples| |OpenCV CUDA module|version >= 3.4.8|for OpenCV wrapper| |ZED SDK|version >= 3.0|for ZED sample| +|Pybind11|latest|for python bindings| ## Build Instructions ``` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..786f345 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,26 @@ +[build-system] +requires = ["scikit-build-core", "pybind11"] +build-backend = "scikit_build_core.build" + +[project] +name = "pysgm" +authors = [ + {name = "Oleksandr Slovak"}, + {name = "Christoph Liebender", email = "christoph.liebender@uni-wuerzburg.de"} +] +license = {file = "LICENSE"} +readme = "README.md" +version = "3.1.0" + +[project.urls] +Repository = "https://github.com/fixstars/libSGM.git" + +[tool.scikit-build] +cmake.args = [ + "-DLIBSGM_SHARED=ON", + "-DBUILD_PYTHON_WRAPPER=ON" +] +wheel.exclude = [ + "**/*.h", + "**/*.cmake" +] diff --git a/pysgm/CMakeLists.txt b/pysgm/CMakeLists.txt new file mode 100644 index 0000000..8123fc9 --- /dev/null +++ b/pysgm/CMakeLists.txt @@ -0,0 +1,15 @@ +find_package(Python COMPONENTS Development Interpreter) +find_package(pybind11 CONFIG) + +pybind11_add_module(pysgm MODULE "pysgm.cpp") +target_include_directories(pysgm PRIVATE "${PROJECT_SOURCE_DIR}/include") +target_link_libraries(pysgm PRIVATE sgm) + +if(BUILD_OPENCV_WRAPPER) + target_link_libraries(pysgm PRIVATE ${OpenCV_LIBS}) +endif() + +install( + TARGETS pysgm + LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}/site-packages +) diff --git a/pysgm/pysgm.cpp b/pysgm/pysgm.cpp new file mode 100644 index 0000000..06f0c41 --- /dev/null +++ b/pysgm/pysgm.cpp @@ -0,0 +1,192 @@ +#include + +#include +#include + +#ifdef BUILD_OPENCV_WRAPPER + +#include + +namespace pybind11 { +namespace detail { +template<> +struct type_caster { + public: + PYBIND11_TYPE_CASTER(cv::Mat, _("numpy.ndarray")); + + //! 1. cast numpy.ndarray to cv::Mat + bool load(handle obj, bool) { + array b = reinterpret_borrow(obj); + buffer_info info = b.request(); + + //const int ndims = (int)info.ndim; + int nh = 1; + int nw = 1; + int nc = 1; + int ndims = info.ndim; + if (ndims == 2) { + nh = info.shape[0]; + nw = info.shape[1]; + } else if (ndims == 3) { + nh = info.shape[0]; + nw = info.shape[1]; + nc = info.shape[2]; + } else { + char msg[64]; + std::sprintf(msg, "Unsupported dim %d, only support 2d, or 3-d", ndims); + throw std::logic_error(msg); + return false; + } + + int dtype; + if (info.format == format_descriptor::format()) { + dtype = CV_8UC(nc); + } else if (info.format == format_descriptor::format()) { + dtype = CV_16UC(nc); + } else if (info.format == format_descriptor::format()) { + dtype = CV_32SC(nc); + } else if (info.format == format_descriptor::format()) { + dtype = CV_32FC(nc); + } else { + throw std::logic_error("Unsupported type, only support uchar, int32, float"); + return false; + } + + value = cv::Mat(nh, nw, dtype, info.ptr); + return true; + } + + //! 2. cast cv::Mat to numpy.ndarray + static handle cast(const cv::Mat &mat, return_value_policy, handle defval) { +// UNUSED(defval); + + + std::string format = format_descriptor::format(); + size_t elemsize = sizeof(unsigned char); + int nw = mat.cols; + int nh = mat.rows; + int nc = mat.channels(); + int depth = mat.depth(); + int type = mat.type(); + int dim = (depth == type) ? 2 : 3; + + if (depth == CV_8U) { + format = format_descriptor::format(); + elemsize = sizeof(unsigned char); + } else if (depth == CV_16U) { + format = format_descriptor::format(); + elemsize = sizeof(unsigned short); + } else if (depth == CV_16S) { + format = format_descriptor::format(); + elemsize = sizeof(short); + } else if (depth == CV_32S) { + format = format_descriptor::format(); + elemsize = sizeof(int); + } else if (depth == CV_32F) { + format = format_descriptor::format(); + elemsize = sizeof(float); + } else { + throw std::logic_error("Unsupport type!"); + } + + std::vector bufferdim; + std::vector strides; + if (dim == 2) { + bufferdim = {(size_t) nh, (size_t) nw}; + strides = {elemsize * (size_t) nw, elemsize}; + } else if (dim == 3) { + bufferdim = {(size_t) nh, (size_t) nw, (size_t) nc}; + strides = {(size_t) elemsize * nw * nc, (size_t) elemsize * nc, (size_t) elemsize}; + } + return array(buffer_info(mat.data, elemsize, format, dim, bufferdim, strides)).release(); + } +}; +} +}//! end namespace pybind11::detail + +#endif // BUILD_OPENCV_WRAPPER + + +#define RW(type_name, field_name) .def_readwrite(#field_name, &type_name::field_name) +namespace py = pybind11; + +PYBIND11_MODULE(pysgm, m) { + + m.doc() = "libSGM python binding"; + + py::enum_(m, "EXECUTE_INOUT") + .value("EXECUTE_INOUT_HOST2HOST", sgm::ExecuteInOut::EXECUTE_INOUT_HOST2HOST) + .value("EXECUTE_INOUT_HOST2CUDA", sgm::ExecuteInOut::EXECUTE_INOUT_HOST2CUDA) + .value("EXECUTE_INOUT_CUDA2HOST", sgm::ExecuteInOut::EXECUTE_INOUT_CUDA2HOST) + .value("EXECUTE_INOUT_CUDA2CUDA", sgm::ExecuteInOut::EXECUTE_INOUT_CUDA2CUDA) + ; + + py::enum_(m, "PathType") + .value("SCAN_4PATH", sgm::PathType::SCAN_4PATH) + .value("SCAN_8PATH", sgm::PathType::SCAN_8PATH) + ; + + py::class_ StereoSGM(m, "StereoSGM"); + + py::class_(StereoSGM, "Parameters") + .def(py::init(), + py::arg("P1") = 10, + py::arg("P2") = 120, + py::arg("uniqueness") = 0.95f, + py::arg("subpixel") = false, + py::arg("PathType") = sgm::PathType::SCAN_8PATH, + py::arg("min_disp") = 0, + py::arg("LR_max_diff") = 1 + ) + RW(sgm::StereoSGM::Parameters, P1) + RW(sgm::StereoSGM::Parameters, P2) + RW(sgm::StereoSGM::Parameters, uniqueness) + RW(sgm::StereoSGM::Parameters, subpixel) + RW(sgm::StereoSGM::Parameters, path_type) + RW(sgm::StereoSGM::Parameters, min_disp) + RW(sgm::StereoSGM::Parameters, LR_max_diff); + + StereoSGM + .def(py::init(), + py::arg("width") = 612, + py::arg("height") = 514, + py::arg("disparity_size") = 128, + py::arg("input_depth_bits") = 8U, + py::arg("output_depth_bits") = 8U, + py::arg("inout_type") = sgm::ExecuteInOut::EXECUTE_INOUT_HOST2HOST, + py::arg("param") = sgm::StereoSGM::Parameters() + ) + .def(py::init()) + .def("execute", [](sgm::StereoSGM &w, uintptr_t left_pixels, uintptr_t right_pixels, uintptr_t dst) { + w.execute((void *)left_pixels, (void *)right_pixels, (void *)dst); + }) + .def("get_invalid_disparity", &sgm::StereoSGM::get_invalid_disparity) + ; + +#ifdef BUILD_OPENCV_WRAPPER + + py::class_ LibSGMWrapper(m, "LibSGMWrapper"); + + LibSGMWrapper + .def(py::init(), + py::arg("numDisparity") = 128, + py::arg("P1") = 10, + py::arg("P2") = 120, + py::arg("uniquenessRatio") = 0.95f, + py::arg("subpixel") = false, + py::arg("pathType") = sgm::PathType::SCAN_8PATH, + py::arg("minDisparity") = 0, + py::arg("lrMaxDiff") = 1) + .def("getInvalidDisparity", &sgm::LibSGMWrapper::getInvalidDisparity) + .def("hasSubpixel", &sgm::LibSGMWrapper::hasSubpixel) + .def("execute", [](sgm::LibSGMWrapper &w, cv::Mat &left_pixels, const cv::Mat &right_pixels) { + cv::Mat disp; + w.execute(left_pixels, right_pixels, disp); + return disp; + }); + +#endif // BUILD_OPENCV_WRAPPER + + m.def("SUBPIXEL_SCALE", []() { return sgm::StereoSGM::SUBPIXEL_SCALE; }); + m.def("SUBPIXEL_SHIFT", []() { return sgm::StereoSGM::SUBPIXEL_SHIFT; }); +} diff --git a/sample/pysgm/pysgm_test_opencv_wrap.py b/sample/pysgm/pysgm_test_opencv_wrap.py new file mode 100644 index 0000000..52b2ed9 --- /dev/null +++ b/sample/pysgm/pysgm_test_opencv_wrap.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +import sys + +import cv2 +import numpy as np + +import pysgm + +disp_size = 128 + +sgm_opencv_wrapper = pysgm.LibSGMWrapper( + numDisparity=int(disp_size), P1=int(10), P2=int(120), uniquenessRatio=np.float32(0.95), + subpixel=False, pathType=pysgm.PathType.SCAN_8PATH, minDisparity=int(0), + lrMaxDiff=int(1)) + + +I1 = cv2.imread(sys.argv[1], cv2.IMREAD_GRAYSCALE) +I2 = cv2.imread(sys.argv[2], cv2.IMREAD_GRAYSCALE) + +disp = sgm_opencv_wrapper.execute(I1, I2) + +if sgm_opencv_wrapper.hasSubpixel(): + disp = disp.astype('float') / pysgm.SUBPIXEL_SCALE() + +if sgm_opencv_wrapper.hasSubpixel(): + mask = disp == (sgm_opencv_wrapper.getInvalidDisparity() / pysgm.SUBPIXEL_SCALE()) +else: + mask = disp == sgm_opencv_wrapper.getInvalidDisparity() + +disp = (255. * disp / disp_size) +disp_color = cv2.applyColorMap(disp.astype("uint8"), cv2.COLORMAP_JET) +disp_color[mask, :] = 0 + +cv2.imshow("disp_color", disp_color) +cv2.waitKey(0) diff --git a/sample/pysgm/pysgm_test_raw.py b/sample/pysgm/pysgm_test_raw.py new file mode 100644 index 0000000..3ffce1d --- /dev/null +++ b/sample/pysgm/pysgm_test_raw.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +import sys + +import cv2 +import numpy as np + +import pysgm + +disp_size = 128 + +I1 = cv2.imread(sys.argv[1], cv2.IMREAD_GRAYSCALE) +I2 = cv2.imread(sys.argv[2], cv2.IMREAD_GRAYSCALE) + +disp = np.zeros_like(I2) + +I1_ptr, _ = I1.__array_interface__['data'] +I2_ptr, _ = I2.__array_interface__['data'] +disp_ptr, _ = disp.__array_interface__['data'] + +height, width = I1.shape + +params = pysgm.StereoSGM.Parameters(P1=int(10), P2=int(120), uniqueness=np.float32(0.95), subpixel=False, + PathType=pysgm.PathType.SCAN_8PATH, min_disp=int(0), LR_max_diff=int(1)) + +sgm = pysgm.StereoSGM(width=width, height=height, disparity_size=int(disp_size), input_depth_bits=int(8), + output_depth_bits=int(8), inout_type=pysgm.EXECUTE_INOUT.EXECUTE_INOUT_HOST2HOST, + param=params) + +sgm.execute(I1_ptr, I2_ptr, disp_ptr) + +mask = disp == np.int16(sgm.get_invalid_disparity()) +disp = (255. * disp / disp_size) + +disp_color = cv2.applyColorMap(disp.astype("uint8"), cv2.COLORMAP_JET) +disp_color[mask] = 0 + +cv2.imshow("disp_color", disp_color) +cv2.waitKey(0)