diff --git a/.gitignore b/.gitignore index d6eceab..8746a79 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # C extensions *.so +*.c3d # Packages *.egg @@ -41,4 +42,4 @@ docs/_build .vscode/ # mypy -.mypy_cache/ \ No newline at end of file +.mypy_cache/ diff --git a/README.rst b/README.rst index 11242de..1c81c8a 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ data recorded by a 3D motion tracking apparatus. Installing ---------- -Install with pip:: +Install with pip (currently outdated, download package from github instead):: pip install c3d @@ -22,53 +22,52 @@ repository and build and install using the normal Python setup process:: Usage ----- +Documentation and examples are available in the `package documentation`_. + Tools ~~~~~ -This package includes a script for converting C3D motion data to CSV format +This package includes scripts_ for converting C3D motion data to CSV format (``c3d2csv``) and an OpenGL-based visualization tool for observing the motion described by a C3D file (``c3d-viewer``). +.. _scripts: ./scripts + Library ~~~~~~~ -To use the C3D library, just import the package and create a ``Reader`` or +To use the C3D library, just import the package and create a ``Reader`` and/or ``Writer`` depending on your intended usage .. code-block:: python import c3d - with open('data.c3d', 'rb') as handle: - reader = c3d.Reader(handle) - for i, (points, analog) in enumerate(reader.read_frames()): - print('Frame {}: {}'.format(i, points.round(2))) - -You can also get and set metadata fields using the library; see the `package -documentation`_ for more details. - -.. _package documentation: http://c3d.readthedocs.org + with open('my-motion.c3d', 'rb') as file: + reader = c3d.Reader(file) + for i, points, analog in reader.read_frames(): + print('frame {}: point {}, analog {}'.format( + i, points.shape, analog.shape)) -Tests -~~~~~ - -To run tests available in the test folder, following command can be run from the root of the package directory:: - - python -m unittest discover . +The library also provide functionality for editing both frame and metadata fields; +see the `package documentation`_ for more details. +.. _package documentation: https://mattiasfredriksson.github.io/py-c3d/c3d/ Caveats ------- -This library is minimally effective, in the sense that the only motion tracking -system I have access to (for testing) is a Phasespace system. If you try out the -library and find that it doesn't work with your motion tracking system, let me -know. Pull requests are also welcome! +The package is tested against the available `software examples`_ but may still not support +every possible format. For example, parameters serialized in multiple parameters +are not handled automatically (such as a LABELS field stored in both POINT:LABELS and +POINT:LABELS2). Reading and writing files from a big-endian system is also not supported. + +Tests are currently only run on Windows, which means that Linux and Mac users may +experience some issues. If you experience issues with a file or feature, feel free +to post an issue (preferably by including example file/code/python exception) +or make a pull request! -Also, if you're looking for more functionality than just reading and writing C3D -files, there are a lot of better toolkits out there that support a lot more file -formats and provide more functionality, perhaps at the cost of increased -complexity. The `biomechanical toolkit`_ is a good package for analyzing motion -data. +The package is Python only, support for other languages is available in other packages, for example see `ezc3d`_. -.. _biomechanical toolkit: http://code.google.com/p/b-tk/ +.. _software examples: https://www.c3d.org/sampledata.html +.. _ezc3d: https://github.com/pyomeca/ezc3d diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/c3d.py b/c3d.py deleted file mode 100644 index 61dac5d..0000000 --- a/c3d.py +++ /dev/null @@ -1,1585 +0,0 @@ -'''A Python module for reading and writing C3D files.''' - -from __future__ import unicode_literals - -import array -import io -import numpy as np -import struct -import warnings -import codecs - -PROCESSOR_INTEL = 84 -PROCESSOR_DEC = 85 -PROCESSOR_MIPS = 86 - - -class DataTypes(object): - ''' Container defining different data types used for reading file data. - Data types depend on the processor format the file is stored in. - ''' - def __init__(self, proc_type): - self.proc_type = proc_type - if proc_type == PROCESSOR_MIPS: - # Big-Endian (SGI/MIPS format) - self.float32 = np.dtype(np.float32).newbyteorder('>') - self.float64 = np.dtype(np.float64).newbyteorder('>') - self.uint8 = np.uint8 - self.uint16 = np.dtype(np.uint16).newbyteorder('>') - self.uint32 = np.dtype(np.uint32).newbyteorder('>') - self.uint64 = np.dtype(np.uint64).newbyteorder('>') - self.int8 = np.int8 - self.int16 = np.dtype(np.int16).newbyteorder('>') - self.int32 = np.dtype(np.int32).newbyteorder('>') - self.int64 = np.dtype(np.int64).newbyteorder('>') - else: - # Little-Endian format (Intel or DEC format) - self.float32 = np.float32 - self.float64 = np.float64 - self.uint8 = np.uint8 - self.uint16 = np.uint16 - self.uint32 = np.uint32 - self.uint64 = np.uint64 - self.int8 = np.int8 - self.int16 = np.int16 - self.int32 = np.int32 - self.int64 = np.int64 - - @property - def is_ieee(self): - ''' True if the associated file is in the Intel format. - ''' - return self.proc_type == PROCESSOR_INTEL - - @property - def is_dec(self): - ''' True if the associated file is in the DEC format. - ''' - return self.proc_type == PROCESSOR_DEC - - @property - def is_mips(self): - ''' True if the associated file is in the SGI/MIPS format. - ''' - return self.proc_type == PROCESSOR_MIPS - - def decode_string(self, bytes): - ''' Decode a byte array to a string. - ''' - # Attempt to decode using different decoders - decoders = ['utf-8', 'latin-1'] - for dec in decoders: - try: - return codecs.decode(bytes, dec) - except UnicodeDecodeError: - continue - # Revert to using default decoder but replace characters - return codecs.decode(bytes, decoders[0], 'replace') - - -def UNPACK_FLOAT_IEEE(uint_32): - '''Unpacks a single 32 bit unsigned int to a IEEE float representation - ''' - return struct.unpack('f', struct.pack("I", uint_32))[0] - - -def DEC_to_IEEE(uint_32): - '''Convert the 32 bit representation of a DEC float to IEEE format. - - Params: - ---- - uint_32 : 32 bit unsigned integer containing the DEC single precision float point bits. - Returns : IEEE formated floating point of the same shape as the input. - ''' - # Follows the bit pattern found: - # http://home.fnal.gov/~yang/Notes/ieee_vs_dec_float.txt - # Further formating descriptions can be found: - # http://www.irig106.org/docs/106-07/appendixO.pdf - # In accodance with the first ref. first & second 16 bit words are placed - # in a big endian 16 bit word representation, and needs to be inverted. - # Second reference describe the DEC->IEEE conversion. - - # Warning! Unsure if NaN numbers are managed appropriately. - - # Shuffle the first two bit words from DEC bit representation to an ordered representation. - # Note that the most significant fraction bits are placed in the first 7 bits. - # - # Below are the DEC layout in accordance with the references: - # ___________________________________________________________________________________ - # | Mantissa (16:0) | SIGN | Exponent (8:0) | Mantissa (23:17) | - # ___________________________________________________________________________________ - # |32- -16| 15 |14- -7|6- -0| - # - # Legend: - # _______________________________________________________ - # | Part (left bit of segment : right bit) | Part | .. - # _______________________________________________________ - # |Bit adress - .. - Bit adress | Bit adress - .. - #### - - # Swap the first and last 16 bits for a consistent alignment of the fraction - reshuffled = ((uint_32 & 0xFFFF0000) >> 16) | ((uint_32 & 0x0000FFFF) << 16) - # After the shuffle each part are in little-endian and ordered as: SIGN-Exponent-Fraction - exp_bits = ((reshuffled & 0xFF000000) - 1) & 0xFF000000 - reshuffled = (reshuffled & 0x00FFFFFF) | exp_bits - return UNPACK_FLOAT_IEEE(reshuffled) - - -def DEC_to_IEEE_BYTES(bytes): - '''Convert byte array containing 32 bit DEC floats to IEEE format. - - Params: - ---- - bytes : Byte array where every 4 bytes represent a single precision DEC float. - Returns : IEEE formated floating point of the same shape as the input. - ''' - - # See comments in DEC_to_IEEE() for DEC format definition - - # Reshuffle - bytes = np.frombuffer(bytes, dtype=np.dtype('B')) - reshuffled = np.empty(len(bytes), dtype=np.dtype('B')) - reshuffled[0::4] = bytes[2::4] - reshuffled[1::4] = bytes[3::4] - reshuffled[2::4] = bytes[0::4] - reshuffled[3::4] = bytes[1::4] + ((bytes[1::4] & 0x7f == 0) - 1) # Decrement exponent by 2, if exp. > 1 - - # There are different ways to adjust for differences in DEC/IEEE representation - # after reshuffle. Two simple methods are: - # 1) Decrement exponent bits by 2, then convert to IEEE. - # 2) Convert to IEEE directly and divide by four. - # 3) Handle edge cases, expensive in python... - # However these are simple methods, and do not accurately convert when: - # 1) Exponent < 2 (without bias), impossible to decrement exponent without adjusting fraction/mantissa. - # 2) Exponent == 0, DEC numbers are then 0 or undefined while IEEE is not. NaN are produced when exponent == 255. - # Here method 1) is used, which mean that only small numbers will be represented incorrectly. - - return np.frombuffer(reshuffled.tobytes(), - dtype=np.float32, - count=int(len(bytes) / 4)) - - -class Header(object): - '''Header information from a C3D file. - - Attributes - ---------- - event_block : int - Index of the 512-byte block where labels (metadata) are found. - parameter_block : int - Index of the 512-byte block where parameters (metadata) are found. - data_block : int - Index of the 512-byte block where data starts. - point_count : int - Number of motion capture channels recorded in this file. - analog_count : int - Number of analog values recorded per frame of 3D point data. - first_frame : int - Index of the first frame of data. - last_frame : int - Index of the last frame of data. - analog_per_frame : int - Number of analog frames per frame of 3D point data. The analog frame - rate (ANALOG:RATE) apparently equals the point frame rate (POINT:RATE) - times this value. - frame_rate : float - The frame rate of the recording, in frames per second. - scale_factor : float - Multiply values in the file by this scale parameter. - long_event_labels : bool - max_gap : int - - .. note:: - The ``scale_factor`` attribute is not used in Phasespace C3D files; - instead, use the POINT.SCALE parameter. - - .. note:: - The ``first_frame`` and ``last_frame`` header attributes are not used in - C3D files generated by Phasespace. Instead, the first and last - frame numbers are stored in the POINTS:ACTUAL_START_FIELD and - POINTS:ACTUAL_END_FIELD parameters. - ''' - - # Read/Write header formats, read values as unsigned ints rather then floats. - BINARY_FORMAT_WRITE = ' 0 - self.event_timings[i] = float_unpack(struct.unpack(unpack_fmt, time_bytes[ilong:ilong+4])[0]) - self.event_labels[i] = dtypes.decode_string(label_bytes[ilong:ilong+4]) - - @property - def events(self): - ''' Get an iterable over displayed events defined in the header. Iterable items are on form (timing, label). - - Note*: - Time as defined by the 'timing' is relative to frame 1 and not the 'first_frame' parameter. - Frame 1 therefor has the time 0.0 in relation to the event timing. - ''' - return zip(self.event_timings[self.event_disp_flags], self.event_labels[self.event_disp_flags]) - - -class Param(object): - '''A class representing a single named parameter from a C3D file. - - Attributes - ---------- - name : str - Name of this parameter. - dtype: DataTypes - Reference to the DataTypes object associated with the file. - desc : str - Brief description of this parameter. - bytes_per_element : int, optional - For array data, this describes the size of each element of data. For - string data (including arrays of strings), this should be -1. - dimensions : list of int - For array data, this describes the dimensions of the array, stored in - column-major order. For arrays of strings, the dimensions here will be - the number of columns (length of each string) followed by the number of - rows (number of strings). - bytes : str - Raw data for this parameter. - handle : - File handle positioned at the first byte of a .c3d parameter description. - ''' - - def __init__(self, - name, - dtype, - desc='', - bytes_per_element=1, - dimensions=None, - bytes=b'', - handle=None): - '''Set up a new parameter, only the name is required.''' - self.name = name - self.dtype = dtype - self.desc = desc - self.bytes_per_element = bytes_per_element - self.dimensions = dimensions or [] - self.bytes = bytes - if handle: - self.read(handle) - - def __repr__(self): - return ''.format(self.desc) - - @property - def num_elements(self): - '''Return the number of elements in this parameter's array value.''' - e = 1 - for d in self.dimensions: - e *= d - return e - - @property - def total_bytes(self): - '''Return the number of bytes used for storing this parameter's data.''' - return self.num_elements * abs(self.bytes_per_element) - - def binary_size(self): - '''Return the number of bytes needed to store this parameter.''' - return ( - 1 + # group_id - 2 + # next offset marker - 1 + len(self.name.encode('utf-8')) + # size of name and name bytes - 1 + # data size - # size of dimensions and dimension bytes - 1 + len(self.dimensions) + - self.total_bytes + # data - 1 + len(self.desc.encode('utf-8')) # size of desc and desc bytes - ) - - def write(self, group_id, handle): - '''Write binary data for this parameter to a file handle. - - Parameters - ---------- - group_id : int - The numerical ID of the group that holds this parameter. - handle : file handle - An open, writable, binary file handle. - ''' - name = self.name.encode('utf-8') - handle.write(struct.pack('bb', len(name), group_id)) - handle.write(name) - handle.write(struct.pack('= 4: - # Check if float value representation is an integer - value = self.float_value - if int(value) == value: - return value - return self.uint32_value - elif self.total_bytes >= 2: - return self.uint16_value - else: - return self.uint8_value - - @property - def int8_value(self): - '''Get the param as an 8-bit signed integer.''' - return self._as(self.dtype.int8) - - @property - def uint8_value(self): - '''Get the param as an 8-bit unsigned integer.''' - return self._as(self.dtype.uint8) - - @property - def int16_value(self): - '''Get the param as a 16-bit signed integer.''' - return self._as(self.dtype.int16) - - @property - def uint16_value(self): - '''Get the param as a 16-bit unsigned integer.''' - return self._as(self.dtype.uint16) - - @property - def int32_value(self): - '''Get the param as a 32-bit signed integer.''' - return self._as(self.dtype.int32) - - @property - def uint32_value(self): - '''Get the param as a 32-bit unsigned integer.''' - return self._as(self.dtype.uint32) - - @property - def float_value(self): - '''Get the param as a 32-bit float.''' - if self.dtype.is_dec: - return DEC_to_IEEE(self._as(np.uint32)) - else: # is_mips or is_ieee - return self._as(self.dtype.float32) - - @property - def bytes_value(self): - '''Get the param as a raw byte string.''' - return self.bytes - - @property - def string_value(self): - '''Get the param as a unicode string.''' - return self.dtype.decode_string(self.bytes) - - @property - def int8_array(self): - '''Get the param as an array of 8-bit signed integers.''' - return self._as_array(self.dtype.int8) - - @property - def uint8_array(self): - '''Get the param as an array of 8-bit unsigned integers.''' - return self._as_array(self.dtype.uint8) - - @property - def int16_array(self): - '''Get the param as an array of 16-bit signed integers.''' - return self._as_array(self.dtype.int16) - - @property - def uint16_array(self): - '''Get the param as an array of 16-bit unsigned integers.''' - return self._as_array(self.dtype.uint16) - - @property - def int32_array(self): - '''Get the param as an array of 32-bit signed integers.''' - return self._as_array(self.dtype.int32) - - @property - def uint32_array(self): - '''Get the param as an array of 32-bit unsigned integers.''' - return self._as_array(self.dtype.uint32) - - @property - def float_array(self): - '''Get the param as an array of 32-bit floats.''' - # Convert float data if not IEEE processor - if self.dtype.is_dec: - # _as_array but for DEC - assert self.dimensions, \ - '{}: cannot get value as {} array!'.format(self.name, self.dtype.float32) - return DEC_to_IEEE_BYTES(self.bytes).reshape(self.dimensions[::-1]) # Reverse fortran format - else: # is_ieee or is_mips - return self._as_array(self.dtype.float32) - - @property - def bytes_array(self): - '''Get the param as an array of raw byte strings.''' - # Decode different dimensions - if len(self.dimensions) == 0: - return np.array([]) - elif len(self.dimensions) == 1: - return np.array(self.bytes) - else: - # Convert Fortran shape (data in memory is identical, shape is transposed) - word_len = self.dimensions[0] - dims = self.dimensions[1:][::-1] # Identical to: [:0:-1] - byte_steps = np.cumprod(self.dimensions[:-1])[::-1] - # Generate mult-dimensional array and parse byte words - byte_arr = np.empty(dims, dtype=object) - for i in np.ndindex(*dims): - # Calculate byte offset as sum of each array index times the byte step of each dimension. - off = np.sum(np.multiply(i, byte_steps)) - byte_arr[i] = self.bytes[off:off+word_len] - return byte_arr - - @property - def string_array(self): - '''Get the param as a python array of unicode strings.''' - # Decode different dimensions - if len(self.dimensions) == 0: - return np.array([]) - elif len(self.dimensions) == 1: - return np.array([self.string_value]) - else: - # Parse byte sequences - byte_arr = self.bytes_array - # Decode sequences - for i in np.ndindex(byte_arr.shape): - byte_arr[i] = self.dtype.decode_string(byte_arr[i]) - return byte_arr - - -class Group(object): - '''A group of parameters from a C3D file. - - In C3D files, parameters are organized in groups. Each group has a name, a - description, and a set of named parameters. - - Attributes - ---------- - name : str - Name of this parameter group. - desc : str - Description for this parameter group. - ''' - - def __init__(self, name=None, desc=None): - self.name = name - self.desc = desc - self.params = {} - - def __repr__(self): - return ''.format(self.desc) - - def get(self, key, default=None): - '''Get a parameter by key. - - Parameters - ---------- - key : any - Parameter key to look up in this group. - default : any, optional - Value to return if the key is not found. Defaults to None. - - Returns - ------- - param : :class:`Param` - A parameter from the current group. - ''' - return self.params.get(key, default) - - def add_param(self, name, dtypes, **kwargs): - '''Add a parameter to this group. - - Parameters - ---------- - name : str - Name of the parameter to add to this group. The name will - automatically be case-normalized. - dtypes : DataTypes - Object struct containing the data types used for reading parameter data. - - Additional keyword arguments will be passed to the `Param` constructor. - ''' - self.params[name.upper()] = Param(name.upper(), dtypes, **kwargs) - - def binary_size(self): - '''Return the number of bytes to store this group and its parameters.''' - return ( - 1 + # group_id - 1 + len(self.name.encode('utf-8')) + # size of name and name bytes - 2 + # next offset marker - 1 + len(self.desc.encode('utf-8')) + # size of desc and desc bytes - sum(p.binary_size() for p in self.params.values())) - - def write(self, group_id, handle): - '''Write this parameter group, with parameters, to a file handle. - - Parameters - ---------- - group_id : int - The numerical ID of the group. - handle : file handle - An open, writable, binary file handle. - ''' - name = self.name.encode('utf-8') - desc = self.desc.encode('utf-8') - handle.write(struct.pack('bb', len(name), -group_id)) - handle.write(name) - handle.write(struct.pack(' 0: - check_parameters(('POINT:LABELS', 'POINT:DESCRIPTIONS')) - else: - warnings.warn('No point data found in file.') - if self.analog_used > 0: - check_parameters(('ANALOG:LABELS', 'ANALOG:DESCRIPTIONS')) - else: - warnings.warn('No analog data found in file.') - - def add_group(self, group_id, name, desc): - '''Add a new parameter group. - - Parameters - ---------- - group_id : int - The numeric ID for a group to check or create. - name : str, optional - If a group is created, assign this name to the group. - desc : str, optional - If a group is created, assign this description to the group. - - Returns - ------- - group : :class:`Group` - A group with the given ID, name, and description. - - Raises - ------ - KeyError - If a group with a duplicate ID or name already exists. - ''' - if group_id in self.groups: - raise KeyError(group_id) - name = name.upper() - if name in self.groups: - raise KeyError(name) - group = self.groups[name] = self.groups[group_id] = Group(name, desc) - return group - - def get(self, group, default=None): - '''Get a group or parameter. - - Parameters - ---------- - group : str - If this string contains a period (.), then the part before the - period will be used to retrieve a group, and the part after the - period will be used to retrieve a parameter from that group. If this - string does not contain a period, then just a group will be - returned. - default : any - Return this value if the named group and parameter are not found. - - Returns - ------- - value : :class:`Group` or :class:`Param` - Either a group or parameter with the specified name(s). If neither - is found, returns the default value. - ''' - if isinstance(group, int): - return self.groups.get(group, default) - group = group.upper() - param = None - if '.' in group: - group, param = group.split('.', 1) - if ':' in group: - group, param = group.split(':', 1) - if group not in self.groups: - return default - group = self.groups[group] - if param is not None: - return group.get(param, default) - return group - - def get_int8(self, key): - '''Get a parameter value as an 8-bit signed integer.''' - return self.get(key).int8_value - - def get_uint8(self, key): - '''Get a parameter value as an 8-bit unsigned integer.''' - return self.get(key).uint8_value - - def get_int16(self, key): - '''Get a parameter value as a 16-bit signed integer.''' - return self.get(key).int16_value - - def get_uint16(self, key): - '''Get a parameter value as a 16-bit unsigned integer.''' - return self.get(key).uint16_value - - def get_int32(self, key): - '''Get a parameter value as a 32-bit signed integer.''' - return self.get(key).int32_value - - def get_uint32(self, key): - '''Get a parameter value as a 32-bit unsigned integer.''' - return self.get(key).uint32_value - - def get_float(self, key): - '''Get a parameter value as a 32-bit float.''' - return self.get(key).float_value - - def get_bytes(self, key): - '''Get a parameter value as a byte string.''' - return self.get(key).bytes_value - - def get_string(self, key): - '''Get a parameter value as a string.''' - return self.get(key).string_value - - def parameter_blocks(self): - '''Compute the size (in 512B blocks) of the parameter section.''' - bytes = 4. + sum(g.binary_size() for g in self.groups.values()) - return int(np.ceil(bytes / 512)) - - @property - def point_rate(self): - ''' Number of sampled 3D coordinates per second. - ''' - try: - return self.get_float('POINT:RATE') - except AttributeError: - return self.header.frame_rate - - @property - def point_scale(self): - try: - return self.get_float('POINT:SCALE') - except AttributeError: - return self.header.scale_factor - - @property - def point_used(self): - ''' Number of sampled 3D point coordinates per frame. - ''' - try: - return self.get_uint16('POINT:USED') - except AttributeError: - return self.header.point_count - - @property - def analog_used(self): - ''' Number of analog measurements, or channels, for each analog data sample. - ''' - try: - return self.get_uint16('ANALOG:USED') - except AttributeError: - return self.header.analog_count - - @property - def analog_rate(self): - ''' Number of analog data samples per second. - ''' - try: - return self.get_float('ANALOG:RATE') - except AttributeError: - return self.header.analog_per_frame * self.point_rate - - @property - def analog_per_frame(self): - ''' Number of analog samples per 3D frame (point sample). - ''' - return int(self.analog_rate / self.point_rate) - - @property - def analog_sample_count(self): - ''' Number of analog samples per channel. - ''' - has_analog = self.analog_used > 0 - return int(self.frame_count * self.analog_per_frame) * has_analog - - @property - def point_labels(self): - return self.get('POINT:LABELS').string_array - - @property - def analog_labels(self): - return self.get('ANALOG:LABELS').string_array - - @property - def frame_count(self): - return self.last_frame - self.first_frame + 1 # Add 1 since range is inclusive [first, last] - - @property - def first_frame(self): - # Start frame seems to be less of an issue to determine. - # this is a hack for phasespace files ... should put it in a subclass. - param = self.get('TRIAL:ACTUAL_START_FIELD') - if param is not None: - return param.uint32_value - return self.header.first_frame - - @property - def last_frame(self): - # Number of frames can be represented in many formats, first check if valid header values - if self.header.first_frame < self.header.last_frame and self.header.last_frame != 65535: - return self.header.last_frame - - # Check different parameter options where the frame can be encoded - end_frame = [self.header.last_frame, 0.0, 0.0, 0.0] - param = self.get('TRIAL:ACTUAL_END_FIELD') - if param is not None: - end_frame[1] = param._as_integer_value - param = self.get('POINT:LONG_FRAMES') - if param is not None: - end_frame[2] = param._as_integer_value - param = self.get('POINT:FRAMES') - if param is not None: - # Can be encoded either as 32 bit float or 16 bit uint - end_frame[3] = param._as_integer_value - # Return the largest of the all (queue bad reading...) - return int(np.max(end_frame)) - - -class Reader(Manager): - '''This class provides methods for reading the data in a C3D file. - - A C3D file contains metadata and frame-based data describing 3D motion. - - You can iterate over the frames in the file by calling `read_frames()` after - construction: - - >>> r = c3d.Reader(open('capture.c3d', 'rb')) - >>> for frame_no, points, analog in r.read_frames(): - ... print('{0.shape} points in this frame'.format(points)) - ''' - - def __init__(self, handle): - '''Initialize this C3D file by reading header and parameter data. - - Parameters - ---------- - handle : file handle - Read metadata and C3D motion frames from the given file handle. This - handle is assumed to be `seek`-able and `read`-able. The handle must - remain open for the life of the `Reader` instance. The `Reader` does - not `close` the handle. - - Raises - ------ - ValueError - If the processor metadata in the C3D file is anything other than 84 - (Intel format). - ''' - super(Reader, self).__init__(Header(handle)) - - self._handle = handle - - def seek_param_section_header(): - ''' Seek to and read the first 4 byte of the parameter header section ''' - self._handle.seek((self.header.parameter_block - 1) * 512) - # metadata header - return self._handle.read(4) - - # Begin by reading the processor type: - buf = seek_param_section_header() - _, _, parameter_blocks, self.processor = struct.unpack('BBBB', buf) - self.dtypes = DataTypes(self.processor) - # Convert header parameters in accordance with the processor type (MIPS format re-reads the header) - self.header.processor_convert(self.dtypes, handle) - - # Restart reading the parameter header after parsing processor type - buf = seek_param_section_header() - is_mips = self.processor == PROCESSOR_MIPS - - start_byte = self._handle.tell() - endbyte = start_byte + 512 * parameter_blocks - 4 - while self._handle.tell() < endbyte: - chars_in_name, group_id = struct.unpack('bb', self._handle.read(2)) - if group_id == 0 or chars_in_name == 0: - # we've reached the end of the parameter section. - break - name = self.dtypes.decode_string(self._handle.read(abs(chars_in_name))).upper() - - # Read the byte segment associated with the parameter and create a - # separate binary stream object from the data. - offset_to_next, = struct.unpack(['h'][is_mips], self._handle.read(2)) - if offset_to_next == 0: - # Last parameter, as number of bytes are unknown, - # read the remaining bytes in the parameter section. - bytes = self._handle.read(endbyte - self._handle.tell()) - else: - bytes = self._handle.read(offset_to_next - 2) - buf = io.BytesIO(bytes) - - if group_id > 0: - # we've just started reading a parameter. if its group doesn't - # exist, create a blank one. add the parameter to the group. - self.groups.setdefault( - group_id, Group()).add_param(name, self.dtypes, handle=buf) - else: - # we've just started reading a group. if a group with the - # appropriate id exists already (because we've already created - # it for a parameter), just set the name of the group. - # otherwise, add a new group. - group_id = abs(group_id) - size, = struct.unpack('B', buf.read(1)) - desc = size and buf.read(size) or '' - group = self.get(group_id) - if group is not None: - group.name = name - group.desc = desc - self.groups[name] = group - else: - self.add_group(group_id, name, desc) - - self.check_metadata() - - def read_frames(self, copy=True): - '''Iterate over the data frames from our C3D file handle. - - Parameters - ---------- - copy : bool - If False, the reader returns a reference to the same data buffers - for every frame. The default is True, which causes the reader to - return a unique data buffer for each frame. Set this to False if you - consume frames as you iterate over them, or True if you store them - for later. - - Returns - ------- - frames : sequence of (frame number, points, analog) - This method generates a sequence of (frame number, points, analog) - tuples, one tuple per frame. The first element of each tuple is the - frame number. The second is a numpy array of parsed, 5D point data - and the third element of each tuple is a numpy array of analog - values that were recorded during the frame. (Often the analog data - are sampled at a higher frequency than the 3D point data, resulting - in multiple analog frames per frame of point data.) - - The first three columns in the returned point data are the (x, y, z) - coordinates of the observed motion capture point. The fourth column - is an estimate of the error for this particular point, and the fifth - column is the number of cameras that observed the point in question. - Both the fourth and fifth values are -1 if the point is considered - to be invalid. - ''' - # Point magnitude scalar, if scale parameter is < 0 data is floating point - # (in which case the magnitude is the absolute value) - scale_mag = abs(self.point_scale) - is_float = self.point_scale < 0 - - if is_float: - point_word_bytes = 4 - point_dtype = self.dtypes.uint32 - else: - point_word_bytes = 2 - point_dtype = self.dtypes.int16 - points = np.zeros((self.point_used, 5), np.float32) - - # TODO: handle ANALOG:BITS parameter here! - p = self.get('ANALOG:FORMAT') - analog_unsigned = p and p.string_value.strip().upper() == 'UNSIGNED' - if is_float: - analog_dtype = self.dtypes.float32 - analog_word_bytes = 4 - elif analog_unsigned: - # Note*: Floating point is 'always' defined for both analog and point data, according to the standard. - analog_dtype = self.dtypes.uint16 - analog_word_bytes = 2 - # Verify BITS parameter for analog - p = self.get('ANALOG:BITS') - if p and p._as_integer_value / 8 != analog_word_bytes: - raise NotImplementedError('Analog data using {} bits is not supported.'.format(p._as_integer_value)) - else: - analog_dtype = self.dtypes.int16 - analog_word_bytes = 2 - - analog = np.array([], float) - offsets = np.zeros((self.analog_used, 1), int) - param = self.get('ANALOG:OFFSET') - if param is not None: - offsets = param.int16_array[:self.analog_used, None] - - analog_scales = np.ones((self.analog_used, 1), float) - param = self.get('ANALOG:SCALE') - if param is not None: - analog_scales[:, :] = param.float_array[:self.analog_used, None] - - gen_scale = 1. - param = self.get('ANALOG:GEN_SCALE') - if param is not None: - gen_scale = param.float_value - - # Seek to the start point of the data blocks - self._handle.seek((self.header.data_block - 1) * 512) - # Number of values (words) read in regard to POINT/ANALOG data - N_point = 4 * self.point_used - N_analog = self.analog_used * self.analog_per_frame - # Total bytes per frame - point_bytes = N_point * point_word_bytes - analog_bytes = N_analog * analog_word_bytes - # Parse the data blocks - for frame_no in range(self.first_frame, self.last_frame + 1): - # Read the byte data (used) for the block - raw_bytes = self._handle.read(N_point * point_word_bytes) - raw_analog = self._handle.read(N_analog * analog_word_bytes) - # Verify read pointers (any of the two can be assumed to be 0) - if len(raw_bytes) < point_bytes: - warnings.warn('''reached end of file (EOF) while reading POINT data at frame index {} - and file pointer {}!'''.format(frame_no - self.first_frame, self._handle.tell())) - return - if len(raw_analog) < analog_bytes: - warnings.warn('''reached end of file (EOF) while reading POINT data at frame index {} - and file pointer {}!'''.format(frame_no - self.first_frame, self._handle.tell())) - return - - if is_float: - # Convert every 4 byte words to a float-32 reprensentation - # (the fourth column is still not a float32 representation) - if self.processor == PROCESSOR_DEC: - # Convert each of the first 6 16-bit words from DEC to IEEE float - points[:, :4] = DEC_to_IEEE_BYTES(raw_bytes).reshape((self.point_used, 4)) - else: # If IEEE or MIPS: - # Re-read the raw byte representation directly - points[:, :4] = np.frombuffer(raw_bytes, - dtype=self.dtypes.float32, - count=N_point).reshape((int(self.point_used), 4)) - - # Parse the camera-observed bits and residuals. - # Notes: - # - Invalid sample if residual is equal to -1. - # - A residual of 0.0 represent modeled data (filtered or interpolated). - # - The same format should be used internally when a float or integer representation is used, - # with the difference that the words are 16 and 8 bit respectively (see the MLS guide). - # - While words are 16 bit, residual and camera mask is always interpreted as 8 packed in a single word! - # - 16 or 32 bit may represent a sign (indication that certain files write a -1 floating point only) - last_word = points[:, 3].astype(np.int32) - valid = (last_word & 0x80008000) == 0 - points[~valid, 3:5] = -1.0 - c = last_word[valid] - - else: - # Convert the bytes to a unsigned 32 bit or signed 16 bit representation - raw = np.frombuffer(raw_bytes, - dtype=point_dtype, - count=N_point).reshape((self.point_used, 4)) - # Read point 2 byte words in int-16 format - points[:, :3] = raw[:, :3] * scale_mag - - # Parse last 16-bit word as two 8-bit words - valid = raw[:, 3] > -1 - points[~valid, 3:5] = -1 - c = raw[valid, 3].astype(self.dtypes.uint16) - - # Convert coordinate data - # fourth value is floating-point (scaled) error estimate (residual) - points[valid, 3] = (c & 0xff).astype(np.float32) * scale_mag - - # fifth value is number of bits set in camera-observation byte - points[valid, 4] = sum((c & (1 << k)) >> k for k in range(8, 15)) - # Get value as is: points[valid, 4] = (c >> 8) - - # Check if analog data exist, and parse if so - if N_analog > 0: - if is_float and self.processor == PROCESSOR_DEC: - # Convert each of the 16-bit words from DEC to IEEE float - analog = DEC_to_IEEE_BYTES(raw_analog) - else: - # Integer or INTEL/MIPS floating point data can be parsed directly - analog = np.frombuffer(raw_analog, dtype=analog_dtype, count=N_analog) - - # Reformat and convert - analog = analog.reshape((-1, self.analog_used)).T - analog = analog.astype(float) - # Convert analog - analog = (analog - offsets) * analog_scales * gen_scale - - # Output buffers - if copy: - yield frame_no, points.copy(), analog # .copy(), a new array is generated per frame for analog data. - else: - yield frame_no, points, analog - - # Function evaluating EOF, note that data section is written in blocks of 512 - final_byte_index = self._handle.tell() - self._handle.seek(0, 2) # os.SEEK_END) - # Check if more then 1 block remain - if self._handle.tell() - final_byte_index >= 512: - warnings.warn('incomplete reading of data blocks. {} bytes remained after all datablocks were read!'.format( - self._handle.tell() - final_byte_index)) - - @property - def proc_type(self): - """ - Get the processory type associated with the data format in the file. - """ - processor_type = ['PROCESSOR_INTEL', 'PROCESSOR_DEC', 'PROCESSOR_MIPS'] - return processor_type[self.processor-PROCESSOR_INTEL] - - -class Writer(Manager): - '''This class writes metadata and frames to a C3D file. - - For example, to read an existing C3D file, apply some sort of data - processing to the frames, and write out another C3D file:: - - >>> r = c3d.Reader(open('data.c3d', 'rb')) - >>> w = c3d.Writer() - >>> w.add_frames(process_frames_somehow(r.read_frames())) - >>> with open('smoothed.c3d', 'wb') as handle: - >>> w.write(handle) - - Parameters - ---------- - point_rate : float, optional - The frame rate of the data. Defaults to 480. - analog_rate : float, optional - The number of analog samples per frame. Defaults to 0. - point_scale : float, optional - The scale factor for point data. Defaults to -1 (i.e., "check the - POINT:SCALE parameter"). - point_units : str, optional - The units that the point numbers represent. Defaults to ``'mm '``. - gen_scale : float, optional - General scaling factor for data. Defaults to 1. - ''' - - def __init__(self, - point_rate=480., - analog_rate=0., - point_scale=-1., - point_units='mm ', - gen_scale=1.): - '''Set metadata for this writer. - - ''' - super(Writer, self).__init__() - self._point_rate = point_rate - self._analog_rate = analog_rate - self._analog_per_frame = analog_rate / point_rate - self._point_scale = point_scale - self._point_units = point_units - self._gen_scale = gen_scale - self._frames = [] - - def add_frames(self, frames): - '''Add frames to this writer instance. - - Parameters - ---------- - frames : sequence of (point, analog) tuples - A sequence of frame data to add to the writer. - ''' - self._frames.extend(frames) - - def _pad_block(self, handle): - '''Pad the file with 0s to the end of the next block boundary.''' - extra = handle.tell() % 512 - if extra: - handle.write(b'\x00' * (512 - extra)) - - def _write_metadata(self, handle): - '''Write metadata to a file handle. - - Parameters - ---------- - handle : file - Write metadata and C3D motion frames to the given file handle. The - writer does not close the handle. - ''' - self.check_metadata() - - # header - self.header.write(handle) - self._pad_block(handle) - assert handle.tell() == 512 - - # groups - handle.write(struct.pack( - 'BBBB', 0, 0, self.parameter_blocks(), PROCESSOR_INTEL)) - id_groups = sorted( - (i, g) for i, g in self.groups.items() if isinstance(i, int)) - for group_id, group in id_groups: - group.write(group_id, handle) - - # padding - self._pad_block(handle) - while handle.tell() != 512 * (self.header.data_block - 1): - handle.write(b'\x00' * 512) - - def _write_frames(self, handle): - '''Write our frame data to the given file handle. - - Parameters - ---------- - handle : file - Write metadata and C3D motion frames to the given file handle. The - writer does not close the handle. - ''' - assert handle.tell() == 512 * (self.header.data_block - 1) - scale = abs(self.point_scale) - is_float = self.point_scale < 0 - if is_float: - point_dtype = np.float32 - point_format = 'f' - point_scale = 1.0 - else: - point_dtype = np.int16 - point_format = 'i' - point_scale = scale - raw = np.empty((self.point_used, 4), point_dtype) - for points, analog in self._frames: - valid = points[:, 3] > -1 - raw[~valid, 3] = -1 - raw[valid, :3] = points[valid, :3] / point_scale - raw[valid, 3] = ( - ((points[valid, 4]).astype(np.uint8) << 8) | - (points[valid, 3] / scale).astype(np.uint16) - ) - point = array.array(point_format) - point.extend(raw.flatten()) - point.tofile(handle) - analog = array.array(point_format) - analog.extend(analog) - analog.tofile(handle) - self._pad_block(handle) - - def write(self, handle, labels): - '''Write metadata and point + analog frames to a file handle. - - Parameters - ---------- - handle : file - Write metadata and C3D motion frames to the given file handle. The - writer does not close the handle. - ''' - if not self._frames: - return - - dtypes = DataTypes(PROCESSOR_INTEL) - - def add(name, desc, bpe, format, bytes, *dimensions): - group.add_param(name, - dtypes, - desc=desc, - bytes_per_element=bpe, - bytes=struct.pack(format, bytes), - dimensions=list(dimensions)) - - def add_str(name, desc, bytes, *dimensions): - group.add_param(name, - dtypes, - desc=desc, - bytes_per_element=-1, - bytes=bytes.encode('utf-8'), - dimensions=list(dimensions)) - - def add_empty_array(name, desc, bpe): - group.add_param(name, dtypes, desc=desc, - bytes_per_element=bpe, dimensions=[0]) - - points, analog = self._frames[0] - ppf = len(points) - labels = np.ravel(labels) - - # POINT group - - # Get longest label name - label_max_size = 0 - label_max_size = max(label_max_size, np.max([len(label) for label in labels])) - - group = self.add_group(1, 'POINT', 'POINT group') - add('USED', 'Number of 3d markers', 2, '') + self.float64 = np.dtype(np.float64).newbyteorder('>') + self.uint8 = np.uint8 + self.uint16 = np.dtype(np.uint16).newbyteorder('>') + self.uint32 = np.dtype(np.uint32).newbyteorder('>') + self.uint64 = np.dtype(np.uint64).newbyteorder('>') + self.int8 = np.int8 + self.int16 = np.dtype(np.int16).newbyteorder('>') + self.int32 = np.dtype(np.int32).newbyteorder('>') + self.int64 = np.dtype(np.int64).newbyteorder('>') + else: + # Little-Endian format (Intel or DEC format) + self.float32 = np.float32 + self.float64 = np.float64 + self.uint8 = np.uint8 + self.uint16 = np.uint16 + self.uint32 = np.uint32 + self.uint64 = np.uint64 + self.int8 = np.int8 + self.int16 = np.int16 + self.int32 = np.int32 + self.int64 = np.int64 + + @property + def is_ieee(self) -> bool: + ''' True if the associated file is in the Intel format. + ''' + return self._proc_type == PROCESSOR_INTEL + + @property + def is_dec(self) -> bool: + ''' True if the associated file is in the DEC format. + ''' + return self._proc_type == PROCESSOR_DEC + + @property + def is_mips(self) -> bool: + ''' True if the associated file is in the SGI/MIPS format. + ''' + return self._proc_type == PROCESSOR_MIPS + + @property + def proc_type(self) -> str: + ''' Get the processory type associated with the data format in the file. + ''' + processor_type = ['INTEL', 'DEC', 'MIPS'] + return processor_type[self._proc_type - PROCESSOR_INTEL] + + @property + def processor(self) -> int: + ''' Get the processor number encoded in the .c3d file. + ''' + return self._proc_type + + @property + def native(self) -> bool: + ''' True if the native (system) byte order matches the file byte order. + ''' + return self._native + + @property + def little_endian_sys(self) -> bool: + ''' True if native byte order is little-endian. + ''' + return self._little_endian_sys + + @property + def big_endian_sys(self) -> bool: + ''' True if native byte order is big-endian. + ''' + return not self._little_endian_sys + + def decode_string(self, bytes) -> str: + ''' Decode a byte array to a string. + ''' + # Attempt to decode using different decoders + decoders = ['utf-8', 'latin-1'] + for dec in decoders: + try: + return codecs.decode(bytes, dec) + except UnicodeDecodeError: + continue + # Revert to using default decoder but replace characters + return codecs.decode(bytes, decoders[0], 'replace') diff --git a/c3d/group.py b/c3d/group.py new file mode 100644 index 0000000..0390493 --- /dev/null +++ b/c3d/group.py @@ -0,0 +1,466 @@ +''' Classes used to represent the concept of parameter groups in a .c3d file. +''' +import struct +import numpy as np +from .parameter import ParamData, Param +from .utils import Decorator + + +class GroupData(object): + '''A group of parameters stored in a C3D file. + + In C3D files, parameters are organized in groups. Each group has a name (key), a + description, and a set of named parameters. Each group is also internally associated + with a numeric key. + + Attributes + ---------- + dtypes : `c3d.dtypes.DataTypes` + Data types object used for parsing. + name : str + Name of this parameter group. + desc : str + Description for this parameter group. + ''' + + def __init__(self, dtypes, name=None, desc=None): + self._params = {} + self._dtypes = dtypes + # Assign through property setters + self.set_name(name) + self.set_desc(desc) + + def __repr__(self): + return ''.format(self.desc) + + def __contains__(self, key): + return key in self._params + + def __getitem__(self, key): + return self._params[key] + + @property + def binary_size(self) -> int: + '''Return the number of bytes to store this group and its parameters.''' + return ( + 1 + # group_id + 1 + len(self.name.encode('utf-8')) + # size of name and name bytes + 2 + # next offset marker + 1 + len(self.desc.encode('utf-8')) + # size of desc and desc bytes + sum(p.binary_size for p in self._params.values())) + + def set_name(self, name): + ''' Set the group name string. ''' + if name is None or isinstance(name, str): + self.name = name + else: + raise TypeError('Expected group name to be string, was %s.' % type(name)) + + def set_desc(self, desc): + ''' Set the Group descriptor. + ''' + if isinstance(desc, bytes): + self.desc = self._dtypes.decode_string(desc) + elif isinstance(desc, str) or desc is None: + self.desc = desc + else: + raise TypeError('Expected descriptor to be python string, bytes or None, was %s.' % type(desc)) + + def add_param(self, name, **kwargs): + '''Add a parameter to this group. + + Parameters + ---------- + name : str + Name of the parameter to add to this group. The name will + automatically be case-normalized. + + See constructor of `c3d.parameter.ParamData` for additional keyword arguments. + + Raises + ------ + TypeError + Input arguments are of the wrong type. + KeyError + Name or numerical key already exist (attempt to overwrite existing data). + ''' + if not isinstance(name, str): + raise TypeError("Expected 'name' argument to be a string, was of type {}".format(type(name))) + name = name.upper() + if name in self._params: + raise KeyError('Parameter already exists with key {}'.format(name)) + self._params[name] = Param(ParamData(name, self._dtypes, **kwargs)) + + def remove_param(self, name): + '''Remove the specified parameter. + + Parameters + ---------- + name : str + Name for the parameter to remove. + ''' + del self._params[name] + + def rename_param(self, name, new_name): + ''' Rename a specified parameter group. + + Parameters + ---------- + name : str, or `c3d.group.GroupReadonly` + Parameter instance, or name. + new_name : str + New name for the parameter. + Raises + ------ + KeyError + If no parameter with the original name exists. + ValueError + If the new name already exist (attempt to overwrite existing data). + ''' + if new_name in self._params: + raise ValueError("Key {} already exist.".format(new_name)) + if isinstance(name, Param): + param = name + name = param.name + else: + # Aquire instance using id + param = self._params[name] + del self._params[name] + self._params[new_name] = param + + def write(self, group_id, handle): + '''Write this parameter group, with parameters, to a file handle. + + Parameters + ---------- + group_id : int + The numerical ID of the group. + handle : file handle + An open, writable, binary file handle. + ''' + name = self.name.encode('utf-8') + desc = self.desc.encode('utf-8') + handle.write(struct.pack('bb', len(name), -group_id)) + handle.write(name) + handle.write(struct.pack(' str: + ''' Access group name. ''' + return self._data.name + + @property + def desc(self) -> str: + '''Access group descriptor. ''' + return self._data.desc + + def items(self): + ''' Get iterator for paramater key-entry pairs. ''' + return ((k, v.readonly()) for k, v in self._data._params.items()) + + def values(self): + ''' Get iterator for parameter entries. ''' + return (v.readonly() for v in self._data._params.values()) + + def keys(self): + ''' Get iterator for parameter entry keys. ''' + return self._data._params.keys() + + def get(self, key, default=None): + '''Get a readonly parameter by key. + + Parameters + ---------- + key : any + Parameter key to look up in this group. + default : any, optional + Value to return if the key is not found. Defaults to None. + + Returns + ------- + param : :class:`ParamReadable` + A parameter from the current group. + ''' + val = self._data._params.get(key, default) + if val: + return val.readonly() + return default + + def get_int8(self, key): + '''Get the value of the given parameter as an 8-bit signed integer.''' + return self._data[key.upper()].int8_value + + def get_uint8(self, key): + '''Get the value of the given parameter as an 8-bit unsigned integer.''' + return self._data[key.upper()].uint8_value + + def get_int16(self, key): + '''Get the value of the given parameter as a 16-bit signed integer.''' + return self._data[key.upper()].int16_value + + def get_uint16(self, key): + '''Get the value of the given parameter as a 16-bit unsigned integer.''' + return self._data[key.upper()].uint16_value + + def get_int32(self, key): + '''Get the value of the given parameter as a 32-bit signed integer.''' + return self._data[key.upper()].int32_value + + def get_uint32(self, key): + '''Get the value of the given parameter as a 32-bit unsigned integer.''' + return self._data[key.upper()].uint32_value + + def get_float(self, key): + '''Get the value of the given parameter as a 32-bit float.''' + return self._data[key.upper()].float_value + + def get_bytes(self, key): + '''Get the value of the given parameter as a byte array.''' + return self._data[key.upper()].bytes_value + + def get_string(self, key): + '''Get the value of the given parameter as a string.''' + return self._data[key.upper()].string_value + + +class Group(GroupReadonly): + ''' Wrapper exposing readable and writeable attributes of a `c3d.group.GroupData` entry. + ''' + def __init__(self, data): + super(Group, self).__init__(data) + + def readonly(self): + ''' Returns a `c3d.group.GroupReadonly` instance with readonly access. ''' + return GroupReadonly(self._data) + + @property + def name(self) -> str: + ''' Get or set name. ''' + return self._data.name + + @name.setter + def name(self, value) -> str: + self._data.set_name(value) + + @property + def desc(self) -> str: + ''' Get or set descriptor. ''' + return self._data.desc + + @desc.setter + def desc(self, value) -> str: + self._data.set_desc(value) + + def items(self): + ''' Iterator for paramater key-entry pairs. ''' + return ((k, v) for k, v in self._data._params.items()) + + def values(self): + ''' Iterator iterator for parameter entries. ''' + return (v for v in self._data._params.values()) + + def get(self, key, default=None): + '''Get a parameter by key. + + Parameters + ---------- + key : any + Parameter key to look up in this group. + default : any, optional + Value to return if the key is not found. Defaults to None. + + Returns + ------- + param : :class:`ParamReadable` + A parameter from the current group. + ''' + return self._data._params.get(key, default) + + # + # Forward param editing + # + def add_param(self, name, **kwargs): + '''Add a parameter to this group. + + See constructor of `c3d.parameter.ParamData` for additional keyword arguments. + ''' + self._data.add_param(name, **kwargs) + + def remove_param(self, name): + '''Remove the specified parameter. + + Parameters + ---------- + name : str + Name for the parameter to remove. + ''' + self._data.remove_param(name) + + def rename_param(self, name, new_name): + ''' Rename a specified parameter group. + + Parameters + ---------- + See arguments in `c3d.group.GroupData.rename_param`. + ''' + self._data.rename_param(name, new_name) + + # + # Convenience functions for adding parameters. + # + def add(self, name, desc, bpe, format, data, *dimensions): + ''' Add a parameter with `data` package formated in accordance with `format`. + + Convenience function for `c3d.group.GroupData.add_param` calling struct.pack() on `data`. + + Example: + + >>> group.set('RATE', 'Point data sample rate', 4, ' 0 + self.event_timings[i] = float_unpack(struct.unpack(unpack_fmt, time_bytes[ilong:ilong + 4])[0]) + self.event_labels[i] = dtypes.decode_string(label_bytes[ilong:ilong + 4]) + + @property + def events(self): + ''' Get an iterable over displayed events defined in the header. Iterable items are on form (timing, label). + + Note*: + Time as defined by the 'timing' is relative to frame 1 and not the 'first_frame' parameter. + Frame 1 therefor has the time 0.0 in relation to the event timing. + ''' + return zip(self.event_timings[self.event_disp_flags], self.event_labels[self.event_disp_flags]) + + def encode_events(self, events): + ''' Encode event data in the event block. + + Parameters + ---------- + events : [(float, str), ...] + Event data, iterable of touples containing the event timing and a 4 character label string. + Event timings should be calculated relative to sample 1 with the timing 0.0s, and should + not be relative to the first_frame header parameter. + ''' + endian = '<' + if sys.byteorder == 'big': + endian = '>' + + # Event block format + fmt = '{}{}{}{}{}'.format(endian, + str(18 * 4) + 's', # Timings + str(18) + 's', # Flags + 'H', # __ + str(18 * 4) + 's' # Labels + ) + # Pack bytes + event_timings = np.zeros(18, dtype=np.float32) + event_disp_flags = np.zeros(18, dtype=np.uint8) + event_labels = np.empty(18, dtype=object) + label_bytes = bytearray(18 * 4) + for i, (time, label) in enumerate(events): + if i > 17: + # Don't raise Error, header events are rarely used. + warnings.warn('Maximum of 18 events can be encoded in the header, skipping remaining events.') + break + + event_timings[i] = time + event_labels[i] = label + label_bytes[i * 4:(i + 1) * 4] = label.encode('utf-8') + + write_count = min(i + 1, 18) + event_disp_flags[:write_count] = 1 + + # Update event headers in self + self.long_event_labels = 0x3039 # Magic number + self.event_count = write_count + # Update event block + self.event_timings = event_timings[:write_count] + self.event_disp_flags = np.ones(write_count, dtype=np.bool) + self.event_labels = event_labels[:write_count] + self.event_block = struct.pack(fmt, + event_timings.tobytes(), + event_disp_flags.tobytes(), + 0, + label_bytes + ) diff --git a/c3d/manager.py b/c3d/manager.py new file mode 100644 index 0000000..816d6d6 --- /dev/null +++ b/c3d/manager.py @@ -0,0 +1,463 @@ +''' Manager base class defining common attributes for both Reader and Writer instances. +''' +import numpy as np +import warnings +from .header import Header +from .group import GroupData, GroupReadonly, Group +from .utils import is_integer, is_iterable + + +class Manager(object): + '''A base class for managing C3D file metadata. + + This class manages a C3D header (which contains some stock metadata fields) + as well as a set of parameter groups. Each group is accessible using its + name. + + Attributes + ---------- + header : `c3d.header.Header` + Header information for the C3D file. + ''' + + def __init__(self, header=None): + '''Set up a new Manager with a Header.''' + self._header = header or Header() + self._groups = {} + + def __contains__(self, key): + return key in self._groups + + def items(self): + ''' Get iterable over pairs of (str, `c3d.group.Group`) entries. + ''' + return ((k, v) for k, v in self._groups.items() if isinstance(k, str)) + + def values(self): + ''' Get iterable over `c3d.group.Group` entries. + ''' + return (v for k, v in self._groups.items() if isinstance(k, str)) + + def keys(self): + ''' Get iterable over parameter name keys. + ''' + return (k for k in self._groups.keys() if isinstance(k, str)) + + def listed(self): + ''' Get iterable over pairs of (int, `c3d.group.Group`) entries. + ''' + return sorted((i, g) for i, g in self._groups.items() if isinstance(i, int)) + + def _check_metadata(self): + ''' Ensure that the metadata in our file is self-consistent. ''' + assert self._header.point_count == self.point_used, ( + 'inconsistent point count! {} header != {} POINT:USED'.format( + self._header.point_count, + self.point_used, + )) + + assert self._header.scale_factor == self.point_scale, ( + 'inconsistent scale factor! {} header != {} POINT:SCALE'.format( + self._header.scale_factor, + self.point_scale, + )) + + assert self._header.frame_rate == self.point_rate, ( + 'inconsistent frame rate! {} header != {} POINT:RATE'.format( + self._header.frame_rate, + self.point_rate, + )) + + if self.point_rate: + ratio = self.analog_rate / self.point_rate + else: + ratio = 0 + assert self._header.analog_per_frame == ratio, ( + 'inconsistent analog rate! {} header != {} analog-fps / {} point-fps'.format( + self._header.analog_per_frame, + self.analog_rate, + self.point_rate, + )) + + count = self.analog_used * self._header.analog_per_frame + assert self._header.analog_count == count, ( + 'inconsistent analog count! {} header != {} analog used * {} per-frame'.format( + self._header.analog_count, + self.analog_used, + self._header.analog_per_frame, + )) + + try: + start = self.get('POINT:DATA_START').uint16_value + if self._header.data_block != start: + warnings.warn('inconsistent data block! {} header != {} POINT:DATA_START'.format( + self._header.data_block, start)) + except AttributeError: + warnings.warn('''no pointer available in POINT:DATA_START indicating the start of the data block, using + header pointer as fallback''') + + def check_parameters(params): + for name in params: + if self.get(name) is None: + warnings.warn('missing parameter {}'.format(name)) + + if self.point_used > 0: + check_parameters(('POINT:LABELS', 'POINT:DESCRIPTIONS')) + else: + lab = self.get('POINT:LABELS') + if lab is None: + warnings.warn('No point data found in file.') + elif lab.num_elements > 0: + warnings.warn('No point data found in file, but file contains POINT:LABELS entries') + if self.analog_used > 0: + check_parameters(('ANALOG:LABELS', 'ANALOG:DESCRIPTIONS')) + else: + lab = self.get('ANALOG:LABELS') + if lab is None: + warnings.warn('No analog data found in file.') + elif lab.num_elements > 0: + warnings.warn('No analog data found in file, but file contains ANALOG:LABELS entries') + + def _add_group(self, group_id, name=None, desc=None): + '''Add a new parameter group. + + Parameters + ---------- + group_id : int + The numeric ID for a group to check or create. + name : str, optional + If a group is created, assign this name to the group. + desc : str, optional + If a group is created, assign this description to the group. + + Returns + ------- + group : :class:`Group` + A group with the given ID, name, and description. + + Raises + ------ + TypeError + Input arguments are of the wrong type. + KeyError + Name or numerical key already exist (attempt to overwrite existing data). + ''' + if not is_integer(group_id): + raise TypeError('Expected Group numerical key to be integer, was %s.' % type(group_id)) + if not isinstance(name, str): + if name is not None: + raise TypeError('Expected Group name key to be string, was %s.' % type(name)) + else: + name = name.upper() + group_id = int(group_id) # Asserts python int + if group_id in self._groups: + raise KeyError('Group with numerical key {} already exists'.format(group_id)) + if name in self._groups: + raise KeyError('No group matched name key {}'.format(name)) + group = self._groups[name] = self._groups[group_id] = Group(GroupData(self._dtypes, name, desc)) + return group + + def _remove_group(self, group_id): + '''Remove the parameter group. + + Parameters + ---------- + group_id : int, or str + The numeric or name ID key for a group to remove all entries for. + ''' + grp = self._groups.get(group_id, None) + if grp is None: + return + gkeys = [k for (k, v) in self._groups.items() if v == grp] + for k in gkeys: + del self._groups[k] + + def _rename_group(self, group_id, new_group_id): + ''' Rename a specified parameter group. + + Parameters + ---------- + group_id : int, str, or `c3d.group.Group` + Group instance, name, or numerical identifier for the group. + new_group_id : str, or int + If string, it is the new name for the group. If integer, it will replace its numerical group id. + + Raises + ------ + KeyError + If a group with a duplicate ID or name already exists. + ''' + if isinstance(group_id, GroupReadonly): + grp = group_id._data + else: + # Aquire instance using id + grp = self._groups.get(group_id, None) + if grp is None: + raise KeyError('No group found matching the identifier: %s' % str(group_id)) + if new_group_id in self._groups: + if new_group_id == group_id: + return + raise ValueError('Key %s for group %s already exist.' % (str(new_group_id), grp.name)) + + # Clear old id + if isinstance(new_group_id, (str, bytes)): + if grp.name in self._groups: + del self._groups[grp.name] + grp.name = new_group_id + elif is_integer(new_group_id): + new_group_id = int(new_group_id) # Ensure python int + del self._groups[group_id] + else: + raise KeyError('Invalid group identifier of type: %s' % str(type(new_group_id))) + # Update + self._groups[new_group_id] = grp + + def get(self, group, default=None): + '''Get a group or parameter. + + Parameters + ---------- + group : str + If this string contains a period (.), then the part before the + period will be used to retrieve a group, and the part after the + period will be used to retrieve a parameter from that group. If this + string does not contain a period, then just a group will be + returned. + default : any + Return this value if the named group and parameter are not found. + + Returns + ------- + value : `c3d.group.Group` or `c3d.parameter.Param` + Either a group or parameter with the specified name(s). If neither + is found, returns the default value. + ''' + if is_integer(group): + group = self._groups.get(int(group)) + if group is None: + return default + return group + group = group.upper() + param = None + if '.' in group: + group, param = group.split('.', 1) + if ':' in group: + group, param = group.split(':', 1) + if group not in self._groups: + return default + group = self._groups[group] + if param is not None: + return group.get(param, default) + return group + + @property + def header(self) -> '`c3d.header.Header`': + ''' Access to .c3d header data. ''' + return self._header + + def parameter_blocks(self) -> int: + '''Compute the size (in 512B blocks) of the parameter section.''' + bytes = 4. + sum(g._data.binary_size for g in self._groups.values()) + return int(np.ceil(bytes / 512)) + + @property + def point_rate(self) -> float: + ''' Number of sampled 3D coordinates per second. ''' + try: + return self.get_float('POINT:RATE') + except AttributeError: + return self.header.frame_rate + + @property + def point_scale(self) -> float: + ''' Scaling applied to non-float data. ''' + try: + return self.get_float('POINT:SCALE') + except AttributeError: + return self.header.scale_factor + + @property + def point_used(self) -> int: + ''' Number of sampled 3D point coordinates per frame. ''' + try: + return self.get_uint16('POINT:USED') + except AttributeError: + return self.header.point_count + + @property + def analog_used(self) -> int: + ''' Number of analog measurements, or channels, for each analog data sample. ''' + try: + return self.get_uint16('ANALOG:USED') + except AttributeError: + per_frame = self.header.analog_per_frame + if per_frame > 0: + return int(self.header.analog_count / per_frame) + return 0 + + @property + def analog_rate(self) -> float: + ''' Number of analog data samples per second. ''' + try: + return self.get_float('ANALOG:RATE') + except AttributeError: + return self.header.analog_per_frame * self.point_rate + + @property + def analog_per_frame(self) -> int: + ''' Number of analog frames per 3D frame (point sample). ''' + return int(self.analog_rate / self.point_rate) + + @property + def analog_sample_count(self) -> int: + ''' Number of analog samples per channel. ''' + has_analog = self.analog_used > 0 + return int(self.frame_count * self.analog_per_frame) * has_analog + + @property + def point_labels(self) -> list: + ''' Labels for each POINT data channel. ''' + return self.get('POINT:LABELS').string_array + + @property + def analog_labels(self) -> list: + ''' Labels for each ANALOG data channel. ''' + return self.get('ANALOG:LABELS').string_array + + @property + def frame_count(self) -> int: + ''' Number of frames recorded in the data. ''' + return self.last_frame - self.first_frame + 1 # Add 1 since range is inclusive [first, last] + + @property + def first_frame(self) -> int: + ''' Trial frame corresponding to the first frame recorded in the data. ''' + # Start frame seems to be less of an issue to determine. + # this is a hack for phasespace files ... should put it in a subclass. + param = self.get('TRIAL:ACTUAL_START_FIELD') + if param is not None: + # ACTUAL_START_FIELD is encoded in two 16 byte words... + return param.uint32_value + return self.header.first_frame + + @property + def last_frame(self) -> int: + ''' Trial frame corresponding to the last frame recorded in the data (inclusive). + ''' + # Number of frames can be represented in many formats. + # Start of by different parameters where the frame can be encoded + hlf = self.header.last_frame + param = self.get('TRIAL:ACTUAL_END_FIELD') + if param is not None: + # Encoded as 2 16 bit words (rather then 1 32 bit word) + # Manual refer to parsing the parameter as 2 16-bit words, but its equivalent to an uint32 + # words = param.uint16_array + # end_frame[1] = words[0] + words[1] * 65536 + end_frame = param.uint32_value + if hlf <= end_frame: + return end_frame + param = self.get('POINT:LONG_FRAMES') + if param is not None: + # 'Should be' encoded as float + if param.bytes_per_element >= 4: + end_frame = int(param.float_value) + else: + end_frame = param.uint16_value + if hlf <= end_frame: + return end_frame + param = self.get('POINT:FRAMES') + if param is not None: + # Can be encoded either as 32 bit float or 16 bit uint + if param.bytes_per_element == 4: + end_frame = int(param.float_value) + else: + end_frame = param.uint16_value + if hlf <= end_frame: + return end_frame + # Return header value by default + return hlf + + def get_screen_xy_strings(self): + ''' Get the POINT:X_SCREEN and POINT:Y_SCREEN parameters as strings. + + See `Manager.get_screen_xy_axis` to get numpy vectors instead. + + Returns + ------- + value : (str, str) or None + Touple containing X_SCREEN and Y_SCREEN strings, or None (if no parameters could be found). + ''' + X = self.get('POINT:X_SCREEN') + Y = self.get('POINT:Y_SCREEN') + if X and Y: + return (X.string_value, Y.string_value) + return None + + def get_screen_xy_axis(self): + ''' Get the POINT:X_SCREEN and POINT:Y_SCREEN parameters as unit row vectors. + + Z axis can be computed using the cross product: + + $$ z = x \\times y $$ + + To move a point coordinate $p_s$ as read from `c3d.reader.Reader.read_frames` out of the system basis do: + + $$ p = | x^T y^T z^T |^T p_s $$ + + + See `Manager.get_screen_xy_strings` to get the parameter as string values instead. + + Returns + ------- + value : ([3,], [3,]) or None + Touple $(x, y)$ containing X_SCREEN and Y_SCREEN as row vectors, or None. + ''' + # Axis conversion dictionary. + AXIS_DICT = { + 'X': np.array([1.0, 0, 0]), + '+X': np.array([1.0, 0, 0]), + '-X': np.array([-1.0, 0, 0]), + 'Y': np.array([0, 1.0, 0]), + '+Y': np.array([0, 1.0, 0]), + '-Y': np.array([0, -1.0, 0]), + 'Z': np.array([0, 0, 1.0]), + '+Z': np.array([0, 0, 1.0]), + '-Z': np.array([0, 0, -1.0]), + } + + val = self.get_screen_xy_strings() + if val is None: + return None + axis_x, axis_y = val + + # Interpret using both X/Y_SCREEN + return AXIS_DICT[axis_x], AXIS_DICT[axis_y] + + def get_analog_transform_parameters(self): + ''' Parse analog data transform parameters. ''' + # Offsets + analog_offsets = np.zeros((self.analog_used), int) + param = self.get('ANALOG:OFFSET') + if param is not None and param.num_elements > 0: + analog_offsets[:] = param.int16_array[:self.analog_used] + + # Scale factors + analog_scales = np.ones((self.analog_used), float) + gen_scale = 1. + param = self.get('ANALOG:GEN_SCALE') + if param is not None: + gen_scale = param.float_value + param = self.get('ANALOG:SCALE') + if param is not None and param.num_elements > 0: + analog_scales[:] = param.float_array[:self.analog_used] + + return gen_scale, analog_scales, analog_offsets + + def get_analog_transform(self): + ''' Get broadcastable analog transformation parameters. + ''' + gen_scale, analog_scales, analog_offsets = self.get_analog_transform_parameters() + analog_scales *= gen_scale + analog_scales = np.broadcast_to(analog_scales[:, np.newaxis], (self.analog_used, self.analog_per_frame)) + analog_offsets = np.broadcast_to(analog_offsets[:, np.newaxis], (self.analog_used, self.analog_per_frame)) + return analog_scales, analog_offsets diff --git a/c3d/parameter.py b/c3d/parameter.py new file mode 100644 index 0000000..4aef7db --- /dev/null +++ b/c3d/parameter.py @@ -0,0 +1,485 @@ +''' Classes used to represent the concept of a parameter in a .c3d file. +''' +import struct +import numpy as np +from .utils import DEC_to_IEEE, DEC_to_IEEE_BYTES + + +class ParamData(object): + '''A class representing a single named parameter from a C3D file. + + Attributes + ---------- + name : str + Name of this parameter. + dtype: DataTypes + Reference to the DataTypes object associated with the file. + desc : str + Brief description of this parameter. + bytes_per_element : int, optional + For array data, this describes the size of each element of data. For + string data (including arrays of strings), this should be -1. + dimensions : list of int + For array data, this describes the dimensions of the array, stored in + column-major (Fortran) order. For arrays of strings, the dimensions here will be + the number of columns (length of each string) followed by the number of + rows (number of strings). + bytes : str + Raw data for this parameter. + ''' + + def __init__(self, + name, + dtype, + desc='', + bytes_per_element=1, + dimensions=None, + bytes=b'', + handle=None): + '''Set up a new parameter, only the name is required.''' + self.name = name + self.dtypes = dtype + self.desc = desc + self.bytes_per_element = bytes_per_element + self.dimensions = dimensions or [] + self.bytes = bytes + if handle: + self.read(handle) + + def __repr__(self): + return ''.format(self.desc) + + @property + def num_elements(self) -> int: + '''Return the number of elements in this parameter's array value.''' + e = 1 + for d in self.dimensions: + e *= d + return e + + @property + def total_bytes(self) -> int: + '''Return the number of bytes used for storing this parameter's data.''' + return self.num_elements * abs(self.bytes_per_element) + + @property + def binary_size(self) -> int: + '''Return the number of bytes needed to store this parameter.''' + return ( + 1 + # group_id + 2 + # next offset marker + 1 + len(self.name.encode('utf-8')) + # size of name and name bytes + 1 + # data size + # size of dimensions and dimension bytes + 1 + len(self.dimensions) + + self.total_bytes + # data + 1 + len(self.desc.encode('utf-8')) # size of desc and desc bytes + ) + + def write(self, group_id, handle): + '''Write binary data for this parameter to a file handle. + + Parameters + ---------- + group_id : int + The numerical ID of the group that holds this parameter. + handle : file handle + An open, writable, binary file handle. + ''' + name = self.name.encode('utf-8') + handle.write(struct.pack('bb', len(name), group_id)) + handle.write(name) + handle.write(struct.pack(' 0: + handle.write(self.bytes) + desc = self.desc.encode('utf-8') + handle.write(struct.pack('B', len(desc))) + handle.write(desc) + + def read(self, handle): + '''Read binary data for this parameter from a file handle. + + This reads exactly enough data from the current position in the file to + initialize the parameter. + ''' + self.bytes_per_element, = struct.unpack('b', handle.read(1)) + dims, = struct.unpack('B', handle.read(1)) + self.dimensions = [struct.unpack('B', handle.read(1))[ + 0] for _ in range(dims)] + self.bytes = b'' + if self.total_bytes: + self.bytes = handle.read(self.total_bytes) + desc_size, = struct.unpack('B', handle.read(1)) + self.desc = desc_size and self.dtypes.decode_string(handle.read(desc_size)) or '' + + def _as(self, dtype): + '''Unpack the raw bytes of this param using the given struct format.''' + return np.frombuffer(self.bytes, count=1, dtype=dtype)[0] + + def _as_array(self, dtype, copy=True): + '''Unpack the raw bytes of this param using the given data format.''' + if not self.dimensions: + return [self._as(dtype)] + elems = np.frombuffer(self.bytes, dtype=dtype) + # Reverse shape as the shape is defined in fortran format + view = elems.reshape(self.dimensions[::-1]) + if copy: + return view.copy() + return view + + +class ParamReadonly(object): + ''' Wrapper exposing readonly attributes of a `c3d.parameter.ParamData` entry. + ''' + + def __init__(self, data): + self._data = data + + def __eq__(self, other): + return self._data is other._data + + @property + def name(self) -> str: + ''' Get the parameter name. ''' + return self._data.name + + @property + def desc(self) -> str: + ''' Get the parameter descriptor. ''' + return self._data.desc + + @property + def dtypes(self): + ''' Convenience accessor to the `c3d.dtypes.DataTypes` instance associated with the parameter. ''' + return self._data.dtypes + + @property + def dimensions(self) -> (int, ...): + ''' Shape of the parameter data (Fortran format). ''' + return self._data.dimensions + + @property + def num_elements(self) -> int: + '''Return the number of elements in this parameter's array value.''' + return self._data.num_elements + + @property + def bytes_per_element(self) -> int: + '''Return the number of bytes used to store each data element.''' + return self._data.bytes_per_element + + @property + def total_bytes(self) -> int: + '''Return the number of bytes used for storing this parameter's data.''' + return self._data.total_bytes + + @property + def binary_size(self) -> int: + '''Return the number of bytes needed to store this parameter.''' + return self._data.binary_size + + @property + def int8_value(self): + '''Get the parameter data as an 8-bit signed integer.''' + return self._data._as(self.dtypes.int8) + + @property + def uint8_value(self): + '''Get the parameter data as an 8-bit unsigned integer.''' + return self._data._as(self.dtypes.uint8) + + @property + def int16_value(self): + '''Get the parameter data as a 16-bit signed integer.''' + return self._data._as(self.dtypes.int16) + + @property + def uint16_value(self): + '''Get the parameter data as a 16-bit unsigned integer.''' + return self._data._as(self.dtypes.uint16) + + @property + def int32_value(self): + '''Get the parameter data as a 32-bit signed integer.''' + return self._data._as(self.dtypes.int32) + + @property + def uint32_value(self): + '''Get the parameter data as a 32-bit unsigned integer.''' + return self._data._as(self.dtypes.uint32) + + @property + def uint_value(self): + ''' Get the parameter data as a unsigned integer of appropriate type. ''' + if self.bytes_per_element >= 4: + return self.uint32_value + elif self.bytes_per_element >= 2: + return self.uint16_value + else: + return self.uint8_value + + @property + def int_value(self): + ''' Get the parameter data as a signed integer of appropriate type. ''' + if self.bytes_per_element >= 4: + return self.int32_value + elif self.bytes_per_element >= 2: + return self.int16_value + else: + return self.int8_value + + @property + def float_value(self): + '''Get the parameter data as a floating point value of appropriate type.''' + if self.bytes_per_element > 4: + if self.dtypes.is_dec: + raise AttributeError("64 bit DEC floating point is not supported.") + # 64-bit floating point is not a standard + return self._data._as(self.dtypes.float64) + elif self.bytes_per_element == 4: + if self.dtypes.is_dec: + return DEC_to_IEEE(self._data._as(np.uint32)) + else: # is_mips or is_ieee + return self._data._as(self.dtypes.float32) + else: + raise AttributeError("Only 32 and 64 bit floating point is supported.") + + @property + def bytes_value(self) -> bytes: + '''Get the raw byte string.''' + return self._data.bytes + + @property + def string_value(self): + '''Get the parameter data as a unicode string.''' + return self.dtypes.decode_string(self._data.bytes) + + @property + def int8_array(self): + '''Get the parameter data as an array of 8-bit signed integers.''' + return self._data._as_array(self.dtypes.int8) + + @property + def uint8_array(self): + '''Get the parameter data as an array of 8-bit unsigned integers.''' + return self._data._as_array(self.dtypes.uint8) + + @property + def int16_array(self): + '''Get the parameter data as an array of 16-bit signed integers.''' + return self._data._as_array(self.dtypes.int16) + + @property + def uint16_array(self): + '''Get the parameter data as an array of 16-bit unsigned integers.''' + return self._data._as_array(self.dtypes.uint16) + + @property + def int32_array(self): + '''Get the parameter data as an array of 32-bit signed integers.''' + return self._data._as_array(self.dtypes.int32) + + @property + def uint32_array(self): + '''Get the parameter data as an array of 32-bit unsigned integers.''' + return self._data._as_array(self.dtypes.uint32) + + @property + def int64_array(self): + '''Get the parameter data as an array of 32-bit signed integers.''' + return self._data._as_array(self.dtypes.int64) + + @property + def uint64_array(self): + '''Get the parameter data as an array of 32-bit unsigned integers.''' + return self._data._as_array(self.dtypes.uint64) + + @property + def float32_array(self): + '''Get the parameter data as an array of 32-bit floats.''' + # Convert float data if not IEEE processor + if self.dtypes.is_dec: + # _as_array but for DEC + if not self.dimensions: + return [self.float_value] + return DEC_to_IEEE_BYTES(self._data.bytes).reshape(self.dimensions[::-1]) # Reverse fortran format + else: # is_ieee or is_mips + return self._data._as_array(self.dtypes.float32) + + @property + def float64_array(self): + '''Get the parameter data as an array of 64-bit floats.''' + # Convert float data if not IEEE processor + if self.dtypes.is_dec: + raise ValueError('Unable to convert bytes encoded in a 64 bit floating point DEC format.') + else: # is_ieee or is_mips + return self._data._as_array(self.dtypes.float64) + + @property + def float_array(self): + '''Get the parameter data as an array of 32 or 64 bit floats.''' + # Convert float data if not IEEE processor + if self.bytes_per_element == 4: + return self.float32_array + elif self.bytes_per_element == 8: + return self.float64_array + else: + raise TypeError("Parsing parameter bytes to an array with %i bit " % self.bytes_per_element + + "floating-point precission is not unsupported.") + + @property + def int_array(self): + '''Get the parameter data as an array of integer values.''' + # Convert float data if not IEEE processor + if self.bytes_per_element == 1: + return self.int8_array + elif self.bytes_per_element == 2: + return self.int16_array + elif self.bytes_per_element == 4: + return self.int32_array + elif self.bytes_per_element == 8: + return self.int64_array + else: + raise TypeError("Parsing parameter bytes to an array with %i bit integer values is not unsupported." % + self.bytes_per_element) + + @property + def uint_array(self): + '''Get the parameter data as an array of integer values.''' + # Convert float data if not IEEE processor + if self.bytes_per_element == 1: + return self.uint8_array + elif self.bytes_per_element == 2: + return self.uint16_array + elif self.bytes_per_element == 4: + return self.uint32_array + elif self.bytes_per_element == 8: + return self.uint64_array + else: + raise TypeError("Parsing parameter bytes to an array with %i bit integer values is not unsupported." % + self.bytes_per_element) + + @property + def bytes_array(self): + '''Get the parameter data as an array of raw byte strings.''' + # Decode different dimensions + if len(self.dimensions) == 0: + return np.array([]) + elif len(self.dimensions) == 1: + return np.array(self._data.bytes) + else: + # Convert Fortran shape (data in memory is identical, shape is transposed) + word_len = self.dimensions[0] + dims = self.dimensions[1:][::-1] # Identical to: [:0:-1] + byte_steps = np.cumprod(self.dimensions[:-1])[::-1] + # Generate mult-dimensional array and parse byte words + byte_arr = np.empty(dims, dtype=object) + for i in np.ndindex(*dims): + # Calculate byte offset as sum of each array index times the byte step of each dimension. + off = np.sum(np.multiply(i, byte_steps)) + byte_arr[i] = self._data.bytes[off:off+word_len] + return byte_arr + + @property + def string_array(self): + '''Get the parameter data as a python array of unicode strings.''' + # Decode different dimensions + if len(self.dimensions) == 0: + return np.array([]) + elif len(self.dimensions) == 1: + return np.array([self.string_value]) + else: + # Parse byte sequences + byte_arr = self.bytes_array + # Decode sequences + for i in np.ndindex(byte_arr.shape): + byte_arr[i] = self.dtypes.decode_string(byte_arr[i]) + return byte_arr + + @property + def any_value(self): + ''' Get the parameter data as a value of 'traditional type'. + + Traditional types are defined in the Parameter section in the [user manual]. + + Returns + ------- + value : int, float, or str + Depending on the `bytes_per_element` field, a traditional type can + be a either a signed byte, signed short, 32-bit float, or a string. + + [user manual]: https://www.c3d.org/docs/C3D_User_Guide.pdf + ''' + if self.bytes_per_element >= 4: + return self.float_value + elif self.bytes_per_element >= 2: + return self.int16_value + elif self.bytes_per_element == -1: + return self.string_value + else: + return self.int8_value + + @property + def any_array(self): + ''' Get the parameter data as an array of 'traditional type'. + + Traditional types are defined in the Parameter section in the [user manual]. + + Returns + ------- + value : array + Depending on the `bytes_per_element` field, a traditional type can + be a either a signed byte, signed short, 32-bit float, or a string. + + [user manual]: https://www.c3d.org/docs/C3D_User_Guide.pdf + ''' + if self.bytes_per_element >= 4: + return self.float_array + elif self.bytes_per_element >= 2: + return self.int16_array + elif self.bytes_per_element == -1: + return self.string_array + else: + return self.int8_array + + @property + def _as_any_uint(self): + ''' Attempt to parse the parameter data as any unsigned integer format. + Checks if the integer is stored as a floating point value. + + Can be used to read 'POINT:FRAMES' or 'POINT:LONG_FRAMES' + when not accessed through `c3d.manager.Manager.last_frame`. + ''' + if self.bytes_per_element >= 4: + # Check if float value representation is an integer + value = self.float_value + if float(value).is_integer(): + return int(value) + return self.uint32_value + elif self.bytes_per_element >= 2: + return self.uint16_value + else: + return self.uint8_value + + +class Param(ParamReadonly): + ''' Wrapper exposing both readable and writable attributes of a `c3d.parameter.ParamData` entry. + ''' + def __init__(self, data): + super(Param, self).__init__(data) + + def readonly(self): + ''' Returns a readonly `c3d.parameter.ParamReadonly` instance. ''' + return ParamReadonly(self._data) + + @property + def bytes(self) -> bytes: + ''' Get or set the parameter bytes. ''' + return self._data.bytes + + @bytes.setter + def bytes(self, value): + self._data.bytes = value diff --git a/c3d/reader.py b/c3d/reader.py new file mode 100644 index 0000000..da5fff4 --- /dev/null +++ b/c3d/reader.py @@ -0,0 +1,336 @@ +'''Contains the Reader class for reading C3D files.''' + +import io +import numpy as np +import struct +import warnings +from .manager import Manager +from .header import Header +from .dtypes import DataTypes +from .utils import DEC_to_IEEE_BYTES + + +class Reader(Manager): + '''This class provides methods for reading the data in a C3D file. + + A C3D file contains metadata and frame-based data describing 3D motion. + + You can iterate over the frames in the file by calling `read_frames()` after + construction: + + >>> r = c3d.Reader(open('capture.c3d', 'rb')) + >>> for frame_no, points, analog in r.read_frames(): + ... print('{0.shape} points in this frame'.format(points)) + ''' + + def __init__(self, handle): + '''Initialize this C3D file by reading header and parameter data. + + Parameters + ---------- + handle : file handle + Read metadata and C3D motion frames from the given file handle. This + handle is assumed to be `seek`-able and `read`-able. The handle must + remain open for the life of the `Reader` instance. The `Reader` does + not `close` the handle. + + Raises + ------ + AssertionError + If the metadata in the C3D file is inconsistent. + ''' + super(Reader, self).__init__(Header(handle)) + + self._handle = handle + + def seek_param_section_header(): + ''' Seek to and read the first 4 byte of the parameter header section ''' + self._handle.seek((self._header.parameter_block - 1) * 512) + # metadata header + return self._handle.read(4) + + # Begin by reading the processor type: + buf = seek_param_section_header() + _, _, parameter_blocks, processor = struct.unpack('BBBB', buf) + self._dtypes = DataTypes(processor) + # Convert header parameters in accordance with the processor type (MIPS format re-reads the header) + self._header._processor_convert(self._dtypes, handle) + + # Restart reading the parameter header after parsing processor type + buf = seek_param_section_header() + + start_byte = self._handle.tell() + endbyte = start_byte + 512 * parameter_blocks - 4 + while self._handle.tell() < endbyte: + chars_in_name, group_id = struct.unpack('bb', self._handle.read(2)) + if group_id == 0 or chars_in_name == 0: + # we've reached the end of the parameter section. + break + name = self._dtypes.decode_string(self._handle.read(abs(chars_in_name))).upper() + + # Read the byte segment associated with the parameter and create a + # separate binary stream object from the data. + offset_to_next, = struct.unpack(['h'][self._dtypes.is_mips], self._handle.read(2)) + if offset_to_next == 0: + # Last parameter, as number of bytes are unknown, + # read the remaining bytes in the parameter section. + bytes = self._handle.read(endbyte - self._handle.tell()) + else: + bytes = self._handle.read(offset_to_next - 2) + buf = io.BytesIO(bytes) + + if group_id > 0: + # We've just started reading a parameter. If its group doesn't + # exist, create a blank one. add the parameter to the group. + group = super(Reader, self).get(group_id) + if group is None: + group = self._add_group(group_id) + group.add_param(name, handle=buf) + else: + # We've just started reading a group. If a group with the + # appropriate numerical id exists already (because we've + # already created it for a parameter), just set the name of + # the group. Otherwise, add a new group. + group_id = abs(group_id) + size, = struct.unpack('B', buf.read(1)) + desc = size and buf.read(size) or '' + group = super(Reader, self).get(group_id) + if group is not None: + self._rename_group(group, name) # Inserts name key + group.desc = desc + else: + self._add_group(group_id, name, desc) + + self._check_metadata() + + def read_frames(self, copy=True, analog_transform=True, check_nan=True, camera_sum=False): + '''Iterate over the data frames from our C3D file handle. + + Parameters + ---------- + copy : bool + If False, the reader returns a reference to the same data buffers + for every frame. The default is True, which causes the reader to + return a unique data buffer for each frame. Set this to False if you + consume frames as you iterate over them, or True if you store them + for later. + analog_transform : bool, default=True + If True, ANALOG:SCALE, ANALOG:GEN_SCALE, and ANALOG:OFFSET transforms + available in the file are applied to the analog channels. + check_nan : bool, default=True + If True, point x,y,z coordinates with nan values will be marked invalidated + and residuals will be set to -1. + camera_sum : bool, default=False + Camera flag bits will be summed, converting the fifth column to a camera visibility counter. + + Returns + ------- + frames : sequence of (frame number, points, analog) + This method generates a sequence of (frame number, points, analog) + tuples, one tuple per frame. The first element of each tuple is the + frame number. The second is a numpy array of parsed, 5D point data + and the third element of each tuple is a numpy array of analog + values that were recorded during the frame. (Often the analog data + are sampled at a higher frequency than the 3D point data, resulting + in multiple analog frames per frame of point data.) + + The first three columns in the returned point data are the (x, y, z) + coordinates of the observed motion capture point. The fourth column + is an estimate of the error for this particular point, and the fifth + column is the number of cameras that observed the point in question. + Both the fourth and fifth values are -1 if the point is considered + to be invalid. + ''' + # Point magnitude scalar, if scale parameter is < 0 data is floating point + # (in which case the magnitude is the absolute value) + scale_mag = abs(self.point_scale) + is_float = self.point_scale < 0 + + if is_float: + point_word_bytes = 4 + else: + point_word_bytes = 2 + points = np.zeros((self.point_used, 5), np.float32) + + # TODO: handle ANALOG:BITS parameter here! + p = self.get('ANALOG:FORMAT') + analog_unsigned = p and p.string_value.strip().upper() == 'UNSIGNED' + if is_float: + analog_dtype = self._dtypes.float32 + analog_word_bytes = 4 + elif analog_unsigned: + # Note*: Floating point is 'always' defined for both analog and point data, according to the standard. + analog_dtype = self._dtypes.uint16 + analog_word_bytes = 2 + # Verify BITS parameter for analog + p = self.get('ANALOG:BITS') + if p and p._as_integer_value / 8 != analog_word_bytes: + raise NotImplementedError('Analog data using {} bits is not supported.'.format(p._as_integer_value)) + else: + analog_dtype = self._dtypes.int16 + analog_word_bytes = 2 + + analog = np.array([], float) + analog_scales, analog_offsets = self.get_analog_transform() + + # Seek to the start point of the data blocks + self._handle.seek((self._header.data_block - 1) * 512) + # Number of values (words) read in regard to POINT/ANALOG data + N_point = 4 * self.point_used + N_analog = self.analog_used * self.analog_per_frame + + # Total bytes per frame + point_bytes = N_point * point_word_bytes + analog_bytes = N_analog * analog_word_bytes + # Parse the data blocks + for frame_no in range(self.first_frame, self.last_frame + 1): + # Read the byte data (used) for the block + raw_bytes = self._handle.read(N_point * point_word_bytes) + raw_analog = self._handle.read(N_analog * analog_word_bytes) + # Verify read pointers (any of the two can be assumed to be 0) + if len(raw_bytes) < point_bytes: + warnings.warn('''reached end of file (EOF) while reading POINT data at frame index {} + and file pointer {}!'''.format(frame_no - self.first_frame, self._handle.tell())) + return + if len(raw_analog) < analog_bytes: + warnings.warn('''reached end of file (EOF) while reading POINT data at frame index {} + and file pointer {}!'''.format(frame_no - self.first_frame, self._handle.tell())) + return + + if is_float: + # Convert every 4 byte words to a float-32 reprensentation + # (the fourth column is still not a float32 representation) + if self._dtypes.is_dec: + # Convert each of the first 6 16-bit words from DEC to IEEE float + points[:, :4] = DEC_to_IEEE_BYTES(raw_bytes).reshape((self.point_used, 4)) + else: # If IEEE or MIPS: + # Convert each of the first 6 16-bit words to native float + points[:, :4] = np.frombuffer(raw_bytes, + dtype=self._dtypes.float32, + count=N_point).reshape((self.point_used, 4)) + + # Cast last word to signed integer in system endian format + last_word = points[:, 3].astype(np.int32) + + else: + # View the bytes as signed 16-bit integers + raw = np.frombuffer(raw_bytes, + dtype=self._dtypes.int16, + count=N_point).reshape((self.point_used, 4)) + # Read the first six 16-bit words as x, y, z coordinates + points[:, :3] = raw[:, :3] * scale_mag + # Cast last word to signed integer in system endian format + last_word = raw[:, 3].astype(np.int16) + + # Parse camera-observed bits and residuals. + # Notes: + # - Invalid sample if residual is equal to -1 (check if word < 0). + # - A residual of 0.0 represent modeled data (filtered or interpolated). + # - Camera and residual words are always 8-bit (1 byte), never 16-bit. + # - If floating point, the byte words are encoded in an integer cast to a float, + # and are written directly in byte form (see the MLS guide). + ## + # Read the residual and camera byte words (Note* if 32 bit word negative sign is discarded). + residual_byte, camera_byte = (last_word & 0x00ff), (last_word & 0x7f00) >> 8 + + # Fourth value is floating-point (scaled) error estimate (residual) + points[:, 3] = residual_byte * scale_mag + + # Determine invalid samples + invalid = last_word < 0 + if check_nan: + is_nan = ~np.all(np.isfinite(points[:, :4]), axis=1) + points[is_nan, :3] = 0.0 + invalid |= is_nan + # Update discarded - sign + points[invalid, 3] = -1 + + # Fifth value is the camera-observation byte + if camera_sum: + # Convert to observation sum + points[:, 4] = sum((camera_byte & (1 << k)) >> k for k in range(7)) + else: + points[:, 4] = camera_byte # .astype(np.float32) + + # Check if analog data exist, and parse if so + if N_analog > 0: + if is_float and self._dtypes.is_dec: + # Convert each of the 16-bit words from DEC to IEEE float + analog = DEC_to_IEEE_BYTES(raw_analog) + else: + # Integer or INTEL/MIPS floating point data can be parsed directly + analog = np.frombuffer(raw_analog, dtype=analog_dtype, count=N_analog) + + # Reformat and convert + analog = analog.reshape((-1, self.analog_used)).T + analog = analog.astype(float) + # Convert analog + analog = (analog - analog_offsets) * analog_scales + + # Output buffers + if copy: + yield frame_no, points.copy(), analog # .copy(), a new array is generated per frame for analog data. + else: + yield frame_no, points, analog + + # Function evaluating EOF, note that data section is written in blocks of 512 + final_byte_index = self._handle.tell() + self._handle.seek(0, 2) # os.SEEK_END) + # Check if more then 1 block remain + if self._handle.tell() - final_byte_index >= 512: + warnings.warn('incomplete reading of data blocks. {} bytes remained after all datablocks were read!'.format( + self._handle.tell() - final_byte_index)) + + @property + def proc_type(self) -> int: + '''Get the processory type associated with the data format in the file. + ''' + return self._dtypes.proc_type + + def to_writer(self, conversion=None): + ''' Converts the reader to a `c3d.writer.Writer` instance using the conversion mode. + + See `c3d.writer.Writer.from_reader()` for supported conversion modes. + ''' + from .writer import Writer + return Writer.from_reader(self, conversion=conversion) + + def get(self, key, default=None): + '''Get a readonly group or parameter. + + Parameters + ---------- + key : str + If this string contains a period (.), then the part before the + period will be used to retrieve a group, and the part after the + period will be used to retrieve a parameter from that group. If this + string does not contain a period, then just a group will be + returned. + default : any + Return this value if the named group and parameter are not found. + + Returns + ------- + value : `c3d.group.GroupReadonly` or `c3d.parameter.ParamReadonly` + Either a group or parameter with the specified name(s). If neither + is found, returns the default value. + ''' + val = super(Reader, self).get(key) + if val: + return val.readonly() + return default + + def items(self): + ''' Get iterable over pairs of (str, `c3d.group.GroupReadonly`) entries. + ''' + return ((k, v.readonly()) for k, v in super(Reader, self).items()) + + def values(self): + ''' Get iterable over `c3d.group.GroupReadonly` entries. + ''' + return (v.readonly() for k, v in super(Reader, self).items()) + + def listed(self): + ''' Get iterable over pairs of (int, `c3d.group.GroupReadonly`) entries. + ''' + return ((k, v.readonly()) for k, v in super(Reader, self).listed()) diff --git a/c3d/utils.py b/c3d/utils.py new file mode 100644 index 0000000..63f684e --- /dev/null +++ b/c3d/utils.py @@ -0,0 +1,154 @@ +''' Trailing utility functions. +''' +import numpy as np +import struct + + +def is_integer(value): + '''Check if value input is integer.''' + return isinstance(value, (int, np.int32, np.int64)) + + +def is_iterable(value): + '''Check if value is iterable.''' + return hasattr(value, '__iter__') + + +def type_npy2struct(dtype): + ''' Convert numpy dtype format to a struct package format string. + ''' + return dtype.byteorder + dtype.char + + +def pack_labels(labels): + ''' Static method used to pack and pad the set of `labels` strings before + passing the output into a `c3d.group.Group.add_str`. + + Parameters + ---------- + labels : iterable + List of strings to pack and pad into a single string suitable for encoding in a Parameter entry. + + Example + ------- + >>> labels = ['RFT1', 'RFT2', 'RFT3', 'LFT1', 'LFT2', 'LFT3'] + >>> param_str, label_max_size = Writer.pack_labels(labels) + >>> writer.point_group.add_str('LABELS', + 'Point labels.', + label_str, + label_max_size, + len(labels)) + + Returns + ------- + param_str : str + String containing `labels` packed into a single variable where + each string is padded to match the longest `labels` string. + label_max_size : int + Number of bytes associated with the longest `label` string, all strings are padded to this length. + ''' + labels = np.ravel(labels) + # Get longest label name + label_max_size = 0 + label_max_size = max(label_max_size, np.max([len(label) for label in labels])) + label_str = ''.join(label.ljust(label_max_size) for label in labels) + return label_str, label_max_size + + +class Decorator(object): + '''Base class for extending (decorating) a python object. + ''' + def __init__(self, decoratee): + self._decoratee = decoratee + + def __getattr__(self, name): + return getattr(self._decoratee, name) + + +def UNPACK_FLOAT_IEEE(uint_32): + '''Unpacks a single 32 bit unsigned int to a IEEE float representation + ''' + return struct.unpack('f', struct.pack("I", uint_32))[0] + + +def DEC_to_IEEE(uint_32): + '''Convert the 32 bit representation of a DEC float to IEEE format. + + Params: + ---- + uint_32 : 32 bit unsigned integer containing the DEC single precision float point bits. + Returns : IEEE formated floating point of the same shape as the input. + ''' + # Follows the bit pattern found: + # http://home.fnal.gov/~yang/Notes/ieee_vs_dec_float.txt + # Further formating descriptions can be found: + # http://www.irig106.org/docs/106-07/appendixO.pdf + # In accodance with the first ref. first & second 16 bit words are placed + # in a big endian 16 bit word representation, and needs to be inverted. + # Second reference describe the DEC->IEEE conversion. + + # Warning! Unsure if NaN numbers are managed appropriately. + + # Shuffle the first two bit words from DEC bit representation to an ordered representation. + # Note that the most significant fraction bits are placed in the first 7 bits. + # + # Below are the DEC layout in accordance with the references: + # ___________________________________________________________________________________ + # | Mantissa (16:0) | SIGN | Exponent (8:0) | Mantissa (23:17) | + # ___________________________________________________________________________________ + # |32- -16| 15 |14- -7|6- -0| + # + # Legend: + # _______________________________________________________ + # | Part (left bit of segment : right bit) | Part | .. + # _______________________________________________________ + # |Bit adress - .. - Bit adress | Bit adress - .. + #### + + # Swap the first and last 16 bits for a consistent alignment of the fraction + reshuffled = ((uint_32 & 0xFFFF0000) >> 16) | ((uint_32 & 0x0000FFFF) << 16) + # After the shuffle each part are in little-endian and ordered as: SIGN-Exponent-Fraction + exp_bits = ((reshuffled & 0xFF000000) - 1) & 0xFF000000 + reshuffled = (reshuffled & 0x00FFFFFF) | exp_bits + return UNPACK_FLOAT_IEEE(reshuffled) + + +def DEC_to_IEEE_BYTES(bytes): + '''Convert byte array containing 32 bit DEC floats to IEEE format. + + Params: + ---- + bytes : Byte array where every 4 bytes represent a single precision DEC float. + Returns : IEEE formated floating point of the same shape as the input. + ''' + + # See comments in DEC_to_IEEE() for DEC format definition + + # Reshuffle + bytes = memoryview(bytes) + reshuffled = np.empty(len(bytes), dtype=np.dtype('B')) + reshuffled[::4] = bytes[2::4] + reshuffled[1::4] = bytes[3::4] + reshuffled[2::4] = bytes[::4] + # Decrement exponent by 2, if exp. > 1 + reshuffled[3::4] = bytes[1::4] + (np.bitwise_and(bytes[1::4], 0x7f) == 0) - 1 + + # There are different ways to adjust for differences in DEC/IEEE representation + # after reshuffle. Two simple methods are: + # 1) Decrement exponent bits by 2, then convert to IEEE. + # 2) Convert to IEEE directly and divide by four. + # 3) Handle edge cases, expensive in python... + # However these are simple methods, and do not accurately convert when: + # 1) Exponent < 2 (without bias), impossible to decrement exponent without adjusting fraction/mantissa. + # 2) Exponent == 0, DEC numbers are then 0 or undefined while IEEE is not. NaN are produced when exponent == 255. + # Here method 1) is used, which mean that only small numbers will be represented incorrectly. + + return np.frombuffer(reshuffled.tobytes(), + dtype=np.float32, + count=int(len(bytes) / 4)) diff --git a/c3d/writer.py b/c3d/writer.py new file mode 100644 index 0000000..742474a --- /dev/null +++ b/c3d/writer.py @@ -0,0 +1,508 @@ +'''Contains the Writer class for writing C3D files.''' + +import copy +import numpy as np +import struct +# import warnings +from . import utils +from .manager import Manager +from .dtypes import DataTypes + + +class Writer(Manager): + '''This class writes metadata and frames to a C3D file. + + For example, to read an existing C3D file, apply some sort of data + processing to the frames, and write out another C3D file:: + + >>> r = c3d.Reader(open('data.c3d', 'rb')) + >>> w = c3d.Writer() + >>> w.add_frames(process_frames_somehow(r.read_frames())) + >>> with open('smoothed.c3d', 'wb') as handle: + >>> w.write(handle) + + Parameters + ---------- + point_rate : float, optional + The frame rate of the data. Defaults to 480. + analog_rate : float, optional + The number of analog samples per frame. Defaults to 0. + point_scale : float, optional + The scale factor for point data. Defaults to -1 (i.e., "check the + POINT:SCALE parameter"). + point_units : str, optional + The units that the point numbers represent. Defaults to ``'mm '``. + gen_scale : float, optional + General scaling factor for analog data. Defaults to 1. + ''' + + def __init__(self, + point_rate=480., + analog_rate=0., + point_scale=-1.): + '''Set minimal metadata for this writer. + + ''' + self._dtypes = DataTypes() # Only support INTEL format from writing + super(Writer, self).__init__() + + # Header properties + self._header.frame_rate = np.float32(point_rate) + self._header.scale_factor = np.float32(point_scale) + self.analog_rate = analog_rate + self._frames = [] + + @staticmethod + def from_reader(reader, conversion=None): + ''' Convert a `c3d.reader.Reader` to a persistent `c3d.writer.Writer` instance. + + Parameters + ---------- + source : `c3d.reader.Reader` + Source to copy data from. + conversion : str + Conversion mode, None is equivalent to the default mode. Supported modes are: + + 'convert' - (Default) Convert the Reader to a Writer + instance and explicitly delete the Reader. + + 'copy' - Reader objects will be deep copied. + + 'copy_metadata' - Similar to 'copy' but only copies metadata and + not point and analog frame data. + + 'copy_shallow' - Similar to 'copy' but group parameters are + not copied. + + 'copy_header' - Similar to 'copy_shallow' but only the + header is copied (frame data is not copied). + + Returns + ------- + param : `c3d.writer.Writer` + A writeable and persistent representation of the `c3d.reader.Reader` object. + + Raises + ------ + ValueError + If mode string is not equivalent to one of the supported modes. + If attempting to convert non-Intel files using mode other than 'shallow_copy'. + ''' + writer = Writer() + # Modes + is_header_only = conversion == 'copy_header' + is_meta_copy = conversion == 'copy_metadata' + is_meta_only = is_header_only or is_meta_copy + is_consume = conversion == 'convert' or conversion is None + is_shallow_copy = conversion == 'shallow_copy' or is_header_only + is_deep_copy = conversion == 'copy' or is_meta_copy + # Verify mode + if not (is_consume or is_shallow_copy or is_deep_copy): + raise ValueError( + "Unknown mode argument %s. Supported modes are: 'consume', 'copy', or 'shallow_copy'".format( + conversion + )) + if not reader._dtypes.is_ieee and not is_shallow_copy: + # Can't copy/consume non-Intel files due to the uncertainty of converting parameter data. + raise ValueError( + "File was read in %s format and only 'shallow_copy' mode is supported for non Intel files!".format( + reader._dtypes.proc_type + )) + + if is_consume: + writer._header = reader._header + writer._groups = reader._groups + elif is_deep_copy: + writer._header = copy.deepcopy(reader._header) + writer._groups = copy.deepcopy(reader._groups) + elif is_shallow_copy: + # Only copy header (no groups) + writer._header = copy.deepcopy(reader._header) + # Reformat header events + writer._header.encode_events(writer._header.events) + + # Transfer a minimal set parameters + writer.set_start_frame(reader.first_frame) + writer.set_point_labels(reader.point_labels) + writer.set_analog_labels(reader.analog_labels) + + gen_scale, analog_scales, analog_offsets = reader.get_analog_transform_parameters() + writer.set_analog_general_scale(gen_scale) + writer.set_analog_scales(analog_scales) + writer.set_analog_offsets(analog_offsets) + + if not is_meta_only: + # Copy frames + for (i, point, analog) in reader.read_frames(copy=True, camera_sum=False): + writer.add_frames((point, analog)) + if is_consume: + # Cleanup + reader._header = None + reader._groups = None + del reader + return writer + + @property + def analog_rate(self): + return super(Writer, self).analog_rate + + @analog_rate.setter + def analog_rate(self, value): + per_frame_rate = value / self.point_rate + assert float(per_frame_rate).is_integer(), "Analog rate must be a multiple of the point rate." + self._header.analog_per_frame = np.uint16(per_frame_rate) + + @property + def numeric_key_max(self): + ''' Get the largest numeric key. + ''' + num = 0 + if len(self._groups) > 0: + for i in self._groups.keys(): + if isinstance(i, int): + num = max(i, num) + return num + + @property + def numeric_key_next(self): + ''' Get a new unique numeric group key. + ''' + return self.numeric_key_max + 1 + + def get_create(self, label): + ''' Get or create a parameter `c3d.group.Group`.''' + label = label.upper() + group = self.get(label) + if group is None: + group = self.add_group(self.numeric_key_next, label, label + ' group') + return group + + @property + def point_group(self): + ''' Get or create the POINT parameter group.''' + return self.get_create('POINT') + + @property + def analog_group(self): + ''' Get or create the ANALOG parameter group.''' + return self.get_create('ANALOG') + + @property + def trial_group(self): + ''' Get or create the TRIAL parameter group.''' + return self.get_create('TRIAL') + + def add_group(self, group_id, name, desc): + '''Add a new parameter group. See Manager.add_group() for more information. + + Returns + ------- + group : `c3d.group.Group` + An editable group instance. + ''' + return super(Writer, self)._add_group(group_id, name, desc) + + def rename_group(self, *args): + ''' Rename a specified parameter group (see Manager._rename_group for args). ''' + super(Writer, self)._rename_group(*args) + + def remove_group(self, *args): + '''Remove the parameter group. (see Manager._rename_group for args). ''' + super(Writer, self)._remove_group(*args) + + def add_frames(self, frames, index=None): + '''Add frames to this writer instance. + + Parameters + ---------- + frames : Single or sequence of (point, analog) pairs + A sequence or frame of frame data to add to the writer. + index : int or None + Insert the frame or sequence at the index (the first sequence frame will be inserted at the given `index`). + Note that the index should be relative to 0 rather then the frame number provided by read_frames()! + ''' + sh = np.shape(frames) + # Single frame + if len(sh) < 2: + frames = [frames] + sh = np.shape(frames) + + # Check data shapes match + if len(self._frames) > 0: + point0, analog0 = self._frames[0] + psh, ash = np.shape(point0), np.shape(analog0) + for f in frames: + if np.shape(f[0]) != psh: + raise ValueError( + 'Shape of analog data does not previous frames. Expexted shape {}, was {}.'.format( + str(psh), str(np.shape(f[0])) + )) + if np.shape(f[1]) != ash: + raise ValueError( + 'Shape of analog data does not previous frames. Expexted shape {}, was {}.'.format( + str(ash), str(np.shape(f[1])) + )) + + # Sequence of invalid shape + if sh[1] != 2: + raise ValueError( + 'Expected frame input to be sequence of point and analog pairs on form (None, 2). ' + + 'Input was of shape {}.'.format(str(sh))) + + if index is not None: + self._frames[index:index] = frames + else: + self._frames.extend(frames) + + def set_point_labels(self, labels): + ''' Set point data labels. + + Parameters + ---------- + labels : iterable + Set POINT:LABELS parameter entry from a set of string labels. + ''' + grp = self.point_group + if labels is None: + grp.add_empty_array('LABELS', 'Point labels.') + else: + label_str, label_max_size = utils.pack_labels(labels) + grp.add_str('LABELS', 'Point labels.', label_str, label_max_size, len(labels)) + + def set_analog_labels(self, labels): + ''' Set analog data labels. + + Parameters + ---------- + labels : iterable + Set ANALOG:LABELS parameter entry from a set of string labels. + ''' + grp = self.analog_group + if labels is None: + grp.add_empty_array('LABELS', 'Analog labels.') + else: + label_str, label_max_size = utils.pack_labels(labels) + grp.add_str('LABELS', 'Analog labels.', label_str, label_max_size, len(labels)) + + def set_analog_general_scale(self, value): + ''' Set ANALOG:GEN_SCALE factor (uniform analog scale factor). + ''' + self.analog_group.set('GEN_SCALE', 'Analog general scale factor', 4, '= UINT16_MAX: + # Should be floating point + group.set('LONG_FRAMES', 'Total frame count', 4, '= 0.0 + raw[~valid, 3] = -1 + raw[valid, :3] = points[valid, :3] / point_scale + raw[valid, 3] = np.bitwise_or(np.rint(points[valid, 3] / scale_mag).astype(np.uint8), + (points[valid, 4].astype(np.uint16) << 8), + dtype=np.uint16) + + # Transform analog data + analog = analog * analog_scales_inv + analog_offsets + analog = analog.T + + # Write + analog = analog.astype(point_dtype) + handle.write(raw.tobytes()) + handle.write(analog.tobytes()) + self._pad_block(handle) diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 7a05fd9..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,153 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/c3d.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/c3d.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/c3d" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/c3d" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/README.rst b/docs/README.rst new file mode 100644 index 0000000..d6acdf7 --- /dev/null +++ b/docs/README.rst @@ -0,0 +1,20 @@ +Reading the docs +----------------- + +Online documentation can be accessed at: https://mattiasfredriksson.github.io/py-c3d/c3d/ + +Building the docs +----------------- + + +Building the docs requires the pdoc3 package:: + + pip install pdoc3 + +Once installed, documentation can be updated from the root directory with the command:: + + pdoc --html c3d --force --config show_source_code=True --output-dir docs -c latex_math=True + +Once updated you can access the documentation in the `docs/c3d/`_ folder. + +.. _docs/c3d/: ./c3d diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..c419263 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman \ No newline at end of file diff --git a/docs/_static/style-tweaks.css b/docs/_static/style-tweaks.css deleted file mode 100644 index 825e6f1..0000000 --- a/docs/_static/style-tweaks.css +++ /dev/null @@ -1 +0,0 @@ -a tt, a:visited tt, a:active tt { color: #369; } diff --git a/docs/_templates/gitwidgets.html b/docs/_templates/gitwidgets.html deleted file mode 100644 index ccc4c9d..0000000 --- a/docs/_templates/gitwidgets.html +++ /dev/null @@ -1,4 +0,0 @@ -
- - -
diff --git a/docs/c3d/dtypes.html b/docs/c3d/dtypes.html new file mode 100644 index 0000000..a7953c6 --- /dev/null +++ b/docs/c3d/dtypes.html @@ -0,0 +1,445 @@ + + + + + + +c3d.dtypes API documentation + + + + + + + + + + + + +
+
+
+

Module c3d.dtypes

+
+
+

State object defining the data types associated with a given .c3d processor format.

+
+ +Expand source code + +
'''
+State object defining the data types associated with a given .c3d processor format.
+'''
+
+import sys
+import codecs
+import numpy as np
+
+PROCESSOR_INTEL = 84
+PROCESSOR_DEC = 85
+PROCESSOR_MIPS = 86
+
+
+class DataTypes(object):
+    ''' Container defining the data types used when parsing byte data.
+        Data types depend on the processor format the file is stored in.
+    '''
+    def __init__(self, proc_type=PROCESSOR_INTEL):
+        self._proc_type = proc_type
+        self._little_endian_sys = sys.byteorder == 'little'
+        self._native = ((self.is_ieee or self.is_dec) and self.little_endian_sys) or \
+                       (self.is_mips and self.big_endian_sys)
+        if self.big_endian_sys:
+            warnings.warn('Systems with native byte order of big-endian are not supported.')
+
+        if self._proc_type == PROCESSOR_MIPS:
+            # Big-Endian (SGI/MIPS format)
+            self.float32 = np.dtype(np.float32).newbyteorder('>')
+            self.float64 = np.dtype(np.float64).newbyteorder('>')
+            self.uint8 = np.uint8
+            self.uint16 = np.dtype(np.uint16).newbyteorder('>')
+            self.uint32 = np.dtype(np.uint32).newbyteorder('>')
+            self.uint64 = np.dtype(np.uint64).newbyteorder('>')
+            self.int8 = np.int8
+            self.int16 = np.dtype(np.int16).newbyteorder('>')
+            self.int32 = np.dtype(np.int32).newbyteorder('>')
+            self.int64 = np.dtype(np.int64).newbyteorder('>')
+        else:
+            # Little-Endian format (Intel or DEC format)
+            self.float32 = np.float32
+            self.float64 = np.float64
+            self.uint8 = np.uint8
+            self.uint16 = np.uint16
+            self.uint32 = np.uint32
+            self.uint64 = np.uint64
+            self.int8 = np.int8
+            self.int16 = np.int16
+            self.int32 = np.int32
+            self.int64 = np.int64
+
+    @property
+    def is_ieee(self) -> bool:
+        ''' True if the associated file is in the Intel format.
+        '''
+        return self._proc_type == PROCESSOR_INTEL
+
+    @property
+    def is_dec(self) -> bool:
+        ''' True if the associated file is in the DEC format.
+        '''
+        return self._proc_type == PROCESSOR_DEC
+
+    @property
+    def is_mips(self) -> bool:
+        ''' True if the associated file is in the SGI/MIPS format.
+        '''
+        return self._proc_type == PROCESSOR_MIPS
+
+    @property
+    def proc_type(self) -> str:
+        ''' Get the processory type associated with the data format in the file.
+        '''
+        processor_type = ['INTEL', 'DEC', 'MIPS']
+        return processor_type[self._proc_type - PROCESSOR_INTEL]
+
+    @property
+    def processor(self) -> int:
+        ''' Get the processor number encoded in the .c3d file.
+        '''
+        return self._proc_type
+
+    @property
+    def native(self) -> bool:
+        ''' True if the native (system) byte order matches the file byte order.
+        '''
+        return self._native
+
+    @property
+    def little_endian_sys(self) -> bool:
+        ''' True if native byte order is little-endian.
+        '''
+        return self._little_endian_sys
+
+    @property
+    def big_endian_sys(self) -> bool:
+        ''' True if native byte order is big-endian.
+        '''
+        return not self._little_endian_sys
+
+    def decode_string(self, bytes) -> str:
+        ''' Decode a byte array to a string.
+        '''
+        # Attempt to decode using different decoders
+        decoders = ['utf-8', 'latin-1']
+        for dec in decoders:
+            try:
+                return codecs.decode(bytes, dec)
+            except UnicodeDecodeError:
+                continue
+        # Revert to using default decoder but replace characters
+        return codecs.decode(bytes, decoders[0], 'replace')
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class DataTypes +(proc_type=84) +
+
+

Container defining the data types used when parsing byte data. +Data types depend on the processor format the file is stored in.

+
+ +Expand source code + +
class DataTypes(object):
+    ''' Container defining the data types used when parsing byte data.
+        Data types depend on the processor format the file is stored in.
+    '''
+    def __init__(self, proc_type=PROCESSOR_INTEL):
+        self._proc_type = proc_type
+        self._little_endian_sys = sys.byteorder == 'little'
+        self._native = ((self.is_ieee or self.is_dec) and self.little_endian_sys) or \
+                       (self.is_mips and self.big_endian_sys)
+        if self.big_endian_sys:
+            warnings.warn('Systems with native byte order of big-endian are not supported.')
+
+        if self._proc_type == PROCESSOR_MIPS:
+            # Big-Endian (SGI/MIPS format)
+            self.float32 = np.dtype(np.float32).newbyteorder('>')
+            self.float64 = np.dtype(np.float64).newbyteorder('>')
+            self.uint8 = np.uint8
+            self.uint16 = np.dtype(np.uint16).newbyteorder('>')
+            self.uint32 = np.dtype(np.uint32).newbyteorder('>')
+            self.uint64 = np.dtype(np.uint64).newbyteorder('>')
+            self.int8 = np.int8
+            self.int16 = np.dtype(np.int16).newbyteorder('>')
+            self.int32 = np.dtype(np.int32).newbyteorder('>')
+            self.int64 = np.dtype(np.int64).newbyteorder('>')
+        else:
+            # Little-Endian format (Intel or DEC format)
+            self.float32 = np.float32
+            self.float64 = np.float64
+            self.uint8 = np.uint8
+            self.uint16 = np.uint16
+            self.uint32 = np.uint32
+            self.uint64 = np.uint64
+            self.int8 = np.int8
+            self.int16 = np.int16
+            self.int32 = np.int32
+            self.int64 = np.int64
+
+    @property
+    def is_ieee(self) -> bool:
+        ''' True if the associated file is in the Intel format.
+        '''
+        return self._proc_type == PROCESSOR_INTEL
+
+    @property
+    def is_dec(self) -> bool:
+        ''' True if the associated file is in the DEC format.
+        '''
+        return self._proc_type == PROCESSOR_DEC
+
+    @property
+    def is_mips(self) -> bool:
+        ''' True if the associated file is in the SGI/MIPS format.
+        '''
+        return self._proc_type == PROCESSOR_MIPS
+
+    @property
+    def proc_type(self) -> str:
+        ''' Get the processory type associated with the data format in the file.
+        '''
+        processor_type = ['INTEL', 'DEC', 'MIPS']
+        return processor_type[self._proc_type - PROCESSOR_INTEL]
+
+    @property
+    def processor(self) -> int:
+        ''' Get the processor number encoded in the .c3d file.
+        '''
+        return self._proc_type
+
+    @property
+    def native(self) -> bool:
+        ''' True if the native (system) byte order matches the file byte order.
+        '''
+        return self._native
+
+    @property
+    def little_endian_sys(self) -> bool:
+        ''' True if native byte order is little-endian.
+        '''
+        return self._little_endian_sys
+
+    @property
+    def big_endian_sys(self) -> bool:
+        ''' True if native byte order is big-endian.
+        '''
+        return not self._little_endian_sys
+
+    def decode_string(self, bytes) -> str:
+        ''' Decode a byte array to a string.
+        '''
+        # Attempt to decode using different decoders
+        decoders = ['utf-8', 'latin-1']
+        for dec in decoders:
+            try:
+                return codecs.decode(bytes, dec)
+            except UnicodeDecodeError:
+                continue
+        # Revert to using default decoder but replace characters
+        return codecs.decode(bytes, decoders[0], 'replace')
+
+

Instance variables

+
+
var big_endian_sys : bool
+
+

True if native byte order is big-endian.

+
+ +Expand source code + +
@property
+def big_endian_sys(self) -> bool:
+    ''' True if native byte order is big-endian.
+    '''
+    return not self._little_endian_sys
+
+
+
var is_dec : bool
+
+

True if the associated file is in the DEC format.

+
+ +Expand source code + +
@property
+def is_dec(self) -> bool:
+    ''' True if the associated file is in the DEC format.
+    '''
+    return self._proc_type == PROCESSOR_DEC
+
+
+
var is_ieee : bool
+
+

True if the associated file is in the Intel format.

+
+ +Expand source code + +
@property
+def is_ieee(self) -> bool:
+    ''' True if the associated file is in the Intel format.
+    '''
+    return self._proc_type == PROCESSOR_INTEL
+
+
+
var is_mips : bool
+
+

True if the associated file is in the SGI/MIPS format.

+
+ +Expand source code + +
@property
+def is_mips(self) -> bool:
+    ''' True if the associated file is in the SGI/MIPS format.
+    '''
+    return self._proc_type == PROCESSOR_MIPS
+
+
+
var little_endian_sys : bool
+
+

True if native byte order is little-endian.

+
+ +Expand source code + +
@property
+def little_endian_sys(self) -> bool:
+    ''' True if native byte order is little-endian.
+    '''
+    return self._little_endian_sys
+
+
+
var native : bool
+
+

True if the native (system) byte order matches the file byte order.

+
+ +Expand source code + +
@property
+def native(self) -> bool:
+    ''' True if the native (system) byte order matches the file byte order.
+    '''
+    return self._native
+
+
+
var proc_type : str
+
+

Get the processory type associated with the data format in the file.

+
+ +Expand source code + +
@property
+def proc_type(self) -> str:
+    ''' Get the processory type associated with the data format in the file.
+    '''
+    processor_type = ['INTEL', 'DEC', 'MIPS']
+    return processor_type[self._proc_type - PROCESSOR_INTEL]
+
+
+
var processor : int
+
+

Get the processor number encoded in the .c3d file.

+
+ +Expand source code + +
@property
+def processor(self) -> int:
+    ''' Get the processor number encoded in the .c3d file.
+    '''
+    return self._proc_type
+
+
+
+

Methods

+
+
+def decode_string(self, bytes) ‑> str +
+
+

Decode a byte array to a string.

+
+ +Expand source code + +
def decode_string(self, bytes) -> str:
+    ''' Decode a byte array to a string.
+    '''
+    # Attempt to decode using different decoders
+    decoders = ['utf-8', 'latin-1']
+    for dec in decoders:
+        try:
+            return codecs.decode(bytes, dec)
+        except UnicodeDecodeError:
+            continue
+    # Revert to using default decoder but replace characters
+    return codecs.decode(bytes, decoders[0], 'replace')
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/c3d/group.html b/docs/c3d/group.html new file mode 100644 index 0000000..95d99b0 --- /dev/null +++ b/docs/c3d/group.html @@ -0,0 +1,2022 @@ + + + + + + +c3d.group API documentation + + + + + + + + + + + + +
+
+
+

Module c3d.group

+
+
+

Classes used to represent the concept of parameter groups in a .c3d file.

+
+ +Expand source code + +
''' Classes used to represent the concept of parameter groups in a .c3d file.
+'''
+import struct
+import numpy as np
+from .parameter import ParamData, Param
+from .utils import Decorator
+
+
+class GroupData(object):
+    '''A group of parameters stored in a C3D file.
+
+    In C3D files, parameters are organized in groups. Each group has a name (key), a
+    description, and a set of named parameters. Each group is also internally associated
+    with a numeric key.
+
+    Attributes
+    ----------
+    dtypes : `c3d.dtypes.DataTypes`
+        Data types object used for parsing.
+    name : str
+        Name of this parameter group.
+    desc : str
+        Description for this parameter group.
+    '''
+
+    def __init__(self, dtypes, name=None, desc=None):
+        self._params = {}
+        self._dtypes = dtypes
+        # Assign through property setters
+        self.set_name(name)
+        self.set_desc(desc)
+
+    def __repr__(self):
+        return '<Group: {}>'.format(self.desc)
+
+    def __contains__(self, key):
+        return key in self._params
+
+    def __getitem__(self, key):
+        return self._params[key]
+
+    @property
+    def binary_size(self) -> int:
+        '''Return the number of bytes to store this group and its parameters.'''
+        return (
+            1 +  # group_id
+            1 + len(self.name.encode('utf-8')) +  # size of name and name bytes
+            2 +  # next offset marker
+            1 + len(self.desc.encode('utf-8')) +  # size of desc and desc bytes
+            sum(p.binary_size for p in self._params.values()))
+
+    def set_name(self, name):
+        ''' Set the group name string. '''
+        if name is None or isinstance(name, str):
+            self.name = name
+        else:
+            raise TypeError('Expected group name to be string, was %s.' % type(name))
+
+    def set_desc(self, desc):
+        ''' Set the Group descriptor.
+        '''
+        if isinstance(desc, bytes):
+            self.desc = self._dtypes.decode_string(desc)
+        elif isinstance(desc, str) or desc is None:
+            self.desc = desc
+        else:
+            raise TypeError('Expected descriptor to be python string, bytes or None, was %s.' % type(desc))
+
+    def add_param(self, name, **kwargs):
+        '''Add a parameter to this group.
+
+        Parameters
+        ----------
+        name : str
+            Name of the parameter to add to this group. The name will
+            automatically be case-normalized.
+
+        See constructor of `c3d.parameter.ParamData` for additional keyword arguments.
+
+        Raises
+        ------
+        TypeError
+            Input arguments are of the wrong type.
+        KeyError
+            Name or numerical key already exist (attempt to overwrite existing data).
+        '''
+        if not isinstance(name, str):
+            raise TypeError("Expected 'name' argument to be a string, was of type {}".format(type(name)))
+        name = name.upper()
+        if name in self._params:
+            raise KeyError('Parameter already exists with key {}'.format(name))
+        self._params[name] = Param(ParamData(name, self._dtypes, **kwargs))
+
+    def remove_param(self, name):
+        '''Remove the specified parameter.
+
+        Parameters
+        ----------
+        name : str
+            Name for the parameter to remove.
+        '''
+        del self._params[name]
+
+    def rename_param(self, name, new_name):
+        ''' Rename a specified parameter group.
+
+        Parameters
+        ----------
+        name : str, or `c3d.group.GroupReadonly`
+            Parameter instance, or name.
+        new_name : str
+            New name for the parameter.
+        Raises
+        ------
+        KeyError
+            If no parameter with the original name exists.
+        ValueError
+            If the new name already exist (attempt to overwrite existing data).
+        '''
+        if new_name in self._params:
+            raise ValueError("Key {} already exist.".format(new_name))
+        if isinstance(name, Param):
+            param = name
+            name = param.name
+        else:
+            # Aquire instance using id
+            param = self._params[name]
+        del self._params[name]
+        self._params[new_name] = param
+
+    def write(self, group_id, handle):
+        '''Write this parameter group, with parameters, to a file handle.
+
+        Parameters
+        ----------
+        group_id : int
+            The numerical ID of the group.
+        handle : file handle
+            An open, writable, binary file handle.
+        '''
+        name = self.name.encode('utf-8')
+        desc = self.desc.encode('utf-8')
+        handle.write(struct.pack('bb', len(name), -group_id))
+        handle.write(name)
+        handle.write(struct.pack('<h', 3 + len(desc)))
+        handle.write(struct.pack('B', len(desc)))
+        handle.write(desc)
+        for param in self._params.values():
+            param._data.write(group_id, handle)
+
+
+class GroupReadonly(object):
+    ''' Wrapper exposing readonly attributes of a `c3d.group.GroupData` entry.
+    '''
+    def __init__(self, data):
+        self._data = data
+
+    def __contains__(self, key):
+        return key in self._data._params
+
+    def __eq__(self, other):
+        return self._data is other._data
+
+    @property
+    def name(self) -> str:
+        ''' Access group name. '''
+        return self._data.name
+
+    @property
+    def desc(self) -> str:
+        '''Access group descriptor. '''
+        return self._data.desc
+
+    def items(self):
+        ''' Get iterator for paramater key-entry pairs. '''
+        return ((k, v.readonly()) for k, v in self._data._params.items())
+
+    def values(self):
+        ''' Get iterator for parameter entries. '''
+        return (v.readonly() for v in self._data._params.values())
+
+    def keys(self):
+        ''' Get iterator for parameter entry keys. '''
+        return self._data._params.keys()
+
+    def get(self, key, default=None):
+        '''Get a readonly parameter by key.
+
+        Parameters
+        ----------
+        key : any
+            Parameter key to look up in this group.
+        default : any, optional
+            Value to return if the key is not found. Defaults to None.
+
+        Returns
+        -------
+        param : :class:`ParamReadable`
+            A parameter from the current group.
+        '''
+        val = self._data._params.get(key, default)
+        if val:
+            return val.readonly()
+        return default
+
+    def get_int8(self, key):
+        '''Get the value of the given parameter as an 8-bit signed integer.'''
+        return self._data[key.upper()].int8_value
+
+    def get_uint8(self, key):
+        '''Get the value of the given parameter as an 8-bit unsigned integer.'''
+        return self._data[key.upper()].uint8_value
+
+    def get_int16(self, key):
+        '''Get the value of the given parameter as a 16-bit signed integer.'''
+        return self._data[key.upper()].int16_value
+
+    def get_uint16(self, key):
+        '''Get the value of the given parameter as a 16-bit unsigned integer.'''
+        return self._data[key.upper()].uint16_value
+
+    def get_int32(self, key):
+        '''Get the value of the given parameter as a 32-bit signed integer.'''
+        return self._data[key.upper()].int32_value
+
+    def get_uint32(self, key):
+        '''Get the value of the given parameter as a 32-bit unsigned integer.'''
+        return self._data[key.upper()].uint32_value
+
+    def get_float(self, key):
+        '''Get the value of the given parameter as a 32-bit float.'''
+        return self._data[key.upper()].float_value
+
+    def get_bytes(self, key):
+        '''Get the value of the given parameter as a byte array.'''
+        return self._data[key.upper()].bytes_value
+
+    def get_string(self, key):
+        '''Get the value of the given parameter as a string.'''
+        return self._data[key.upper()].string_value
+
+
+class Group(GroupReadonly):
+    ''' Wrapper exposing readable and writeable attributes of a `c3d.group.GroupData` entry.
+    '''
+    def __init__(self, data):
+        super(Group, self).__init__(data)
+
+    def readonly(self):
+        ''' Returns a `c3d.group.GroupReadonly` instance with readonly access. '''
+        return GroupReadonly(self._data)
+
+    @property
+    def name(self) -> str:
+        ''' Get or set name. '''
+        return self._data.name
+
+    @name.setter
+    def name(self, value) -> str:
+        self._data.set_name(value)
+
+    @property
+    def desc(self) -> str:
+        ''' Get or set descriptor. '''
+        return self._data.desc
+
+    @desc.setter
+    def desc(self, value) -> str:
+        self._data.set_desc(value)
+
+    def items(self):
+        ''' Iterator for paramater key-entry pairs. '''
+        return ((k, v) for k, v in self._data._params.items())
+
+    def values(self):
+        ''' Iterator iterator for parameter entries. '''
+        return (v for v in self._data._params.values())
+
+    def get(self, key, default=None):
+        '''Get a parameter by key.
+
+        Parameters
+        ----------
+        key : any
+            Parameter key to look up in this group.
+        default : any, optional
+            Value to return if the key is not found. Defaults to None.
+
+        Returns
+        -------
+        param : :class:`ParamReadable`
+            A parameter from the current group.
+        '''
+        return self._data._params.get(key, default)
+
+    #
+    #  Forward param editing
+    #
+    def add_param(self, name, **kwargs):
+        '''Add a parameter to this group.
+
+        See constructor of `c3d.parameter.ParamData` for additional keyword arguments.
+        '''
+        self._data.add_param(name, **kwargs)
+
+    def remove_param(self, name):
+        '''Remove the specified parameter.
+
+        Parameters
+        ----------
+        name : str
+            Name for the parameter to remove.
+        '''
+        self._data.remove_param(name)
+
+    def rename_param(self, name, new_name):
+        ''' Rename a specified parameter group.
+
+        Parameters
+        ----------
+        See arguments in `c3d.group.GroupData.rename_param`.
+        '''
+        self._data.rename_param(name, new_name)
+
+    #
+    #   Convenience functions for adding parameters.
+    #
+    def add(self, name, desc, bpe, format, data, *dimensions):
+        ''' Add a parameter with `data` package formated in accordance with `format`.
+
+        Convenience function for `c3d.group.GroupData.add_param` calling struct.pack() on `data`.
+
+        Example:
+
+        >>> group.set('RATE', 'Point data sample rate', 4, '<f', 100)
+
+        Parameters
+        ----------
+        name : str
+            Parameter name.
+        desc : str
+            Parameter descriptor.
+        bpe : int
+            Number of bytes for each atomic element.
+        format : str or None
+            `struct.format()` compatible format string see:
+            https://docs.python.org/3/library/struct.html#format-characters
+        *dimensions : int, optional
+            Shape associated with the data (if the data argument represents multiple elements).
+        '''
+        if isinstance(data, bytes):
+            pass
+        else:
+            data = struct.pack(format, data)
+
+        self.add_param(name,
+                       desc=desc,
+                       bytes_per_element=bpe,
+                       bytes=data,
+                       dimensions=list(dimensions))
+
+    def add_array(self, name, desc, data, dtype=None):
+        '''Add a parameter with the `data` package.
+
+        Parameters
+        ----------
+        name : str
+            Parameter name.
+        desc : str
+            Parameter descriptor.
+        data : np.ndarray, or iterable
+            Data array to encode in the parameter.
+        dtype : np.dtype, optional
+            Numpy data type used to encode the array (optional only if `data.dtype` returns a numpy type).
+        '''
+        if not isinstance(data, np.ndarray):
+            if dtype is None:
+                dtype = data.dtype
+            data = np.array(data, dtype=dtype)
+        elif dtype is None:
+            dtype = data.dtype
+
+        self.add_param(name,
+                       desc=desc,
+                       bytes_per_element=dtype.itemsize,
+                       bytes=data.tobytes(),
+                       dimensions=data.shape[::-1])
+
+    def add_str(self, name, desc, data, *dimensions):
+        ''' Add a string parameter.
+
+        Parameters
+        ----------
+        name : str
+            Parameter name.
+        desc : str
+            Parameter descriptor.
+        data : str
+            String to encode in the parameter.
+        *dimensions : int, optional
+            Shape associated with the string (if the string represents multiple elements).
+        '''
+        shape = list(dimensions)
+        self.add_param(name,
+                       desc=desc,
+                       bytes_per_element=-1,
+                       bytes=data.encode('utf-8'),
+                       dimensions=shape or [len(data)])
+
+    def add_empty_array(self, name, desc=''):
+        ''' Add an empty parameter block.
+
+        Parameters
+        ----------
+        name : str
+            Parameter name.
+        '''
+        self.add_param(name, desc=desc,
+                       bytes_per_element=0, dimensions=[0])
+
+    #
+    #   Convenience functions for adding or overwriting parameters.
+    #
+    def set(self, name, *args, **kwargs):
+        ''' Add or overwrite a parameter with 'bytes' package formated in accordance with 'format'.
+
+        See arguments in `c3d.group.Group.add`.
+        '''
+        try:
+            self.remove_param(name)
+        except KeyError as e:
+            pass
+        self.add(name, *args, **kwargs)
+
+    def set_str(self, name, *args, **kwargs):
+        ''' Add or overwrite a string parameter.
+
+        See arguments in `c3d.group.Group.add_str`.
+        '''
+        try:
+            self.remove_param(name)
+        except KeyError as e:
+            pass
+        self.add_str(name, *args, **kwargs)
+
+    def set_array(self, name, *args, **kwargs):
+        ''' Add or overwrite a parameter with the `data` package.
+
+        See arguments in `c3d.group.Group.add_array`.
+        '''
+        try:
+            self.remove_param(name)
+        except KeyError as e:
+            pass
+        self.add_array(name, *args, **kwargs)
+
+    def set_empty_array(self, name, *args, **kwargs):
+        ''' Add an empty parameter block.
+
+        See arguments in `c3d.group.Group.add_empty_array`.
+        '''
+        try:
+            self.remove_param(name)
+        except KeyError as e:
+            pass
+        self.add_empty_array(name, *args, **kwargs)
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Group +(data) +
+
+

Wrapper exposing readable and writeable attributes of a GroupData entry.

+
+ +Expand source code + +
class Group(GroupReadonly):
+    ''' Wrapper exposing readable and writeable attributes of a `c3d.group.GroupData` entry.
+    '''
+    def __init__(self, data):
+        super(Group, self).__init__(data)
+
+    def readonly(self):
+        ''' Returns a `c3d.group.GroupReadonly` instance with readonly access. '''
+        return GroupReadonly(self._data)
+
+    @property
+    def name(self) -> str:
+        ''' Get or set name. '''
+        return self._data.name
+
+    @name.setter
+    def name(self, value) -> str:
+        self._data.set_name(value)
+
+    @property
+    def desc(self) -> str:
+        ''' Get or set descriptor. '''
+        return self._data.desc
+
+    @desc.setter
+    def desc(self, value) -> str:
+        self._data.set_desc(value)
+
+    def items(self):
+        ''' Iterator for paramater key-entry pairs. '''
+        return ((k, v) for k, v in self._data._params.items())
+
+    def values(self):
+        ''' Iterator iterator for parameter entries. '''
+        return (v for v in self._data._params.values())
+
+    def get(self, key, default=None):
+        '''Get a parameter by key.
+
+        Parameters
+        ----------
+        key : any
+            Parameter key to look up in this group.
+        default : any, optional
+            Value to return if the key is not found. Defaults to None.
+
+        Returns
+        -------
+        param : :class:`ParamReadable`
+            A parameter from the current group.
+        '''
+        return self._data._params.get(key, default)
+
+    #
+    #  Forward param editing
+    #
+    def add_param(self, name, **kwargs):
+        '''Add a parameter to this group.
+
+        See constructor of `c3d.parameter.ParamData` for additional keyword arguments.
+        '''
+        self._data.add_param(name, **kwargs)
+
+    def remove_param(self, name):
+        '''Remove the specified parameter.
+
+        Parameters
+        ----------
+        name : str
+            Name for the parameter to remove.
+        '''
+        self._data.remove_param(name)
+
+    def rename_param(self, name, new_name):
+        ''' Rename a specified parameter group.
+
+        Parameters
+        ----------
+        See arguments in `c3d.group.GroupData.rename_param`.
+        '''
+        self._data.rename_param(name, new_name)
+
+    #
+    #   Convenience functions for adding parameters.
+    #
+    def add(self, name, desc, bpe, format, data, *dimensions):
+        ''' Add a parameter with `data` package formated in accordance with `format`.
+
+        Convenience function for `c3d.group.GroupData.add_param` calling struct.pack() on `data`.
+
+        Example:
+
+        >>> group.set('RATE', 'Point data sample rate', 4, '<f', 100)
+
+        Parameters
+        ----------
+        name : str
+            Parameter name.
+        desc : str
+            Parameter descriptor.
+        bpe : int
+            Number of bytes for each atomic element.
+        format : str or None
+            `struct.format()` compatible format string see:
+            https://docs.python.org/3/library/struct.html#format-characters
+        *dimensions : int, optional
+            Shape associated with the data (if the data argument represents multiple elements).
+        '''
+        if isinstance(data, bytes):
+            pass
+        else:
+            data = struct.pack(format, data)
+
+        self.add_param(name,
+                       desc=desc,
+                       bytes_per_element=bpe,
+                       bytes=data,
+                       dimensions=list(dimensions))
+
+    def add_array(self, name, desc, data, dtype=None):
+        '''Add a parameter with the `data` package.
+
+        Parameters
+        ----------
+        name : str
+            Parameter name.
+        desc : str
+            Parameter descriptor.
+        data : np.ndarray, or iterable
+            Data array to encode in the parameter.
+        dtype : np.dtype, optional
+            Numpy data type used to encode the array (optional only if `data.dtype` returns a numpy type).
+        '''
+        if not isinstance(data, np.ndarray):
+            if dtype is None:
+                dtype = data.dtype
+            data = np.array(data, dtype=dtype)
+        elif dtype is None:
+            dtype = data.dtype
+
+        self.add_param(name,
+                       desc=desc,
+                       bytes_per_element=dtype.itemsize,
+                       bytes=data.tobytes(),
+                       dimensions=data.shape[::-1])
+
+    def add_str(self, name, desc, data, *dimensions):
+        ''' Add a string parameter.
+
+        Parameters
+        ----------
+        name : str
+            Parameter name.
+        desc : str
+            Parameter descriptor.
+        data : str
+            String to encode in the parameter.
+        *dimensions : int, optional
+            Shape associated with the string (if the string represents multiple elements).
+        '''
+        shape = list(dimensions)
+        self.add_param(name,
+                       desc=desc,
+                       bytes_per_element=-1,
+                       bytes=data.encode('utf-8'),
+                       dimensions=shape or [len(data)])
+
+    def add_empty_array(self, name, desc=''):
+        ''' Add an empty parameter block.
+
+        Parameters
+        ----------
+        name : str
+            Parameter name.
+        '''
+        self.add_param(name, desc=desc,
+                       bytes_per_element=0, dimensions=[0])
+
+    #
+    #   Convenience functions for adding or overwriting parameters.
+    #
+    def set(self, name, *args, **kwargs):
+        ''' Add or overwrite a parameter with 'bytes' package formated in accordance with 'format'.
+
+        See arguments in `c3d.group.Group.add`.
+        '''
+        try:
+            self.remove_param(name)
+        except KeyError as e:
+            pass
+        self.add(name, *args, **kwargs)
+
+    def set_str(self, name, *args, **kwargs):
+        ''' Add or overwrite a string parameter.
+
+        See arguments in `c3d.group.Group.add_str`.
+        '''
+        try:
+            self.remove_param(name)
+        except KeyError as e:
+            pass
+        self.add_str(name, *args, **kwargs)
+
+    def set_array(self, name, *args, **kwargs):
+        ''' Add or overwrite a parameter with the `data` package.
+
+        See arguments in `c3d.group.Group.add_array`.
+        '''
+        try:
+            self.remove_param(name)
+        except KeyError as e:
+            pass
+        self.add_array(name, *args, **kwargs)
+
+    def set_empty_array(self, name, *args, **kwargs):
+        ''' Add an empty parameter block.
+
+        See arguments in `c3d.group.Group.add_empty_array`.
+        '''
+        try:
+            self.remove_param(name)
+        except KeyError as e:
+            pass
+        self.add_empty_array(name, *args, **kwargs)
+
+

Ancestors

+ +

Instance variables

+
+
var desc : str
+
+

Get or set descriptor.

+
+ +Expand source code + +
@property
+def desc(self) -> str:
+    ''' Get or set descriptor. '''
+    return self._data.desc
+
+
+
var name : str
+
+

Get or set name.

+
+ +Expand source code + +
@property
+def name(self) -> str:
+    ''' Get or set name. '''
+    return self._data.name
+
+
+
+

Methods

+
+
+def add(self, name, desc, bpe, format, data, *dimensions) +
+
+

Add a parameter with data package formated in accordance with format.

+

Convenience function for GroupData.add_param() calling struct.pack() on data.

+

Example:

+
>>> group.set('RATE', 'Point data sample rate', 4, '<f', 100)
+
+

Parameters

+
+
name : str
+
Parameter name.
+
desc : str
+
Parameter descriptor.
+
bpe : int
+
Number of bytes for each atomic element.
+
format : str or None
+
struct.format() compatible format string see: +https://docs.python.org/3/library/struct.html#format-characters
+
*dimensions : int, optional
+
Shape associated with the data (if the data argument represents multiple elements).
+
+
+ +Expand source code + +
def add(self, name, desc, bpe, format, data, *dimensions):
+    ''' Add a parameter with `data` package formated in accordance with `format`.
+
+    Convenience function for `c3d.group.GroupData.add_param` calling struct.pack() on `data`.
+
+    Example:
+
+    >>> group.set('RATE', 'Point data sample rate', 4, '<f', 100)
+
+    Parameters
+    ----------
+    name : str
+        Parameter name.
+    desc : str
+        Parameter descriptor.
+    bpe : int
+        Number of bytes for each atomic element.
+    format : str or None
+        `struct.format()` compatible format string see:
+        https://docs.python.org/3/library/struct.html#format-characters
+    *dimensions : int, optional
+        Shape associated with the data (if the data argument represents multiple elements).
+    '''
+    if isinstance(data, bytes):
+        pass
+    else:
+        data = struct.pack(format, data)
+
+    self.add_param(name,
+                   desc=desc,
+                   bytes_per_element=bpe,
+                   bytes=data,
+                   dimensions=list(dimensions))
+
+
+
+def add_array(self, name, desc, data, dtype=None) +
+
+

Add a parameter with the data package.

+

Parameters

+
+
name : str
+
Parameter name.
+
desc : str
+
Parameter descriptor.
+
data : np.ndarray, or iterable
+
Data array to encode in the parameter.
+
dtype : np.dtype, optional
+
Numpy data type used to encode the array (optional only if data.dtype returns a numpy type).
+
+
+ +Expand source code + +
def add_array(self, name, desc, data, dtype=None):
+    '''Add a parameter with the `data` package.
+
+    Parameters
+    ----------
+    name : str
+        Parameter name.
+    desc : str
+        Parameter descriptor.
+    data : np.ndarray, or iterable
+        Data array to encode in the parameter.
+    dtype : np.dtype, optional
+        Numpy data type used to encode the array (optional only if `data.dtype` returns a numpy type).
+    '''
+    if not isinstance(data, np.ndarray):
+        if dtype is None:
+            dtype = data.dtype
+        data = np.array(data, dtype=dtype)
+    elif dtype is None:
+        dtype = data.dtype
+
+    self.add_param(name,
+                   desc=desc,
+                   bytes_per_element=dtype.itemsize,
+                   bytes=data.tobytes(),
+                   dimensions=data.shape[::-1])
+
+
+
+def add_empty_array(self, name, desc='') +
+
+

Add an empty parameter block.

+

Parameters

+
+
name : str
+
Parameter name.
+
+
+ +Expand source code + +
def add_empty_array(self, name, desc=''):
+    ''' Add an empty parameter block.
+
+    Parameters
+    ----------
+    name : str
+        Parameter name.
+    '''
+    self.add_param(name, desc=desc,
+                   bytes_per_element=0, dimensions=[0])
+
+
+
+def add_param(self, name, **kwargs) +
+
+

Add a parameter to this group.

+

See constructor of ParamData for additional keyword arguments.

+
+ +Expand source code + +
def add_param(self, name, **kwargs):
+    '''Add a parameter to this group.
+
+    See constructor of `c3d.parameter.ParamData` for additional keyword arguments.
+    '''
+    self._data.add_param(name, **kwargs)
+
+
+
+def add_str(self, name, desc, data, *dimensions) +
+
+

Add a string parameter.

+

Parameters

+
+
name : str
+
Parameter name.
+
desc : str
+
Parameter descriptor.
+
data : str
+
String to encode in the parameter.
+
*dimensions : int, optional
+
Shape associated with the string (if the string represents multiple elements).
+
+
+ +Expand source code + +
def add_str(self, name, desc, data, *dimensions):
+    ''' Add a string parameter.
+
+    Parameters
+    ----------
+    name : str
+        Parameter name.
+    desc : str
+        Parameter descriptor.
+    data : str
+        String to encode in the parameter.
+    *dimensions : int, optional
+        Shape associated with the string (if the string represents multiple elements).
+    '''
+    shape = list(dimensions)
+    self.add_param(name,
+                   desc=desc,
+                   bytes_per_element=-1,
+                   bytes=data.encode('utf-8'),
+                   dimensions=shape or [len(data)])
+
+
+
+def get(self, key, default=None) +
+
+

Get a parameter by key.

+

Parameters

+
+
key : any
+
Parameter key to look up in this group.
+
default : any, optional
+
Value to return if the key is not found. Defaults to None.
+
+

Returns

+
+
param : :class:ParamReadable``
+
A parameter from the current group.
+
+
+ +Expand source code + +
def get(self, key, default=None):
+    '''Get a parameter by key.
+
+    Parameters
+    ----------
+    key : any
+        Parameter key to look up in this group.
+    default : any, optional
+        Value to return if the key is not found. Defaults to None.
+
+    Returns
+    -------
+    param : :class:`ParamReadable`
+        A parameter from the current group.
+    '''
+    return self._data._params.get(key, default)
+
+
+
+def items(self) +
+
+

Iterator for paramater key-entry pairs.

+
+ +Expand source code + +
def items(self):
+    ''' Iterator for paramater key-entry pairs. '''
+    return ((k, v) for k, v in self._data._params.items())
+
+
+
+def readonly(self) +
+
+

Returns a GroupReadonly instance with readonly access.

+
+ +Expand source code + +
def readonly(self):
+    ''' Returns a `c3d.group.GroupReadonly` instance with readonly access. '''
+    return GroupReadonly(self._data)
+
+
+
+def remove_param(self, name) +
+
+

Remove the specified parameter.

+

Parameters

+
+
name : str
+
Name for the parameter to remove.
+
+
+ +Expand source code + +
def remove_param(self, name):
+    '''Remove the specified parameter.
+
+    Parameters
+    ----------
+    name : str
+        Name for the parameter to remove.
+    '''
+    self._data.remove_param(name)
+
+
+
+def rename_param(self, name, new_name) +
+
+

Rename a specified parameter group.

+

Parameters

+

See arguments in GroupData.rename_param().

+
+ +Expand source code + +
def rename_param(self, name, new_name):
+    ''' Rename a specified parameter group.
+
+    Parameters
+    ----------
+    See arguments in `c3d.group.GroupData.rename_param`.
+    '''
+    self._data.rename_param(name, new_name)
+
+
+
+def set(self, name, *args, **kwargs) +
+
+

Add or overwrite a parameter with 'bytes' package formated in accordance with 'format'.

+

See arguments in Group.add().

+
+ +Expand source code + +
def set(self, name, *args, **kwargs):
+    ''' Add or overwrite a parameter with 'bytes' package formated in accordance with 'format'.
+
+    See arguments in `c3d.group.Group.add`.
+    '''
+    try:
+        self.remove_param(name)
+    except KeyError as e:
+        pass
+    self.add(name, *args, **kwargs)
+
+
+
+def set_array(self, name, *args, **kwargs) +
+
+

Add or overwrite a parameter with the data package.

+

See arguments in Group.add_array().

+
+ +Expand source code + +
def set_array(self, name, *args, **kwargs):
+    ''' Add or overwrite a parameter with the `data` package.
+
+    See arguments in `c3d.group.Group.add_array`.
+    '''
+    try:
+        self.remove_param(name)
+    except KeyError as e:
+        pass
+    self.add_array(name, *args, **kwargs)
+
+
+
+def set_empty_array(self, name, *args, **kwargs) +
+
+

Add an empty parameter block.

+

See arguments in Group.add_empty_array().

+
+ +Expand source code + +
def set_empty_array(self, name, *args, **kwargs):
+    ''' Add an empty parameter block.
+
+    See arguments in `c3d.group.Group.add_empty_array`.
+    '''
+    try:
+        self.remove_param(name)
+    except KeyError as e:
+        pass
+    self.add_empty_array(name, *args, **kwargs)
+
+
+
+def set_str(self, name, *args, **kwargs) +
+
+

Add or overwrite a string parameter.

+

See arguments in Group.add_str().

+
+ +Expand source code + +
def set_str(self, name, *args, **kwargs):
+    ''' Add or overwrite a string parameter.
+
+    See arguments in `c3d.group.Group.add_str`.
+    '''
+    try:
+        self.remove_param(name)
+    except KeyError as e:
+        pass
+    self.add_str(name, *args, **kwargs)
+
+
+
+def values(self) +
+
+

Iterator iterator for parameter entries.

+
+ +Expand source code + +
def values(self):
+    ''' Iterator iterator for parameter entries. '''
+    return (v for v in self._data._params.values())
+
+
+
+

Inherited members

+ +
+
+class GroupData +(dtypes, name=None, desc=None) +
+
+

A group of parameters stored in a C3D file.

+

In C3D files, parameters are organized in groups. Each group has a name (key), a +description, and a set of named parameters. Each group is also internally associated +with a numeric key.

+

Attributes

+
+
dtypes : DataTypes
+
Data types object used for parsing.
+
name : str
+
Name of this parameter group.
+
desc : str
+
Description for this parameter group.
+
+
+ +Expand source code + +
class GroupData(object):
+    '''A group of parameters stored in a C3D file.
+
+    In C3D files, parameters are organized in groups. Each group has a name (key), a
+    description, and a set of named parameters. Each group is also internally associated
+    with a numeric key.
+
+    Attributes
+    ----------
+    dtypes : `c3d.dtypes.DataTypes`
+        Data types object used for parsing.
+    name : str
+        Name of this parameter group.
+    desc : str
+        Description for this parameter group.
+    '''
+
+    def __init__(self, dtypes, name=None, desc=None):
+        self._params = {}
+        self._dtypes = dtypes
+        # Assign through property setters
+        self.set_name(name)
+        self.set_desc(desc)
+
+    def __repr__(self):
+        return '<Group: {}>'.format(self.desc)
+
+    def __contains__(self, key):
+        return key in self._params
+
+    def __getitem__(self, key):
+        return self._params[key]
+
+    @property
+    def binary_size(self) -> int:
+        '''Return the number of bytes to store this group and its parameters.'''
+        return (
+            1 +  # group_id
+            1 + len(self.name.encode('utf-8')) +  # size of name and name bytes
+            2 +  # next offset marker
+            1 + len(self.desc.encode('utf-8')) +  # size of desc and desc bytes
+            sum(p.binary_size for p in self._params.values()))
+
+    def set_name(self, name):
+        ''' Set the group name string. '''
+        if name is None or isinstance(name, str):
+            self.name = name
+        else:
+            raise TypeError('Expected group name to be string, was %s.' % type(name))
+
+    def set_desc(self, desc):
+        ''' Set the Group descriptor.
+        '''
+        if isinstance(desc, bytes):
+            self.desc = self._dtypes.decode_string(desc)
+        elif isinstance(desc, str) or desc is None:
+            self.desc = desc
+        else:
+            raise TypeError('Expected descriptor to be python string, bytes or None, was %s.' % type(desc))
+
+    def add_param(self, name, **kwargs):
+        '''Add a parameter to this group.
+
+        Parameters
+        ----------
+        name : str
+            Name of the parameter to add to this group. The name will
+            automatically be case-normalized.
+
+        See constructor of `c3d.parameter.ParamData` for additional keyword arguments.
+
+        Raises
+        ------
+        TypeError
+            Input arguments are of the wrong type.
+        KeyError
+            Name or numerical key already exist (attempt to overwrite existing data).
+        '''
+        if not isinstance(name, str):
+            raise TypeError("Expected 'name' argument to be a string, was of type {}".format(type(name)))
+        name = name.upper()
+        if name in self._params:
+            raise KeyError('Parameter already exists with key {}'.format(name))
+        self._params[name] = Param(ParamData(name, self._dtypes, **kwargs))
+
+    def remove_param(self, name):
+        '''Remove the specified parameter.
+
+        Parameters
+        ----------
+        name : str
+            Name for the parameter to remove.
+        '''
+        del self._params[name]
+
+    def rename_param(self, name, new_name):
+        ''' Rename a specified parameter group.
+
+        Parameters
+        ----------
+        name : str, or `c3d.group.GroupReadonly`
+            Parameter instance, or name.
+        new_name : str
+            New name for the parameter.
+        Raises
+        ------
+        KeyError
+            If no parameter with the original name exists.
+        ValueError
+            If the new name already exist (attempt to overwrite existing data).
+        '''
+        if new_name in self._params:
+            raise ValueError("Key {} already exist.".format(new_name))
+        if isinstance(name, Param):
+            param = name
+            name = param.name
+        else:
+            # Aquire instance using id
+            param = self._params[name]
+        del self._params[name]
+        self._params[new_name] = param
+
+    def write(self, group_id, handle):
+        '''Write this parameter group, with parameters, to a file handle.
+
+        Parameters
+        ----------
+        group_id : int
+            The numerical ID of the group.
+        handle : file handle
+            An open, writable, binary file handle.
+        '''
+        name = self.name.encode('utf-8')
+        desc = self.desc.encode('utf-8')
+        handle.write(struct.pack('bb', len(name), -group_id))
+        handle.write(name)
+        handle.write(struct.pack('<h', 3 + len(desc)))
+        handle.write(struct.pack('B', len(desc)))
+        handle.write(desc)
+        for param in self._params.values():
+            param._data.write(group_id, handle)
+
+

Instance variables

+
+
var binary_size : int
+
+

Return the number of bytes to store this group and its parameters.

+
+ +Expand source code + +
@property
+def binary_size(self) -> int:
+    '''Return the number of bytes to store this group and its parameters.'''
+    return (
+        1 +  # group_id
+        1 + len(self.name.encode('utf-8')) +  # size of name and name bytes
+        2 +  # next offset marker
+        1 + len(self.desc.encode('utf-8')) +  # size of desc and desc bytes
+        sum(p.binary_size for p in self._params.values()))
+
+
+
+

Methods

+
+
+def add_param(self, name, **kwargs) +
+
+

Add a parameter to this group.

+

Parameters

+
+
name : str
+
Name of the parameter to add to this group. The name will +automatically be case-normalized.
+
+

See constructor of ParamData for additional keyword arguments.

+

Raises

+
+
TypeError
+
Input arguments are of the wrong type.
+
KeyError
+
Name or numerical key already exist (attempt to overwrite existing data).
+
+
+ +Expand source code + +
def add_param(self, name, **kwargs):
+    '''Add a parameter to this group.
+
+    Parameters
+    ----------
+    name : str
+        Name of the parameter to add to this group. The name will
+        automatically be case-normalized.
+
+    See constructor of `c3d.parameter.ParamData` for additional keyword arguments.
+
+    Raises
+    ------
+    TypeError
+        Input arguments are of the wrong type.
+    KeyError
+        Name or numerical key already exist (attempt to overwrite existing data).
+    '''
+    if not isinstance(name, str):
+        raise TypeError("Expected 'name' argument to be a string, was of type {}".format(type(name)))
+    name = name.upper()
+    if name in self._params:
+        raise KeyError('Parameter already exists with key {}'.format(name))
+    self._params[name] = Param(ParamData(name, self._dtypes, **kwargs))
+
+
+
+def remove_param(self, name) +
+
+

Remove the specified parameter.

+

Parameters

+
+
name : str
+
Name for the parameter to remove.
+
+
+ +Expand source code + +
def remove_param(self, name):
+    '''Remove the specified parameter.
+
+    Parameters
+    ----------
+    name : str
+        Name for the parameter to remove.
+    '''
+    del self._params[name]
+
+
+
+def rename_param(self, name, new_name) +
+
+

Rename a specified parameter group.

+

Parameters

+
+
name : str, or GroupReadonly
+
Parameter instance, or name.
+
new_name : str
+
New name for the parameter.
+
+

Raises

+
+
KeyError
+
If no parameter with the original name exists.
+
ValueError
+
If the new name already exist (attempt to overwrite existing data).
+
+
+ +Expand source code + +
def rename_param(self, name, new_name):
+    ''' Rename a specified parameter group.
+
+    Parameters
+    ----------
+    name : str, or `c3d.group.GroupReadonly`
+        Parameter instance, or name.
+    new_name : str
+        New name for the parameter.
+    Raises
+    ------
+    KeyError
+        If no parameter with the original name exists.
+    ValueError
+        If the new name already exist (attempt to overwrite existing data).
+    '''
+    if new_name in self._params:
+        raise ValueError("Key {} already exist.".format(new_name))
+    if isinstance(name, Param):
+        param = name
+        name = param.name
+    else:
+        # Aquire instance using id
+        param = self._params[name]
+    del self._params[name]
+    self._params[new_name] = param
+
+
+
+def set_desc(self, desc) +
+
+

Set the Group descriptor.

+
+ +Expand source code + +
def set_desc(self, desc):
+    ''' Set the Group descriptor.
+    '''
+    if isinstance(desc, bytes):
+        self.desc = self._dtypes.decode_string(desc)
+    elif isinstance(desc, str) or desc is None:
+        self.desc = desc
+    else:
+        raise TypeError('Expected descriptor to be python string, bytes or None, was %s.' % type(desc))
+
+
+
+def set_name(self, name) +
+
+

Set the group name string.

+
+ +Expand source code + +
def set_name(self, name):
+    ''' Set the group name string. '''
+    if name is None or isinstance(name, str):
+        self.name = name
+    else:
+        raise TypeError('Expected group name to be string, was %s.' % type(name))
+
+
+
+def write(self, group_id, handle) +
+
+

Write this parameter group, with parameters, to a file handle.

+

Parameters

+
+
group_id : int
+
The numerical ID of the group.
+
handle : file handle
+
An open, writable, binary file handle.
+
+
+ +Expand source code + +
def write(self, group_id, handle):
+    '''Write this parameter group, with parameters, to a file handle.
+
+    Parameters
+    ----------
+    group_id : int
+        The numerical ID of the group.
+    handle : file handle
+        An open, writable, binary file handle.
+    '''
+    name = self.name.encode('utf-8')
+    desc = self.desc.encode('utf-8')
+    handle.write(struct.pack('bb', len(name), -group_id))
+    handle.write(name)
+    handle.write(struct.pack('<h', 3 + len(desc)))
+    handle.write(struct.pack('B', len(desc)))
+    handle.write(desc)
+    for param in self._params.values():
+        param._data.write(group_id, handle)
+
+
+
+
+
+class GroupReadonly +(data) +
+
+

Wrapper exposing readonly attributes of a GroupData entry.

+
+ +Expand source code + +
class GroupReadonly(object):
+    ''' Wrapper exposing readonly attributes of a `c3d.group.GroupData` entry.
+    '''
+    def __init__(self, data):
+        self._data = data
+
+    def __contains__(self, key):
+        return key in self._data._params
+
+    def __eq__(self, other):
+        return self._data is other._data
+
+    @property
+    def name(self) -> str:
+        ''' Access group name. '''
+        return self._data.name
+
+    @property
+    def desc(self) -> str:
+        '''Access group descriptor. '''
+        return self._data.desc
+
+    def items(self):
+        ''' Get iterator for paramater key-entry pairs. '''
+        return ((k, v.readonly()) for k, v in self._data._params.items())
+
+    def values(self):
+        ''' Get iterator for parameter entries. '''
+        return (v.readonly() for v in self._data._params.values())
+
+    def keys(self):
+        ''' Get iterator for parameter entry keys. '''
+        return self._data._params.keys()
+
+    def get(self, key, default=None):
+        '''Get a readonly parameter by key.
+
+        Parameters
+        ----------
+        key : any
+            Parameter key to look up in this group.
+        default : any, optional
+            Value to return if the key is not found. Defaults to None.
+
+        Returns
+        -------
+        param : :class:`ParamReadable`
+            A parameter from the current group.
+        '''
+        val = self._data._params.get(key, default)
+        if val:
+            return val.readonly()
+        return default
+
+    def get_int8(self, key):
+        '''Get the value of the given parameter as an 8-bit signed integer.'''
+        return self._data[key.upper()].int8_value
+
+    def get_uint8(self, key):
+        '''Get the value of the given parameter as an 8-bit unsigned integer.'''
+        return self._data[key.upper()].uint8_value
+
+    def get_int16(self, key):
+        '''Get the value of the given parameter as a 16-bit signed integer.'''
+        return self._data[key.upper()].int16_value
+
+    def get_uint16(self, key):
+        '''Get the value of the given parameter as a 16-bit unsigned integer.'''
+        return self._data[key.upper()].uint16_value
+
+    def get_int32(self, key):
+        '''Get the value of the given parameter as a 32-bit signed integer.'''
+        return self._data[key.upper()].int32_value
+
+    def get_uint32(self, key):
+        '''Get the value of the given parameter as a 32-bit unsigned integer.'''
+        return self._data[key.upper()].uint32_value
+
+    def get_float(self, key):
+        '''Get the value of the given parameter as a 32-bit float.'''
+        return self._data[key.upper()].float_value
+
+    def get_bytes(self, key):
+        '''Get the value of the given parameter as a byte array.'''
+        return self._data[key.upper()].bytes_value
+
+    def get_string(self, key):
+        '''Get the value of the given parameter as a string.'''
+        return self._data[key.upper()].string_value
+
+

Subclasses

+ +

Instance variables

+
+
var desc : str
+
+

Access group descriptor.

+
+ +Expand source code + +
@property
+def desc(self) -> str:
+    '''Access group descriptor. '''
+    return self._data.desc
+
+
+
var name : str
+
+

Access group name.

+
+ +Expand source code + +
@property
+def name(self) -> str:
+    ''' Access group name. '''
+    return self._data.name
+
+
+
+

Methods

+
+
+def get(self, key, default=None) +
+
+

Get a readonly parameter by key.

+

Parameters

+
+
key : any
+
Parameter key to look up in this group.
+
default : any, optional
+
Value to return if the key is not found. Defaults to None.
+
+

Returns

+
+
param : :class:ParamReadable``
+
A parameter from the current group.
+
+
+ +Expand source code + +
def get(self, key, default=None):
+    '''Get a readonly parameter by key.
+
+    Parameters
+    ----------
+    key : any
+        Parameter key to look up in this group.
+    default : any, optional
+        Value to return if the key is not found. Defaults to None.
+
+    Returns
+    -------
+    param : :class:`ParamReadable`
+        A parameter from the current group.
+    '''
+    val = self._data._params.get(key, default)
+    if val:
+        return val.readonly()
+    return default
+
+
+
+def get_bytes(self, key) +
+
+

Get the value of the given parameter as a byte array.

+
+ +Expand source code + +
def get_bytes(self, key):
+    '''Get the value of the given parameter as a byte array.'''
+    return self._data[key.upper()].bytes_value
+
+
+
+def get_float(self, key) +
+
+

Get the value of the given parameter as a 32-bit float.

+
+ +Expand source code + +
def get_float(self, key):
+    '''Get the value of the given parameter as a 32-bit float.'''
+    return self._data[key.upper()].float_value
+
+
+
+def get_int16(self, key) +
+
+

Get the value of the given parameter as a 16-bit signed integer.

+
+ +Expand source code + +
def get_int16(self, key):
+    '''Get the value of the given parameter as a 16-bit signed integer.'''
+    return self._data[key.upper()].int16_value
+
+
+
+def get_int32(self, key) +
+
+

Get the value of the given parameter as a 32-bit signed integer.

+
+ +Expand source code + +
def get_int32(self, key):
+    '''Get the value of the given parameter as a 32-bit signed integer.'''
+    return self._data[key.upper()].int32_value
+
+
+
+def get_int8(self, key) +
+
+

Get the value of the given parameter as an 8-bit signed integer.

+
+ +Expand source code + +
def get_int8(self, key):
+    '''Get the value of the given parameter as an 8-bit signed integer.'''
+    return self._data[key.upper()].int8_value
+
+
+
+def get_string(self, key) +
+
+

Get the value of the given parameter as a string.

+
+ +Expand source code + +
def get_string(self, key):
+    '''Get the value of the given parameter as a string.'''
+    return self._data[key.upper()].string_value
+
+
+
+def get_uint16(self, key) +
+
+

Get the value of the given parameter as a 16-bit unsigned integer.

+
+ +Expand source code + +
def get_uint16(self, key):
+    '''Get the value of the given parameter as a 16-bit unsigned integer.'''
+    return self._data[key.upper()].uint16_value
+
+
+
+def get_uint32(self, key) +
+
+

Get the value of the given parameter as a 32-bit unsigned integer.

+
+ +Expand source code + +
def get_uint32(self, key):
+    '''Get the value of the given parameter as a 32-bit unsigned integer.'''
+    return self._data[key.upper()].uint32_value
+
+
+
+def get_uint8(self, key) +
+
+

Get the value of the given parameter as an 8-bit unsigned integer.

+
+ +Expand source code + +
def get_uint8(self, key):
+    '''Get the value of the given parameter as an 8-bit unsigned integer.'''
+    return self._data[key.upper()].uint8_value
+
+
+
+def items(self) +
+
+

Get iterator for paramater key-entry pairs.

+
+ +Expand source code + +
def items(self):
+    ''' Get iterator for paramater key-entry pairs. '''
+    return ((k, v.readonly()) for k, v in self._data._params.items())
+
+
+
+def keys(self) +
+
+

Get iterator for parameter entry keys.

+
+ +Expand source code + +
def keys(self):
+    ''' Get iterator for parameter entry keys. '''
+    return self._data._params.keys()
+
+
+
+def values(self) +
+
+

Get iterator for parameter entries.

+
+ +Expand source code + +
def values(self):
+    ''' Get iterator for parameter entries. '''
+    return (v.readonly() for v in self._data._params.values())
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/c3d/header.html b/docs/c3d/header.html new file mode 100644 index 0000000..df07378 --- /dev/null +++ b/docs/c3d/header.html @@ -0,0 +1,958 @@ + + + + + + +c3d.header API documentation + + + + + + + + + + + + +
+
+
+

Module c3d.header

+
+
+

Defines the header class used for reading, writing and tracking metadata in the .c3d header.

+
+ +Expand source code + +
'''
+Defines the header class used for reading, writing and tracking metadata in the .c3d header.
+'''
+import sys
+import struct
+import numpy as np
+from .utils import UNPACK_FLOAT_IEEE, DEC_to_IEEE
+
+
+class Header(object):
+    '''Header information from a C3D file.
+
+    Attributes
+    ----------
+    event_block : int
+        Index of the 512-byte block where labels (metadata) are found.
+    parameter_block : int
+        Index of the 512-byte block where parameters (metadata) are found.
+    data_block : int
+        Index of the 512-byte block where data starts.
+    point_count : int
+        Number of motion capture channels recorded in this file.
+    analog_count : int
+        Number of analog values recorded per frame of 3D point data.
+    first_frame : int
+        Index of the first frame of data.
+    last_frame : int
+        Index of the last frame of data.
+    analog_per_frame : int
+        Number of analog frames per frame of 3D point data. The analog frame
+        rate (ANALOG:RATE) is equivalent to the point frame rate (POINT:RATE)
+        times the analog per frame value.
+    frame_rate : float
+        The frame rate of the recording, in frames per second.
+    scale_factor : float
+        Multiply values in the file by this scale parameter.
+    long_event_labels : bool
+    max_gap : int
+
+    .. note::
+        The ``scale_factor`` attribute is not used in Phasespace C3D files;
+        instead, use the POINT.SCALE parameter.
+
+    .. note::
+        The ``first_frame`` and ``last_frame`` header attributes are not used in
+        C3D files generated by Phasespace. Instead, the first and last
+        frame numbers are stored in the POINTS:ACTUAL_START_FIELD and
+        POINTS:ACTUAL_END_FIELD parameters.
+    '''
+
+    # Read/Write header formats, read values as unsigned ints rather then floats.
+    BINARY_FORMAT_WRITE = '<BBHHHHHfHHf274sHHH164s44s'
+    BINARY_FORMAT_READ = '<BBHHHHHIHHI274sHHH164s44s'
+    BINARY_FORMAT_READ_BIG_ENDIAN = '>BBHHHHHIHHI274sHHH164s44s'
+
+    def __init__(self, handle=None):
+        '''Create a new Header object.
+
+        Parameters
+        ----------
+        handle : file handle, optional
+            If given, initialize attributes for the Header from this file
+            handle. The handle must be seek-able and readable. If `handle` is
+            not given, Header attributes are initialized with default values.
+        '''
+        self.parameter_block = 2
+        self.data_block = 3
+
+        self.point_count = 50
+        self.analog_count = 0
+
+        self.first_frame = 1
+        self.last_frame = 1
+        self.analog_per_frame = 0
+        self.frame_rate = 60.0
+
+        self.max_gap = 0
+        self.scale_factor = -1.0
+        self.long_event_labels = False
+        self.event_count = 0
+
+        self.event_block = b''
+        self.event_timings = np.zeros(0, dtype=np.float32)
+        self.event_disp_flags = np.zeros(0, dtype=np.bool)
+        self.event_labels = []
+
+        if handle:
+            self.read(handle)
+
+    def write(self, handle):
+        '''Write binary header data to a file handle.
+
+        This method writes exactly 512 bytes to the beginning of the given file
+        handle.
+
+        Parameters
+        ----------
+        handle : file handle
+            The given handle will be reset to 0 using `seek` and then 512 bytes
+            will be written to describe the parameters in this Header. The
+            handle must be writeable.
+        '''
+        handle.seek(0)
+        handle.write(struct.pack(self.BINARY_FORMAT_WRITE,
+                                 # Pack vars:
+                                 self.parameter_block,
+                                 0x50,
+                                 self.point_count,
+                                 self.analog_count,
+                                 self.first_frame,
+                                 self.last_frame,
+                                 self.max_gap,
+                                 self.scale_factor,
+                                 self.data_block,
+                                 self.analog_per_frame,
+                                 self.frame_rate,
+                                 b'',
+                                 self.long_event_labels and 0x3039 or 0x0,  # If True write long_event_key else 0
+                                 self.event_count,
+                                 0x0,
+                                 self.event_block,
+                                 b''))
+
+    def __str__(self):
+        '''Return a string representation of this Header's attributes.'''
+        return '''\
+                  parameter_block: {0.parameter_block}
+                      point_count: {0.point_count}
+                     analog_count: {0.analog_count}
+                      first_frame: {0.first_frame}
+                       last_frame: {0.last_frame}
+                          max_gap: {0.max_gap}
+                     scale_factor: {0.scale_factor}
+                       data_block: {0.data_block}
+                 analog_per_frame: {0.analog_per_frame}
+                       frame_rate: {0.frame_rate}
+                long_event_labels: {0.long_event_labels}
+                      event_block: {0.event_block}'''.format(self)
+
+    def read(self, handle, fmt=BINARY_FORMAT_READ):
+        '''Read and parse binary header data from a file handle.
+
+        This method reads exactly 512 bytes from the beginning of the given file
+        handle.
+
+        Parameters
+        ----------
+        handle : file handle
+            The given handle will be reset to 0 using `seek` and then 512 bytes
+            will be read to initialize the attributes in this Header. The handle
+            must be readable.
+
+        fmt : Formating string used to read the header.
+
+        Raises
+        ------
+        AssertionError
+            If the magic byte from the header is not 80 (the C3D magic value).
+        '''
+        handle.seek(0)
+        raw = handle.read(512)
+
+        (self.parameter_block,
+         magic,
+         self.point_count,
+         self.analog_count,
+         self.first_frame,
+         self.last_frame,
+         self.max_gap,
+         self.scale_factor,
+         self.data_block,
+         self.analog_per_frame,
+         self.frame_rate,
+         _,
+         self.long_event_labels,
+         self.event_count,
+         __,
+         self.event_block,
+         _) = struct.unpack(fmt, raw)
+
+        # Check magic number
+        assert magic == 80, 'C3D magic {} != 80 !'.format(magic)
+
+        # Check long event key
+        self.long_event_labels = self.long_event_labels == 0x3039
+
+    def _processor_convert(self, dtypes, handle):
+        ''' Function interpreting the header once a processor type has been determined.
+        '''
+
+        if dtypes.is_dec:
+            self.scale_factor = DEC_to_IEEE(self.scale_factor)
+            self.frame_rate = DEC_to_IEEE(self.frame_rate)
+            float_unpack = DEC_to_IEEE
+        elif dtypes.is_ieee:
+            self.scale_factor = UNPACK_FLOAT_IEEE(self.scale_factor)
+            self.frame_rate = UNPACK_FLOAT_IEEE(self.frame_rate)
+            float_unpack = UNPACK_FLOAT_IEEE
+        elif dtypes.is_mips:
+            # Re-read header in big-endian
+            self.read(handle, Header.BINARY_FORMAT_READ_BIG_ENDIAN)
+            # Then unpack
+            self.scale_factor = UNPACK_FLOAT_IEEE(self.scale_factor)
+            self.frame_rate = UNPACK_FLOAT_IEEE(self.frame_rate)
+            float_unpack = UNPACK_FLOAT_IEEE
+
+        self._parse_events(dtypes, float_unpack)
+
+    def _parse_events(self, dtypes, float_unpack):
+        ''' Parse the event section of the header.
+        '''
+
+        # Event section byte blocks
+        time_bytes = self.event_block[:72]
+        disp_bytes = self.event_block[72:90]
+        label_bytes = self.event_block[92:]
+
+        if dtypes.is_mips:
+            unpack_fmt = '>I'
+        else:
+            unpack_fmt = '<I'
+
+        read_count = self.event_count
+        self.event_timings = np.zeros(read_count, dtype=np.float32)
+        self.event_disp_flags = np.zeros(read_count, dtype=np.bool)
+        self.event_labels = np.empty(read_count, dtype=object)
+        for i in range(read_count):
+            ilong = i * 4
+            # Unpack
+            self.event_disp_flags[i] = disp_bytes[i] > 0
+            self.event_timings[i] = float_unpack(struct.unpack(unpack_fmt, time_bytes[ilong:ilong + 4])[0])
+            self.event_labels[i] = dtypes.decode_string(label_bytes[ilong:ilong + 4])
+
+    @property
+    def events(self):
+        ''' Get an iterable over displayed events defined in the header. Iterable items are on form (timing, label).
+
+            Note*:
+            Time as defined by the 'timing' is relative to frame 1 and not the 'first_frame' parameter.
+            Frame 1 therefor has the time 0.0 in relation to the event timing.
+        '''
+        return zip(self.event_timings[self.event_disp_flags], self.event_labels[self.event_disp_flags])
+
+    def encode_events(self, events):
+        ''' Encode event data in the event block.
+
+        Parameters
+        ----------
+        events : [(float, str), ...]
+            Event data, iterable of touples containing the event timing and a 4 character label string.
+             Event timings should be calculated relative to sample 1 with the timing 0.0s, and should
+             not be relative to the first_frame header parameter.
+        '''
+        endian = '<'
+        if sys.byteorder == 'big':
+            endian = '>'
+
+        # Event block format
+        fmt = '{}{}{}{}{}'.format(endian,
+                                  str(18 * 4) + 's',  # Timings
+                                  str(18) + 's',      # Flags
+                                  'H',                # __
+                                  str(18 * 4) + 's'   # Labels
+                                  )
+        # Pack bytes
+        event_timings = np.zeros(18, dtype=np.float32)
+        event_disp_flags = np.zeros(18, dtype=np.uint8)
+        event_labels = np.empty(18, dtype=object)
+        label_bytes = bytearray(18 * 4)
+        for i, (time, label) in enumerate(events):
+            if i > 17:
+                # Don't raise Error, header events are rarely used.
+                warnings.warn('Maximum of 18 events can be encoded in the header, skipping remaining events.')
+                break
+
+            event_timings[i] = time
+            event_labels[i] = label
+            label_bytes[i * 4:(i + 1) * 4] = label.encode('utf-8')
+
+        write_count = min(i + 1, 18)
+        event_disp_flags[:write_count] = 1
+
+        # Update event headers in self
+        self.long_event_labels = 0x3039  # Magic number
+        self.event_count = write_count
+        # Update event block
+        self.event_timings = event_timings[:write_count]
+        self.event_disp_flags = np.ones(write_count, dtype=np.bool)
+        self.event_labels = event_labels[:write_count]
+        self.event_block = struct.pack(fmt,
+                                       event_timings.tobytes(),
+                                       event_disp_flags.tobytes(),
+                                       0,
+                                       label_bytes
+                                       )
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Header +(handle=None) +
+
+

Header information from a C3D file.

+

Attributes

+
+
event_block : int
+
Index of the 512-byte block where labels (metadata) are found.
+
parameter_block : int
+
Index of the 512-byte block where parameters (metadata) are found.
+
data_block : int
+
Index of the 512-byte block where data starts.
+
point_count : int
+
Number of motion capture channels recorded in this file.
+
analog_count : int
+
Number of analog values recorded per frame of 3D point data.
+
first_frame : int
+
Index of the first frame of data.
+
last_frame : int
+
Index of the last frame of data.
+
analog_per_frame : int
+
Number of analog frames per frame of 3D point data. The analog frame +rate (ANALOG:RATE) is equivalent to the point frame rate (POINT:RATE) +times the analog per frame value.
+
frame_rate : float
+
The frame rate of the recording, in frames per second.
+
scale_factor : float
+
Multiply values in the file by this scale parameter.
+
long_event_labels : bool
+
 
+
max_gap : int
+
 
+
+
+

Note

+

The scale_factor attribute is not used in Phasespace C3D files; +instead, use the POINT.SCALE parameter.

+
+
+

Note

+

The first_frame and last_frame header attributes are not used in +C3D files generated by Phasespace. Instead, the first and last +frame numbers are stored in the POINTS:ACTUAL_START_FIELD and +POINTS:ACTUAL_END_FIELD parameters.

+
+

Create a new Header object.

+

Parameters

+
+
handle : file handle, optional
+
If given, initialize attributes for the Header from this file +handle. The handle must be seek-able and readable. If handle is +not given, Header attributes are initialized with default values.
+
+
+ +Expand source code + +
class Header(object):
+    '''Header information from a C3D file.
+
+    Attributes
+    ----------
+    event_block : int
+        Index of the 512-byte block where labels (metadata) are found.
+    parameter_block : int
+        Index of the 512-byte block where parameters (metadata) are found.
+    data_block : int
+        Index of the 512-byte block where data starts.
+    point_count : int
+        Number of motion capture channels recorded in this file.
+    analog_count : int
+        Number of analog values recorded per frame of 3D point data.
+    first_frame : int
+        Index of the first frame of data.
+    last_frame : int
+        Index of the last frame of data.
+    analog_per_frame : int
+        Number of analog frames per frame of 3D point data. The analog frame
+        rate (ANALOG:RATE) is equivalent to the point frame rate (POINT:RATE)
+        times the analog per frame value.
+    frame_rate : float
+        The frame rate of the recording, in frames per second.
+    scale_factor : float
+        Multiply values in the file by this scale parameter.
+    long_event_labels : bool
+    max_gap : int
+
+    .. note::
+        The ``scale_factor`` attribute is not used in Phasespace C3D files;
+        instead, use the POINT.SCALE parameter.
+
+    .. note::
+        The ``first_frame`` and ``last_frame`` header attributes are not used in
+        C3D files generated by Phasespace. Instead, the first and last
+        frame numbers are stored in the POINTS:ACTUAL_START_FIELD and
+        POINTS:ACTUAL_END_FIELD parameters.
+    '''
+
+    # Read/Write header formats, read values as unsigned ints rather then floats.
+    BINARY_FORMAT_WRITE = '<BBHHHHHfHHf274sHHH164s44s'
+    BINARY_FORMAT_READ = '<BBHHHHHIHHI274sHHH164s44s'
+    BINARY_FORMAT_READ_BIG_ENDIAN = '>BBHHHHHIHHI274sHHH164s44s'
+
+    def __init__(self, handle=None):
+        '''Create a new Header object.
+
+        Parameters
+        ----------
+        handle : file handle, optional
+            If given, initialize attributes for the Header from this file
+            handle. The handle must be seek-able and readable. If `handle` is
+            not given, Header attributes are initialized with default values.
+        '''
+        self.parameter_block = 2
+        self.data_block = 3
+
+        self.point_count = 50
+        self.analog_count = 0
+
+        self.first_frame = 1
+        self.last_frame = 1
+        self.analog_per_frame = 0
+        self.frame_rate = 60.0
+
+        self.max_gap = 0
+        self.scale_factor = -1.0
+        self.long_event_labels = False
+        self.event_count = 0
+
+        self.event_block = b''
+        self.event_timings = np.zeros(0, dtype=np.float32)
+        self.event_disp_flags = np.zeros(0, dtype=np.bool)
+        self.event_labels = []
+
+        if handle:
+            self.read(handle)
+
+    def write(self, handle):
+        '''Write binary header data to a file handle.
+
+        This method writes exactly 512 bytes to the beginning of the given file
+        handle.
+
+        Parameters
+        ----------
+        handle : file handle
+            The given handle will be reset to 0 using `seek` and then 512 bytes
+            will be written to describe the parameters in this Header. The
+            handle must be writeable.
+        '''
+        handle.seek(0)
+        handle.write(struct.pack(self.BINARY_FORMAT_WRITE,
+                                 # Pack vars:
+                                 self.parameter_block,
+                                 0x50,
+                                 self.point_count,
+                                 self.analog_count,
+                                 self.first_frame,
+                                 self.last_frame,
+                                 self.max_gap,
+                                 self.scale_factor,
+                                 self.data_block,
+                                 self.analog_per_frame,
+                                 self.frame_rate,
+                                 b'',
+                                 self.long_event_labels and 0x3039 or 0x0,  # If True write long_event_key else 0
+                                 self.event_count,
+                                 0x0,
+                                 self.event_block,
+                                 b''))
+
+    def __str__(self):
+        '''Return a string representation of this Header's attributes.'''
+        return '''\
+                  parameter_block: {0.parameter_block}
+                      point_count: {0.point_count}
+                     analog_count: {0.analog_count}
+                      first_frame: {0.first_frame}
+                       last_frame: {0.last_frame}
+                          max_gap: {0.max_gap}
+                     scale_factor: {0.scale_factor}
+                       data_block: {0.data_block}
+                 analog_per_frame: {0.analog_per_frame}
+                       frame_rate: {0.frame_rate}
+                long_event_labels: {0.long_event_labels}
+                      event_block: {0.event_block}'''.format(self)
+
+    def read(self, handle, fmt=BINARY_FORMAT_READ):
+        '''Read and parse binary header data from a file handle.
+
+        This method reads exactly 512 bytes from the beginning of the given file
+        handle.
+
+        Parameters
+        ----------
+        handle : file handle
+            The given handle will be reset to 0 using `seek` and then 512 bytes
+            will be read to initialize the attributes in this Header. The handle
+            must be readable.
+
+        fmt : Formating string used to read the header.
+
+        Raises
+        ------
+        AssertionError
+            If the magic byte from the header is not 80 (the C3D magic value).
+        '''
+        handle.seek(0)
+        raw = handle.read(512)
+
+        (self.parameter_block,
+         magic,
+         self.point_count,
+         self.analog_count,
+         self.first_frame,
+         self.last_frame,
+         self.max_gap,
+         self.scale_factor,
+         self.data_block,
+         self.analog_per_frame,
+         self.frame_rate,
+         _,
+         self.long_event_labels,
+         self.event_count,
+         __,
+         self.event_block,
+         _) = struct.unpack(fmt, raw)
+
+        # Check magic number
+        assert magic == 80, 'C3D magic {} != 80 !'.format(magic)
+
+        # Check long event key
+        self.long_event_labels = self.long_event_labels == 0x3039
+
+    def _processor_convert(self, dtypes, handle):
+        ''' Function interpreting the header once a processor type has been determined.
+        '''
+
+        if dtypes.is_dec:
+            self.scale_factor = DEC_to_IEEE(self.scale_factor)
+            self.frame_rate = DEC_to_IEEE(self.frame_rate)
+            float_unpack = DEC_to_IEEE
+        elif dtypes.is_ieee:
+            self.scale_factor = UNPACK_FLOAT_IEEE(self.scale_factor)
+            self.frame_rate = UNPACK_FLOAT_IEEE(self.frame_rate)
+            float_unpack = UNPACK_FLOAT_IEEE
+        elif dtypes.is_mips:
+            # Re-read header in big-endian
+            self.read(handle, Header.BINARY_FORMAT_READ_BIG_ENDIAN)
+            # Then unpack
+            self.scale_factor = UNPACK_FLOAT_IEEE(self.scale_factor)
+            self.frame_rate = UNPACK_FLOAT_IEEE(self.frame_rate)
+            float_unpack = UNPACK_FLOAT_IEEE
+
+        self._parse_events(dtypes, float_unpack)
+
+    def _parse_events(self, dtypes, float_unpack):
+        ''' Parse the event section of the header.
+        '''
+
+        # Event section byte blocks
+        time_bytes = self.event_block[:72]
+        disp_bytes = self.event_block[72:90]
+        label_bytes = self.event_block[92:]
+
+        if dtypes.is_mips:
+            unpack_fmt = '>I'
+        else:
+            unpack_fmt = '<I'
+
+        read_count = self.event_count
+        self.event_timings = np.zeros(read_count, dtype=np.float32)
+        self.event_disp_flags = np.zeros(read_count, dtype=np.bool)
+        self.event_labels = np.empty(read_count, dtype=object)
+        for i in range(read_count):
+            ilong = i * 4
+            # Unpack
+            self.event_disp_flags[i] = disp_bytes[i] > 0
+            self.event_timings[i] = float_unpack(struct.unpack(unpack_fmt, time_bytes[ilong:ilong + 4])[0])
+            self.event_labels[i] = dtypes.decode_string(label_bytes[ilong:ilong + 4])
+
+    @property
+    def events(self):
+        ''' Get an iterable over displayed events defined in the header. Iterable items are on form (timing, label).
+
+            Note*:
+            Time as defined by the 'timing' is relative to frame 1 and not the 'first_frame' parameter.
+            Frame 1 therefor has the time 0.0 in relation to the event timing.
+        '''
+        return zip(self.event_timings[self.event_disp_flags], self.event_labels[self.event_disp_flags])
+
+    def encode_events(self, events):
+        ''' Encode event data in the event block.
+
+        Parameters
+        ----------
+        events : [(float, str), ...]
+            Event data, iterable of touples containing the event timing and a 4 character label string.
+             Event timings should be calculated relative to sample 1 with the timing 0.0s, and should
+             not be relative to the first_frame header parameter.
+        '''
+        endian = '<'
+        if sys.byteorder == 'big':
+            endian = '>'
+
+        # Event block format
+        fmt = '{}{}{}{}{}'.format(endian,
+                                  str(18 * 4) + 's',  # Timings
+                                  str(18) + 's',      # Flags
+                                  'H',                # __
+                                  str(18 * 4) + 's'   # Labels
+                                  )
+        # Pack bytes
+        event_timings = np.zeros(18, dtype=np.float32)
+        event_disp_flags = np.zeros(18, dtype=np.uint8)
+        event_labels = np.empty(18, dtype=object)
+        label_bytes = bytearray(18 * 4)
+        for i, (time, label) in enumerate(events):
+            if i > 17:
+                # Don't raise Error, header events are rarely used.
+                warnings.warn('Maximum of 18 events can be encoded in the header, skipping remaining events.')
+                break
+
+            event_timings[i] = time
+            event_labels[i] = label
+            label_bytes[i * 4:(i + 1) * 4] = label.encode('utf-8')
+
+        write_count = min(i + 1, 18)
+        event_disp_flags[:write_count] = 1
+
+        # Update event headers in self
+        self.long_event_labels = 0x3039  # Magic number
+        self.event_count = write_count
+        # Update event block
+        self.event_timings = event_timings[:write_count]
+        self.event_disp_flags = np.ones(write_count, dtype=np.bool)
+        self.event_labels = event_labels[:write_count]
+        self.event_block = struct.pack(fmt,
+                                       event_timings.tobytes(),
+                                       event_disp_flags.tobytes(),
+                                       0,
+                                       label_bytes
+                                       )
+
+

Class variables

+
+
var BINARY_FORMAT_READ
+
+
+
+
var BINARY_FORMAT_READ_BIG_ENDIAN
+
+
+
+
var BINARY_FORMAT_WRITE
+
+
+
+
+

Instance variables

+
+
var events
+
+

Get an iterable over displayed events defined in the header. Iterable items are on form (timing, label).

+

Note*: +Time as defined by the 'timing' is relative to frame 1 and not the 'first_frame' parameter. +Frame 1 therefor has the time 0.0 in relation to the event timing.

+
+ +Expand source code + +
@property
+def events(self):
+    ''' Get an iterable over displayed events defined in the header. Iterable items are on form (timing, label).
+
+        Note*:
+        Time as defined by the 'timing' is relative to frame 1 and not the 'first_frame' parameter.
+        Frame 1 therefor has the time 0.0 in relation to the event timing.
+    '''
+    return zip(self.event_timings[self.event_disp_flags], self.event_labels[self.event_disp_flags])
+
+
+
+

Methods

+
+
+def encode_events(self, events) +
+
+

Encode event data in the event block.

+

Parameters

+
+
events : [(float, str), …]
+
Event data, iterable of touples containing the event timing and a 4 character label string. +Event timings should be calculated relative to sample 1 with the timing 0.0s, and should +not be relative to the first_frame header parameter.
+
+
+ +Expand source code + +
def encode_events(self, events):
+    ''' Encode event data in the event block.
+
+    Parameters
+    ----------
+    events : [(float, str), ...]
+        Event data, iterable of touples containing the event timing and a 4 character label string.
+         Event timings should be calculated relative to sample 1 with the timing 0.0s, and should
+         not be relative to the first_frame header parameter.
+    '''
+    endian = '<'
+    if sys.byteorder == 'big':
+        endian = '>'
+
+    # Event block format
+    fmt = '{}{}{}{}{}'.format(endian,
+                              str(18 * 4) + 's',  # Timings
+                              str(18) + 's',      # Flags
+                              'H',                # __
+                              str(18 * 4) + 's'   # Labels
+                              )
+    # Pack bytes
+    event_timings = np.zeros(18, dtype=np.float32)
+    event_disp_flags = np.zeros(18, dtype=np.uint8)
+    event_labels = np.empty(18, dtype=object)
+    label_bytes = bytearray(18 * 4)
+    for i, (time, label) in enumerate(events):
+        if i > 17:
+            # Don't raise Error, header events are rarely used.
+            warnings.warn('Maximum of 18 events can be encoded in the header, skipping remaining events.')
+            break
+
+        event_timings[i] = time
+        event_labels[i] = label
+        label_bytes[i * 4:(i + 1) * 4] = label.encode('utf-8')
+
+    write_count = min(i + 1, 18)
+    event_disp_flags[:write_count] = 1
+
+    # Update event headers in self
+    self.long_event_labels = 0x3039  # Magic number
+    self.event_count = write_count
+    # Update event block
+    self.event_timings = event_timings[:write_count]
+    self.event_disp_flags = np.ones(write_count, dtype=np.bool)
+    self.event_labels = event_labels[:write_count]
+    self.event_block = struct.pack(fmt,
+                                   event_timings.tobytes(),
+                                   event_disp_flags.tobytes(),
+                                   0,
+                                   label_bytes
+                                   )
+
+
+
+def read(self, handle, fmt='<BBHHHHHIHHI274sHHH164s44s') +
+
+

Read and parse binary header data from a file handle.

+

This method reads exactly 512 bytes from the beginning of the given file +handle.

+

Parameters

+
+
handle : file handle
+
The given handle will be reset to 0 using seek and then 512 bytes +will be read to initialize the attributes in this Header. The handle +must be readable.
+
+

fmt : Formating string used to read the header.

+

Raises

+
+
AssertionError
+
If the magic byte from the header is not 80 (the C3D magic value).
+
+
+ +Expand source code + +
def read(self, handle, fmt=BINARY_FORMAT_READ):
+    '''Read and parse binary header data from a file handle.
+
+    This method reads exactly 512 bytes from the beginning of the given file
+    handle.
+
+    Parameters
+    ----------
+    handle : file handle
+        The given handle will be reset to 0 using `seek` and then 512 bytes
+        will be read to initialize the attributes in this Header. The handle
+        must be readable.
+
+    fmt : Formating string used to read the header.
+
+    Raises
+    ------
+    AssertionError
+        If the magic byte from the header is not 80 (the C3D magic value).
+    '''
+    handle.seek(0)
+    raw = handle.read(512)
+
+    (self.parameter_block,
+     magic,
+     self.point_count,
+     self.analog_count,
+     self.first_frame,
+     self.last_frame,
+     self.max_gap,
+     self.scale_factor,
+     self.data_block,
+     self.analog_per_frame,
+     self.frame_rate,
+     _,
+     self.long_event_labels,
+     self.event_count,
+     __,
+     self.event_block,
+     _) = struct.unpack(fmt, raw)
+
+    # Check magic number
+    assert magic == 80, 'C3D magic {} != 80 !'.format(magic)
+
+    # Check long event key
+    self.long_event_labels = self.long_event_labels == 0x3039
+
+
+
+def write(self, handle) +
+
+

Write binary header data to a file handle.

+

This method writes exactly 512 bytes to the beginning of the given file +handle.

+

Parameters

+
+
handle : file handle
+
The given handle will be reset to 0 using seek and then 512 bytes +will be written to describe the parameters in this Header. The +handle must be writeable.
+
+
+ +Expand source code + +
def write(self, handle):
+    '''Write binary header data to a file handle.
+
+    This method writes exactly 512 bytes to the beginning of the given file
+    handle.
+
+    Parameters
+    ----------
+    handle : file handle
+        The given handle will be reset to 0 using `seek` and then 512 bytes
+        will be written to describe the parameters in this Header. The
+        handle must be writeable.
+    '''
+    handle.seek(0)
+    handle.write(struct.pack(self.BINARY_FORMAT_WRITE,
+                             # Pack vars:
+                             self.parameter_block,
+                             0x50,
+                             self.point_count,
+                             self.analog_count,
+                             self.first_frame,
+                             self.last_frame,
+                             self.max_gap,
+                             self.scale_factor,
+                             self.data_block,
+                             self.analog_per_frame,
+                             self.frame_rate,
+                             b'',
+                             self.long_event_labels and 0x3039 or 0x0,  # If True write long_event_key else 0
+                             self.event_count,
+                             0x0,
+                             self.event_block,
+                             b''))
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/c3d/index.html b/docs/c3d/index.html new file mode 100644 index 0000000..7a90562 --- /dev/null +++ b/docs/c3d/index.html @@ -0,0 +1,242 @@ + + + + + + +c3d API documentation + + + + + + + + + + + + +
+
+
+

Package c3d

+
+
+
+

Python C3D Processing

+

This package provides pure Python modules for reading, writing, and editing binary +motion-capture files in the C3D file format.

+

Installing

+

See the main page or github page.

+

Examples

+

Access to data blocks in a .c3d file is provided through the Reader and Writer classes. +Implementation of the examples below are provided in the examples/ directory in the github repository.

+

Reading

+

To read the frames from a C3D file, use a Reader instance:

+
import c3d
+
+with open('my-motion.c3d', 'rb') as file:
+    reader = c3d.Reader(file)
+    for i, points, analog in reader.read_frames():
+        print('frame {}: point {}, analog {}'.format(
+              i, points.shape, analog.shape))
+
+

The method Reader.read_frames() generates tuples +containing the trial frame number, a numpy array of point +data, and a numpy array of analog data.

+

Writing

+

To write data frames to a C3D file, use a Writer +instance:

+
import c3d
+import numpy as np
+
+# Writes 100 frames recorded at 200 Hz.
+# Each frame contains recordings for 24 coordinate markers.
+writer = c3d.Writer(point_rate=200)
+for _ in range(100):
+    writer.add_frames((np.random.randn(24, 5), ()))
+
+writer.set_point_labels(['RFT1', 'RFT2', 'RFT3', 'RFT4',
+                         'LFT1', 'LFT2', 'LFT3', 'LFT4',
+                         'RSK1', 'RSK2', 'RSK3', 'RSK4',
+                         'LSK1', 'LSK2', 'LSK3', 'LSK4',
+                         'RTH1', 'RTH2', 'RTH3', 'RTH4',
+                         'LTH1', 'LTH2', 'LTH3', 'LTH4'
+                        ])
+writer.set_analog_labels(None)
+
+with open('random-points.c3d', 'wb') as h:
+    writer.write(h)
+
+

The function Writer.add_frames() take pairs of numpy or python +arrays, with the first array in each tuple defining point data and the second +analog data for the frame. Leaving one of the arrays empty indicates +to the writer that no analog — or point data— should be included in the file. +References of the data arrays are tracked until Writer.write() +is called, which serializes the metadata and data frames into a C3D binary file stream.

+

Editing

+

Editing c3d files is possible by combining the use of Reader and Writer +instances through the Reader.to_writer() method. To edit a file, open a file stream and pass +it to the Reader constructor. Use the Reader.to_writer() method to create +an independent Writer instance containing a heap copy of its file contents. +To edit the sequence, one can now reread the data frames from the reader and through inserting the +frames in reverse to create a looped version of the original motion sequence!

+
import c3d
+
+with open('my-motion.c3d', 'rb') as file:
+    reader = c3d.Reader(file)
+    writer = reader.to_writer('copy')
+    for i, points, analog in reader.read_frames():
+        writer.add_frames((points, analog), index=reader.frame_count)
+
+with open('my-looped-motion.c3d', 'wb') as h:
+    writer.write(h)
+
+

Accessing Metadata

+

Metadata in a .c3d file exists in two forms, a Header and ParamData. +Reading metadata fields can be done though the Reader but editing requires a +Writer instance.

+

Header fields can be accessed from the common Manager.header attribute. +Parameters are available through a parameter Group, and can be accessed +through the Manager.get() and Group.get() methods:

+
group = reader.get('POINT')
+param = group.get('LABELS')
+
+

or simply use

+
param = reader.get('POINT:LABELS')
+
+

Note that for accessing parameters in the Reader, Reader.get() +returns a GroupReadonly instance. Convenience functions are provided +for some of the common metadata fields such as Manager.frame_count. +In the case you require specific metadata fields, consider exploring +the C3D format manual and/or inspect the file using the c3d-metadata script.

+

Writing Metadata

+

Once a Writer instance is created, to edit +parameter data Writer.get_create() a group:

+
group = writer.get_create('ANALOG')
+
+

and to write a float32 entry, use the Group.add() or Group.set() functions

+
group.set('GEN_SCALE', 'Analog general scale factor', 4, '<f', value)
+
+

In this case, one can use the Writer.set_analog_general_scale() method instead. +For serializing other types, see the source code for some of the convenience functions. For example: +Writer.set_point_labels() (2D string array) or +Writer.set_analog_scales() (1D float array).

+
+ +Expand source code + +
"""
+---------------------
+Python C3D Processing
+---------------------
+
+This package provides pure Python modules for reading, writing, and editing binary
+motion-capture files in the [C3D file format].
+
+[C3D file format]: https://www.c3d.org/HTML/default.htm
+
+Installing
+----------
+
+See the [main page] or [github page].
+
+[main page]: https://mattiasfredriksson.github.io/py-c3d/
+[github page]: https://github.com/EmbodiedCognition/py-c3d
+
+.. include:: ../docs/examples.md
+
+"""
+from . import dtypes
+from . import group
+from . import header
+from . import manager
+from . import parameter
+from . import utils
+from .reader import Reader
+from .writer import Writer
+
+
+
+

Sub-modules

+
+
c3d.dtypes
+
+

State object defining the data types associated with a given .c3d processor format.

+
+
c3d.group
+
+

Classes used to represent the concept of parameter groups in a .c3d file.

+
+
c3d.header
+
+

Defines the header class used for reading, writing and tracking metadata in the .c3d header.

+
+
c3d.manager
+
+

Manager base class defining common attributes for both Reader and Writer instances.

+
+
c3d.parameter
+
+

Classes used to represent the concept of a parameter in a .c3d file.

+
+
c3d.reader
+
+

Contains the Reader class for reading C3D files.

+
+
c3d.utils
+
+

Trailing utility functions.

+
+
c3d.writer
+
+

Contains the Writer class for writing C3D files.

+
+
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/c3d/manager.html b/docs/c3d/manager.html new file mode 100644 index 0000000..eb17ef0 --- /dev/null +++ b/docs/c3d/manager.html @@ -0,0 +1,1556 @@ + + + + + + +c3d.manager API documentation + + + + + + + + + + + + +
+
+
+

Module c3d.manager

+
+
+

Manager base class defining common attributes for both Reader and Writer instances.

+
+ +Expand source code + +
''' Manager base class defining common attributes for both Reader and Writer instances.
+'''
+import numpy as np
+import warnings
+from .header import Header
+from .group import GroupData, GroupReadonly, Group
+from .utils import is_integer, is_iterable
+
+
+class Manager(object):
+    '''A base class for managing C3D file metadata.
+
+    This class manages a C3D header (which contains some stock metadata fields)
+    as well as a set of parameter groups. Each group is accessible using its
+    name.
+
+    Attributes
+    ----------
+    header : `c3d.header.Header`
+        Header information for the C3D file.
+    '''
+
+    def __init__(self, header=None):
+        '''Set up a new Manager with a Header.'''
+        self._header = header or Header()
+        self._groups = {}
+
+    def __contains__(self, key):
+        return key in self._groups
+
+    def items(self):
+        ''' Get iterable over pairs of (str, `c3d.group.Group`) entries.
+        '''
+        return ((k, v) for k, v in self._groups.items() if isinstance(k, str))
+
+    def values(self):
+        ''' Get iterable over `c3d.group.Group` entries.
+        '''
+        return (v for k, v in self._groups.items() if isinstance(k, str))
+
+    def keys(self):
+        ''' Get iterable over parameter name keys.
+        '''
+        return (k for k in self._groups.keys() if isinstance(k, str))
+
+    def listed(self):
+        ''' Get iterable over pairs of (int, `c3d.group.Group`) entries.
+        '''
+        return sorted((i, g) for i, g in self._groups.items() if isinstance(i, int))
+
+    def _check_metadata(self):
+        ''' Ensure that the metadata in our file is self-consistent. '''
+        assert self._header.point_count == self.point_used, (
+            'inconsistent point count! {} header != {} POINT:USED'.format(
+                self._header.point_count,
+                self.point_used,
+            ))
+
+        assert self._header.scale_factor == self.point_scale, (
+            'inconsistent scale factor! {} header != {} POINT:SCALE'.format(
+                self._header.scale_factor,
+                self.point_scale,
+            ))
+
+        assert self._header.frame_rate == self.point_rate, (
+            'inconsistent frame rate! {} header != {} POINT:RATE'.format(
+                self._header.frame_rate,
+                self.point_rate,
+            ))
+
+        if self.point_rate:
+            ratio = self.analog_rate / self.point_rate
+        else:
+            ratio = 0
+        assert self._header.analog_per_frame == ratio, (
+            'inconsistent analog rate! {} header != {} analog-fps / {} point-fps'.format(
+                self._header.analog_per_frame,
+                self.analog_rate,
+                self.point_rate,
+            ))
+
+        count = self.analog_used * self._header.analog_per_frame
+        assert self._header.analog_count == count, (
+            'inconsistent analog count! {} header != {} analog used * {} per-frame'.format(
+                self._header.analog_count,
+                self.analog_used,
+                self._header.analog_per_frame,
+            ))
+
+        try:
+            start = self.get('POINT:DATA_START').uint16_value
+            if self._header.data_block != start:
+                warnings.warn('inconsistent data block! {} header != {} POINT:DATA_START'.format(
+                    self._header.data_block, start))
+        except AttributeError:
+            warnings.warn('''no pointer available in POINT:DATA_START indicating the start of the data block, using
+                             header pointer as fallback''')
+
+        def check_parameters(params):
+            for name in params:
+                if self.get(name) is None:
+                    warnings.warn('missing parameter {}'.format(name))
+
+        if self.point_used > 0:
+            check_parameters(('POINT:LABELS', 'POINT:DESCRIPTIONS'))
+        else:
+            lab = self.get('POINT:LABELS')
+            if lab is None:
+                warnings.warn('No point data found in file.')
+            elif lab.num_elements > 0:
+                warnings.warn('No point data found in file, but file contains POINT:LABELS entries')
+        if self.analog_used > 0:
+            check_parameters(('ANALOG:LABELS', 'ANALOG:DESCRIPTIONS'))
+        else:
+            lab = self.get('ANALOG:LABELS')
+            if lab is None:
+                warnings.warn('No analog data found in file.')
+            elif lab.num_elements > 0:
+                warnings.warn('No analog data found in file, but file contains ANALOG:LABELS entries')
+
+    def _add_group(self, group_id, name=None, desc=None):
+        '''Add a new parameter group.
+
+        Parameters
+        ----------
+        group_id : int
+            The numeric ID for a group to check or create.
+        name : str, optional
+            If a group is created, assign this name to the group.
+        desc : str, optional
+            If a group is created, assign this description to the group.
+
+        Returns
+        -------
+        group : :class:`Group`
+            A group with the given ID, name, and description.
+
+        Raises
+        ------
+        TypeError
+            Input arguments are of the wrong type.
+        KeyError
+            Name or numerical key already exist (attempt to overwrite existing data).
+        '''
+        if not is_integer(group_id):
+            raise TypeError('Expected Group numerical key to be integer, was %s.' % type(group_id))
+        if not isinstance(name, str):
+            if name is not None:
+                raise TypeError('Expected Group name key to be string, was %s.' % type(name))
+        else:
+            name = name.upper()
+        group_id = int(group_id)  # Asserts python int
+        if group_id in self._groups:
+            raise KeyError('Group with numerical key {} already exists'.format(group_id))
+        if name in self._groups:
+            raise KeyError('No group matched name key {}'.format(name))
+        group = self._groups[name] = self._groups[group_id] = Group(GroupData(self._dtypes, name, desc))
+        return group
+
+    def _remove_group(self, group_id):
+        '''Remove the parameter group.
+
+        Parameters
+        ----------
+        group_id : int, or str
+            The numeric or name ID key for a group to remove all entries for.
+        '''
+        grp = self._groups.get(group_id, None)
+        if grp is None:
+            return
+        gkeys = [k for (k, v) in self._groups.items() if v == grp]
+        for k in gkeys:
+            del self._groups[k]
+
+    def _rename_group(self, group_id, new_group_id):
+        ''' Rename a specified parameter group.
+
+        Parameters
+        ----------
+        group_id : int, str, or `c3d.group.Group`
+            Group instance, name, or numerical identifier for the group.
+        new_group_id : str, or int
+            If string, it is the new name for the group. If integer, it will replace its numerical group id.
+
+        Raises
+        ------
+        KeyError
+            If a group with a duplicate ID or name already exists.
+        '''
+        if isinstance(group_id, GroupReadonly):
+            grp = group_id._data
+        else:
+            # Aquire instance using id
+            grp = self._groups.get(group_id, None)
+            if grp is None:
+                raise KeyError('No group found matching the identifier: %s' % str(group_id))
+        if new_group_id in self._groups:
+            if new_group_id == group_id:
+                return
+            raise ValueError('Key %s for group %s already exist.' % (str(new_group_id), grp.name))
+
+        # Clear old id
+        if isinstance(new_group_id, (str, bytes)):
+            if grp.name in self._groups:
+                del self._groups[grp.name]
+            grp.name = new_group_id
+        elif is_integer(new_group_id):
+            new_group_id = int(new_group_id)  # Ensure python int
+            del self._groups[group_id]
+        else:
+            raise KeyError('Invalid group identifier of type: %s' % str(type(new_group_id)))
+        # Update
+        self._groups[new_group_id] = grp
+
+    def get(self, group, default=None):
+        '''Get a group or parameter.
+
+        Parameters
+        ----------
+        group : str
+            If this string contains a period (.), then the part before the
+            period will be used to retrieve a group, and the part after the
+            period will be used to retrieve a parameter from that group. If this
+            string does not contain a period, then just a group will be
+            returned.
+        default : any
+            Return this value if the named group and parameter are not found.
+
+        Returns
+        -------
+        value : `c3d.group.Group` or `c3d.parameter.Param`
+            Either a group or parameter with the specified name(s). If neither
+            is found, returns the default value.
+        '''
+        if is_integer(group):
+            group = self._groups.get(int(group))
+            if group is None:
+                return default
+            return group
+        group = group.upper()
+        param = None
+        if '.' in group:
+            group, param = group.split('.', 1)
+        if ':' in group:
+            group, param = group.split(':', 1)
+        if group not in self._groups:
+            return default
+        group = self._groups[group]
+        if param is not None:
+            return group.get(param, default)
+        return group
+
+    @property
+    def header(self) -> '`c3d.header.Header`':
+        ''' Access to .c3d header data. '''
+        return self._header
+
+    def parameter_blocks(self) -> int:
+        '''Compute the size (in 512B blocks) of the parameter section.'''
+        bytes = 4. + sum(g._data.binary_size for g in self._groups.values())
+        return int(np.ceil(bytes / 512))
+
+    @property
+    def point_rate(self) -> float:
+        ''' Number of sampled 3D coordinates per second. '''
+        try:
+            return self.get_float('POINT:RATE')
+        except AttributeError:
+            return self.header.frame_rate
+
+    @property
+    def point_scale(self) -> float:
+        ''' Scaling applied to non-float data. '''
+        try:
+            return self.get_float('POINT:SCALE')
+        except AttributeError:
+            return self.header.scale_factor
+
+    @property
+    def point_used(self) -> int:
+        ''' Number of sampled 3D point coordinates per frame. '''
+        try:
+            return self.get_uint16('POINT:USED')
+        except AttributeError:
+            return self.header.point_count
+
+    @property
+    def analog_used(self) -> int:
+        ''' Number of analog measurements, or channels, for each analog data sample. '''
+        try:
+            return self.get_uint16('ANALOG:USED')
+        except AttributeError:
+            per_frame = self.header.analog_per_frame
+            if per_frame > 0:
+                return int(self.header.analog_count / per_frame)
+            return 0
+
+    @property
+    def analog_rate(self) -> float:
+        '''  Number of analog data samples per second. '''
+        try:
+            return self.get_float('ANALOG:RATE')
+        except AttributeError:
+            return self.header.analog_per_frame * self.point_rate
+
+    @property
+    def analog_per_frame(self) -> int:
+        '''  Number of analog frames per 3D frame (point sample). '''
+        return int(self.analog_rate / self.point_rate)
+
+    @property
+    def analog_sample_count(self) -> int:
+        ''' Number of analog samples per channel. '''
+        has_analog = self.analog_used > 0
+        return int(self.frame_count * self.analog_per_frame) * has_analog
+
+    @property
+    def point_labels(self) -> list:
+        ''' Labels for each POINT data channel. '''
+        return self.get('POINT:LABELS').string_array
+
+    @property
+    def analog_labels(self) -> list:
+        ''' Labels for each ANALOG data channel. '''
+        return self.get('ANALOG:LABELS').string_array
+
+    @property
+    def frame_count(self) -> int:
+        ''' Number of frames recorded in the data. '''
+        return self.last_frame - self.first_frame + 1  # Add 1 since range is inclusive [first, last]
+
+    @property
+    def first_frame(self) -> int:
+        ''' Trial frame corresponding to the first frame recorded in the data. '''
+        # Start frame seems to be less of an issue to determine.
+        # this is a hack for phasespace files ... should put it in a subclass.
+        param = self.get('TRIAL:ACTUAL_START_FIELD')
+        if param is not None:
+            # ACTUAL_START_FIELD is encoded in two 16 byte words...
+            return param.uint32_value
+        return self.header.first_frame
+
+    @property
+    def last_frame(self) -> int:
+        ''' Trial frame corresponding to the last frame recorded in the data (inclusive). '''
+        # Number of frames can be represented in many formats, first check if valid header values
+        #if self.header.first_frame < self.header.last_frame and self.header.last_frame != 65535:
+        #    return self.header.last_frame
+
+        # Try different parameters where the frame can be encoded
+        hlf = self.header.last_frame
+        param = self.get('TRIAL:ACTUAL_END_FIELD')
+        if param is not None:
+            # Encoded as 2 16 bit words (rather then 1 32 bit word)
+            # words = param.uint16_array
+            # end_frame[1] = words[0] + words[1] * 65536
+            end_frame = param.uint32_value
+            if hlf <= end_frame:
+                return end_frame
+        param = self.get('POINT:LONG_FRAMES')
+        if param is not None:
+            # 'Should be' encoded as float
+            if param.bytes_per_element >= 4:
+                end_frame = int(param.float_value)
+            else:
+                end_frame = param.uint16_value
+            if hlf <= end_frame:
+                return end_frame
+        param = self.get('POINT:FRAMES')
+        if param is not None:
+            # Can be encoded either as 32 bit float or 16 bit uint
+            if param.bytes_per_element == 4:
+                end_frame = int(param.float_value)
+            else:
+                end_frame = param.uint16_value
+            if hlf <= end_frame:
+                return end_frame
+        # Return header value by default
+        return hlf
+
+    def get_screen_xy_strings(self):
+        ''' Get the POINT:X_SCREEN and POINT:Y_SCREEN parameters as strings.
+
+        See `Manager.get_screen_xy_axis` to get numpy vectors instead.
+
+        Returns
+        -------
+        value : (str, str) or None
+            Touple containing X_SCREEN and Y_SCREEN strings, or None (if no parameters could be found).
+        '''
+        X = self.get('POINT:X_SCREEN')
+        Y = self.get('POINT:Y_SCREEN')
+        if X and Y:
+            return (X.string_value, Y.string_value)
+        return None
+
+    def get_screen_xy_axis(self):
+        ''' Get the POINT:X_SCREEN and POINT:Y_SCREEN parameters as unit row vectors.
+
+        Z axis can be computed using the cross product:
+
+        $$ z = x \\times y $$
+
+        To move a point coordinate $p_s$ as read from `c3d.reader.Reader.read_frames` out of the system basis do:
+
+        $$ p = | x^T y^T z^T |^T p_s  $$
+
+
+        See `Manager.get_screen_xy_strings` to get the parameter as string values instead.
+
+        Returns
+        -------
+        value : ([3,], [3,]) or None
+            Touple $(x, y)$ containing X_SCREEN and Y_SCREEN as row vectors, or None.
+        '''
+        # Axis conversion dictionary.
+        AXIS_DICT = {
+            'X': np.array([1.0, 0, 0]),
+            '+X': np.array([1.0, 0, 0]),
+            '-X': np.array([-1.0, 0, 0]),
+            'Y': np.array([0, 1.0, 0]),
+            '+Y': np.array([0, 1.0, 0]),
+            '-Y': np.array([0, -1.0, 0]),
+            'Z': np.array([0, 0, 1.0]),
+            '+Z': np.array([0, 0, 1.0]),
+            '-Z': np.array([0, 0, -1.0]),
+        }
+
+        val = self.get_screen_xy_strings()
+        if val is None:
+            return None
+        axis_x, axis_y = val
+
+        # Interpret using both X/Y_SCREEN
+        return AXIS_DICT[axis_x], AXIS_DICT[axis_y]
+
+    def get_analog_transform_parameters(self):
+        ''' Parse analog data transform parameters. '''
+        # Offsets
+        analog_offsets = np.zeros((self.analog_used), int)
+        param = self.get('ANALOG:OFFSET')
+        if param is not None and param.num_elements > 0:
+            analog_offsets[:] = param.int16_array[:self.analog_used]
+
+        # Scale factors
+        analog_scales = np.ones((self.analog_used), float)
+        gen_scale = 1.
+        param = self.get('ANALOG:GEN_SCALE')
+        if param is not None:
+            gen_scale = param.float_value
+        param = self.get('ANALOG:SCALE')
+        if param is not None and param.num_elements > 0:
+            analog_scales[:] = param.float_array[:self.analog_used]
+
+        return gen_scale, analog_scales, analog_offsets
+
+    def get_analog_transform(self):
+        ''' Get broadcastable analog transformation parameters.
+        '''
+        gen_scale, analog_scales, analog_offsets = self.get_analog_transform_parameters()
+        analog_scales *= gen_scale
+        analog_scales = np.broadcast_to(analog_scales[:, np.newaxis], (self.analog_used, self.analog_per_frame))
+        analog_offsets = np.broadcast_to(analog_offsets[:, np.newaxis], (self.analog_used, self.analog_per_frame))
+        return analog_scales, analog_offsets
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Manager +(header=None) +
+
+

A base class for managing C3D file metadata.

+

This class manages a C3D header (which contains some stock metadata fields) +as well as a set of parameter groups. Each group is accessible using its +name.

+

Attributes

+
+
header : Header
+
Header information for the C3D file.
+
+

Set up a new Manager with a Header.

+
+ +Expand source code + +
class Manager(object):
+    '''A base class for managing C3D file metadata.
+
+    This class manages a C3D header (which contains some stock metadata fields)
+    as well as a set of parameter groups. Each group is accessible using its
+    name.
+
+    Attributes
+    ----------
+    header : `c3d.header.Header`
+        Header information for the C3D file.
+    '''
+
+    def __init__(self, header=None):
+        '''Set up a new Manager with a Header.'''
+        self._header = header or Header()
+        self._groups = {}
+
+    def __contains__(self, key):
+        return key in self._groups
+
+    def items(self):
+        ''' Get iterable over pairs of (str, `c3d.group.Group`) entries.
+        '''
+        return ((k, v) for k, v in self._groups.items() if isinstance(k, str))
+
+    def values(self):
+        ''' Get iterable over `c3d.group.Group` entries.
+        '''
+        return (v for k, v in self._groups.items() if isinstance(k, str))
+
+    def keys(self):
+        ''' Get iterable over parameter name keys.
+        '''
+        return (k for k in self._groups.keys() if isinstance(k, str))
+
+    def listed(self):
+        ''' Get iterable over pairs of (int, `c3d.group.Group`) entries.
+        '''
+        return sorted((i, g) for i, g in self._groups.items() if isinstance(i, int))
+
+    def _check_metadata(self):
+        ''' Ensure that the metadata in our file is self-consistent. '''
+        assert self._header.point_count == self.point_used, (
+            'inconsistent point count! {} header != {} POINT:USED'.format(
+                self._header.point_count,
+                self.point_used,
+            ))
+
+        assert self._header.scale_factor == self.point_scale, (
+            'inconsistent scale factor! {} header != {} POINT:SCALE'.format(
+                self._header.scale_factor,
+                self.point_scale,
+            ))
+
+        assert self._header.frame_rate == self.point_rate, (
+            'inconsistent frame rate! {} header != {} POINT:RATE'.format(
+                self._header.frame_rate,
+                self.point_rate,
+            ))
+
+        if self.point_rate:
+            ratio = self.analog_rate / self.point_rate
+        else:
+            ratio = 0
+        assert self._header.analog_per_frame == ratio, (
+            'inconsistent analog rate! {} header != {} analog-fps / {} point-fps'.format(
+                self._header.analog_per_frame,
+                self.analog_rate,
+                self.point_rate,
+            ))
+
+        count = self.analog_used * self._header.analog_per_frame
+        assert self._header.analog_count == count, (
+            'inconsistent analog count! {} header != {} analog used * {} per-frame'.format(
+                self._header.analog_count,
+                self.analog_used,
+                self._header.analog_per_frame,
+            ))
+
+        try:
+            start = self.get('POINT:DATA_START').uint16_value
+            if self._header.data_block != start:
+                warnings.warn('inconsistent data block! {} header != {} POINT:DATA_START'.format(
+                    self._header.data_block, start))
+        except AttributeError:
+            warnings.warn('''no pointer available in POINT:DATA_START indicating the start of the data block, using
+                             header pointer as fallback''')
+
+        def check_parameters(params):
+            for name in params:
+                if self.get(name) is None:
+                    warnings.warn('missing parameter {}'.format(name))
+
+        if self.point_used > 0:
+            check_parameters(('POINT:LABELS', 'POINT:DESCRIPTIONS'))
+        else:
+            lab = self.get('POINT:LABELS')
+            if lab is None:
+                warnings.warn('No point data found in file.')
+            elif lab.num_elements > 0:
+                warnings.warn('No point data found in file, but file contains POINT:LABELS entries')
+        if self.analog_used > 0:
+            check_parameters(('ANALOG:LABELS', 'ANALOG:DESCRIPTIONS'))
+        else:
+            lab = self.get('ANALOG:LABELS')
+            if lab is None:
+                warnings.warn('No analog data found in file.')
+            elif lab.num_elements > 0:
+                warnings.warn('No analog data found in file, but file contains ANALOG:LABELS entries')
+
+    def _add_group(self, group_id, name=None, desc=None):
+        '''Add a new parameter group.
+
+        Parameters
+        ----------
+        group_id : int
+            The numeric ID for a group to check or create.
+        name : str, optional
+            If a group is created, assign this name to the group.
+        desc : str, optional
+            If a group is created, assign this description to the group.
+
+        Returns
+        -------
+        group : :class:`Group`
+            A group with the given ID, name, and description.
+
+        Raises
+        ------
+        TypeError
+            Input arguments are of the wrong type.
+        KeyError
+            Name or numerical key already exist (attempt to overwrite existing data).
+        '''
+        if not is_integer(group_id):
+            raise TypeError('Expected Group numerical key to be integer, was %s.' % type(group_id))
+        if not isinstance(name, str):
+            if name is not None:
+                raise TypeError('Expected Group name key to be string, was %s.' % type(name))
+        else:
+            name = name.upper()
+        group_id = int(group_id)  # Asserts python int
+        if group_id in self._groups:
+            raise KeyError('Group with numerical key {} already exists'.format(group_id))
+        if name in self._groups:
+            raise KeyError('No group matched name key {}'.format(name))
+        group = self._groups[name] = self._groups[group_id] = Group(GroupData(self._dtypes, name, desc))
+        return group
+
+    def _remove_group(self, group_id):
+        '''Remove the parameter group.
+
+        Parameters
+        ----------
+        group_id : int, or str
+            The numeric or name ID key for a group to remove all entries for.
+        '''
+        grp = self._groups.get(group_id, None)
+        if grp is None:
+            return
+        gkeys = [k for (k, v) in self._groups.items() if v == grp]
+        for k in gkeys:
+            del self._groups[k]
+
+    def _rename_group(self, group_id, new_group_id):
+        ''' Rename a specified parameter group.
+
+        Parameters
+        ----------
+        group_id : int, str, or `c3d.group.Group`
+            Group instance, name, or numerical identifier for the group.
+        new_group_id : str, or int
+            If string, it is the new name for the group. If integer, it will replace its numerical group id.
+
+        Raises
+        ------
+        KeyError
+            If a group with a duplicate ID or name already exists.
+        '''
+        if isinstance(group_id, GroupReadonly):
+            grp = group_id._data
+        else:
+            # Aquire instance using id
+            grp = self._groups.get(group_id, None)
+            if grp is None:
+                raise KeyError('No group found matching the identifier: %s' % str(group_id))
+        if new_group_id in self._groups:
+            if new_group_id == group_id:
+                return
+            raise ValueError('Key %s for group %s already exist.' % (str(new_group_id), grp.name))
+
+        # Clear old id
+        if isinstance(new_group_id, (str, bytes)):
+            if grp.name in self._groups:
+                del self._groups[grp.name]
+            grp.name = new_group_id
+        elif is_integer(new_group_id):
+            new_group_id = int(new_group_id)  # Ensure python int
+            del self._groups[group_id]
+        else:
+            raise KeyError('Invalid group identifier of type: %s' % str(type(new_group_id)))
+        # Update
+        self._groups[new_group_id] = grp
+
+    def get(self, group, default=None):
+        '''Get a group or parameter.
+
+        Parameters
+        ----------
+        group : str
+            If this string contains a period (.), then the part before the
+            period will be used to retrieve a group, and the part after the
+            period will be used to retrieve a parameter from that group. If this
+            string does not contain a period, then just a group will be
+            returned.
+        default : any
+            Return this value if the named group and parameter are not found.
+
+        Returns
+        -------
+        value : `c3d.group.Group` or `c3d.parameter.Param`
+            Either a group or parameter with the specified name(s). If neither
+            is found, returns the default value.
+        '''
+        if is_integer(group):
+            group = self._groups.get(int(group))
+            if group is None:
+                return default
+            return group
+        group = group.upper()
+        param = None
+        if '.' in group:
+            group, param = group.split('.', 1)
+        if ':' in group:
+            group, param = group.split(':', 1)
+        if group not in self._groups:
+            return default
+        group = self._groups[group]
+        if param is not None:
+            return group.get(param, default)
+        return group
+
+    @property
+    def header(self) -> '`c3d.header.Header`':
+        ''' Access to .c3d header data. '''
+        return self._header
+
+    def parameter_blocks(self) -> int:
+        '''Compute the size (in 512B blocks) of the parameter section.'''
+        bytes = 4. + sum(g._data.binary_size for g in self._groups.values())
+        return int(np.ceil(bytes / 512))
+
+    @property
+    def point_rate(self) -> float:
+        ''' Number of sampled 3D coordinates per second. '''
+        try:
+            return self.get_float('POINT:RATE')
+        except AttributeError:
+            return self.header.frame_rate
+
+    @property
+    def point_scale(self) -> float:
+        ''' Scaling applied to non-float data. '''
+        try:
+            return self.get_float('POINT:SCALE')
+        except AttributeError:
+            return self.header.scale_factor
+
+    @property
+    def point_used(self) -> int:
+        ''' Number of sampled 3D point coordinates per frame. '''
+        try:
+            return self.get_uint16('POINT:USED')
+        except AttributeError:
+            return self.header.point_count
+
+    @property
+    def analog_used(self) -> int:
+        ''' Number of analog measurements, or channels, for each analog data sample. '''
+        try:
+            return self.get_uint16('ANALOG:USED')
+        except AttributeError:
+            per_frame = self.header.analog_per_frame
+            if per_frame > 0:
+                return int(self.header.analog_count / per_frame)
+            return 0
+
+    @property
+    def analog_rate(self) -> float:
+        '''  Number of analog data samples per second. '''
+        try:
+            return self.get_float('ANALOG:RATE')
+        except AttributeError:
+            return self.header.analog_per_frame * self.point_rate
+
+    @property
+    def analog_per_frame(self) -> int:
+        '''  Number of analog frames per 3D frame (point sample). '''
+        return int(self.analog_rate / self.point_rate)
+
+    @property
+    def analog_sample_count(self) -> int:
+        ''' Number of analog samples per channel. '''
+        has_analog = self.analog_used > 0
+        return int(self.frame_count * self.analog_per_frame) * has_analog
+
+    @property
+    def point_labels(self) -> list:
+        ''' Labels for each POINT data channel. '''
+        return self.get('POINT:LABELS').string_array
+
+    @property
+    def analog_labels(self) -> list:
+        ''' Labels for each ANALOG data channel. '''
+        return self.get('ANALOG:LABELS').string_array
+
+    @property
+    def frame_count(self) -> int:
+        ''' Number of frames recorded in the data. '''
+        return self.last_frame - self.first_frame + 1  # Add 1 since range is inclusive [first, last]
+
+    @property
+    def first_frame(self) -> int:
+        ''' Trial frame corresponding to the first frame recorded in the data. '''
+        # Start frame seems to be less of an issue to determine.
+        # this is a hack for phasespace files ... should put it in a subclass.
+        param = self.get('TRIAL:ACTUAL_START_FIELD')
+        if param is not None:
+            # ACTUAL_START_FIELD is encoded in two 16 byte words...
+            return param.uint32_value
+        return self.header.first_frame
+
+    @property
+    def last_frame(self) -> int:
+        ''' Trial frame corresponding to the last frame recorded in the data (inclusive). '''
+        # Number of frames can be represented in many formats, first check if valid header values
+        #if self.header.first_frame < self.header.last_frame and self.header.last_frame != 65535:
+        #    return self.header.last_frame
+
+        # Try different parameters where the frame can be encoded
+        hlf = self.header.last_frame
+        param = self.get('TRIAL:ACTUAL_END_FIELD')
+        if param is not None:
+            # Encoded as 2 16 bit words (rather then 1 32 bit word)
+            # words = param.uint16_array
+            # end_frame[1] = words[0] + words[1] * 65536
+            end_frame = param.uint32_value
+            if hlf <= end_frame:
+                return end_frame
+        param = self.get('POINT:LONG_FRAMES')
+        if param is not None:
+            # 'Should be' encoded as float
+            if param.bytes_per_element >= 4:
+                end_frame = int(param.float_value)
+            else:
+                end_frame = param.uint16_value
+            if hlf <= end_frame:
+                return end_frame
+        param = self.get('POINT:FRAMES')
+        if param is not None:
+            # Can be encoded either as 32 bit float or 16 bit uint
+            if param.bytes_per_element == 4:
+                end_frame = int(param.float_value)
+            else:
+                end_frame = param.uint16_value
+            if hlf <= end_frame:
+                return end_frame
+        # Return header value by default
+        return hlf
+
+    def get_screen_xy_strings(self):
+        ''' Get the POINT:X_SCREEN and POINT:Y_SCREEN parameters as strings.
+
+        See `Manager.get_screen_xy_axis` to get numpy vectors instead.
+
+        Returns
+        -------
+        value : (str, str) or None
+            Touple containing X_SCREEN and Y_SCREEN strings, or None (if no parameters could be found).
+        '''
+        X = self.get('POINT:X_SCREEN')
+        Y = self.get('POINT:Y_SCREEN')
+        if X and Y:
+            return (X.string_value, Y.string_value)
+        return None
+
+    def get_screen_xy_axis(self):
+        ''' Get the POINT:X_SCREEN and POINT:Y_SCREEN parameters as unit row vectors.
+
+        Z axis can be computed using the cross product:
+
+        $$ z = x \\times y $$
+
+        To move a point coordinate $p_s$ as read from `c3d.reader.Reader.read_frames` out of the system basis do:
+
+        $$ p = | x^T y^T z^T |^T p_s  $$
+
+
+        See `Manager.get_screen_xy_strings` to get the parameter as string values instead.
+
+        Returns
+        -------
+        value : ([3,], [3,]) or None
+            Touple $(x, y)$ containing X_SCREEN and Y_SCREEN as row vectors, or None.
+        '''
+        # Axis conversion dictionary.
+        AXIS_DICT = {
+            'X': np.array([1.0, 0, 0]),
+            '+X': np.array([1.0, 0, 0]),
+            '-X': np.array([-1.0, 0, 0]),
+            'Y': np.array([0, 1.0, 0]),
+            '+Y': np.array([0, 1.0, 0]),
+            '-Y': np.array([0, -1.0, 0]),
+            'Z': np.array([0, 0, 1.0]),
+            '+Z': np.array([0, 0, 1.0]),
+            '-Z': np.array([0, 0, -1.0]),
+        }
+
+        val = self.get_screen_xy_strings()
+        if val is None:
+            return None
+        axis_x, axis_y = val
+
+        # Interpret using both X/Y_SCREEN
+        return AXIS_DICT[axis_x], AXIS_DICT[axis_y]
+
+    def get_analog_transform_parameters(self):
+        ''' Parse analog data transform parameters. '''
+        # Offsets
+        analog_offsets = np.zeros((self.analog_used), int)
+        param = self.get('ANALOG:OFFSET')
+        if param is not None and param.num_elements > 0:
+            analog_offsets[:] = param.int16_array[:self.analog_used]
+
+        # Scale factors
+        analog_scales = np.ones((self.analog_used), float)
+        gen_scale = 1.
+        param = self.get('ANALOG:GEN_SCALE')
+        if param is not None:
+            gen_scale = param.float_value
+        param = self.get('ANALOG:SCALE')
+        if param is not None and param.num_elements > 0:
+            analog_scales[:] = param.float_array[:self.analog_used]
+
+        return gen_scale, analog_scales, analog_offsets
+
+    def get_analog_transform(self):
+        ''' Get broadcastable analog transformation parameters.
+        '''
+        gen_scale, analog_scales, analog_offsets = self.get_analog_transform_parameters()
+        analog_scales *= gen_scale
+        analog_scales = np.broadcast_to(analog_scales[:, np.newaxis], (self.analog_used, self.analog_per_frame))
+        analog_offsets = np.broadcast_to(analog_offsets[:, np.newaxis], (self.analog_used, self.analog_per_frame))
+        return analog_scales, analog_offsets
+
+

Subclasses

+ +

Instance variables

+
+
var analog_labels : list
+
+

Labels for each ANALOG data channel.

+
+ +Expand source code + +
@property
+def analog_labels(self) -> list:
+    ''' Labels for each ANALOG data channel. '''
+    return self.get('ANALOG:LABELS').string_array
+
+
+
var analog_per_frame : int
+
+

Number of analog frames per 3D frame (point sample).

+
+ +Expand source code + +
@property
+def analog_per_frame(self) -> int:
+    '''  Number of analog frames per 3D frame (point sample). '''
+    return int(self.analog_rate / self.point_rate)
+
+
+
var analog_rate : float
+
+

Number of analog data samples per second.

+
+ +Expand source code + +
@property
+def analog_rate(self) -> float:
+    '''  Number of analog data samples per second. '''
+    try:
+        return self.get_float('ANALOG:RATE')
+    except AttributeError:
+        return self.header.analog_per_frame * self.point_rate
+
+
+
var analog_sample_count : int
+
+

Number of analog samples per channel.

+
+ +Expand source code + +
@property
+def analog_sample_count(self) -> int:
+    ''' Number of analog samples per channel. '''
+    has_analog = self.analog_used > 0
+    return int(self.frame_count * self.analog_per_frame) * has_analog
+
+
+
var analog_used : int
+
+

Number of analog measurements, or channels, for each analog data sample.

+
+ +Expand source code + +
@property
+def analog_used(self) -> int:
+    ''' Number of analog measurements, or channels, for each analog data sample. '''
+    try:
+        return self.get_uint16('ANALOG:USED')
+    except AttributeError:
+        per_frame = self.header.analog_per_frame
+        if per_frame > 0:
+            return int(self.header.analog_count / per_frame)
+        return 0
+
+
+
var first_frame : int
+
+

Trial frame corresponding to the first frame recorded in the data.

+
+ +Expand source code + +
@property
+def first_frame(self) -> int:
+    ''' Trial frame corresponding to the first frame recorded in the data. '''
+    # Start frame seems to be less of an issue to determine.
+    # this is a hack for phasespace files ... should put it in a subclass.
+    param = self.get('TRIAL:ACTUAL_START_FIELD')
+    if param is not None:
+        # ACTUAL_START_FIELD is encoded in two 16 byte words...
+        return param.uint32_value
+    return self.header.first_frame
+
+
+
var frame_count : int
+
+

Number of frames recorded in the data.

+
+ +Expand source code + +
@property
+def frame_count(self) -> int:
+    ''' Number of frames recorded in the data. '''
+    return self.last_frame - self.first_frame + 1  # Add 1 since range is inclusive [first, last]
+
+
+
var header : `Header`
+
+

Access to .c3d header data.

+
+ +Expand source code + +
@property
+def header(self) -> '`c3d.header.Header`':
+    ''' Access to .c3d header data. '''
+    return self._header
+
+
+
var last_frame : int
+
+

Trial frame corresponding to the last frame recorded in the data (inclusive).

+
+ +Expand source code + +
@property
+def last_frame(self) -> int:
+    ''' Trial frame corresponding to the last frame recorded in the data (inclusive). '''
+    # Number of frames can be represented in many formats, first check if valid header values
+    #if self.header.first_frame < self.header.last_frame and self.header.last_frame != 65535:
+    #    return self.header.last_frame
+
+    # Try different parameters where the frame can be encoded
+    hlf = self.header.last_frame
+    param = self.get('TRIAL:ACTUAL_END_FIELD')
+    if param is not None:
+        # Encoded as 2 16 bit words (rather then 1 32 bit word)
+        # words = param.uint16_array
+        # end_frame[1] = words[0] + words[1] * 65536
+        end_frame = param.uint32_value
+        if hlf <= end_frame:
+            return end_frame
+    param = self.get('POINT:LONG_FRAMES')
+    if param is not None:
+        # 'Should be' encoded as float
+        if param.bytes_per_element >= 4:
+            end_frame = int(param.float_value)
+        else:
+            end_frame = param.uint16_value
+        if hlf <= end_frame:
+            return end_frame
+    param = self.get('POINT:FRAMES')
+    if param is not None:
+        # Can be encoded either as 32 bit float or 16 bit uint
+        if param.bytes_per_element == 4:
+            end_frame = int(param.float_value)
+        else:
+            end_frame = param.uint16_value
+        if hlf <= end_frame:
+            return end_frame
+    # Return header value by default
+    return hlf
+
+
+
var point_labels : list
+
+

Labels for each POINT data channel.

+
+ +Expand source code + +
@property
+def point_labels(self) -> list:
+    ''' Labels for each POINT data channel. '''
+    return self.get('POINT:LABELS').string_array
+
+
+
var point_rate : float
+
+

Number of sampled 3D coordinates per second.

+
+ +Expand source code + +
@property
+def point_rate(self) -> float:
+    ''' Number of sampled 3D coordinates per second. '''
+    try:
+        return self.get_float('POINT:RATE')
+    except AttributeError:
+        return self.header.frame_rate
+
+
+
var point_scale : float
+
+

Scaling applied to non-float data.

+
+ +Expand source code + +
@property
+def point_scale(self) -> float:
+    ''' Scaling applied to non-float data. '''
+    try:
+        return self.get_float('POINT:SCALE')
+    except AttributeError:
+        return self.header.scale_factor
+
+
+
var point_used : int
+
+

Number of sampled 3D point coordinates per frame.

+
+ +Expand source code + +
@property
+def point_used(self) -> int:
+    ''' Number of sampled 3D point coordinates per frame. '''
+    try:
+        return self.get_uint16('POINT:USED')
+    except AttributeError:
+        return self.header.point_count
+
+
+
+

Methods

+
+
+def get(self, group, default=None) +
+
+

Get a group or parameter.

+

Parameters

+
+
group : str
+
If this string contains a period (.), then the part before the +period will be used to retrieve a group, and the part after the +period will be used to retrieve a parameter from that group. If this +string does not contain a period, then just a group will be +returned.
+
default : any
+
Return this value if the named group and parameter are not found.
+
+

Returns

+
+
value : Group or Param
+
Either a group or parameter with the specified name(s). If neither +is found, returns the default value.
+
+
+ +Expand source code + +
def get(self, group, default=None):
+    '''Get a group or parameter.
+
+    Parameters
+    ----------
+    group : str
+        If this string contains a period (.), then the part before the
+        period will be used to retrieve a group, and the part after the
+        period will be used to retrieve a parameter from that group. If this
+        string does not contain a period, then just a group will be
+        returned.
+    default : any
+        Return this value if the named group and parameter are not found.
+
+    Returns
+    -------
+    value : `c3d.group.Group` or `c3d.parameter.Param`
+        Either a group or parameter with the specified name(s). If neither
+        is found, returns the default value.
+    '''
+    if is_integer(group):
+        group = self._groups.get(int(group))
+        if group is None:
+            return default
+        return group
+    group = group.upper()
+    param = None
+    if '.' in group:
+        group, param = group.split('.', 1)
+    if ':' in group:
+        group, param = group.split(':', 1)
+    if group not in self._groups:
+        return default
+    group = self._groups[group]
+    if param is not None:
+        return group.get(param, default)
+    return group
+
+
+
+def get_analog_transform(self) +
+
+

Get broadcastable analog transformation parameters.

+
+ +Expand source code + +
def get_analog_transform(self):
+    ''' Get broadcastable analog transformation parameters.
+    '''
+    gen_scale, analog_scales, analog_offsets = self.get_analog_transform_parameters()
+    analog_scales *= gen_scale
+    analog_scales = np.broadcast_to(analog_scales[:, np.newaxis], (self.analog_used, self.analog_per_frame))
+    analog_offsets = np.broadcast_to(analog_offsets[:, np.newaxis], (self.analog_used, self.analog_per_frame))
+    return analog_scales, analog_offsets
+
+
+
+def get_analog_transform_parameters(self) +
+
+

Parse analog data transform parameters.

+
+ +Expand source code + +
def get_analog_transform_parameters(self):
+    ''' Parse analog data transform parameters. '''
+    # Offsets
+    analog_offsets = np.zeros((self.analog_used), int)
+    param = self.get('ANALOG:OFFSET')
+    if param is not None and param.num_elements > 0:
+        analog_offsets[:] = param.int16_array[:self.analog_used]
+
+    # Scale factors
+    analog_scales = np.ones((self.analog_used), float)
+    gen_scale = 1.
+    param = self.get('ANALOG:GEN_SCALE')
+    if param is not None:
+        gen_scale = param.float_value
+    param = self.get('ANALOG:SCALE')
+    if param is not None and param.num_elements > 0:
+        analog_scales[:] = param.float_array[:self.analog_used]
+
+    return gen_scale, analog_scales, analog_offsets
+
+
+
+def get_screen_xy_axis(self) +
+
+

Get the POINT:X_SCREEN and POINT:Y_SCREEN parameters as unit row vectors.

+

Z axis can be computed using the cross product:

+

z = x \times y

+

To move a point coordinate $p_s$ as read from Reader.read_frames() out of the system basis do:

+

p = | x^T y^T z^T |^T p_s +

+

See Manager.get_screen_xy_strings() to get the parameter as string values instead.

+

Returns

+
+
value : ([3,], [3,]) or None
+
Touple $(x, y)$ containing X_SCREEN and Y_SCREEN as row vectors, or None.
+
+
+ +Expand source code + +
def get_screen_xy_axis(self):
+    ''' Get the POINT:X_SCREEN and POINT:Y_SCREEN parameters as unit row vectors.
+
+    Z axis can be computed using the cross product:
+
+    $$ z = x \\times y $$
+
+    To move a point coordinate $p_s$ as read from `c3d.reader.Reader.read_frames` out of the system basis do:
+
+    $$ p = | x^T y^T z^T |^T p_s  $$
+
+
+    See `Manager.get_screen_xy_strings` to get the parameter as string values instead.
+
+    Returns
+    -------
+    value : ([3,], [3,]) or None
+        Touple $(x, y)$ containing X_SCREEN and Y_SCREEN as row vectors, or None.
+    '''
+    # Axis conversion dictionary.
+    AXIS_DICT = {
+        'X': np.array([1.0, 0, 0]),
+        '+X': np.array([1.0, 0, 0]),
+        '-X': np.array([-1.0, 0, 0]),
+        'Y': np.array([0, 1.0, 0]),
+        '+Y': np.array([0, 1.0, 0]),
+        '-Y': np.array([0, -1.0, 0]),
+        'Z': np.array([0, 0, 1.0]),
+        '+Z': np.array([0, 0, 1.0]),
+        '-Z': np.array([0, 0, -1.0]),
+    }
+
+    val = self.get_screen_xy_strings()
+    if val is None:
+        return None
+    axis_x, axis_y = val
+
+    # Interpret using both X/Y_SCREEN
+    return AXIS_DICT[axis_x], AXIS_DICT[axis_y]
+
+
+
+def get_screen_xy_strings(self) +
+
+

Get the POINT:X_SCREEN and POINT:Y_SCREEN parameters as strings.

+

See Manager.get_screen_xy_axis() to get numpy vectors instead.

+

Returns

+
+
value : (str, str) or None
+
Touple containing X_SCREEN and Y_SCREEN strings, or None (if no parameters could be found).
+
+
+ +Expand source code + +
def get_screen_xy_strings(self):
+    ''' Get the POINT:X_SCREEN and POINT:Y_SCREEN parameters as strings.
+
+    See `Manager.get_screen_xy_axis` to get numpy vectors instead.
+
+    Returns
+    -------
+    value : (str, str) or None
+        Touple containing X_SCREEN and Y_SCREEN strings, or None (if no parameters could be found).
+    '''
+    X = self.get('POINT:X_SCREEN')
+    Y = self.get('POINT:Y_SCREEN')
+    if X and Y:
+        return (X.string_value, Y.string_value)
+    return None
+
+
+
+def items(self) +
+
+

Get iterable over pairs of (str, Group) entries.

+
+ +Expand source code + +
def items(self):
+    ''' Get iterable over pairs of (str, `c3d.group.Group`) entries.
+    '''
+    return ((k, v) for k, v in self._groups.items() if isinstance(k, str))
+
+
+
+def keys(self) +
+
+

Get iterable over parameter name keys.

+
+ +Expand source code + +
def keys(self):
+    ''' Get iterable over parameter name keys.
+    '''
+    return (k for k in self._groups.keys() if isinstance(k, str))
+
+
+
+def listed(self) +
+
+

Get iterable over pairs of (int, Group) entries.

+
+ +Expand source code + +
def listed(self):
+    ''' Get iterable over pairs of (int, `c3d.group.Group`) entries.
+    '''
+    return sorted((i, g) for i, g in self._groups.items() if isinstance(i, int))
+
+
+
+def parameter_blocks(self) ‑> int +
+
+

Compute the size (in 512B blocks) of the parameter section.

+
+ +Expand source code + +
def parameter_blocks(self) -> int:
+    '''Compute the size (in 512B blocks) of the parameter section.'''
+    bytes = 4. + sum(g._data.binary_size for g in self._groups.values())
+    return int(np.ceil(bytes / 512))
+
+
+
+def values(self) +
+
+

Get iterable over Group entries.

+
+ +Expand source code + +
def values(self):
+    ''' Get iterable over `c3d.group.Group` entries.
+    '''
+    return (v for k, v in self._groups.items() if isinstance(k, str))
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/c3d/parameter.html b/docs/c3d/parameter.html new file mode 100644 index 0000000..4a3c1be --- /dev/null +++ b/docs/c3d/parameter.html @@ -0,0 +1,1958 @@ + + + + + + +c3d.parameter API documentation + + + + + + + + + + + + +
+
+
+

Module c3d.parameter

+
+
+

Classes used to represent the concept of a parameter in a .c3d file.

+
+ +Expand source code + +
''' Classes used to represent the concept of a parameter in a .c3d file.
+'''
+import struct
+import numpy as np
+from .utils import DEC_to_IEEE, DEC_to_IEEE_BYTES
+
+
+class ParamData(object):
+    '''A class representing a single named parameter from a C3D file.
+
+    Attributes
+    ----------
+    name : str
+        Name of this parameter.
+    dtype: DataTypes
+        Reference to the DataTypes object associated with the file.
+    desc : str
+        Brief description of this parameter.
+    bytes_per_element : int, optional
+        For array data, this describes the size of each element of data. For
+        string data (including arrays of strings), this should be -1.
+    dimensions : list of int
+        For array data, this describes the dimensions of the array, stored in
+        column-major (Fortran) order. For arrays of strings, the dimensions here will be
+        the number of columns (length of each string) followed by the number of
+        rows (number of strings).
+    bytes : str
+        Raw data for this parameter.
+    '''
+
+    def __init__(self,
+                 name,
+                 dtype,
+                 desc='',
+                 bytes_per_element=1,
+                 dimensions=None,
+                 bytes=b'',
+                 handle=None):
+        '''Set up a new parameter, only the name is required.'''
+        self.name = name
+        self.dtypes = dtype
+        self.desc = desc
+        self.bytes_per_element = bytes_per_element
+        self.dimensions = dimensions or []
+        self.bytes = bytes
+        if handle:
+            self.read(handle)
+
+    def __repr__(self):
+        return '<Param: {}>'.format(self.desc)
+
+    @property
+    def num_elements(self) -> int:
+        '''Return the number of elements in this parameter's array value.'''
+        e = 1
+        for d in self.dimensions:
+            e *= d
+        return e
+
+    @property
+    def total_bytes(self) -> int:
+        '''Return the number of bytes used for storing this parameter's data.'''
+        return self.num_elements * abs(self.bytes_per_element)
+
+    @property
+    def binary_size(self) -> int:
+        '''Return the number of bytes needed to store this parameter.'''
+        return (
+            1 +  # group_id
+            2 +  # next offset marker
+            1 + len(self.name.encode('utf-8')) +  # size of name and name bytes
+            1 +  # data size
+            # size of dimensions and dimension bytes
+            1 + len(self.dimensions) +
+            self.total_bytes +  # data
+            1 + len(self.desc.encode('utf-8'))  # size of desc and desc bytes
+        )
+
+    def write(self, group_id, handle):
+        '''Write binary data for this parameter to a file handle.
+
+        Parameters
+        ----------
+        group_id : int
+            The numerical ID of the group that holds this parameter.
+        handle : file handle
+            An open, writable, binary file handle.
+        '''
+        name = self.name.encode('utf-8')
+        handle.write(struct.pack('bb', len(name), group_id))
+        handle.write(name)
+        handle.write(struct.pack('<h', self.binary_size - 2 - len(name)))
+        handle.write(struct.pack('b', self.bytes_per_element))
+        handle.write(struct.pack('B', len(self.dimensions)))
+        handle.write(struct.pack('B' * len(self.dimensions), *self.dimensions))
+        if self.bytes is not None and len(self.bytes) > 0:
+            handle.write(self.bytes)
+        desc = self.desc.encode('utf-8')
+        handle.write(struct.pack('B', len(desc)))
+        handle.write(desc)
+
+    def read(self, handle):
+        '''Read binary data for this parameter from a file handle.
+
+        This reads exactly enough data from the current position in the file to
+        initialize the parameter.
+        '''
+        self.bytes_per_element, = struct.unpack('b', handle.read(1))
+        dims, = struct.unpack('B', handle.read(1))
+        self.dimensions = [struct.unpack('B', handle.read(1))[
+            0] for _ in range(dims)]
+        self.bytes = b''
+        if self.total_bytes:
+            self.bytes = handle.read(self.total_bytes)
+        desc_size, = struct.unpack('B', handle.read(1))
+        self.desc = desc_size and self.dtypes.decode_string(handle.read(desc_size)) or ''
+
+    def _as(self, dtype):
+        '''Unpack the raw bytes of this param using the given struct format.'''
+        return np.frombuffer(self.bytes, count=1, dtype=dtype)[0]
+
+    def _as_array(self, dtype, copy=True):
+        '''Unpack the raw bytes of this param using the given data format.'''
+        if not self.dimensions:
+            return [self._as(dtype)]
+        elems = np.frombuffer(self.bytes, dtype=dtype)
+        # Reverse shape as the shape is defined in fortran format
+        view = elems.reshape(self.dimensions[::-1])
+        if copy:
+            return view.copy()
+        return view
+
+
+class ParamReadonly(object):
+    ''' Wrapper exposing readonly attributes of a `c3d.parameter.ParamData` entry.
+    '''
+
+    def __init__(self, data):
+        self._data = data
+
+    def __eq__(self, other):
+        return self._data is other._data
+
+    @property
+    def name(self) -> str:
+        ''' Get the parameter name. '''
+        return self._data.name
+
+    @property
+    def desc(self) -> str:
+        ''' Get the parameter descriptor. '''
+        return self._data.desc
+
+    @property
+    def dtypes(self):
+        ''' Convenience accessor to the `c3d.dtypes.DataTypes` instance associated with the parameter. '''
+        return self._data.dtypes
+
+    @property
+    def dimensions(self) -> (int, ...):
+        ''' Shape of the parameter data (Fortran format). '''
+        return self._data.dimensions
+
+    @property
+    def num_elements(self) -> int:
+        '''Return the number of elements in this parameter's array value.'''
+        return self._data.num_elements
+
+    @property
+    def bytes_per_element(self) -> int:
+        '''Return the number of bytes used to store each data element.'''
+        return self._data.bytes_per_element
+
+    @property
+    def total_bytes(self) -> int:
+        '''Return the number of bytes used for storing this parameter's data.'''
+        return self._data.total_bytes
+
+    @property
+    def binary_size(self) -> int:
+        '''Return the number of bytes needed to store this parameter.'''
+        return self._data.binary_size
+
+    @property
+    def int8_value(self):
+        '''Get the parameter data as an 8-bit signed integer.'''
+        return self._data._as(self.dtypes.int8)
+
+    @property
+    def uint8_value(self):
+        '''Get the parameter data as an 8-bit unsigned integer.'''
+        return self._data._as(self.dtypes.uint8)
+
+    @property
+    def int16_value(self):
+        '''Get the parameter data as a 16-bit signed integer.'''
+        return self._data._as(self.dtypes.int16)
+
+    @property
+    def uint16_value(self):
+        '''Get the parameter data as a 16-bit unsigned integer.'''
+        return self._data._as(self.dtypes.uint16)
+
+    @property
+    def int32_value(self):
+        '''Get the parameter data as a 32-bit signed integer.'''
+        return self._data._as(self.dtypes.int32)
+
+    @property
+    def uint32_value(self):
+        '''Get the parameter data as a 32-bit unsigned integer.'''
+        return self._data._as(self.dtypes.uint32)
+
+    @property
+    def uint_value(self):
+        ''' Get the parameter data as a unsigned integer of appropriate type. '''
+        if self.bytes_per_element >= 4:
+            return self.uint32_value
+        elif self.bytes_per_element >= 2:
+            return self.uint16_value
+        else:
+            return self.uint8_value
+
+    @property
+    def int_value(self):
+        ''' Get the parameter data as a signed integer of appropriate type. '''
+        if self.bytes_per_element >= 4:
+            return self.int32_value
+        elif self.bytes_per_element >= 2:
+            return self.int16_value
+        else:
+            return self.int8_value
+
+    @property
+    def float_value(self):
+        '''Get the parameter data as a floating point value of appropriate type.'''
+        if self.bytes_per_element > 4:
+            if self.dtypes.is_dec:
+                raise AttributeError("64 bit DEC floating point is not supported.")
+            # 64-bit floating point is not a standard
+            return self._data._as(self.dtypes.float64)
+        elif self.bytes_per_element == 4:
+            if self.dtypes.is_dec:
+                return DEC_to_IEEE(self._data._as(np.uint32))
+            else:  # is_mips or is_ieee
+                return self._data._as(self.dtypes.float32)
+        else:
+            raise AttributeError("Only 32 and 64 bit floating point is supported.")
+
+    @property
+    def bytes_value(self) -> bytes:
+        '''Get the raw byte string.'''
+        return self._data.bytes
+
+    @property
+    def string_value(self):
+        '''Get the parameter data as a unicode string.'''
+        return self.dtypes.decode_string(self._data.bytes)
+
+    @property
+    def int8_array(self):
+        '''Get the parameter data as an array of 8-bit signed integers.'''
+        return self._data._as_array(self.dtypes.int8)
+
+    @property
+    def uint8_array(self):
+        '''Get the parameter data as an array of 8-bit unsigned integers.'''
+        return self._data._as_array(self.dtypes.uint8)
+
+    @property
+    def int16_array(self):
+        '''Get the parameter data as an array of 16-bit signed integers.'''
+        return self._data._as_array(self.dtypes.int16)
+
+    @property
+    def uint16_array(self):
+        '''Get the parameter data as an array of 16-bit unsigned integers.'''
+        return self._data._as_array(self.dtypes.uint16)
+
+    @property
+    def int32_array(self):
+        '''Get the parameter data as an array of 32-bit signed integers.'''
+        return self._data._as_array(self.dtypes.int32)
+
+    @property
+    def uint32_array(self):
+        '''Get the parameter data as an array of 32-bit unsigned integers.'''
+        return self._data._as_array(self.dtypes.uint32)
+
+    @property
+    def int64_array(self):
+        '''Get the parameter data as an array of 32-bit signed integers.'''
+        return self._data._as_array(self.dtypes.int64)
+
+    @property
+    def uint64_array(self):
+        '''Get the parameter data as an array of 32-bit unsigned integers.'''
+        return self._data._as_array(self.dtypes.uint64)
+
+    @property
+    def float32_array(self):
+        '''Get the parameter data as an array of 32-bit floats.'''
+        # Convert float data if not IEEE processor
+        if self.dtypes.is_dec:
+            # _as_array but for DEC
+            if not self.dimensions:
+                return [self.float_value]
+            return DEC_to_IEEE_BYTES(self._data.bytes).reshape(self.dimensions[::-1])  # Reverse fortran format
+        else:  # is_ieee or is_mips
+            return self._data._as_array(self.dtypes.float32)
+
+    @property
+    def float64_array(self):
+        '''Get the parameter data as an array of 64-bit floats.'''
+        # Convert float data if not IEEE processor
+        if self.dtypes.is_dec:
+            raise ValueError('Unable to convert bytes encoded in a 64 bit floating point DEC format.')
+        else:  # is_ieee or is_mips
+            return self._data._as_array(self.dtypes.float64)
+
+    @property
+    def float_array(self):
+        '''Get the parameter data as an array of 32 or 64 bit floats.'''
+        # Convert float data if not IEEE processor
+        if self.bytes_per_element == 4:
+            return self.float32_array
+        elif self.bytes_per_element == 8:
+            return self.float64_array
+        else:
+            raise TypeError("Parsing parameter bytes to an array with %i bit " % self.bytes_per_element +
+                            "floating-point precission is not unsupported.")
+
+    @property
+    def int_array(self):
+        '''Get the parameter data as an array of integer values.'''
+        # Convert float data if not IEEE processor
+        if self.bytes_per_element == 1:
+            return self.int8_array
+        elif self.bytes_per_element == 2:
+            return self.int16_array
+        elif self.bytes_per_element == 4:
+            return self.int32_array
+        elif self.bytes_per_element == 8:
+            return self.int64_array
+        else:
+            raise TypeError("Parsing parameter bytes to an array with %i bit integer values is not unsupported." %
+                            self.bytes_per_element)
+
+    @property
+    def uint_array(self):
+        '''Get the parameter data as an array of integer values.'''
+        # Convert float data if not IEEE processor
+        if self.bytes_per_element == 1:
+            return self.uint8_array
+        elif self.bytes_per_element == 2:
+            return self.uint16_array
+        elif self.bytes_per_element == 4:
+            return self.uint32_array
+        elif self.bytes_per_element == 8:
+            return self.uint64_array
+        else:
+            raise TypeError("Parsing parameter bytes to an array with %i bit integer values is not unsupported." %
+                            self.bytes_per_element)
+
+    @property
+    def bytes_array(self):
+        '''Get the parameter data as an array of raw byte strings.'''
+        # Decode different dimensions
+        if len(self.dimensions) == 0:
+            return np.array([])
+        elif len(self.dimensions) == 1:
+            return np.array(self._data.bytes)
+        else:
+            # Convert Fortran shape (data in memory is identical, shape is transposed)
+            word_len = self.dimensions[0]
+            dims = self.dimensions[1:][::-1]  # Identical to: [:0:-1]
+            byte_steps = np.cumprod(self.dimensions[:-1])[::-1]
+            # Generate mult-dimensional array and parse byte words
+            byte_arr = np.empty(dims, dtype=object)
+            for i in np.ndindex(*dims):
+                # Calculate byte offset as sum of each array index times the byte step of each dimension.
+                off = np.sum(np.multiply(i, byte_steps))
+                byte_arr[i] = self._data.bytes[off:off+word_len]
+            return byte_arr
+
+    @property
+    def string_array(self):
+        '''Get the parameter data as a python array of unicode strings.'''
+        # Decode different dimensions
+        if len(self.dimensions) == 0:
+            return np.array([])
+        elif len(self.dimensions) == 1:
+            return np.array([self.string_value])
+        else:
+            # Parse byte sequences
+            byte_arr = self.bytes_array
+            # Decode sequences
+            for i in np.ndindex(byte_arr.shape):
+                byte_arr[i] = self.dtypes.decode_string(byte_arr[i])
+            return byte_arr
+
+    @property
+    def any_value(self):
+        ''' Get the parameter data as a value of 'traditional type'.
+
+        Traditional types are defined in the Parameter section in the [user manual].
+
+        Returns
+        -------
+        value : int, float, or str
+            Depending on the `bytes_per_element` field, a traditional type can
+            be a either a signed byte, signed short, 32-bit float, or a string.
+
+        [user manual]: https://www.c3d.org/docs/C3D_User_Guide.pdf
+        '''
+        if self.bytes_per_element >= 4:
+            return self.float_value
+        elif self.bytes_per_element >= 2:
+            return self.int16_value
+        elif self.bytes_per_element == -1:
+            return self.string_value
+        else:
+            return self.int8_value
+
+    @property
+    def any_array(self):
+        ''' Get the parameter data as an array of 'traditional type'.
+
+        Traditional types are defined in the Parameter section in the [user manual].
+
+        Returns
+        -------
+        value : array
+            Depending on the `bytes_per_element` field, a traditional type can
+            be a either a signed byte, signed short, 32-bit float, or a string.
+
+        [user manual]: https://www.c3d.org/docs/C3D_User_Guide.pdf
+        '''
+        if self.bytes_per_element >= 4:
+            return self.float_array
+        elif self.bytes_per_element >= 2:
+            return self.int16_array
+        elif self.bytes_per_element == -1:
+            return self.string_array
+        else:
+            return self.int8_array
+
+    @property
+    def _as_any_uint(self):
+        ''' Attempt to parse the parameter data as any unsigned integer format.
+            Checks if the integer is stored as a floating point value.
+
+            Can be used to read 'POINT:FRAMES' or 'POINT:LONG_FRAMES'
+            when not accessed through `c3d.manager.Manager.last_frame`.
+        '''
+        if self.bytes_per_element >= 4:
+            # Check if float value representation is an integer
+            value = self.float_value
+            if float(value).is_integer():
+                return int(value)
+            return self.uint32_value
+        elif self.bytes_per_element >= 2:
+            return self.uint16_value
+        else:
+            return self.uint8_value
+
+
+class Param(ParamReadonly):
+    ''' Wrapper exposing both readable and writable attributes of a `c3d.parameter.ParamData` entry.
+    '''
+    def __init__(self, data):
+        super(Param, self).__init__(data)
+
+    def readonly(self):
+        ''' Returns a readonly `c3d.parameter.ParamReadonly` instance. '''
+        return ParamReadonly(self._data)
+
+    @property
+    def bytes(self) -> bytes:
+        ''' Get or set the parameter bytes. '''
+        return self._data.bytes
+
+    @bytes.setter
+    def bytes(self, value):
+        self._data.bytes = value
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Param +(data) +
+
+

Wrapper exposing both readable and writable attributes of a ParamData entry.

+
+ +Expand source code + +
class Param(ParamReadonly):
+    ''' Wrapper exposing both readable and writable attributes of a `c3d.parameter.ParamData` entry.
+    '''
+    def __init__(self, data):
+        super(Param, self).__init__(data)
+
+    def readonly(self):
+        ''' Returns a readonly `c3d.parameter.ParamReadonly` instance. '''
+        return ParamReadonly(self._data)
+
+    @property
+    def bytes(self) -> bytes:
+        ''' Get or set the parameter bytes. '''
+        return self._data.bytes
+
+    @bytes.setter
+    def bytes(self, value):
+        self._data.bytes = value
+
+

Ancestors

+ +

Instance variables

+
+
var bytes : bytes
+
+

Get or set the parameter bytes.

+
+ +Expand source code + +
@property
+def bytes(self) -> bytes:
+    ''' Get or set the parameter bytes. '''
+    return self._data.bytes
+
+
+
+

Methods

+
+
+def readonly(self) +
+
+

Returns a readonly ParamReadonly instance.

+
+ +Expand source code + +
def readonly(self):
+    ''' Returns a readonly `c3d.parameter.ParamReadonly` instance. '''
+    return ParamReadonly(self._data)
+
+
+
+

Inherited members

+ +
+
+class ParamData +(name, dtype, desc='', bytes_per_element=1, dimensions=None, bytes=b'', handle=None) +
+
+

A class representing a single named parameter from a C3D file.

+

Attributes

+
+
name : str
+
Name of this parameter.
+
dtype : DataTypes
+
Reference to the DataTypes object associated with the file.
+
desc : str
+
Brief description of this parameter.
+
bytes_per_element : int, optional
+
For array data, this describes the size of each element of data. For +string data (including arrays of strings), this should be -1.
+
dimensions : list of int
+
For array data, this describes the dimensions of the array, stored in +column-major (Fortran) order. For arrays of strings, the dimensions here will be +the number of columns (length of each string) followed by the number of +rows (number of strings).
+
bytes : str
+
Raw data for this parameter.
+
+

Set up a new parameter, only the name is required.

+
+ +Expand source code + +
class ParamData(object):
+    '''A class representing a single named parameter from a C3D file.
+
+    Attributes
+    ----------
+    name : str
+        Name of this parameter.
+    dtype: DataTypes
+        Reference to the DataTypes object associated with the file.
+    desc : str
+        Brief description of this parameter.
+    bytes_per_element : int, optional
+        For array data, this describes the size of each element of data. For
+        string data (including arrays of strings), this should be -1.
+    dimensions : list of int
+        For array data, this describes the dimensions of the array, stored in
+        column-major (Fortran) order. For arrays of strings, the dimensions here will be
+        the number of columns (length of each string) followed by the number of
+        rows (number of strings).
+    bytes : str
+        Raw data for this parameter.
+    '''
+
+    def __init__(self,
+                 name,
+                 dtype,
+                 desc='',
+                 bytes_per_element=1,
+                 dimensions=None,
+                 bytes=b'',
+                 handle=None):
+        '''Set up a new parameter, only the name is required.'''
+        self.name = name
+        self.dtypes = dtype
+        self.desc = desc
+        self.bytes_per_element = bytes_per_element
+        self.dimensions = dimensions or []
+        self.bytes = bytes
+        if handle:
+            self.read(handle)
+
+    def __repr__(self):
+        return '<Param: {}>'.format(self.desc)
+
+    @property
+    def num_elements(self) -> int:
+        '''Return the number of elements in this parameter's array value.'''
+        e = 1
+        for d in self.dimensions:
+            e *= d
+        return e
+
+    @property
+    def total_bytes(self) -> int:
+        '''Return the number of bytes used for storing this parameter's data.'''
+        return self.num_elements * abs(self.bytes_per_element)
+
+    @property
+    def binary_size(self) -> int:
+        '''Return the number of bytes needed to store this parameter.'''
+        return (
+            1 +  # group_id
+            2 +  # next offset marker
+            1 + len(self.name.encode('utf-8')) +  # size of name and name bytes
+            1 +  # data size
+            # size of dimensions and dimension bytes
+            1 + len(self.dimensions) +
+            self.total_bytes +  # data
+            1 + len(self.desc.encode('utf-8'))  # size of desc and desc bytes
+        )
+
+    def write(self, group_id, handle):
+        '''Write binary data for this parameter to a file handle.
+
+        Parameters
+        ----------
+        group_id : int
+            The numerical ID of the group that holds this parameter.
+        handle : file handle
+            An open, writable, binary file handle.
+        '''
+        name = self.name.encode('utf-8')
+        handle.write(struct.pack('bb', len(name), group_id))
+        handle.write(name)
+        handle.write(struct.pack('<h', self.binary_size - 2 - len(name)))
+        handle.write(struct.pack('b', self.bytes_per_element))
+        handle.write(struct.pack('B', len(self.dimensions)))
+        handle.write(struct.pack('B' * len(self.dimensions), *self.dimensions))
+        if self.bytes is not None and len(self.bytes) > 0:
+            handle.write(self.bytes)
+        desc = self.desc.encode('utf-8')
+        handle.write(struct.pack('B', len(desc)))
+        handle.write(desc)
+
+    def read(self, handle):
+        '''Read binary data for this parameter from a file handle.
+
+        This reads exactly enough data from the current position in the file to
+        initialize the parameter.
+        '''
+        self.bytes_per_element, = struct.unpack('b', handle.read(1))
+        dims, = struct.unpack('B', handle.read(1))
+        self.dimensions = [struct.unpack('B', handle.read(1))[
+            0] for _ in range(dims)]
+        self.bytes = b''
+        if self.total_bytes:
+            self.bytes = handle.read(self.total_bytes)
+        desc_size, = struct.unpack('B', handle.read(1))
+        self.desc = desc_size and self.dtypes.decode_string(handle.read(desc_size)) or ''
+
+    def _as(self, dtype):
+        '''Unpack the raw bytes of this param using the given struct format.'''
+        return np.frombuffer(self.bytes, count=1, dtype=dtype)[0]
+
+    def _as_array(self, dtype, copy=True):
+        '''Unpack the raw bytes of this param using the given data format.'''
+        if not self.dimensions:
+            return [self._as(dtype)]
+        elems = np.frombuffer(self.bytes, dtype=dtype)
+        # Reverse shape as the shape is defined in fortran format
+        view = elems.reshape(self.dimensions[::-1])
+        if copy:
+            return view.copy()
+        return view
+
+

Instance variables

+
+
var binary_size : int
+
+

Return the number of bytes needed to store this parameter.

+
+ +Expand source code + +
@property
+def binary_size(self) -> int:
+    '''Return the number of bytes needed to store this parameter.'''
+    return (
+        1 +  # group_id
+        2 +  # next offset marker
+        1 + len(self.name.encode('utf-8')) +  # size of name and name bytes
+        1 +  # data size
+        # size of dimensions and dimension bytes
+        1 + len(self.dimensions) +
+        self.total_bytes +  # data
+        1 + len(self.desc.encode('utf-8'))  # size of desc and desc bytes
+    )
+
+
+
var num_elements : int
+
+

Return the number of elements in this parameter's array value.

+
+ +Expand source code + +
@property
+def num_elements(self) -> int:
+    '''Return the number of elements in this parameter's array value.'''
+    e = 1
+    for d in self.dimensions:
+        e *= d
+    return e
+
+
+
var total_bytes : int
+
+

Return the number of bytes used for storing this parameter's data.

+
+ +Expand source code + +
@property
+def total_bytes(self) -> int:
+    '''Return the number of bytes used for storing this parameter's data.'''
+    return self.num_elements * abs(self.bytes_per_element)
+
+
+
+

Methods

+
+
+def read(self, handle) +
+
+

Read binary data for this parameter from a file handle.

+

This reads exactly enough data from the current position in the file to +initialize the parameter.

+
+ +Expand source code + +
def read(self, handle):
+    '''Read binary data for this parameter from a file handle.
+
+    This reads exactly enough data from the current position in the file to
+    initialize the parameter.
+    '''
+    self.bytes_per_element, = struct.unpack('b', handle.read(1))
+    dims, = struct.unpack('B', handle.read(1))
+    self.dimensions = [struct.unpack('B', handle.read(1))[
+        0] for _ in range(dims)]
+    self.bytes = b''
+    if self.total_bytes:
+        self.bytes = handle.read(self.total_bytes)
+    desc_size, = struct.unpack('B', handle.read(1))
+    self.desc = desc_size and self.dtypes.decode_string(handle.read(desc_size)) or ''
+
+
+
+def write(self, group_id, handle) +
+
+

Write binary data for this parameter to a file handle.

+

Parameters

+
+
group_id : int
+
The numerical ID of the group that holds this parameter.
+
handle : file handle
+
An open, writable, binary file handle.
+
+
+ +Expand source code + +
def write(self, group_id, handle):
+    '''Write binary data for this parameter to a file handle.
+
+    Parameters
+    ----------
+    group_id : int
+        The numerical ID of the group that holds this parameter.
+    handle : file handle
+        An open, writable, binary file handle.
+    '''
+    name = self.name.encode('utf-8')
+    handle.write(struct.pack('bb', len(name), group_id))
+    handle.write(name)
+    handle.write(struct.pack('<h', self.binary_size - 2 - len(name)))
+    handle.write(struct.pack('b', self.bytes_per_element))
+    handle.write(struct.pack('B', len(self.dimensions)))
+    handle.write(struct.pack('B' * len(self.dimensions), *self.dimensions))
+    if self.bytes is not None and len(self.bytes) > 0:
+        handle.write(self.bytes)
+    desc = self.desc.encode('utf-8')
+    handle.write(struct.pack('B', len(desc)))
+    handle.write(desc)
+
+
+
+
+
+class ParamReadonly +(data) +
+
+

Wrapper exposing readonly attributes of a ParamData entry.

+
+ +Expand source code + +
class ParamReadonly(object):
+    ''' Wrapper exposing readonly attributes of a `c3d.parameter.ParamData` entry.
+    '''
+
+    def __init__(self, data):
+        self._data = data
+
+    def __eq__(self, other):
+        return self._data is other._data
+
+    @property
+    def name(self) -> str:
+        ''' Get the parameter name. '''
+        return self._data.name
+
+    @property
+    def desc(self) -> str:
+        ''' Get the parameter descriptor. '''
+        return self._data.desc
+
+    @property
+    def dtypes(self):
+        ''' Convenience accessor to the `c3d.dtypes.DataTypes` instance associated with the parameter. '''
+        return self._data.dtypes
+
+    @property
+    def dimensions(self) -> (int, ...):
+        ''' Shape of the parameter data (Fortran format). '''
+        return self._data.dimensions
+
+    @property
+    def num_elements(self) -> int:
+        '''Return the number of elements in this parameter's array value.'''
+        return self._data.num_elements
+
+    @property
+    def bytes_per_element(self) -> int:
+        '''Return the number of bytes used to store each data element.'''
+        return self._data.bytes_per_element
+
+    @property
+    def total_bytes(self) -> int:
+        '''Return the number of bytes used for storing this parameter's data.'''
+        return self._data.total_bytes
+
+    @property
+    def binary_size(self) -> int:
+        '''Return the number of bytes needed to store this parameter.'''
+        return self._data.binary_size
+
+    @property
+    def int8_value(self):
+        '''Get the parameter data as an 8-bit signed integer.'''
+        return self._data._as(self.dtypes.int8)
+
+    @property
+    def uint8_value(self):
+        '''Get the parameter data as an 8-bit unsigned integer.'''
+        return self._data._as(self.dtypes.uint8)
+
+    @property
+    def int16_value(self):
+        '''Get the parameter data as a 16-bit signed integer.'''
+        return self._data._as(self.dtypes.int16)
+
+    @property
+    def uint16_value(self):
+        '''Get the parameter data as a 16-bit unsigned integer.'''
+        return self._data._as(self.dtypes.uint16)
+
+    @property
+    def int32_value(self):
+        '''Get the parameter data as a 32-bit signed integer.'''
+        return self._data._as(self.dtypes.int32)
+
+    @property
+    def uint32_value(self):
+        '''Get the parameter data as a 32-bit unsigned integer.'''
+        return self._data._as(self.dtypes.uint32)
+
+    @property
+    def uint_value(self):
+        ''' Get the parameter data as a unsigned integer of appropriate type. '''
+        if self.bytes_per_element >= 4:
+            return self.uint32_value
+        elif self.bytes_per_element >= 2:
+            return self.uint16_value
+        else:
+            return self.uint8_value
+
+    @property
+    def int_value(self):
+        ''' Get the parameter data as a signed integer of appropriate type. '''
+        if self.bytes_per_element >= 4:
+            return self.int32_value
+        elif self.bytes_per_element >= 2:
+            return self.int16_value
+        else:
+            return self.int8_value
+
+    @property
+    def float_value(self):
+        '''Get the parameter data as a floating point value of appropriate type.'''
+        if self.bytes_per_element > 4:
+            if self.dtypes.is_dec:
+                raise AttributeError("64 bit DEC floating point is not supported.")
+            # 64-bit floating point is not a standard
+            return self._data._as(self.dtypes.float64)
+        elif self.bytes_per_element == 4:
+            if self.dtypes.is_dec:
+                return DEC_to_IEEE(self._data._as(np.uint32))
+            else:  # is_mips or is_ieee
+                return self._data._as(self.dtypes.float32)
+        else:
+            raise AttributeError("Only 32 and 64 bit floating point is supported.")
+
+    @property
+    def bytes_value(self) -> bytes:
+        '''Get the raw byte string.'''
+        return self._data.bytes
+
+    @property
+    def string_value(self):
+        '''Get the parameter data as a unicode string.'''
+        return self.dtypes.decode_string(self._data.bytes)
+
+    @property
+    def int8_array(self):
+        '''Get the parameter data as an array of 8-bit signed integers.'''
+        return self._data._as_array(self.dtypes.int8)
+
+    @property
+    def uint8_array(self):
+        '''Get the parameter data as an array of 8-bit unsigned integers.'''
+        return self._data._as_array(self.dtypes.uint8)
+
+    @property
+    def int16_array(self):
+        '''Get the parameter data as an array of 16-bit signed integers.'''
+        return self._data._as_array(self.dtypes.int16)
+
+    @property
+    def uint16_array(self):
+        '''Get the parameter data as an array of 16-bit unsigned integers.'''
+        return self._data._as_array(self.dtypes.uint16)
+
+    @property
+    def int32_array(self):
+        '''Get the parameter data as an array of 32-bit signed integers.'''
+        return self._data._as_array(self.dtypes.int32)
+
+    @property
+    def uint32_array(self):
+        '''Get the parameter data as an array of 32-bit unsigned integers.'''
+        return self._data._as_array(self.dtypes.uint32)
+
+    @property
+    def int64_array(self):
+        '''Get the parameter data as an array of 32-bit signed integers.'''
+        return self._data._as_array(self.dtypes.int64)
+
+    @property
+    def uint64_array(self):
+        '''Get the parameter data as an array of 32-bit unsigned integers.'''
+        return self._data._as_array(self.dtypes.uint64)
+
+    @property
+    def float32_array(self):
+        '''Get the parameter data as an array of 32-bit floats.'''
+        # Convert float data if not IEEE processor
+        if self.dtypes.is_dec:
+            # _as_array but for DEC
+            if not self.dimensions:
+                return [self.float_value]
+            return DEC_to_IEEE_BYTES(self._data.bytes).reshape(self.dimensions[::-1])  # Reverse fortran format
+        else:  # is_ieee or is_mips
+            return self._data._as_array(self.dtypes.float32)
+
+    @property
+    def float64_array(self):
+        '''Get the parameter data as an array of 64-bit floats.'''
+        # Convert float data if not IEEE processor
+        if self.dtypes.is_dec:
+            raise ValueError('Unable to convert bytes encoded in a 64 bit floating point DEC format.')
+        else:  # is_ieee or is_mips
+            return self._data._as_array(self.dtypes.float64)
+
+    @property
+    def float_array(self):
+        '''Get the parameter data as an array of 32 or 64 bit floats.'''
+        # Convert float data if not IEEE processor
+        if self.bytes_per_element == 4:
+            return self.float32_array
+        elif self.bytes_per_element == 8:
+            return self.float64_array
+        else:
+            raise TypeError("Parsing parameter bytes to an array with %i bit " % self.bytes_per_element +
+                            "floating-point precission is not unsupported.")
+
+    @property
+    def int_array(self):
+        '''Get the parameter data as an array of integer values.'''
+        # Convert float data if not IEEE processor
+        if self.bytes_per_element == 1:
+            return self.int8_array
+        elif self.bytes_per_element == 2:
+            return self.int16_array
+        elif self.bytes_per_element == 4:
+            return self.int32_array
+        elif self.bytes_per_element == 8:
+            return self.int64_array
+        else:
+            raise TypeError("Parsing parameter bytes to an array with %i bit integer values is not unsupported." %
+                            self.bytes_per_element)
+
+    @property
+    def uint_array(self):
+        '''Get the parameter data as an array of integer values.'''
+        # Convert float data if not IEEE processor
+        if self.bytes_per_element == 1:
+            return self.uint8_array
+        elif self.bytes_per_element == 2:
+            return self.uint16_array
+        elif self.bytes_per_element == 4:
+            return self.uint32_array
+        elif self.bytes_per_element == 8:
+            return self.uint64_array
+        else:
+            raise TypeError("Parsing parameter bytes to an array with %i bit integer values is not unsupported." %
+                            self.bytes_per_element)
+
+    @property
+    def bytes_array(self):
+        '''Get the parameter data as an array of raw byte strings.'''
+        # Decode different dimensions
+        if len(self.dimensions) == 0:
+            return np.array([])
+        elif len(self.dimensions) == 1:
+            return np.array(self._data.bytes)
+        else:
+            # Convert Fortran shape (data in memory is identical, shape is transposed)
+            word_len = self.dimensions[0]
+            dims = self.dimensions[1:][::-1]  # Identical to: [:0:-1]
+            byte_steps = np.cumprod(self.dimensions[:-1])[::-1]
+            # Generate mult-dimensional array and parse byte words
+            byte_arr = np.empty(dims, dtype=object)
+            for i in np.ndindex(*dims):
+                # Calculate byte offset as sum of each array index times the byte step of each dimension.
+                off = np.sum(np.multiply(i, byte_steps))
+                byte_arr[i] = self._data.bytes[off:off+word_len]
+            return byte_arr
+
+    @property
+    def string_array(self):
+        '''Get the parameter data as a python array of unicode strings.'''
+        # Decode different dimensions
+        if len(self.dimensions) == 0:
+            return np.array([])
+        elif len(self.dimensions) == 1:
+            return np.array([self.string_value])
+        else:
+            # Parse byte sequences
+            byte_arr = self.bytes_array
+            # Decode sequences
+            for i in np.ndindex(byte_arr.shape):
+                byte_arr[i] = self.dtypes.decode_string(byte_arr[i])
+            return byte_arr
+
+    @property
+    def any_value(self):
+        ''' Get the parameter data as a value of 'traditional type'.
+
+        Traditional types are defined in the Parameter section in the [user manual].
+
+        Returns
+        -------
+        value : int, float, or str
+            Depending on the `bytes_per_element` field, a traditional type can
+            be a either a signed byte, signed short, 32-bit float, or a string.
+
+        [user manual]: https://www.c3d.org/docs/C3D_User_Guide.pdf
+        '''
+        if self.bytes_per_element >= 4:
+            return self.float_value
+        elif self.bytes_per_element >= 2:
+            return self.int16_value
+        elif self.bytes_per_element == -1:
+            return self.string_value
+        else:
+            return self.int8_value
+
+    @property
+    def any_array(self):
+        ''' Get the parameter data as an array of 'traditional type'.
+
+        Traditional types are defined in the Parameter section in the [user manual].
+
+        Returns
+        -------
+        value : array
+            Depending on the `bytes_per_element` field, a traditional type can
+            be a either a signed byte, signed short, 32-bit float, or a string.
+
+        [user manual]: https://www.c3d.org/docs/C3D_User_Guide.pdf
+        '''
+        if self.bytes_per_element >= 4:
+            return self.float_array
+        elif self.bytes_per_element >= 2:
+            return self.int16_array
+        elif self.bytes_per_element == -1:
+            return self.string_array
+        else:
+            return self.int8_array
+
+    @property
+    def _as_any_uint(self):
+        ''' Attempt to parse the parameter data as any unsigned integer format.
+            Checks if the integer is stored as a floating point value.
+
+            Can be used to read 'POINT:FRAMES' or 'POINT:LONG_FRAMES'
+            when not accessed through `c3d.manager.Manager.last_frame`.
+        '''
+        if self.bytes_per_element >= 4:
+            # Check if float value representation is an integer
+            value = self.float_value
+            if float(value).is_integer():
+                return int(value)
+            return self.uint32_value
+        elif self.bytes_per_element >= 2:
+            return self.uint16_value
+        else:
+            return self.uint8_value
+
+

Subclasses

+ +

Instance variables

+
+
var any_array
+
+

Get the parameter data as an array of 'traditional type'.

+

Traditional types are defined in the Parameter section in the user manual.

+

Returns

+
+
value : array
+
Depending on the bytes_per_element field, a traditional type can +be a either a signed byte, signed short, 32-bit float, or a string.
+
+
+ +Expand source code + +
@property
+def any_array(self):
+    ''' Get the parameter data as an array of 'traditional type'.
+
+    Traditional types are defined in the Parameter section in the [user manual].
+
+    Returns
+    -------
+    value : array
+        Depending on the `bytes_per_element` field, a traditional type can
+        be a either a signed byte, signed short, 32-bit float, or a string.
+
+    [user manual]: https://www.c3d.org/docs/C3D_User_Guide.pdf
+    '''
+    if self.bytes_per_element >= 4:
+        return self.float_array
+    elif self.bytes_per_element >= 2:
+        return self.int16_array
+    elif self.bytes_per_element == -1:
+        return self.string_array
+    else:
+        return self.int8_array
+
+
+
var any_value
+
+

Get the parameter data as a value of 'traditional type'.

+

Traditional types are defined in the Parameter section in the user manual.

+

Returns

+
+
value : int, float, or str
+
Depending on the bytes_per_element field, a traditional type can +be a either a signed byte, signed short, 32-bit float, or a string.
+
+
+ +Expand source code + +
@property
+def any_value(self):
+    ''' Get the parameter data as a value of 'traditional type'.
+
+    Traditional types are defined in the Parameter section in the [user manual].
+
+    Returns
+    -------
+    value : int, float, or str
+        Depending on the `bytes_per_element` field, a traditional type can
+        be a either a signed byte, signed short, 32-bit float, or a string.
+
+    [user manual]: https://www.c3d.org/docs/C3D_User_Guide.pdf
+    '''
+    if self.bytes_per_element >= 4:
+        return self.float_value
+    elif self.bytes_per_element >= 2:
+        return self.int16_value
+    elif self.bytes_per_element == -1:
+        return self.string_value
+    else:
+        return self.int8_value
+
+
+
var binary_size : int
+
+

Return the number of bytes needed to store this parameter.

+
+ +Expand source code + +
@property
+def binary_size(self) -> int:
+    '''Return the number of bytes needed to store this parameter.'''
+    return self._data.binary_size
+
+
+
var bytes_array
+
+

Get the parameter data as an array of raw byte strings.

+
+ +Expand source code + +
@property
+def bytes_array(self):
+    '''Get the parameter data as an array of raw byte strings.'''
+    # Decode different dimensions
+    if len(self.dimensions) == 0:
+        return np.array([])
+    elif len(self.dimensions) == 1:
+        return np.array(self._data.bytes)
+    else:
+        # Convert Fortran shape (data in memory is identical, shape is transposed)
+        word_len = self.dimensions[0]
+        dims = self.dimensions[1:][::-1]  # Identical to: [:0:-1]
+        byte_steps = np.cumprod(self.dimensions[:-1])[::-1]
+        # Generate mult-dimensional array and parse byte words
+        byte_arr = np.empty(dims, dtype=object)
+        for i in np.ndindex(*dims):
+            # Calculate byte offset as sum of each array index times the byte step of each dimension.
+            off = np.sum(np.multiply(i, byte_steps))
+            byte_arr[i] = self._data.bytes[off:off+word_len]
+        return byte_arr
+
+
+
var bytes_per_element : int
+
+

Return the number of bytes used to store each data element.

+
+ +Expand source code + +
@property
+def bytes_per_element(self) -> int:
+    '''Return the number of bytes used to store each data element.'''
+    return self._data.bytes_per_element
+
+
+
var bytes_value : bytes
+
+

Get the raw byte string.

+
+ +Expand source code + +
@property
+def bytes_value(self) -> bytes:
+    '''Get the raw byte string.'''
+    return self._data.bytes
+
+
+
var desc : str
+
+

Get the parameter descriptor.

+
+ +Expand source code + +
@property
+def desc(self) -> str:
+    ''' Get the parameter descriptor. '''
+    return self._data.desc
+
+
+
var dimensions : (, Ellipsis)
+
+

Shape of the parameter data (Fortran format).

+
+ +Expand source code + +
@property
+def dimensions(self) -> (int, ...):
+    ''' Shape of the parameter data (Fortran format). '''
+    return self._data.dimensions
+
+
+
var dtypes
+
+

Convenience accessor to the DataTypes instance associated with the parameter.

+
+ +Expand source code + +
@property
+def dtypes(self):
+    ''' Convenience accessor to the `c3d.dtypes.DataTypes` instance associated with the parameter. '''
+    return self._data.dtypes
+
+
+
var float32_array
+
+

Get the parameter data as an array of 32-bit floats.

+
+ +Expand source code + +
@property
+def float32_array(self):
+    '''Get the parameter data as an array of 32-bit floats.'''
+    # Convert float data if not IEEE processor
+    if self.dtypes.is_dec:
+        # _as_array but for DEC
+        if not self.dimensions:
+            return [self.float_value]
+        return DEC_to_IEEE_BYTES(self._data.bytes).reshape(self.dimensions[::-1])  # Reverse fortran format
+    else:  # is_ieee or is_mips
+        return self._data._as_array(self.dtypes.float32)
+
+
+
var float64_array
+
+

Get the parameter data as an array of 64-bit floats.

+
+ +Expand source code + +
@property
+def float64_array(self):
+    '''Get the parameter data as an array of 64-bit floats.'''
+    # Convert float data if not IEEE processor
+    if self.dtypes.is_dec:
+        raise ValueError('Unable to convert bytes encoded in a 64 bit floating point DEC format.')
+    else:  # is_ieee or is_mips
+        return self._data._as_array(self.dtypes.float64)
+
+
+
var float_array
+
+

Get the parameter data as an array of 32 or 64 bit floats.

+
+ +Expand source code + +
@property
+def float_array(self):
+    '''Get the parameter data as an array of 32 or 64 bit floats.'''
+    # Convert float data if not IEEE processor
+    if self.bytes_per_element == 4:
+        return self.float32_array
+    elif self.bytes_per_element == 8:
+        return self.float64_array
+    else:
+        raise TypeError("Parsing parameter bytes to an array with %i bit " % self.bytes_per_element +
+                        "floating-point precission is not unsupported.")
+
+
+
var float_value
+
+

Get the parameter data as a floating point value of appropriate type.

+
+ +Expand source code + +
@property
+def float_value(self):
+    '''Get the parameter data as a floating point value of appropriate type.'''
+    if self.bytes_per_element > 4:
+        if self.dtypes.is_dec:
+            raise AttributeError("64 bit DEC floating point is not supported.")
+        # 64-bit floating point is not a standard
+        return self._data._as(self.dtypes.float64)
+    elif self.bytes_per_element == 4:
+        if self.dtypes.is_dec:
+            return DEC_to_IEEE(self._data._as(np.uint32))
+        else:  # is_mips or is_ieee
+            return self._data._as(self.dtypes.float32)
+    else:
+        raise AttributeError("Only 32 and 64 bit floating point is supported.")
+
+
+
var int16_array
+
+

Get the parameter data as an array of 16-bit signed integers.

+
+ +Expand source code + +
@property
+def int16_array(self):
+    '''Get the parameter data as an array of 16-bit signed integers.'''
+    return self._data._as_array(self.dtypes.int16)
+
+
+
var int16_value
+
+

Get the parameter data as a 16-bit signed integer.

+
+ +Expand source code + +
@property
+def int16_value(self):
+    '''Get the parameter data as a 16-bit signed integer.'''
+    return self._data._as(self.dtypes.int16)
+
+
+
var int32_array
+
+

Get the parameter data as an array of 32-bit signed integers.

+
+ +Expand source code + +
@property
+def int32_array(self):
+    '''Get the parameter data as an array of 32-bit signed integers.'''
+    return self._data._as_array(self.dtypes.int32)
+
+
+
var int32_value
+
+

Get the parameter data as a 32-bit signed integer.

+
+ +Expand source code + +
@property
+def int32_value(self):
+    '''Get the parameter data as a 32-bit signed integer.'''
+    return self._data._as(self.dtypes.int32)
+
+
+
var int64_array
+
+

Get the parameter data as an array of 32-bit signed integers.

+
+ +Expand source code + +
@property
+def int64_array(self):
+    '''Get the parameter data as an array of 32-bit signed integers.'''
+    return self._data._as_array(self.dtypes.int64)
+
+
+
var int8_array
+
+

Get the parameter data as an array of 8-bit signed integers.

+
+ +Expand source code + +
@property
+def int8_array(self):
+    '''Get the parameter data as an array of 8-bit signed integers.'''
+    return self._data._as_array(self.dtypes.int8)
+
+
+
var int8_value
+
+

Get the parameter data as an 8-bit signed integer.

+
+ +Expand source code + +
@property
+def int8_value(self):
+    '''Get the parameter data as an 8-bit signed integer.'''
+    return self._data._as(self.dtypes.int8)
+
+
+
var int_array
+
+

Get the parameter data as an array of integer values.

+
+ +Expand source code + +
@property
+def int_array(self):
+    '''Get the parameter data as an array of integer values.'''
+    # Convert float data if not IEEE processor
+    if self.bytes_per_element == 1:
+        return self.int8_array
+    elif self.bytes_per_element == 2:
+        return self.int16_array
+    elif self.bytes_per_element == 4:
+        return self.int32_array
+    elif self.bytes_per_element == 8:
+        return self.int64_array
+    else:
+        raise TypeError("Parsing parameter bytes to an array with %i bit integer values is not unsupported." %
+                        self.bytes_per_element)
+
+
+
var int_value
+
+

Get the parameter data as a signed integer of appropriate type.

+
+ +Expand source code + +
@property
+def int_value(self):
+    ''' Get the parameter data as a signed integer of appropriate type. '''
+    if self.bytes_per_element >= 4:
+        return self.int32_value
+    elif self.bytes_per_element >= 2:
+        return self.int16_value
+    else:
+        return self.int8_value
+
+
+
var name : str
+
+

Get the parameter name.

+
+ +Expand source code + +
@property
+def name(self) -> str:
+    ''' Get the parameter name. '''
+    return self._data.name
+
+
+
var num_elements : int
+
+

Return the number of elements in this parameter's array value.

+
+ +Expand source code + +
@property
+def num_elements(self) -> int:
+    '''Return the number of elements in this parameter's array value.'''
+    return self._data.num_elements
+
+
+
var string_array
+
+

Get the parameter data as a python array of unicode strings.

+
+ +Expand source code + +
@property
+def string_array(self):
+    '''Get the parameter data as a python array of unicode strings.'''
+    # Decode different dimensions
+    if len(self.dimensions) == 0:
+        return np.array([])
+    elif len(self.dimensions) == 1:
+        return np.array([self.string_value])
+    else:
+        # Parse byte sequences
+        byte_arr = self.bytes_array
+        # Decode sequences
+        for i in np.ndindex(byte_arr.shape):
+            byte_arr[i] = self.dtypes.decode_string(byte_arr[i])
+        return byte_arr
+
+
+
var string_value
+
+

Get the parameter data as a unicode string.

+
+ +Expand source code + +
@property
+def string_value(self):
+    '''Get the parameter data as a unicode string.'''
+    return self.dtypes.decode_string(self._data.bytes)
+
+
+
var total_bytes : int
+
+

Return the number of bytes used for storing this parameter's data.

+
+ +Expand source code + +
@property
+def total_bytes(self) -> int:
+    '''Return the number of bytes used for storing this parameter's data.'''
+    return self._data.total_bytes
+
+
+
var uint16_array
+
+

Get the parameter data as an array of 16-bit unsigned integers.

+
+ +Expand source code + +
@property
+def uint16_array(self):
+    '''Get the parameter data as an array of 16-bit unsigned integers.'''
+    return self._data._as_array(self.dtypes.uint16)
+
+
+
var uint16_value
+
+

Get the parameter data as a 16-bit unsigned integer.

+
+ +Expand source code + +
@property
+def uint16_value(self):
+    '''Get the parameter data as a 16-bit unsigned integer.'''
+    return self._data._as(self.dtypes.uint16)
+
+
+
var uint32_array
+
+

Get the parameter data as an array of 32-bit unsigned integers.

+
+ +Expand source code + +
@property
+def uint32_array(self):
+    '''Get the parameter data as an array of 32-bit unsigned integers.'''
+    return self._data._as_array(self.dtypes.uint32)
+
+
+
var uint32_value
+
+

Get the parameter data as a 32-bit unsigned integer.

+
+ +Expand source code + +
@property
+def uint32_value(self):
+    '''Get the parameter data as a 32-bit unsigned integer.'''
+    return self._data._as(self.dtypes.uint32)
+
+
+
var uint64_array
+
+

Get the parameter data as an array of 32-bit unsigned integers.

+
+ +Expand source code + +
@property
+def uint64_array(self):
+    '''Get the parameter data as an array of 32-bit unsigned integers.'''
+    return self._data._as_array(self.dtypes.uint64)
+
+
+
var uint8_array
+
+

Get the parameter data as an array of 8-bit unsigned integers.

+
+ +Expand source code + +
@property
+def uint8_array(self):
+    '''Get the parameter data as an array of 8-bit unsigned integers.'''
+    return self._data._as_array(self.dtypes.uint8)
+
+
+
var uint8_value
+
+

Get the parameter data as an 8-bit unsigned integer.

+
+ +Expand source code + +
@property
+def uint8_value(self):
+    '''Get the parameter data as an 8-bit unsigned integer.'''
+    return self._data._as(self.dtypes.uint8)
+
+
+
var uint_array
+
+

Get the parameter data as an array of integer values.

+
+ +Expand source code + +
@property
+def uint_array(self):
+    '''Get the parameter data as an array of integer values.'''
+    # Convert float data if not IEEE processor
+    if self.bytes_per_element == 1:
+        return self.uint8_array
+    elif self.bytes_per_element == 2:
+        return self.uint16_array
+    elif self.bytes_per_element == 4:
+        return self.uint32_array
+    elif self.bytes_per_element == 8:
+        return self.uint64_array
+    else:
+        raise TypeError("Parsing parameter bytes to an array with %i bit integer values is not unsupported." %
+                        self.bytes_per_element)
+
+
+
var uint_value
+
+

Get the parameter data as a unsigned integer of appropriate type.

+
+ +Expand source code + +
@property
+def uint_value(self):
+    ''' Get the parameter data as a unsigned integer of appropriate type. '''
+    if self.bytes_per_element >= 4:
+        return self.uint32_value
+    elif self.bytes_per_element >= 2:
+        return self.uint16_value
+    else:
+        return self.uint8_value
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/c3d/reader.html b/docs/c3d/reader.html new file mode 100644 index 0000000..359809b --- /dev/null +++ b/docs/c3d/reader.html @@ -0,0 +1,1162 @@ + + + + + + +c3d.reader API documentation + + + + + + + + + + + + +
+
+
+

Module c3d.reader

+
+
+

Contains the Reader class for reading C3D files.

+
+ +Expand source code + +
'''Contains the Reader class for reading C3D files.'''
+
+import io
+import numpy as np
+import struct
+import warnings
+from .manager import Manager
+from .header import Header
+from .dtypes import DataTypes
+from .utils import DEC_to_IEEE_BYTES
+
+
+class Reader(Manager):
+    '''This class provides methods for reading the data in a C3D file.
+
+    A C3D file contains metadata and frame-based data describing 3D motion.
+
+    You can iterate over the frames in the file by calling `read_frames()` after
+    construction:
+
+    >>> r = c3d.Reader(open('capture.c3d', 'rb'))
+    >>> for frame_no, points, analog in r.read_frames():
+    ...     print('{0.shape} points in this frame'.format(points))
+    '''
+
+    def __init__(self, handle):
+        '''Initialize this C3D file by reading header and parameter data.
+
+        Parameters
+        ----------
+        handle : file handle
+            Read metadata and C3D motion frames from the given file handle. This
+            handle is assumed to be `seek`-able and `read`-able. The handle must
+            remain open for the life of the `Reader` instance. The `Reader` does
+            not `close` the handle.
+
+        Raises
+        ------
+        AssertionError
+            If the metadata in the C3D file is inconsistent.
+        '''
+        super(Reader, self).__init__(Header(handle))
+
+        self._handle = handle
+
+        def seek_param_section_header():
+            ''' Seek to and read the first 4 byte of the parameter header section '''
+            self._handle.seek((self._header.parameter_block - 1) * 512)
+            # metadata header
+            return self._handle.read(4)
+
+        # Begin by reading the processor type:
+        buf = seek_param_section_header()
+        _, _, parameter_blocks, processor = struct.unpack('BBBB', buf)
+        self._dtypes = DataTypes(processor)
+        # Convert header parameters in accordance with the processor type (MIPS format re-reads the header)
+        self._header._processor_convert(self._dtypes, handle)
+
+        # Restart reading the parameter header after parsing processor type
+        buf = seek_param_section_header()
+
+        start_byte = self._handle.tell()
+        endbyte = start_byte + 512 * parameter_blocks - 4
+        while self._handle.tell() < endbyte:
+            chars_in_name, group_id = struct.unpack('bb', self._handle.read(2))
+            if group_id == 0 or chars_in_name == 0:
+                # we've reached the end of the parameter section.
+                break
+            name = self._dtypes.decode_string(self._handle.read(abs(chars_in_name))).upper()
+
+            # Read the byte segment associated with the parameter and create a
+            # separate binary stream object from the data.
+            offset_to_next, = struct.unpack(['<h', '>h'][self._dtypes.is_mips], self._handle.read(2))
+            if offset_to_next == 0:
+                # Last parameter, as number of bytes are unknown,
+                # read the remaining bytes in the parameter section.
+                bytes = self._handle.read(endbyte - self._handle.tell())
+            else:
+                bytes = self._handle.read(offset_to_next - 2)
+            buf = io.BytesIO(bytes)
+
+            if group_id > 0:
+                # We've just started reading a parameter. If its group doesn't
+                # exist, create a blank one. add the parameter to the group.
+                group = super(Reader, self).get(group_id)
+                if group is None:
+                    group = self._add_group(group_id)
+                group.add_param(name, handle=buf)
+            else:
+                # We've just started reading a group. If a group with the
+                # appropriate numerical id exists already (because we've
+                # already created it for a parameter), just set the name of
+                # the group. Otherwise, add a new group.
+                group_id = abs(group_id)
+                size, = struct.unpack('B', buf.read(1))
+                desc = size and buf.read(size) or ''
+                group = super(Reader, self).get(group_id)
+                if group is not None:
+                    self._rename_group(group, name)  # Inserts name key
+                    group.desc = desc
+                else:
+                    self._add_group(group_id, name, desc)
+
+        self._check_metadata()
+
+    def read_frames(self, copy=True, analog_transform=True, check_nan=True, camera_sum=False):
+        '''Iterate over the data frames from our C3D file handle.
+
+        Parameters
+        ----------
+        copy : bool
+            If False, the reader returns a reference to the same data buffers
+            for every frame. The default is True, which causes the reader to
+            return a unique data buffer for each frame. Set this to False if you
+            consume frames as you iterate over them, or True if you store them
+            for later.
+        analog_transform : bool, default=True
+            If True, ANALOG:SCALE, ANALOG:GEN_SCALE, and ANALOG:OFFSET transforms
+            available in the file are applied to the analog channels.
+        check_nan : bool, default=True
+            If True, point x,y,z coordinates with nan values will be marked invalidated
+            and residuals will be set to -1.
+        camera_sum : bool, default=False
+            Camera flag bits will be summed, converting the fifth column to a camera visibility counter.
+
+        Returns
+        -------
+        frames : sequence of (frame number, points, analog)
+            This method generates a sequence of (frame number, points, analog)
+            tuples, one tuple per frame. The first element of each tuple is the
+            frame number. The second is a numpy array of parsed, 5D point data
+            and the third element of each tuple is a numpy array of analog
+            values that were recorded during the frame. (Often the analog data
+            are sampled at a higher frequency than the 3D point data, resulting
+            in multiple analog frames per frame of point data.)
+
+            The first three columns in the returned point data are the (x, y, z)
+            coordinates of the observed motion capture point. The fourth column
+            is an estimate of the error for this particular point, and the fifth
+            column is the number of cameras that observed the point in question.
+            Both the fourth and fifth values are -1 if the point is considered
+            to be invalid.
+        '''
+        # Point magnitude scalar, if scale parameter is < 0 data is floating point
+        # (in which case the magnitude is the absolute value)
+        scale_mag = abs(self.point_scale)
+        is_float = self.point_scale < 0
+
+        if is_float:
+            point_word_bytes = 4
+        else:
+            point_word_bytes = 2
+        points = np.zeros((self.point_used, 5), np.float32)
+
+        # TODO: handle ANALOG:BITS parameter here!
+        p = self.get('ANALOG:FORMAT')
+        analog_unsigned = p and p.string_value.strip().upper() == 'UNSIGNED'
+        if is_float:
+            analog_dtype = self._dtypes.float32
+            analog_word_bytes = 4
+        elif analog_unsigned:
+            # Note*: Floating point is 'always' defined for both analog and point data, according to the standard.
+            analog_dtype = self._dtypes.uint16
+            analog_word_bytes = 2
+            # Verify BITS parameter for analog
+            p = self.get('ANALOG:BITS')
+            if p and p._as_integer_value / 8 != analog_word_bytes:
+                raise NotImplementedError('Analog data using {} bits is not supported.'.format(p._as_integer_value))
+        else:
+            analog_dtype = self._dtypes.int16
+            analog_word_bytes = 2
+
+        analog = np.array([], float)
+        analog_scales, analog_offsets = self.get_analog_transform()
+
+        # Seek to the start point of the data blocks
+        self._handle.seek((self._header.data_block - 1) * 512)
+        # Number of values (words) read in regard to POINT/ANALOG data
+        N_point = 4 * self.point_used
+        N_analog = self.analog_used * self.analog_per_frame
+
+        # Total bytes per frame
+        point_bytes = N_point * point_word_bytes
+        analog_bytes = N_analog * analog_word_bytes
+        # Parse the data blocks
+        for frame_no in range(self.first_frame, self.last_frame + 1):
+            # Read the byte data (used) for the block
+            raw_bytes = self._handle.read(N_point * point_word_bytes)
+            raw_analog = self._handle.read(N_analog * analog_word_bytes)
+            # Verify read pointers (any of the two can be assumed to be 0)
+            if len(raw_bytes) < point_bytes:
+                warnings.warn('''reached end of file (EOF) while reading POINT data at frame index {}
+                                 and file pointer {}!'''.format(frame_no - self.first_frame, self._handle.tell()))
+                return
+            if len(raw_analog) < analog_bytes:
+                warnings.warn('''reached end of file (EOF) while reading POINT data at frame index {}
+                                 and file pointer {}!'''.format(frame_no - self.first_frame, self._handle.tell()))
+                return
+
+            if is_float:
+                # Convert every 4 byte words to a float-32 reprensentation
+                # (the fourth column is still not a float32 representation)
+                if self._dtypes.is_dec:
+                    # Convert each of the first 6 16-bit words from DEC to IEEE float
+                    points[:, :4] = DEC_to_IEEE_BYTES(raw_bytes).reshape((self.point_used, 4))
+                else:  # If IEEE or MIPS:
+                    # Convert each of the first 6 16-bit words to native float
+                    points[:, :4] = np.frombuffer(raw_bytes,
+                                                  dtype=self._dtypes.float32,
+                                                  count=N_point).reshape((self.point_used, 4))
+
+                # Cast last word to signed integer in system endian format
+                last_word = points[:, 3].astype(np.int32)
+
+            else:
+                # View the bytes as signed 16-bit integers
+                raw = np.frombuffer(raw_bytes,
+                                    dtype=self._dtypes.int16,
+                                    count=N_point).reshape((self.point_used, 4))
+                # Read the first six 16-bit words as x, y, z coordinates
+                points[:, :3] = raw[:, :3] * scale_mag
+                # Cast last word to signed integer in system endian format
+                last_word = raw[:, 3].astype(np.int16)
+
+            # Parse camera-observed bits and residuals.
+            # Notes:
+            # - Invalid sample if residual is equal to -1 (check if word < 0).
+            # - A residual of 0.0 represent modeled data (filtered or interpolated).
+            # - Camera and residual words are always 8-bit (1 byte), never 16-bit.
+            # - If floating point, the byte words are encoded in an integer cast to a float,
+            #    and are written directly in byte form (see the MLS guide).
+            ##
+            # Read the residual and camera byte words (Note* if 32 bit word negative sign is discarded).
+            residual_byte, camera_byte = (last_word & 0x00ff), (last_word & 0x7f00) >> 8
+
+            # Fourth value is floating-point (scaled) error estimate (residual)
+            points[:, 3] = residual_byte * scale_mag
+
+            # Determine invalid samples
+            invalid = last_word < 0
+            if check_nan:
+                is_nan = ~np.all(np.isfinite(points[:, :4]), axis=1)
+                points[is_nan, :3] = 0.0
+                invalid |= is_nan
+            # Update discarded - sign
+            points[invalid, 3] = -1
+
+            # Fifth value is the camera-observation byte
+            if camera_sum:
+                # Convert to observation sum
+                points[:, 4] = sum((camera_byte & (1 << k)) >> k for k in range(7))
+            else:
+                points[:, 4] = camera_byte  # .astype(np.float32)
+
+            # Check if analog data exist, and parse if so
+            if N_analog > 0:
+                if is_float and self._dtypes.is_dec:
+                    # Convert each of the 16-bit words from DEC to IEEE float
+                    analog = DEC_to_IEEE_BYTES(raw_analog)
+                else:
+                    # Integer or INTEL/MIPS floating point data can be parsed directly
+                    analog = np.frombuffer(raw_analog, dtype=analog_dtype, count=N_analog)
+
+                # Reformat and convert
+                analog = analog.reshape((-1, self.analog_used)).T
+                analog = analog.astype(float)
+                # Convert analog
+                analog = (analog - analog_offsets) * analog_scales
+
+            # Output buffers
+            if copy:
+                yield frame_no, points.copy(), analog  # .copy(), a new array is generated per frame for analog data.
+            else:
+                yield frame_no, points, analog
+
+        # Function evaluating EOF, note that data section is written in blocks of 512
+        final_byte_index = self._handle.tell()
+        self._handle.seek(0, 2)  # os.SEEK_END)
+        # Check if more then 1 block remain
+        if self._handle.tell() - final_byte_index >= 512:
+            warnings.warn('incomplete reading of data blocks. {} bytes remained after all datablocks were read!'.format(
+                self._handle.tell() - final_byte_index))
+
+    @property
+    def proc_type(self) -> int:
+        '''Get the processory type associated with the data format in the file.
+        '''
+        return self._dtypes.proc_type
+
+    def to_writer(self, conversion=None):
+        ''' Converts the reader to a `c3d.writer.Writer` instance using the conversion mode.
+
+        See `c3d.writer.Writer.from_reader()` for supported conversion modes.
+        '''
+        from .writer import Writer
+        return Writer.from_reader(self, conversion=conversion)
+
+    def get(self, key, default=None):
+        '''Get a readonly group or parameter.
+
+        Parameters
+        ----------
+        key : str
+            If this string contains a period (.), then the part before the
+            period will be used to retrieve a group, and the part after the
+            period will be used to retrieve a parameter from that group. If this
+            string does not contain a period, then just a group will be
+            returned.
+        default : any
+            Return this value if the named group and parameter are not found.
+
+        Returns
+        -------
+        value : `c3d.group.GroupReadonly` or `c3d.parameter.ParamReadonly`
+            Either a group or parameter with the specified name(s). If neither
+            is found, returns the default value.
+        '''
+        val = super(Reader, self).get(key)
+        if val:
+            return val.readonly()
+        return default
+
+    def items(self):
+        ''' Get iterable over pairs of (str, `c3d.group.GroupReadonly`) entries.
+        '''
+        return ((k, v.readonly()) for k, v in super(Reader, self).items())
+
+    def values(self):
+        ''' Get iterable over `c3d.group.GroupReadonly` entries.
+        '''
+        return (v.readonly() for k, v in super(Reader, self).items())
+
+    def listed(self):
+        ''' Get iterable over pairs of (int, `c3d.group.GroupReadonly`) entries.
+        '''
+        return ((k, v.readonly()) for k, v in super(Reader, self).listed())
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Reader +(handle) +
+
+

This class provides methods for reading the data in a C3D file.

+

A C3D file contains metadata and frame-based data describing 3D motion.

+

You can iterate over the frames in the file by calling read_frames() after +construction:

+
>>> r = c3d.Reader(open('capture.c3d', 'rb'))
+>>> for frame_no, points, analog in r.read_frames():
+...     print('{0.shape} points in this frame'.format(points))
+
+

Initialize this C3D file by reading header and parameter data.

+

Parameters

+
+
handle : file handle
+
Read metadata and C3D motion frames from the given file handle. This +handle is assumed to be seek-able and read-able. The handle must +remain open for the life of the Reader instance. The Reader does +not close the handle.
+
+

Raises

+
+
AssertionError
+
If the metadata in the C3D file is inconsistent.
+
+
+ +Expand source code + +
class Reader(Manager):
+    '''This class provides methods for reading the data in a C3D file.
+
+    A C3D file contains metadata and frame-based data describing 3D motion.
+
+    You can iterate over the frames in the file by calling `read_frames()` after
+    construction:
+
+    >>> r = c3d.Reader(open('capture.c3d', 'rb'))
+    >>> for frame_no, points, analog in r.read_frames():
+    ...     print('{0.shape} points in this frame'.format(points))
+    '''
+
+    def __init__(self, handle):
+        '''Initialize this C3D file by reading header and parameter data.
+
+        Parameters
+        ----------
+        handle : file handle
+            Read metadata and C3D motion frames from the given file handle. This
+            handle is assumed to be `seek`-able and `read`-able. The handle must
+            remain open for the life of the `Reader` instance. The `Reader` does
+            not `close` the handle.
+
+        Raises
+        ------
+        AssertionError
+            If the metadata in the C3D file is inconsistent.
+        '''
+        super(Reader, self).__init__(Header(handle))
+
+        self._handle = handle
+
+        def seek_param_section_header():
+            ''' Seek to and read the first 4 byte of the parameter header section '''
+            self._handle.seek((self._header.parameter_block - 1) * 512)
+            # metadata header
+            return self._handle.read(4)
+
+        # Begin by reading the processor type:
+        buf = seek_param_section_header()
+        _, _, parameter_blocks, processor = struct.unpack('BBBB', buf)
+        self._dtypes = DataTypes(processor)
+        # Convert header parameters in accordance with the processor type (MIPS format re-reads the header)
+        self._header._processor_convert(self._dtypes, handle)
+
+        # Restart reading the parameter header after parsing processor type
+        buf = seek_param_section_header()
+
+        start_byte = self._handle.tell()
+        endbyte = start_byte + 512 * parameter_blocks - 4
+        while self._handle.tell() < endbyte:
+            chars_in_name, group_id = struct.unpack('bb', self._handle.read(2))
+            if group_id == 0 or chars_in_name == 0:
+                # we've reached the end of the parameter section.
+                break
+            name = self._dtypes.decode_string(self._handle.read(abs(chars_in_name))).upper()
+
+            # Read the byte segment associated with the parameter and create a
+            # separate binary stream object from the data.
+            offset_to_next, = struct.unpack(['<h', '>h'][self._dtypes.is_mips], self._handle.read(2))
+            if offset_to_next == 0:
+                # Last parameter, as number of bytes are unknown,
+                # read the remaining bytes in the parameter section.
+                bytes = self._handle.read(endbyte - self._handle.tell())
+            else:
+                bytes = self._handle.read(offset_to_next - 2)
+            buf = io.BytesIO(bytes)
+
+            if group_id > 0:
+                # We've just started reading a parameter. If its group doesn't
+                # exist, create a blank one. add the parameter to the group.
+                group = super(Reader, self).get(group_id)
+                if group is None:
+                    group = self._add_group(group_id)
+                group.add_param(name, handle=buf)
+            else:
+                # We've just started reading a group. If a group with the
+                # appropriate numerical id exists already (because we've
+                # already created it for a parameter), just set the name of
+                # the group. Otherwise, add a new group.
+                group_id = abs(group_id)
+                size, = struct.unpack('B', buf.read(1))
+                desc = size and buf.read(size) or ''
+                group = super(Reader, self).get(group_id)
+                if group is not None:
+                    self._rename_group(group, name)  # Inserts name key
+                    group.desc = desc
+                else:
+                    self._add_group(group_id, name, desc)
+
+        self._check_metadata()
+
+    def read_frames(self, copy=True, analog_transform=True, check_nan=True, camera_sum=False):
+        '''Iterate over the data frames from our C3D file handle.
+
+        Parameters
+        ----------
+        copy : bool
+            If False, the reader returns a reference to the same data buffers
+            for every frame. The default is True, which causes the reader to
+            return a unique data buffer for each frame. Set this to False if you
+            consume frames as you iterate over them, or True if you store them
+            for later.
+        analog_transform : bool, default=True
+            If True, ANALOG:SCALE, ANALOG:GEN_SCALE, and ANALOG:OFFSET transforms
+            available in the file are applied to the analog channels.
+        check_nan : bool, default=True
+            If True, point x,y,z coordinates with nan values will be marked invalidated
+            and residuals will be set to -1.
+        camera_sum : bool, default=False
+            Camera flag bits will be summed, converting the fifth column to a camera visibility counter.
+
+        Returns
+        -------
+        frames : sequence of (frame number, points, analog)
+            This method generates a sequence of (frame number, points, analog)
+            tuples, one tuple per frame. The first element of each tuple is the
+            frame number. The second is a numpy array of parsed, 5D point data
+            and the third element of each tuple is a numpy array of analog
+            values that were recorded during the frame. (Often the analog data
+            are sampled at a higher frequency than the 3D point data, resulting
+            in multiple analog frames per frame of point data.)
+
+            The first three columns in the returned point data are the (x, y, z)
+            coordinates of the observed motion capture point. The fourth column
+            is an estimate of the error for this particular point, and the fifth
+            column is the number of cameras that observed the point in question.
+            Both the fourth and fifth values are -1 if the point is considered
+            to be invalid.
+        '''
+        # Point magnitude scalar, if scale parameter is < 0 data is floating point
+        # (in which case the magnitude is the absolute value)
+        scale_mag = abs(self.point_scale)
+        is_float = self.point_scale < 0
+
+        if is_float:
+            point_word_bytes = 4
+        else:
+            point_word_bytes = 2
+        points = np.zeros((self.point_used, 5), np.float32)
+
+        # TODO: handle ANALOG:BITS parameter here!
+        p = self.get('ANALOG:FORMAT')
+        analog_unsigned = p and p.string_value.strip().upper() == 'UNSIGNED'
+        if is_float:
+            analog_dtype = self._dtypes.float32
+            analog_word_bytes = 4
+        elif analog_unsigned:
+            # Note*: Floating point is 'always' defined for both analog and point data, according to the standard.
+            analog_dtype = self._dtypes.uint16
+            analog_word_bytes = 2
+            # Verify BITS parameter for analog
+            p = self.get('ANALOG:BITS')
+            if p and p._as_integer_value / 8 != analog_word_bytes:
+                raise NotImplementedError('Analog data using {} bits is not supported.'.format(p._as_integer_value))
+        else:
+            analog_dtype = self._dtypes.int16
+            analog_word_bytes = 2
+
+        analog = np.array([], float)
+        analog_scales, analog_offsets = self.get_analog_transform()
+
+        # Seek to the start point of the data blocks
+        self._handle.seek((self._header.data_block - 1) * 512)
+        # Number of values (words) read in regard to POINT/ANALOG data
+        N_point = 4 * self.point_used
+        N_analog = self.analog_used * self.analog_per_frame
+
+        # Total bytes per frame
+        point_bytes = N_point * point_word_bytes
+        analog_bytes = N_analog * analog_word_bytes
+        # Parse the data blocks
+        for frame_no in range(self.first_frame, self.last_frame + 1):
+            # Read the byte data (used) for the block
+            raw_bytes = self._handle.read(N_point * point_word_bytes)
+            raw_analog = self._handle.read(N_analog * analog_word_bytes)
+            # Verify read pointers (any of the two can be assumed to be 0)
+            if len(raw_bytes) < point_bytes:
+                warnings.warn('''reached end of file (EOF) while reading POINT data at frame index {}
+                                 and file pointer {}!'''.format(frame_no - self.first_frame, self._handle.tell()))
+                return
+            if len(raw_analog) < analog_bytes:
+                warnings.warn('''reached end of file (EOF) while reading POINT data at frame index {}
+                                 and file pointer {}!'''.format(frame_no - self.first_frame, self._handle.tell()))
+                return
+
+            if is_float:
+                # Convert every 4 byte words to a float-32 reprensentation
+                # (the fourth column is still not a float32 representation)
+                if self._dtypes.is_dec:
+                    # Convert each of the first 6 16-bit words from DEC to IEEE float
+                    points[:, :4] = DEC_to_IEEE_BYTES(raw_bytes).reshape((self.point_used, 4))
+                else:  # If IEEE or MIPS:
+                    # Convert each of the first 6 16-bit words to native float
+                    points[:, :4] = np.frombuffer(raw_bytes,
+                                                  dtype=self._dtypes.float32,
+                                                  count=N_point).reshape((self.point_used, 4))
+
+                # Cast last word to signed integer in system endian format
+                last_word = points[:, 3].astype(np.int32)
+
+            else:
+                # View the bytes as signed 16-bit integers
+                raw = np.frombuffer(raw_bytes,
+                                    dtype=self._dtypes.int16,
+                                    count=N_point).reshape((self.point_used, 4))
+                # Read the first six 16-bit words as x, y, z coordinates
+                points[:, :3] = raw[:, :3] * scale_mag
+                # Cast last word to signed integer in system endian format
+                last_word = raw[:, 3].astype(np.int16)
+
+            # Parse camera-observed bits and residuals.
+            # Notes:
+            # - Invalid sample if residual is equal to -1 (check if word < 0).
+            # - A residual of 0.0 represent modeled data (filtered or interpolated).
+            # - Camera and residual words are always 8-bit (1 byte), never 16-bit.
+            # - If floating point, the byte words are encoded in an integer cast to a float,
+            #    and are written directly in byte form (see the MLS guide).
+            ##
+            # Read the residual and camera byte words (Note* if 32 bit word negative sign is discarded).
+            residual_byte, camera_byte = (last_word & 0x00ff), (last_word & 0x7f00) >> 8
+
+            # Fourth value is floating-point (scaled) error estimate (residual)
+            points[:, 3] = residual_byte * scale_mag
+
+            # Determine invalid samples
+            invalid = last_word < 0
+            if check_nan:
+                is_nan = ~np.all(np.isfinite(points[:, :4]), axis=1)
+                points[is_nan, :3] = 0.0
+                invalid |= is_nan
+            # Update discarded - sign
+            points[invalid, 3] = -1
+
+            # Fifth value is the camera-observation byte
+            if camera_sum:
+                # Convert to observation sum
+                points[:, 4] = sum((camera_byte & (1 << k)) >> k for k in range(7))
+            else:
+                points[:, 4] = camera_byte  # .astype(np.float32)
+
+            # Check if analog data exist, and parse if so
+            if N_analog > 0:
+                if is_float and self._dtypes.is_dec:
+                    # Convert each of the 16-bit words from DEC to IEEE float
+                    analog = DEC_to_IEEE_BYTES(raw_analog)
+                else:
+                    # Integer or INTEL/MIPS floating point data can be parsed directly
+                    analog = np.frombuffer(raw_analog, dtype=analog_dtype, count=N_analog)
+
+                # Reformat and convert
+                analog = analog.reshape((-1, self.analog_used)).T
+                analog = analog.astype(float)
+                # Convert analog
+                analog = (analog - analog_offsets) * analog_scales
+
+            # Output buffers
+            if copy:
+                yield frame_no, points.copy(), analog  # .copy(), a new array is generated per frame for analog data.
+            else:
+                yield frame_no, points, analog
+
+        # Function evaluating EOF, note that data section is written in blocks of 512
+        final_byte_index = self._handle.tell()
+        self._handle.seek(0, 2)  # os.SEEK_END)
+        # Check if more then 1 block remain
+        if self._handle.tell() - final_byte_index >= 512:
+            warnings.warn('incomplete reading of data blocks. {} bytes remained after all datablocks were read!'.format(
+                self._handle.tell() - final_byte_index))
+
+    @property
+    def proc_type(self) -> int:
+        '''Get the processory type associated with the data format in the file.
+        '''
+        return self._dtypes.proc_type
+
+    def to_writer(self, conversion=None):
+        ''' Converts the reader to a `c3d.writer.Writer` instance using the conversion mode.
+
+        See `c3d.writer.Writer.from_reader()` for supported conversion modes.
+        '''
+        from .writer import Writer
+        return Writer.from_reader(self, conversion=conversion)
+
+    def get(self, key, default=None):
+        '''Get a readonly group or parameter.
+
+        Parameters
+        ----------
+        key : str
+            If this string contains a period (.), then the part before the
+            period will be used to retrieve a group, and the part after the
+            period will be used to retrieve a parameter from that group. If this
+            string does not contain a period, then just a group will be
+            returned.
+        default : any
+            Return this value if the named group and parameter are not found.
+
+        Returns
+        -------
+        value : `c3d.group.GroupReadonly` or `c3d.parameter.ParamReadonly`
+            Either a group or parameter with the specified name(s). If neither
+            is found, returns the default value.
+        '''
+        val = super(Reader, self).get(key)
+        if val:
+            return val.readonly()
+        return default
+
+    def items(self):
+        ''' Get iterable over pairs of (str, `c3d.group.GroupReadonly`) entries.
+        '''
+        return ((k, v.readonly()) for k, v in super(Reader, self).items())
+
+    def values(self):
+        ''' Get iterable over `c3d.group.GroupReadonly` entries.
+        '''
+        return (v.readonly() for k, v in super(Reader, self).items())
+
+    def listed(self):
+        ''' Get iterable over pairs of (int, `c3d.group.GroupReadonly`) entries.
+        '''
+        return ((k, v.readonly()) for k, v in super(Reader, self).listed())
+
+

Ancestors

+ +

Instance variables

+
+
var proc_type : int
+
+

Get the processory type associated with the data format in the file.

+
+ +Expand source code + +
@property
+def proc_type(self) -> int:
+    '''Get the processory type associated with the data format in the file.
+    '''
+    return self._dtypes.proc_type
+
+
+
+

Methods

+
+
+def get(self, key, default=None) +
+
+

Get a readonly group or parameter.

+

Parameters

+
+
key : str
+
If this string contains a period (.), then the part before the +period will be used to retrieve a group, and the part after the +period will be used to retrieve a parameter from that group. If this +string does not contain a period, then just a group will be +returned.
+
default : any
+
Return this value if the named group and parameter are not found.
+
+

Returns

+
+
value : GroupReadonly or ParamReadonly
+
Either a group or parameter with the specified name(s). If neither +is found, returns the default value.
+
+
+ +Expand source code + +
def get(self, key, default=None):
+    '''Get a readonly group or parameter.
+
+    Parameters
+    ----------
+    key : str
+        If this string contains a period (.), then the part before the
+        period will be used to retrieve a group, and the part after the
+        period will be used to retrieve a parameter from that group. If this
+        string does not contain a period, then just a group will be
+        returned.
+    default : any
+        Return this value if the named group and parameter are not found.
+
+    Returns
+    -------
+    value : `c3d.group.GroupReadonly` or `c3d.parameter.ParamReadonly`
+        Either a group or parameter with the specified name(s). If neither
+        is found, returns the default value.
+    '''
+    val = super(Reader, self).get(key)
+    if val:
+        return val.readonly()
+    return default
+
+
+
+def items(self) +
+
+

Get iterable over pairs of (str, GroupReadonly) entries.

+
+ +Expand source code + +
def items(self):
+    ''' Get iterable over pairs of (str, `c3d.group.GroupReadonly`) entries.
+    '''
+    return ((k, v.readonly()) for k, v in super(Reader, self).items())
+
+
+
+def listed(self) +
+
+

Get iterable over pairs of (int, GroupReadonly) entries.

+
+ +Expand source code + +
def listed(self):
+    ''' Get iterable over pairs of (int, `c3d.group.GroupReadonly`) entries.
+    '''
+    return ((k, v.readonly()) for k, v in super(Reader, self).listed())
+
+
+
+def read_frames(self, copy=True, analog_transform=True, check_nan=True, camera_sum=False) +
+
+

Iterate over the data frames from our C3D file handle.

+

Parameters

+
+
copy : bool
+
If False, the reader returns a reference to the same data buffers +for every frame. The default is True, which causes the reader to +return a unique data buffer for each frame. Set this to False if you +consume frames as you iterate over them, or True if you store them +for later.
+
analog_transform : bool, default=True
+
If True, ANALOG:SCALE, ANALOG:GEN_SCALE, and ANALOG:OFFSET transforms +available in the file are applied to the analog channels.
+
check_nan : bool, default=True
+
If True, point x,y,z coordinates with nan values will be marked invalidated +and residuals will be set to -1.
+
camera_sum : bool, default=False
+
Camera flag bits will be summed, converting the fifth column to a camera visibility counter.
+
+

Returns

+
+
frames : sequence of (frame number, points, analog)
+
+

This method generates a sequence of (frame number, points, analog) +tuples, one tuple per frame. The first element of each tuple is the +frame number. The second is a numpy array of parsed, 5D point data +and the third element of each tuple is a numpy array of analog +values that were recorded during the frame. (Often the analog data +are sampled at a higher frequency than the 3D point data, resulting +in multiple analog frames per frame of point data.)

+

The first three columns in the returned point data are the (x, y, z) +coordinates of the observed motion capture point. The fourth column +is an estimate of the error for this particular point, and the fifth +column is the number of cameras that observed the point in question. +Both the fourth and fifth values are -1 if the point is considered +to be invalid.

+
+
+
+ +Expand source code + +
def read_frames(self, copy=True, analog_transform=True, check_nan=True, camera_sum=False):
+    '''Iterate over the data frames from our C3D file handle.
+
+    Parameters
+    ----------
+    copy : bool
+        If False, the reader returns a reference to the same data buffers
+        for every frame. The default is True, which causes the reader to
+        return a unique data buffer for each frame. Set this to False if you
+        consume frames as you iterate over them, or True if you store them
+        for later.
+    analog_transform : bool, default=True
+        If True, ANALOG:SCALE, ANALOG:GEN_SCALE, and ANALOG:OFFSET transforms
+        available in the file are applied to the analog channels.
+    check_nan : bool, default=True
+        If True, point x,y,z coordinates with nan values will be marked invalidated
+        and residuals will be set to -1.
+    camera_sum : bool, default=False
+        Camera flag bits will be summed, converting the fifth column to a camera visibility counter.
+
+    Returns
+    -------
+    frames : sequence of (frame number, points, analog)
+        This method generates a sequence of (frame number, points, analog)
+        tuples, one tuple per frame. The first element of each tuple is the
+        frame number. The second is a numpy array of parsed, 5D point data
+        and the third element of each tuple is a numpy array of analog
+        values that were recorded during the frame. (Often the analog data
+        are sampled at a higher frequency than the 3D point data, resulting
+        in multiple analog frames per frame of point data.)
+
+        The first three columns in the returned point data are the (x, y, z)
+        coordinates of the observed motion capture point. The fourth column
+        is an estimate of the error for this particular point, and the fifth
+        column is the number of cameras that observed the point in question.
+        Both the fourth and fifth values are -1 if the point is considered
+        to be invalid.
+    '''
+    # Point magnitude scalar, if scale parameter is < 0 data is floating point
+    # (in which case the magnitude is the absolute value)
+    scale_mag = abs(self.point_scale)
+    is_float = self.point_scale < 0
+
+    if is_float:
+        point_word_bytes = 4
+    else:
+        point_word_bytes = 2
+    points = np.zeros((self.point_used, 5), np.float32)
+
+    # TODO: handle ANALOG:BITS parameter here!
+    p = self.get('ANALOG:FORMAT')
+    analog_unsigned = p and p.string_value.strip().upper() == 'UNSIGNED'
+    if is_float:
+        analog_dtype = self._dtypes.float32
+        analog_word_bytes = 4
+    elif analog_unsigned:
+        # Note*: Floating point is 'always' defined for both analog and point data, according to the standard.
+        analog_dtype = self._dtypes.uint16
+        analog_word_bytes = 2
+        # Verify BITS parameter for analog
+        p = self.get('ANALOG:BITS')
+        if p and p._as_integer_value / 8 != analog_word_bytes:
+            raise NotImplementedError('Analog data using {} bits is not supported.'.format(p._as_integer_value))
+    else:
+        analog_dtype = self._dtypes.int16
+        analog_word_bytes = 2
+
+    analog = np.array([], float)
+    analog_scales, analog_offsets = self.get_analog_transform()
+
+    # Seek to the start point of the data blocks
+    self._handle.seek((self._header.data_block - 1) * 512)
+    # Number of values (words) read in regard to POINT/ANALOG data
+    N_point = 4 * self.point_used
+    N_analog = self.analog_used * self.analog_per_frame
+
+    # Total bytes per frame
+    point_bytes = N_point * point_word_bytes
+    analog_bytes = N_analog * analog_word_bytes
+    # Parse the data blocks
+    for frame_no in range(self.first_frame, self.last_frame + 1):
+        # Read the byte data (used) for the block
+        raw_bytes = self._handle.read(N_point * point_word_bytes)
+        raw_analog = self._handle.read(N_analog * analog_word_bytes)
+        # Verify read pointers (any of the two can be assumed to be 0)
+        if len(raw_bytes) < point_bytes:
+            warnings.warn('''reached end of file (EOF) while reading POINT data at frame index {}
+                             and file pointer {}!'''.format(frame_no - self.first_frame, self._handle.tell()))
+            return
+        if len(raw_analog) < analog_bytes:
+            warnings.warn('''reached end of file (EOF) while reading POINT data at frame index {}
+                             and file pointer {}!'''.format(frame_no - self.first_frame, self._handle.tell()))
+            return
+
+        if is_float:
+            # Convert every 4 byte words to a float-32 reprensentation
+            # (the fourth column is still not a float32 representation)
+            if self._dtypes.is_dec:
+                # Convert each of the first 6 16-bit words from DEC to IEEE float
+                points[:, :4] = DEC_to_IEEE_BYTES(raw_bytes).reshape((self.point_used, 4))
+            else:  # If IEEE or MIPS:
+                # Convert each of the first 6 16-bit words to native float
+                points[:, :4] = np.frombuffer(raw_bytes,
+                                              dtype=self._dtypes.float32,
+                                              count=N_point).reshape((self.point_used, 4))
+
+            # Cast last word to signed integer in system endian format
+            last_word = points[:, 3].astype(np.int32)
+
+        else:
+            # View the bytes as signed 16-bit integers
+            raw = np.frombuffer(raw_bytes,
+                                dtype=self._dtypes.int16,
+                                count=N_point).reshape((self.point_used, 4))
+            # Read the first six 16-bit words as x, y, z coordinates
+            points[:, :3] = raw[:, :3] * scale_mag
+            # Cast last word to signed integer in system endian format
+            last_word = raw[:, 3].astype(np.int16)
+
+        # Parse camera-observed bits and residuals.
+        # Notes:
+        # - Invalid sample if residual is equal to -1 (check if word < 0).
+        # - A residual of 0.0 represent modeled data (filtered or interpolated).
+        # - Camera and residual words are always 8-bit (1 byte), never 16-bit.
+        # - If floating point, the byte words are encoded in an integer cast to a float,
+        #    and are written directly in byte form (see the MLS guide).
+        ##
+        # Read the residual and camera byte words (Note* if 32 bit word negative sign is discarded).
+        residual_byte, camera_byte = (last_word & 0x00ff), (last_word & 0x7f00) >> 8
+
+        # Fourth value is floating-point (scaled) error estimate (residual)
+        points[:, 3] = residual_byte * scale_mag
+
+        # Determine invalid samples
+        invalid = last_word < 0
+        if check_nan:
+            is_nan = ~np.all(np.isfinite(points[:, :4]), axis=1)
+            points[is_nan, :3] = 0.0
+            invalid |= is_nan
+        # Update discarded - sign
+        points[invalid, 3] = -1
+
+        # Fifth value is the camera-observation byte
+        if camera_sum:
+            # Convert to observation sum
+            points[:, 4] = sum((camera_byte & (1 << k)) >> k for k in range(7))
+        else:
+            points[:, 4] = camera_byte  # .astype(np.float32)
+
+        # Check if analog data exist, and parse if so
+        if N_analog > 0:
+            if is_float and self._dtypes.is_dec:
+                # Convert each of the 16-bit words from DEC to IEEE float
+                analog = DEC_to_IEEE_BYTES(raw_analog)
+            else:
+                # Integer or INTEL/MIPS floating point data can be parsed directly
+                analog = np.frombuffer(raw_analog, dtype=analog_dtype, count=N_analog)
+
+            # Reformat and convert
+            analog = analog.reshape((-1, self.analog_used)).T
+            analog = analog.astype(float)
+            # Convert analog
+            analog = (analog - analog_offsets) * analog_scales
+
+        # Output buffers
+        if copy:
+            yield frame_no, points.copy(), analog  # .copy(), a new array is generated per frame for analog data.
+        else:
+            yield frame_no, points, analog
+
+    # Function evaluating EOF, note that data section is written in blocks of 512
+    final_byte_index = self._handle.tell()
+    self._handle.seek(0, 2)  # os.SEEK_END)
+    # Check if more then 1 block remain
+    if self._handle.tell() - final_byte_index >= 512:
+        warnings.warn('incomplete reading of data blocks. {} bytes remained after all datablocks were read!'.format(
+            self._handle.tell() - final_byte_index))
+
+
+
+def to_writer(self, conversion=None) +
+
+

Converts the reader to a Writer instance using the conversion mode.

+

See Writer.from_reader() for supported conversion modes.

+
+ +Expand source code + +
def to_writer(self, conversion=None):
+    ''' Converts the reader to a `c3d.writer.Writer` instance using the conversion mode.
+
+    See `c3d.writer.Writer.from_reader()` for supported conversion modes.
+    '''
+    from .writer import Writer
+    return Writer.from_reader(self, conversion=conversion)
+
+
+
+def values(self) +
+
+

Get iterable over GroupReadonly entries.

+
+ +Expand source code + +
def values(self):
+    ''' Get iterable over `c3d.group.GroupReadonly` entries.
+    '''
+    return (v.readonly() for k, v in super(Reader, self).items())
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/c3d/utils.html b/docs/c3d/utils.html new file mode 100644 index 0000000..a79fbe9 --- /dev/null +++ b/docs/c3d/utils.html @@ -0,0 +1,500 @@ + + + + + + +c3d.utils API documentation + + + + + + + + + + + + +
+
+
+

Module c3d.utils

+
+
+

Trailing utility functions.

+
+ +Expand source code + +
''' Trailing utility functions.
+'''
+import numpy as np
+import struct
+
+
+def is_integer(value):
+    '''Check if value input is integer.'''
+    return isinstance(value, (int, np.int32, np.int64))
+
+
+def is_iterable(value):
+    '''Check if value is iterable.'''
+    return hasattr(value, '__iter__')
+
+
+def type_npy2struct(dtype):
+    ''' Convert numpy dtype format to a struct package format string.
+    '''
+    return dtype.byteorder + dtype.char
+
+
+def pack_labels(labels):
+    ''' Static method used to pack and pad the set of `labels` strings before
+        passing the output into a `c3d.group.Group.add_str`.
+
+    Parameters
+    ----------
+    labels : iterable
+        List of strings to pack and pad into a single string suitable for encoding in a Parameter entry.
+
+    Example
+    -------
+    >>> labels = ['RFT1', 'RFT2', 'RFT3', 'LFT1', 'LFT2', 'LFT3']
+    >>> param_str, label_max_size = Writer.pack_labels(labels)
+    >>> writer.point_group.add_str('LABELS',
+                                   'Point labels.',
+                                   label_str,
+                                   label_max_size,
+                                   len(labels))
+
+    Returns
+    -------
+    param_str : str
+        String containing `labels` packed into a single variable where
+        each string is padded to match the longest `labels` string.
+    label_max_size : int
+        Number of bytes associated with the longest `label` string, all strings are padded to this length.
+    '''
+    labels = np.ravel(labels)
+    # Get longest label name
+    label_max_size = 0
+    label_max_size = max(label_max_size, np.max([len(label) for label in labels]))
+    label_str = ''.join(label.ljust(label_max_size) for label in labels)
+    return label_str, label_max_size
+
+
+class Decorator(object):
+    '''Base class for extending (decorating) a python object.
+    '''
+    def __init__(self, decoratee):
+        self._decoratee = decoratee
+
+    def __getattr__(self, name):
+        return getattr(self._decoratee, name)
+
+
+def UNPACK_FLOAT_IEEE(uint_32):
+    '''Unpacks a single 32 bit unsigned int to a IEEE float representation
+    '''
+    return struct.unpack('f', struct.pack("<I", uint_32))[0]
+
+
+def UNPACK_FLOAT_MIPS(uint_32):
+    '''Unpacks a single 32 bit unsigned int to a IEEE float representation
+    '''
+    return struct.unpack('f', struct.pack(">I", uint_32))[0]
+
+
+def DEC_to_IEEE(uint_32):
+    '''Convert the 32 bit representation of a DEC float to IEEE format.
+
+    Params:
+    ----
+    uint_32 : 32 bit unsigned integer containing the DEC single precision float point bits.
+    Returns : IEEE formated floating point of the same shape as the input.
+    '''
+    # Follows the bit pattern found:
+    #   http://home.fnal.gov/~yang/Notes/ieee_vs_dec_float.txt
+    # Further formating descriptions can be found:
+    #   http://www.irig106.org/docs/106-07/appendixO.pdf
+    # In accodance with the first ref. first & second 16 bit words are placed
+    # in a big endian 16 bit word representation, and needs to be inverted.
+    # Second reference describe the DEC->IEEE conversion.
+
+    # Warning! Unsure if NaN numbers are managed appropriately.
+
+    # Shuffle the first two bit words from DEC bit representation to an ordered representation.
+    # Note that the most significant fraction bits are placed in the first 7 bits.
+    #
+    # Below are the DEC layout in accordance with the references:
+    # ___________________________________________________________________________________
+    # |         Mantissa (16:0)         |       SIGN    |       Exponent (8:0)  |       Mantissa (23:17)        |
+    # ___________________________________________________________________________________
+    # |32-                                        -16|   15        |14-                           -7|6-                                   -0|
+    #
+    # Legend:
+    # _______________________________________________________
+    # | Part (left bit of segment : right bit) | Part | ..
+    # _______________________________________________________
+    # |Bit adress -     ..       - Bit adress | Bit adress - ..
+    ####
+
+    # Swap the first and last 16  bits for a consistent alignment of the fraction
+    reshuffled = ((uint_32 & 0xFFFF0000) >> 16) | ((uint_32 & 0x0000FFFF) << 16)
+    # After the shuffle each part are in little-endian and ordered as: SIGN-Exponent-Fraction
+    exp_bits = ((reshuffled & 0xFF000000) - 1) & 0xFF000000
+    reshuffled = (reshuffled & 0x00FFFFFF) | exp_bits
+    return UNPACK_FLOAT_IEEE(reshuffled)
+
+
+def DEC_to_IEEE_BYTES(bytes):
+    '''Convert byte array containing 32 bit DEC floats to IEEE format.
+
+    Params:
+    ----
+    bytes : Byte array where every 4 bytes represent a single precision DEC float.
+    Returns : IEEE formated floating point of the same shape as the input.
+    '''
+
+    # See comments in DEC_to_IEEE() for DEC format definition
+
+    # Reshuffle
+    bytes = memoryview(bytes)
+    reshuffled = np.empty(len(bytes), dtype=np.dtype('B'))
+    reshuffled[::4] = bytes[2::4]
+    reshuffled[1::4] = bytes[3::4]
+    reshuffled[2::4] = bytes[::4]
+    # Decrement exponent by 2, if exp. > 1
+    reshuffled[3::4] = bytes[1::4] + (np.bitwise_and(bytes[1::4], 0x7f) == 0) - 1
+
+    # There are different ways to adjust for differences in DEC/IEEE representation
+    # after reshuffle. Two simple methods are:
+    # 1) Decrement exponent bits by 2, then convert to IEEE.
+    # 2) Convert to IEEE directly and divide by four.
+    # 3) Handle edge cases, expensive in python...
+    # However these are simple methods, and do not accurately convert when:
+    # 1) Exponent < 2 (without bias), impossible to decrement exponent without adjusting fraction/mantissa.
+    # 2) Exponent == 0, DEC numbers are then 0 or undefined while IEEE is not. NaN are produced when exponent == 255.
+    # Here method 1) is used, which mean that only small numbers will be represented incorrectly.
+
+    return np.frombuffer(reshuffled.tobytes(),
+                         dtype=np.float32,
+                         count=int(len(bytes) / 4))
+
+
+
+
+
+
+
+

Functions

+
+
+def DEC_to_IEEE(uint_32) +
+
+

Convert the 32 bit representation of a DEC float to IEEE format.

+

Params:

+

uint_32 : 32 bit unsigned integer containing the DEC single precision float point bits. +Returns : IEEE formated floating point of the same shape as the input.

+
+ +Expand source code + +
def DEC_to_IEEE(uint_32):
+    '''Convert the 32 bit representation of a DEC float to IEEE format.
+
+    Params:
+    ----
+    uint_32 : 32 bit unsigned integer containing the DEC single precision float point bits.
+    Returns : IEEE formated floating point of the same shape as the input.
+    '''
+    # Follows the bit pattern found:
+    #   http://home.fnal.gov/~yang/Notes/ieee_vs_dec_float.txt
+    # Further formating descriptions can be found:
+    #   http://www.irig106.org/docs/106-07/appendixO.pdf
+    # In accodance with the first ref. first & second 16 bit words are placed
+    # in a big endian 16 bit word representation, and needs to be inverted.
+    # Second reference describe the DEC->IEEE conversion.
+
+    # Warning! Unsure if NaN numbers are managed appropriately.
+
+    # Shuffle the first two bit words from DEC bit representation to an ordered representation.
+    # Note that the most significant fraction bits are placed in the first 7 bits.
+    #
+    # Below are the DEC layout in accordance with the references:
+    # ___________________________________________________________________________________
+    # |         Mantissa (16:0)         |       SIGN    |       Exponent (8:0)  |       Mantissa (23:17)        |
+    # ___________________________________________________________________________________
+    # |32-                                        -16|   15        |14-                           -7|6-                                   -0|
+    #
+    # Legend:
+    # _______________________________________________________
+    # | Part (left bit of segment : right bit) | Part | ..
+    # _______________________________________________________
+    # |Bit adress -     ..       - Bit adress | Bit adress - ..
+    ####
+
+    # Swap the first and last 16  bits for a consistent alignment of the fraction
+    reshuffled = ((uint_32 & 0xFFFF0000) >> 16) | ((uint_32 & 0x0000FFFF) << 16)
+    # After the shuffle each part are in little-endian and ordered as: SIGN-Exponent-Fraction
+    exp_bits = ((reshuffled & 0xFF000000) - 1) & 0xFF000000
+    reshuffled = (reshuffled & 0x00FFFFFF) | exp_bits
+    return UNPACK_FLOAT_IEEE(reshuffled)
+
+
+
+def DEC_to_IEEE_BYTES(bytes) +
+
+

Convert byte array containing 32 bit DEC floats to IEEE format.

+

Params:

+

bytes : Byte array where every 4 bytes represent a single precision DEC float. +Returns : IEEE formated floating point of the same shape as the input.

+
+ +Expand source code + +
def DEC_to_IEEE_BYTES(bytes):
+    '''Convert byte array containing 32 bit DEC floats to IEEE format.
+
+    Params:
+    ----
+    bytes : Byte array where every 4 bytes represent a single precision DEC float.
+    Returns : IEEE formated floating point of the same shape as the input.
+    '''
+
+    # See comments in DEC_to_IEEE() for DEC format definition
+
+    # Reshuffle
+    bytes = memoryview(bytes)
+    reshuffled = np.empty(len(bytes), dtype=np.dtype('B'))
+    reshuffled[::4] = bytes[2::4]
+    reshuffled[1::4] = bytes[3::4]
+    reshuffled[2::4] = bytes[::4]
+    # Decrement exponent by 2, if exp. > 1
+    reshuffled[3::4] = bytes[1::4] + (np.bitwise_and(bytes[1::4], 0x7f) == 0) - 1
+
+    # There are different ways to adjust for differences in DEC/IEEE representation
+    # after reshuffle. Two simple methods are:
+    # 1) Decrement exponent bits by 2, then convert to IEEE.
+    # 2) Convert to IEEE directly and divide by four.
+    # 3) Handle edge cases, expensive in python...
+    # However these are simple methods, and do not accurately convert when:
+    # 1) Exponent < 2 (without bias), impossible to decrement exponent without adjusting fraction/mantissa.
+    # 2) Exponent == 0, DEC numbers are then 0 or undefined while IEEE is not. NaN are produced when exponent == 255.
+    # Here method 1) is used, which mean that only small numbers will be represented incorrectly.
+
+    return np.frombuffer(reshuffled.tobytes(),
+                         dtype=np.float32,
+                         count=int(len(bytes) / 4))
+
+
+
+def UNPACK_FLOAT_IEEE(uint_32) +
+
+

Unpacks a single 32 bit unsigned int to a IEEE float representation

+
+ +Expand source code + +
def UNPACK_FLOAT_IEEE(uint_32):
+    '''Unpacks a single 32 bit unsigned int to a IEEE float representation
+    '''
+    return struct.unpack('f', struct.pack("<I", uint_32))[0]
+
+
+
+def UNPACK_FLOAT_MIPS(uint_32) +
+
+

Unpacks a single 32 bit unsigned int to a IEEE float representation

+
+ +Expand source code + +
def UNPACK_FLOAT_MIPS(uint_32):
+    '''Unpacks a single 32 bit unsigned int to a IEEE float representation
+    '''
+    return struct.unpack('f', struct.pack(">I", uint_32))[0]
+
+
+
+def is_integer(value) +
+
+

Check if value input is integer.

+
+ +Expand source code + +
def is_integer(value):
+    '''Check if value input is integer.'''
+    return isinstance(value, (int, np.int32, np.int64))
+
+
+
+def is_iterable(value) +
+
+

Check if value is iterable.

+
+ +Expand source code + +
def is_iterable(value):
+    '''Check if value is iterable.'''
+    return hasattr(value, '__iter__')
+
+
+
+def pack_labels(labels) +
+
+

Static method used to pack and pad the set of labels strings before +passing the output into a Group.add_str().

+

Parameters

+
+
labels : iterable
+
List of strings to pack and pad into a single string suitable for encoding in a Parameter entry.
+
+

Example

+
>>> labels = ['RFT1', 'RFT2', 'RFT3', 'LFT1', 'LFT2', 'LFT3']
+>>> param_str, label_max_size = Writer.pack_labels(labels)
+>>> writer.point_group.add_str('LABELS',
+                               'Point labels.',
+                               label_str,
+                               label_max_size,
+                               len(labels))
+
+

Returns

+
+
param_str : str
+
String containing labels packed into a single variable where +each string is padded to match the longest labels string.
+
label_max_size : int
+
Number of bytes associated with the longest label string, all strings are padded to this length.
+
+
+ +Expand source code + +
def pack_labels(labels):
+    ''' Static method used to pack and pad the set of `labels` strings before
+        passing the output into a `c3d.group.Group.add_str`.
+
+    Parameters
+    ----------
+    labels : iterable
+        List of strings to pack and pad into a single string suitable for encoding in a Parameter entry.
+
+    Example
+    -------
+    >>> labels = ['RFT1', 'RFT2', 'RFT3', 'LFT1', 'LFT2', 'LFT3']
+    >>> param_str, label_max_size = Writer.pack_labels(labels)
+    >>> writer.point_group.add_str('LABELS',
+                                   'Point labels.',
+                                   label_str,
+                                   label_max_size,
+                                   len(labels))
+
+    Returns
+    -------
+    param_str : str
+        String containing `labels` packed into a single variable where
+        each string is padded to match the longest `labels` string.
+    label_max_size : int
+        Number of bytes associated with the longest `label` string, all strings are padded to this length.
+    '''
+    labels = np.ravel(labels)
+    # Get longest label name
+    label_max_size = 0
+    label_max_size = max(label_max_size, np.max([len(label) for label in labels]))
+    label_str = ''.join(label.ljust(label_max_size) for label in labels)
+    return label_str, label_max_size
+
+
+
+def type_npy2struct(dtype) +
+
+

Convert numpy dtype format to a struct package format string.

+
+ +Expand source code + +
def type_npy2struct(dtype):
+    ''' Convert numpy dtype format to a struct package format string.
+    '''
+    return dtype.byteorder + dtype.char
+
+
+
+
+
+

Classes

+
+
+class Decorator +(decoratee) +
+
+

Base class for extending (decorating) a python object.

+
+ +Expand source code + +
class Decorator(object):
+    '''Base class for extending (decorating) a python object.
+    '''
+    def __init__(self, decoratee):
+        self._decoratee = decoratee
+
+    def __getattr__(self, name):
+        return getattr(self._decoratee, name)
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/c3d/writer.html b/docs/c3d/writer.html new file mode 100644 index 0000000..0d7b9e6 --- /dev/null +++ b/docs/c3d/writer.html @@ -0,0 +1,1804 @@ + + + + + + +c3d.writer API documentation + + + + + + + + + + + + +
+
+
+

Module c3d.writer

+
+
+

Contains the Writer class for writing C3D files.

+
+ +Expand source code + +
'''Contains the Writer class for writing C3D files.'''
+
+import copy
+import numpy as np
+import struct
+# import warnings
+from . import utils
+from .manager import Manager
+from .dtypes import DataTypes
+
+
+class Writer(Manager):
+    '''This class writes metadata and frames to a C3D file.
+
+    For example, to read an existing C3D file, apply some sort of data
+    processing to the frames, and write out another C3D file::
+
+    >>> r = c3d.Reader(open('data.c3d', 'rb'))
+    >>> w = c3d.Writer()
+    >>> w.add_frames(process_frames_somehow(r.read_frames()))
+    >>> with open('smoothed.c3d', 'wb') as handle:
+    >>>     w.write(handle)
+
+    Parameters
+    ----------
+    point_rate : float, optional
+        The frame rate of the data. Defaults to 480.
+    analog_rate : float, optional
+        The number of analog samples per frame. Defaults to 0.
+    point_scale : float, optional
+        The scale factor for point data. Defaults to -1 (i.e., "check the
+        POINT:SCALE parameter").
+    point_units : str, optional
+        The units that the point numbers represent. Defaults to ``'mm  '``.
+    gen_scale : float, optional
+        General scaling factor for analog data. Defaults to 1.
+    '''
+
+    def __init__(self,
+                 point_rate=480.,
+                 analog_rate=0.,
+                 point_scale=-1.):
+        '''Set minimal metadata for this writer.
+
+        '''
+        self._dtypes = DataTypes()  # Only support INTEL format from writing
+        super(Writer, self).__init__()
+
+        # Header properties
+        self._header.frame_rate = np.float32(point_rate)
+        self._header.scale_factor = np.float32(point_scale)
+        self.analog_rate = analog_rate
+        self._frames = []
+
+    @staticmethod
+    def from_reader(reader, conversion=None):
+        ''' Convert a `c3d.reader.Reader` to a persistent `c3d.writer.Writer` instance.
+
+        Parameters
+        ----------
+        source : `c3d.reader.Reader`
+            Source to copy data from.
+        conversion : str
+            Conversion mode, None is equivalent to the default mode. Supported modes are:
+
+                'convert'       - (Default) Convert the Reader to a Writer
+                                  instance and explicitly delete the Reader.
+
+                'copy'          - Reader objects will be deep copied.
+
+                'copy_metadata' - Similar to 'copy' but only copies metadata and
+                                  not point and analog frame data.
+
+                'copy_shallow'  - Similar to 'copy' but group parameters are
+                                  not copied.
+
+                'copy_header'   - Similar to 'copy_shallow' but only the
+                                  header is copied (frame data is not copied).
+
+        Returns
+        -------
+        param : `c3d.writer.Writer`
+            A writeable and persistent representation of the `c3d.reader.Reader` object.
+
+        Raises
+        ------
+        ValueError
+            If mode string is not equivalent to one of the supported modes.
+            If attempting to convert non-Intel files using mode other than 'shallow_copy'.
+        '''
+        writer = Writer()
+        # Modes
+        is_header_only = conversion == 'copy_header'
+        is_meta_copy = conversion == 'copy_metadata'
+        is_meta_only = is_header_only or is_meta_copy
+        is_consume = conversion == 'convert' or conversion is None
+        is_shallow_copy = conversion == 'shallow_copy' or is_header_only
+        is_deep_copy = conversion == 'copy' or is_meta_copy
+        # Verify mode
+        if not (is_consume or is_shallow_copy or is_deep_copy):
+            raise ValueError(
+                "Unknown mode argument %s. Supported modes are: 'consume', 'copy', or 'shallow_copy'".format(
+                    conversion
+                ))
+        if not reader._dtypes.is_ieee and not is_shallow_copy:
+            # Can't copy/consume non-Intel files due to the uncertainty of converting parameter data.
+            raise ValueError(
+                "File was read in %s format and only 'shallow_copy' mode is supported for non Intel files!".format(
+                    reader._dtypes.proc_type
+                ))
+
+        if is_consume:
+            writer._header = reader._header
+            writer._groups = reader._groups
+        elif is_deep_copy:
+            writer._header = copy.deepcopy(reader._header)
+            writer._groups = copy.deepcopy(reader._groups)
+        elif is_shallow_copy:
+            # Only copy header (no groups)
+            writer._header = copy.deepcopy(reader._header)
+            # Reformat header events
+            writer._header.encode_events(writer._header.events)
+
+            # Transfer a minimal set parameters
+            writer.set_start_frame(reader.first_frame)
+            writer.set_point_labels(reader.point_labels)
+            writer.set_analog_labels(reader.analog_labels)
+
+            gen_scale, analog_scales, analog_offsets = reader.get_analog_transform_parameters()
+            writer.set_analog_general_scale(gen_scale)
+            writer.set_analog_scales(analog_scales)
+            writer.set_analog_offsets(analog_offsets)
+
+        if not is_meta_only:
+            # Copy frames
+            for (i, point, analog) in reader.read_frames(copy=True, camera_sum=False):
+                writer.add_frames((point, analog))
+        if is_consume:
+            # Cleanup
+            reader._header = None
+            reader._groups = None
+            del reader
+        return writer
+
+    @property
+    def analog_rate(self):
+        return super(Writer, self).analog_rate
+
+    @analog_rate.setter
+    def analog_rate(self, value):
+        per_frame_rate = value / self.point_rate
+        assert float(per_frame_rate).is_integer(), "Analog rate must be a multiple of the point rate."
+        self._header.analog_per_frame = np.uint16(per_frame_rate)
+
+    @property
+    def numeric_key_max(self):
+        ''' Get the largest numeric key.
+        '''
+        num = 0
+        if len(self._groups) > 0:
+            for i in self._groups.keys():
+                if isinstance(i, int):
+                    num = max(i, num)
+        return num
+
+    @property
+    def numeric_key_next(self):
+        ''' Get a new unique numeric group key.
+        '''
+        return self.numeric_key_max + 1
+
+    def get_create(self, label):
+        ''' Get or create a parameter `c3d.group.Group`.'''
+        label = label.upper()
+        group = self.get(label)
+        if group is None:
+            group = self.add_group(self.numeric_key_next, label, label + ' group')
+        return group
+
+    @property
+    def point_group(self):
+        ''' Get or create the POINT parameter group.'''
+        return self.get_create('POINT')
+
+    @property
+    def analog_group(self):
+        ''' Get or create the ANALOG parameter group.'''
+        return self.get_create('ANALOG')
+
+    @property
+    def trial_group(self):
+        ''' Get or create the TRIAL parameter group.'''
+        return self.get_create('TRIAL')
+
+    def add_group(self, group_id, name, desc):
+        '''Add a new parameter group. See Manager.add_group() for more information.
+
+        Returns
+        -------
+        group : `c3d.group.Group`
+            An editable group instance.
+        '''
+        return super(Writer, self)._add_group(group_id, name, desc)
+
+    def rename_group(self, *args):
+        ''' Rename a specified parameter group (see Manager._rename_group for args). '''
+        super(Writer, self)._rename_group(*args)
+
+    def remove_group(self, *args):
+        '''Remove the parameter group. (see Manager._rename_group for args). '''
+        super(Writer, self)._remove_group(*args)
+
+    def add_frames(self, frames, index=None):
+        '''Add frames to this writer instance.
+
+        Parameters
+        ----------
+        frames : Single or sequence of (point, analog) pairs
+            A sequence or frame of frame data to add to the writer.
+        index : int or None
+            Insert the frame or sequence at the index (the first sequence frame will be inserted at the given `index`).
+            Note that the index should be relative to 0 rather then the frame number provided by read_frames()!
+        '''
+        sh = np.shape(frames)
+        # Single frame
+        if len(sh) < 2:
+            frames = [frames]
+            sh = np.shape(frames)
+
+        # Check data shapes match
+        if len(self._frames) > 0:
+            point0, analog0 = self._frames[0]
+            psh, ash = np.shape(point0), np.shape(analog0)
+            for f in frames:
+                if np.shape(f[0]) != psh:
+                    raise ValueError(
+                        'Shape of analog data does not previous frames. Expexted shape {}, was {}.'.format(
+                            str(psh), str(np.shape(f[0]))
+                        ))
+                if np.shape(f[1]) != ash:
+                    raise ValueError(
+                        'Shape of analog data does not previous frames. Expexted shape {}, was {}.'.format(
+                            str(ash), str(np.shape(f[1]))
+                        ))
+
+        # Sequence of invalid shape
+        if sh[1] != 2:
+            raise ValueError(
+                'Expected frame input to be sequence of point and analog pairs on form (None, 2). ' +
+                'Input was of shape {}.'.format(str(sh)))
+
+        if index is not None:
+            self._frames[index:index] = frames
+        else:
+            self._frames.extend(frames)
+
+    def set_point_labels(self, labels):
+        ''' Set point data labels.
+
+        Parameters
+        ----------
+        labels : iterable
+            Set POINT:LABELS parameter entry from a set of string labels.
+        '''
+        grp = self.point_group
+        if labels is None:
+            grp.add_empty_array('LABELS', 'Point labels.')
+        else:
+            label_str, label_max_size = utils.pack_labels(labels)
+            grp.add_str('LABELS', 'Point labels.', label_str, label_max_size, len(labels))
+
+    def set_analog_labels(self, labels):
+        ''' Set analog data labels.
+
+        Parameters
+        ----------
+        labels : iterable
+            Set ANALOG:LABELS parameter entry from a set of string labels.
+        '''
+        grp = self.analog_group
+        if labels is None:
+            grp.add_empty_array('LABELS', 'Analog labels.')
+        else:
+            label_str, label_max_size = utils.pack_labels(labels)
+            grp.add_str('LABELS', 'Analog labels.', label_str, label_max_size, len(labels))
+
+    def set_analog_general_scale(self, value):
+        ''' Set ANALOG:GEN_SCALE factor (uniform analog scale factor).
+        '''
+        self.analog_group.set('GEN_SCALE', 'Analog general scale factor', 4, '<f', value)
+
+    def set_analog_scales(self, values):
+        ''' Set ANALOG:SCALE factors (per channel scale factor).
+
+        Parameters
+        ----------
+        values : iterable or None
+            Iterable containing individual scale factors (float32) for scaling analog channel data.
+        '''
+        if utils.is_iterable(values):
+            data = np.array([v for v in values], dtype=np.float32)
+            self.analog_group.set_array('SCALE', 'Analog channel scale factors', data)
+        elif values is None:
+            self.analog_group.set_empty_array('SCALE', 'Analog channel scale factors')
+        else:
+            raise ValueError('Expected iterable containing analog scale factors.')
+
+    def set_analog_offsets(self, values):
+        ''' Set ANALOG:OFFSET offsets (per channel offset).
+
+        Parameters
+        ----------
+        values : iterable or None
+            Iterable containing individual offsets (int16) for encoding analog channel data.
+        '''
+        if utils.is_iterable(values):
+            data = np.array([v for v in values], dtype=np.int16)
+            self.analog_group.set_array('OFFSET', 'Analog channel offsets', data)
+        elif values is None:
+            self.analog_group.set_empty_array('OFFSET', 'Analog channel offsets')
+        else:
+            raise ValueError('Expected iterable containing analog data offsets.')
+
+    def set_start_frame(self, frame=1):
+        ''' Set the 'TRIAL:ACTUAL_START_FIELD' parameter and header.first_frame entry.
+
+        Parameters
+        ----------
+        frame : int
+            Number for the first frame recorded in the file.
+            Frame counter for a trial recording always start at 1 for the first frame.
+        '''
+        self.trial_group.set('ACTUAL_START_FIELD', 'Actual start frame', 2, '<I', frame, 2)
+        if frame < 65535:
+            self._header.first_frame = np.uint16(frame)
+        else:
+            self._header.first_frame = np.uint16(65535)
+
+    def _set_last_frame(self, frame):
+        ''' Sets the 'TRIAL:ACTUAL_END_FIELD' parameter and header.last_frame entry.
+        '''
+        self.trial_group.set('ACTUAL_END_FIELD', 'Actual end frame', 2, '<I', frame, 2)
+        self._header.last_frame = np.uint16(min(frame, 65535))
+
+    def set_screen_axis(self, X='+X', Y='+Y'):
+        ''' Set the X_SCREEN and Y_SCREEN parameters in the POINT group.
+
+        Parameters
+        ----------
+        X : str
+            Two byte string with first character indicating positive or negative axis (+/-),
+            and the second axis (X/Y/Z). Example strings '+X' or '-Y'
+        Y : str
+            Second axis string with same format as Y. Determines the second Y screen axis.
+        '''
+        if len(X) != 2:
+            raise ValueError('Expected string literal to be a 2 character string for the X_SCREEN parameter.')
+        if len(Y) != 2:
+            raise ValueError('Expected string literal to be a 2 character string for the Y_SCREEN parameter.')
+        group = self.point_group
+        group.set_str('X_SCREEN', 'X_SCREEN parameter', X)
+        group.set_str('Y_SCREEN', 'Y_SCREEN parameter', Y)
+
+    def write(self, handle):
+        '''Write metadata, point and analog frames to a file handle.
+
+        Parameters
+        ----------
+        handle : file
+            Write metadata and C3D motion frames to the given file handle. The
+            writer does not close the handle.
+        '''
+        if not self._frames:
+            raise RuntimeError('Attempted to write empty file.')
+
+        points, analog = self._frames[0]
+        ppf = len(points)
+        apf = len(analog)
+
+        first_frame = self.first_frame
+        if first_frame <= 0:  # Bad value
+            first_frame = 1
+        nframes = len(self._frames)
+        last_frame = first_frame + nframes - 1
+
+        UINT16_MAX = 65535
+
+        # POINT group
+        group = self.point_group
+        group.set('USED', 'Number of point samples', 2, '<H', ppf)
+        group.set('FRAMES', 'Total frame count', 2, '<H', min(UINT16_MAX, nframes))
+        if nframes >= UINT16_MAX:
+            # Should be floating point
+            group.set('LONG_FRAMES', 'Total frame count', 4, '<f', nframes)
+        elif 'LONG_FRAMES' in group:
+            # Docs states it should not exist if frame_count < 65535
+            group.remove_param('LONG_FRAMES')
+        group.set('DATA_START', 'First data block containing frame samples.', 2, '<H', 0)
+        group.set('SCALE', 'Point data scaling factor', 4, '<f', self.point_scale)
+        group.set('RATE', 'Point data sample rate', 4, '<f', self.point_rate)
+        # Optional
+        if 'UNITS' not in group:
+            group.add_str('UNITS', 'Units used for point data measurements.', 'mm')
+        if 'DESCRIPTIONS' not in group:
+            group.add_str('DESCRIPTIONS', 'Channel descriptions.', '  ' * ppf, 2, ppf)
+
+        # ANALOG group
+        group = self.analog_group
+        group.set('USED', 'Analog channel count', 2, '<H', apf)
+        group.set('RATE', 'Analog samples per second', 4, '<f', self.analog_rate)
+        if 'GEN_SCALE' not in group:
+            self.set_analog_general_scale(1.0)
+        # Optional
+        if 'SCALE' not in group:
+            self.set_analog_scales(None)
+        if 'OFFSET' not in group:
+            self.set_analog_offsets(None)
+        if 'DESCRIPTIONS' not in group:
+            group.add_str('DESCRIPTIONS', 'Channel descriptions.', '  ' * apf, 2, apf)
+
+        # TRIAL group
+        self.set_start_frame(first_frame)
+        self._set_last_frame(last_frame)
+
+        # sync parameter information to header.
+        start_block = self.parameter_blocks() + 2
+        self.get('POINT:DATA_START').bytes = struct.pack('<H', start_block)
+        self._header.data_block = np.uint16(start_block)
+        self._header.point_count = np.uint16(ppf)
+        self._header.analog_count = np.uint16(np.prod(np.shape(analog)))
+
+        self._write_metadata(handle)
+        self._write_frames(handle)
+
+    def _pad_block(self, handle):
+        '''Pad the file with 0s to the end of the next block boundary.'''
+        extra = handle.tell() % 512
+        if extra:
+            handle.write(b'\x00' * (512 - extra))
+
+    def _write_metadata(self, handle):
+        '''Write metadata to a file handle.
+
+        Parameters
+        ----------
+        handle : file
+            Write metadata and C3D motion frames to the given file handle. The
+            writer does not close the handle.
+        '''
+        self._check_metadata()
+
+        # Header
+        self._header.write(handle)
+        self._pad_block(handle)
+        assert handle.tell() == 512
+
+        # Groups
+        handle.write(struct.pack(
+            'BBBB', 0, 0, self.parameter_blocks(), self._dtypes.processor))
+        for group_id, group in self.listed():
+            group._data.write(group_id, handle)
+
+        # Padding
+        self._pad_block(handle)
+        while handle.tell() != 512 * (self.header.data_block - 1):
+            handle.write(b'\x00' * 512)
+
+    def _write_frames(self, handle):
+        '''Write our frame data to the given file handle.
+
+        Parameters
+        ----------
+        handle : file
+            Write metadata and C3D motion frames to the given file handle. The
+            writer does not close the handle.
+        '''
+        assert handle.tell() == 512 * (self._header.data_block - 1)
+        scale_mag = abs(self.point_scale)
+        is_float = self.point_scale < 0
+        if is_float:
+            point_dtype = self._dtypes.float32
+            point_scale = 1.0
+        else:
+            point_dtype = self._dtypes.int16
+            point_scale = scale_mag
+        raw = np.zeros((self.point_used, 4), point_dtype)
+
+        analog_scales, analog_offsets = self.get_analog_transform()
+        analog_scales_inv = 1.0 / analog_scales
+
+        for points, analog in self._frames:
+            # Transform point data
+            valid = points[:, 3] >= 0.0
+            raw[~valid, 3] = -1
+            raw[valid, :3] = points[valid, :3] / point_scale
+            raw[valid, 3] = np.bitwise_or(np.rint(points[valid, 3] / scale_mag).astype(np.uint8),
+                                          (points[valid, 4].astype(np.uint16) << 8),
+                                          dtype=np.uint16)
+
+            # Transform analog data
+            analog = analog * analog_scales_inv + analog_offsets
+            analog = analog.T
+
+            # Write
+            analog = analog.astype(point_dtype)
+            handle.write(raw.tobytes())
+            handle.write(analog.tobytes())
+        self._pad_block(handle)
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Writer +(point_rate=480.0, analog_rate=0.0, point_scale=-1.0) +
+
+

This class writes metadata and frames to a C3D file.

+

For example, to read an existing C3D file, apply some sort of data +processing to the frames, and write out another C3D file::

+
>>> r = c3d.Reader(open('data.c3d', 'rb'))
+>>> w = c3d.Writer()
+>>> w.add_frames(process_frames_somehow(r.read_frames()))
+>>> with open('smoothed.c3d', 'wb') as handle:
+>>>     w.write(handle)
+
+

Parameters

+
+
point_rate : float, optional
+
The frame rate of the data. Defaults to 480.
+
analog_rate : float, optional
+
The number of analog samples per frame. Defaults to 0.
+
point_scale : float, optional
+
The scale factor for point data. Defaults to -1 (i.e., "check the +POINT:SCALE parameter").
+
point_units : str, optional
+
The units that the point numbers represent. Defaults to 'mm +'.
+
gen_scale : float, optional
+
General scaling factor for analog data. Defaults to 1.
+
+

Set minimal metadata for this writer.

+
+ +Expand source code + +
class Writer(Manager):
+    '''This class writes metadata and frames to a C3D file.
+
+    For example, to read an existing C3D file, apply some sort of data
+    processing to the frames, and write out another C3D file::
+
+    >>> r = c3d.Reader(open('data.c3d', 'rb'))
+    >>> w = c3d.Writer()
+    >>> w.add_frames(process_frames_somehow(r.read_frames()))
+    >>> with open('smoothed.c3d', 'wb') as handle:
+    >>>     w.write(handle)
+
+    Parameters
+    ----------
+    point_rate : float, optional
+        The frame rate of the data. Defaults to 480.
+    analog_rate : float, optional
+        The number of analog samples per frame. Defaults to 0.
+    point_scale : float, optional
+        The scale factor for point data. Defaults to -1 (i.e., "check the
+        POINT:SCALE parameter").
+    point_units : str, optional
+        The units that the point numbers represent. Defaults to ``'mm  '``.
+    gen_scale : float, optional
+        General scaling factor for analog data. Defaults to 1.
+    '''
+
+    def __init__(self,
+                 point_rate=480.,
+                 analog_rate=0.,
+                 point_scale=-1.):
+        '''Set minimal metadata for this writer.
+
+        '''
+        self._dtypes = DataTypes()  # Only support INTEL format from writing
+        super(Writer, self).__init__()
+
+        # Header properties
+        self._header.frame_rate = np.float32(point_rate)
+        self._header.scale_factor = np.float32(point_scale)
+        self.analog_rate = analog_rate
+        self._frames = []
+
+    @staticmethod
+    def from_reader(reader, conversion=None):
+        ''' Convert a `c3d.reader.Reader` to a persistent `c3d.writer.Writer` instance.
+
+        Parameters
+        ----------
+        source : `c3d.reader.Reader`
+            Source to copy data from.
+        conversion : str
+            Conversion mode, None is equivalent to the default mode. Supported modes are:
+
+                'convert'       - (Default) Convert the Reader to a Writer
+                                  instance and explicitly delete the Reader.
+
+                'copy'          - Reader objects will be deep copied.
+
+                'copy_metadata' - Similar to 'copy' but only copies metadata and
+                                  not point and analog frame data.
+
+                'copy_shallow'  - Similar to 'copy' but group parameters are
+                                  not copied.
+
+                'copy_header'   - Similar to 'copy_shallow' but only the
+                                  header is copied (frame data is not copied).
+
+        Returns
+        -------
+        param : `c3d.writer.Writer`
+            A writeable and persistent representation of the `c3d.reader.Reader` object.
+
+        Raises
+        ------
+        ValueError
+            If mode string is not equivalent to one of the supported modes.
+            If attempting to convert non-Intel files using mode other than 'shallow_copy'.
+        '''
+        writer = Writer()
+        # Modes
+        is_header_only = conversion == 'copy_header'
+        is_meta_copy = conversion == 'copy_metadata'
+        is_meta_only = is_header_only or is_meta_copy
+        is_consume = conversion == 'convert' or conversion is None
+        is_shallow_copy = conversion == 'shallow_copy' or is_header_only
+        is_deep_copy = conversion == 'copy' or is_meta_copy
+        # Verify mode
+        if not (is_consume or is_shallow_copy or is_deep_copy):
+            raise ValueError(
+                "Unknown mode argument %s. Supported modes are: 'consume', 'copy', or 'shallow_copy'".format(
+                    conversion
+                ))
+        if not reader._dtypes.is_ieee and not is_shallow_copy:
+            # Can't copy/consume non-Intel files due to the uncertainty of converting parameter data.
+            raise ValueError(
+                "File was read in %s format and only 'shallow_copy' mode is supported for non Intel files!".format(
+                    reader._dtypes.proc_type
+                ))
+
+        if is_consume:
+            writer._header = reader._header
+            writer._groups = reader._groups
+        elif is_deep_copy:
+            writer._header = copy.deepcopy(reader._header)
+            writer._groups = copy.deepcopy(reader._groups)
+        elif is_shallow_copy:
+            # Only copy header (no groups)
+            writer._header = copy.deepcopy(reader._header)
+            # Reformat header events
+            writer._header.encode_events(writer._header.events)
+
+            # Transfer a minimal set parameters
+            writer.set_start_frame(reader.first_frame)
+            writer.set_point_labels(reader.point_labels)
+            writer.set_analog_labels(reader.analog_labels)
+
+            gen_scale, analog_scales, analog_offsets = reader.get_analog_transform_parameters()
+            writer.set_analog_general_scale(gen_scale)
+            writer.set_analog_scales(analog_scales)
+            writer.set_analog_offsets(analog_offsets)
+
+        if not is_meta_only:
+            # Copy frames
+            for (i, point, analog) in reader.read_frames(copy=True, camera_sum=False):
+                writer.add_frames((point, analog))
+        if is_consume:
+            # Cleanup
+            reader._header = None
+            reader._groups = None
+            del reader
+        return writer
+
+    @property
+    def analog_rate(self):
+        return super(Writer, self).analog_rate
+
+    @analog_rate.setter
+    def analog_rate(self, value):
+        per_frame_rate = value / self.point_rate
+        assert float(per_frame_rate).is_integer(), "Analog rate must be a multiple of the point rate."
+        self._header.analog_per_frame = np.uint16(per_frame_rate)
+
+    @property
+    def numeric_key_max(self):
+        ''' Get the largest numeric key.
+        '''
+        num = 0
+        if len(self._groups) > 0:
+            for i in self._groups.keys():
+                if isinstance(i, int):
+                    num = max(i, num)
+        return num
+
+    @property
+    def numeric_key_next(self):
+        ''' Get a new unique numeric group key.
+        '''
+        return self.numeric_key_max + 1
+
+    def get_create(self, label):
+        ''' Get or create a parameter `c3d.group.Group`.'''
+        label = label.upper()
+        group = self.get(label)
+        if group is None:
+            group = self.add_group(self.numeric_key_next, label, label + ' group')
+        return group
+
+    @property
+    def point_group(self):
+        ''' Get or create the POINT parameter group.'''
+        return self.get_create('POINT')
+
+    @property
+    def analog_group(self):
+        ''' Get or create the ANALOG parameter group.'''
+        return self.get_create('ANALOG')
+
+    @property
+    def trial_group(self):
+        ''' Get or create the TRIAL parameter group.'''
+        return self.get_create('TRIAL')
+
+    def add_group(self, group_id, name, desc):
+        '''Add a new parameter group. See Manager.add_group() for more information.
+
+        Returns
+        -------
+        group : `c3d.group.Group`
+            An editable group instance.
+        '''
+        return super(Writer, self)._add_group(group_id, name, desc)
+
+    def rename_group(self, *args):
+        ''' Rename a specified parameter group (see Manager._rename_group for args). '''
+        super(Writer, self)._rename_group(*args)
+
+    def remove_group(self, *args):
+        '''Remove the parameter group. (see Manager._rename_group for args). '''
+        super(Writer, self)._remove_group(*args)
+
+    def add_frames(self, frames, index=None):
+        '''Add frames to this writer instance.
+
+        Parameters
+        ----------
+        frames : Single or sequence of (point, analog) pairs
+            A sequence or frame of frame data to add to the writer.
+        index : int or None
+            Insert the frame or sequence at the index (the first sequence frame will be inserted at the given `index`).
+            Note that the index should be relative to 0 rather then the frame number provided by read_frames()!
+        '''
+        sh = np.shape(frames)
+        # Single frame
+        if len(sh) < 2:
+            frames = [frames]
+            sh = np.shape(frames)
+
+        # Check data shapes match
+        if len(self._frames) > 0:
+            point0, analog0 = self._frames[0]
+            psh, ash = np.shape(point0), np.shape(analog0)
+            for f in frames:
+                if np.shape(f[0]) != psh:
+                    raise ValueError(
+                        'Shape of analog data does not previous frames. Expexted shape {}, was {}.'.format(
+                            str(psh), str(np.shape(f[0]))
+                        ))
+                if np.shape(f[1]) != ash:
+                    raise ValueError(
+                        'Shape of analog data does not previous frames. Expexted shape {}, was {}.'.format(
+                            str(ash), str(np.shape(f[1]))
+                        ))
+
+        # Sequence of invalid shape
+        if sh[1] != 2:
+            raise ValueError(
+                'Expected frame input to be sequence of point and analog pairs on form (None, 2). ' +
+                'Input was of shape {}.'.format(str(sh)))
+
+        if index is not None:
+            self._frames[index:index] = frames
+        else:
+            self._frames.extend(frames)
+
+    def set_point_labels(self, labels):
+        ''' Set point data labels.
+
+        Parameters
+        ----------
+        labels : iterable
+            Set POINT:LABELS parameter entry from a set of string labels.
+        '''
+        grp = self.point_group
+        if labels is None:
+            grp.add_empty_array('LABELS', 'Point labels.')
+        else:
+            label_str, label_max_size = utils.pack_labels(labels)
+            grp.add_str('LABELS', 'Point labels.', label_str, label_max_size, len(labels))
+
+    def set_analog_labels(self, labels):
+        ''' Set analog data labels.
+
+        Parameters
+        ----------
+        labels : iterable
+            Set ANALOG:LABELS parameter entry from a set of string labels.
+        '''
+        grp = self.analog_group
+        if labels is None:
+            grp.add_empty_array('LABELS', 'Analog labels.')
+        else:
+            label_str, label_max_size = utils.pack_labels(labels)
+            grp.add_str('LABELS', 'Analog labels.', label_str, label_max_size, len(labels))
+
+    def set_analog_general_scale(self, value):
+        ''' Set ANALOG:GEN_SCALE factor (uniform analog scale factor).
+        '''
+        self.analog_group.set('GEN_SCALE', 'Analog general scale factor', 4, '<f', value)
+
+    def set_analog_scales(self, values):
+        ''' Set ANALOG:SCALE factors (per channel scale factor).
+
+        Parameters
+        ----------
+        values : iterable or None
+            Iterable containing individual scale factors (float32) for scaling analog channel data.
+        '''
+        if utils.is_iterable(values):
+            data = np.array([v for v in values], dtype=np.float32)
+            self.analog_group.set_array('SCALE', 'Analog channel scale factors', data)
+        elif values is None:
+            self.analog_group.set_empty_array('SCALE', 'Analog channel scale factors')
+        else:
+            raise ValueError('Expected iterable containing analog scale factors.')
+
+    def set_analog_offsets(self, values):
+        ''' Set ANALOG:OFFSET offsets (per channel offset).
+
+        Parameters
+        ----------
+        values : iterable or None
+            Iterable containing individual offsets (int16) for encoding analog channel data.
+        '''
+        if utils.is_iterable(values):
+            data = np.array([v for v in values], dtype=np.int16)
+            self.analog_group.set_array('OFFSET', 'Analog channel offsets', data)
+        elif values is None:
+            self.analog_group.set_empty_array('OFFSET', 'Analog channel offsets')
+        else:
+            raise ValueError('Expected iterable containing analog data offsets.')
+
+    def set_start_frame(self, frame=1):
+        ''' Set the 'TRIAL:ACTUAL_START_FIELD' parameter and header.first_frame entry.
+
+        Parameters
+        ----------
+        frame : int
+            Number for the first frame recorded in the file.
+            Frame counter for a trial recording always start at 1 for the first frame.
+        '''
+        self.trial_group.set('ACTUAL_START_FIELD', 'Actual start frame', 2, '<I', frame, 2)
+        if frame < 65535:
+            self._header.first_frame = np.uint16(frame)
+        else:
+            self._header.first_frame = np.uint16(65535)
+
+    def _set_last_frame(self, frame):
+        ''' Sets the 'TRIAL:ACTUAL_END_FIELD' parameter and header.last_frame entry.
+        '''
+        self.trial_group.set('ACTUAL_END_FIELD', 'Actual end frame', 2, '<I', frame, 2)
+        self._header.last_frame = np.uint16(min(frame, 65535))
+
+    def set_screen_axis(self, X='+X', Y='+Y'):
+        ''' Set the X_SCREEN and Y_SCREEN parameters in the POINT group.
+
+        Parameters
+        ----------
+        X : str
+            Two byte string with first character indicating positive or negative axis (+/-),
+            and the second axis (X/Y/Z). Example strings '+X' or '-Y'
+        Y : str
+            Second axis string with same format as Y. Determines the second Y screen axis.
+        '''
+        if len(X) != 2:
+            raise ValueError('Expected string literal to be a 2 character string for the X_SCREEN parameter.')
+        if len(Y) != 2:
+            raise ValueError('Expected string literal to be a 2 character string for the Y_SCREEN parameter.')
+        group = self.point_group
+        group.set_str('X_SCREEN', 'X_SCREEN parameter', X)
+        group.set_str('Y_SCREEN', 'Y_SCREEN parameter', Y)
+
+    def write(self, handle):
+        '''Write metadata, point and analog frames to a file handle.
+
+        Parameters
+        ----------
+        handle : file
+            Write metadata and C3D motion frames to the given file handle. The
+            writer does not close the handle.
+        '''
+        if not self._frames:
+            raise RuntimeError('Attempted to write empty file.')
+
+        points, analog = self._frames[0]
+        ppf = len(points)
+        apf = len(analog)
+
+        first_frame = self.first_frame
+        if first_frame <= 0:  # Bad value
+            first_frame = 1
+        nframes = len(self._frames)
+        last_frame = first_frame + nframes - 1
+
+        UINT16_MAX = 65535
+
+        # POINT group
+        group = self.point_group
+        group.set('USED', 'Number of point samples', 2, '<H', ppf)
+        group.set('FRAMES', 'Total frame count', 2, '<H', min(UINT16_MAX, nframes))
+        if nframes >= UINT16_MAX:
+            # Should be floating point
+            group.set('LONG_FRAMES', 'Total frame count', 4, '<f', nframes)
+        elif 'LONG_FRAMES' in group:
+            # Docs states it should not exist if frame_count < 65535
+            group.remove_param('LONG_FRAMES')
+        group.set('DATA_START', 'First data block containing frame samples.', 2, '<H', 0)
+        group.set('SCALE', 'Point data scaling factor', 4, '<f', self.point_scale)
+        group.set('RATE', 'Point data sample rate', 4, '<f', self.point_rate)
+        # Optional
+        if 'UNITS' not in group:
+            group.add_str('UNITS', 'Units used for point data measurements.', 'mm')
+        if 'DESCRIPTIONS' not in group:
+            group.add_str('DESCRIPTIONS', 'Channel descriptions.', '  ' * ppf, 2, ppf)
+
+        # ANALOG group
+        group = self.analog_group
+        group.set('USED', 'Analog channel count', 2, '<H', apf)
+        group.set('RATE', 'Analog samples per second', 4, '<f', self.analog_rate)
+        if 'GEN_SCALE' not in group:
+            self.set_analog_general_scale(1.0)
+        # Optional
+        if 'SCALE' not in group:
+            self.set_analog_scales(None)
+        if 'OFFSET' not in group:
+            self.set_analog_offsets(None)
+        if 'DESCRIPTIONS' not in group:
+            group.add_str('DESCRIPTIONS', 'Channel descriptions.', '  ' * apf, 2, apf)
+
+        # TRIAL group
+        self.set_start_frame(first_frame)
+        self._set_last_frame(last_frame)
+
+        # sync parameter information to header.
+        start_block = self.parameter_blocks() + 2
+        self.get('POINT:DATA_START').bytes = struct.pack('<H', start_block)
+        self._header.data_block = np.uint16(start_block)
+        self._header.point_count = np.uint16(ppf)
+        self._header.analog_count = np.uint16(np.prod(np.shape(analog)))
+
+        self._write_metadata(handle)
+        self._write_frames(handle)
+
+    def _pad_block(self, handle):
+        '''Pad the file with 0s to the end of the next block boundary.'''
+        extra = handle.tell() % 512
+        if extra:
+            handle.write(b'\x00' * (512 - extra))
+
+    def _write_metadata(self, handle):
+        '''Write metadata to a file handle.
+
+        Parameters
+        ----------
+        handle : file
+            Write metadata and C3D motion frames to the given file handle. The
+            writer does not close the handle.
+        '''
+        self._check_metadata()
+
+        # Header
+        self._header.write(handle)
+        self._pad_block(handle)
+        assert handle.tell() == 512
+
+        # Groups
+        handle.write(struct.pack(
+            'BBBB', 0, 0, self.parameter_blocks(), self._dtypes.processor))
+        for group_id, group in self.listed():
+            group._data.write(group_id, handle)
+
+        # Padding
+        self._pad_block(handle)
+        while handle.tell() != 512 * (self.header.data_block - 1):
+            handle.write(b'\x00' * 512)
+
+    def _write_frames(self, handle):
+        '''Write our frame data to the given file handle.
+
+        Parameters
+        ----------
+        handle : file
+            Write metadata and C3D motion frames to the given file handle. The
+            writer does not close the handle.
+        '''
+        assert handle.tell() == 512 * (self._header.data_block - 1)
+        scale_mag = abs(self.point_scale)
+        is_float = self.point_scale < 0
+        if is_float:
+            point_dtype = self._dtypes.float32
+            point_scale = 1.0
+        else:
+            point_dtype = self._dtypes.int16
+            point_scale = scale_mag
+        raw = np.zeros((self.point_used, 4), point_dtype)
+
+        analog_scales, analog_offsets = self.get_analog_transform()
+        analog_scales_inv = 1.0 / analog_scales
+
+        for points, analog in self._frames:
+            # Transform point data
+            valid = points[:, 3] >= 0.0
+            raw[~valid, 3] = -1
+            raw[valid, :3] = points[valid, :3] / point_scale
+            raw[valid, 3] = np.bitwise_or(np.rint(points[valid, 3] / scale_mag).astype(np.uint8),
+                                          (points[valid, 4].astype(np.uint16) << 8),
+                                          dtype=np.uint16)
+
+            # Transform analog data
+            analog = analog * analog_scales_inv + analog_offsets
+            analog = analog.T
+
+            # Write
+            analog = analog.astype(point_dtype)
+            handle.write(raw.tobytes())
+            handle.write(analog.tobytes())
+        self._pad_block(handle)
+
+

Ancestors

+ +

Static methods

+
+
+def from_reader(reader, conversion=None) +
+
+

Convert a Reader to a persistent Writer instance.

+

Parameters

+
+
source : Reader
+
Source to copy data from.
+
conversion : str
+
Conversion mode, None is equivalent to the default mode. Supported modes are:
'convert'       - (Default) Convert the Reader to a Writer
+                  instance and explicitly delete the Reader.
+
+'copy'          - Reader objects will be deep copied.
+
+'copy_metadata' - Similar to 'copy' but only copies metadata and
+                  not point and analog frame data.
+
+'copy_shallow'  - Similar to 'copy' but group parameters are
+                  not copied.
+
+'copy_header'   - Similar to 'copy_shallow' but only the
+                  header is copied (frame data is not copied).
+
+
+
+

Returns

+
+
param : Writer
+
A writeable and persistent representation of the Reader object.
+
+

Raises

+
+
ValueError
+
If mode string is not equivalent to one of the supported modes. +If attempting to convert non-Intel files using mode other than 'shallow_copy'.
+
+
+ +Expand source code + +
@staticmethod
+def from_reader(reader, conversion=None):
+    ''' Convert a `c3d.reader.Reader` to a persistent `c3d.writer.Writer` instance.
+
+    Parameters
+    ----------
+    source : `c3d.reader.Reader`
+        Source to copy data from.
+    conversion : str
+        Conversion mode, None is equivalent to the default mode. Supported modes are:
+
+            'convert'       - (Default) Convert the Reader to a Writer
+                              instance and explicitly delete the Reader.
+
+            'copy'          - Reader objects will be deep copied.
+
+            'copy_metadata' - Similar to 'copy' but only copies metadata and
+                              not point and analog frame data.
+
+            'copy_shallow'  - Similar to 'copy' but group parameters are
+                              not copied.
+
+            'copy_header'   - Similar to 'copy_shallow' but only the
+                              header is copied (frame data is not copied).
+
+    Returns
+    -------
+    param : `c3d.writer.Writer`
+        A writeable and persistent representation of the `c3d.reader.Reader` object.
+
+    Raises
+    ------
+    ValueError
+        If mode string is not equivalent to one of the supported modes.
+        If attempting to convert non-Intel files using mode other than 'shallow_copy'.
+    '''
+    writer = Writer()
+    # Modes
+    is_header_only = conversion == 'copy_header'
+    is_meta_copy = conversion == 'copy_metadata'
+    is_meta_only = is_header_only or is_meta_copy
+    is_consume = conversion == 'convert' or conversion is None
+    is_shallow_copy = conversion == 'shallow_copy' or is_header_only
+    is_deep_copy = conversion == 'copy' or is_meta_copy
+    # Verify mode
+    if not (is_consume or is_shallow_copy or is_deep_copy):
+        raise ValueError(
+            "Unknown mode argument %s. Supported modes are: 'consume', 'copy', or 'shallow_copy'".format(
+                conversion
+            ))
+    if not reader._dtypes.is_ieee and not is_shallow_copy:
+        # Can't copy/consume non-Intel files due to the uncertainty of converting parameter data.
+        raise ValueError(
+            "File was read in %s format and only 'shallow_copy' mode is supported for non Intel files!".format(
+                reader._dtypes.proc_type
+            ))
+
+    if is_consume:
+        writer._header = reader._header
+        writer._groups = reader._groups
+    elif is_deep_copy:
+        writer._header = copy.deepcopy(reader._header)
+        writer._groups = copy.deepcopy(reader._groups)
+    elif is_shallow_copy:
+        # Only copy header (no groups)
+        writer._header = copy.deepcopy(reader._header)
+        # Reformat header events
+        writer._header.encode_events(writer._header.events)
+
+        # Transfer a minimal set parameters
+        writer.set_start_frame(reader.first_frame)
+        writer.set_point_labels(reader.point_labels)
+        writer.set_analog_labels(reader.analog_labels)
+
+        gen_scale, analog_scales, analog_offsets = reader.get_analog_transform_parameters()
+        writer.set_analog_general_scale(gen_scale)
+        writer.set_analog_scales(analog_scales)
+        writer.set_analog_offsets(analog_offsets)
+
+    if not is_meta_only:
+        # Copy frames
+        for (i, point, analog) in reader.read_frames(copy=True, camera_sum=False):
+            writer.add_frames((point, analog))
+    if is_consume:
+        # Cleanup
+        reader._header = None
+        reader._groups = None
+        del reader
+    return writer
+
+
+
+

Instance variables

+
+
var analog_group
+
+

Get or create the ANALOG parameter group.

+
+ +Expand source code + +
@property
+def analog_group(self):
+    ''' Get or create the ANALOG parameter group.'''
+    return self.get_create('ANALOG')
+
+
+
var numeric_key_max
+
+

Get the largest numeric key.

+
+ +Expand source code + +
@property
+def numeric_key_max(self):
+    ''' Get the largest numeric key.
+    '''
+    num = 0
+    if len(self._groups) > 0:
+        for i in self._groups.keys():
+            if isinstance(i, int):
+                num = max(i, num)
+    return num
+
+
+
var numeric_key_next
+
+

Get a new unique numeric group key.

+
+ +Expand source code + +
@property
+def numeric_key_next(self):
+    ''' Get a new unique numeric group key.
+    '''
+    return self.numeric_key_max + 1
+
+
+
var point_group
+
+

Get or create the POINT parameter group.

+
+ +Expand source code + +
@property
+def point_group(self):
+    ''' Get or create the POINT parameter group.'''
+    return self.get_create('POINT')
+
+
+
var trial_group
+
+

Get or create the TRIAL parameter group.

+
+ +Expand source code + +
@property
+def trial_group(self):
+    ''' Get or create the TRIAL parameter group.'''
+    return self.get_create('TRIAL')
+
+
+
+

Methods

+
+
+def add_frames(self, frames, index=None) +
+
+

Add frames to this writer instance.

+

Parameters

+
+
frames : Single or sequence of (point, analog) pairs
+
A sequence or frame of frame data to add to the writer.
+
index : int or None
+
Insert the frame or sequence at the index (the first sequence frame will be inserted at the given index). +Note that the index should be relative to 0 rather then the frame number provided by read_frames()!
+
+
+ +Expand source code + +
def add_frames(self, frames, index=None):
+    '''Add frames to this writer instance.
+
+    Parameters
+    ----------
+    frames : Single or sequence of (point, analog) pairs
+        A sequence or frame of frame data to add to the writer.
+    index : int or None
+        Insert the frame or sequence at the index (the first sequence frame will be inserted at the given `index`).
+        Note that the index should be relative to 0 rather then the frame number provided by read_frames()!
+    '''
+    sh = np.shape(frames)
+    # Single frame
+    if len(sh) < 2:
+        frames = [frames]
+        sh = np.shape(frames)
+
+    # Check data shapes match
+    if len(self._frames) > 0:
+        point0, analog0 = self._frames[0]
+        psh, ash = np.shape(point0), np.shape(analog0)
+        for f in frames:
+            if np.shape(f[0]) != psh:
+                raise ValueError(
+                    'Shape of analog data does not previous frames. Expexted shape {}, was {}.'.format(
+                        str(psh), str(np.shape(f[0]))
+                    ))
+            if np.shape(f[1]) != ash:
+                raise ValueError(
+                    'Shape of analog data does not previous frames. Expexted shape {}, was {}.'.format(
+                        str(ash), str(np.shape(f[1]))
+                    ))
+
+    # Sequence of invalid shape
+    if sh[1] != 2:
+        raise ValueError(
+            'Expected frame input to be sequence of point and analog pairs on form (None, 2). ' +
+            'Input was of shape {}.'.format(str(sh)))
+
+    if index is not None:
+        self._frames[index:index] = frames
+    else:
+        self._frames.extend(frames)
+
+
+
+def add_group(self, group_id, name, desc) +
+
+

Add a new parameter group. See Manager.add_group() for more information.

+

Returns

+
+
group : Group
+
An editable group instance.
+
+
+ +Expand source code + +
def add_group(self, group_id, name, desc):
+    '''Add a new parameter group. See Manager.add_group() for more information.
+
+    Returns
+    -------
+    group : `c3d.group.Group`
+        An editable group instance.
+    '''
+    return super(Writer, self)._add_group(group_id, name, desc)
+
+
+
+def get_create(self, label) +
+
+

Get or create a parameter Group.

+
+ +Expand source code + +
def get_create(self, label):
+    ''' Get or create a parameter `c3d.group.Group`.'''
+    label = label.upper()
+    group = self.get(label)
+    if group is None:
+        group = self.add_group(self.numeric_key_next, label, label + ' group')
+    return group
+
+
+
+def remove_group(self, *args) +
+
+

Remove the parameter group. (see Manager._rename_group for args).

+
+ +Expand source code + +
def remove_group(self, *args):
+    '''Remove the parameter group. (see Manager._rename_group for args). '''
+    super(Writer, self)._remove_group(*args)
+
+
+
+def rename_group(self, *args) +
+
+

Rename a specified parameter group (see Manager._rename_group for args).

+
+ +Expand source code + +
def rename_group(self, *args):
+    ''' Rename a specified parameter group (see Manager._rename_group for args). '''
+    super(Writer, self)._rename_group(*args)
+
+
+
+def set_analog_general_scale(self, value) +
+
+

Set ANALOG:GEN_SCALE factor (uniform analog scale factor).

+
+ +Expand source code + +
def set_analog_general_scale(self, value):
+    ''' Set ANALOG:GEN_SCALE factor (uniform analog scale factor).
+    '''
+    self.analog_group.set('GEN_SCALE', 'Analog general scale factor', 4, '<f', value)
+
+
+
+def set_analog_labels(self, labels) +
+
+

Set analog data labels.

+

Parameters

+
+
labels : iterable
+
Set ANALOG:LABELS parameter entry from a set of string labels.
+
+
+ +Expand source code + +
def set_analog_labels(self, labels):
+    ''' Set analog data labels.
+
+    Parameters
+    ----------
+    labels : iterable
+        Set ANALOG:LABELS parameter entry from a set of string labels.
+    '''
+    grp = self.analog_group
+    if labels is None:
+        grp.add_empty_array('LABELS', 'Analog labels.')
+    else:
+        label_str, label_max_size = utils.pack_labels(labels)
+        grp.add_str('LABELS', 'Analog labels.', label_str, label_max_size, len(labels))
+
+
+
+def set_analog_offsets(self, values) +
+
+

Set ANALOG:OFFSET offsets (per channel offset).

+

Parameters

+
+
values : iterable or None
+
Iterable containing individual offsets (int16) for encoding analog channel data.
+
+
+ +Expand source code + +
def set_analog_offsets(self, values):
+    ''' Set ANALOG:OFFSET offsets (per channel offset).
+
+    Parameters
+    ----------
+    values : iterable or None
+        Iterable containing individual offsets (int16) for encoding analog channel data.
+    '''
+    if utils.is_iterable(values):
+        data = np.array([v for v in values], dtype=np.int16)
+        self.analog_group.set_array('OFFSET', 'Analog channel offsets', data)
+    elif values is None:
+        self.analog_group.set_empty_array('OFFSET', 'Analog channel offsets')
+    else:
+        raise ValueError('Expected iterable containing analog data offsets.')
+
+
+
+def set_analog_scales(self, values) +
+
+

Set ANALOG:SCALE factors (per channel scale factor).

+

Parameters

+
+
values : iterable or None
+
Iterable containing individual scale factors (float32) for scaling analog channel data.
+
+
+ +Expand source code + +
def set_analog_scales(self, values):
+    ''' Set ANALOG:SCALE factors (per channel scale factor).
+
+    Parameters
+    ----------
+    values : iterable or None
+        Iterable containing individual scale factors (float32) for scaling analog channel data.
+    '''
+    if utils.is_iterable(values):
+        data = np.array([v for v in values], dtype=np.float32)
+        self.analog_group.set_array('SCALE', 'Analog channel scale factors', data)
+    elif values is None:
+        self.analog_group.set_empty_array('SCALE', 'Analog channel scale factors')
+    else:
+        raise ValueError('Expected iterable containing analog scale factors.')
+
+
+
+def set_point_labels(self, labels) +
+
+

Set point data labels.

+

Parameters

+
+
labels : iterable
+
Set POINT:LABELS parameter entry from a set of string labels.
+
+
+ +Expand source code + +
def set_point_labels(self, labels):
+    ''' Set point data labels.
+
+    Parameters
+    ----------
+    labels : iterable
+        Set POINT:LABELS parameter entry from a set of string labels.
+    '''
+    grp = self.point_group
+    if labels is None:
+        grp.add_empty_array('LABELS', 'Point labels.')
+    else:
+        label_str, label_max_size = utils.pack_labels(labels)
+        grp.add_str('LABELS', 'Point labels.', label_str, label_max_size, len(labels))
+
+
+
+def set_screen_axis(self, X='+X', Y='+Y') +
+
+

Set the X_SCREEN and Y_SCREEN parameters in the POINT group.

+

Parameters

+
+
X : str
+
Two byte string with first character indicating positive or negative axis (+/-), +and the second axis (X/Y/Z). Example strings '+X' or '-Y'
+
Y : str
+
Second axis string with same format as Y. Determines the second Y screen axis.
+
+
+ +Expand source code + +
def set_screen_axis(self, X='+X', Y='+Y'):
+    ''' Set the X_SCREEN and Y_SCREEN parameters in the POINT group.
+
+    Parameters
+    ----------
+    X : str
+        Two byte string with first character indicating positive or negative axis (+/-),
+        and the second axis (X/Y/Z). Example strings '+X' or '-Y'
+    Y : str
+        Second axis string with same format as Y. Determines the second Y screen axis.
+    '''
+    if len(X) != 2:
+        raise ValueError('Expected string literal to be a 2 character string for the X_SCREEN parameter.')
+    if len(Y) != 2:
+        raise ValueError('Expected string literal to be a 2 character string for the Y_SCREEN parameter.')
+    group = self.point_group
+    group.set_str('X_SCREEN', 'X_SCREEN parameter', X)
+    group.set_str('Y_SCREEN', 'Y_SCREEN parameter', Y)
+
+
+
+def set_start_frame(self, frame=1) +
+
+

Set the 'TRIAL:ACTUAL_START_FIELD' parameter and header.first_frame entry.

+

Parameters

+
+
frame : int
+
Number for the first frame recorded in the file. +Frame counter for a trial recording always start at 1 for the first frame.
+
+
+ +Expand source code + +
def set_start_frame(self, frame=1):
+    ''' Set the 'TRIAL:ACTUAL_START_FIELD' parameter and header.first_frame entry.
+
+    Parameters
+    ----------
+    frame : int
+        Number for the first frame recorded in the file.
+        Frame counter for a trial recording always start at 1 for the first frame.
+    '''
+    self.trial_group.set('ACTUAL_START_FIELD', 'Actual start frame', 2, '<I', frame, 2)
+    if frame < 65535:
+        self._header.first_frame = np.uint16(frame)
+    else:
+        self._header.first_frame = np.uint16(65535)
+
+
+
+def write(self, handle) +
+
+

Write metadata, point and analog frames to a file handle.

+

Parameters

+
+
handle : file
+
Write metadata and C3D motion frames to the given file handle. The +writer does not close the handle.
+
+
+ +Expand source code + +
def write(self, handle):
+    '''Write metadata, point and analog frames to a file handle.
+
+    Parameters
+    ----------
+    handle : file
+        Write metadata and C3D motion frames to the given file handle. The
+        writer does not close the handle.
+    '''
+    if not self._frames:
+        raise RuntimeError('Attempted to write empty file.')
+
+    points, analog = self._frames[0]
+    ppf = len(points)
+    apf = len(analog)
+
+    first_frame = self.first_frame
+    if first_frame <= 0:  # Bad value
+        first_frame = 1
+    nframes = len(self._frames)
+    last_frame = first_frame + nframes - 1
+
+    UINT16_MAX = 65535
+
+    # POINT group
+    group = self.point_group
+    group.set('USED', 'Number of point samples', 2, '<H', ppf)
+    group.set('FRAMES', 'Total frame count', 2, '<H', min(UINT16_MAX, nframes))
+    if nframes >= UINT16_MAX:
+        # Should be floating point
+        group.set('LONG_FRAMES', 'Total frame count', 4, '<f', nframes)
+    elif 'LONG_FRAMES' in group:
+        # Docs states it should not exist if frame_count < 65535
+        group.remove_param('LONG_FRAMES')
+    group.set('DATA_START', 'First data block containing frame samples.', 2, '<H', 0)
+    group.set('SCALE', 'Point data scaling factor', 4, '<f', self.point_scale)
+    group.set('RATE', 'Point data sample rate', 4, '<f', self.point_rate)
+    # Optional
+    if 'UNITS' not in group:
+        group.add_str('UNITS', 'Units used for point data measurements.', 'mm')
+    if 'DESCRIPTIONS' not in group:
+        group.add_str('DESCRIPTIONS', 'Channel descriptions.', '  ' * ppf, 2, ppf)
+
+    # ANALOG group
+    group = self.analog_group
+    group.set('USED', 'Analog channel count', 2, '<H', apf)
+    group.set('RATE', 'Analog samples per second', 4, '<f', self.analog_rate)
+    if 'GEN_SCALE' not in group:
+        self.set_analog_general_scale(1.0)
+    # Optional
+    if 'SCALE' not in group:
+        self.set_analog_scales(None)
+    if 'OFFSET' not in group:
+        self.set_analog_offsets(None)
+    if 'DESCRIPTIONS' not in group:
+        group.add_str('DESCRIPTIONS', 'Channel descriptions.', '  ' * apf, 2, apf)
+
+    # TRIAL group
+    self.set_start_frame(first_frame)
+    self._set_last_frame(last_frame)
+
+    # sync parameter information to header.
+    start_block = self.parameter_blocks() + 2
+    self.get('POINT:DATA_START').bytes = struct.pack('<H', start_block)
+    self._header.data_block = np.uint16(start_block)
+    self._header.point_count = np.uint16(ppf)
+    self._header.analog_count = np.uint16(np.prod(np.shape(analog)))
+
+    self._write_metadata(handle)
+    self._write_frames(handle)
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 9d308c6..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,62 +0,0 @@ -import better - -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - # 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.mathjax', - # 'sphinx.ext.pngmath', - 'sphinx.ext.viewcode', - 'numpydoc', - ] -autosummary_generate = True -autodoc_default_flags = ['members'] -numpydoc_show_class_members = False -numpydoc_show_inherited_class_members = True -source_suffix = '.rst' -source_encoding = 'utf-8-sig' -master_doc = 'index' -project = u'C3D' -copyright = u'2015, Leif Johnson' -version = '0.3' -release = '0.3.1pre' -exclude_patterns = ['_build'] -templates_path = ['_templates'] -pygments_style = 'tango' - -html_theme = 'better' -html_theme_path = [better.better_theme_path] -html_theme_options = dict( - rightsidebar=False, - inlinecss='', - cssfiles=['_static/style-tweaks.css'], - showheader=True, - showrelbartop=True, - showrelbarbottom=True, - linktotheme=True, - sidebarwidth='15rem', - textcolor='#111', - headtextcolor='#333', - footertextcolor='#333', - ga_ua='', - ga_domain='', -) -html_short_title = 'Home' -html_static_path = ['_static'] - - -def h(xs): - return ['{}.html'.format(x) for x in xs.split()] - - -html_sidebars = { - 'index': h('gitwidgets globaltoc sourcelink searchbox'), - '**': h('gitwidgets localtoc sourcelink searchbox'), -} - -intersphinx_mapping = { - 'python': ('http://docs.python.org/', None), - 'numpy': ('http://docs.scipy.org/doc/numpy/', None), - 'scipy': ('http://docs.scipy.org/doc/scipy/reference/', None), -} diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..8b1ceba --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,123 @@ +Examples +======== + +Access to data blocks in a .c3d file is provided through the `c3d.reader.Reader` and `c3d.writer.Writer` classes. +Implementation of the examples below are provided in the [examples/] directory in the github repository. + +[examples/]: https://github.com/EmbodiedCognition/py-c3d/tree/master/examples + +Reading +------- + +To read the frames from a C3D file, use a `c3d.reader.Reader` instance: + + import c3d + + with open('my-motion.c3d', 'rb') as file: + reader = c3d.Reader(file) + for i, points, analog in reader.read_frames(): + print('frame {}: point {}, analog {}'.format( + i, points.shape, analog.shape)) + +The method `c3d.reader.Reader.read_frames` generates tuples +containing the trial frame number, a ``numpy`` array of point +data, and a ``numpy`` array of analog data. + +Writing +------- + +To write data frames to a C3D file, use a `c3d.writer.Writer` +instance: + + import c3d + import numpy as np + + # Writes 100 frames recorded at 200 Hz. + # Each frame contains recordings for 24 coordinate markers. + writer = c3d.Writer(point_rate=200) + for _ in range(100): + writer.add_frames((np.random.randn(24, 5), ())) + + writer.set_point_labels(['RFT1', 'RFT2', 'RFT3', 'RFT4', + 'LFT1', 'LFT2', 'LFT3', 'LFT4', + 'RSK1', 'RSK2', 'RSK3', 'RSK4', + 'LSK1', 'LSK2', 'LSK3', 'LSK4', + 'RTH1', 'RTH2', 'RTH3', 'RTH4', + 'LTH1', 'LTH2', 'LTH3', 'LTH4' + ]) + writer.set_analog_labels(None) + + with open('random-points.c3d', 'wb') as h: + writer.write(h) + +The function `c3d.writer.Writer.add_frames` take pairs of ``numpy`` or ``python +arrays``, with the first array in each tuple defining point data and the second +analog data for the frame. Leaving one of the arrays empty indicates +to the writer that no analog --- or point data--- should be included in the file. +References of the data arrays are tracked until `c3d.writer.Writer.write` +is called, which serializes the metadata and data frames into a C3D binary file stream. + +Editing +------- + +Editing c3d files is possible by combining the use of `c3d.reader.Reader` and `c3d.writer.Writer` +instances through the `c3d.reader.Reader.to_writer` method. To edit a file, open a file stream and pass +it to the `c3d.reader.Reader` constructor. Use the `c3d.reader.Reader.to_writer` method to create +an independent `c3d.writer.Writer` instance containing a heap copy of its file contents. +To edit the sequence, one can now reread the data frames from the reader and through inserting the +frames in reverse to create a looped version of the original motion sequence! + + import c3d + + with open('my-motion.c3d', 'rb') as file: + reader = c3d.Reader(file) + writer = reader.to_writer('copy') + for i, points, analog in reader.read_frames(): + writer.add_frames((points, analog), index=reader.frame_count) + + with open('my-looped-motion.c3d', 'wb') as h: + writer.write(h) + + +Accessing metadata +---------------- + +Metadata in a .c3d file exists in two forms, a `c3d.header.Header` and `c3d.parameter.ParamData`. +Reading metadata fields can be done though the `c3d.reader.Reader` but editing requires a +`c3d.writer.Writer` instance. + +`c3d.header.Header` fields can be accessed from the common `c3d.manager.Manager.header` attribute. +Parameters are available through a parameter `c3d.group.Group`, and can be accessed +through the `c3d.manager.Manager.get` and `c3d.group.Group.get` methods: + + group = reader.get('POINT') + param = group.get('LABELS') + +or simply use + + param = reader.get('POINT:LABELS') + +Note that for accessing parameters in the `c3d.reader.Reader`, `c3d.reader.Reader.get` +returns a `c3d.group.GroupReadonly` instance. Convenience functions are provided +for some of the common metadata fields such as `c3d.manager.Manager.frame_count`. +In the case you require specific metadata fields, consider exploring +the [C3D format manual] and/or inspect the file using the c3d-metadata script. + +[C3D format manual]: https://c3d.org/docs/C3D_User_Guide.pdf + +Writing metadata +---------------- + +Once a `c3d.writer.Writer` instance is created, to edit +parameter data `c3d.writer.Writer.get_create` a group: + + group = writer.get_create('ANALOG') + +and to write a float32 entry, use the `c3d.group.Group.add` or `c3d.group.Group.set` functions + + group.set('GEN_SCALE', 'Analog general scale factor', 4, '` instance:: - - import c3d - - reader = c3d.Reader(open('my-motion.c3d', 'rb')) - for i, points, analog in reader.read_frames(): - print('frame {}: point {}, analog {}'.format( - i, points.shape, analog.shape) - -The :func:`read_frames ` method generates tuples -containing the frame index, a ``numpy`` array of point data, and a ``numpy`` -array of analog data. - -Writing -------- - -To write data frames to a C3D file, use a :class:`Writer ` -instance:: - - import c3d - import numpy as np - - writer = c3d.Writer() - for _ in range(100): - writer.add_frame(np.random.randn(30, 5)) - with open('random-points.c3d', 'wb') as h: - writer.write(h) - -The :func:`add_frame ` method takes a ``numpy`` array of -point data---and, optionally, a ``numpy`` array of analog data---and adds it to -an internal data buffer. The :func:`write ` method serializes -all of the frame data to a C3D binary file. - -Command-Line Scripts -==================== - -The ``c3d`` package also contains several command-line scripts. - -Two of these scripts convert C3D binary data to other formats: -- ``c3d2csv``: Converts C3D data to comma-separated values. -- ``c3d2npz``: Converts C3D data to serialized ``numpy`` arrays. - -The ``c3d-metadata`` script simply prints out the metadata from a C3D file. - -Finally, the ``c3d-viewer`` script provides a basic OpenGL viewer for C3D data. -This script depends on ``pyglet``. - -.. toctree:: - :maxdepth: 2 - - reference diff --git a/docs/reference.rst b/docs/reference.rst deleted file mode 100644 index bfbb4e5..0000000 --- a/docs/reference.rst +++ /dev/null @@ -1,13 +0,0 @@ -========= -Reference -========= - -.. automodule:: c3d - :no-members: - :no-inherited-members: - -.. autosummary:: - :toctree: generated/ - - Reader - Writer diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index e10c929..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -numpydoc -sphinx-better-theme diff --git a/examples/edit.py b/examples/edit.py new file mode 100644 index 0000000..c1c9444 --- /dev/null +++ b/examples/edit.py @@ -0,0 +1,21 @@ +import numpy as np +import os +try: + import c3d +except ModuleNotFoundError: + # Force load package with no instalation + import sys + sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..\\')) + import c3d + +# My motion == Sample01.Eb015pi.c3d +file_path = os.path.join(os.path.dirname(__file__), 'my-motion.c3d') + +with open(file_path, 'rb') as file: + reader = c3d.Reader(file) + writer = reader.to_writer('copy') + for i, points, analog in reader.read_frames(): + writer.add_frames((points, analog), index=reader.frame_count) + +with open('my-looped-motion.c3d', 'wb') as h: + writer.write(h) diff --git a/examples/my-motion.c3d b/examples/my-motion.c3d new file mode 100644 index 0000000..c2ab9da Binary files /dev/null and b/examples/my-motion.c3d differ diff --git a/examples/read.py b/examples/read.py new file mode 100644 index 0000000..e85e885 --- /dev/null +++ b/examples/read.py @@ -0,0 +1,18 @@ +import numpy as np +import os +try: + import c3d +except ModuleNotFoundError: + # Force load package with no instalation + import sys + sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..\\')) + import c3d + +# My motion == Sample01.Eb015pi.c3d +file_path = os.path.join(os.path.dirname(__file__), 'my-motion.c3d') + +with open(file_path, 'rb') as file: + reader = c3d.Reader(file) + for i, points, analog in reader.read_frames(): + print('frame {}: point {}, analog {}'.format( + i, points.shape, analog.shape)) diff --git a/examples/write.py b/examples/write.py new file mode 100644 index 0000000..644d770 --- /dev/null +++ b/examples/write.py @@ -0,0 +1,24 @@ +import numpy as np +try: + import c3d +except ModuleNotFoundError: + # Force load package with no instalation + import sys + import os + sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..\\')) + import c3d + +# Writes 100 frames recorded at 200 Hz. +# Each frame contains recordings for 24 coordinate markers. +writer = c3d.Writer(point_rate=200) +for _ in range(100): + writer.add_frames((np.random.randn(24, 5), ())) + +writer.set_point_labels(['RFT1', 'RFT2', 'RFT3', 'RFT4', 'LFT1', 'LFT2', 'LFT3', 'LFT4', + 'RSK1', 'RSK2', 'RSK3', 'RSK4', 'LSK1', 'LSK2', 'LSK3', 'LSK4', + 'RTH1', 'RTH2', 'RTH3', 'RTH4', 'LTH1', 'LTH2', 'LTH3', 'LTH4' + ]) +writer.set_analog_labels(None) + +with open('random-points.c3d', 'wb') as h: + writer.write(h) diff --git a/scripts/README.rst b/scripts/README.rst new file mode 100644 index 0000000..7bfe662 --- /dev/null +++ b/scripts/README.rst @@ -0,0 +1,67 @@ +Scripts are installed with the package and can be executed from a suitable command prompt by:: + + script-name.py 'path-to-c3d-file' + +or from the script directory:: + + py .\script-name.py 'path-to-c3d-file' -options + +The first option only works on Windows if .py files are associated with the python executable (`this answer`_ might help if its not working). + +.. _this answer: https://stackoverflow.com/questions/1934675/how-to-execute-python-scripts-in-windows + + +CSV converter +~~~~~ + +Generate a .csv file from frame data contained in a .c3d file. + +Invoke as:: + + c3d2csv.py 'path-to-c3d-file' -options + +Commandline options :: + + -r : Output column with residual values after point x,y,z coordinate columns. + -c : Output column with camera counts as the last point coordinate column. + -a : Output analog channel values after point coordinates. + -e : Endline string appended after each record (defaults to '\n'). + -s : Separator string inserted between records (defaults to ','). + + +NPZ converter +~~~~~ + +Generate a .npz file from frame data (point and analog) contained in a .c3d file. + +Invoke as:: + + c3d2npz.py 'path-to-c3d-file' + +C3D Viewer +~~~~~ + +Simple 3D-viewer for displaying .c3d data. + +Requirements :: + + pip install pyglet + +Invoke as:: + + c3d-viewer.py 'path-to-c3d-file' + +Interaction commands :: + + Esc : Terminate + Space : Pause + Mouse : Orientate view + +Metadata viewer +~~~~~ + +Prints metadata for a .c3d file to console. + +Invoke as:: + + c3d-metadata.py 'path-to-c3d-file' diff --git a/scripts/c3d-metadata b/scripts/c3d-metadata.py old mode 100755 new mode 100644 similarity index 80% rename from scripts/c3d-metadata rename to scripts/c3d-metadata.py index 2cec59b..388fd6e --- a/scripts/c3d-metadata +++ b/scripts/c3d-metadata.py @@ -4,23 +4,26 @@ from __future__ import print_function -import c3d import argparse import sys +try: + import c3d +except ModuleNotFoundError: + import os + sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..\\')) + import c3d parser = argparse.ArgumentParser(description='Display C3D group and parameter information.') parser.add_argument('input', default='-', metavar='FILE', nargs='+', - help='process C3D data from this input FILE') + help='process C3D data from this input FILE') def print_metadata(reader): print('Header information:\n{}'.format(reader.header)) - groups = ((k, v) for k, v in reader.groups.items() if isinstance(k, str)) - for key, g in sorted(groups): - if not isinstance(key, int): - print('') - for key, p in sorted(g.params.items()): - print_param(g, p) + for key, g in sorted(reader.items()): + print('') + for key, p in sorted(g.items()): + print_param(g, p) def print_param(g, p): @@ -28,7 +31,7 @@ def print_param(g, p): if len(p.dimensions) == 0: val = None - width = len(p.bytes) + width = p.total_bytes if width == 2: val = p.int16_value elif width == 4: @@ -53,7 +56,7 @@ def print_param(g, p): C, R = p.dimensions for r in range(R): print('{0.name}.{1.name}[{2}] = {3}'.format( - g, p, r, repr(p.bytes[r * C:(r+1) * C]))) + g, p, r, repr(p.bytes_value[r * C:(r+1) * C]))) def main(args): diff --git a/scripts/c3d-viewer b/scripts/c3d-viewer.py old mode 100755 new mode 100644 similarity index 94% rename from scripts/c3d-viewer rename to scripts/c3d-viewer.py index 5be64b1..6096019 --- a/scripts/c3d-viewer +++ b/scripts/c3d-viewer.py @@ -1,14 +1,18 @@ #!/usr/bin/env python '''A simple OpenGL viewer for C3D files.''' - -import c3d +try: + import c3d +except ModuleNotFoundError: + import sys + import os + sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..\\')) + import c3d import argparse import collections import contextlib import numpy as np import pyglet - from pyglet.gl import * parser = argparse.ArgumentParser(description='A simple OpenGL viewer for C3D files.') @@ -90,7 +94,8 @@ def __init__(self, c3d_reader, trace=None, paused=False): super(Viewer, self).__init__( width=800, height=450, resizable=True, vsync=False, config=config) - self._frames = c3d_reader.read_frames(copy=False) + self.c3d_reader = c3d_reader + self._frames = iter(()) self._frame_rate = c3d_reader.header.frame_rate self._maxlen = 16 @@ -106,7 +111,7 @@ def __init__(self, c3d_reader, trace=None, paused=False): self.ry = 30 self.rz = 30 - #self.fps = pyglet.clock.ClockDisplay() + # self.fps = pyglet.clock.ClockDisplay() self.on_resize(self.width, self.height) @@ -152,7 +157,8 @@ def __init__(self, c3d_reader, trace=None, paused=False): len(vtx) // 3, idx, ('v3f/static', vtx), ('n3f/static', nrm)) def on_mouse_scroll(self, x, y, dx, dy): - if dy == 0: return + if dy == 0: + return self.zoom *= 1.1 ** (-1 if dy < 0 else 1) def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): @@ -164,7 +170,6 @@ def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): # roll self.ry += 0.2 * -dy self.rz += 0.2 * dx - #print('z', self.zoom, 't', self.ty, self.tz, 'r', self.ry, self.rz) def on_resize(self, width, height): glViewport(0, 0, width, height) @@ -221,6 +226,8 @@ def _next_frame(self): try: return next(self._frames) except StopIteration: + self._frames = self.c3d_reader.read_frames(copy=False) + return self._next_frame() pyglet.app.exit() def update(self, dt): diff --git a/scripts/c3d2csv.py b/scripts/c3d2csv.py index 128b01a..b15966f 100644 --- a/scripts/c3d2csv.py +++ b/scripts/c3d2csv.py @@ -4,14 +4,19 @@ from __future__ import print_function -import c3d import sys import argparse +try: + import c3d +except ModuleNotFoundError: + import os + sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..\\')) + import c3d parser = argparse.ArgumentParser(description='Convert a C3D file to CSV (text) format.') -parser.add_argument('-a', '--include-analog', action='store_true', help='output analog values after point positions') -parser.add_argument('-c', '--include-camera', action='store_true', help='output camera count with each point position') -parser.add_argument('-r', '--include-error', action='store_true', help='output error value with each point position') +parser.add_argument('-a', '--include-analog', action='store_true', help='Output analog channel values after point coordinates.') +parser.add_argument('-c', '--include-camera', action='store_true', help='Output column with camera counts as the last point coordinate column.') +parser.add_argument('-r', '--include-error', action='store_true', help='Output column with residual values after point x,y,z coordinate columns.') parser.add_argument('-e', '--end', default='\\n', metavar='K', help='write K between records') parser.add_argument('-s', '--sep', default=',', metavar='C', help='write C between fields in a record') parser.add_argument('input', default='-', metavar='FILE', nargs='+', help='process data from this input FILE') @@ -24,8 +29,9 @@ def convert(filename, args, sep, end): if open_file_streams: input = open(filename, 'rb') output = open(filename.replace('.c3d', '.csv'), 'w') + try: - for frame_no, points, analog in c3d.Reader(input).read_frames(copy=False): + for frame_no, points, analog in c3d.Reader(input).read_frames(copy=False, camera_sum=True): fields = [frame_no] for x, y, z, err, cam in points: fields.append(str(x)) diff --git a/scripts/c3d2npz b/scripts/c3d2npz.py old mode 100755 new mode 100644 similarity index 84% rename from scripts/c3d2npz rename to scripts/c3d2npz.py index d2a7e79..dd0609d --- a/scripts/c3d2npz +++ b/scripts/c3d2npz.py @@ -11,18 +11,25 @@ import numpy as np import sys from tempfile import TemporaryFile +try: + import c3d +except ModuleNotFoundError: + import os + sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..\\')) + import c3d parser = argparse.ArgumentParser(description='Convert a C3D file to NPZ (numpy binary) format.') parser.add_argument('input', default='-', metavar='FILE', nargs='+', help='process data from this input FILE') parser.add_argument("-v", "--verbose", help="increase output verbosity", action="store_true") + def convert(filename, args): input = sys.stdin outname = '-' if filename != '-': input = open(filename, 'rb') outname = filename.replace('.c3d', '.npz') - + points = [] analog = [] for i, (_, p, a) in enumerate(c3d.Reader(input).read_frames()): @@ -32,13 +39,14 @@ def convert(filename, args): logging.debug('%s: extracted %d point frames', outname, len(points)) np.savez(outname, points=points, analog=analog) - print(outname + ': saved', len(points), "x", str(points[0].shape), "points,", - len(analog), analog[0].shape, 'analog' if len(analog) else () - ) - + print(outname + ': saved', len(points), "x", str(points[0].shape), "points,", + len(analog), analog[0].shape, 'analog' if len(analog) else () + ) + if filename != '-': input.close() + def main(args): if args.verbose: logging.basicConfig(level=logging.DEBUG) diff --git a/setup.py b/setup.py index 5f847ab..d795261 100644 --- a/setup.py +++ b/setup.py @@ -3,8 +3,8 @@ setuptools.setup( name='c3d', - version='0.3.0', - py_modules=['c3d'], + version='0.6.0', + py_modules=['c3d.dtypes', 'c3d.group', 'c3d.header', 'c3d.manager', 'c3d.reader', 'c3d.writer', 'c3d.parameter', 'c3d.utils', 'scripts.c3d-viewer'], author='UT Vision, Cognition, and Action Lab', author_email='leif@cs.utexas.edu', description='A library for manipulating C3D binary files', @@ -13,7 +13,7 @@ url='http://github.com/EmbodiedCognition/py-c3d', keywords=('c3d motion-capture'), install_requires=['numpy'], - scripts=['scripts/c3d{}'.format(s) for s in '-metadata -viewer 2csv 2npz'.split()], + scripts=['scripts/c3d{}.py'.format(s) for s in '-metadata -viewer 2csv 2npz'.split()], classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Science/Research', diff --git a/test/README.rst b/test/README.rst new file mode 100644 index 0000000..bf286c0 --- /dev/null +++ b/test/README.rst @@ -0,0 +1,10 @@ +Tests +~~~~~ + +To run tests, use the following command from the root of the package directory:: + + python -m unittest discover . + +Test scripts will automatically download test files from `c3d.org`_. + +.. _c3d.org: https://www.c3d.org/sampledata.html diff --git a/test/base.py b/test/base.py index b409c22..afc3ab5 100644 --- a/test/base.py +++ b/test/base.py @@ -4,6 +4,7 @@ VERBOSE = False + class Base(unittest.TestCase): def setUp(self): Zipload.download() diff --git a/test/test_c3d.py b/test/test_c3d.py index 119e650..c8bda28 100644 --- a/test/test_c3d.py +++ b/test/test_c3d.py @@ -1,7 +1,10 @@ +''' Basic Reader and Writer tests. +''' import c3d import importlib import io import unittest +import numpy as np from test.base import Base from test.zipload import Zipload climate_spec = importlib.util.find_spec("climate") @@ -15,6 +18,8 @@ class ReaderTest(Base): + ''' Test basic Reader functionality + ''' def test_format_pi(self): r = c3d.Reader(Zipload._get('sample01.zip', 'Eb015pi.c3d')) self._log(r) @@ -36,11 +41,11 @@ def test_paramsa(self): def test_paramsb(self): r = c3d.Reader(Zipload._get('sample08.zip', 'TESTBPI.c3d')) self._log(r) - for g in r.groups.values(): - for p in g.params.values(): + for g in r.values(): + for p in g.values(): if len(p.dimensions) == 0: val = None - width = len(p.bytes) + width = p.bytes_per_element if width == 2: val = p.int16_value elif width == 4: @@ -51,8 +56,8 @@ def test_paramsb(self): assert r.point_used == 26 assert r.point_rate == 50 assert r.analog_used == 16 - assert r.get_float('POINT:RATE') == 50 - assert r.get_float('ANALOG:RATE') == 200 + assert r.get('POINT:RATE').float_value == 50 + assert r.get('ANALOG:RATE').float_value == 200 def test_paramsc(self): r = c3d.Reader(Zipload._get('sample08.zip', 'TESTCPI.c3d')) @@ -82,18 +87,55 @@ def test_frames(self): class WriterTest(Base): - def test_paramsd(self): + ''' Test basic writer functionality + ''' + def test_add_frames(self): + r = c3d.Reader(Zipload._get('sample08.zip', 'TESTDPI.c3d')) + w = c3d.Writer( + point_rate=r.point_rate, + analog_rate=r.analog_rate, + point_scale=r.point_scale, + ) + w.add_frames([(p, a) for _, p, a in r.read_frames()]) + w.add_frames([(p, a) for _, p, a in r.read_frames()], index=5) + + h = io.BytesIO() + w.set_point_labels(r.point_labels) + w.set_analog_labels(r.analog_labels) + w.set_analog_general_scale(r.get('ANALOG:GEN_SCALE').float_value) + w.write(h) + + def test_set_params(self): r = c3d.Reader(Zipload._get('sample08.zip', 'TESTDPI.c3d')) w = c3d.Writer( point_rate=r.point_rate, analog_rate=r.analog_rate, point_scale=r.point_scale, - gen_scale=r.get_float('ANALOG:GEN_SCALE'), ) - w.add_frames((p, a) for _, p, a in r.read_frames()) + w.add_frames([(p, a) for _, p, a in r.read_frames()]) h = io.BytesIO() - w.write(h, r.point_labels) + w.set_start_frame(255) + w.set_point_labels(r.point_labels) + w.set_analog_labels(r.analog_labels) + w.set_analog_general_scale(r.get('ANALOG:GEN_SCALE').float_value) + + # Screen axis + X, Y = '-Y', '+Z' + w.set_screen_axis() + w.set_screen_axis(X, Y) + X_v, Y_v = w.get_screen_xy_strings() + assert X_v == X and Y == Y_v, 'Mismatch between set & get screen axis.' + assert np.all(np.equal(r.point_labels, w.point_labels)), 'Expected labels to be equal.' + + test_name = 'TEST_PARAM' + test_string = 'lorem ipsum' + w.point_group.add_str(test_name, 'void descriptor', test_string) + + assert w.point_group.get(test_name).total_bytes == len(test_string), \ + "Mismatch in number of bytes encoded by 'Group.add_str'" + + w.write(h) if __name__ == '__main__': diff --git a/test/test_examples.py b/test/test_examples.py new file mode 100644 index 0000000..4724f40 --- /dev/null +++ b/test/test_examples.py @@ -0,0 +1,51 @@ +''' Tests for the documentation examples +''' +import c3d +import io +import unittest +import numpy as np +from test.base import Base + +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..\\examples')) + + +class Examples(Base): + ''' Test basic writer functionality + ''' + def test_read(self): + # Silence print + with open(os.devnull, 'w') as f: + stdout = sys.stdout + sys.stdout = f + import read + sys.stdout = stdout + + def test_write(self): + import write + path = 'random-points.c3d' + + with open(path, 'rb') as f: + reader = c3d.Reader(f) + assert reader.frame_count == 100, \ + 'Expected 30 point frames in write.py test, was {}'.format(reader.frame_count) + assert reader.point_used == 24, \ + 'Expected 5 point samples in write.py test, was {}'.format(reader.point_used) + # Raises 'FileNotFound' if the file was not generated + os.remove(path) + + def test_edit(self): + import edit + path = 'my-looped-motion.c3d' + + with open(path, 'rb') as f: + reader = c3d.Reader(f) + assert reader.frame_count == 900, \ + 'Expected 900 point frames in edit.py test, was {}'.format(reader.frame_count) + # Raises 'FileNotFound' if the file was not generated + os.remove(path) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_group_accessors.py b/test/test_group_accessors.py new file mode 100644 index 0000000..a4e0a62 --- /dev/null +++ b/test/test_group_accessors.py @@ -0,0 +1,229 @@ +''' Purpose for this file is to verify functions associated with Manager._groups dictionary. +''' +import unittest +import c3d +import numpy as np +import test.verify as verify +from test.zipload import Zipload +from test.base import Base + + +class GroupSample(): + ''' Helper object to verify group entries persist or terminate properly. ''' + def __init__(self, manager): + self.manager = manager + self.sample() + + @property + def group_items(self): + '''Helper to access group items. ''' + return [(k, g) for (k, g) in self.manager.items()] + + @property + def group_listed(self): + '''Helper to access group numerical key-value pairs. ''' + return [(k, g) for (k, g) in self.manager.listed()] + + @property + def fetch_groups(self): + '''Acquire both group sets. ''' + return self.group_items, self.group_listed + + @property + def max_key(self): + if len(self.group_items) > 0: + return np.max([k for (k, g) in self.group_listed]) + return 0 + + def sample(self): + '''Call before applying changes. ''' + self.s_grp_items, self.s_grp_list = self.fetch_groups + + def assert_entry_count(self, delta=0): + '''Assert all values in group still exist. + + Arguments + --------- + delta : int + Number of entries added (+) or removed (-) since last sample. + ''' + grp_items, grp_list = self.fetch_groups + + assert len(self.s_grp_items) + delta == len(grp_items),\ + 'Rename added item entry. Expected %i entries, now has %i.' %\ + (len(self.s_grp_items), len(grp_items)) + assert len(self.s_grp_list) + delta == len(grp_list),\ + 'Rename added list entry. Expected %i entries, now has %i.' %\ + (len(self.s_grp_list) + delta, len(grp_list)) + assert len(grp_items) == len(grp_list),\ + 'Mismatch in the number of numerical and name keys. Has %i numerical entries and %i name entries.' %\ + (len(grp_items), len(grp_list)) + + def assert_group_items(self): + '''Assert all named (str, Group) pairs persisted after change.''' + enumerator = range(len(self.s_grp_items)) + for i, (n, g), (n2, g2) in zip(enumerator, sorted(self.s_grp_items), sorted(self.group_items)): + assert n == n2, 'Group numeric id missmatch after changes for entry %i. ' % i +\ + 'Initially %i, after change entry was %i' % (n, n2) + assert g == g2, 'Group listed order changed for entry %i.' % i + + def assert_group_list(self): + '''Assert all numerical (int, Group) pairs persisted after change.''' + enumerator = range(len(self.s_grp_list)) + for i, (n, g), (n2, g2) in zip(enumerator, self.s_grp_list, self.group_listed): + assert n == n2, 'Group string id missmatch after changes for entry %i. ' % i +\ + 'Initially %i, after change entry was %i' % (n, n2) + assert g == g2, 'Group listed order changed for entry %i.' % i + + def verify_add_group(self, N): + '''Add N groups and verify count at each iteration.''' + self.sample() + max_key = self.max_key + for i in range(1, N): + test_name = 'TEST_ADD_GROUP_%i' % i + self.manager.add_group(max_key + i, test_name, '') + assert self.manager.get(test_name) is not None, 'Added group does not exist.' + self.assert_entry_count(delta=i) + + def verify_remove_all_using_numeric(self): + '''Remove all groups using numeric key and verify count at each iteration.''' + self.sample() + keys = [k for (k, g) in self.group_listed] + for i, key in enumerate(keys): + grp = self.manager.get(key) + assert grp is not None, 'Expected group to exist.' + self.manager.remove_group(key) + assert self.manager.get(key) is None, 'Removed group persisted.' + assert self.manager.get(grp.name) is None, 'Removed group persisted.' + self.assert_entry_count(delta=-1 - i) + + def verify_remove_all_using_name(self): + '''Remove all groups using name key and verify count at each iteration.''' + self.sample() + keys = [k for (k, g) in self.group_items] + for i, key in enumerate(keys): + grp = self.manager.get(key) + assert grp is not None, 'Expected group to exist.' + self.manager.remove_group(key) + assert self.manager.get(key) is None, 'Removed group persisted.' + assert self.manager.get(grp.name) is None, 'Removed group persisted.' + self.assert_entry_count(delta=-1 - i) + + +class TestGroupAccessors(Base): + ''' Tests functionality associated with editing Group entries in the Manager class. + ''' + ZIP = 'sample01.zip' + INTEL_INT = 'Eb015pi.c3d' + INTEL_REAL = 'Eb015pr.c3d' + + def test_Manager_group_items(self): + '''Test Manager.group_items''' + reader = c3d.Reader(Zipload._get(self.ZIP, self.INTEL_REAL)) + grp_keys = [k for (k, g) in reader.items()] + assert len(grp_keys) > 0, 'No group items in file or Manager.group_items failed' + + def test_Manager_group_listed(self): + '''Test Manager.group_listed''' + reader = c3d.Reader(Zipload._get(self.ZIP, self.INTEL_REAL)) + grp_list = [k for (k, g) in reader.listed()] + assert len(grp_list) > 0, 'No group items in file or Manager.group_listed failed' + + def test_Manager_add_group(self): + '''Test if renaming groups acts as intended.''' + reader = c3d.Reader(Zipload._get(self.ZIP, self.INTEL_REAL)) + keys = reader.keys() + try: + reader.add_group(0) + raise ValueError('Reader should not allow adding groups.') + except AttributeError: + pass + try: + reader.remove_group(keys[0]) + raise ValueError('Reader should not allow removing groups.') + except AttributeError: + pass + try: + reader.rename_group(keys[0], 'TEST_NAME') + raise ValueError('Reader should not allow renaming groups.') + except AttributeError: + pass + + def test_Manager_add_group(self): + '''Test if renaming groups acts as intended.''' + reader = c3d.Reader(Zipload._get(self.ZIP, self.INTEL_REAL)) + ref = GroupSample(reader.to_writer()) + ref.verify_add_group(100) + ref.verify_remove_all_using_numeric() + + def test_Manager_removing_group_from_numeric(self): + '''Test if removing groups acts as intended.''' + reader = c3d.Reader(Zipload._get(self.ZIP, self.INTEL_REAL)) + ref = GroupSample(reader.to_writer()) + ref.verify_remove_all_using_numeric() + ref.verify_add_group(100) + + def test_Manager_removing_group_from_name(self): + '''Test if removing groups acts as intended.''' + reader = c3d.Reader(Zipload._get(self.ZIP, self.INTEL_REAL)) + ref = GroupSample(reader.to_writer()) + ref.verify_remove_all_using_name() + ref.verify_add_group(100) + + def test_Manager_rename_group(self): + '''Test if renaming groups acts as intended.''' + reader = c3d.Reader(Zipload._get(self.ZIP, self.INTEL_REAL)) + writer = reader.to_writer() + ref = GroupSample(writer) + grp_keys = [k for (k, g) in ref.group_items] + + new_names = ['TEST_NAME' + str(i) for i in range(len(grp_keys))] + + for key, test_name in zip(grp_keys, new_names): + grp = writer.get(key) + writer.rename_group(key, test_name) + grp2 = writer.get(test_name) + + assert grp2 is not None, "Rename failed, group with name '%s' does not exist." + assert grp == grp2, 'Rename failed, group acquired from new name is not identical.' + + ref.assert_entry_count() + ref.assert_group_list() + + try: + writer.rename_group(new_names[0], new_names[1]) + raise RuntimeError('Overwriting existing numerical ID should raise a KeyError.') + except ValueError as e: + pass # Correct + + def test_Manager_renumber_group(self): + '''Test if renaming (renumbering) groups acts as intended.''' + reader = c3d.Reader(Zipload._get(self.ZIP, self.INTEL_REAL)) + writer = reader.to_writer() + ref = GroupSample(writer) + grp_ids = [k for (k, g) in ref.group_listed] + + max_key = ref.max_key + + for i, key in enumerate(grp_ids): + test_num = max_key + i + 1 + + grp = writer.get(key) + writer.rename_group(key, test_num) + grp2 = writer.get(test_num) + + assert grp2 is not None, "Rename failed, group with name '%s' does not exist." + assert grp == grp2, 'Rename failed, group acquired from new name is not identical.' + + ref.assert_entry_count() + ref.assert_group_items() + + try: + writer.rename_group(max_key + 1, max_key + 2) + raise RuntimeError('Overwriting existing numerical ID should raise a KeyError.') + except ValueError as e: + pass # Correct + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_parameter_accessors.py b/test/test_parameter_accessors.py new file mode 100644 index 0000000..cbfa451 --- /dev/null +++ b/test/test_parameter_accessors.py @@ -0,0 +1,158 @@ +''' Purpose for this file is to verify functions associated with Groups._params dictionary. +''' +import unittest +import c3d +from c3d.group import Group +import numpy as np +import test.verify as verify +from test.zipload import Zipload +from test.base import Base + +rnd = np.random.default_rng() + + +def add_dummy_param(group, name='TEST_NAME', shape=(10, 2), flt_range=(-1e6, 1e6)): + arr = rnd.uniform(*flt_range, size=shape).astype(np.float32) + group.add_param(name, bytes_per_element=4, dimensions=arr.shape, bytes=arr.T.tobytes()) + + +class ParamSample(): + ''' Helper object to verify parameter entries persist or terminate properly. ''' + def __init__(self, group): + assert isinstance(group, Group), \ + 'Must pass Group to ParamSample instance, was %s' % type(group) + self.group = group + self.sample() + + @property + def items(self): + '''Helper to access group items. ''' + return [(k, g) for (k, g) in self.group.items()] + + @property + def keys(self): + '''Helper to access group items. ''' + return [k for (k, g) in self.group.items()] + + def sample(self): + '''Call before applying changes. ''' + self.s_items = self.items + self.s_keys = self.keys + + def assert_entry_count(self, delta=0): + '''Assert entry count. + + Arguments + --------- + delta: Number of entries added (+) or removed (-) since last sample. + ''' + items = self.items + assert len(self.s_items) + delta == len(items),\ + 'Rename added item entry. Expected %i entries, now has %i.' %\ + (len(self.s_items), len(items)) + + def assert_group_items(self, ignore=None): + '''Assert all named (str, Group) pairs persisted after change.''' + enumerator = range(len(self.s_items)) + for i, (n, g) in enumerate(self.s_items): + if n == ignore: + continue + g2 = self.group.get(n) + assert g == g2, 'Group listed order changed for entry %i.' % i + + def verify_add_parameter(self, N): + '''Add N parameters and verify count at each iteration.''' + self.sample() + for i in range(1, N): + test_name = 'TEST_ADD_PARAM_%i' % i + add_dummy_param(self.group, test_name) + assert self.group.get(test_name) is not None, 'Added group does not exist.' + self.assert_group_items() + + def verify_remove_all(self): + '''Remove all groups using name key and verify count at each iteration.''' + self.sample() + keys = [k for (k, g) in self.items] + for i, key in enumerate(keys): + grp = self.group.get(key) + assert grp is not None, 'Expected group to exist.' + self.group.remove_param(key) + assert self.group.get(key) is None, 'Removed param persisted.' + self.assert_entry_count(delta=-1 - i) + + +class TestParameterAccessors(Base): + ''' Tests functionality associated with accessing and editing Paramater entries in Group objects. + ''' + ZIP = 'sample01.zip' + INTEL_INT = 'Eb015pi.c3d' + INTEL_REAL = 'Eb015pr.c3d' + + def test_Group_values(self): + '''Test Group.values()''' + reader = c3d.Reader(Zipload._get(self.ZIP, self.INTEL_REAL)) + for g in reader.values(): + N = len([v for v in g.values()]) + assert N > 0, 'No group values in file or GroupReadonly.values() failed' + + def test_Group_items(self): + '''Test Group.items()''' + reader = c3d.Reader(Zipload._get(self.ZIP, self.INTEL_REAL)) + for g in reader.values(): + N = len([kv for kv in g.items()]) + assert N > 0, 'No group items in file or GroupReadonly.items() failed' + + def test_Group_readonly_add_param(self): + '''Test if adding parameter to a readonly group fails.''' + reader = c3d.Reader(Zipload._get(self.ZIP, self.INTEL_REAL)) + for g in reader.values(): + try: + add_dummy_param(g) + raise RuntimeError('Adding to readonly should not be possible.') + except AttributeError: + pass + + def test_Group_add_param(self): + '''Test if adding and groups acts as intended.''' + reader = c3d.Reader(Zipload._get(self.ZIP, self.INTEL_REAL)) + writer = reader.to_writer() + for g in writer.values(): + ref = ParamSample(g) + ref.verify_add_parameter(100) + ref.verify_remove_all() + + def test_Group_remove_param(self): + '''Test if removing groups acts as intended.''' + reader = c3d.Reader(Zipload._get(self.ZIP, self.INTEL_REAL)) + writer = reader.to_writer() + for g in writer.values(): + ref = ParamSample(g) + ref.verify_remove_all() + ref.verify_add_parameter(100) + + def test_Group_rename_param(self): + ''' Test if renaming groups acts as intended.''' + reader = c3d.Reader(Zipload._get(self.ZIP, self.INTEL_REAL)) + + writer = reader.to_writer() + for g in writer.values(): + ref = ParamSample(g) + prm_keys = ref.keys + new_names = ['TEST_NAME' + str(i) for i in range(len(prm_keys))] + for key, nname in zip(prm_keys, new_names): + prm = g.get(key) + g.rename_param(key, nname) + prm2 = g.get(nname) + assert prm2 is not None, "Rename failed, renamed param does not exist." + assert prm == prm2, 'Rename failed, param acquired from new name is not identical.' + + ref.assert_entry_count() + try: + g.rename_param(new_names[0], new_names[1]) + raise RuntimeError('Overwriting existing numerical ID should raise a ValueError.') + except ValueError as e: + pass # Correct + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_parameter_functions.py b/test/test_parameter_bytes_conversion.py similarity index 69% rename from test/test_parameter_functions.py rename to test/test_parameter_bytes_conversion.py index 18fc76b..026dfc4 100644 --- a/test/test_parameter_functions.py +++ b/test/test_parameter_bytes_conversion.py @@ -1,11 +1,12 @@ -import c3d import struct import unittest import numpy as np +from c3d.dtypes import DataTypes, PROCESSOR_INTEL +from c3d.parameter import ParamData, ParamReadonly def genByteWordArr(word, shape): - ''' Generate a multi-dimensional byte array from a specific word. + ''' Generate a multi-dimensional byte array from a specific word. ''' arr = np.array(word) for d in shape[::-1]: @@ -14,7 +15,7 @@ def genByteWordArr(word, shape): def genRndByteArr(wordlen, shape, pad): - ''' Generate a multi-dimensional byte array with random data. + ''' Generate a multi-dimensional byte array with random data. ''' tot_len = wordlen + pad*wordlen arr = np.empty(shape, dtype=np.dtype('S'+str(tot_len))) @@ -27,13 +28,17 @@ def genRndByteArr(wordlen, shape, pad): def genRndFloatArr(shape, rnd, range=(-1e6, 1e6)): - ''' Generate a multi-dimensional array of 32 bit floating point data. + ''' Generate a multi-dimensional array of 32 bit floating point data. ''' return rnd.uniform(range[0], range[1], shape) +def make_param(*args, **kwargs): + return ParamReadonly(ParamData(*args, **kwargs)) + + class ParameterValueTest(unittest.TestCase): - ''' Test read Parameter arrays + ''' Test read Parameter arrays ''' RANGE_8_BIT = (-127, 127) @@ -46,195 +51,207 @@ class ParameterValueTest(unittest.TestCase): def setUp(self): self.rnd = np.random.default_rng() - self.dtypes = c3d.DataTypes(c3d.PROCESSOR_INTEL) + self.dtypes = DataTypes(PROCESSOR_INTEL) def test_a_param_float32(self): - ''' Verify a single 32 bit floating point value is parsed correctly + ''' Verify a single 32 bit floating point value is parsed correctly ''' for i in range(ParameterValueTest.TEST_ITERATIONS): value = np.float32(self.rnd.uniform(*ParameterValueTest.RANGE_32_BIT)) bytes = struct.pack(' 5) - P = c3d.Param('STRING_TEST', self.dtypes, bytes_per_element=-1, dimensions=shape, bytes=arr.T.tobytes()) + P = make_param('STRING_TEST', self.dtypes, bytes_per_element=-1, dimensions=shape, bytes=arr.T.tobytes()) arr_out = P.string_array assert arr.T.shape == arr_out.shape, "Mismatch in 'string_array' converted shape. Was %s, expected %s" %\ @@ -311,7 +328,7 @@ def test_i_parse_random_string_array(self): # 4 dims for wlen in range(10): arr, shape = genRndByteArr(wlen, [7, 5, 3], wlen > 5) - P = c3d.Param('STRING_TEST', self.dtypes, bytes_per_element=-1, dimensions=shape, bytes=arr.T.tobytes()) + P = make_param('STRING_TEST', self.dtypes, bytes_per_element=-1, dimensions=shape, bytes=arr.T.tobytes()) arr_out = P.string_array assert arr.T.shape == arr_out.shape, "Mismatch in 'string_array' converted shape. Was %s, expected %s" %\ @@ -323,7 +340,7 @@ def test_i_parse_random_string_array(self): # 5 dims for wlen in range(10): arr, shape = genRndByteArr(wlen, [7, 6, 5, 3], wlen > 5) - P = c3d.Param('STRING_TEST', self.dtypes, bytes_per_element=-1, dimensions=shape, bytes=arr.T.tobytes()) + P = make_param('STRING_TEST', self.dtypes, bytes_per_element=-1, dimensions=shape, bytes=arr.T.tobytes()) arr_out = P.string_array assert arr.T.shape == arr_out.shape, "Mismatch in 'string_array' converted shape. Was %s, expected %s" %\ diff --git a/test/test_software_examples.py b/test/test_software_examples.py index 2a5fa3e..0310f25 100644 --- a/test/test_software_examples.py +++ b/test/test_software_examples.py @@ -1,9 +1,14 @@ +''' Tests ability to read plausible values from software examples. +''' import unittest import test.verify as verify from test.base import Base class Sample00(verify.WithinRangeTest, Base): + ''' Sample00 containing software file examples from: + https://www.c3d.org/sampledata.html + ''' ZIP = 'sample00.zip' DATA_RANGE = (-1e6, 1e6) @@ -15,9 +20,8 @@ class Sample00(verify.WithinRangeTest, Base): ('Innovative Sports Training', ['Gait with EMG.c3d', 'Static Pose.c3d']), ('Motion Analysis Corporation', ['Sample_Jump2.c3d', 'Walk1.c3d']), ('NexGen Ergonomics', ['test1.c3d']), - ('Vicon Motion Systems', ['TableTennis.c3d']), - # Vicon files are weird, uses non-standard encodings. Walking01.c3d contain nan values and is not tested. - # ('Vicon Motion Systems', ['pyCGM2 lower limb CGM24 Walking01.c3d', 'TableTennis.c3d']), + # Vicon files are weird, uses non-standard encodings. Walking01.c3d contain nan values. + ('Vicon Motion Systems', ['pyCGM2 lower limb CGM24 Walking01.c3d', 'TableTennis.c3d']), ] diff --git a/test/test_software_examples_write_read.py b/test/test_software_examples_write_read.py new file mode 100644 index 0000000..df6125a --- /dev/null +++ b/test/test_software_examples_write_read.py @@ -0,0 +1,93 @@ +''' Tests +''' + +import c3d +import unittest +import os +import test.verify as verify +from test.base import Base +from test.zipload import Zipload, TEMP + + +def verify_read_write(zip, file_path, proc_type='INTEL', real=True): + ''' Compare read write ouput to original read file. + ''' + A = c3d.Reader(Zipload._get(zip, file_path)) + + cpy_mode = 'copy' + if proc_type != 'INTEL': + cpy_mode = 'shallow_copy' + writer = A.to_writer(cpy_mode) + + tmp_path = os.path.join(TEMP, 'write_test.c3d') + with open(tmp_path, 'wb') as handle: + writer.write(handle) + + aname = 'Original' + bname = 'WriteRead' + test_id = '{} write read test'.format(proc_type) + + with open(tmp_path, 'rb') as handle: + B = c3d.Reader(handle) + verify.equal_headers(test_id, A, B, aname, bname, real, real) + verify.data_is_equal(A, B, aname, bname) + + +class Sample00(Base): + + ZIP = 'sample00.zip' + zip_files = \ + [ + ('Advanced Realtime Tracking GmbH', ['arthuman-sample.c3d', 'arthuman-sample-fingers.c3d']), + ('Codamotion', ['codamotion_gaitwands_19970212.c3d', 'codamotion_gaitwands_20150204.c3d']), + ('Cometa Systems', ['EMG Data Cometa.c3d']), + ('Innovative Sports Training', ['Gait with EMG.c3d', 'Static Pose.c3d']), + ('Motion Analysis Corporation', ['Sample_Jump2.c3d', 'Walk1.c3d']), + ('NexGen Ergonomics', ['test1.c3d']), + # Vicon files are weird, uses non-standard encodings. Walking01.c3d contain nan values. + ('Vicon Motion Systems', ['pyCGM2 lower limb CGM24 Walking01.c3d', 'TableTennis.c3d']), + ] + + def test_read_write_examples(self): + ''' Compare write ouput to original read + ''' + + print('----------------------------') + print(type(self)) + print('----------------------------') + for folder, files in self.zip_files: + print('{} | Validating...'.format(folder)) + for file in files: + verify_read_write(self.ZIP, '{}/{}'.format(folder, file)) + print('... OK') + print('DONE') + + +class Sample01(Base): + + ZIP = 'sample01.zip' + zip_files = \ + [ + ('Eb015pi.c3d', 'INTEL', False), + ('Eb015pr.c3d', 'INTEL', True), + ('Eb015vi.c3d', 'DEC', False), + ('Eb015vr.c3d', 'DEC', True), + ('Eb015si.c3d', 'SGI', False), + ('Eb015sr.c3d', 'SGI', True), + ] + + def test_read_write_examples(self): + ''' Compare write ouput to original read + ''' + print('----------------------------') + print(type(self)) + print('----------------------------') + for (file, proc, is_real) in self.zip_files: + print('{} | Validating...'.format(file)) + verify_read_write(self.ZIP, file, proc, is_real) + print('... OK') + print('Done.') + + +if __name__ == '__main__': + unittest.main() diff --git a/test/verify.py b/test/verify.py index c49d78c..37de442 100644 --- a/test/verify.py +++ b/test/verify.py @@ -110,21 +110,26 @@ def check_zipfile(file_path): analog_min = np.min(analog) analog_max = np.max(analog) - assert np.all(npoint == reader.frame_count),\ - """Failed verifying POINT data in range ({}, {}), found {} number of mismatches in each axis - for all samples. Range for data was ({}, {})."""\ - .format(min_range, max_range, np.sum(np.abs(npoint - reader.frame_count), axis=0), - point_min, point_max) - assert np.all(nanalog == reader.analog_sample_count),\ - """Failed verifying ANALOG data in range ({}, {}), found {} number of mismatches for each channel - for all samples. Range for data was ({}, {})."""\ - .format(min_range, max_range, np.abs(nanalog - reader.analog_sample_count), analog_min, analog_max) + assert np.all(npoint == reader.frame_count), '\n' +\ + 'Failed verifying POINT data in range ({}, {}).\n'.format(min_range, max_range) +\ + 'Found a total of {} values outside plausible range.\n'.format( + np.sum(np.abs(npoint - reader.frame_count), axis=0)) +\ + 'Range for data was ({}, {})'.format(point_min, point_max) + + assert np.all(nanalog == reader.analog_sample_count), '\n' +\ + 'Failed verifying ANALOG data in range ({}, {}).\n'.format(min_range, max_range) +\ + 'Found a total of {} values outside plausible range.\n'.format( + np.abs(nanalog - reader.analog_sample_count)) +\ + 'Range for data was ({}, {})'.format(analog_min, analog_max) print('{} | READ: OK'.format(file)) # Allow self.zipfiles on form: # ['FILE', ..] # [('FOLDER', ['FILE', ..])] + print('----------------------------') + print(type(self)) + print('----------------------------') if len(np.shape(self.zip_files)) == 1: for file in self.zip_files: check_zipfile(file) @@ -133,6 +138,7 @@ def check_zipfile(file_path): print('{} | Validating...'.format(folder)) for file in files: check_zipfile('{}/{}'.format(folder, file)) + print('DONE') ## @@ -277,37 +283,64 @@ def data_is_equal(areader, breader, alabel, blabel): apoint, aanalog = Base.load_data(areader) bpoint, banalog = Base.load_data(breader) - nsampled_coordinates = areader.point_used * frame_count - nsampled_analog = areader.analog_used * analog_count + apoint = np.reshape(apoint, (-1, 5)) + bpoint = np.reshape(bpoint, (-1, 5)) + + avalid = apoint[:, 3] >= 0 + bvalid = bpoint[:, 3] >= 0 + valid_diff = np.sum(np.logical_xor(avalid, bvalid)) + assert valid_diff == 0, '\n' +\ + 'Error in number of valid samples between {} and {}.\n'.format(alabel, blabel) +\ + 'Total number of validation mismatches: {} of {}'.format(valid_diff, len(avalid)) + + # Only compare valid point data + valid = avalid + apoint = apoint[valid] + bpoint = bpoint[valid] + + tot_points = len(apoint) + tot_analog = areader.analog_used * analog_count # Compare point data (coordinates) c = ['X', 'Y', 'Z'] + # Tolerance (allow scale x integer rounding error) + atol = equal_scale_fac * abs(areader.point_scale) for i in range(3): - axis_diff = nsampled_coordinates - np.sum(np.isclose(apoint[:, :, i], bpoint[:, :, i], - atol=equal_scale_fac*abs(areader.point_scale))) - assert axis_diff == 0, \ - 'Mismatched coordinates on {} axis for {} and {}, number of sampled diff: {} of {}'.format( - c[i], alabel, blabel, axis_diff, nsampled_coordinates) + was_close = np.isclose(apoint[:, i], bpoint[:, i], atol=atol) + axis_notclose = tot_points - np.sum(was_close) + assert axis_notclose == 0, '\n' +\ + 'Mismatched coordinates on {} axis between {} and {}.\n'.format(c[i], alabel, blabel) +\ + 'Samples with absolute difference larger then {:0.4f}: {} of {}.\n'.format( + atol, axis_notclose, tot_points) +\ + 'Maximum difference: {}'.format(np.max(np.abs(apoint[:, i] - bpoint[:, i]))) # Word 4 (residual + camera bits) - residual_diff = nsampled_coordinates - np.sum(np.isclose(apoint[:, :, 3], bpoint[:, :, 3])) - cam_diff = nsampled_coordinates - np.sum(np.isclose(apoint[:, :, 4], bpoint[:, :, 4], atol=1.001)) - cam_diff_non_equal = nsampled_coordinates - np.sum(np.isclose(apoint[:, :, 4], bpoint[:, :, 4])) + residual_diff = tot_points - np.sum(np.isclose(apoint[:, 3], bpoint[:, 3])) + cam_close = np.isclose(apoint[:, 4], bpoint[:, 4]) + cam_diff_non_equal = tot_points - np.sum(cam_close) # Camera bit errors (warn if non identical, allow 1 cam bit diff, might be bad DEC implementation, or bad data) if cam_diff_non_equal > 0: - assert cam_diff == 0, 'Mismatch error in camera bit flags for {} and {}, number of samples with flag diff:\ - {} of {}'.format(alabel, blabel, cam_diff, nsampled_coordinates) - err_str = 'Mismatch in camera bit flags between {} and {}, number of samples with flag diff:\ - {} of {}'.format(alabel, blabel, cam_diff_non_equal, nsampled_coordinates) + # print(apoint[~cam_close, 4]) + # print(bpoint[~cam_close, 4]) + cam_close = np.isclose(apoint[:, 4], bpoint[:, 4], atol=1.001) + cam_diff = tot_points - np.sum(cam_close) + assert cam_diff == 0, '\n' + \ + 'Mismatch in camera bit flags between {} and {}.\n'.format(alabel, blabel) +\ + 'Number of samples with flag differences larger then 1: {} of {}'.format(cam_diff, tot_points) + err_str = '\n' + \ + 'Mismatch in camera bit flags between {} and {}.\n'.format(alabel, blabel) +\ + 'Number of samples with flag difference of 1: {} of {}'.format(cam_diff_non_equal, tot_points) warnings.warn(err_str, RuntimeWarning) # Residual assert - assert residual_diff == 0, \ - 'Error in sample residuals between {} and {}, number of residual diff: {} of {}'.format( - alabel, blabel, residual_diff, nsampled_coordinates) + assert residual_diff == 0, '\n' +\ + 'Error in sample residuals between {} and {}.\n'.format(alabel, blabel) +\ + 'Total number of failed samples: {} of {}'.format(residual_diff, tot_points) # Compare analog - analog_diff = nsampled_analog - np.sum(np.isclose(aanalog, banalog)) - assert analog_diff == 0, \ - 'Mismatched analog samples between {} and {}, number of sampled diff: {} of {}'.format( - alabel, blabel, analog_diff, nsampled_analog) + was_close = np.isclose(aanalog, banalog) + analog_notclose = tot_analog - np.sum(was_close) + assert analog_notclose == 0, '\n' + \ + 'Mismatched analog samples between {} and {}.\n'.format(alabel, blabel) +\ + 'Total number of failed samples: {} of {}.\n'.format(analog_notclose, tot_analog) +\ + 'Largest absolute difference: {}.'.format(np.max(np.abs(aanalog - banalog))) diff --git a/test/zipload.py b/test/zipload.py index 0bf5482..3e32f00 100644 --- a/test/zipload.py +++ b/test/zipload.py @@ -37,10 +37,10 @@ def extract(zf): out_path = os.path.join(TEMP, os.path.basename(zf)[:-4]) zip = zipfile.ZipFile(os.path.join(TEMP, zf)) + # Loop equivalent to zip.extractall(out_path) but avoids overwriting files for zf in zip.namelist(): - #zip.extractall(out_path) but avoids overwriting files fpath = os.path.join(out_path, zf) - # If file already exist, don' extract + # If file already exist, don't extract if not os.path.isfile(fpath) and not os.path.isdir(fpath): print('Extracted:', fpath) zip.extract(zf, path=out_path)