pupyC3D.c3dfile

  1# pupyC3D (c) by Antoine MARIN antoine.marin@univ-rennes2.fr
  2#
  3# pupyC3D is licensed under a
  4# Creative Commons Attribution-NonCommercial 4.0 International License.
  5#
  6# You should have received a copy of the license along with this
  7# work. If not, see <https://creativecommons.org/licenses/by-nc/4.0/>.
  8
  9import os
 10import warnings
 11import struct
 12import math
 13import numpy as np
 14from .decoder import *
 15
 16class Metadata:
 17    """Base class for Parameters and ParameterGroups.
 18    
 19    Handles common functionality for C3D metadata elements including
 20    name, description, and group ID management.
 21    """
 22
 23    def __init__(self, name:str, group_id:int):
 24        """Initialize metadata object.
 25        
 26        Args:
 27            name (str): Name of the metadata element
 28            group_id (int): ID of the parameter group
 29        """
 30        self.name = name
 31        self.description = ''
 32        self.group_id = group_id
 33
 34    def read_from_buffer(self, buffer:ProcStream):
 35        """Read metadata from binary buffer.
 36        
 37        Args:
 38            buffer (ProcStream): Binary data stream
 39            
 40        Returns:
 41            int: Number of bytes read
 42        """
 43        return 0
 44
 45    def write_to_buffer(self, buffer: ProcStream):
 46        """Write metadata to binary buffer.
 47        
 48        Args:
 49            buffer (ProcStream): Binary data stream to write to
 50        """
 51        buffer.write_int8(len(self.name))
 52        buffer.write_int8(self.group_id)
 53        buffer.write_string(self.name)
 54
 55    def _get_offset(self):
 56        """Calculate byte offset for metadata structure.
 57        
 58        Returns:
 59            int: Offset in bytes
 60        """
 61        return 2 + len(self.description) + 1
 62
 63    def get_size(self):
 64        """Calculate total size of metadata structure.
 65        
 66        Returns:
 67            int: Total size in bytes
 68        """
 69        return 2 + len(self.name) + self._get_offset()
 70
 71
 72class Parameter(Metadata):
 73    """Represents a C3D parameter with typed data.
 74    
 75    Parameters can store scalar values or multi-dimensional arrays
 76    of various types (int8, uint16, float, string).
 77    """
 78
 79    def __init__(self, name:str, group_id:int):
 80        """Initialize parameter.
 81        
 82        Args:
 83            name (str): Parameter name
 84            group_id (int): ID of the parameter group
 85        """
 86        super(Parameter, self).__init__(name, group_id)
 87        self.data_type = 0
 88        self.value = None
 89
 90    def read_from_buffer(self, buffer:ProcStream):
 91        """Read parameter data from binary buffer.
 92        
 93        Supports scalar and multi-dimensional array data of types:
 94        - int8 (data_type=1)
 95        - uint16 (data_type=2) 
 96        - float (data_type=4)
 97        - string (data_type=-1)
 98        
 99        Args:
100            buffer (ProcStream): Binary data stream
101            
102        Returns:
103            int: Number of bytes read
104        """
105        offset = 0
106        self.data_type = buffer.get_int8()
107        offset += 1
108        n_dim = buffer.get_int8()
109        offset += 1
110        if n_dim == 0:
111            if self.data_type == 1:
112                self.value = buffer.get_int8()
113            elif self.data_type == 2:
114                self.value = buffer.get_uint16()
115            elif self.data_type == 4:
116                self.value = buffer.get_float()
117            data_size = abs(self.data_type)
118        else:
119            dims = []
120            for i in range(n_dim):
121                dims.append(buffer.get_uint8())
122                offset += 1
123            prod = math.prod(dims[:n_dim])
124            if self.data_type == -1:
125                if len(dims) >= 2:
126                    row = 1
127                    inc2 = 1
128                    while inc2 < n_dim:
129                        row *= dims[inc2]
130                        inc2 += 1
131                    data = []
132                    for i in range(row):
133                        data.append(buffer.get_string(dims[0]).strip())
134                    data = np.array(data)
135                    if data.size == 0:
136                        data = np.empty((dims[:]), str)
137                else:
138                    data = np.array([buffer.get_string(prod).strip()])
139            elif self.data_type == 1:
140                data = np.array([buffer.get_int8() for _ in range(prod)]).reshape(dims)
141            elif self.data_type == 2:
142                data = np.array([buffer.get_uint16() for _ in range(prod)]).reshape(dims)
143            elif self.data_type == 4:
144                data = np.array([buffer.get_float() for _ in range(prod)]).reshape(dims)
145            else:
146                data = np.array([])
147
148            self.value = data
149            data_size = prod * abs(self.data_type)
150        offset += data_size
151        desc_len = buffer.get_uint8()
152        self.description = buffer.get_string(desc_len)
153        offset += desc_len
154        return offset
155
156    def write_to_buffer(self, buffer: ProcStream, last_entry=False):
157        super(Parameter, self).write_to_buffer(buffer)
158        if last_entry:
159            offset = 0
160        else:
161            offset = self._get_offset()
162        buffer.write_uint16(offset)
163        buffer.write_int8(self.data_type)
164
165        dims = self.__get_dim()
166        n_dim = len(dims)
167        buffer.write_int8(n_dim)
168        if n_dim == 0:
169            if self.data_type == 1:
170                buffer.write_int8(self.value)
171            elif self.data_type == 2:
172                buffer.write_uint16(self.value)
173            elif self.data_type == 4:
174                buffer.write_float(self.value)
175        else:
176            for each in dims:
177                buffer.write_uint8(each)
178            data = self.value.flatten()
179            for each in data:
180                if self.data_type == 1:
181                    buffer.write_int8(each)
182                elif self.data_type == 2:
183                    buffer.write_uint16(each)
184                elif self.data_type == 4:
185                    buffer.write_float(each)
186                elif self.data_type == -1:
187                    buffer.write_string(each.ljust(dims[0]))
188
189        buffer.write_uint8(len(self.description))
190        buffer.write_string(self.description)
191
192    def _get_offset(self):
193        offset = 2 + 1 + 1 #offset, data_type, dim
194        if not isinstance(self.value, np.ndarray):
195            offset += self.data_type#value
196        else:
197            # n_dimension
198            dims = self.__get_dim()
199            n_dim = len(dims)
200            offset += n_dim
201            # data size
202            prod = math.prod(dims[:n_dim])
203            data_size = prod * abs(self.data_type)
204            offset += data_size
205
206        offset +=1 # desc length
207        offset += len(self.description) #desc
208        return offset
209
210    def __get_dim(self):
211        if not isinstance(self.value, np.ndarray):
212            dims  = []
213        else:
214            dims = self.value.shape
215            if self.data_type == -1:
216                if dims[0] == 1:
217                    dims = [len(self.value[0])]
218                else:
219                    if self.value.size > 0:
220                        d1 = max([len(x) for x in self.value])
221                        dims = [d1, dims[0]]
222        return dims
223
224
225class ParameterGroup(Metadata):
226    """Represents a group of related C3D parameters.
227    
228    Parameter groups organize parameters by functionality
229    (e.g., POINT, ANALOG, TRIAL groups).
230    """
231
232    def __init__(self, name:str, group_id:int):
233        """Initialize parameter group.
234        
235        Args:
236            name (str): Group name
237            group_id (int): Unique group identifier
238        """
239        super(ParameterGroup, self).__init__(name, group_id)
240        self.parameters = dict()
241
242    def add_parameter(self, name)->Parameter:
243        """Add a new parameter to this group.
244        
245        Args:
246            name (str): Parameter name
247            
248        Returns:
249            Parameter: New or existing parameter
250        """
251        if name not in self.parameters:
252            param = Parameter(name, -self.group_id)
253            self.parameters[param.name] = param
254            return param
255        warnings.warn('Parameter %s already exists' %name)
256        return self.parameters[name]
257
258    def remove_parameter(self, name):
259        """Remove a parameter from this group.
260        
261        Args:
262            name (str): Parameter name to remove
263            
264        Returns:
265            bool: True if parameter was removed, False if not found
266        """
267        if name in self.parameters:
268            self.parameters.pop(name)
269            return True
270        return False
271
272    def get_parameter(self, name):
273        """Get a parameter by name.
274        
275        Args:
276            name (str): Parameter name
277            
278        Returns:
279            Parameter or None: Parameter object if found
280        """
281        if name not in self.parameters:
282            return None
283        return self.parameters[name]
284
285    def read_from_buffer(self, buffer:ProcStream):
286        desc_len = buffer.get_uint8()
287        offset = 1
288        desc = buffer.get_string(desc_len)
289        offset += desc_len
290        self.description = desc
291        return offset
292
293    def write_to_buffer(self, buffer: ProcStream, last_entry=False):
294        super(ParameterGroup, self).write_to_buffer(buffer)
295        # offset = 2 + len(self.description) + 1
296        offset = self._get_offset()
297        buffer.write_uint16(offset)
298        buffer.write_uint8(len(self.description))
299        buffer.write_string(self.description)
300
301
302class C3DFile:
303    """Main class for reading and writing C3D files.
304    
305    C3D files contain 3D coordinate data, parameters, and metadata
306    commonly used in biomechanics and motion capture applications.
307    
308    Attributes:
309        filename (str): Path to the C3D file
310        header (dict): File header information
311        groups (dict): Parameter groups indexed by group ID
312        data (dict): Point and analog data
313    """
314
315    def __init__(self, filename:str=''):
316        """Initialize C3DFile object.
317        
318        Args:
319            filename (str, optional): Path to C3D file. If file exists,
320                it will be automatically loaded.
321        """
322        self.filename = filename
323        self.__decoder = None
324        self.header = dict()
325        self.groups = dict()
326        self.data = dict()
327        if os.path.exists(filename):
328            self.read_file()
329
330    def read_file(self):
331        """
332        Read the C3D file associated with self.filename
333        """
334        if os.path.exists(self.filename):
335            with open(self.filename, 'rb') as handle:
336                self.__read_header(handle)
337                self.__read_parameters(handle)
338                self.__read_data(handle)
339        else:
340            raise FileNotFoundError(self.filename)
341
342    def write(self, filename:str='', **kwargs):
343        """Write C3D data to file.
344        
345        Args:
346            filename (str, optional): Output file path. Defaults to self.filename.
347            **kwargs: Additional options:
348                overwrite (bool): Allow overwriting existing file. Defaults to False.
349                
350        Raises:
351            Warning: If file exists and overwrite=False
352        """
353        overwrite = kwargs.get('overwrite', False)
354        if filename == '':
355            filename = self.filename
356        if os.path.exists(filename) and not overwrite:
357            warnings.warn('File %s already exist. If you wish to overwrite it set ''overwrite'' argument to True' %filename)
358            return
359        with open(filename, 'wb') as handle:
360            self.__write_header(handle)
361            self.__write_parameters(handle)
362            self.__write_data(handle)
363
364    def add_parameter_group(self, name, gid=0)->ParameterGroup:
365        """Add a new parameter group.
366        
367        Args:
368            name (str): Group name
369            gid (int, optional): Group ID. If 0, auto-assigned. Defaults to 0.
370            
371        Returns:
372            ParameterGroup or None: New group if created, None if already exists
373        """
374        assert(gid <= 0)
375        if gid == 0:
376            g = self.get_parameter_group(name)
377        else:
378            g = self.get_parameter_group(gid)
379        if g is not None:
380            return None
381        if gid == 0:
382            ids = list(self.groups.keys())
383            gid = ids[0]
384            # find first missing id
385            for number in ids:
386                if number != gid:
387                    break
388                gid -= 1
389        self.groups[gid] = ParameterGroup(name, gid)
390        return self.groups[gid]
391
392    def remove_parameter_group(self, group)->bool:
393        """Remove a parameter group.
394        
395        Args:
396            group (int or str): Group ID or name
397            
398        Returns:
399            bool: True if group was removed, False if not found
400        """
401        g = self.get_parameter_group(group)
402        if g is not None:
403            self.groups.pop(g.group_id)
404            return True
405        return False
406
407    def get_parameter_group(self, group_id)->ParameterGroup:
408        """Get a parameter group by ID or name.
409        
410        Args:
411            group_id (int or str): Group ID or name
412            
413        Returns:
414            ParameterGroup or None: Parameter group if found
415            
416        Raises:
417            TypeError: If group_id is neither int nor str
418        """
419        if isinstance(group_id, int):
420            return self.groups.get(group_id, None)
421        elif isinstance(group_id, str):
422            g = {v.name: v for v in self.groups.values()}
423            return g.get(group_id, None)
424        else:
425            raise TypeError('Argument ''group_id'' should be either str or int')
426
427    def add_parameter(self, name, gid):
428        group = self.get_parameter_group(gid)
429        if group is not None:
430            param = group.add_parameter(name)
431            return param
432        return None
433
434    def remove_parameter(self, name, group_id):
435        g = self.get_parameter_group(group_id)
436        if g is not None:
437            return g.remove_parameter(name)
438        return False
439
440    def get_parameter(self, group_id, param_name):
441        g = self.get_parameter_group(group_id)
442        if g is not None:
443            return g.parameters.get(param_name, None)
444        return None
445
446    def get_point_data(self, name: str):
447        """
448        Return trajectory of a point
449        :param name: the point name in string
450        :return: trajectory as np.ndarray (NbFrames X 3)
451        """
452        if name not in self.data['POINTS']:
453            raise ValueError('Point %s does not exists!' %name)
454        return self.data['POINTS'][name]
455
456    def get_points_data(self, name_list:list[str]):
457        """
458        Get several points trajectories
459        :param name_list: list of the points names
460        :return: dict of name:trajectories data
461        """
462        return {name: self.get_point_data(name) for name in name_list}
463
464    def get_point_names(self):
465        """
466        Get a list of all points in the c3d
467        :return: List[str] with all points labels
468        """
469        return list(self.data['POINTS'].keys())
470
471    def get_analog_data(self, name: str):
472        """
473        Return values for an analog chanel
474        :param name: analog name in string
475        :return: analog chanel values as np.ndarray(NbAnalogFrames X 1)
476        """
477        if name not in self.data['ANALOGS']:
478            raise ValueError('Chanel %s does not exists!' %name)
479        return self.data['ANALOGS'][name]
480
481    def get_analogs_data(self, name_list:list[str]):
482        """
483        Get several analog channels
484        :param name_list: list of the analogs names
485        :return: dict of name:values data
486        """
487        return {name: self.get_analog_data(name) for name in name_list}
488
489    def get_analog_names(self):
490        """
491        Get a list of all analogs chanels in the c3d
492        :return: List[str] with all analogs labels
493        """
494        return list(self.data['ANALOGS'].keys())
495
496    def get_rotation_data(self, name:str):
497        """
498        Return rotation data (mainly for c3d created by theia markerless software)
499        :param name: Joint name
500        :return: rotation and position data np.ndarray(NbFrames X 4 X 4)
501        """
502        if name not in self.data['ROTATIONS']:
503            raise ValueError('Rotation %s does not exists!' %name)
504        return self.data['ROTATIONS'][name]
505
506    def get_rotations_data(self, name_list:list[str]):
507        """
508        Get several rotations data
509        :param name_list: list of the rotations
510        :return: dict of name:values data
511        """
512        return {name: self.get_rotation_data(name) for name in name_list}
513
514    def get_rotation_names(self):
515        """
516        Get a list of all rotation data in the c3d
517        :return: List[str] with all rotations labels
518        """
519        return list(self.data['ROTATIONS'].keys())
520
521    @property
522    def point_count(self):
523        """
524        Get the number of 3D points in the c3d
525        :return: int
526        """
527        p = self.get_parameter('POINT', 'USED')
528        return p.value
529
530    @property
531    def analog_count(self):
532        """
533        Get the number of analog channels in the c3d
534        :return: int
535        """
536        p = self.get_parameter('ANALOG', 'USED')
537        if p is not None:
538            return p.value
539        return 0
540
541    @property
542    def frame_count(self):
543        """
544        Get the number of 3D points frames
545        :return: int
546        """
547        return self.header['last_frame'] - self.header['first_frame'] + 1
548
549    @property
550    def analog_frame_count(self):
551        """
552        Get the number of analog frames
553        :return: int
554        """
555        if self.analog_count > 0:
556            return self.frame_count * self.header['analog_per_frame']
557        return 0
558
559    @property
560    def frame_rate(self):
561        """
562        Get the 3D points frame rate
563        :return: float
564        """
565        p = self.get_parameter('POINT', 'RATE')
566        return p.value
567
568    @property
569    def analog_rate(self):
570        """
571        Get the analog channels frame rate
572        :return: float
573        """
574        p = self.get_parameter('ANALOG', 'RATE')
575        if p is not None:
576            return p.value
577        return 0
578
579    @property
580    def point_unit(self):
581        """
582        Get the 3D points unit (usually mm or m)
583        :return: str
584        """
585        p = self.get_parameter('POINT', 'UNITS')
586        return p.value
587
588    @property
589    def analog_unit(self):
590        """
591        Get the analog channels unit (mV, V, ...)
592        :return: str
593        """
594        p = self.get_parameter('ANALOG', 'UNITS')
595        if p is not None:
596            return p.value
597        return []
598
599    def __read_header(self, handle):
600        handle.seek(0)
601        # check if it's a c3d file
602        self.header['parameter_block'], magic = struct.unpack('BB', handle.read(2))
603        if magic != 80:
604            warnings.warn('%s is not a c3d file!' %self.filename)
605            return
606        # go to the start of the parameter block
607        handle.seek((self.header['parameter_block'] - 1) * 512 + 3)
608        # find the good encoder
609        processor = struct.unpack('B', handle.read(1))[0]
610        if processor == PROCESSOR_INTEL:
611            self.__decoder = DecoderIntel(handle)
612        elif processor == PROCESSOR_DEC:
613            self.__decoder = DecoderDec(handle)
614        elif processor == PROCESSOR_MIPS:
615            self.__decoder = DecoderMips(handle)
616
617        #  start reading header
618        handle.seek(2)
619        self.header['point_count'] = self.__decoder.get_uint16()
620        self.header['analog_count'] = self.__decoder.get_uint16()
621        self.header['first_frame'] = self.__decoder.get_uint16()
622        self.header['last_frame'] = self.__decoder.get_uint16()
623        self.header['max_gap'] = self.__decoder.get_uint16()
624        self.header['scale_factor'] = self.__decoder.get_float()
625        self.header['data_block'] = self.__decoder.get_uint16()
626        self.header['analog_per_frame'] = self.__decoder.get_uint16()
627        if self.header['analog_per_frame'] > 0 and self.header['analog_count'] > 0:
628            self.header['analog_count'] /= self.header['analog_per_frame']
629            self.header['analog_count'] = int(self.header['analog_count'])
630        if self.header['analog_per_frame'] == 0:
631            self.header['analog_per_frame'] = 1
632        self.header['frame_rate'] = self.__decoder.get_float()
633
634        self.header['events'] = dict()
635        handle.read(270)
636        self.header['events']['label_range_section'] = self.__decoder.get_uint16()
637        self.header['events']['label_first_block'] = self.__decoder.get_uint16()
638        self.header['events']['label_event_fmt'] = self.__decoder.get_uint16()
639        if self.header['events']['label_event_fmt'] == 12345:
640            self.header['events']['long_event_labels'] = True
641        else:
642            self.header['events']['long_event_labels'] = False
643        self.header['events']['num_events'] = self.__decoder.get_uint16()
644        if self.header['events']['num_events'] > 0:
645            self.header['events']['data'] = {'labels': [], 'time': [], 'display': []}
646            handle.read(2)
647            for i in range(self.header['events']['num_events']):
648                self.header['events']['data']['time'].append(self.__decoder.get_float())
649            handle.seek(198)
650            print(handle.tell())
651            for i in range(self.header['events']['num_events']):
652                self.header['events']['data']['display'].append(self.__decoder.get_uint8())
653                # print(self.header['events']['data']['display'][i])
654            handle.seek(198 * 2)
655            if self.header['events']['long_event_labels']:
656                num_char = 4
657            else:
658                num_char = 2
659            for i in range(self.header['events']['num_events']):
660                name = self.__decoder.get_string(num_char)
661                self.header['events']['data']['labels'].append(name)
662
663    def __write_header(self, handle):
664        if self.__decoder == 0:
665            self.__decoder = DecoderIntel(handle)
666        else:
667            self.__decoder.handle = handle
668
669        handle.seek(0)
670        self.__decoder.write_uint8(self.header['parameter_block'])
671        self.__decoder.write_uint8(80)
672
673        if isinstance(self.__decoder, DecoderIntel):
674            processor = PROCESSOR_INTEL
675        elif isinstance(self.__decoder, DecoderDec):
676            processor = PROCESSOR_DEC
677        elif isinstance(self.__decoder, DecoderMips):
678            processor = PROCESSOR_MIPS
679        else:
680            raise ValueError('Processor not supported')
681
682        handle.seek((self.header['parameter_block'] - 1) * 512 + 3)
683        self.__decoder.write_uint8(processor)
684
685        handle.seek(2)
686        p = self.get_parameter('POINT', 'USED')
687        self.__decoder.write_uint16(p.value)
688        if self.header['analog_per_frame'] > 0 and self.header['analog_count'] > 0:
689            analog_count = self.header['analog_count']* self.header['analog_per_frame']
690        else:
691            analog_count = self.header['analog_count']
692        self.__decoder.write_uint16(analog_count)
693        self.__decoder.write_uint16(self.header['first_frame'])
694        self.__decoder.write_uint16(self.header['last_frame'])
695        self.__decoder.write_uint16(self.header['max_gap'])
696        self.__decoder.write_float(self.header['scale_factor'])
697        data_block = self.__get_parameters_blocknum() + 2
698        self.__decoder.write_uint16(data_block)
699        if self.header['analog_per_frame'] == 1:
700            analog_per_frame = 0
701        else:
702            analog_per_frame = self.header['analog_per_frame']
703        self.__decoder.write_uint16(analog_per_frame)
704        self.__decoder.write_float(self.header['frame_rate'])
705
706        handle.seek(handle.tell() + 270)
707        self.__decoder.write_uint16(self.header['events']['label_range_section'])
708        self.__decoder.write_uint16(self.header['events']['label_first_block'])
709        self.__decoder.write_uint16(self.header['events']['label_event_fmt'])
710        self.__decoder.write_uint16(self.header['events']['num_events'])
711        if self.header['events']['num_events'] > 0:
712            handle.seek(handle.tell() + 2)
713            for i in range(self.header['events']['num_events']):
714                self.__decoder.write_float(self.header['events']['data']['time'][i])
715            handle.seek(198)
716            print(handle.tell())
717            for i in range(self.header['events']['num_events']):
718                self.__decoder.write_uint8(self.header['events']['data']['display'][i])
719            handle.seek(198 * 2)
720            if self.header['events']['long_event_labels']:
721                num_char = 4
722            else:
723                num_char = 2
724            for i in range(self.header['events']['num_events']):
725                name = self.header['events']['data']['labels'][i].ljust(num_char)
726                self.__decoder.write_string(name)
727
728    def __read_parameters(self, handle):
729        handle.seek((self.header['parameter_block'] - 1) * 512)
730        handle.read(4)
731        last_entry = False
732        while not last_entry:
733            nb_char_label = self.__decoder.get_int8()
734            if nb_char_label == 0:
735                break
736            group_id = self.__decoder.get_int8()
737            name = self.__decoder.get_string(abs(nb_char_label))
738            if group_id < 0:#group
739                current = self.add_parameter_group(name, group_id)
740            else:#parameter
741                group = self.get_parameter_group(-group_id)
742                if group is not None:
743                    current = group.add_parameter(name)
744                else :
745                    current = Parameter(name, group_id)
746
747            offset = self.__decoder.get_uint16()
748            if offset == 0:
749                last_entry = True
750            offset -= 2
751            offset -= current.read_from_buffer(self.__decoder)
752
753    def __write_parameters(self, handle):
754        handle.seek((self.header['parameter_block'] - 1) * 512 + 2)
755        self.__decoder.write_uint8(self.__get_parameters_blocknum())
756        handle.seek((self.header['parameter_block'] - 1) * 512+4)
757        groups = list(self.groups.values())
758        for g in groups:
759            g.write_to_buffer(self.__decoder)
760        for group in groups:
761            params = list(group.parameters.values())
762            for param in params:
763                if group == groups[-1] and param == params[-1]:
764                    param.write_to_buffer(self.__decoder, True)
765                else:
766                    param.write_to_buffer(self.__decoder)
767
768    def __get_parameters_blocknum(self):
769        parameters_size = 0
770        for g in self.groups.values():
771            parameters_size += g.get_size()
772            for p in g.parameters.values():
773                parameters_size += p.get_size()
774        return math.ceil(parameters_size/512)
775
776    def __read_data(self, handle):
777        handle.seek((512 * (self.header['data_block'] - 1)))
778        nb_frames = self.header['last_frame'] - self.header['first_frame'] + 1
779        point_used = self.header['point_count']
780        scale = abs(self.header['scale_factor'])
781        is_float = self.header['scale_factor'] < 0
782        if point_used > 0:
783            point_scale = [scale, 1][is_float]
784            names_param = self.get_parameter('POINT', 'LABELS')
785            marker_names = names_param.value.tolist()
786            self.data['POINTS'] = dict()
787            for each in marker_names:
788                self.data['POINTS'][each] = np.zeros([nb_frames, 4])
789
790        analog_used = self.header['analog_count']
791        if analog_used > 0:
792            offsets = np.zeros((analog_used,), int)
793            param = self.get_parameter('ANALOG', 'OFFSET')
794            if param is not None:
795                offsets = param.value
796
797            scales = np.ones((analog_used,), float)
798            param = self.get_parameter('ANALOG', 'SCALE')
799            if param is not None:
800                scales = param.value
801
802            gen_scale = 1.
803            param = self.get_parameter('ANALOG', 'GEN_SCALE')
804            if param is not None:
805                gen_scale = param.value
806            names_param = self.get_parameter('ANALOG', 'LABELS')
807            if names_param is not None:
808                analog_names = names_param.value.tolist()
809
810            self.data['ANALOGS'] = dict()
811            for each in analog_names:
812                self.data['ANALOGS'][each] = np.zeros([nb_frames*self.header['analog_per_frame'], 1])
813
814        rot_param = self.get_parameter_group('ROTATION')
815        if rot_param is not None:
816            names_param = self.get_parameter('ROTATION', 'LABELS')
817            rotation_names = names_param.value.tolist()
818            self.data['ROTATIONS'] = dict()
819            for each in rotation_names:
820                self.data['ROTATIONS'][each] = np.zeros([nb_frames, 4, 4])
821
822        for i in range(nb_frames):
823            if point_used > 0:
824                self.__read_point_frame(i, is_float, point_scale, marker_names)
825            if rot_param is not None:
826                self.__read_rotation_frame(i, rotation_names)
827            if analog_used >0:
828                self.__read_analog_frame(i, is_float, self.header['analog_per_frame'], offsets, scales, gen_scale, analog_names)
829
830    def __read_point_frame(self, frame_num, is_float, point_scale, marker_names):
831        for j, m in enumerate(marker_names):
832            if is_float:
833                p = np.array([self.__decoder.get_float(), self.__decoder.get_float(), self.__decoder.get_float(),
834                              self.__decoder.get_float()])
835            else:
836                p = np.array([self.__decoder.get_uint16(), self.__decoder.get_uint16(), self.__decoder.get_uint16(),
837                              self.__decoder.get_uint16()])
838            p = p * point_scale
839            self.data['POINTS'][m][frame_num, :] = p
840
841    def __read_analog_frame(self, frame_num, is_float, sub_frames, offsets, scales, gen_scale, analog_names):
842        if frame_num == 315:
843            stop = 1
844        for j in range(int(sub_frames)):
845            for k, analog in enumerate(analog_names):
846                if is_float:
847                    c = self.__decoder.get_float()
848                else:
849                    c = self.__decoder.get_uint16()
850                self.data['ANALOGS'][analog][frame_num*sub_frames+j] = (c-offsets[k]) * scales[k]*gen_scale
851
852    def __read_rotation_frame(self, frame_num, rotation_names):
853        for i, name in enumerate(rotation_names):
854            p = np.array([[self.__decoder.get_float(), self.__decoder.get_float(), self.__decoder.get_float(),
855                           self.__decoder.get_float()],
856                          [self.__decoder.get_float(), self.__decoder.get_float(), self.__decoder.get_float(),
857                           self.__decoder.get_float()],
858                          [self.__decoder.get_float(), self.__decoder.get_float(), self.__decoder.get_float(),
859                           self.__decoder.get_float()],
860                          [self.__decoder.get_float(), self.__decoder.get_float(), self.__decoder.get_float(),
861                           self.__decoder.get_float()]])
862            self.data['ROTATIONS'][name][frame_num, :, :] = p.transpose()
863
864    def __write_data(self, handle):
865        handle.seek((512 * (self.header['data_block'] - 1)))
866        point_used = self.header['point_count']>0
867        if point_used:
868            scale = abs(self.header['scale_factor'])
869            is_float = self.header['scale_factor'] < 0
870            point_scale = [scale, 1][is_float]
871            names_param = self.get_parameter('POINT', 'LABELS')
872            marker_names = names_param.value.tolist()
873
874        analog_used = self.header['analog_count']
875        if analog_used > 0:
876            offsets = np.zeros((analog_used,), int)
877            param = self.get_parameter('ANALOG', 'OFFSET')
878            if param is not None:
879                offsets = param.value
880
881            scales = np.ones((analog_used,), float)
882            param = self.get_parameter('ANALOG', 'SCALE')
883            if param is not None:
884                scales = param.value
885
886            gen_scale = 1.
887            param = self.get_parameter('ANALOG', 'GEN_SCALE')
888            if param is not None:
889                gen_scale = param.value
890            names_param = self.get_parameter('ANALOG', 'LABELS')
891            if names_param is not None:
892                analog_names = names_param.value.tolist()
893
894        rot_param = self.get_parameter_group('ROTATION')
895        if rot_param is not None:
896            names_param = self.get_parameter('ROTATION', 'LABELS')
897            rotation_names = names_param.value.tolist()
898
899        nb_frames = self.header['last_frame'] - self.header['first_frame'] + 1
900        for i in range(nb_frames):
901            if point_used:
902                self.__write_point_frame(marker_names, i, is_float, point_scale)
903            if rot_param is not None:
904                self.__write_rotation_frame(i, rotation_names)
905            if analog_used:
906                self.__write_analog_frame(i, self.header['analog_per_frame'], analog_names, is_float, scales, gen_scale, offsets)
907
908    def __write_point_frame(self, marker_names, frame_num, is_float, point_scale):
909        for j, m in enumerate(marker_names):
910            p = self.data['POINTS'][m][frame_num, :]/point_scale
911            for i in range(4):
912                if is_float:
913                    self.__decoder.write_float(p[i])
914                else:
915                    self.__decoder.write_uint16(p[i])
916
917    def __write_analog_frame(self, frame_num, sub_frames, analog_names, is_float, scales, gen_scale, offsets):
918        for j in range(int(sub_frames)):
919            for k, analog in enumerate(analog_names):
920                data = self.data['ANALOGS'][analog][frame_num*sub_frames+j]
921                c = ((data/gen_scale/scales[k]))+offsets[k]
922                if is_float:
923                    self.__decoder.write_float(c)
924                else:
925                    self.__decoder.write_uint16(c)
926
927    def __write_rotation_frame(self, frame_num, rotation_names):
928        for i, name in enumerate(rotation_names):
929            r = self.data['ROTATIONS'][name][frame_num, :, :].transpose()
930            for j in range(4):
931                for k in range(4):
932                    self.__decoder.write_float(r[j,k])
class Metadata:
17class Metadata:
18    """Base class for Parameters and ParameterGroups.
19    
20    Handles common functionality for C3D metadata elements including
21    name, description, and group ID management.
22    """
23
24    def __init__(self, name:str, group_id:int):
25        """Initialize metadata object.
26        
27        Args:
28            name (str): Name of the metadata element
29            group_id (int): ID of the parameter group
30        """
31        self.name = name
32        self.description = ''
33        self.group_id = group_id
34
35    def read_from_buffer(self, buffer:ProcStream):
36        """Read metadata from binary buffer.
37        
38        Args:
39            buffer (ProcStream): Binary data stream
40            
41        Returns:
42            int: Number of bytes read
43        """
44        return 0
45
46    def write_to_buffer(self, buffer: ProcStream):
47        """Write metadata to binary buffer.
48        
49        Args:
50            buffer (ProcStream): Binary data stream to write to
51        """
52        buffer.write_int8(len(self.name))
53        buffer.write_int8(self.group_id)
54        buffer.write_string(self.name)
55
56    def _get_offset(self):
57        """Calculate byte offset for metadata structure.
58        
59        Returns:
60            int: Offset in bytes
61        """
62        return 2 + len(self.description) + 1
63
64    def get_size(self):
65        """Calculate total size of metadata structure.
66        
67        Returns:
68            int: Total size in bytes
69        """
70        return 2 + len(self.name) + self._get_offset()

