From 64b0a9217c90853a0d67bae561e17d93bb96811f Mon Sep 17 00:00:00 2001
From: Sebastian Hahta <joseha@utu.fi>
Date: Tue, 10 Nov 2020 19:34:32 +0200
Subject: [PATCH] get camera matrix, python ctors

---
 SDK/CPP/public/CMakeLists.txt                 |  5 +-
 .../public/include/voltu/types/intrinsics.hpp | 15 +++-
 SDK/CPP/public/python/README.md               |  2 +-
 SDK/CPP/public/python/gen.py                  | 75 +++++++++++++++++--
 SDK/CPP/public/src/types/intrinsics.cpp       | 10 +++
 SDK/CPP/public/{ => src}/voltu.cpp            |  0
 SDK/CPP/public/{ => src}/voltu_cv.cpp         |  0
 SDK/CPP/tests/CMakeLists.txt                  |  1 +
 SDK/CPP/tests/test_intrinsics.py              | 32 ++++++++
 9 files changed, 129 insertions(+), 11 deletions(-)
 create mode 100644 SDK/CPP/public/src/types/intrinsics.cpp
 rename SDK/CPP/public/{ => src}/voltu.cpp (100%)
 rename SDK/CPP/public/{ => src}/voltu_cv.cpp (100%)
 create mode 100644 SDK/CPP/tests/test_intrinsics.py

diff --git a/SDK/CPP/public/CMakeLists.txt b/SDK/CPP/public/CMakeLists.txt
index 51c276271..1128913e5 100644
--- a/SDK/CPP/public/CMakeLists.txt
+++ b/SDK/CPP/public/CMakeLists.txt
@@ -30,14 +30,15 @@ else()
 endif()
 
 set(VOLTU_SRCS
-	voltu.cpp
+	src/voltu.cpp
+	src/types/intrinsics.cpp
 )
 
 set(OPTIONAL_DEPENDENCIES)
 
 if (WITH_OPENCV)
 	list(APPEND OPTIONAL_DEPENDENCIES ${OpenCV_LIBS})
-	list(APPEND VOLTU_SRCS voltu_cv.cpp)
+	list(APPEND VOLTU_SRCS src/voltu_cv.cpp)
 	set(CMAKE_REQUIRED_INCLUDES ${OpenCV_INCLUDE_DIRS})
 endif()
 
diff --git a/SDK/CPP/public/include/voltu/types/intrinsics.hpp b/SDK/CPP/public/include/voltu/types/intrinsics.hpp
index cc4ee4caa..8571c9ea1 100644
--- a/SDK/CPP/public/include/voltu/types/intrinsics.hpp
+++ b/SDK/CPP/public/include/voltu/types/intrinsics.hpp
@@ -6,9 +6,14 @@
 
 #pragma once
 
