From 772cf4ac68ce150be36f848039e185dd81f17497 Mon Sep 17 00:00:00 2001 From: Sebastian Hahta <joseha@utu.fi> Date: Tue, 22 Oct 2019 18:32:29 +0300 Subject: [PATCH] Python module for reading .ftl files --- python/README.md | 12 ++ python/ftl/__init__.py | 1 + python/ftl/ftlstream.py | 246 ++++++++++++++++++++++++++++++++++++++++ python/ftl/libde265.py | 203 +++++++++++++++++++++++++++++++++ 4 files changed, 462 insertions(+) create mode 100644 python/README.md create mode 100644 python/ftl/__init__.py create mode 100644 python/ftl/ftlstream.py create mode 100644 python/ftl/libde265.py diff --git a/python/README.md b/python/README.md new file mode 100644 index 000000000..d4b687fca --- /dev/null +++ b/python/README.md @@ -0,0 +1,12 @@ +Python support for `.ftl` files. At the moment, only reading RGB channels +(left/right) supported. + +Required **Python** modules: + + * msgpack + * numpy + * skimage **or** OpenCV + +Required libraries + + * libde265 (available on most Linux distributions) diff --git a/python/ftl/__init__.py b/python/ftl/__init__.py new file mode 100644 index 000000000..963e374bd --- /dev/null +++ b/python/ftl/__init__.py @@ -0,0 +1 @@ +from . ftlstream import FTLStream \ No newline at end of file diff --git a/python/ftl/ftlstream.py b/python/ftl/ftlstream.py new file mode 100644 index 000000000..7ab436e00 --- /dev/null +++ b/python/ftl/ftlstream.py @@ -0,0 +1,246 @@ +import msgpack + +import numpy as np + +from enum import IntEnum +from collections import namedtuple +from . libde265 import Decoder + +try: + import cv2 as cv + + def _ycrcb2rgb(img): + return cv.cvtColor(img, cv.COLOR_YCrCb2RGB) + +except ImportError: + def _ycrcb2rgb(img): + ''' YCrCb to RGB, based on OpenCV documentation definition. + + Note: It seems this implementation is not perfectly equivalent to OpenCV's + ''' + + rgb = np.zeros(img.shape, np.float) + + Y = img[:,:,0].astype(np.float) + Cr = img[:,:,1].astype(np.float) + Cb = img[:,:,2].astype(np.float) + delta = 128.0 + + rgb[:,:,0] = Y + 1.403 * (Cr - delta) + rgb[:,:,1] = Y - 0.714 * (Cr - delta) - 0.344 * (Cb - delta) + rgb[:,:,2] = Y + 1.773 * (Cb - delta) + + return rgb.round().astype(np.uint8) + +# FTL definitions + +_packet = namedtuple("Packet", ["codec", "definition", "block_total", "block_number", "flags", "data"]) +_stream_packet = namedtuple("StreamPacket", ["timestamp", "streamID", "chanel_count", "channel"]) + +_definition_t = { + 0 : (), + 1 : (), + 2 : (1080, 1920), + 3 : (720, 1280), + 4 : (), + 5 : (), + 6 : (), + 7 : (), + 8 : () +} + +class NALType(IntEnum): + CODED_SLICE_TRAIL_N = 0 + CODED_SLICE_TRAIL_R = 1 + + CODED_SLICE_TSA_N = 2 + CODED_SLICE_TSA_R = 3 + + CODED_SLICE_STSA_N = 4 + CODED_SLICE_STSA_R = 5 + + CODED_SLICE_RADL_N = 6 + CODED_SLICE_RADL_R = 7 + + CODED_SLICE_RASL_N = 8 + CODED_SLICE_RASL_R = 9 + + RESERVED_VCL_N10 = 10 + RESERVED_VCL_R11 = 11 + RESERVED_VCL_N12 = 12 + RESERVED_VCL_R13 = 13 + RESERVED_VCL_N14 = 14 + RESERVED_VCL_R15 = 15 + + CODED_SLICE_BLA_W_LP = 16 + CODED_SLICE_BLA_W_RADL = 17 + CODED_SLICE_BLA_N_LP = 18 + CODED_SLICE_IDR_W_RADL = 19 + CODED_SLICE_IDR_N_LP = 20 + CODED_SLICE_CRA = 21 + RESERVED_IRAP_VCL22 = 22 + RESERVED_IRAP_VCL23 = 23 + + RESERVED_VCL24 = 24 + RESERVED_VCL25 = 25 + RESERVED_VCL26 = 26 + RESERVED_VCL27 = 27 + RESERVED_VCL28 = 28 + RESERVED_VCL29 = 29 + RESERVED_VCL30 = 30 + RESERVED_VCL31 = 31 + + VPS = 32 + SPS = 33 + PPS = 34 + ACCESS_UNIT_DELIMITER = 35 + EOS = 36 + EOB = 37 + FILLER_DATA = 38 + PREFIX_SEI = 39 + SUFFIX_SEI = 40 + + RESERVED_NVCL41 = 41 + RESERVED_NVCL42 = 42 + RESERVED_NVCL43 = 43 + RESERVED_NVCL44 = 44 + RESERVED_NVCL45 = 45 + RESERVED_NVCL46 = 46 + RESERVED_NVCL47 = 47 + UNSPECIFIED_48 = 48 + UNSPECIFIED_49 = 49 + UNSPECIFIED_50 = 50 + UNSPECIFIED_51 = 51 + UNSPECIFIED_52 = 52 + UNSPECIFIED_53 = 53 + UNSPECIFIED_54 = 54 + UNSPECIFIED_55 = 55 + UNSPECIFIED_56 = 56 + UNSPECIFIED_57 = 57 + UNSPECIFIED_58 = 58 + UNSPECIFIED_59 = 59 + UNSPECIFIED_60 = 60 + UNSPECIFIED_61 = 61 + UNSPECIFIED_62 = 62 + UNSPECIFIED_63 = 63 + INVALID = 64 + +def get_NAL_type(data): + if not isinstance(data, bytes): + raise ValueError("expected bytes") + + return NALType((data[4] >> 1) & 0x3f) + +class FTLStream: + def __init__(self, file): + self._file = open(file, "br") + self._decoders = {} + self._frames = {} + + try: + magic = self._file.read(5) + if magic[:4] != bytearray(ord(c) for c in "FTLF"): + raise Exception("wrong magic") + + self._unpacker = msgpack.Unpacker(self._file, raw=True, use_list=False) + + except Exception as ex: + self._file.close() + raise ex + + self._packets_read = 0 + + def __del__(self): + self._file.close() + + def _read_next(self): + v1, v2 = self._unpacker.unpack() + return _stream_packet._make(v1), _packet._make(v2) + + def _update_calib(self, sp, p): + ''' Update calibration ''' + pass + + def _update_pose(self, sp, p): + ''' Update pose ''' + pass + + def _decode_frame_hevc(self, sp, p): + ''' Decode HEVC frame ''' + + k = (sp.streamID, sp.channel) + + if k not in self._decoders: + self._decoders[k] = Decoder(_definition_t[p.definition]) + + decoder = self._decoders[k] + + decoder.push_data(p.data) + decoder.decode() + + img = decoder.get_next_picture() + + if img is not None: + self._frames[k] = _ycrcb2rgb(img) + + def _flush_decoders(self): + for decoder in self._decoders.values(): + decoder.flush_data() + + def read(self): + ''' + Reads data for until the next timestamp. Returns False if there is no + more data to read, otherwise returns True. + ''' + if self._packets_read == 0: + self._sp, self._p = self._read_next() + self._packets_read += 1 + + self._frames = {} + + ts = self._sp.timestamp + ex = None + + while self._sp.timestamp == ts: + try: + if self._p.codec == 100: # JSON + NotImplementedError("json decoding not implemented") + + elif self._p.codec == 101: # CALIBRATION + self._update_calib(self._sp, self._p) + + elif self._p.codec == 102: # POSE + self._update_pose(self._sp, self._p) + + elif self._p.codec == 3: # HEVC + self._decode_frame_hevc(self._sp, self._p) + + else: + raise ValueError("unkowno codec %i" % p.codec) + + except Exception as e: + # TODO: Multiple exceptions possible. Re-design read()? + ex = e + + try: + self._sp, self._p = self._read_next() + self._packets_read += 1 + + except msgpack.OutOfData: + return False + + if ex is not None: + raise ex + + return True + + def get_frames(self): + ''' Returns all frames ''' + return self._frames + + def get_frame(self, source, channel): + k = (source, channel) + if k in self._frames: + return self._frames[k] + else: + return None diff --git a/python/ftl/libde265.py b/python/ftl/libde265.py new file mode 100644 index 000000000..74a1dcedb --- /dev/null +++ b/python/ftl/libde265.py @@ -0,0 +1,203 @@ +''' +Python wrapper for libde265. Only decoding is (partly) implemented. + +Requirements: + * libde265 library (libde265.so.0) + * numpy + * opencv or skimage + +''' + +try: + import cv2 as cv + def _resize(img, size): + return cv.resize(img, dsize=tuple(reversed(size)), interpolation=cv.INTER_CUBIC) + +except ImportError: + from skimage.transform import resize as resize_skimage + def _resize(img, size): + # skimage resize() return dtype float64, convert back to uint8 + # order: 0 nn, 1 bilinear, 3 bicubic + return (resize_skimage(img, size, order=3, mode="constant", cval=0) * 255).astype(np.uint8) + +import ctypes +from enum import IntEnum + +import numpy as np + +# error codes copied from header (de265.h) + +class libde265error(IntEnum): + DE265_OK = 0 + DE265_ERROR_NO_SUCH_FILE=1 + DE265_ERROR_COEFFICIENT_OUT_OF_IMAGE_BOUNDS=4 + DE265_ERROR_CHECKSUM_MISMATCH=5 + DE265_ERROR_CTB_OUTSIDE_IMAGE_AREA=6 + DE265_ERROR_OUT_OF_MEMORY=7 + DE265_ERROR_CODED_PARAMETER_OUT_OF_RANGE=8 + DE265_ERROR_IMAGE_BUFFER_FULL=9 + DE265_ERROR_CANNOT_START_THREADPOOL=10 + DE265_ERROR_LIBRARY_INITIALIZATION_FAILED=11 + DE265_ERROR_LIBRARY_NOT_INITIALIZED=12 + DE265_ERROR_WAITING_FOR_INPUT_DATA=13 + DE265_ERROR_CANNOT_PROCESS_SEI=14 + DE265_ERROR_PARAMETER_PARSING=15 + DE265_ERROR_NO_INITIAL_SLICE_HEADER=16 + DE265_ERROR_PREMATURE_END_OF_SLICE=17 + DE265_ERROR_UNSPECIFIED_DECODING_ERROR=18 + DE265_ERROR_NOT_IMPLEMENTED_YET = 502 + DE265_WARNING_NO_WPP_CANNOT_USE_MULTITHREADING = 1000 + DE265_WARNING_WARNING_BUFFER_FULL=1001 + DE265_WARNING_PREMATURE_END_OF_SLICE_SEGMENT=1002 + DE265_WARNING_INCORRECT_ENTRY_POINT_OFFSET=1003 + DE265_WARNING_CTB_OUTSIDE_IMAGE_AREA=1004 + DE265_WARNING_SPS_HEADER_INVALID=1005 + DE265_WARNING_PPS_HEADER_INVALID=1006 + DE265_WARNING_SLICEHEADER_INVALID=1007 + DE265_WARNING_INCORRECT_MOTION_VECTOR_SCALING=1008 + DE265_WARNING_NONEXISTING_PPS_REFERENCED=1009 + DE265_WARNING_NONEXISTING_SPS_REFERENCED=1010 + DE265_WARNING_BOTH_PREDFLAGS_ZERO=1011 + DE265_WARNING_NONEXISTING_REFERENCE_PICTURE_ACCESSED=1012 + DE265_WARNING_NUMMVP_NOT_EQUAL_TO_NUMMVQ=1013 + DE265_WARNING_NUMBER_OF_SHORT_TERM_REF_PIC_SETS_OUT_OF_RANGE=1014 + DE265_WARNING_SHORT_TERM_REF_PIC_SET_OUT_OF_RANGE=1015 + DE265_WARNING_FAULTY_REFERENCE_PICTURE_LIST=1016 + DE265_WARNING_EOSS_BIT_NOT_SET=1017 + DE265_WARNING_MAX_NUM_REF_PICS_EXCEEDED=1018 + DE265_WARNING_INVALID_CHROMA_FORMAT=1019 + DE265_WARNING_SLICE_SEGMENT_ADDRESS_INVALID=1020 + DE265_WARNING_DEPENDENT_SLICE_WITH_ADDRESS_ZERO=1021 + DE265_WARNING_NUMBER_OF_THREADS_LIMITED_TO_MAXIMUM=1022 + DE265_NON_EXISTING_LT_REFERENCE_CANDIDATE_IN_SLICE_HEADER=1023 + DE265_WARNING_CANNOT_APPLY_SAO_OUT_OF_MEMORY=1024 + DE265_WARNING_SPS_MISSING_CANNOT_DECODE_SEI=1025 + DE265_WARNING_COLLOCATED_MOTION_VECTOR_OUTSIDE_IMAGE_AREA=1026 + +libde265 = ctypes.cdll.LoadLibrary("libde265.so.0") + +libde265.de265_get_error_text.argtypes = [ctypes.c_void_p] +libde265.de265_get_error_text.restype = ctypes.c_char_p +libde265.de265_get_version_number_major.restype = ctypes.c_uint32 +libde265.de265_get_version_number_minor.restype = ctypes.c_uint32 + +libde265.de265_new_decoder.restype = ctypes.c_void_p + +libde265.de265_free_decoder.argtypes = [ctypes.c_void_p] +libde265.de265_free_decoder.restype = ctypes.c_int + +libde265.de265_start_worker_threads.argtypes = [ctypes.c_void_p, ctypes.c_int] +libde265.de265_start_worker_threads.restype = ctypes.c_int + +libde265.de265_push_data.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p] +libde265.de265_push_data.restype = ctypes.c_int + +libde265.de265_push_NAL.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p] +libde265.de265_push_data.restype = ctypes.c_int + +libde265.de265_push_end_of_frame.argtypes = [ctypes.c_void_p] + +libde265.de265_flush_data.argtypes = [ctypes.c_void_p] +libde265.de265_flush_data.restype = ctypes.c_int + +libde265.de265_decode.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_int)] +libde265.de265_decode.restype = ctypes.c_int + +libde265.de265_get_next_picture.argtypes = [ctypes.c_void_p] +libde265.de265_get_next_picture.restype = ctypes.c_void_p + +libde265.de265_get_image_width.argtypes = [ctypes.c_void_p, ctypes.c_int] +libde265.de265_get_image_width.restype = ctypes.c_int + +libde265.de265_get_image_height.argtypes = [ctypes.c_void_p, ctypes.c_int] +libde265.de265_get_image_height.restype = ctypes.c_int + +libde265.de265_get_bits_per_pixel.argtypes = [ctypes.c_void_p, ctypes.c_int] +libde265.de265_get_bits_per_pixel.restype = ctypes.c_int + +libde265.de265_get_image_plane.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.POINTER(ctypes.c_int)] +libde265.de265_get_image_plane.restype = ctypes.POINTER(ctypes.c_char) + +class Decoder: + def __init__(self, size, threads=1): + self._size = size + self._more = ctypes.c_int() + self._out_stride = ctypes.c_int() + self._ctx = libde265.de265_new_decoder() + err = libde265.de265_start_worker_threads(self._ctx, threads) + if err: + raise Exception(self.get_error_str(err)) + + def __del__(self): + libde265.de265_free_decoder(self._ctx) + + def get_error_str(self, code): + return libde265.de265_get_error_text(code).decode("ascii") + + def push_data(self, data): + if not isinstance(data, bytes): + raise ValueError("expected bytes") + + err = libde265.de265_push_data(self._ctx, data, len(data), None, None) + + if err: + raise Exception(self.get_error_str(err)) + + def push_end_of_frame(self): + err = libde265.de265_push_end_of_frame(self._ctx) + + if err: + raise Exception(self.get_error_str(err)) + + def push_NAL(self, data): + if not isinstance(data, bytes): + raise ValueError("expected bytes") + + err = libde265.de265_push_NAL(self._ctx, data, len(data), None, None) + + if err: + raise Exception(self.get_error_str(err)) + + def decode(self): + err = libde265.de265_decode(self._ctx, self._more) + + if err and err != libde265error.DE265_ERROR_WAITING_FOR_INPUT_DATA: + raise Exception(self.get_error_str(err)) + + return self._more.value != 0 + + def flush_data(self): + err = libde265.de265_flush_data(self._ctx) + + if err: + raise Exception(self.get_error_str(err)) + + def get_next_picture(self): + ''' + Returns next decoded frame. Image in YCbCr format. If no frame available + returns None. + ''' + img = libde265.de265_get_next_picture(self._ctx) + + if not img: + return None + + res = np.zeros((self._size[0], self._size[1], 3), dtype=np.uint8) + + for c in range(0, 3): + size = (libde265.de265_get_image_height(img, c), + libde265.de265_get_image_width(img, c)) + + bpp = libde265.de265_get_bits_per_pixel(img, c) + + if bpp != 8: + raise NotImplementedError("unsupported bits per pixel %i" % bpp) + + img_ptr = libde265.de265_get_image_plane(img, c, self._out_stride) + + ch = np.frombuffer(img_ptr[:size[0] * size[1]], dtype=np.uint8) + ch.shape = size + + res[:,:,c] = _resize(ch, self._size) + + return res \ No newline at end of file -- GitLab