Base class for Parameters and ParameterGroups.

Handles common functionality for C3D metadata elements including name, description, and group ID management.

Metadata(name: str, group_id: int)
24    def __init__(self, name:str, group_id:int):
25        """Initialize metadata object.
26        
27        Args:
28            name (str): Name of the metadata element
29            group_id (int): ID of the parameter group
30        """
31        self.name = name
32        self.description = ''
33        self.group_id = group_id

Initialize metadata object.

Args: name (str): Name of the metadata element group_id (int): ID of the parameter group

name
description
group_id
def read_from_buffer(self, buffer: pupyC3D.decoder.ProcStream):
35    def read_from_buffer(self, buffer:ProcStream):
36        """Read metadata from binary buffer.
37        
38        Args:
39            buffer (ProcStream): Binary data stream
40            
41        Returns:
42            int: Number of bytes read
43        """
44        return 0

Read metadata from binary buffer.

Args: buffer (ProcStream): Binary data stream

Returns: int: Number of bytes read

def write_to_buffer(self, buffer: pupyC3D.decoder.ProcStream):
46    def write_to_buffer(self, buffer: ProcStream):
47        """Write metadata to binary buffer.
48        
49        Args:
50            buffer (ProcStream): Binary data stream to write to
51        """
52        buffer.write_int8(len(self.name))
53        buffer.write_int8(self.group_id)
54        buffer.write_string(self.name)

