From 44ddae4c135ac7745bac0191007bdd49b52265e3 Mon Sep 17 00:00:00 2001 From: Martin Valgur Date: Fri, 20 Mar 2020 17:40:05 +0200 Subject: [PATCH] add Python bindings installable with setup.py Based on https://github.com/pybind/cmake_example --- .gitignore | 102 +++++++++++++++++++ CMakeLists.txt | 12 ++- include/surface_normal.h | 23 +++++ setup.py | 65 ++++++++++++ src/python.cpp | 42 ++++++++ surface_normal.cpp => src/surface_normal.cpp | 28 +---- surface_normal.h | 25 ----- 7 files changed, 244 insertions(+), 53 deletions(-) create mode 100644 include/surface_normal.h create mode 100644 setup.py create mode 100644 src/python.cpp rename surface_normal.cpp => src/surface_normal.cpp (78%) delete mode 100644 surface_normal.h diff --git a/.gitignore b/.gitignore index df76acb..af52e44 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,105 @@ fabric.properties *.out *.app + +# Created by https://www.gitignore.io/api/python +# Edit at https://www.gitignore.io/?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + diff --git a/CMakeLists.txt b/CMakeLists.txt index 28629f2..dd6e98c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,9 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.11) project(surface_normal CXX) find_package(OpenCV REQUIRED) +include_directories(${OpenCV_INCLUDE_DIRS} include) +link_libraries(${OpenCV_LIBS}) set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -9,7 +11,7 @@ set(CMAKE_CXX_EXTENSIONS OFF) add_compile_options(-O3 -Wall -Wpedantic -Wno-sign-compare) -include_directories(${OpenCV_INCLUDE_DIRS}) -link_libraries(${OpenCV_LIBS}) - -add_executable(${PROJECT_NAME} surface_normal.cpp surface_normal.h) +include(FetchContent) +FetchContent_Declare(pybind11 GIT_REPOSITORY https://github.com/pybind/pybind11) +FetchContent_MakeAvailable(pybind11) +pybind11_add_module(${PROJECT_NAME} src/python.cpp src/surface_normal.cpp) diff --git a/include/surface_normal.h b/include/surface_normal.h new file mode 100644 index 0000000..f34f4c1 --- /dev/null +++ b/include/surface_normal.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include + +using Plane = cv::Vec4f; + +struct CameraIntrinsics { + float f; + float cx; + float cy; +}; + +cv::Mat3f normals_from_depth(const cv::Mat &depth, CameraIntrinsics intrinsics, int window_size, + float rel_dist_threshold); + +cv::Mat3b normals_to_rgb(const cv::Mat3f &normals); + +cv::Mat1f get_surrounding_points(const cv::Mat &depth, int i, int j, CameraIntrinsics intrinsics, + size_t window_size, float threshold); + +cv::Vec3f fit_plane(const cv::Mat &points); diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..211c027 --- /dev/null +++ b/setup.py @@ -0,0 +1,65 @@ +import os +import re +import sys +import platform +import subprocess + +from setuptools import setup, Extension +from setuptools.command.build_ext import build_ext +from distutils.version import LooseVersion + + +class CMakeExtension(Extension): + def __init__(self, name, sourcedir=''): + Extension.__init__(self, name, sources=[]) + self.sourcedir = os.path.abspath(sourcedir) + + +class CMakeBuild(build_ext): + def run(self): + try: + out = subprocess.check_output(['cmake', '--version']) + except OSError: + raise RuntimeError("CMake must be installed to build the following extensions: " + + ", ".join(e.name for e in self.extensions)) + + if platform.system() == "Windows": + cmake_version = LooseVersion(re.search(r'version\s*([\d.]+)', out.decode()).group(1)) + if cmake_version < '3.1.0': + raise RuntimeError("CMake >= 3.1.0 is required on Windows") + + for ext in self.extensions: + self.build_extension(ext) + + def build_extension(self, ext): + extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name))) + cmake_args = ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=' + extdir, + '-DPYTHON_EXECUTABLE=' + sys.executable] + + cfg = 'Debug' if self.debug else 'Release' + build_args = ['--config', cfg] + + if platform.system() == "Windows": + cmake_args += ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}'.format(cfg.upper(), extdir)] + if sys.maxsize > 2 ** 32: + cmake_args += ['-A', 'x64'] + build_args += ['--', '/m'] + else: + cmake_args += ['-DCMAKE_BUILD_TYPE=' + cfg] + build_args += ['--', '-j2'] + + env = os.environ.copy() + env['CXXFLAGS'] = '{} -DVERSION_INFO=\\"{}\\"'.format(env.get('CXXFLAGS', ''), + self.distribution.get_version()) + if not os.path.exists(self.build_temp): + os.makedirs(self.build_temp) + subprocess.check_call(['cmake', ext.sourcedir] + cmake_args, cwd=self.build_temp, env=env) + subprocess.check_call(['cmake', '--build', '.'] + build_args, cwd=self.build_temp) + + +setup( + name='surface_normal', + ext_modules=[CMakeExtension('cmake_example')], + cmdclass=dict(build_ext=CMakeBuild), + zip_safe=False, +) diff --git a/src/python.cpp b/src/python.cpp new file mode 100644 index 0000000..847f83b --- /dev/null +++ b/src/python.cpp @@ -0,0 +1,42 @@ +#include +#include + +#include +#include +#include + +#include + +#include "surface_normal.h" + +namespace py = pybind11; + +using CameraIntrinsicsTuple = std::tuple; + +void normals_from_depth_wrapper(const std::string &in_img_path, const std::string &out_img_path, + CameraIntrinsicsTuple intrinsics_tuple, int window_size, + float rel_dist_threshold = 0.1) { + CameraIntrinsics intrinsics{}; + intrinsics.f = std::get<0>(intrinsics_tuple); + intrinsics.cx = std::get<1>(intrinsics_tuple); + intrinsics.cy = std::get<2>(intrinsics_tuple); + cv::Mat depth = cv::imread(in_img_path, cv::IMREAD_ANYDEPTH | cv::IMREAD_GRAYSCALE); + depth.convertTo(depth, CV_32F); + cv::Mat3f normals = normals_from_depth(depth, intrinsics, window_size, rel_dist_threshold); + cv::Mat3b normals_rgb = normals_to_rgb(normals); + cvtColor(normals_rgb, normals_rgb, cv::COLOR_RGB2BGR); + imwrite(out_img_path, normals_rgb); +} + +PYBIND11_MODULE(surface_normal, m) { + m.doc() = ""; + m.def("normals_from_depth", &normals_from_depth_wrapper, py::arg("in_img_path"), + py::arg("out_img_path"), py::arg("intrinsics"), py::arg("window_size") = 15, + py::arg("rel_dist_threshold") = 0.1); + +#ifdef VERSION_INFO + m.attr("__version__") = VERSION_INFO; +#else + m.attr("__version__") = "dev"; +#endif +} \ No newline at end of file diff --git a/surface_normal.cpp b/src/surface_normal.cpp similarity index 78% rename from surface_normal.cpp rename to src/surface_normal.cpp index b5eb96b..485ca57 100644 --- a/surface_normal.cpp +++ b/src/surface_normal.cpp @@ -1,12 +1,12 @@ #include #include -#include -#include #include "surface_normal.h" -Mat1f get_surrounding_points(const Mat &depth, int i, int j, CameraParams intrinsics, +using namespace cv; + +Mat1f get_surrounding_points(const Mat &depth, int i, int j, CameraIntrinsics intrinsics, size_t window_size, float threshold) { float f_inv = 1.f / intrinsics.f; float cx = intrinsics.cx; @@ -58,7 +58,7 @@ Vec3f fit_plane(const Mat &points) { return normal; } -Mat3f normals_from_depth(const Mat &depth, CameraParams intrinsics, int window_size, +Mat3f normals_from_depth(const Mat &depth, CameraIntrinsics intrinsics, int window_size, float rel_dist_threshold) { Mat3f normals = Mat::zeros(depth.size(), CV_32FC3); for (int i = 0; i < depth.rows; i++) { @@ -87,7 +87,7 @@ Mat3f normals_from_depth(const Mat &depth, CameraParams intrinsics, int window_s constexpr uint8_t f2b(float x) { return static_cast(127.5 * (1 - x)); } -Mat3b normals_to_rgb(const Mat &normals) { +Mat3b normals_to_rgb(const Mat3f &normals) { Mat3b res = Mat::zeros(normals.size(), CV_8UC3); for (int i = 0; i < normals.rows; i++) { for (int j = 0; j < normals.cols; j++) { @@ -101,21 +101,3 @@ Mat3b normals_to_rgb(const Mat &normals) { } return res; } - -int main() { - CameraParams intrinsics{}; - intrinsics.f = 721.5377; - intrinsics.cx = 596.5593; - intrinsics.cy = 149.854; - int window_size = 15; - float rel_dist_threshold = 0.1; - - Mat depth = cv::imread("gt.png", cv::IMREAD_ANYDEPTH | cv::IMREAD_GRAYSCALE); - depth.convertTo(depth, CV_32F); - Mat3f normals = normals_from_depth(depth, intrinsics, window_size, rel_dist_threshold); - Mat3b normals_rgb = normals_to_rgb(normals); - cvtColor(normals_rgb, normals_rgb, COLOR_BGR2RGB); - cv::imwrite("gt_out.png", normals_rgb); - - return 0; -} diff --git a/surface_normal.h b/surface_normal.h deleted file mode 100644 index 7f505c4..0000000 --- a/surface_normal.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include - -#include - -using namespace cv; - -using Plane = cv::Vec4f; - -struct CameraParams { - float f; - float cx; - float cy; -}; - -Mat3f normals_from_depth(const Mat &depth, CameraParams intrinsics, int window_size, - float rel_dist_threshold); - -Mat3b normals_to_rgb(const Mat &normals); - -Mat1f get_surrounding_points(const Mat &depth, int i, int j, CameraParams intrinsics, - size_t window_size, float threshold); - -Vec3f fit_plane(const Mat &points);