+#include "../defines.hpp"
+
+#include <Eigen/Eigen>
+
 namespace voltu
 {
 
+/** Intrinsic camera paramters */
 struct Intrinsics
 {
 	unsigned int width;
@@ -17,12 +22,20 @@ struct Intrinsics
 	float principle_y;
 	float focal_x;
 	float focal_y;
+
+	/** Projection matrix */
+	PY_API Eigen::Matrix3d matrix();
 };
 
+/** Stereo camera intrinsic parameters.
+ *
+ * Baseline can be used to estimate depth accuracy for known depth. Assuming 1px
+ * disparity accuracy, accuracy of given depth z, the accuracy of depth is
+ * z^2/(f*T), where f is focal length and T baseline.
+ */
 struct StereoIntrinsics : public Intrinsics
 {
 	float baseline;
-	float doffs;
 	float min_depth;
 	float max_depth;
 };
diff --git a/SDK/CPP/public/python/README.md b/SDK/CPP/public/python/README.md
index 076ec86e3..1b8dcfddd 100644
--- a/SDK/CPP/public/python/README.md
+++ b/SDK/CPP/public/python/README.md
@@ -39,7 +39,7 @@ build. Several (empty) macros are used in headers to annoate Python API details.
 ## Not supported (yet) in automatic binding generation:
  * Nested classes
  * Static members
- * Constructors
+ * Constructors for non-POD structs.
  * 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/gen.py b/SDK/CPP/public/python/gen.py
index c284c737e..7bb7643ca 100755
--- a/SDK/CPP/public/python/gen.py
+++ b/SDK/CPP/public/python/gen.py
@@ -50,12 +50,47 @@ def print_err(msg, loc=None):
         msg += " " + get_loc_msg(loc)
     print("gen.py error: %s" % msg, file=sys.stderr)
 
+# Helpers ======================================================================
+
+def is_singleton_class(ccls):
+    # covers both cases PY_SINGLETON and PY_SINGLETON_OBJECT
+    return "PY_SINGLETON" in ccls["debug"]
+
+def is_member_of_singleton_class(func):
+    if func["parent"] is None:
+        return False
+
+    return is_singleton_class(func["parent"])
+
 def include_in_api(data):
     return "PY_API" in data["debug"]
 
 def no_shared_ptr_cls(clsdata):
     return (clsdata["declaration_method"] == "struct") or ("PY_NO_SHARED_PTR" in clsdata["debug"])
 
+def rv_bind_lifetime_to_parent(func):
+    if "PY_RV_LIFETIME_PARENT" in func["debug"]:
+        if func["parent"] is None:
+            print_err("PY_RV_LIFETIME_PARENT can not be used for function or singleton", func)
+            sys.exit(2) # generated code most likely wrong and unsafe (crash/UB)
+            return False
+
+        if is_member_of_singleton_class(func):
+            print_warn(("Return value of %s lifetime bound to parent, but " +
+                        "parent is singleton of %s. This is possibly an " +
+                        "error (lifetime of the return value is the lifetime " +
+                        "of the program).") %
+                        (func["name"], func["parent"]["name"]))
+
+        return True
+
+    return False
+
+def is_struct(clsdata):
+    return clsdata["declaration_method"] == "struct"
+
+# Code generation ==============================================================
+
 def create_enum_bindigs(enum, parent=[], pybind_handle="", export_values=False):
     name_full = parent + [enum["name"]]
     name_py = enum["name"]
@@ -76,6 +111,35 @@ def create_enum_bindigs(enum, parent=[], pybind_handle="", export_values=False):
 
     return "\n\t".join(cpp)
 
+def create_struct_ctor(struct):
+    types = []
+    args = []
+    ctors = ["def(py::init<>())"]
+
+    for item in struct["properties"]["public"]:
+        types.append(item["type"])
+        arg = "py::arg(\"%s\")" % item["name"]
+        has_defaults = False
+
+        if "default" in item:
+            has_defaults = True
+            arg += "=%s" % item["default"]
+
+        args.append(arg)
+
+    if len(struct["inherits"]) > 0 or has_defaults:
+        # NOTE: Structs which inherit another struct require managing state,
+        #       as base class possibly defines its own members from base
+        #       class(es). Move everything to class?
+        # NOTE: Non-POD structs which do not inherit base class but set default
+        #       values can be supported by generating lambda which assigns
+        #       the values.
+        print_warn("Only aggregate initialization supported for structs")
+        return ctors
+
+    ctors.append("def(py::init<%s>(), %s)" % (", ".join(types), ", ".join(args)))
+    return ctors
+
 def process_func_args(func):
     args = []
 
@@ -133,11 +197,7 @@ def create_function_bindings(func, parent=[], pybind_handle=None, bind_to_single
 
     args += process_func_args(func)
 
-    if "PY_RV_LIFETIME_PARENT" in func["debug"]:
-        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()
-
+    if rv_bind_lifetime_to_parent(func):
         args.append("py::return_value_policy::reference_internal")
 
     cpp = "def({0})".format(", ".join(args))
@@ -160,8 +220,8 @@ def create_class_bindings(clsdata, parent=[], pybind_handle=""):
 
     cpp.append(cls_cpp.format(handle=pybind_handle, name=clsdata_name))
 
-    if clsdata["declaration_method"] == "struct":
-        cpp.append(".def(py::init<>())")
+    if is_struct(clsdata):
+        cpp += ["." + ctor for ctor in create_struct_ctor(clsdata)]
 
     for method in clsdata["methods"]["public"]:
         if include_in_api(method):
@@ -172,6 +232,7 @@ def create_class_bindings(clsdata, parent=[], pybind_handle=""):
             field_cpp = ".def_property_readonly(\"{name}\", &{cpp_name})"
         else:
             field_cpp = ".def_readwrite(\"{name}\", &{cpp_name})"
+
         field_name = field["name"]
         field_name_cpp = "::".join(full_name + [field_name])
         cpp.append(field_cpp.format(name=field_name, cpp_name=field_name_cpp))
diff --git a/SDK/CPP/public/src/types/intrinsics.cpp b/SDK/CPP/public/src/types/intrinsics.cpp
new file mode 100644
index 000000000..63cfbc590
--- /dev/null
+++ b/SDK/CPP/public/src/types/intrinsics.cpp
@@ -0,0 +1,10 @@
+#include <voltu/types/intrinsics.hpp>
+#include <Eigen/Eigen>
+
+Eigen::Matrix3d voltu::Intrinsics::matrix() {
+	Eigen::Matrix3d K;
+	K <<		focal_x,			0.0,	-principle_x,
+					0.0,		focal_y,	-principle_y,
+					0.0,			0.0,			 1.0;
+	return K;
+}
diff --git a/SDK/CPP/public/voltu.cpp b/SDK/CPP/public/src/voltu.cpp
similarity index 100%
rename from SDK/CPP/public/voltu.cpp
rename to SDK/CPP/public/src/voltu.cpp
diff --git a/SDK/CPP/public/voltu_cv.cpp b/SDK/CPP/public/src/voltu_cv.cpp
similarity index 100%
rename from SDK/CPP/public/voltu_cv.cpp
rename to SDK/CPP/public/src/voltu_cv.cpp
diff --git a/SDK/CPP/tests/CMakeLists.txt b/SDK/CPP/tests/CMakeLists.txt
index 489f6e338..d7f25424e 100644
--- a/SDK/CPP/tests/CMakeLists.txt
+++ b/SDK/CPP/tests/CMakeLists.txt
@@ -13,3 +13,4 @@ function(add_python_test TEST_NAME TEST_SCRIPT)
 endfunction()
 
 add_python_test(Py_TestLoad test_load.py)
+add_python_test(Py_TestIntrinsics test_intrinsics.py)
diff --git a/SDK/CPP/tests/test_intrinsics.py b/SDK/CPP/tests/test_intrinsics.py
new file mode 100644
index 000000000..52d3fdab7
--- /dev/null
+++ b/SDK/CPP/tests/test_intrinsics.py
@@ -0,0 +1,32 @@
+import unittest
+import voltu
+import numpy as np
+
+class Intrinsics(unittest.TestCase):
+
+    def test_default_ctor(self):
+        intr = voltu.Intrinsics()
+        self.assertIsNotNone(intr)
+
+    def test_create(self):
+        fx = 2.0
+        fy = 3.0
+        cx = 4.0
+        cy = 5.0
+
+        intr = voltu.Intrinsics(
+            width = 6,
+            height = 7,
+            focal_x = fx,
+            focal_y = fy,
+            principle_x = -cx,
+            principle_y = -cy
+        )
+
+        K = np.array([
+            [fx , 0.0,  cx],
+            [0.0,  fy,  cy],
+            [0.0, 0.0, 1.0],
+        ])
+
+        self.assertTrue(np.array_equal(intr.matrix(), K))
-- 
GitLab