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
andlast_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''))