Write metadata to binary buffer.

Args: buffer (ProcStream): Binary data stream to write to

def get_size(self):
64    def get_size(self):
65        """Calculate total size of metadata structure.
66        
67        Returns:
68            int: Total size in bytes
69        """
70        return 2 + len(self.name) + self._get_offset()

Calculate total size of metadata structure.

Returns: int: Total size in bytes

class Parameter(Metadata):
 73class Parameter(Metadata):
 74    """Represents a C3D parameter with typed data.
 75    
 76    Parameters can store scalar values or multi-dimensional arrays
 77    of various types (int8, uint16, float, string).
 78    """
 79
 80    def __init__(self, name:str, group_id:int):
 81        """Initialize parameter.
 82        
 83        Args:
 84            name (str): Parameter name
 85            group_id (int): ID of the parameter group
 86        """
 87        super(Parameter, self).__init__(name, group_id)
 88        self.data_type = 0
 89        self.value = None
 90
 91    def read_from_buffer(self, buffer:ProcStream):
 92        """Read parameter data from binary buffer.
 93        
 94        Supports scalar and multi-dimensional array data of types:
 95        - int8 (data_type=1)
 96        - uint16 (data_type=2) 
 97        - float (data_type=4)
 98        - string (data_type=-1)
 99        
100        Args:
101            buffer (ProcStream): Binary data stream
102            
103        Returns:
104            int: Number of bytes read
105        """
106        offset = 0
107        self.data_type = buffer.get_int8()
108        offset += 1
109        n_dim = buffer.get_int8()
110        offset += 1
111        if n_dim == 0:
112            if self.data_type == 1:
113                self.value = buffer.get_int8()
114            elif self.data_type == 2:
115                self.value = buffer.get_uint16()
116            elif self.data_type == 4:
117                self.value = buffer.get_float()
118            data_size = abs(self.data_type)
119        else:
120            dims = []
121            for i in range(n_dim):
122                dims.append(buffer.get_uint8())
123                offset += 1
124            prod = math.prod(dims[:n_dim])
125            if self.data_type == -1:
126                if len(dims) >= 2:
127                    row = 1
128                    inc2 = 1
129                    while inc2 < n_dim:
130                        row *= dims[inc2]
131                        inc2 += 1
132                    data = []
133                    for i in range(row):
134                        data.append(buffer.get_string(dims[0]).strip())
135                    data = np.array(data)
136                    if data.size == 0:
137                        data = np.empty((dims[:]), str)
138                else:
139                    data = np.array([buffer.get_string(prod).strip()])
140            elif self.data_type == 1:
141                data = np.array([buffer.get_int8() for _ in range(prod)]).reshape(dims)
142            elif self.data_type == 2:
143                data = np.array([buffer.get_uint16() for _ in range(prod)]).reshape(dims)
144            elif self.data_type == 4:
145                data = np.array([buffer.get_float() for _ in range(prod)]).reshape(dims)
146            else:
147                data = np.array([])
148
149            self.value = data
150            data_size = prod * abs(self.data_type)
151        offset += data_size
152        desc_len = buffer.get_uint8()
153        self.description = buffer.get_string(desc_len)
154        offset += desc_len
155        return offset
156
157    def write_to_buffer(self, buffer: ProcStream, last_entry=False):
158        super(Parameter, self).write_to_buffer(buffer)
159        if last_entry:
160            offset = 0
161        else:
162            offset = self._get_offset()
163        buffer.write_uint16(offset)
164        buffer.write_int8(self.data_type)
165
166        dims = self.__get_dim()
167        n_dim = len(dims)
168        buffer.write_int8(n_dim)
169        if n_dim == 0:
170            if self.data_type == 1:
171                buffer.write_int8(self.value)
172            elif self.data_type == 2:
173                buffer.write_uint16(self.value)
174            elif self.data_type == 4:
175                buffer.write_float(self.value)
176        else:
177            for each in dims:
178                buffer.write_uint8(each)
179            data = self.value.flatten()
180            for each in data:
181                if self.data_type == 1:
182                    buffer.write_int8(each)
183                elif self.data_type == 2:
184                    buffer.write_uint16(each)
185                elif self.data_type == 4:
186                    buffer.write_float(each)
187                elif self.data_type == -1:
188                    buffer.write_string(each.ljust(dims[0]))
189
190        buffer.write_uint8(len(self.description))
191        buffer.write_string(self.description)
192
193    def _get_offset(self):
194        offset = 2 + 1 + 1 #offset, data_type, dim
195        if not isinstance(self.value, np.ndarray):
196            offset += self.data_type#value
197        else:
198            # n_dimension
199            dims = self.__get_dim()
200            n_dim = len(dims)
201            offset += n_dim
202            # data size
203            prod = math.prod(dims[:n_dim])
204            data_size = prod * abs(self.data_type)
205            offset += data_size
206
207        offset +=1 # desc length
208        offset += len(self.description) #desc
209        return offset
210
211    def __get_dim(self):
212        if not isinstance(self.value, np.ndarray):
213            dims  = []
214        else:
215            dims = self.value.shape
216            if self.data_type == -1:
217                if dims[0] == 1:
218                    dims = [len(self.value[0])]
219                else:
220                    if self.value.size > 0:
221                        d1 = max([len(x) for x in self.value])
222                        dims = [d1, dims[0]]
223        return dims

