diff --git a/SDK/CPP/public/include/voltu/defines.hpp b/SDK/CPP/public/include/voltu/defines.hpp index 19f2416f2812cc41b69b6c2f73a7a7f3bd4f7588..513190c63f792199e063c857f48def876794660a 100644 --- a/SDK/CPP/public/include/voltu/defines.hpp +++ b/SDK/CPP/public/include/voltu/defines.hpp @@ -1,5 +1,5 @@ /** - * @file system.hpp + * @file defines.hpp * @copyright Copyright (c) 2020 Sebastian Hahta, MIT License * @author Sebastian Hahta */ @@ -20,3 +20,15 @@ /// Lifetime of the return value is tied to the lifetime of a parent object #define PY_RV_LIFETIME_PARENT #endif + +#ifndef PY_SINGLETON +/// Singleton instance, members exported to module. Requires creating the +/// instance in PyModule constructor. +#define PY_SINGLETON +#endif + +#ifndef PY_SINGLETON_OBJECT +/// Export as singleton instance instead of exporting members to module +#define PY_SINGLETON_OBJECT +#endif + diff --git a/SDK/CPP/public/include/voltu/initialise.hpp b/SDK/CPP/public/include/voltu/initialise.hpp index 4a6e5b3ffc2c2326b5934d2577d0458605ddc51c..5d34a1e691797b8c4922aa274775d7f84ef401ac 100644 --- a/SDK/CPP/public/include/voltu/initialise.hpp +++ b/SDK/CPP/public/include/voltu/initialise.hpp @@ -13,34 +13,34 @@ namespace voltu { /** * @brief Get core VolTu instance. - * + * * This method returns a smart pointer to a singleton VolTu runtime * instance and must be the first VolTu call. On any given machine it is * only sensible and possible to have one runtime instance of VolTu due to * its use of hardware devices. Multiple real instances are not possible. - * + * * @code * int main(int argc, char** argv) { * auto vtu = voltu::instance(); - * + * * vtu->open("device:camera"); * ... * } * @endcode - * + * * @note * This method must only be called once. - * + * * @throw voltu::exceptions::LibraryLoadFailed * If runtime not found or is invalid. * * @throw voltu::exceptions::RuntimeVersionMismatch * If major or minor version does not match the SDK headers. - * + * * @throw voltu::exceptions::RuntimeAlreadyInUse * If a runtime instance is in use by another application. - * + * * @return Singleton VolTu runtime instance. */ - PY_API std::shared_ptr<voltu::System> instance(); + std::shared_ptr<voltu::System> instance(); } diff --git a/SDK/CPP/public/include/voltu/system.hpp b/SDK/CPP/public/include/voltu/system.hpp index 6a7e4cd96387a6e50b4933dad6892eb52104755d..0ac85870c0579ac953d40430d60507026f3bf94b 100644 --- a/SDK/CPP/public/include/voltu/system.hpp +++ b/SDK/CPP/public/include/voltu/system.hpp @@ -29,50 +29,51 @@ struct Version /** * @brief Singleton Voltu system instance. - * + * * Provides access to the key components such as opening streams or files and * creating virtual cameras. Use `voltu::instance()` to obtain the object. All - * object instances in VolTu are managed by shared smart pointers. + * object instances in VolTu are managed by shared smart pointers. Python API + * automatically creates instance available as voltu.System. */ -class System +PY_SINGLETON_OBJECT class System { public: virtual ~System() = default; - + /** * @brief Get the runtime version information. - * + * * This method gets the VolTu version of the runtime shared library, which * may not be the same as the version of the SDK here. - * + * * @see voltu.hpp - * + * * @return Always returns semantic versioning structure. */ virtual voltu::Version getVersion() const = 0; /** * @brief Make a virtual room or composite room. - * + * * @return A new virtual room instance. */ PY_API virtual voltu::RoomPtr createRoom() = 0; /** * @brief Create a virtual observer. - * + * * An observer renderers virtual camera views, audio and other data from * submitted framesets. It is possible and recommended that a single * observer instance be used to renderer multiple different views, rather * than creating lots of observers. This saves memory resources. - * + * * @return A new observer instance. */ PY_API virtual voltu::ObserverPtr createObserver() = 0; /** * @brief Open a file, device or network stream using a URI. - * + * * All data sources in VolTu are represented by Universal Resource * Identifiers (URIs), with some non-standard additions. A few examples * are: @@ -83,11 +84,11 @@ public: * * `./file.ftl` * * `device:camera` * * `device:screen` - * + * * Note that returning from this call does not guarantee that the source * is fully open and operational, this depends on network handshakes or * file processing that occurs asynchronously. - * + * * @throw voltu::exceptions::BadSourceURI If an unrecognised URI is given. * @return A feed management object for the data source. */ @@ -95,14 +96,14 @@ public: /** * @brief Get a list of all available rooms. - * + * * A room is a 3D captured physical space, or a combination of such spaces, * and is represented by a unique identifier within the local system. This * method obtains a list of all available rooms from all sources. To obtain * rooms, either use `open` or `createRoom`. - * + * * @return A list of room ids, which can be empty. - * + * * @see getRoom * @see open */ @@ -110,13 +111,13 @@ public: /** * @brief Get a room instance from identifier. - * + * * A room instance enables access to all data frames associated with that * room, including image data. Calling `getRoom` with the same ID * multiple times will return different smart pointers to room instances * but provides access to the same data regardless and is valid. An invalid * room ID will throw an exception. - * + * * @throw voltu::exceptions::InvalidRoomId If the ID does not exist. * @return Room instance or accessing room data. */ @@ -127,14 +128,14 @@ public: /** * @brief Make an empty operator pipeline for frame processing. - * + * * A pipeline allows a sequence of processing operations to be applied to * a data frame. These operations include stereo correspondence, fusion, * data evaluation and various image processing operations. Only some * of these operators are exposed in the SDK. Once a pipeline instance * is obtained, you can add specific operators to it, configure them and * then submit frames from processing. - * + * * @return A unique pipeline instance. */ PY_API virtual voltu::PipelinePtr createPipeline() = 0; diff --git a/SDK/CPP/public/python/CMakeLists.txt b/SDK/CPP/public/python/CMakeLists.txt index 473b0626c2190b4602b485b855fa3447ca3839bc..27a2a8bdf953f51e343883412aebbbdbf6a14c0b 100644 --- a/SDK/CPP/public/python/CMakeLists.txt +++ b/SDK/CPP/public/python/CMakeLists.txt @@ -18,7 +18,7 @@ add_custom_command( ${CMAKE_CURRENT_SOURCE_DIR}/../include ${SDK_AUTO_HEADERS} - DEPENDS voltu_sdk + DEPENDS voltu_sdk gen.py ) pybind11_add_module(voltu_sdk_py MODULE diff --git a/SDK/CPP/public/python/README.md b/SDK/CPP/public/python/README.md index a5cd30d6d96bab1733841acf5bca73947d6655f0..52773c98c80059795aed630660c4c882de90c671 100644 --- a/SDK/CPP/public/python/README.md +++ b/SDK/CPP/public/python/README.md @@ -1,7 +1,8 @@ # Python API generator Dependencies - * python3-ply (build only) + * python3-dev + * python3-ply Build system uses Pybind11 to generate a python module. Most of the bindings are automatically generated by gen.py script which is automatically called by CMake @@ -14,11 +15,14 @@ build. Several (empty) macros are used in headers to annoate Python API details. * PY_API function/method is to be included in Python API * PY_NO_SHARED_PTR shared_ptr<> is not used with instances of this class. - https://pybind11.readthedocs.io/en/latest/advanced/smart_ptrs.html?#std-shared-ptr + See [pybind11 documentation](https://pybind11.readthedocs.io/en/latest/advanced/smart_ptrs.html?#std-shared-ptr) + for techncal details. * PY_RV_LIFETIME_PARENT lifetime of method's return valued is tied to - lifetime of parent objects (this). (return_value_policy::reference_internal + lifetime of parent objects (this). ( + [return_value_policy::reference_internal](https://pybind11.readthedocs.io/en/latest/advanced/functions.html#return-value-policies) is set for this method) - https://pybind11.readthedocs.io/en/latest/advanced/functions.html#return-value-policies + * PY_SINGLETON Singleton class, methods are exported to to module scope. + * PY_SINGLETON_OBJECT Singleton instance is accessible as module attribute. ## Notes: * Binding to default constructor is generated for structs. Class constructors @@ -28,6 +32,10 @@ build. Several (empty) macros are used in headers to annoate Python API details. * Default arguments are supported (extracted from header). * Public class properties are available in python, read-only if const, otherwise read write. + * Singletons have to be created in PyModule constructor and be available + as class members. + * Exceptions have to be included manually in module.cpp + * C++ preprocessor is not used ## Not supported (yet) in automatic binding generation: * Nested classes @@ -35,3 +43,4 @@ build. Several (empty) macros are used in headers to annoate Python API details. * Constructors * Automatic documentation (Doxygen) * Generator does not verify that shared_ptr<> is used consistently/correctly + * Member variables of singleton classes diff --git a/SDK/CPP/public/python/automatic_bindings.cpp.in b/SDK/CPP/public/python/automatic_bindings.cpp.in deleted file mode 100644 index 291de8913a266dd029a6e7ccbc4e7f3796ed82d3..0000000000000000000000000000000000000000 --- a/SDK/CPP/public/python/automatic_bindings.cpp.in +++ /dev/null @@ -1,15 +0,0 @@ -{includes} - -#include <pybind11/pybind11.h> -#include <pybind11/stl.h> -#include <pybind11/eigen.h> - -#include "types/image.hpp" - -namespace py = pybind11; - -using namespace voltu; - -void py_automatic_bindings(py::module& m) {{ - {code} -}} diff --git a/SDK/CPP/public/python/examples/example1.py b/SDK/CPP/public/python/examples/example1.py index 1cc1a26681470b9212662da2959224daf35bf89c..e124328892a0164790e029c0f447f8df58d0ba60 100644 --- a/SDK/CPP/public/python/examples/example1.py +++ b/SDK/CPP/public/python/examples/example1.py @@ -3,7 +3,6 @@ import cv2 import voltu -import time import sys import os @@ -11,23 +10,15 @@ if len(sys.argv) != 2: print("%s filename" % os.path.basename(__file__)) exit(1) -#if not os.path.exists(sys.argv[1]): -# print("can't find %s" % sys.argv[1]) -# exit(1) - -api = voltu.instance() +api = voltu.System api.open(sys.argv[1]) room = api.getRoom(0) while True: - try: - room.waitNextFrame(1000) + if room.waitNextFrame(1000): frames = room.getFrame().getImageSet(voltu.Channel.kColour) im = frames[0].getHost() cv2.imshow("im", im) - if cv2.waitKey(10) == 27: - break - - except Exception as e: - print(e) + if cv2.waitKey(10) == 27: + break diff --git a/SDK/CPP/public/python/examples/example2.py b/SDK/CPP/public/python/examples/example2.py index 629fec948de0f93c9d94184e8a79033afb211896..8b8bfcf777d558ef8e267ce4a6685af564d15b22 100644 --- a/SDK/CPP/public/python/examples/example2.py +++ b/SDK/CPP/public/python/examples/example2.py @@ -3,7 +3,6 @@ import cv2 import voltu -import time import sys import os @@ -11,25 +10,17 @@ if len(sys.argv) != 2: print("%s filename" % os.path.basename(__file__)) exit(1) -#if not os.path.exists(sys.argv[1]): -# print("can't find %s" % sys.argv[1]) -# exit(1) - -api = voltu.instance() +api = voltu.System api.open(sys.argv[1]) room = api.getRoom(0) cam = api.createCamera() while True: - try: - room.waitNextFrame(1000) + if room.waitNextFrame(1000): cam.submit(room.getFrame()) frames = cam.getFrame().getImageSet(voltu.Channel.kColour) im = frames[0].getHost() cv2.imshow("im", im) - if cv2.waitKey(10) == 27: - break - - except Exception as e: - print(e) + if cv2.waitKey(10) == 27: + break diff --git a/SDK/CPP/public/python/gen.py b/SDK/CPP/public/python/gen.py index 00e8252c7efea6975acc331c5076453aa5c3695b..649b45b6f69f731ff38c9c0a833b7c0fd8b27474 100755 --- a/SDK/CPP/public/python/gen.py +++ b/SDK/CPP/public/python/gen.py @@ -1,5 +1,30 @@ #!/usr/bin/env python3 +""" +Python binding generator. Creates pybind11 bindings for given headers. +See README.md for details. +""" + +template = """ /* This file was automatically generated by gen.py. */ + +{includes} + +#include <pybind11/pybind11.h> +#include <pybind11/stl.h> +#include <pybind11/eigen.h> + +#include "types/image.hpp" +#include "module.hpp" + +namespace py = pybind11; + +using namespace voltu; + +void PyModule::py_automatic_bindings(py::module& m) {{ + {code} +}} +""" + import sys import os @@ -19,12 +44,12 @@ def get_loc_msg(data): def print_warn(msg, loc=None): if loc is not None: msg += " " + get_loc_msg(loc) - print("WARNING: %s" % msg, file=sys.stderr) + print("gen.py warning: %s" % msg, file=sys.stderr) def print_err(msg, loc=None): if loc is not None: msg += " " + get_loc_msg(loc) - print("ERROR: %s" % msg, file=sys.stderr) + print("gen.py error: %s" % msg, file=sys.stderr) def include_in_api(data): return "PY_API" in data["debug"] @@ -49,23 +74,14 @@ def create_enum_bindigs(enum, parent=[], pybind_handle="", export_values=False): return "\n\t".join(cpp) -def create_function_bindings(func, parent=[], pybind_handle=None): - func_name = func["name"] - full_name = parent + [func_name] - full_name_cpp = "::".join(full_name) - - if "PY_API" not in func["debug"]: - print_err("%s not included in Python API" % full_name_cpp, func) - raise ValueError("No PY_API") - - args = ["\"{0}\"".format(func_name), "&{0}".format(full_name_cpp)] +def process_func_args(func): + args = [] for param in func["parameters"]: param_name = param["name"] - if param_name == "&": - print_warn("Argument name missing for %s" % full_name_cpp, func) - + if param_name in ["&", "", "*"]: + print_warn("Argument name missing for %s" % func["name"]) continue if "default" in param: @@ -74,9 +90,50 @@ def create_function_bindings(func, parent=[], pybind_handle=None): else: args.append("py::arg(\"{0}\")".format(param_name)) + return args + +def wrap_lambda(func, instance_ptr, capture=True): + """ Wrap func to instance (singleton) with C++ lambda """ + args_lambda = [] + args_lambda_sig = [] + for i, param in enumerate(func["parameters"]): + argname = "p" + str(i) + args_lambda_sig.append("%s %s" % (param["type"], argname)) + args_lambda.append(argname) + + return "[%s](%s){ return %s->%s(%s); }" % ( + ("&" if capture else ""), + ", ".join(args_lambda_sig), + instance_ptr, + func["name"], + ", ".join(args_lambda)) + +def create_function_bindings(func, parent=[], pybind_handle=None, bind_to_singleton=None): + """ Create function bindings, if bind_to_singleton is set, C++ lamda is + generated to wrap the call to instance (named by bind_by_singleton). + """ + + func_name = func["name"] + full_name = parent + [func_name] + full_name_cpp = "::".join(full_name) + + if not include_in_api(func): + print_err("%s not included in Python API" % full_name_cpp, func) + raise ValueError("No PY_API") + + args = ["\"{0}\"".format(func_name)] + + if bind_to_singleton is None: + args.append("&{0}".format(full_name_cpp)) + + else: + args.append(wrap_lambda(func, bind_to_singleton)) + + args += process_func_args(func) + if "PY_RV_LIFETIME_PARENT" in func["debug"]: - if func["parent"] is None: - print_err("PY_RV_LIFETIME_PARENT used for function", func) + if func["parent"] is None or bind_to_singleton is not None: + print_err("PY_RV_LIFETIME_PARENT used for function or singleton", func) raise ValueError() args.append("py::return_value_policy::reference_internal") @@ -88,6 +145,8 @@ def create_function_bindings(func, parent=[], pybind_handle=None): return cpp def create_class_bindings(cls, parent=[], pybind_handle=""): + """ Create bindings for a class """ + cls_name = cls["name"] full_name = parent + [cls_name] cpp = [] @@ -116,8 +175,30 @@ def create_class_bindings(cls, parent=[], pybind_handle=""): return "\n\t\t".join(cpp) -if __name__ == "__main__": +def create_singleton_bindings(instance_ptr, cls, parent=[], pybind_handle="", export=False): + """ Singleton class bindings, either creates a singleton instance or + exports functions directly to module. Singleton pointer available with + instance_ptr, which should be class member of PyModule. + """ + if not export: + # use usual bindings and export as attribute to pybind handle + return "\n\t".join([ + create_class_bindings(cls, parent, pybind_handle) + ";", + "{0}.attr(\"{1}\") = {2};".format(pybind_handle, cls["name"], instance_ptr) + ]) + + else: + # use C++ lambdas to wrap all methods + cpp = [] + for func in cls["methods"]["public"]: + if not include_in_api(func): + continue + + cpp.append(create_function_bindings(func, parent, pybind_handle, instance_ptr) + ";") + + return "\n\t".join(cpp) +if __name__ == "__main__": from pprint import pprint if (len(sys.argv) < 4): @@ -131,6 +212,7 @@ if __name__ == "__main__": out = [] includes = [] + for fname in fsin: includes.append(fname) @@ -145,7 +227,13 @@ if __name__ == "__main__": ns = data["namespace"] # workaround: parser does not save debug in same way as for funcs data["debug"] = read_line(data["filename"], data["line_number"]) - out.append(create_class_bindings(data, [ns], handle) + ";") + + if "PY_SINGLETON_OBJECT" in data["debug"]: + out.append(create_singleton_bindings("instance_" + data["name"], data, [ns], handle, False)) + elif "PY_SINGLETON" in data["debug"]: + out.append(create_singleton_bindings("instance_" + data["name"], data, [ns], handle, True)) + else: + out.append(create_class_bindings(data, [ns], handle) + ";") for data in hdr.functions: ns = data["namespace"].strip("::") # bug? in parser @@ -153,9 +241,7 @@ if __name__ == "__main__": out.append(create_function_bindings(data, [ns], handle) + ";") includes = "\n".join("#include <{0}>".format(i) for i in includes) - template_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "automatic_bindings.cpp.in") - with open(template_file, "r") as f: - template = f.read() + out = template.format(includes=includes, code="\n\t".join(out)) with open(fout, "w",) as f: f.write(out) diff --git a/SDK/CPP/public/python/module.cpp b/SDK/CPP/public/python/module.cpp index 8564a388ef67f2b2adbdc929ae3c2e5e579700c9..636e7b809fcb6757776ef711dd837eb330130c9f 100644 --- a/SDK/CPP/public/python/module.cpp +++ b/SDK/CPP/public/python/module.cpp @@ -1,13 +1,16 @@ #include "module.hpp" -#include <voltu/voltu.hpp> #include <voltu/initialise.hpp> #include <voltu/types/errors.hpp> namespace py = pybind11; -void py_exceptions(pybind11::module& m) { +PyModule::PyModule() { + instance_System = voltu::instance(); +} + +void PyModule::py_exceptions(pybind11::module& m) { py::register_exception<voltu::exceptions::Exception>(m, "Error"); py::register_exception<voltu::exceptions::BadImageChannel>(m, "ErrorBadImageChannel"); py::register_exception<voltu::exceptions::NoFrame>(m, "ErrorNoFrame"); @@ -15,8 +18,11 @@ void py_exceptions(pybind11::module& m) { py::register_exception<voltu::exceptions::BadSourceURI>(m, "ErrorBadSourceURI"); } +static auto module = PyModule(); + PYBIND11_MODULE(voltu, m) { m.attr("version") = py::make_tuple(VOLTU_VERSION_MAJOR, VOLTU_VERSION_MINOR, VOLTU_VERSION_PATCH); - py_exceptions(m); - py_automatic_bindings(m); + + module.py_exceptions(m); + module.py_automatic_bindings(m); } diff --git a/SDK/CPP/public/python/module.hpp b/SDK/CPP/public/python/module.hpp index b605cf7f6b7030701858cf8d4cf2ffbba95636bf..f59e4d6280356357b56d4d2db928653aadd4a973 100644 --- a/SDK/CPP/public/python/module.hpp +++ b/SDK/CPP/public/python/module.hpp @@ -1,5 +1,15 @@ #pragma once #include <pybind11/pybind11.h> +#include <voltu/voltu.hpp> +#include <voltu/system.hpp> -void py_automatic_bindings(pybind11::module& m); +class PyModule { +public: + PyModule(); + void py_automatic_bindings(pybind11::module& m); + void py_exceptions(pybind11::module& m); + +private: + std::shared_ptr<voltu::System> instance_System; +}; diff --git a/SDK/CPP/public/python/tests/test_load.py b/SDK/CPP/public/python/tests/test_load.py index da800f67563627d12ee3809cef2f641dd01ee9a7..5118876f9a66210f24abc674cb782a9cb01a8a48 100644 --- a/SDK/CPP/public/python/tests/test_load.py +++ b/SDK/CPP/public/python/tests/test_load.py @@ -1,16 +1,19 @@ import unittest import os -import voltu class LoadLibrary(unittest.TestCase): - def test_get_instance(self): - self.assertIsNotNone(voltu.instance()) - # second call to instance() returns None; should - # return same instance instead? - # self.assertIsNotNone(voltu.instance()) + def test_import(self): + import voltu + + def test_import_twice(self): + # verify that System instance is created just once + import voltu + import voltu + self.assertIsNotNone(voltu.System) def test_version(self): + import voltu major, minor, patch = voltu.version if __name__ == '__main__':