From f9591b939f29ac31d25e0e49a6a2d3fc4b936321 Mon Sep 17 00:00:00 2001
From: Sebastian Hahta <joseha@utu.fi>
Date: Mon, 23 Mar 2020 15:25:13 +0200
Subject: [PATCH] Python SDK: FTLStreamReader

---
 SDK/C/src/streams.cpp                         |  15 +-
 SDK/Python/blender_script.py                  | 317 ------------------
 SDK/Python/ftl/__init__.py                    |   9 +-
 SDK/Python/ftl/calibration.py                 |  43 +++
 {python => SDK/Python}/ftl/codecs.py          | 147 ++++++--
 {python => SDK/Python}/ftl/libde265.py        |   0
 .../Python/ftl/streamreader.py                |  79 +++--
 SDK/Python/ftl/streamwriter.py                |  11 +
 python/ftl/misc.py => SDK/Python/ftl/util.py  |  55 +--
 SDK/Python/test.py                            |   1 +
 SDK/Python/test/test_readwrite.py             |  44 +++
 SDK/Python/test/test_streamwriter.py          |  29 +-
 SDK/{Python => }/blender.py                   |  19 +-
 python/.gitignore                             |   2 -
 python/README.md                              |  89 -----
 python/ftl/.gitignore                         |   2 -
 python/ftl/__init__.py                        |   4 -
 python/ftl/ftlstreamwriter.py                 |  75 -----
 python/ftl/ftltype.py                         |  92 -----
 19 files changed, 306 insertions(+), 727 deletions(-)
 delete mode 100644 SDK/Python/blender_script.py
 create mode 100644 SDK/Python/ftl/calibration.py
 rename {python => SDK/Python}/ftl/codecs.py (75%)
 rename {python => SDK/Python}/ftl/libde265.py (100%)
 rename python/ftl/ftlstreamreader.py => SDK/Python/ftl/streamreader.py (52%)
 rename python/ftl/misc.py => SDK/Python/ftl/util.py (76%)
 create mode 100644 SDK/Python/test.py
 create mode 100644 SDK/Python/test/test_readwrite.py
 rename SDK/{Python => }/blender.py (96%)
 delete mode 100644 python/.gitignore
 delete mode 100644 python/README.md
 delete mode 100644 python/ftl/.gitignore
 delete mode 100644 python/ftl/__init__.py
 delete mode 100644 python/ftl/ftlstreamwriter.py
 delete mode 100644 python/ftl/ftltype.py