Represents a C3D parameter with typed data.

Parameters can store scalar values or multi-dimensional arrays of various types (int8, uint16, float, string).

Parameter(name: str, group_id: int)
80    def __init__(self, name:str, group_id:int):
81        """Initialize parameter.
82        
83        Args:
84            name (str): Parameter name
85            group_id (int): ID of the parameter group
86        """
87        super(Parameter, self).__init__(name, group_id)
88        self.data_type = 0
89        self.value = None

Initialize parameter.

Args: name (str): Parameter name group_id (int): ID of the parameter group

data_type
value
def read_from_buffer(self, buffer: pupyC3D.decoder.ProcStream):
 91    def read_from_buffer(self, buffer:ProcStream):
 92        """Read parameter data from binary buffer.
 93        
 94        Supports scalar and multi-dimensional array data of types:
 95        - int8 (data_type=1)
 96        - uint16 (data_type=2) 
 97        - float (data_type=4)
 98        - string (data_type=-1)
 99        
100        Args:
101            buffer (ProcStream): Binary data stream
102            
103        Returns:
104            int: Number of bytes read
105        """
106        offset = 0
107        self.data_type = buffer.get_int8()
108        offset += 1
109        n_dim = buffer.get_int8()
110        offset += 1
111        if n_dim == 0:
112            if self.data_type == 1:
113                self.value = buffer.get_int8()
114            elif self.data_type == 2:
115                self.value = buffer.get_uint16()
116            elif self.data_type == 4:
117                self.value = buffer.get_float()
118            data_size = abs(self.data_type)
119        else:
120            dims = []
121            for i in range(n_dim):
122                dims.append(buffer.get_uint8())
123                offset += 1
124            prod = math.prod(dims[:n_dim])
125            if self.data_type == -1:
126                if len(dims) >= 2:
127                    row = 1
128                    inc2 = 1
129                    while inc2 < n_dim:
130                        row *= dims[inc2]
131                        inc2 += 1
132                    data = []
133                    for i in range(row):
134                        data.append(buffer.get_string(dims[0]).strip())
135                    data = np.array(data)
136                    if data.size == 0:
137                        data = np.empty((dims[:]), str)
138                else:
139                    data = np.array([buffer.get_string(prod).strip()])
140            elif self.data_type == 1:
141                data = np.array([buffer.get_int8() for _ in range(prod)]).reshape(dims)
142            elif self.data_type == 2:
143                data = np.array([buffer.get_uint16() for _ in range(prod)]).reshape(dims)
144            elif self.data_type == 4:
145                data = np.array([buffer.get_float() for _ in range(prod)]).reshape(dims)
146            else:
147                data = np.array([])
148
149            self.value = data
150            data_size = prod * abs(self.data_type)
151        offset += data_size
152        desc_len = buffer.get_uint8()
153        self.description = buffer.get_string(desc_len)
154        offset += desc_len
155        return offset

