From b1cf8e20121d7e6bb0002a66f4db66fc1048dbb6 Mon Sep 17 00:00:00 2001
From: Sebastian Hahta <joseha@utu.fi>
Date: Tue, 17 Mar 2020 18:16:17 +0200
Subject: [PATCH] Blender plugin (export to .ftl)

---
 SDK/Python/blender.py          | 345 +++++++++++++++++++++++++++++++++
 SDK/Python/ftl/__init__.py     |   3 +
 SDK/Python/ftl/streamwriter.py | 204 +++++++++++++++++++
 SDK/Python/ftl/types.py        |  84 ++++++++
 4 files changed, 636 insertions(+)
 create mode 100644 SDK/Python/blender.py
 create mode 100644 SDK/Python/ftl/__init__.py
 create mode 100644 SDK/Python/ftl/streamwriter.py
 create mode 100644 SDK/Python/ftl/types.py

diff --git a/SDK/Python/blender.py b/SDK/Python/blender.py
new file mode 100644
index 000000000..b5a74504e
--- /dev/null
+++ b/SDK/Python/blender.py
@@ -0,0 +1,345 @@
+bl_info = {
+    "name": "FTL plugin",
+    "blender": (2, 80, 2),
+    "category": "Import-Export",
+}
+
+import bpy
+import numpy as np
+from mathutils import Matrix, Vector
+
+from ftl import Camera, FTLStreamWriter
+from ftl.types import Channel, Pipeline
+
+################################################################################
+# https://blender.stackexchange.com/a/120063
+# https://blender.stackexchange.com/questions/15102/what-is-blenders-camera-projection-matrix-model
+
+def get_sensor_size(sensor_fit, sensor_x, sensor_y):
+    if sensor_fit == 'VERTICAL':
+        return sensor_y
+    return sensor_x
+
+def get_sensor_fit(sensor_fit, size_x, size_y):
+    if sensor_fit == 'AUTO':
+        if size_x >= size_y:
+            return 'HORIZONTAL'
+        else:
+            return 'VERTICAL'
+    return sensor_fit
+
+
+def get_calibration_matrix_K_from_blender(camd):
+    if camd.type != 'PERSP':
+        raise ValueError('Non-perspective cameras not supported')
+
+    scene = bpy.context.scene
+    f_in_mm = camd.lens
+    scale = scene.render.resolution_percentage / 100
+    resolution_x_in_px = scale * scene.render.resolution_x
+    resolution_y_in_px = scale * scene.render.resolution_y
+    sensor_size_in_mm = get_sensor_size(camd.sensor_fit, camd.sensor_width, camd.sensor_height)
+    sensor_fit = get_sensor_fit(
+        camd.sensor_fit,
+        scene.render.pixel_aspect_x * resolution_x_in_px,
+        scene.render.pixel_aspect_y * resolution_y_in_px
+    )
+    pixel_aspect_ratio = scene.render.pixel_aspect_y / scene.render.pixel_aspect_x
+    if sensor_fit == 'HORIZONTAL':
+        view_fac_in_px = resolution_x_in_px
+    else:
+        view_fac_in_px = pixel_aspect_ratio * resolution_y_in_px
+    pixel_size_mm_per_px = sensor_size_in_mm / f_in_mm / view_fac_in_px
+    s_u = 1 / pixel_size_mm_per_px
+    s_v = 1 / pixel_size_mm_per_px / pixel_aspect_ratio
+
+    # Parameters of intrinsic calibration matrix K
+    u_0 = resolution_x_in_px / 2 - camd.shift_x * view_fac_in_px
+    v_0 = resolution_y_in_px / 2 + camd.shift_y * view_fac_in_px / pixel_aspect_ratio
+    skew = 0 # only use rectangular pixels
+
+    K = Matrix(
+        ((s_u, skew, u_0),
+        (   0,  s_v, v_0),
+        (   0,    0,   1)))
+    return K
+
+# Returns camera rotation and translation matrices from Blender.
+#
+# There are 3 coordinate systems involved:
+#    1. The World coordinates: "world"
+#       - right-handed
+#    2. The Blender camera coordinates: "bcam"
+#       - x is horizontal
+#       - y is up
+#       - right-handed: negative z look-at direction
+#    3. The desired computer vision camera coordinates: "cv"
+#       - x is horizontal
+#       - y is down (to align to the actual pixel coordinates
+#         used in digital images)
+#       - right-handed: positive z look-at direction
+def get_3x4_RT_matrix_from_blender(cam):
+    # bcam stands for blender camera
+    R_bcam2cv = Matrix(
+        ((1, 0,  0),
+        (0, -1, 0),
+        (0, 0, -1)))
+
+    # Transpose since the rotation is object rotation,
+    # and we want coordinate rotation
+    # R_world2bcam = cam.rotation_euler.to_matrix().transposed()
+    # T_world2bcam = -1*R_world2bcam * location
+    #
+    # Use matrix_world instead to account for all constraints
+    location, rotation = cam.matrix_world.decompose()[0:2]
+    R_world2bcam = rotation.to_matrix().transposed()
+
+    # Convert camera location to translation vector used in coordinate changes
+    # T_world2bcam = -1*R_world2bcam*cam.location
+    # Use location from matrix_world to account for constraints:
+    T_world2bcam = -1*R_world2bcam @ location
+
+    # Build the coordinate transform matrix from world to computer vision camera
+    R_world2cv = R_bcam2cv@R_world2bcam
+    T_world2cv = R_bcam2cv@T_world2bcam
+
+    # put into 3x4 matrix
+    RT = Matrix((
+        R_world2cv[0][:] + (T_world2cv[0],),
+        R_world2cv[1][:] + (T_world2cv[1],),
+        R_world2cv[2][:] + (T_world2cv[2],)
+        ))
+
+    return RT
+
+def get_ftl_calibration_from_blender(camd, d_min=0.0, d_max=np.inf, baseline=0.0, doff=0.0):
+
+    K = get_calibration_matrix_K_from_blender(camd)
+    scene = bpy.context.scene
+    scale = scene.render.resolution_percentage / 100
+    resolution_x_in_px = scale * scene.render.resolution_x
+    resolution_y_in_px = scale * scene.render.resolution_y
+
+    ftlcam = Camera(K[0][0], K[1][1], -K[0][2], -K[1][2],
+                    int(resolution_x_in_px), int(resolution_y_in_px),
+                    d_min, d_max, baseline, doff)
+
+    return ftlcam
+
+################################################################################
+
+import typing
+
+class StereoImage(typing.NamedTuple):
+    intrinsics: Camera
+    pose: np.array
+    imL: np.array
+    depthL: np.array
+    imR: np.array
+    depthR: np.array
+
+def render():
+    """ render active camera (image and depth) """
+
+    use_nodes = bpy.context.scene.use_nodes
+    bpy.context.scene.use_nodes = True
+    tree = bpy.context.scene.node_tree
+    links = tree.links
+
+    # possible issues with existing nodes of same type when accessing via bpy.data (?)
+    rl = tree.nodes.new('CompositorNodeRLayers')
+    v = tree.nodes.new('CompositorNodeViewer')
+    v.use_alpha = True
+
+    try:
+        links.new(rl.outputs['Image'], v.inputs['Image'])
+        # depth cannot be accessed in python; hack uses alpha to store z-values
+        links.new(rl.outputs['Depth'], v.inputs['Alpha'])
+
+        bpy.ops.render.render()
+
+        pixels = bpy.data.images['Viewer Node']
+        pix = np.array(pixels.pixels[:])
+
+        # sRGB conversion
+        #pix2 = np.zeros(pix.shape[:], dtype=np.float)
+        pix_srgb = np.copy(pix)
+        np.copyto(pix_srgb, 1.055*(pix**(1.0/2.4)) - 0.055, where=pix <= 1)
+        np.copyto(pix_srgb, pix * 12.92, where=pix <= 0.0031308)
+
+        # Clamp?
+        pix_srgb[pix_srgb > 1.0] = 1.0
+
+        im = pix_srgb.reshape((pixels.size[1], pixels.size[0], pixels.channels))[:,:,0:3]
+        depth = np.array(pixels.pixels[:]).reshape((pixels.size[1], pixels.size[0], pixels.channels))[:,:,3]
+
+        # set invalid depth values to 0.0
+        d_max = 65504.0
+        depth[depth >= d_max] = 0.0
+
+        return im, depth
+
+    finally:
+        tree.nodes.remove(v)
+        tree.nodes.remove(rl)
+        bpy.context.scene.use_nodes = use_nodes
+
+def render_stereo(camera, baseline=0.15):
+    camera_old = bpy.context.scene.camera
+    try:
+        bpy.context.scene.camera = camera
+        imL, depthL = render()
+
+        location_old = camera.location.copy()
+        try:
+            camera.location = (camera.matrix_world @ Vector((baseline, 0.0, 0.0, 1.0)))[0:3]
+            imR, depthR = render()
+
+        finally:
+            camera.location = location_old
+
+    finally:
+        bpy.context.scene.camera = camera_old
+
+    d_max = max(np.max(depthL), np.max(depthR))
+    pose = np.identity(4,dtype=np.float32)
+    pose[0:3,0:4] = get_3x4_RT_matrix_from_blender(camera)
+    pose = np.linalg.inv(pose.T)
+
+    ftlcamera = get_ftl_calibration_from_blender(camera.data, baseline=baseline, d_max=d_max)
+
+    return StereoImage(ftlcamera, np.array(pose), imL, depthL, imR, depthR)
+
+################################################################################
+
+class FTL_Options(bpy.types.PropertyGroup):
+
+    file_path : bpy.props.StringProperty(name="File",
+                                         description="Output file",
+                                         default="./blender.ftl",
+                                         maxlen=1024,
+                                         subtype="FILE_PATH")
+
+    baseline : bpy.props.FloatProperty(name="Baseline",
+                                       description="Distance between cameras (x-direction relative to camera)",
+                                       default=0.15)
+
+    use_sgm : bpy.props.BoolProperty(name="Use SGM",
+                                     description="Calculate disparity using SGM",
+                                     default=False)
+
+
+    use_ground_truth : bpy.props.BoolProperty(name="Save as ground truth",
+                                              description="Save depth in Ground Truth instead of Depth channel.",
+                                              default=True)
+
+    mask_occlusions : bpy.props.BoolProperty(name="Mask occlusions",
+                                             description="Right camera depth is used to mask occluded pixels.",
+                                             default=True)
+
+    cameras : bpy.props.EnumProperty(
+        name = "Cameras",
+        description = "Cameras for rendering",
+        items = [
+            ("ACTIVE", "Active camera", "Only use active camera"),
+            ("SELECTED", "Selected cameras", "Use all selected cameras"),
+            ("ALL", "All cameras", "Use all available cameras")
+        ],
+        default = "ACTIVE"
+    )
+
+import ftl
+class FTL_OT_Operator(bpy.types.Operator):
+    bl_idname = "scene.ftl_operator"
+    bl_label = "FTL Operator"
+
+    def execute(self, context):
+        options = context.scene.ftl_options
+        writer = FTLStreamWriter(options.file_path)
+
+        if options.use_sgm:
+            writer.enable_pipeline(Pipeline.DEPTH)
+
+        cameras = []
+
+        if options.cameras == 'ACTIVE':
+            cameras.append(bpy.context.scene.camera)
+
+        elif options.cameras == 'SELECTED':
+            for obj in context.selected_objects:
+                if obj.type != 'CAMERA':
+                    continue
+                cameras.append(obj)
+
+        elif options.cameras == 'ALL':
+            for obj in context.scene.objects:
+                if obj.type != 'CAMERA':
+                    continue
+                cameras.append(obj)
+
+        depth_channel = Channel.Depth if not options.use_ground_truth else Channel.GroundTruth
+        baseline = options.baseline
+
+        for i, camera in enumerate(cameras):
+            res = render_stereo(camera, baseline)
+            writer.write(i, Channel.Calibration, res.intrinsics)
+            writer.write(i, Channel.Pose, res.pose)
+            writer.write(i, Channel.Left, res.imL)
+            writer.write(i, Channel.Right, res.imR)
+            writer.write(i, depth_channel, res.depthL)
+
+            if options.mask_occlusions:
+                writer.mask_occlusion(i, depth_channel, res.depthR)
+
+        print("saved to %s" % options.file_path)
+        return {'FINISHED'}
+
+class FTL_PT_Panel(bpy.types.Panel):
+    bl_label = "FTL Export"
+    bl_idname = "FTL_PT_ftl"
+    bl_space_type = 'PROPERTIES'
+    bl_region_type = 'WINDOW'
+    bl_context = "output"
+
+    def draw(self, context):
+        layout = self.layout
+
+        ftl_options = context.scene.ftl_options
+
+        row = layout.row()
+        row.prop(ftl_options, "file_path")
+
+        row = layout.row()
+        row.prop(ftl_options, "baseline")
+
+        row = layout.row()
+        row.prop(ftl_options, "cameras")
+
+        row = layout.row()
+        row.prop(ftl_options, "use_sgm")
+
+        row = layout.row()
+        row.prop(ftl_options, "use_ground_truth")
+
+        row = layout.row()
+        row.prop(ftl_options, "mask_occlusions")
+
+        row = layout.row()
+        row.operator("scene.ftl_operator", text="Generate")
+
+classes = (FTL_Options,
+           FTL_OT_Operator,
+           FTL_PT_Panel)
+
+def register():
+    for cls in classes:
+        bpy.utils.register_class(cls)
+    bpy.types.Scene.ftl_options = bpy.props.PointerProperty(type=FTL_Options)
+
+def unregister():
+    for cls in classes:
+        bpy.utils.unregister_class(cls)
+    del bpy.types.Scene.ftl_options
+
+if __name__ == "__main__":
+    register()
diff --git a/SDK/Python/ftl/__init__.py b/SDK/Python/ftl/__init__.py
new file mode 100644
index 000000000..1fdef3ada
--- /dev/null
+++ b/SDK/Python/ftl/__init__.py
@@ -0,0 +1,3 @@
+from . streamwriter import FTLStreamWriter
+from . types import Camera
+import types
\ No newline at end of file
diff --git a/SDK/Python/ftl/streamwriter.py b/SDK/Python/ftl/streamwriter.py
new file mode 100644
index 000000000..8d75dc793
--- /dev/null
+++ b/SDK/Python/ftl/streamwriter.py
@@ -0,0 +1,204 @@
+from . types import Channel, is_float_channel, Camera, Pipeline
+import numpy as np
+from enum import IntEnum
+
+import ctypes
+
+import os.path
+
+_paths = [
+    ".",
+    "/usr/lib",
+    "/usr/local/lib",
+]
+
+class _imageformat_t(IntEnum):
+	FLOAT     = 0
+	BGRA      = 1
+	RGBA      = 2
+	RGB       = 3
+	BGR       = 4
+	RGB_FLOAT = 5
+
+_libpath = None
+for p in _paths:
+    p = os.path.join(p, "libftl-dev.so")
+    if os.path.exists(p):
+        _libpath = p
+        break
+
+if _libpath is None:
+    raise FileNotFoundError("libftl-dev.so not found")
+
+_c_api = ctypes.CDLL(_libpath)
+
+_c_api.ftlCreateWriteStream.restype = ctypes.c_void_p
+_c_api.ftlCreateWriteStream.argtypes = [ctypes.c_char_p]
+
+_c_api.ftlIntrinsicsWriteLeft.restype = ctypes.c_int
+_c_api.ftlIntrinsicsWriteLeft.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_float, ctypes.c_float, ctypes.c_float, ctypes.c_float, ctypes.c_float, ctypes.c_float]
+
+_c_api.ftlIntrinsicsWriteRight.restype = ctypes.c_int
+_c_api.ftlIntrinsicsWriteRight.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_float, ctypes.c_float, ctypes.c_float, ctypes.c_float, ctypes.c_float, ctypes.c_float]
+
+_c_api.ftlImageWrite.restype = ctypes.c_int
+_c_api.ftlImageWrite.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_void_p]
+
+_c_api.ftlPoseWrite.restype = ctypes.c_int
+_c_api.ftlPoseWrite.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p]
+
+_c_api.ftlRemoveOcclusion.restype = ctypes.c_int
+_c_api.ftlRemoveOcclusion.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_void_p]
+
+_c_api.ftlMaskOcclusion.restype = ctypes.c_int
+_c_api.ftlMaskOcclusion.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_void_p]
+
+_c_api.ftlEnablePipeline.restype = ctypes.c_int
+_c_api.ftlEnablePipeline.argtypes = [ctypes.c_void_p, ctypes.c_int]
+
+_c_api.ftlDisablePipeline.restype = ctypes.c_int
+_c_api.ftlDisablePipeline.argtypes = [ctypes.c_void_p, ctypes.c_int]
+
+_c_api.ftlSelect.restype = ctypes.c_int
+_c_api.ftlSelect.argtypes = [ctypes.c_void_p, ctypes.c_int]
+
+_c_api.ftlNextFrame.restype = ctypes.c_int
+_c_api.ftlNextFrame.argtypes = [ctypes.c_void_p]
+
+_c_api.ftlDestroyStream.restype = ctypes.c_int
+_c_api.ftlDestroyStream.argtypes = [ctypes.c_void_p]
+
+def _ftl_check(retval):
+    if retval != 0:
+        raise Exception("FTL api returned %i" % retval)
+
+class FTLStreamWriter:
+    def __init__(self, fname):
+        self._sources = {}
+        self._instance = _c_api.ftlCreateWriteStream(bytes(fname, "utf8"))
+
+        if self._instance is None:
+            raise Exception("Error: ftlCreateWriteStream")
+
+    def __del__(self):
+        if self._instance is not None:
+            _c_api.ftlDestroyStream(self._instance)
+
+    def _check_image(self, source, channel, data):
+        """ Check image is has correct number of channels and correct size
+             and convert to compatible datatype if necessary. Raises
+             TypeError/ValueError on failure.
+        """
+
+        if not type(data) is np.ndarray:
+            raise TypeError("Data must be in numpy array")
+
+        if source not in self._sources:
+            raise ValueError("Source must be configured before adding images")
+
+        if len(data.shape) not in [2, 3]:
+            raise ValueError("Unsupported array shape %s" % str(data.shape))
+
+        height, width = self._sources[source].height, self._sources[source].width
+        if data.shape[0] != height or data.shape[1] != width:
+            # TODO: this can happen (depth and color), requires support in C api
+            raise ValueError("Image size different than previously configured")
+
+        if is_float_channel(channel):
+            data = data.astype(np.float32)
+            ftl_dtype = _imageformat_t.FLOAT
+
+        else:
+            if len(data.shape) == 2:
+                raise ValueError("Expected multi-channel image for channel %s" % str(channel))
+
+            nchans = data.shape[2]
+            if data.dtype in [np.float32, np.float64]:
+                data = data.astype(np.float32)
+                ftl_dtype = _imageformat_t.RGB_FLOAT
+                if nchans != 3:
+                    raise ValueError("Unsupported number of channels: %i" % nchans)
+
+            elif data.dtype in [np.int8, np.uint8]:
+                if nchans == 3:
+                    ftl_dtype = _imageformat_t.RGB
+                elif nchans == 4:
+                    ftl_dtype = _imageformat_t.RGBA
+                else:
+                    raise ValueError("Unsupported number of channels: %i" % nchans)
+
+            else:
+                raise ValueError ("Unsupported numpy data type")
+
+        data = np.ascontiguousarray(data)
+        return data, ftl_dtype
+
+    def _write_image(self, source, channel, data):
+        """ Wrapper for ftlImageWrite """
+
+        data, ftl_dtype = self._check_image(source, channel, data)
+        _ftl_check(_c_api.ftlImageWrite(self._instance, ctypes.c_int(source),
+                                        channel, ftl_dtype, 0,
+                                        data.ctypes.data_as(ctypes.c_void_p)))
+
+    def _write_calibration(self, source, channel, camera):
+        """ Wrapper for ftlIntrinsicsWriteLeft and ftlIntrinsicsWriteRight """
+
+        # TODO: type checks for camera
+
+        func = _c_api.ftlIntrinsicsWriteLeft if channel == Channel.Calibration else _c_api.ftlIntrinsicsWriteRight
+
+        _ftl_check(func(self._instance, source, camera.width, camera.height,
+                        camera.fx, camera.cx, camera.cy, camera.baseline,
+                        camera.min_depth, camera.max_depth))
+
+        self._sources[source] = camera
+
+    def _write_pose(self, source, channel, pose):
+        """ Wrapper for ftlftlPoseWrite """
+
+        if type(pose) not in [np.ndarray, np.matrix]:
+            raise TypeError("Data must be in numpy array or matrix")
+
+        if len(pose.shape) != 2 or pose.shape[0] != 4 or pose.shape[1] != 4:
+            raise ValueError("Pose must be a 4x4 matrix")
+
+        pose = np.ascontiguousarray(pose.astype(np.float32).T)
+        _ftl_check(_c_api.ftlPoseWrite(self._instance, source, pose.ctypes.data_as(ctypes.c_void_p)))
+
+    def write(self, source, channel, data):
+        """ Write data to stream """
+
+        if channel in [Channel.Calibration, Channel.Calibration2]:
+            self._write_calibration(source, channel, data)
+        elif channel == Channel.Pose:
+            self._write_pose(source, channel, data)
+        else:
+            self._write_image(source, channel, data)
+
+    def enable_pipeline(self, t):
+        if t not in Pipeline:
+            raise ValueError("Unknown pipeline")
+
+        _ftl_check(_c_api.ftlEnablePipeline(self._instance, t))
+
+    def disable_pipeline(self, t):
+        if t not in Pipeline:
+            raise ValueError("Unknown pipeline")
+
+        _ftl_check(_c_api.ftlDisablePipeline(self._instance, t))
+
+    def mask_occlusion(self, source, channel, data):
+        data, ftl_dtype = self._check_image(source, channel, data)
+
+        if not is_float_channel(channel):
+            raise ValueError("Bad channel")
+
+        if len(data.shape) != 2:
+            raise ValueError("Wrong number of channels")
+
+        _ftl_check(_c_api.ftlMaskOcclusion(self._instance, source, channel, 0,
+                                           data.ctypes.data_as(ctypes.c_void_p)))
+
+    def next_frame(self):
+        _ftl_check(_c_api.ftlNextFrame(self._instance))
diff --git a/SDK/Python/ftl/types.py b/SDK/Python/ftl/types.py
new file mode 100644
index 000000000..78d78e430
--- /dev/null
+++ b/SDK/Python/ftl/types.py
@@ -0,0 +1,84 @@
+from typing import NamedTuple
+from enum import IntEnum
+
+class Pipeline(IntEnum):
+	DEPTH = 0
+	RECONSTRUCT = 1
+
+# components/rgbd-sources/include/ftl/rgbd/camera.hpp
+class Camera(NamedTuple):
+    fx : float
+    fy : float
+    cx : float
+    cy : float
+    width : int
+    height : int
+    min_depth : float
+    max_depth : float
+    baseline : float
+    doff : float
+
+# components/codecs/include/ftl/codecs/channels.hpp
+class Channel(IntEnum):
+    None_           = -1
+    Colour			= 0	# 8UC3 or 8UC4
+    Left			= 0
+    Depth			= 1	# 32S or 32F
+    Right			= 2	# 8UC3 or 8UC4
+    Colour2			= 2
+    Depth2			= 3
+    Deviation		= 4
+    Screen			= 4
+    Normals			= 5	# 16FC4
+    Weights			= 6	# short
+    Confidence		= 7	# 32F
+    Contribution	= 7	# 32F
+    EnergyVector	= 8	# 32FC4
+    Flow			= 9	# 32F
+    Energy			= 10	# 32F
+    Mask			= 11	# 32U
+    Density			= 12	# 32F
+    Support1		= 13	# 8UC4 (currently)
+    Support2		= 14	# 8UC4 (currently)
+    Segmentation	= 15	# 32S?
+    Normals2		= 16	# 16FC4
+    ColourHighRes	= 17	# 8UC3 or 8UC4
+    LeftHighRes		= 17	# 8UC3 or 8UC4
+    Disparity		= 18
+    Smoothing		= 19	# 32F
+    RightHighRes	= 20	# 8UC3 or 8UC4
+    Colour2HighRes	= 20
+    Overlay			= 21   # 8UC4
+    GroundTruth		= 22	# 32F
+
+    Audio			= 32
+    AudioMono		= 32
+    AudioStereo		= 33
+
+    Configuration	= 64	# JSON Data
+    Settings1		= 65
+    Calibration		= 65	# Camera Parameters Object
+    Pose			= 66	# Eigen::Matrix4d
+    Settings2		= 67
+    Calibration2	= 67	# Right camera parameters
+    Index           = 68
+    Control			= 69	# For stream and encoder control
+    Settings3		= 70
+
+    Data			= 2048	# Custom data any codec.
+    Faces			= 2049 # Data about detected faces
+    Transforms		= 2050	# Transformation matrices for framesets
+    Shapes3D		= 2051	# Labeled 3D shapes
+    Messages		= 2052	# Vector of Strings
+
+_float_channels = [
+    Channel.Depth,
+    Channel.Confidence,
+    Channel.Density,
+    Channel.Energy,
+    Channel.GroundTruth,
+	Channel.Flow
+]
+
+def is_float_channel(channel):
+    return channel in _float_channels
-- 
GitLab