diff --git a/SDK/C/src/streams.cpp b/SDK/C/src/streams.cpp
index f0037f7ca..4e449b24b 100644
--- a/SDK/C/src/streams.cpp
+++ b/SDK/C/src/streams.cpp
@@ -153,7 +153,6 @@ ftlError_t ftlImageWrite(
 		LOG(INFO) << "MIN MAX " << minVal << " - " << maxVal;
 
 		if (tmp2.empty()) return FTLERROR_STREAM_NO_DATA;
-		cv::flip(tmp2, tmp2, 0);  // Flip to get opencv form.
 		img.upload(tmp2);
 
 		ftl::codecs::Channels<0> channels;
@@ -264,12 +263,10 @@ ftlError_t ftlRemoveOcclusion(ftlStream_t stream, int32_t sourceId, ftlChannel_t
 	//auto &mask = frame.create<cv::cuda::GpuMat>(ftl::codecs::Channel::Mask);
 	auto &depth = frame.get<cv::cuda::GpuMat>(static_cast<ftl::codecs::Channel>(channel));
 	auto &intrin = frame.getLeft();
-	
+
 	cv::Mat depthR(intrin.height, intrin.width, CV_32F, const_cast<float*>(data), pitch);
-	cv::Mat tmp;
-	cv::flip(depthR, tmp, 0);
 	cv::cuda::GpuMat depthRGPU;
-	depthRGPU.upload(tmp);
+	depthRGPU.upload(depthR);
 	ftl::cuda::remove_occlusions(depth, depthRGPU, intrin, 0);
 
 	return FTLERROR_OK;
@@ -291,14 +288,12 @@ ftlError_t ftlMaskOcclusion(ftlStream_t stream, int32_t sourceId, ftlChannel_t c
 	auto &mask = frame.create<cv::cuda::GpuMat>(ftl::codecs::Channel::Mask);
 	auto &depth = frame.get<cv::cuda::GpuMat>(static_cast<ftl::codecs::Channel>(channel));
 	auto &intrin = frame.getLeft();
-	
+
 	mask.create(depth.size(), CV_8UC1);
 
 	cv::Mat depthR(intrin.height, intrin.width, CV_32F, const_cast<float*>(data), pitch);
-	cv::Mat tmp;
-	cv::flip(depthR, tmp, 0);
 	cv::cuda::GpuMat depthRGPU;
-	depthRGPU.upload(tmp);
+	depthRGPU.upload(depthR);
 	ftl::cuda::mask_occlusions(depth, depthRGPU, mask, intrin, 0);
 
 	ftlSelect(stream, FTLCHANNEL_Mask);
@@ -334,7 +329,7 @@ ftlError_t ftlSelect(ftlStream_t stream, ftlChannel_t channel) {
 	if (!stream->stream) return FTLERROR_STREAM_INVALID_STREAM;
 	if (static_cast<int>(channel) < 0 || static_cast<int>(channel) > 32)
 		return FTLERROR_STREAM_BAD_CHANNEL;
-	
+
 	ftl::codecs::Channels<0> channels;
 	if (stream->stream->size() > static_cast<unsigned int>(stream->video_fs.id)) channels = stream->stream->selected(stream->video_fs.id);
 	channels += static_cast<ftl::codecs::Channel>(channel);
diff --git a/SDK/Python/blender_script.py b/SDK/Python/blender_script.py
deleted file mode 100644
index 96dea4cb4..000000000
--- a/SDK/Python/blender_script.py
+++ /dev/null
@@ -1,317 +0,0 @@
-import bpy
-import numpy as np
-from mathutils import Matrix, Vector
-from collections import namedtuple
-
-Camera = namedtuple("Camera", ["fx", "fy", "cx", "cy", "width", "height",
-                                "min_depth", "max_depth", "baseline", "doff"])
-
-_d_max = 65504.0
-
-def lin2s(x):
-    a = 0.055
-    if x <=0.0031308:
-        y = x * 12.92
-    elif 0.0031308 < x <= 1 :
-        y = 1.055*(x**(1.0/2.4)) - 0.055
-
-    return y
-
-################################################################################
-# https://blender.stackexchange.com/a/120063
-
-# BKE_camera_sensor_size
-def get_sensor_size(sensor_fit, sensor_x, sensor_y):
-    if sensor_fit == 'VERTICAL':
-        return sensor_y
-    return sensor_x
-
-# BKE_camera_sensor_fit
-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
-
-# Build intrinsic camera parameters from Blender camera data
-#
-# See notes on this in 
-# blender.stackexchange.com/questions/15102/what-is-blenders-camera-projection-matrix-model
-# as well as
-# https://blender.stackexchange.com/a/120063/3581
-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
-
-def get_ftl_calibration_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
-
-    ftlcam = Camera(s_u, s_v, -u_0, -v_0, resolution_x_in_px, resolution_y_in_px, 0.1, 16.0, 0.15, 0.0)
-    return ftlcam
-
-# 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_3x4_P_matrix_from_blender(cam):
-    K = get_calibration_matrix_K_from_blender(cam.data)
-    RT = get_3x4_RT_matrix_from_blender(cam)
-    return K@RT, K, RT
-
-################################################################################
-
-import typing
-
-class StereoImage(typing.NamedTuple):
-    intrinsics: Camera
-    pose: np.array
-    baseline: float
-    imL: np.array
-    depthL: np.array
-    imR: np.array
-    depthR: np.array
-
-def render():
-    """ render active camera (image and depth) """
-    
-    bpy.context.scene.use_nodes = True
-    tree = bpy.context.scene.node_tree
-    links = tree.links
-
-    for n in tree.nodes:
-        tree.nodes.remove(n)
-
-    rl = tree.nodes.new('CompositorNodeRLayers')
-
-    v = tree.nodes.new('CompositorNodeViewer')
-    v.use_alpha = True
-
-    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)
-    pix2 = np.copy(pix)
-    np.copyto(pix2, 1.055*(pix**(1.0/2.4)) - 0.055, where=pix <= 1)
-    np.copyto(pix2, pix * 12.92, where=pix <= 0.0031308)
-    
-    # Clamp?
-    pix2[pix2 > 1.0] = 1.0
-    
-    
-    im = pix2.reshape((pixels.size[1], pixels.size[0], pixels.channels))
-    im2 = (im[:,:,0:3]).astype(np.float32)
-    
-    depthim = (np.array(pixels.pixels[:]).reshape((pixels.size[1], pixels.size[0], pixels.channels))[:,:,3]).astype(np.float32)
-    # set invalid depth values to 0.0
-    depthim[depthim >= _d_max] = 0.0
-    
-    return im2, depthim
-
-def render_stereo(camera, baseline=0.15):
-    bpy.context.scene.camera = camera
-    #_, K, pose = get_3x4_P_matrix_from_blender(camera)
-    ftlcam = get_ftl_calibration_from_blender(camera.data)
-    pose = get_3x4_RT_matrix_from_blender(camera)
-    imL, depthL = render()
-    
-    location_old = camera.location.copy()
-    camera.location = (camera.matrix_world @ Vector((baseline, 0.0, 0.0, 1.0)))[0:3]
-    
-    imR, depthR = render()
-    
-    camera.location = location_old
-    
-    return StereoImage(Camera(ftlcam.fx, ftlcam.fy, ftlcam.cx, ftlcam.cy, ftlcam.width, ftlcam.height, 0.1, np.amax(depthL), baseline, 0.0), pose, baseline, imL, depthL, imR, depthR)
-
-
-# ====== Load the FTL SDK ======================================================
-# TODO: Wrap this properly
-from ctypes import *
-ftl = CDLL('/home/nick/git-repos/ftl/build/SDK/C/libftl-dev.so.0')
-
-ftlCreateWriteStream = ftl.ftlCreateWriteStream
-ftlCreateWriteStream.restype = c_void_p
-ftlCreateWriteStream.argtypes = [c_char_p]
-
-ftlIntrinsicsWriteLeft = ftl.ftlIntrinsicsWriteLeft
-ftlIntrinsicsWriteLeft.restype = c_int
-ftlIntrinsicsWriteLeft.argtypes = [c_void_p, c_int, c_int, c_int, c_float, c_float, c_float, c_float, c_float, c_float]
-
-ftlIntrinsicsWriteRight = ftl.ftlIntrinsicsWriteRight
-ftlIntrinsicsWriteRight.restype = c_int
-ftlIntrinsicsWriteRight.argtypes = [c_void_p, c_int, c_int, c_int, c_float, c_float, c_float, c_float, c_float, c_float]
-
-ftlImageWrite = ftl.ftlImageWrite
-ftlImageWrite.restype = c_int
-ftlImageWrite.argtypes = [c_void_p, c_int, c_int, c_int, c_int, c_void_p]
-
-ftlRemoveOcclusion = ftl.ftlRemoveOcclusion
-ftlRemoveOcclusion.restype = c_int
-ftlRemoveOcclusion.argtypes = [c_void_p, c_int, c_int, c_int, c_void_p]
-
-ftlMaskOcclusion = ftl.ftlMaskOcclusion
-ftlMaskOcclusion.restype = c_int
-ftlMaskOcclusion.argtypes = [c_void_p, c_int, c_int, c_int, c_void_p]
-
-ftlEnablePipeline = ftl.ftlEnablePipeline
-ftlEnablePipeline.restype = c_int
-ftlEnablePipeline.argtypes = [c_void_p, c_int]
-
-ftlSelect = ftl.ftlSelect
-ftlSelect.restype = c_int
-ftlSelect.argtypes = [c_void_p, c_int]
-
-ftlPoseWrite = ftl.ftlPoseWrite
-ftlPoseWrite.restype = c_int
-ftlPoseWrite.argtypes = [c_void_p, c_int, c_void_p]
-
-ftlDestroyStream = ftl.ftlDestroyStream
-ftlDestroyStream.restype = c_int
-ftlDestroyStream.argtypes = [c_void_p]
-
-# ==============================================================================
-
-def ftlCheck(err):
-    if err != 0:
-        print("FTL SDK Error: ", err)
-
-def render_and_save(filename, cameras):
-    i = 0
-
-    stream = ftlCreateWriteStream(filename)
-    if stream == None:
-        print("Could not create FTL stream")
-        return
-
-    ftlCheck(ftlEnablePipeline(stream, 0))
-
-    for camname in cameras:
-        obj = bpy.context.scene.objects[camname]
-        if obj.type == 'CAMERA':
-            image = render_stereo(obj, 0.15)
-
-            ftlCheck(ftlIntrinsicsWriteLeft(c_void_p(stream), c_int(i), c_int(int(image.intrinsics.width)), c_int(int(image.intrinsics.height)), c_float(image.intrinsics.fx), c_float(image.intrinsics.cx), c_float(image.intrinsics.cy), c_float(image.intrinsics.baseline), c_float(image.intrinsics.min_depth), c_float(image.intrinsics.max_depth)))
-            ftlCheck(ftlIntrinsicsWriteRight(c_void_p(stream), c_int(i), c_int(int(image.intrinsics.width)), c_int(int(image.intrinsics.height)), c_float(image.intrinsics.fx), c_float(image.intrinsics.cx), c_float(image.intrinsics.cy), c_float(image.intrinsics.baseline), c_float(image.intrinsics.min_depth), c_float(image.intrinsics.max_depth)))
-            
-            M = np.identity(4,dtype=np.float32)
-            M[0:3,0:4] = image.pose
-            M = np.transpose(M)
-            M = np.linalg.inv(M)
-            ftlCheck(ftlPoseWrite(stream, c_int(i), M.ctypes.data_as(c_void_p)))
-
-            ftlCheck(ftlImageWrite(stream, c_int(i), 0, 5, 0, image.imL.ctypes.data_as(c_void_p)))
-            ftlCheck(ftlImageWrite(stream, c_int(i), 2, 5, 0, image.imR.ctypes.data_as(c_void_p)))
-            ftlCheck(ftlImageWrite(stream, c_int(i), 22, 0, 0, image.depthL.ctypes.data_as(c_void_p)))
-            ftlCheck(ftlMaskOcclusion(stream, c_int(i), 22, 0, image.depthR.ctypes.data_as(c_void_p)))
-            i = i + 1
-
-    ftlCheck(ftlDestroyStream(stream))
-
-render_and_save(b'./blender.ftl', ['Camera'])
-
diff --git a/SDK/Python/ftl/__init__.py b/SDK/Python/ftl/__init__.py
index 1fdef3ada..0ac811185 100644
--- a/SDK/Python/ftl/__init__.py
+++ b/SDK/Python/ftl/__init__.py
@@ -1,3 +1,10 @@
 from . streamwriter import FTLStreamWriter
 from . types import Camera
-import types
\ No newline at end of file
+import types
+
+from warnings import warn as _warn
+
+try:
+    from . streamreader import FTLStreamReader
+except ImportError as e:
+    _warn("Could not import StreamReader, missing dependecies? %s" % str(e))
\ No newline at end of file
diff --git a/SDK/Python/ftl/calibration.py b/SDK/Python/ftl/calibration.py
new file mode 100644
index 000000000..36fdab2f0
--- /dev/null
+++ b/SDK/Python/ftl/calibration.py
@@ -0,0 +1,43 @@
+import numpy as np
+from . types import Camera
+
+def get_camera_matrix(calib):
+    K = np.identity(3, dtype=np.float64)
+    K[0,0] = calib.fx
+    K[1,1] = calib.fy
+    K[0,2] = -calib.cx
+    K[1,2] = -calib.cy
+    return K
+
+def get_Q(calib):
+    """ Disparity to depth matrix. Explained in "Learning OpenCV: Computer
+        Vision with the OpenCV Library" (2008) p. 435.
+    """
+    Q = np.identity(4, dtype=np.float64)
+    Q[0,3] = calib.cx
+    Q[1,3] = calib.cy
+    Q[2,2] = 0.0
+    Q[2,3] = calib.fx
+    Q[3,2] = -1 / calib.baseline
+    Q[3,3] = calib.doff
+    return Q
+
+class Calibration:
+    @staticmethod
+    def from_K(K, size, min_depth=0.0, max_depth=100.0, baseline=0.0, doff=0.0):
+        calib = Camera._make([K[0,0], K[1,1], K[0,2], K[1,2], size[1], size[0],
+                             min_depth, max_depth, baseline, doff])
+        return Calibration(calib, None, None)
+
+    def __init__(self, calib, channel, capabilities):
+        self._calib = calib
+        self._capabilities = capabilities
+
+    def matrix(self):
+        return get_camera_matrix(self._calib)
+
+    def Q(self):
+        return get_Q(self._calib)
+
+    def camera(self):
+        return self._calib
diff --git a/python/ftl/codecs.py b/SDK/Python/ftl/codecs.py
similarity index 75%
rename from python/ftl/codecs.py
rename to SDK/Python/ftl/codecs.py
index bf4e21e68..dd14c27d8 100644
--- a/python/ftl/codecs.py
+++ b/SDK/Python/ftl/codecs.py
@@ -4,12 +4,67 @@ import msgpack
 import struct
 from warnings import warn
 
-from . import ftltype
+from . types import Channel, Camera, is_float_channel
 from . import libde265
-from . misc import Calibration
+from . calibration import Calibration
 
+from collections import namedtuple
 from enum import IntEnum
 
+################################################################################
+
+# components/codecs/include/ftl/codecs/packet.hpp
+Packet = namedtuple("Packet", ["codec", "definition", "block_total",
+                               "block_number", "flags", "data"])
+
+StreamPacket = namedtuple("StreamPacket", ["timestamp", "frameset_id",
+                                           "frame_number", "channel"])
+
+class PacketFlags:
+    RGB = 0x01
+    MappedDepth = 0x02
+    Float = 0x04
+    Partial = 0x10
+    Multiple = 0x80
+
+# components/codecs/include/ftl/codecs/codecs.hpp
+class codec_t(IntEnum):
+    JPG = 0
+    PNG = 1
+    H264 = 2
+    HEVC = 3
+    WAV = 4
+    JSON = 100
+    CALIBRATION = 101
+    POSE = 102
+    MSGPACK = 103,
+    STRING = 104,
+    RAW = 105
+
+definition_t = {
+    0 : (7680, 4320),
+    1 : (2160, 3840),
+    2 : (1080, 1920),
+    3 : (720, 1280),
+    4 : (576, 1024),
+    5 : (480, 854),
+    6 : (360, 640),
+    7 : (0, 0),
+    8 : (2056, 1852)
+}
+
+def get_definition(shape):
+	for k, v in definition_t.items():
+		if shape[:2] == v:
+			return k
+
+	return 7 # (None)
+
+def process_flags_image(packet, im):
+    return im
+
+################################################################################
+
 _has_opencv = False
 try:
     import cv2 as cv
@@ -71,19 +126,41 @@ class FTLDecoder:
 # OpenCV (optional)
 ################################################################################
 
+def split_images(packet, im):
+    if packet.block_total == 1:
+        return im
+
+    n = packet.block_total
+    height, width = definition_t[packet.definition]
+    cols = im.shape[1] // width
+
+    imgs = []
+    for i in range(0, n):
+        y_start = (i//cols) * height
+        y_end = y_start + height
+        x_start = (i%cols) * width
+        x_end = x_start + width
+        imgs.append(im[y_start:y_end,x_start:x_end,...])
+
+    return imgs
+
 def decode_codec_opencv(packet):
     if packet.block_total != 1 or packet.block_number != 0:
-        raise Exception("Unsupported block format (todo)")
+        warn("Unsupported block format (todo)") # is this relevant?
 
-    return _int_to_float(cv.imdecode(np.frombuffer(packet.data, dtype=np.uint8),
-                                                   cv.IMREAD_UNCHANGED))
+    im = _int_to_float(cv.imdecode(np.frombuffer(packet.data, dtype=np.uint8),
+                                                 cv.IMREAD_UNCHANGED))
+
+    return split_images(packet, im)
 
 def decode_codec_opencv_float(packet):
     if packet.block_total != 1 or packet.block_number != 0:
-        raise Exception("Unsupported block format (todo)")
+        warn("Unsupported block format (todo)") # is this relevant?
+
+    im = cv.imdecode(np.frombuffer(packet.data, dtype=np.uint8),
+                                   cv.IMREAD_UNCHANGED).astype(np.float) / 1000.0
 
-    return cv.imdecode(np.frombuffer(packet.data, dtype=np.uint8),
-                                     cv.IMREAD_UNCHANGED).astype(np.float) / 1000.0
+    return split_images(packet, im)
 
 ################################################################################
 # HEVC
@@ -237,7 +314,7 @@ class FTLDecoder_HEVC_Float(FTLDecoder_HEVC):
         if img is None:
             return None
 
-        if (packet.flags & ftltype.PacketFlags.MappedDepth):
+        if (packet.flags & PacketFlags.MappedDepth):
             return self._decode_format_mappeddepth(img)
 
         else:
@@ -265,11 +342,11 @@ class FTLDecoder_HEVC_YCbCr(FTLDecoder_HEVC):
 
 def decode_codec_calibration(packet):
     calibration = struct.unpack("@ddddIIdddd", packet.data[:(4*8+2*4+4*8)])
-    return Calibration(ftltype.Camera._make(calibration), 0, 0)
+    return Calibration(Camera._make(calibration), 0, 0)
 
 def decode_codec_msgpack_calibration(packet):
     calib, channel, capabilities = msgpack.unpackb(packet.data)
-    return Calibration(ftltype.Camera._make(calib), channel, capabilities)
+    return Calibration(Camera._make(calib), channel, capabilities)
 
 def decode_codec_msgpack_pose(packet):
     raw = msgpack.unpackb(packet.data)
@@ -292,8 +369,8 @@ def create_decoder(codec, channel, version=3):
         @returns    callable which takes packet as argument
     """
 
-    if codec == ftltype.codec_t.HEVC:
-        if ftltype.is_float_channel(channel):
+    if codec == codec_t.HEVC:
+        if is_float_channel(channel):
             return FTLDecoder_HEVC_Float().decode
         else:
             if version < 3:
@@ -301,33 +378,33 @@ def create_decoder(codec, channel, version=3):
             else:
                 return FTLDecoder_HEVC_YCbCr().decode
 
-    elif codec == ftltype.codec_t.PNG:
+    elif codec == codec_t.PNG:
         if not _has_opencv:
             raise Exception("OpenCV required for OpenCV (png/jpeg) decoding")
 
-        if ftltype.is_float_channel(channel):
+        if is_float_channel(channel):
             return decode_codec_opencv_float
         else:
             return decode_codec_opencv
 
-    elif codec == ftltype.codec_t.JPG:
+    elif codec == codec_t.JPG:
         if not _has_opencv:
             raise Exception("OpenCV required for OpenCV (png/jpeg) decoding")
 
         return decode_codec_opencv
 
-    elif codec == ftltype.codec_t.MSGPACK:
-        if channel == ftltype.Channel.Calibration:
+    elif codec == codec_t.MSGPACK:
+        if channel == Channel.Calibration:
             return decode_codec_msgpack_calibration
-        elif channel == ftltype.Channel.Pose:
+        elif channel == Channel.Pose:
             return decode_codec_msgpack_pose
         else:
             return lambda packet: msgpack.unpackb(packet.data)
 
-    elif codec == ftltype.codec_t.CALIBRATION:
+    elif codec == codec_t.CALIBRATION:
         return decode_codec_calibration
 
-    elif codec == ftltype.codec_t.POSE:
+    elif codec == codec_t.POSE:
         return decode_codec_pose
 
     else:
@@ -338,7 +415,7 @@ def create_decoder(codec, channel, version=3):
 ################################################################################
 
 def create_packet(codec, definition, flags, data):
-    return ftltype.Packet._make((codec, definition, 1, 0, flags, data))
+    return Packet._make((codec, definition, 1, 0, flags, data))
 
 # TODO exception types?
 
@@ -346,8 +423,8 @@ def encode_codec_opencv_jpg(data, **kwargs):
     params = []
     retval, encoded = cv.imencode(".jpg", data, params)
     if retval:
-        return create_packet(ftltype.codec_t.JPG,
-                             ftltype.get_definition(data),
+        return create_packet(codec_t.JPG,
+                             get_definition(data),
                              0,
                              encoded)
     else:
@@ -358,8 +435,8 @@ def encode_codec_opencv_png(data, **kwargs):
     params = [cv.IMWRITE_PNG_COMPRESSION, 9]
     retval, encoded = cv.imencode(".png", data, params)
     if retval:
-        return create_packet(ftltype.codec_t.PNG,
-                             ftltype.get_definition(data),
+        return create_packet(codec_t.PNG,
+                             get_definition(data),
                              0,
                              encoded)
     else:
@@ -371,8 +448,8 @@ def encode_codec_opencv_png_float(data, compression=9):
     params = [cv.IMWRITE_PNG_COMPRESSION, compression]
     retval, encoded = cv.imencode(".png", data, params)
     if retval:
-        return create_packet(ftltype.codec_t.PNG,
-                             ftltype.get_definition(data),
+        return create_packet(codec_t.PNG,
+                             get_definition(data),
                              0,
                              encoded)
     else:
@@ -387,23 +464,23 @@ def create_encoder(codec, channel, **options):
         @returns    callable which takes unencoded data and optional parameters
     """
 
-    if codec == ftltype.codec_t.JPG:
-        if not ftltype.is_float_channel(channel):
+    if codec == codec_t.JPG:
+        if not is_float_channel(channel):
             return encode_codec_opencv_jpg
         else:
             raise Exception("JPG not supported for float channels")
 
-    elif codec == ftltype.codec_t.PNG:
-        if ftltype.is_float_channel(channel):
+    elif codec == codec_t.PNG:
+        if is_float_channel(channel):
             return encode_codec_opencv_png_float
         else:
             return encode_codec_opencv_png
 
-    elif codec == ftltype.codec_t.MSGPACK:
-        if channel == ftltype.Channel.Pose:
+    elif codec == codec_t.MSGPACK:
+        if channel == Channel.Pose:
             raise NotImplementedError("todo")
 
-        elif channel == ftltype.Channel.Calibration:
+        elif channel == Channel.Calibration:
             raise NotImplementedError("todo")
 
         else:
diff --git a/python/ftl/libde265.py b/SDK/Python/ftl/libde265.py
similarity index 100%
rename from python/ftl/libde265.py
rename to SDK/Python/ftl/libde265.py
diff --git a/python/ftl/ftlstreamreader.py b/SDK/Python/ftl/streamreader.py
similarity index 52%
rename from python/ftl/ftlstreamreader.py
rename to SDK/Python/ftl/streamreader.py
index 1a33d16f3..e5c82c846 100644
--- a/python/ftl/ftlstreamreader.py
+++ b/SDK/Python/ftl/streamreader.py
@@ -3,21 +3,23 @@ import numpy as np
 
 from warnings import warn
 
-from . import ftltype
-from . codecs import create_decoder
+from . types import Channel, Camera
+from . codecs import create_decoder, StreamPacket, Packet, PacketFlags
 
 class FTLStreamReader:
     """ FTL file reader. """
 
-    def __init__(self, file):
+    def __init__(self, file, max_buffer_size=64*2**20):
         self._file = open(file, "br")
         self._version = 0
 
         self._decoders = {}
-        self._enabled_sources = []
         self._available_sources = []
 
-        self._data = None
+        self._p = None
+        self._sp = None
+        self._decoded = None
+        self._frames = []
 
         try:
             magic = self._file.read(5)
@@ -29,7 +31,9 @@ class FTLStreamReader:
                 # first 64 bytes reserved
                 self._file.read(8*8)
 
-            self._unpacker = msgpack.Unpacker(self._file, raw=True, use_list=False)
+            self._unpacker = msgpack.Unpacker(self._file, raw=True,
+                                              use_list=False,
+                                              max_buffer_size=max_buffer_size)
 
         except Exception as ex:
             self._file.close()
@@ -40,9 +44,28 @@ class FTLStreamReader:
     def __del__(self):
         self._file.close()
 
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        self._file.close()
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        if len(self._frames) > 0:
+            return self._frames.pop(0)
+
+        if not self.read():
+            raise StopIteration
+
+        self._frames = self.get()
+        return self._frames.pop(0)
+
     def _read_next(self):
         v1, v2 = self._unpacker.unpack()
-        return ftltype.StreamPacket._make(v1), ftltype.Packet._make(v2)
+        return StreamPacket._make(v1), Packet._make(v2)
 
     def seek(self, ts):
         """ Read until timestamp reached """
@@ -58,7 +81,7 @@ class FTLStreamReader:
         Reads data for until the next timestamp. Returns False if there is no
         more data to read, otherwise returns True.
         """
-        self._data = None
+        self._decoded = None
 
         try:
             self._sp, self._p = self._read_next()
@@ -67,39 +90,41 @@ class FTLStreamReader:
         except msgpack.OutOfData:
             return False
 
-        if self._sp.streamID not in self._available_sources:
-            self._available_sources.append(self._sp.streamID)
-
-        if self._enabled_sources and self._sp.streamID not in self._enabled_sources:
-            return True
+        if self._sp.frameset_id not in self._available_sources:
+            self._available_sources.append(self._sp.frameset_id)
 
-        k = (self._sp.streamID, self._sp.channel)
+        k = (self._sp.frameset_id, self._sp.channel)
         if k not in self._decoders:
             self._decoders[k] = create_decoder(self._p.codec, self._sp.channel, self._version)
 
-        self._data = self._decoders[k](self._p)
-
+        self._decoded = self._decoders[k](self._p)
         return True
 
+    def get(self):
+        if isinstance(self._decoded, list):
+            if self._p.flags == PacketFlags.Partial:
+                raise NotImplementedError("partial packets not implemented (todo)")
+
+            res = []
+            for src, data in enumerate(self._decoded):
+                # How are sources and frames mapped? What if frame missing from
+                # one of the sources?
+                res.append((self.get_timestamp(), src, Channel(self._sp.channel), data))
+
+            return res
+
+        else:
+            return [(self.get_timestamp(), self._sp.frame_number, Channel(self._sp.channel), self._decoded)]
+
     def get_raw(self):
         """ Returns previously read StreamPacket and Packet """
         return self._sp, self._p
 
-    def get_channel(self):
-        return ftltype.Channel(self._sp.channel)
-
-    def get_source_id(self):
-        return self._sp.streamID
-
     def get_timestamp(self):
         return self._sp.timestamp
 
-    def get_data(self):
-        """ Returns decoded data """
-        return self._data
-
     def get_sources(self):
-        """ Return list of sources. Can change as stream is read. """
+        """ Return list of sources. """
         return list(self._available_sources)
 
     def get_version(self):
diff --git a/SDK/Python/ftl/streamwriter.py b/SDK/Python/ftl/streamwriter.py
index ce8d86479..b4703ade2 100644
--- a/SDK/Python/ftl/streamwriter.py
+++ b/SDK/Python/ftl/streamwriter.py
@@ -99,10 +99,21 @@ class FTLStreamWriter:
         if self._instance is None:
             raise Exception("Error: ftlCreateWriteStream")
 
+    def close(self):
+        if self._instance is not None:
+            _ftl_check(_c_api.ftlDestroyStream(self._instance))
+            self._instance = None
+
     def __del__(self):
         if self._instance is not None:
             _c_api.ftlDestroyStream(self._instance)
 
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        self.close()
+
     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
diff --git a/python/ftl/misc.py b/SDK/Python/ftl/util.py
similarity index 76%
rename from python/ftl/misc.py
rename to SDK/Python/ftl/util.py
index cde61b973..71ede7160 100644
--- a/python/ftl/misc.py
+++ b/SDK/Python/ftl/util.py
@@ -1,66 +1,23 @@
 import numpy as np
-from . ftltype import Camera
-
-def get_camera_matrix(calib):
-    K = np.identity(3, dtype=np.float64)
-    K[0,0] = calib.fx
-    K[1,1] = calib.fy
-    K[0,2] = calib.cx
-    K[1,2] = calib.cy
-    return K
-
-def get_Q(calib):
-    """ Disparity to depth matrix. Explained in "Learning OpenCV: Computer
-        Vision with the OpenCV Library" (2008) p. 435.
-    """
-    Q = np.identity(4, dtype=np.float64)
-    Q[0,3] = calib.cx
-    Q[1,3] = calib.cy
-    Q[2,2] = 0.0
-    Q[2,3] = calib.fx
-    Q[3,2] = -1 / calib.baseline
-    Q[3,3] = calib.doff
-    return Q
-
-def disparity_to_depth(disparity, camera, max_depth=10.0, invalid_value=0.0):
+from . types import Camera
+
+def disparity_to_depth(disparity, camera, invalid_value=0.0):
     """ Calculate depth map from disparity map. Depth values smaller than 0.0
 	    and larger than max_depth are set to invalid_value.
     """
 
     depth = (camera.fx * camera.baseline) / (disparity - camera.doff)
     depth[depth < 0] = invalid_value
-    depth[depth > max_depth] = invalid_value
+    depth[depth > camera.max_depth] = invalid_value
     return depth
 
 def depth_to_disparity(depth, camera, invalid_value=0.0):
     """ Calculate disparity from depth image. Inverse of disparity_to_depth().
     """
-    invalid = depth == 0.0
-    depth[invalid] = 1.0
-    disparity = ((camera.fx * camera.baseline) / depth) + camera.doff
-    disparity[invalid] = invalid_value
+    valid = depth != invalid_value
+    disparity = np.divide((camera.fx * camera.baseline), depth, where=valid) + camera.doff
     return disparity
 
-class Calibration:
-    @staticmethod
-    def from_K(K, size, min_depth=0.0, max_depth=100.0, baseline=0.0, doff=0.0):
-        calib = Camera._make([K[0,0], K[1,1], K[0,2], K[1,2], size[1], size[0],
-                             min_depth, max_depth, baseline, doff])
-        return Calibration(calib, None, None)
-
-    def __init__(self, calib, channel, capabilities):
-        self._calib = calib
-        self._capabilities = capabilities
-
-    def matrix(self):
-        return get_camera_matrix(self._calib)
-
-    def Q(self):
-        return get_Q(self._calib)
-
-    def params(self):
-        return self._calib
-
 def point3d(calib, u, v, d):
     """ Calculate point 3D coordinates
         @param  calib   calibration
diff --git a/SDK/Python/test.py b/SDK/Python/test.py
new file mode 100644
index 000000000..308486e74
--- /dev/null
+++ b/SDK/Python/test.py
@@ -0,0 +1 @@
+from ftl import FTLStreamReader
\ No newline at end of file
diff --git a/SDK/Python/test/test_readwrite.py b/SDK/Python/test/test_readwrite.py
new file mode 100644
index 000000000..030dcb7d0
--- /dev/null
+++ b/SDK/Python/test/test_readwrite.py
@@ -0,0 +1,44 @@
+import unittest
+import tempfile
+import os
+
+from ftl import FTLStreamWriter, FTLStreamReader
+from ftl.types import Channel, Camera, FTLException
+
+import numpy as np
+import cv2
+
+class TestStreamWriter(unittest.TestCase):
+
+    def test_read_write_frames_uint8_1080p(self):
+        """ Write calibration and random 1080p image and then read them """
+
+        f = tempfile.NamedTemporaryFile(suffix=".ftl")
+
+        frames = [
+            (0, Channel.Calibration, Camera(700.0, 700.0, 960.0, 540.0, 1920, 1080, 0.0, 10.0, 0.25, 0.0)),
+            (0, Channel.Colour, (np.random.rand(1080, 1920, 3) * 255).astype(np.uint8))
+        ]
+
+        with FTLStreamWriter(f.name) as writer:
+            for frame in frames:
+                writer.write(*frame)
+
+        with FTLStreamReader(f.name) as reader:
+            for (_, src, channel, data), orig in zip(reader, frames):
+                self.assertEqual(src, orig[0])
+                self.assertEqual(channel, orig[1])
+
+                if channel == Channel.Calibration:
+                    # floating point accuracy can cause issues (writer uses 32
+                    # bit representation while python uses 64 by default)
+                    self.assertEqual(data.camera(), orig[2])
+
+                elif channel == Channel.Colour:
+                    # reader returns color channel as float, while writer
+                    # internally converts it to uint8
+                    im = (data[:,:,0:3] * 255).astype(np.uint8)
+                    self.assertTrue(np.array_equal(im, orig[2]))
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/SDK/Python/test/test_streamwriter.py b/SDK/Python/test/test_streamwriter.py
index b822a739c..cd6b31dee 100644
--- a/SDK/Python/test/test_streamwriter.py
+++ b/SDK/Python/test/test_streamwriter.py
@@ -10,17 +10,18 @@ import numpy as np
 class TestStreamWriter(unittest.TestCase):
 
     def test_create_and_delete_file(self):
-        """ Test constructor and destructor (empty file) """
+        """ Test constructor and destructor """
 
-        with tempfile.NamedTemporaryFile(suffix=".ftl") as f:
-            stream = FTLStreamWriter(f.name)
-            stream.__del__()
+        f = tempfile.NamedTemporaryFile(suffix=".ftl")
+        stream = FTLStreamWriter(f.name)
+        stream.close()
+        stream.__del__()
 
     def test_write_frames_float32_1080p(self):
         """ Write random image, correct types and values """
 
-        with tempfile.NamedTemporaryFile(suffix=".ftl") as f:
-            stream = FTLStreamWriter(f.name)
+        f = tempfile.NamedTemporaryFile(suffix=".ftl")
+        with FTLStreamWriter(f.name) as stream:
             calib = Camera(700.0, 700.0, 960.0, 540.0, 1920, 1080, 0.0, 10.0, 0.20, 0.0)
             im = np.random.rand(1080, 1920, 3).astype(np.float32)
 
@@ -29,29 +30,27 @@ class TestStreamWriter(unittest.TestCase):
 
     def test_write_calib_wrong_compatible_type(self):
         """ Write calibration with incorrect but compatible types (float/int) """
+        f = tempfile.NamedTemporaryFile(suffix=".ftl")
 
-        with tempfile.NamedTemporaryFile(suffix=".ftl") as f:
-            stream = FTLStreamWriter(f.name)
+        with FTLStreamWriter(f.name) as stream:
             calib = Camera(700, 700.0, 960, 540.0, 1920.0, 1080, 0, 10.0, 0.2, 0)
-
             stream.write(0, Channel.Calibration, calib)
 
     def test_write_calib_wrong_incompatible_type(self):
         """ Write calibration with incorrect and incompatible types """
 
-        with tempfile.NamedTemporaryFile(suffix=".ftl") as f:
-            stream = FTLStreamWriter(f.name)
-            calib = Camera("foo", "bar", 960, 540.0, 1920.0, 1080, 0, 10.0, 0.2, 0)
+        f = tempfile.NamedTemporaryFile(suffix=".ftl")
 
+        with FTLStreamWriter(f.name) as stream:
+            calib = Camera("foo", "bar", 960, 540.0, 1920.0, 1080, 0, 10.0, 0.2, 0)
             with self.assertRaises(ValueError):
                 stream.write(0, Channel.Calibration, calib)
 
     def test_empty_nextframe(self):
         """ Call nextframe() on empty stream """
 
-        with tempfile.NamedTemporaryFile(suffix=".ftl") as f:
-            stream = FTLStreamWriter(f.name)
-
+        f = tempfile.NamedTemporaryFile(suffix=".ftl")
+        with FTLStreamWriter(f.name) as stream:
             with self.assertRaises(FTLException):
                 stream.next_frame()
 
diff --git a/SDK/Python/blender.py b/SDK/blender.py
similarity index 96%
rename from SDK/Python/blender.py
rename to SDK/blender.py
index c238dc99b..517f11dec 100644
--- a/SDK/Python/blender.py
+++ b/SDK/blender.py
@@ -259,27 +259,28 @@ class FTL_OT_Operator(bpy.types.Operator):
 
         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)
 
-            depthL = res.depthL
-            depthR = res.depthR
+            imR = np.flip(res.imR, 0)
+            imL = np.flip(res.imL, 0)
+            depthL = np.flip(res.depthL, 0)
+            depthR = np.flip(res.depthR, 0)
 
             if options.depth_eevee and context.scene.render.engine != 'BLENDER_EEVEE':
                 engine = context.scene.render.engine
                 try:
                     context.scene.render.engine = 'BLENDER_EEVEE'
                     res_eevee = render_stereo(camera, baseline)
-                    depthL = res_eevee.depthL
-                    depthR = res_eevee.depthR
+                    depthL = np.flip(res_eevee.depthL, 0)
+                    depthR = np.flip(res_eevee.depthR, 0)
 
                 finally:
                     context.scene.render.engine = engine
 
+            writer.write(i, Channel.Calibration, res.intrinsics)
+            writer.write(i, Channel.Pose, res.pose)
+            writer.write(i, Channel.Left, imL)
+            writer.write(i, Channel.Right, imR)
             writer.write(i, depth_channel, depthL)
-
             if options.mask_occlusions:
                 writer.mask_occlusion(i, depth_channel, depthR)
 
diff --git a/python/.gitignore b/python/.gitignore
deleted file mode 100644
index feae5c192..000000000
--- a/python/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-*.pyc
-__pycache__
\ No newline at end of file
diff --git a/python/README.md b/python/README.md
deleted file mode 100644
index a2918492f..000000000
--- a/python/README.md
+++ /dev/null
@@ -1,89 +0,0 @@
-Python support for `.ftl` files. At the moment, only reading RGB channels
-(left/right) supported. Non-parallel decoding of 8 streams has a frame rate
-of ~15 fps on i7-9700K.
-
-Required **Python** modules:
-
- * msgpack 
- * numpy
- * skimage **or** OpenCV 
-
-Required libraries
-
- * libde265 (available on most Linux distributions)
-
-## Example
-
-Example reads from `input.ftl` and writes to `output.ftl`. Calibration and
-pose are copied directly (same method can be used for other channels as well).
-The program copies left and right frames of source 0 to new file (and re-encodes
-them in JPG) when both frames are available.
-
-```python
-import ftl
-from ftl import types
-
-reader = ftl.FTLStreamReader("./input.ftl")
-writer = ftl.FTLStreamWriter("./output")
-
-source_id = 0
-fps = 25
-frame_t = int(1000.0/fps)
-timestamp_out = 0
-timestamp_in = 0
-
-im_left = None
-im_right = None
-
-while reader.read():
-    channel = reader.get_channel_type()
-    timestamp = reader.get_timestamp()
-    frame = reader.get_frame()
-    
-    if reader.get_source_id() != source_id:
-        # not interested in this source, skip
-        continue
-    
-    if channel in (types.Channel.Calibration, types.Channel.Configuration):
-        # copy calibration and pose (but replace timestamp with new value)
-        
-        sp, p = reader.get_raw()
-        sp = sp._replace(timestamp=timestamp_out)
-        writer.add_raw(sp, p)
-        continue
-
-    if channel not in (types.Channel.Left, types.Channel.Right):
-        # only interested in left and right frame
-        continue
-
-    if frame is None:
-        # no frame if decoding failed
-        continue
-
-    if timestamp_in != timestamp:
-        # new timestamp, process available frames
-
-        if not (im_left is None or im_right is None):
-            # save frames only if both of them were found for this timestamp
-            
-            # Note: In this expample channel is re-encoded. If channel content
-            # is not modified, lossy channels should be passed directly
-            # (using add_raw() in same way as for calibration/pose) instead of 
-            # re-encoding them.
-            
-            writer.add_frame(timestamp_out, 0, types.Channel.Left, 2,
-                             types.codec_t.JPG, im_left)
-            writer.add_frame(timestamp_out, 0, types.Channel.Right, 2,
-                             types.codec_t.JPG, im_right)
-        
-        
-        timestamp_out += frame_t
-        timestamp_in = timestamp
-        im_left, im_right = None, None
-
-    if channel is types.Channel.Left:
-        im_left = frame
-    else:
-        im_right = frame
-
-```
diff --git a/python/ftl/.gitignore b/python/ftl/.gitignore
deleted file mode 100644
index a295864e3..000000000
--- a/python/ftl/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-*.pyc
-__pycache__
diff --git a/python/ftl/__init__.py b/python/ftl/__init__.py
deleted file mode 100644
index 9f7ea874e..000000000
--- a/python/ftl/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from . ftlstreamreader import FTLStreamReader
-#from . ftlstreamwriter import FTLStreamWriter
-
-from . import ftltype as types
diff --git a/python/ftl/ftlstreamwriter.py b/python/ftl/ftlstreamwriter.py
deleted file mode 100644
index 3dfc8903b..000000000
--- a/python/ftl/ftlstreamwriter.py
+++ /dev/null
@@ -1,75 +0,0 @@
-import msgpack
-import struct
-
-from . import ftltype
-
-from codecs import create_encoder
-
-class FTLStreamWriter:
-    def __init__(self, file, version=3):
-        self._file = open(file, "wb")
-        self._file.write(bytes(ord(c) for c in "FTLF")) # magic
-        self._file.write(bytes([version]))              # version
-        self._file.write(bytes([0]*64))                 # reserved
-        self._packer = msgpack.Packer(strict_types=False, use_bin_type=True)
-
-        self._encoders = {}
-        self._channel_count = 0
-
-    def __del__(self):
-        self.close()
-
-    def close(self):
-        self._file.close()
-
-    def add_raw(self, sp, p):
-        if len(sp) != len(ftltype.StreamPacket._fields):
-           raise ValueError("invalid StreamPacket")
-
-        if len(p) != len(ftltype.Packet._fields):
-            raise ValueError("invalid Packet")
-
-        self._file.write(self._packer.pack((sp, p)))
-        self._file.flush()
-
-    def create_encoder(self, source, codec, channel, **kwargs):
-        if channel not in ftltype.Channel:
-            raise ValueError("unknown channel")
-
-        if not isinstance(source, int):
-            raise ValueError("source id must be int")
-
-        if source < 0:
-            raise ValueError("source id must be positive")
-
-        encoder = create_encoder(codec, channel, **kwargs)
-        self._encoders[(int(source), int(channel))] = encoder
-        self._channel_count += 1
-
-    def encode(self, source, timestamp, channel, data):
-        if not isinstance(source, int):
-            raise ValueError("source id must be int")
-
-        if source < 0:
-            raise ValueError("source id must be positive")
-
-        if timestamp < 0:
-            raise ValueError("timestamp must be positive")
-
-        if channel not in ftltype.Channel:
-            raise ValueError("unknown channel")
-
-        try:
-            p = self._encoders[(int(source), int(channel))](data)
-        except KeyError:
-            raise Exception("no encoder found, create_encoder() has to be" +
-                            "called for every source and channel")
-        except Exception as ex:
-            raise Exception("Encoding error:" + str(ex))
-
-        sp = ftltype.StreamPacket._make((timestamp,
-                                         int(source),
-                                         int(channel),
-                                         self._channel_count))
-
-        self.add_raw(sp, p)
diff --git a/python/ftl/ftltype.py b/python/ftl/ftltype.py
deleted file mode 100644
index 9efad0172..000000000
--- a/python/ftl/ftltype.py
+++ /dev/null
@@ -1,92 +0,0 @@
-from collections import namedtuple
-from enum import IntEnum
-
-# components/rgbd-sources/include/ftl/rgbd/camera.hpp
-Camera = namedtuple("Camera", ["fx", "fy", "cx", "cy", "width", "height",
-                                "min_depth", "max_depth", "baseline", "doff"])
-
-# components/codecs/include/ftl/codecs/packet.hpp
-Packet = namedtuple("Packet", ["codec", "definition", "block_total",
-                               "block_number", "flags", "data"])
-
-StreamPacket = namedtuple("StreamPacket", ["timestamp", "streamID",
-                                           "channel_count", "channel"])
-
-class PacketFlags:
-    RGB = 0x00000001
-    MappedDepth = 0x00000002
-
-# components/codecs/include/ftl/codecs/channels.hpp
-class Channel(IntEnum):
-    None_           = -1
-    Colour          = 0
-    Left            = 0
-    Depth           = 1
-    Right           = 2
-    Colour2         = 2
-    Disparity       = 3
-    Depth2          = 3
-    Deviation       = 4
-    Normals         = 5
-    Points          = 6
-    Confidence      = 7
-    Contribution    = 7
-    EnergyVector    = 8
-    Flow            = 9
-    Energy          = 10
-    Mask            = 11
-    Density         = 12
-    LeftGray        = 13
-    RightGray       = 14
-    Overlay1        = 15
-
-    AudioLeft       = 32
-    AudioRight      = 33
-
-    Configuration   = 64
-    Calibration     = 65
-    Pose            = 66
-    Data            = 67
-
-_float_channels = [
-    Channel.Depth,
-    Channel.Confidence,
-    Channel.Density,
-    Channel.Energy
-]
-
-def is_float_channel(channel):
-    return channel in _float_channels
-
-# components/codecs/include/ftl/codecs/codecs.hpp
-class codec_t(IntEnum):
-    JPG = 0
-    PNG = 1
-    H264 = 2
-    HEVC = 3
-    WAV = 4
-    JSON = 100
-    CALIBRATION = 101
-    POSE = 102
-    MSGPACK = 103,
-    STRING = 104,
-    RAW = 105
-
-definition_t = {
-    0 : (7680, 4320),
-    1 : (2160, 3840),
-    2 : (1080, 1920),
-    3 : (720, 1280),
-    4 : (576, 1024),
-    5 : (480, 854),
-    6 : (360, 640),
-    7 : (0, 0),
-    8 : (2056, 1852)
-}
-
-def get_definition(shape):
-	for k, v in definition_t.items():
-		if shape[:2] == v:
-			return k
-
-	return 7 # (None)
-- 
GitLab