Read parameter data from binary buffer.

Supports scalar and multi-dimensional array data of types:

  • int8 (data_type=1)
  • uint16 (data_type=2)
  • float (data_type=4)
  • string (data_type=-1)

Args: buffer (ProcStream): Binary data stream

Returns: int: Number of bytes read

def write_to_buffer(self, buffer: pupyC3D.decoder.ProcStream, last_entry=False):
157    def write_to_buffer(self, buffer: ProcStream, last_entry=False):
158        super(Parameter, self).write_to_buffer(buffer)
159        if last_entry:
160            offset = 0
161        else:
162            offset = self._get_offset()
163        buffer.write_uint16(offset)
164        buffer.write_int8(self.data_type)
165
166        dims = self.__get_dim()
167        n_dim = len(dims)
168        buffer.write_int8(n_dim)
169        if n_dim == 0:
170            if self.data_type == 1:
171                buffer.write_int8(self.value)
172            elif self.data_type == 2:
173                buffer.write_uint16(self.value)
174            elif self.data_type == 4:
175                buffer.write_float(self.value)
176        else:
177            for each in dims:
178                buffer.write_uint8(each)
179            data = self.value.flatten()
180            for each in data:
181                if self.data_type == 1:
182                    buffer.write_int8(each)
183                elif self.data_type == 2:
184                    buffer.write_uint16(each)
185                elif self.data_type == 4:
186                    buffer.write_float(each)
187                elif self.data_type == -1:
188                    buffer.write_string(each.ljust(dims[0]))
189
190        buffer.write_uint8(len(self.description))
191        buffer.write_string(self.description)

Write metadata to binary buffer.

Args: buffer (ProcStream): Binary data stream to write to

class ParameterGroup(Metadata):
226class ParameterGroup(Metadata):
227    """Represents a group of related C3D parameters.
228    
229    Parameter groups organize parameters by functionality
230    (e.g., POINT, ANALOG, TRIAL groups).
231    """
232
233    def __init__(self, name:str, group_id:int):
234        """Initialize parameter group.
235        
236        Args:
237            name (str): Group name
238            group_id (int): Unique group identifier
239        """
240        super(ParameterGroup, self).__init__(name, group_id)
241        self.parameters = dict()
242
243    def add_parameter(self, name)->Parameter:
244        """Add a new parameter to this group.
245        
246        Args:
247            name (str): Parameter name
248            
249        Returns:
250            Parameter: New or existing parameter
251        """
252        if name not in self.parameters:
253            param = Parameter(name, -self.group_id)
254            self.parameters[param.name] = param
255            return param
256        warnings.warn('Parameter %s already exists' %name)
257        return self.parameters[name]
258
259    def remove_parameter(self, name):
260        """Remove a parameter from this group.
261        
262        Args:
263            name (str): Parameter name to remove
264            
265        Returns:
266            bool: True if parameter was removed, False if not found
267        """
268        if name in self.parameters:
269            self.parameters.pop(name)
270            return True
271        return False
272
273    def get_parameter(self, name):
274        """Get a parameter by name.
275        
276        Args:
277            name (str): Parameter name
278            
279        Returns:
280            Parameter or None: Parameter object if found
281        """
282        if name not in self.parameters:
283            return None
284        return self.parameters[name]
285
286    def read_from_buffer(self, buffer:ProcStream):
287        desc_len = buffer.get_uint8()
288        offset = 1
289        desc = buffer.get_string(desc_len)
290        offset += desc_len
291        self.description = desc
292        return offset
293
294    def write_to_buffer(self, buffer: ProcStream, last_entry=False):
295        super(ParameterGroup, self).write_to_buffer(buffer)
296        # offset = 2 + len(self.description) + 1
297        offset = self._get_offset()
298        buffer.write_uint16(offset)
299        buffer.write_uint8(len(self.description))
300        buffer.write_string(self.description)

Represents a group of related C3D parameters.

Parameter groups organize parameters by functionality (e.g., POINT, ANALOG, TRIAL groups).

ParameterGroup(name: str, group_id: int)
233    def __init__(self, name:str, group_id:int):
234        """Initialize parameter group.
235        
236        Args:
237            name (str): Group name
238            group_id (int): Unique group identifier
239        """
240        super(ParameterGroup, self).__init__(name, group_id)
241        self.parameters = dict()

Initialize parameter group.

Args: name (str): Group name group_id (int): Unique group identifier

parameters
def add_parameter(self, name) -> Parameter:
243    def add_parameter(self, name)->Parameter:
244        """Add a new parameter to this group.
245        
246        Args:
247            name (str): Parameter name
248            
249        Returns:
250            Parameter: New or existing parameter
251        """
252        if name not in self.parameters:
253            param = Parameter(name, -self.group_id)
254            self.parameters[param.name] = param
255            return param
256        warnings.warn('Parameter %s already exists' %name)
257        return self.parameters[name]

Add a new parameter to this group.

Args: name (str): Parameter name

Returns: Parameter: New or existing parameter

def remove_parameter(self, name):
259    def remove_parameter(self, name):
260        """Remove a parameter from this group.
261        
262        Args:
263            name (str): Parameter name to remove
264            
265        Returns:
266            bool: True if parameter was removed, False if not found
267        """
268        if name in self.parameters:
269            self.parameters.pop(name)
270            return True
271        return False

Remove a parameter from this group.

Args: name (str): Parameter name to remove

Returns: bool: True if parameter was removed, False if not found

def get_parameter(self, name):
273    def get_parameter(self, name):
274        """Get a parameter by name.
275        
276        Args:
277            name (str): Parameter name
278            
279        Returns:
280            Parameter or None: Parameter object if found
281        """
282        if name not in self.parameters:
283            return None
284        return self.parameters[name]

Get a parameter by name.

Args: name (str): Parameter name

Returns: Parameter or None: Parameter object if found

def read_from_buffer(self, buffer: pupyC3D.decoder.ProcStream):
286    def read_from_buffer(self, buffer:ProcStream):
287        desc_len = buffer.get_uint8()
288        offset = 1
289        desc = buffer.get_string(desc_len)
290        offset += desc_len
291        self.description = desc
292        return offset

Read metadata from binary buffer.

Args: buffer (ProcStream): Binary data stream

Returns: int: Number of bytes read

def write_to_buffer(self, buffer: pupyC3D.decoder.ProcStream, last_entry=False):
294    def write_to_buffer(self, buffer: ProcStream, last_entry=False):
295        super(ParameterGroup, self).write_to_buffer(buffer)
296        # offset = 2 + len(self.description) + 1
297        offset = self._get_offset()
298        buffer.write_uint16(offset)
299        buffer.write_uint8(len(self.description))
300        buffer.write_string(self.description)

Write metadata to binary buffer.

Args: buffer (ProcStream): Binary data stream to write to

class C3DFile:
303class C3DFile:
304    """Main class for reading and writing C3D files.
305    
306    C3D files contain 3D coordinate data, parameters, and metadata
307    commonly used in biomechanics and motion capture applications.
308    
309    Attributes:
310        filename (str): Path to the C3D file
311        header (dict): File header information
312        groups (dict): Parameter groups indexed by group ID
313        data (dict): Point and analog data
314    """
315
316    def __init__(self, filename:str=''):
317        """Initialize C3DFile object.
318        
319        Args:
320            filename (str, optional): Path to C3D file. If file exists,
321                it will be automatically loaded.
322        """
323        self.filename = filename
324        self.__decoder = None
325        self.header = dict()
326        self.groups = dict()
327        self.data = dict()
328        if os.path.exists(filename):
329            self.read_file()
330
331    def read_file(self):
332        """
333        Read the C3D file associated with self.filename
334        """
335        if os.path.exists(self.filename):
336            with open(self.filename, 'rb') as handle:
337                self.__read_header(handle)
338                self.__read_parameters(handle)
339                self.__read_data(handle)
340        else:
341            raise FileNotFoundError(self.filename)
342
343    def write(self, filename:str='', **kwargs):
344        """Write C3D data to file.
345        
346        Args:
347            filename (str, optional): Output file path. Defaults to self.filename.
348            **kwargs: Additional options:
349                overwrite (bool): Allow overwriting existing file. Defaults to False.
350                
351        Raises:
352            Warning: If file exists and overwrite=False
353        """
354        overwrite = kwargs.get('overwrite', False)
355        if filename == '':
356            filename = self.filename
357        if os.path.exists(filename) and not overwrite:
358            warnings.warn('File %s already exist. If you wish to overwrite it set ''overwrite'' argument to True' %filename)
359            return
360        with open(filename, 'wb') as handle:
361            self.__write_header(handle)
362            self.__write_parameters(handle)
363            self.__write_data(handle)
364
365    def add_parameter_group(self, name, gid=0)->ParameterGroup:
366        """Add a new parameter group.
367        
368        Args:
369            name (str): Group name
370            gid (int, optional): Group ID. If 0, auto-assigned. Defaults to 0.
371            
372        Returns:
373            ParameterGroup or None: New group if created, None if already exists
374        """
375        assert(gid <= 0)
376        if gid == 0:
377            g = self.get_parameter_group(name)
378        else:
379            g = self.get_parameter_group(gid)
380        if g is not None:
381            return None
382        if gid == 0:
383            ids = list(self.groups.keys())
384            gid = ids[0]
385            # find first missing id
386            for number in ids:
387                if number != gid:
388                    break
389                gid -= 1
390        self.groups[gid] = ParameterGroup(name, gid)
391        return self.groups[gid]
392
393    def remove_parameter_group(self, group)->bool:
394        """Remove a parameter group.
395        
396        Args:
397            group (int or str): Group ID or name
398            
399        Returns:
400            bool: True if group was removed, False if not found
401        """
402        g = self.get_parameter_group(group)
403        if g is not None:
404            self.groups.pop(g.group_id)
405            return True
406        return False
407
408    def get_parameter_group(self, group_id)->ParameterGroup:
409        """Get a parameter group by ID or name.
410        
411        Args:
412            group_id (int or str): Group ID or name
413            
414        Returns:
415            ParameterGroup or None: Parameter group if found
416            
417        Raises:
418            TypeError: If group_id is neither int nor str
419        """
420        if isinstance(group_id, int):
421            return self.groups.get(group_id, None)
422        elif isinstance(group_id, str):
423            g = {v.name: v for v in self.groups.values()}
424            return g.get(group_id, None)
425        else:
426            raise TypeError('Argument ''group_id'' should be either str or int')
427
428    def add_parameter(self, name, gid):
429        group = self.get_parameter_group(gid)
430        if group is not None:
431            param = group.add_parameter(name)
432            return param
433        return None
434
435    def remove_parameter(self, name, group_id):
436        g = self.get_parameter_group(group_id)
437        if g is not None:
438            return g.remove_parameter(name)
439        return False
440
441    def get_parameter(self, group_id, param_name):
442        g = self.get_parameter_group(group_id)
443        if g is not None:
444            return g.parameters.get(param_name, None)
445        return None
446
447    def get_point_data(self, name: str):
448        """
449        Return trajectory of a point
450        :param name: the point name in string
451        :return: trajectory as np.ndarray (NbFrames X 3)
452        """
453        if name not in self.data['POINTS']:
454            raise ValueError('Point %s does not exists!' %name)
455        return self.data['POINTS'][name]
456
457    def get_points_data(self, name_list:list[str]):
458        """
459        Get several points trajectories
460        :param name_list: list of the points names
461        :return: dict of name:trajectories data
462        """
463        return {name: self.get_point_data(name) for name in name_list}
464
465    def get_point_names(self):
466        """
467        Get a list of all points in the c3d
468        :return: List[str] with all points labels
469        """
470        return list(self.data['POINTS'].keys())
471
472    def get_analog_data(self, name: str):
473        """
474        Return values for an analog chanel
475        :param name: analog name in string
476        :return: analog chanel values as np.ndarray(NbAnalogFrames X 1)
477        """
478        if name not in self.data['ANALOGS']:
479            raise ValueError('Chanel %s does not exists!' %name)
480        return self.data['ANALOGS'][name]
481
482    def get_analogs_data(self, name_list:list[str]):
483        """
484        Get several analog channels
485        :param name_list: list of the analogs names
486        :return: dict of name:values data
487        """
488        return {name: self.get_analog_data(name) for name in name_list}
489
490    def get_analog_names(self):
491        """
492        Get a list of all analogs chanels in the c3d
493        :return: List[str] with all analogs labels
494        """
495        return list(self.data['ANALOGS'].keys())
496
497    def get_rotation_data(self, name:str):
498        """
499        Return rotation data (mainly for c3d created by theia markerless software)
500        :param name: Joint name
501        :return: rotation and position data np.ndarray(NbFrames X 4 X 4)
502        """
503        if name not in self.data['ROTATIONS']:
504            raise ValueError('Rotation %s does not exists!' %name)
505        return self.data['ROTATIONS'][name]
506
507    def get_rotations_data(self, name_list:list[str]):
508        """
509        Get several rotations data
510        :param name_list: list of the rotations
511        :return: dict of name:values data
512        """
513        return {name: self.get_rotation_data(name) for name in name_list}
514
515    def get_rotation_names(self):
516        """
517        Get a list of all rotation data in the c3d
518        :return: List[str] with all rotations labels
519        """
520        return list(self.data['ROTATIONS'].keys())
521
522    @property
523    def point_count(self):
524        """
525        Get the number of 3D points in the c3d
526        :return: int
527        """
528        p = self.get_parameter('POINT', 'USED')
529        return p.value
530
531    @property
532    def analog_count(self):
533        """
534        Get the number of analog channels in the c3d
535        :return: int
536        """
537        p = self.get_parameter('ANALOG', 'USED')
538        if p is not None:
539            return p.value
540        return 0
541
542    @property
543    def frame_count(self):
544        """
545        Get the number of 3D points frames
546        :return: int
547        """
548        return self.header['last_frame'] - self.header['first_frame'] + 1
549
550    @property
551    def analog_frame_count(self):
552        """
553        Get the number of analog frames
554        :return: int
555        """
556        if self.analog_count > 0:
557            return self.frame_count * self.header['analog_per_frame']
558        return 0
559
560    @property
561    def frame_rate(self):
562        """
563        Get the 3D points frame rate
564        :return: float
565        """
566        p = self.get_parameter('POINT', 'RATE')
567        return p.value
568
569    @property
570    def analog_rate(self):
571        """
572        Get the analog channels frame rate
573        :return: float
574        """
575        p = self.get_parameter('ANALOG', 'RATE')
576        if p is not None:
577            return p.value
578        return 0
579
580    @property
581    def point_unit(self):
582        """
583        Get the 3D points unit (usually mm or m)
584        :return: str
585        """
586        p = self.get_parameter('POINT', 'UNITS')
587        return p.value
588
589    @property
590    def analog_unit(self):
591        """
592        Get the analog channels unit (mV, V, ...)
593        :return: str
594        """
595        p = self.get_parameter('ANALOG', 'UNITS')
596        if p is not None:
597            return p.value
598        return []
599
600    def __read_header(self, handle):
601        handle.seek(0)
602        # check if it's a c3d file
603        self.header['parameter_block'], magic = struct.unpack('BB', handle.read(2))
604        if magic != 80:
605            warnings.warn('%s is not a c3d file!' %self.filename)
606            return
607        # go to the start of the parameter block
608        handle.seek((self.header['parameter_block'] - 1) * 512 + 3)
609        # find the good encoder
610        processor = struct.unpack('B', handle.read(1))[0]
611        if processor == PROCESSOR_INTEL:
612            self.__decoder = DecoderIntel(handle)
613        elif processor == PROCESSOR_DEC:
614            self.__decoder = DecoderDec(handle)
615        elif processor == PROCESSOR_MIPS:
616            self.__decoder = DecoderMips(handle)
617
618        #  start reading header
619        handle.seek(2)
620        self.header['point_count'] = self.__decoder.get_uint16()
621        self.header['analog_count'] = self.__decoder.get_uint16()
622        self.header['first_frame'] = self.__decoder.get_uint16()
623        self.header['last_frame'] = self.__decoder.get_uint16()
624        self.header['max_gap'] = self.__decoder.get_uint16()
625        self.header['scale_factor'] = self.__decoder.get_float()
626        self.header['data_block'] = self.__decoder.get_uint16()
627        self.header['analog_per_frame'] = self.__decoder.get_uint16()
628        if self.header['analog_per_frame'] > 0 and self.header['analog_count'] > 0:
629            self.header['analog_count'] /= self.header['analog_per_frame']
630            self.header['analog_count'] = int(self.header['analog_count'])
631        if self.header['analog_per_frame'] == 0:
632            self.header['analog_per_frame'] = 1
633        self.header['frame_rate'] = self.__decoder.get_float()
634
635        self.header['events'] = dict()
636        handle.read(270)
637        self.header['events']['label_range_section'] = self.__decoder.get_uint16()
638        self.header['events']['label_first_block'] = self.__decoder.get_uint16()
639        self.header['events']['label_event_fmt'] = self.__decoder.get_uint16()
640        if self.header['events']['label_event_fmt'] == 12345:
641            self.header['events']['long_event_labels'] = True
642        else:
643            self.header['events']['long_event_labels'] = False
644        self.header['events']['num_events'] = self.__decoder.get_uint16()
645        if self.header['events']['num_events'] > 0:
646            self.header['events']['data'] = {'labels': [], 'time': [], 'display': []}
647            handle.read(2)
648            for i in range(self.header['events']['num_events']):
649                self.header['events']['data']['time'].append(self.__decoder.get_float())
650            handle.seek(198)
651            print(handle.tell())
652            for i in range(self.header['events']['num_events']):
653                self.header['events']['data']['display'].append(self.__decoder.get_uint8())
654                # print(self.header['events']['data']['display'][i])
655            handle.seek(198 * 2)
656            if self.header['events']['long_event_labels']:
657                num_char = 4
658            else:
659                num_char = 2
660            for i in range(self.header['events']['num_events']):
661                name = self.__decoder.get_string(num_char)
662                self.header['events']['data']['labels'].append(name)
663
664    def __write_header(self, handle):
665        if self.__decoder == 0:
666            self.__decoder = DecoderIntel(handle)
667        else:
668            self.__decoder.handle = handle
669
670        handle.seek(0)
671        self.__decoder.write_uint8(self.header['parameter_block'])
672        self.__decoder.write_uint8(80)
673
674        if isinstance(self.__decoder, DecoderIntel):
675            processor = PROCESSOR_INTEL
676        elif isinstance(self.__decoder, DecoderDec):
677            processor = PROCESSOR_DEC
678        elif isinstance(self.__decoder, DecoderMips):
679            processor = PROCESSOR_MIPS
680        else:
681            raise ValueError('Processor not supported')
682
683        handle.seek((self.header['parameter_block'] - 1) * 512 + 3)
684        self.__decoder.write_uint8(processor)
685
686        handle.seek(2)
687        p = self.get_parameter('POINT', 'USED')
688        self.__decoder.write_uint16(p.value)
689        if self.header['analog_per_frame'] > 0 and self.header['analog_count'] > 0:
690            analog_count = self.header['analog_count']* self.header['analog_per_frame']
691        else:
692            analog_count = self.header['analog_count']
693        self.__decoder.write_uint16(analog_count)
694        self.__decoder.write_uint16(self.header['first_frame'])
695        self.__decoder.write_uint16(self.header['last_frame'])
696        self.__decoder.write_uint16(self.header['max_gap'])
697        self.__decoder.write_float(self.header['scale_factor'])
698        data_block = self.__get_parameters_blocknum() + 2
699        self.__decoder.write_uint16(data_block)
700        if self.header['analog_per_frame'] == 1:
701            analog_per_frame = 0
702        else:
703            analog_per_frame = self.header['analog_per_frame']
704        self.__decoder.write_uint16(analog_per_frame)
705        self.__decoder.write_float(self.header['frame_rate'])
706
707        handle.seek(handle.tell() + 270)
708        self.__decoder.write_uint16(self.header['events']['label_range_section'])
709        self.__decoder.write_uint16(self.header['events']['label_first_block'])
710        self.__decoder.write_uint16(self.header['events']['label_event_fmt'])
711        self.__decoder.write_uint16(self.header['events']['num_events'])
712        if self.header['events']['num_events'] > 0:
713            handle.seek(handle.tell() + 2)
714            for i in range(self.header['events']['num_events']):
715                self.__decoder.write_float(self.header['events']['data']['time'][i])
716            handle.seek(198)
717            print(handle.tell())
718            for i in range(self.header['events']['num_events']):
719                self.__decoder.write_uint8(self.header['events']['data']['display'][i])
720            handle.seek(198 * 2)
721            if self.header['events']['long_event_labels']:
722                num_char = 4
723            else:
724                num_char = 2
725            for i in range(self.header['events']['num_events']):
726                name = self.header['events']['data']['labels'][i].ljust(num_char)
727                self.__decoder.write_string(name)
728
729    def __read_parameters(self, handle):
730        handle.seek((self.header['parameter_block'] - 1) * 512)
731        handle.read(4)
732        last_entry = False
733        while not last_entry:
734            nb_char_label = self.__decoder.get_int8()
735            if nb_char_label == 0:
736                break
737            group_id = self.__decoder.get_int8()
738            name = self.__decoder.get_string(abs(nb_char_label))
739            if group_id < 0:#group
740                current = self.add_parameter_group(name, group_id)
741            else:#parameter
742                group = self.get_parameter_group(-group_id)
743                if group is not None:
744                    current = group.add_parameter(name)
745                else :
746                    current = Parameter(name, group_id)
747
748            offset = self.__decoder.get_uint16()
749            if offset == 0:
750                last_entry = True
751            offset -= 2
752            offset -= current.read_from_buffer(self.__decoder)
753
754    def __write_parameters(self, handle):
755        handle.seek((self.header['parameter_block'] - 1) * 512 + 2)
756        self.__decoder.write_uint8(self.__get_parameters_blocknum())
757        handle.seek((self.header['parameter_block'] - 1) * 512+4)
758        groups = list(self.groups.values())
759        for g in groups:
760            g.write_to_buffer(self.__decoder)
761        for group in groups:
762            params = list(group.parameters.values())
763            for param in params:
764                if group == groups[-1] and param == params[-1]:
765                    param.write_to_buffer(self.__decoder, True)
766                else:
767                    param.write_to_buffer(self.__decoder)
768
769    def __get_parameters_blocknum(self):
770        parameters_size = 0
771        for g in self.groups.values():
772            parameters_size += g.get_size()
773            for p in g.parameters.values():
774                parameters_size += p.get_size()
775        return math.ceil(parameters_size/512)
776
777    def __read_data(self, handle):
778        handle.seek((512 * (self.header['data_block'] - 1)))
779        nb_frames = self.header['last_frame'] - self.header['first_frame'] + 1
780        point_used = self.header['point_count']
781        scale = abs(self.header['scale_factor'])
782        is_float = self.header['scale_factor'] < 0
783        if point_used > 0:
784            point_scale = [scale, 1][is_float]
785            names_param = self.get_parameter('POINT', 'LABELS')
786            marker_names = names_param.value.tolist()
787            self.data['POINTS'] = dict()
788            for each in marker_names:
789                self.data['POINTS'][each] = np.zeros([nb_frames, 4])
790
791        analog_used = self.header['analog_count']
792        if analog_used > 0:
793            offsets = np.zeros((analog_used,), int)
794            param = self.get_parameter('ANALOG', 'OFFSET')
795            if param is not None:
796                offsets = param.value
797
798            scales = np.ones((analog_used,), float)
799            param = self.get_parameter('ANALOG', 'SCALE')
800            if param is not None:
801                scales = param.value
802
803            gen_scale = 1.
804            param = self.get_parameter('ANALOG', 'GEN_SCALE')
805            if param is not None:
806                gen_scale = param.value
807            names_param = self.get_parameter('ANALOG', 'LABELS')
808            if names_param is not None:
809                analog_names = names_param.value.tolist()
810
811            self.data['ANALOGS'] = dict()
812            for each in analog_names:
813                self.data['ANALOGS'][each] = np.zeros([nb_frames*self.header['analog_per_frame'], 1])
814
815        rot_param = self.get_parameter_group('ROTATION')
816        if rot_param is not None:
817            names_param = self.get_parameter('ROTATION', 'LABELS')
818            rotation_names = names_param.value.tolist()
819            self.data['ROTATIONS'] = dict()
820            for each in rotation_names:
821                self.data['ROTATIONS'][each] = np.zeros([nb_frames, 4, 4])
822
823        for i in range(nb_frames):
824            if point_used > 0:
825                self.__read_point_frame(i, is_float, point_scale, marker_names)
826            if rot_param is not None:
827                self.__read_rotation_frame(i, rotation_names)
828            if analog_used >0:
829                self.__read_analog_frame(i, is_float, self.header['analog_per_frame'], offsets, scales, gen_scale, analog_names)
830
831    def __read_point_frame(self, frame_num, is_float, point_scale, marker_names):
832        for j, m in enumerate(marker_names):
833            if is_float:
834                p = np.array([self.__decoder.get_float(), self.__decoder.get_float(), self.__decoder.get_float(),
835                              self.__decoder.get_float()])
836            else:
837                p = np.array([self.__decoder.get_uint16(), self.__decoder.get_uint16(), self.__decoder.get_uint16(),
838                              self.__decoder.get_uint16()])
839            p = p * point_scale
840            self.data['POINTS'][m][frame_num, :] = p
841
842    def __read_analog_frame(self, frame_num, is_float, sub_frames, offsets, scales, gen_scale, analog_names):
843        if frame_num == 315:
844            stop = 1
845        for j in range(int(sub_frames)):
846            for k, analog in enumerate(analog_names):
847                if is_float:
848                    c = self.__decoder.get_float()
849                else:
850                    c = self.__decoder.get_uint16()
851                self.data['ANALOGS'][analog][frame_num*sub_frames+j] = (c-offsets[k]) * scales[k]*gen_scale
852
853    def __read_rotation_frame(self, frame_num, rotation_names):
854        for i, name in enumerate(rotation_names):
855            p = np.array([[self.__decoder.get_float(), self.__decoder.get_float(), self.__decoder.get_float(),
856                           self.__decoder.get_float()],
857                          [self.__decoder.get_float(), self.__decoder.get_float(), self.__decoder.get_float(),
858                           self.__decoder.get_float()],
859                          [self.__decoder.get_float(), self.__decoder.get_float(), self.__decoder.get_float(),
860                           self.__decoder.get_float()],
861                          [self.__decoder.get_float(), self.__decoder.get_float(), self.__decoder.get_float(),
862                           self.__decoder.get_float()]])
863            self.data['ROTATIONS'][name][frame_num, :, :] = p.transpose()
864
865    def __write_data(self, handle):
866        handle.seek((512 * (self.header['data_block'] - 1)))
867        point_used = self.header['point_count']>0
868        if point_used:
869            scale = abs(self.header['scale_factor'])
870            is_float = self.header['scale_factor'] < 0
871            point_scale = [scale, 1][is_float]
872            names_param = self.get_parameter('POINT', 'LABELS')
873            marker_names = names_param.value.tolist()
874
875        analog_used = self.header['analog_count']
876        if analog_used > 0:
877            offsets = np.zeros((analog_used,), int)
878            param = self.get_parameter('ANALOG', 'OFFSET')
879            if param is not None:
880                offsets = param.value
881
882            scales = np.ones((analog_used,), float)
883            param = self.get_parameter('ANALOG', 'SCALE')
884            if param is not None:
885                scales = param.value
886
887            gen_scale = 1.
888            param = self.get_parameter('ANALOG', 'GEN_SCALE')
889            if param is not None:
890                gen_scale = param.value
891            names_param = self.get_parameter('ANALOG', 'LABELS')
892            if names_param is not None:
893                analog_names = names_param.value.tolist()
894
895        rot_param = self.get_parameter_group('ROTATION')
896        if rot_param is not None:
897            names_param = self.get_parameter('ROTATION', 'LABELS')
898            rotation_names = names_param.value.tolist()
899
900        nb_frames = self.header['last_frame'] - self.header['first_frame'] + 1
901        for i in range(nb_frames):
902            if point_used:
903                self.__write_point_frame(marker_names, i, is_float, point_scale)
904            if rot_param is not None:
905                self.__write_rotation_frame(i, rotation_names)
906            if analog_used:
907                self.__write_analog_frame(i, self.header['analog_per_frame'], analog_names, is_float, scales, gen_scale, offsets)
908
909    def __write_point_frame(self, marker_names, frame_num, is_float, point_scale):
910        for j, m in enumerate(marker_names):
911            p = self.data['POINTS'][m][frame_num, :]/point_scale
912            for i in range(4):
913                if is_float:
914                    self.__decoder.write_float(p[i])
915                else:
916                    self.__decoder.write_uint16(p[i])
917
918    def __write_analog_frame(self, frame_num, sub_frames, analog_names, is_float, scales, gen_scale, offsets):
919        for j in range(int(sub_frames)):
920            for k, analog in enumerate(analog_names):
921                data = self.data['ANALOGS'][analog][frame_num*sub_frames+j]
922                c = ((data/gen_scale/scales[k]))+offsets[k]
923                if is_float:
924                    self.__decoder.write_float(c)
925                else:
926                    self.__decoder.write_uint16(c)
927
928    def __write_rotation_frame(self, frame_num, rotation_names):
929        for i, name in enumerate(rotation_names):
930            r = self.data['ROTATIONS'][name][frame_num, :, :].transpose()
931            for j in range(4):
932                for k in range(4):
933                    self.__decoder.write_float(r[j,k])

Main class for reading and writing C3D files.

C3D files contain 3D coordinate data, parameters, and metadata commonly used in biomechanics and motion capture applications.

Attributes: filename (str): Path to the C3D file header (dict): File header information groups (dict): Parameter groups indexed by group ID data (dict): Point and analog data

C3DFile(filename: str = '')
316    def __init__(self, filename:str=''):
317        """Initialize C3DFile object.
318        
319        Args:
320            filename (str, optional): Path to C3D file. If file exists,
321                it will be automatically loaded.
322        """
323        self.filename = filename
324        self.__decoder = None
325        self.header = dict()
326        self.groups = dict()
327        self.data = dict()
328        if os.path.exists(filename):
329            self.read_file()

Initialize C3DFile object.

Args: filename (str, optional): Path to C3D file. If file exists, it will be automatically loaded.

filename
header
groups
data
def read_file(self):
331    def read_file(self):
332        """
333        Read the C3D file associated with self.filename
334        """
335        if os.path.exists(self.filename):
336            with open(self.filename, 'rb') as handle:
337                self.__read_header(handle)
338                self.__read_parameters(handle)
339                self.__read_data(handle)
340        else:
341            raise FileNotFoundError(self.filename)

Read the C3D file associated with self.filename

def write(self, filename: str = '', **kwargs):
343    def write(self, filename:str='', **kwargs):
344        """Write C3D data to file.
345        
346        Args:
347            filename (str, optional): Output file path. Defaults to self.filename.
348            **kwargs: Additional options:
349                overwrite (bool): Allow overwriting existing file. Defaults to False.
350                
351        Raises:
352            Warning: If file exists and overwrite=False
353        """
354        overwrite = kwargs.get('overwrite', False)
355        if filename == '':
356            filename = self.filename
357        if os.path.exists(filename) and not overwrite:
358            warnings.warn('File %s already exist. If you wish to overwrite it set ''overwrite'' argument to True' %filename)
359            return
360        with open(filename, 'wb') as handle:
361            self.__write_header(handle)
362            self.__write_parameters(handle)
363            self.__write_data(handle)

Write C3D data to file.

Args: filename (str, optional): Output file path. Defaults to self.filename. **kwargs: Additional options: overwrite (bool): Allow overwriting existing file. Defaults to False.

Raises: Warning: If file exists and overwrite=False

def add_parameter_group(self, name, gid=0) -> ParameterGroup:
365    def add_parameter_group(self, name, gid=0)->ParameterGroup:
366        """Add a new parameter group.
367        
368        Args:
369            name (str): Group name
370            gid (int, optional): Group ID. If 0, auto-assigned. Defaults to 0.
371            
372        Returns:
373            ParameterGroup or None: New group if created, None if already exists
374        """
375        assert(gid <= 0)
376        if gid == 0:
377            g = self.get_parameter_group(name)
378        else:
379            g = self.get_parameter_group(gid)
380        if g is not None:
381            return None
382        if gid == 0:
383            ids = list(self.groups.keys())
384            gid = ids[0]
385            # find first missing id
386            for number in ids:
387                if number != gid:
388                    break
389                gid -= 1
390        self.groups[gid] = ParameterGroup(name, gid)
391        return self.groups[gid]

Add a new parameter group.

Args: name (str): Group name gid (int, optional): Group ID. If 0, auto-assigned. Defaults to 0.

Returns: ParameterGroup or None: New group if created, None if already exists

def remove_parameter_group(self, group) -> bool:
393    def remove_parameter_group(self, group)->bool:
394        """Remove a parameter group.
395        
396        Args:
397            group (int or str): Group ID or name
398            
399        Returns:
400            bool: True if group was removed, False if not found
401        """
402        g = self.get_parameter_group(group)
403        if g is not None:
404            self.groups.pop(g.group_id)
405            return True
406        return False

Remove a parameter group.

Args: group (int or str): Group ID or name

Returns: bool: True if group was removed, False if not found

def get_parameter_group(self, group_id) -> ParameterGroup:
408    def get_parameter_group(self, group_id)->ParameterGroup:
409        """Get a parameter group by ID or name.
410        
411        Args:
412            group_id (int or str): Group ID or name
413            
414        Returns:
415            ParameterGroup or None: Parameter group if found
416            
417        Raises:
418            TypeError: If group_id is neither int nor str
419        """
420        if isinstance(group_id, int):
421            return self.groups.get(group_id, None)
422        elif isinstance(group_id, str):
423            g = {v.name: v for v in self.groups.values()}
424            return g.get(group_id, None)
425        else:
426            raise TypeError('Argument ''group_id'' should be either str or int')

Get a parameter group by ID or name.

Args: group_id (int or str): Group ID or name

Returns: ParameterGroup or None: Parameter group if found

Raises: TypeError: If group_id is neither int nor str

def add_parameter(self, name, gid):
428    def add_parameter(self, name, gid):
429        group = self.get_parameter_group(gid)
430        if group is not None:
431            param = group.add_parameter(name)
432            return param
433        return None
def remove_parameter(self, name, group_id):
435    def remove_parameter(self, name, group_id):
436        g = self.get_parameter_group(group_id)
437        if g is not None:
438            return g.remove_parameter(name)
439        return False
def get_parameter(self, group_id, param_name):
441    def get_parameter(self, group_id, param_name):
442        g = self.get_parameter_group(group_id)
443        if g is not None:
444            return g.parameters.get(param_name, None)
445        return None
def get_point_data(self, name: str):
447    def get_point_data(self, name: str):
448        """
449        Return trajectory of a point
450        :param name: the point name in string
451        :return: trajectory as np.ndarray (NbFrames X 3)
452        """
453        if name not in self.data['POINTS']:
454            raise ValueError('Point %s does not exists!' %name)
455        return self.data['POINTS'][name]

Return trajectory of a point

Parameters
  • name: the point name in string
Returns

trajectory as np.ndarray (NbFrames X 3)

def get_points_data(self, name_list: list[str]):
457    def get_points_data(self, name_list:list[str]):
458        """
459        Get several points trajectories
460        :param name_list: list of the points names
461        :return: dict of name:trajectories data
462        """
463        return {name: self.get_point_data(name) for name in name_list}

Get several points trajectories

Parameters
  • name_list: list of the points names
Returns

dict of name:trajectories data

def get_point_names(self):
465    def get_point_names(self):
466        """
467        Get a list of all points in the c3d
468        :return: List[str] with all points labels
469        """
470        return list(self.data['POINTS'].keys())

Get a list of all points in the c3d

Returns

List[str] with all points labels

def get_analog_data(self, name: str):
472    def get_analog_data(self, name: str):
473        """
474        Return values for an analog chanel
475        :param name: analog name in string
476        :return: analog chanel values as np.ndarray(NbAnalogFrames X 1)
477        """
478        if name not in self.data['ANALOGS']:
479            raise ValueError('Chanel %s does not exists!' %name)
480        return self.data['ANALOGS'][name]

Return values for an analog chanel

Parameters
  • name: analog name in string
Returns

analog chanel values as np.ndarray(NbAnalogFrames X 1)

def get_analogs_data(self, name_list: list[str]):
482    def get_analogs_data(self, name_list:list[str]):
483        """
484        Get several analog channels
485        :param name_list: list of the analogs names
486        :return: dict of name:values data
487        """
488        return {name: self.get_analog_data(name) for name in name_list}

Get several analog channels

Parameters
  • name_list: list of the analogs names
Returns

dict of name:values data

def get_analog_names(self):
490    def get_analog_names(self):
491        """
492        Get a list of all analogs chanels in the c3d
493        :return: List[str] with all analogs labels
494        """
495        return list(self.data['ANALOGS'].keys())

Get a list of all analogs chanels in the c3d

Returns

List[str] with all analogs labels

def get_rotation_data(self, name: str):
497    def get_rotation_data(self, name:str):
498        """
499        Return rotation data (mainly for c3d created by theia markerless software)
500        :param name: Joint name
501        :return: rotation and position data np.ndarray(NbFrames X 4 X 4)
502        """
503        if name not in self.data['ROTATIONS']:
504            raise ValueError('Rotation %s does not exists!' %name)
505        return self.data['ROTATIONS'][name]

Return rotation data (mainly for c3d created by theia markerless software)

Parameters
  • name: Joint name
Returns

rotation and position data np.ndarray(NbFrames X 4 X 4)

def get_rotations_data(self, name_list: list[str]):
507    def get_rotations_data(self, name_list:list[str]):
508        """
509        Get several rotations data
510        :param name_list: list of the rotations
511        :return: dict of name:values data
512        """
513        return {name: self.get_rotation_data(name) for name in name_list}

Get several rotations data

Parameters
  • name_list: list of the rotations
Returns

dict of name:values data

def get_rotation_names(self):
515    def get_rotation_names(self):
516        """
517        Get a list of all rotation data in the c3d
518        :return: List[str] with all rotations labels
519        """
520        return list(self.data['ROTATIONS'].keys())

Get a list of all rotation data in the c3d

Returns

List[str] with all rotations labels

point_count
522    @property
523    def point_count(self):
524        """
525        Get the number of 3D points in the c3d
526        :return: int
527        """
528        p = self.get_parameter('POINT', 'USED')
529        return p.value

Get the number of 3D points in the c3d

Returns

int

analog_count
531    @property
532    def analog_count(self):
533        """
534        Get the number of analog channels in the c3d
535        :return: int
536        """
537        p = self.get_parameter('ANALOG', 'USED')
538        if p is not None:
539            return p.value
540        return 0

Get the number of analog channels in the c3d

Returns

int

frame_count
542    @property
543    def frame_count(self):
544        """
545        Get the number of 3D points frames
546        :return: int
547        """
548        return self.header['last_frame'] - self.header['first_frame'] + 1

Get the number of 3D points frames

Returns

int

analog_frame_count
550    @property
551    def analog_frame_count(self):
552        """
553        Get the number of analog frames
554        :return: int
555        """
556        if self.analog_count > 0:
557            return self.frame_count * self.header['analog_per_frame']
558        return 0

Get the number of analog frames

Returns

int

frame_rate
560    @property
561    def frame_rate(self):
562        """
563        Get the 3D points frame rate
564        :return: float
565        """
566        p = self.get_parameter('POINT', 'RATE')
567        return p.value

Get the 3D points frame rate

Returns

float

analog_rate
569    @property
570    def analog_rate(self):
571        """
572        Get the analog channels frame rate
573        :return: float
574        """
575        p = self.get_parameter('ANALOG', 'RATE')
576        if p is not None:
577            return p.value
578        return 0

Get the analog channels frame rate

Returns

float

point_unit
580    @property
581    def point_unit(self):
582        """
583        Get the 3D points unit (usually mm or m)
584        :return: str
585        """
586        p = self.get_parameter('POINT', 'UNITS')
587        return p.value

Get the 3D points unit (usually mm or m)

Returns

str

analog_unit
589    @property
590    def analog_unit(self):
591        """
592        Get the analog channels unit (mV, V, ...)
593        :return: str
594        """
595        p = self.get_parameter('ANALOG', 'UNITS')
596        if p is not None:
597            return p.value
598        return []

Get the analog channels unit (mV, V, ...)

Returns

str