biomechzoo 0.4.10__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of biomechzoo might be problematic. Click here for more details.

Files changed (61) hide show
  1. {biomechzoo-0.4.10/src/biomechzoo.egg-info → biomechzoo-0.5.0}/PKG-INFO +1 -1
  2. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/pyproject.toml +1 -1
  3. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/biomech_ops/filter_data.py +17 -15
  4. biomechzoo-0.5.0/src/biomechzoo/biomech_ops/filter_line.py +85 -0
  5. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/biomech_ops/normalize_data.py +4 -3
  6. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/biomechzoo.py +61 -32
  7. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/conversion/c3d2zoo_data.py +35 -8
  8. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/conversion/mvnx2zoo_data.py +1 -3
  9. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/conversion/table2zoo_data.py +51 -42
  10. biomechzoo-0.5.0/src/biomechzoo/processing/addevent_data.py +98 -0
  11. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/processing/explodechannel_data.py +25 -3
  12. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/processing/partition_data.py +14 -27
  13. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/processing/removechannel_data.py +16 -6
  14. biomechzoo-0.5.0/src/biomechzoo/processing/removeevent_data.py +57 -0
  15. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/processing/renameevent_data.py +0 -6
  16. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/utils/engine.py +7 -3
  17. biomechzoo-0.5.0/src/biomechzoo/utils/peak_sign.py +24 -0
  18. biomechzoo-0.5.0/src/biomechzoo/utils/split_trial.py +35 -0
  19. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/utils/zload.py +11 -0
  20. {biomechzoo-0.4.10 → biomechzoo-0.5.0/src/biomechzoo.egg-info}/PKG-INFO +1 -1
  21. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo.egg-info/SOURCES.txt +2 -0
  22. biomechzoo-0.4.10/src/biomechzoo/biomech_ops/filter_line.py +0 -88
  23. biomechzoo-0.4.10/src/biomechzoo/processing/addevent_data.py +0 -56
  24. biomechzoo-0.4.10/src/biomechzoo/utils/split_trial.py +0 -23
  25. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/LICENSE +0 -0
  26. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/README.md +0 -0
  27. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/setup.cfg +0 -0
  28. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/__init__.py +0 -0
  29. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/__init__.py +0 -0
  30. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/__main__.py +0 -0
  31. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/biomech_ops/__init__.py +0 -0
  32. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/biomech_ops/continuous_relative_phase_data.py +0 -0
  33. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/biomech_ops/continuous_relative_phase_line.py +0 -0
  34. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/biomech_ops/normalize_line.py +0 -0
  35. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/biomech_ops/phase_angle_data.py +0 -0
  36. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/biomech_ops/phase_angle_line.py +0 -0
  37. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/conversion/__init__.py +0 -0
  38. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/conversion/opencap2zoo_data.py +0 -0
  39. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/mvn/__init__.py +0 -0
  40. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/mvn/load_mvnx.py +0 -0
  41. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/mvn/main_mvnx.py +0 -0
  42. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/mvn/mvn.py +0 -0
  43. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/mvn/mvnx_file_accessor.py +0 -0
  44. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/processing/__init__.py +0 -0
  45. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/processing/add_channel_data.py +0 -0
  46. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/processing/addchannel_data.py +0 -0
  47. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/processing/renamechannel_data.py +0 -0
  48. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/processing/split_trial_by_gait_cycle.py +0 -0
  49. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/utils/__init__.py +0 -0
  50. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/utils/batchdisp.py +0 -0
  51. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/utils/compute_sampling_rate_from_time.py +0 -0
  52. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/utils/findfield.py +0 -0
  53. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/utils/get_split_events.py +0 -0
  54. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/utils/set_zoosystem.py +0 -0
  55. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/utils/version.py +0 -0
  56. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/utils/zplot.py +0 -0
  57. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo/utils/zsave.py +0 -0
  58. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo.egg-info/dependency_links.txt +0 -0
  59. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo.egg-info/entry_points.txt +0 -0
  60. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo.egg-info/requires.txt +0 -0
  61. {biomechzoo-0.4.10 → biomechzoo-0.5.0}/src/biomechzoo.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: biomechzoo
3
- Version: 0.4.10
3
+ Version: 0.5.0
4
4
  Summary: Python implementation of the biomechZoo toolbox
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/mcgillmotionlab/biomechzoo
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "biomechzoo"
3
- version = "0.4.10"
3
+ version = "0.5.0"
4
4
  description = "Python implementation of the biomechZoo toolbox"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11,<3.12" # max version for opencap-process tools is 3.11
@@ -13,10 +13,10 @@ def filter_data(data, ch, filt=None):
13
13
  The name(s) of the channel(s) to filter.
14
14
  filt : dict, optional
15
15
  Dictionary specifying filter parameters. Keys may include:
16
- - 'type': 'butter' (default)
16
+ - 'ftype': 'butter' (default)
17
17
  - 'order': filter order (default: 4)
18
18
  - 'cutoff': cutoff frequency or tuple (Hz)
19
- - 'btype': 'low', 'high', 'bandpass', 'bandstop' (default: 'low')
19
+ - 'btype': 'low', 'high', 'bandpass', 'bandstop' (default: 'lowpass')
20
20
 
21
21
  Returns
22
22
  -------
@@ -25,32 +25,34 @@ def filter_data(data, ch, filt=None):
25
25
  """
26
26
 
27
27
  if filt is None:
28
- filt = {}
28
+ filt = {'ftype': 'butter',
29
+ 'order': 4,
30
+ 'cutoff': 10,
31
+ 'btype': 'lowpass',
32
+ 'filtfilt': True}
29
33
 
30
34
  if isinstance(ch, str):
31
35
  ch = [ch]
32
36
 
33
- analog_channels = data['zoosystem']['Analog']['Channels']
34
- if analog_channels:
35
- analog_freq = data['zoosystem']['Analog']['Freq']
36
- video_channels = data['zoosystem']['Video']['Channels']
37
- if video_channels:
38
- video_freq = data['zoosystem']['Video']['Freq']
39
-
37
+ # loop through all channels and filter
40
38
  for c in ch:
41
39
  if c not in data:
42
40
  raise KeyError('Channel {} not found in data'.format(c))
43
41
 
44
42
  if 'fs' not in filt:
43
+
44
+ video_channels = data['zoosystem']['Video']['Channels']
45
+ analog_channels = data['zoosystem']['Analog']['Channels']
46
+
45
47
  if c in analog_channels:
46
- filt['fs'] = analog_freq
47
- elif c in video_freq:
48
- filt['fs'] = video_freq
48
+ filt['fs'] = data['zoosystem']['Analog']['Freq']
49
+ elif c in video_channels:
50
+ filt['fs'] = data['zoosystem']['Video']['Freq']
49
51
  else:
50
- raise ValueError('frequency not provided and cannot be inferred from zoosystem for channel'.format(c))
52
+ raise ValueError('Channel not analog or video')
51
53
 
52
54
  signal_raw = data[c]['line']
53
- signal_filtered = filter_line(signal_raw, filt)
55
+ signal_filtered = filter_line(signal_raw=signal_raw, filt=filt)
54
56
  data[c]['line'] = signal_filtered
55
57
 
56
58
  return data
@@ -0,0 +1,85 @@
1
+ import numpy as np
2
+ import scipy.signal as sgl
3
+
4
+
5
+ def filter_line(signal_raw, filt=None, fs=None):
6
+ """Filter an array using a Butterworth filter."""
7
+ #todo: verify that filter is working correctly
8
+ #todo add more filters
9
+ #todo: consider using kineticstoolkit
10
+
11
+ if filt is None:
12
+ filt = {'ftype': 'butter',
13
+ 'order': 4,
14
+ 'cutoff': 10,
15
+ 'btype': 'lowpass',
16
+ 'filtfilt': True}
17
+ if fs is None:
18
+ raise ValueError('fs is required if no filt is specified')
19
+
20
+ else:
21
+ if 'fs' not in filt:
22
+ raise ValueError('fs is a required key of filt')
23
+
24
+ # Normalize filter type strings
25
+ if filt['ftype'] == 'butterworth':
26
+ filt['ftype'] = 'butter'
27
+ if filt['btype'] is 'low':
28
+ filt['btype'] = 'lowpass'
29
+ if filt['btype'] is 'high':
30
+ filt['btype'] = 'highpass'
31
+
32
+ # Extract parameters
33
+ ftype = filt['ftype']
34
+ order = filt['order']
35
+ cutoff = filt['cutoff']
36
+ btype = filt['btype']
37
+ filtfilt = filt['filtfilt']
38
+ fs = filt['fs']
39
+
40
+ # prepare normalized cutoff(s)
41
+ nyq = 0.5 * fs
42
+ norm_cutoff = np.atleast_1d(np.array(cutoff) / nyq)
43
+
44
+ if ftype is 'butter':
45
+ signal_filtered = kt_butter(ts=signal_raw, fc=norm_cutoff, fs=fs, order=order, btype=btype, filtfilt=filtfilt)
46
+ else:
47
+ raise NotImplementedError(f"Filter type '{ftype}' not implemented.")
48
+
49
+ return signal_filtered
50
+
51
+
52
+ def kt_butter(ts, fc, fs, order=2, btype='lowpass', filtfilt=True):
53
+ """
54
+ Apply a Butterworth filter to data.
55
+
56
+ Parameters
57
+ ----------
58
+ ts, ndarray, 1d.
59
+ fc, Cut-off frequency in Hz. This is a float for single-frequency filters
60
+ (lowpass, highpass), or a tuple of two floats (e.g., (10., 13.)
61
+ for two-frequency filters (bandpass, bandstop)).
62
+ order, Optional. Order of the filter. Default is 2.
63
+ btype, Optional. Can be either "lowpass", "highpass", "bandpass" or
64
+ "bandstop". Default is "lowpass".
65
+ filtfilt, Optional. If True, the filter is applied two times in reverse direction
66
+ to eliminate time lag. If False, the filter is applied only in forward
67
+ direction. Default is True.
68
+
69
+ Returns
70
+ -------
71
+ ts_f, A copy of the input data which each data being filtered.
72
+
73
+ Notes:
74
+ - This code was adapted from kineticstoolkit Thanks @felxi
75
+ """
76
+
77
+ sos = sgl.butter(order, fc, btype, analog=False, output="sos", fs=fs)
78
+
79
+ # Filter
80
+ if filtfilt:
81
+ ts_f = sgl.sosfiltfilt(sos, ts, axis=0)
82
+ else:
83
+ ts_f = sgl.sosfilt(sos,ts, axis=0)
84
+
85
+ return ts_f
@@ -1,4 +1,5 @@
1
1
  import warnings
2
+ import copy
2
3
  from biomechzoo.biomech_ops.normalize_line import normalize_line
3
4
 
4
5
 
@@ -14,14 +15,14 @@ def normalize_data(data, nlength=101):
14
15
  """
15
16
 
16
17
  # normalize channel length
17
- data_new = data.copy()
18
+ data_new = copy.deepcopy(data)
18
19
  for ch_name, ch_data in data_new.items():
19
20
  if ch_name != 'zoosystem':
20
21
  ch_data_line = ch_data['line']
21
22
  ch_data_event = ch_data['event']
22
23
  ch_data_normalized = normalize_line(ch_data_line, nlength)
23
- data_new[ch_name]['line'] = ch_data_normalized
24
- data_new[ch_name]['event'] = ch_data_event
24
+ data_new[ch_name]['line'] = ch_data_normalized
25
+ data_new[ch_name]['event'] = ch_data_event
25
26
  warnings.warn('event data have not been normalized')
26
27
 
27
28
  # update zoosystem
@@ -12,6 +12,7 @@ from biomechzoo.conversion.table2zoo_data import table2zoo_data
12
12
  from biomechzoo.conversion.mvnx2zoo_data import mvnx2zoo_data
13
13
  from biomechzoo.processing.removechannel_data import removechannel_data
14
14
  from biomechzoo.processing.renamechannel_data import renamechannel_data
15
+ from biomechzoo.processing.removeevent_data import removeevent_data
15
16
  from biomechzoo.processing.explodechannel_data import explodechannel_data
16
17
  from biomechzoo.processing.addevent_data import addevent_data
17
18
  from biomechzoo.processing.partition_data import partition_data
@@ -19,6 +20,7 @@ from biomechzoo.processing.renameevent_data import renameevent_data
19
20
  from biomechzoo.biomech_ops.normalize_data import normalize_data
20
21
  from biomechzoo.biomech_ops.phase_angle_data import phase_angle_data
21
22
  from biomechzoo.biomech_ops.continuous_relative_phase_data import continuous_relative_phase_data
23
+ from biomechzoo.biomech_ops.filter_data import filter_data
22
24
 
23
25
  class BiomechZoo:
24
26
  def __init__(self, in_folder, inplace=False, subfolders=None, name_contains=None, verbose=0):
@@ -61,6 +63,8 @@ class BiomechZoo:
61
63
  in_folder_path = os.path.dirname(in_folder)
62
64
  self.in_folder = os.path.join(in_folder_path, out_folder)
63
65
 
66
+ batchdisp('all files saved to: {}'.format(self.in_folder ), level=1, verbose=self.verbose)
67
+
64
68
  def mvnx2zoo(self, out_folder=None, inplace=False):
65
69
  """ Converts all .mvnx files in the folder to .zoo format """
66
70
  start_time = time.time()
@@ -75,7 +79,7 @@ class BiomechZoo:
75
79
  f_zoo = f.replace('.mvnx', '.zoo')
76
80
  zsave(f_zoo, data, inplace=inplace, out_folder=out_folder, root_folder=in_folder)
77
81
  method_name = inspect.currentframe().f_code.co_name
78
- batchdisp('{} conversion complete for {} files'.format(method_name, len(fl)), level=1, verbose=verbose)
82
+ batchdisp('{} process complete for {} file(s) in {:.2f} secs'.format(method_name, len(fl), time.time() - start_time), level=1, verbose=verbose)
79
83
  # Update self.folder after processing
80
84
  self._update_folder(out_folder, inplace, in_folder)
81
85
 
@@ -99,17 +103,21 @@ class BiomechZoo:
99
103
  # Update self.folder after processing
100
104
  self._update_folder(out_folder, inplace, in_folder)
101
105
 
102
- def table2zoo(self, out_folder=None, inplace=None, skip_rows=0, extension='csv'):
106
+ def table2zoo(self, extension, out_folder=None, inplace=None, skip_rows=0, freq=None):
103
107
  """ Converts generic .csv file in the folder to .zoo format """
104
108
  start_time = time.time()
105
109
  verbose = self.verbose
106
110
  in_folder = self.in_folder
111
+
112
+ if extension.startswith('.'):
113
+ extension = extension[1:]
114
+
107
115
  if inplace is None:
108
116
  inplace = self.inplace
109
117
  fl = engine(in_folder, extension=extension, name_contains=self.name_contains, subfolders=self.subfolders)
110
118
  for f in fl:
111
119
  batchdisp('converting {} to zoo for {}'.format(extension, f), level=2, verbose=verbose)
112
- data = table2zoo_data(f, type=extension, skip_rows=skip_rows)
120
+ data = table2zoo_data(f, extension=extension, skip_rows=skip_rows, freq=freq)
113
121
  f_zoo = f.replace(extension, '.zoo')
114
122
  zsave(f_zoo, data, inplace=inplace, out_folder=out_folder, root_folder=in_folder)
115
123
  method_name = inspect.currentframe().f_code.co_name
@@ -118,8 +126,13 @@ class BiomechZoo:
118
126
  self._update_folder(out_folder, inplace, in_folder)
119
127
 
120
128
  def xls2zoo(self, out_folder=None, inplace=None):
121
- """ Converts generic .xls file in the folder to .zoo format """
122
- raise NotImplementedError
129
+ raise NotImplementedError('Use table2zoo instead')
130
+
131
+ def csv2zoo(self, out_folder=None, inplace=None):
132
+ raise NotImplementedError('Use table2zoo instead')
133
+
134
+ def parquet2zoo(self, out_folder=None, inplace=None):
135
+ raise NotImplementedError('Use table2zoo instead')
123
136
 
124
137
  def phase_angle(self, ch, out_folder=None, inplace=None):
125
138
  """ computes phase angles"""
@@ -184,7 +197,8 @@ class BiomechZoo:
184
197
  batchdisp('splitting by gait cycle from {} to {} for {}'.format(start, end, f), level=2,
185
198
  verbose=verbose)
186
199
  data_new = split_trial(data, start, end)
187
- zsave(fl_new, data_new, inplace=inplace, out_folder=out_folder, root_folder=in_folder)
200
+ if data_new is not None:
201
+ zsave(fl_new, data_new, inplace=inplace, out_folder=out_folder, root_folder=in_folder)
188
202
  method_name = inspect.currentframe().f_code.co_name
189
203
  batchdisp('{} process complete for {} file(s) in {:.2f} secs'.format(method_name, len(fl), time.time() - start_time), level=1, verbose=verbose)
190
204
 
@@ -266,6 +280,27 @@ class BiomechZoo:
266
280
  # Update self.folder after processing
267
281
  self._update_folder(out_folder, inplace, in_folder)
268
282
 
283
+
284
+ def removeevent(self, events, mode='remove', out_folder=None, inplace=None):
285
+ """ removes channels from zoo files """
286
+ start_time = time.time()
287
+ verbose = self.verbose
288
+ in_folder = self.in_folder
289
+ if inplace is None:
290
+ inplace = self.inplace
291
+ fl = engine(in_folder, extension='.zoo', name_contains=self.name_contains, subfolders=self.subfolders)
292
+ for f in fl:
293
+ batchdisp('removing events {} for {}'.format(events, f), level=2, verbose=verbose)
294
+ data = zload(f)
295
+ data = removeevent_data(data, events, mode)
296
+ zsave(f, data, inplace=inplace, out_folder=out_folder, root_folder=in_folder)
297
+ method_name = inspect.currentframe().f_code.co_name
298
+ batchdisp('{} process complete for {} file(s) in {:.2f} secs'.format(method_name, len(fl), time.time() - start_time), level=1, verbose=verbose)
299
+
300
+ # Update self.folder after processing
301
+ self._update_folder(out_folder, inplace, in_folder)
302
+
303
+
269
304
  def explodechannel(self, out_folder=None, inplace=None):
270
305
  """ explodes all channels in a zoo file """
271
306
  start_time = time.time()
@@ -306,7 +341,7 @@ class BiomechZoo:
306
341
  # Update self.folder after processing
307
342
  self._update_folder(out_folder, inplace, in_folder)
308
343
 
309
- def addevent(self, ch, event_type, event_name, out_folder=None, inplace=None):
344
+ def addevent(self, ch, event_type, event_name, out_folder=None, inplace=None, constant=None):
310
345
  """ adds events of type evt_type with name evt_name to channel ch """
311
346
  start_time = time.time()
312
347
  verbose = self.verbose
@@ -318,7 +353,7 @@ class BiomechZoo:
318
353
  if verbose:
319
354
  batchdisp('adding event {} to channel {} for {}'.format(event_type, ch, f), level=2, verbose=verbose)
320
355
  data = zload(f)
321
- data = addevent_data(data, ch, event_type, event_name)
356
+ data = addevent_data(data, ch, event_type, event_name, constant)
322
357
  zsave(f, data, inplace=inplace, out_folder=out_folder, root_folder=in_folder)
323
358
  method_name = inspect.currentframe().f_code.co_name
324
359
  batchdisp('{} process complete for {} file(s) in {:.2f} secs'.format(method_name, len(fl), time.time() - start_time), level=1, verbose=verbose)
@@ -346,28 +381,22 @@ class BiomechZoo:
346
381
  self._update_folder(out_folder, inplace, in_folder)
347
382
 
348
383
  def filter(self, ch, filt=None, out_folder=None, inplace=None):
349
- raise NotImplementedError
350
- # verbose = self.verbose
351
- # in_folder = self.in_folder
352
- # if inplace is None:
353
- # inplace = self.inplace
354
- #
355
- # # set filter type
356
- # if filt is None:
357
- # filt = {'type': 'butterworth',
358
- # 'order': 3,
359
- # 'pass': 'lowpass'}
360
- #
361
- # fl = engine(in_folder)
362
- # for f in fl:
363
- # batchdisp('filtering data in channels {} for {}'.format(ch, f), level=2, verbose=verbose)
364
- # data = zload(f)
365
- # data = filter_data(data, ch, filt)
366
- # zsave(f, data, inplace=inplace, root_folder=in_folder, out_folder=out_folder, verbose=verbose)
367
- # method_name = inspect.currentframe().f_code.co_name
368
- # batchdisp('{} computation complete for {} file(s)'.format(method_name, len(fl)), level=1, verbose=verbose)
369
- #
370
- # # Update self.folder after processing
371
- # self._update_folder(out_folder, inplace, in_folder)
372
-
384
+ """ filter data"""
385
+ start_time = time.time()
386
+ verbose = self.verbose
387
+ in_folder = self.in_folder
388
+ if inplace is None:
389
+ inplace = self.inplace
390
+ fl = engine(in_folder, name_contains=self.name_contains, subfolders=self.subfolders)
391
+ for f in fl:
392
+ if verbose:
393
+ batchdisp('filtering data for channel {} in {}'.format(ch, f), level=2, verbose=verbose)
394
+ data = zload(f)
395
+ data = filter_data(data, ch, filt)
396
+ zsave(f, data, inplace=inplace, out_folder=out_folder, root_folder=in_folder)
397
+ method_name = inspect.currentframe().f_code.co_name
398
+ batchdisp('{} process complete for {} file(s) in {:.2f} secs'.format(method_name, len(fl), time.time() - start_time),
399
+ level=1, verbose=verbose)
400
+ # Update self.folder after processing
401
+ self._update_folder(out_folder, inplace, in_folder)
373
402
 
@@ -8,10 +8,14 @@ def c3d2zoo_data(c3d_obj):
8
8
  - data (dict): Zoo dictionary with 'line' and 'event' fields per channel.
9
9
  """
10
10
  data = {}
11
-
11
+ data['zoosystem'] = set_zoosystem()
12
+ video_freq = None
13
+ analog_freq = None
14
+ # extract "video" data
12
15
  if 'points' in c3d_obj['data']:
13
16
  points = c3d_obj['data']['points'] # shape: (4, n_markers, n_frames)
14
- labels = c3d_obj['parameters']['POINT']['LABELS']['value']
17
+ labels = list(c3d_obj['parameters']['POINT']['LABELS']['value'])
18
+ video_freq = int(c3d_obj['parameters']['POINT']['RATE']['value'][0])
15
19
  for i, label in enumerate(labels):
16
20
  line_data = points[:3, i, :].T # shape: (frames, 3)
17
21
  data[label] = {
@@ -19,9 +23,32 @@ def c3d2zoo_data(c3d_obj):
19
23
  'event': {} # empty for now
20
24
  }
21
25
 
22
- params = c3d_obj['parameters']
23
- video_freq = c3d_obj['parameters']['POINT']['RATE']['value'][0]
24
- if 'EVENT' in params and 'TIMES' in params['EVENT']:
26
+ data['zoosystem']['Video']['Freq'] = video_freq
27
+ data['zoosystem']['Video']['Channels'] = labels
28
+
29
+ if 'analogs' in c3d_obj['data']:
30
+ analog_data = c3d_obj['data']['analogs'] # shape: (subframes, n_analog_channels, n_frames)
31
+ analog_labels = list(c3d_obj['parameters']['ANALOG']['LABELS']['value'])
32
+ analog_freq = int(c3d_obj['parameters']['ANALOG']['RATE']['value'][0])
33
+ # Flatten to 2D: (n_samples, n_channels)
34
+ # ezc3d stores analogs as subframes per frame, so we flatten across all
35
+ n_subframes, n_channels, n_frames = analog_data.shape
36
+ analog_data = analog_data.reshape(n_subframes * n_frames, n_channels)
37
+
38
+ for i, label in enumerate(analog_labels):
39
+ line_data = analog_data[:, i].reshape(-1, 1) # shape: (samples, 1)
40
+ data[label] = {
41
+ 'line': line_data,
42
+ 'event': {},
43
+ }
44
+
45
+ data['zoosystem']['Analog']['Freq'] = analog_freq
46
+ data['zoosystem']['Analog']['Channels'] = analog_labels
47
+
48
+ # extract event information
49
+ params = c3d_obj['parameters']
50
+ if 'EVENT' in params and 'TIMES' in params['EVENT']:
51
+ if 'points' in c3d_obj['data']:
25
52
  times_array = params['EVENT']['TIMES']['value']
26
53
  frames = times_array[1] # should be time depending on C3D file
27
54
 
@@ -61,8 +88,8 @@ def c3d2zoo_data(c3d_obj):
61
88
  else:
62
89
  data[labels[0]]['event'][key_name] = [frame-1, 0, 0] # remove 1 to follow python
63
90
 
64
- # todo add relevant meta data to zoosystem
65
- data['zoosystem'] = set_zoosystem()
66
- data['zoosystem']['Analog']['Freq'] = int(params['ANALOG']['RATE']['value'][0])
91
+ # add more zoosystem
92
+ if analog_freq is not None and video_freq is not None:
93
+ data['zoosystem']['AVR'] = analog_freq/video_freq
67
94
 
68
95
  return data
@@ -9,8 +9,7 @@ def mvnx2zoo_data(fl):
9
9
  mvnx_file = load_mvnx(fl)
10
10
 
11
11
  # create zoo data dict
12
- data = {}
13
-
12
+ data = {'zoosystem': set_zoosystem()}
14
13
  # extract joint angle data (All JOINTS may not exist in a given dataset)
15
14
  for key, val in JOINTS.items():
16
15
  try:
@@ -58,7 +57,6 @@ def is_valid_for_zoo(val):
58
57
 
59
58
  def _get_meta_info(fl, mvnx_file, data):
60
59
  # todo: add more, see mvnx_file object
61
- data['zoosystem'] = set_zoosystem(fl)
62
60
  data['zoosystem']['Video']['Freq'] = int(mvnx_file.frame_rate)
63
61
  data['zoosystem']['mvnx_version'] = mvnx_file.version
64
62
  data['zoosystem']['mvnx_configuration'] = mvnx_file.configuration
@@ -6,42 +6,59 @@ from biomechzoo.utils.set_zoosystem import set_zoosystem
6
6
  from biomechzoo.utils.compute_sampling_rate_from_time import compute_sampling_rate_from_time
7
7
 
8
8
 
9
- def table2zoo_data(csv_path, type='csv', skip_rows=0, freq=None):
10
- # todo: check calculation for sampling rate
9
+ def table2zoo_data(fl, extension, skip_rows=0, freq=None):
11
10
 
12
- if type not in ['csv', 'parquet']:
13
- raise NotImplementedError('Only csv and parquet currently supported')
11
+ if extension == 'csv':
12
+ df, metadata, freq = _csv2zoo(fl, skip_rows=skip_rows, freq=freq)
14
13
 
15
- # Read header lines until 'endheader'
16
- metadata = {}
17
- if type == 'csv' and skip_rows > 0:
18
- header_lines = []
19
- with open(csv_path, 'r') as f:
20
- for line in f:
21
- header_lines.append(line.strip())
22
- if line.strip().lower() == 'endheader':
23
- break
24
- # Parse metadata
25
- metadata = _parse_metadata(header_lines)
26
-
27
- if type == 'csv':
28
- df = pd.read_csv(csv_path, skiprows=skip_rows)
29
- elif type =='parquet':
30
- df = pd.read_parquet(csv_path)
14
+ elif extension == 'parquet':
15
+ df, metadata, freq = _parquet2zoo(fl, skip_rows=skip_rows, freq=freq)
31
16
  else:
32
- raise ValueError('type must be csv or parquet')
33
-
34
- # Use all columns
35
- data = df.iloc[:, 0:]
17
+ raise ValueError('extension {} not implemented'.format(extension))
36
18
 
37
19
  # assemble zoo data
38
- zoo_data = {}
39
- for ch in data.columns:
40
- zoo_data[ch] = {
41
- 'line': data[ch].values,
20
+ data = {'zoosystem': set_zoosystem()}
21
+ for ch in df.columns:
22
+ data[ch] = {
23
+ 'line': df[ch].values,
42
24
  'event': []
43
25
  }
44
26
 
27
+
28
+ # now try to calculate freq from a time column
29
+ if freq is None:
30
+ time_col = [col for col in df.columns if 'time' in col.lower()]
31
+ if time_col is not None and len(time_col) > 0:
32
+ time_data = df[time_col].to_numpy()[:, 0]
33
+ freq = compute_sampling_rate_from_time(time_data)
34
+ else:
35
+ raise ValueError('Unable to compute sampling rate for time column, please specify a sampling frequency'
36
+ )
37
+ # add metadata
38
+ data['zoosystem']['Video']['Freq'] = freq
39
+ data['zoosystem']['Analog']['Freq'] = 'None'
40
+
41
+ if metadata is not None:
42
+ data['zoosystem']['Other'] = metadata
43
+
44
+ return data
45
+
46
+
47
+ def _parquet2zoo(fl, skip_rows=0, freq=None):
48
+ df = pd.read_parquet(fl)
49
+ metadata = None
50
+ return df, metadata, freq
51
+
52
+ def _csv2zoo(fl, skip_rows=0, freq=None):
53
+ header_lines = []
54
+ with open(fl, 'r') as f:
55
+ for line in f:
56
+ header_lines.append(line.strip())
57
+ if line.strip().lower() == 'endheader':
58
+ break
59
+ # Parse metadata
60
+ metadata = _parse_metadata(header_lines)
61
+
45
62
  # try to find frequency in metadata
46
63
  if freq is None:
47
64
  if 'freq' in metadata:
@@ -51,22 +68,12 @@ def table2zoo_data(csv_path, type='csv', skip_rows=0, freq=None):
51
68
  else:
52
69
  freq = None # or raise an error
53
70
 
54
- # now try to calculate freq from a time column
55
- if freq is None:
56
- time_col = [col for col in df.columns if 'time' in col.lower()]
57
- if time_col is not None:
58
- time_data = df[time_col].to_numpy()[:, 0]
59
- freq = compute_sampling_rate_from_time(time_data)
71
+ # read csv
72
+ df = pd.read_csv(fl, skiprows=skip_rows)
73
+
74
+ return df, metadata, freq
60
75
 
61
- # add metadata
62
- # todo update zoosystem to match biomechzoo requirements
63
- zoo_data['zoosystem'] = set_zoosystem(csv_path)
64
- zoo_data['zoosystem']['Video']['Freq'] = freq
65
- zoo_data['zoosystem']['Analog']['Freq'] = 'None'
66
- if 'version' in metadata:
67
- zoo_data['zoosystem']['collection_system_version'] = metadata['version']
68
76
 
69
- return zoo_data
70
77
 
71
78
 
72
79
  def _parse_metadata(header_lines):
@@ -96,6 +103,8 @@ def _parse_metadata(header_lines):
96
103
  return metadata
97
104
 
98
105
 
106
+
107
+
99
108
  if __name__ == '__main__':
100
109
  """ for unit testing"""
101
110
  current_dir = os.path.dirname(os.path.abspath(__file__))
@@ -0,0 +1,98 @@
1
+ import numpy as np
2
+ import copy
3
+ import warnings
4
+ from biomechzoo.utils.peak_sign import peak_sign
5
+
6
+ def addevent_data(data, channels, ename, etype, constant=None):
7
+
8
+ data_new = copy.deepcopy(data)
9
+
10
+ if isinstance(channels, str):
11
+ channels = [channels]
12
+
13
+ if len(channels) == 1 and channels[0].lower() == 'all':
14
+ channels = [key for key in data if key != 'zoosystem']
15
+
16
+ for channel in channels:
17
+ if ename == '':
18
+ data[channel]['event'] = {}
19
+ continue
20
+
21
+ if channel not in data:
22
+ raise KeyError('Channel {} does not exist'.format(channel))
23
+
24
+ yd = data_new[channel]['line'] # 1D array
25
+ etype = etype.lower()
26
+ if etype == 'absmax':
27
+ exd = int(np.argmax(np.abs(yd)))
28
+ eyd = float(yd[exd])
29
+ elif etype == 'first':
30
+ exd = 0
31
+ eyd = float(yd[exd])
32
+ elif etype == 'last':
33
+ exd = len(yd) - 1
34
+ eyd = float(yd[exd])
35
+ elif etype == 'max':
36
+ exd = int(np.argmax(yd))
37
+ eyd = float(yd[exd])
38
+ elif etype == 'min':
39
+ exd = int(np.argmin(yd))
40
+ eyd = float(yd[exd])
41
+ elif etype == 'rom':
42
+ eyd = float(np.max(yd) - np.min(yd))
43
+ exd = 0 # dummy index (like MATLAB version)
44
+ elif etype == 'max_stance':
45
+ # special event for gait and running
46
+ exd = max_stance(yd)
47
+ eyd = float(yd[exd])
48
+ eyd = float(yd[exd])
49
+ elif etype in ['fs_fp', 'fo_fp']:
50
+ # --- Handle constant ---
51
+ if constant is None:
52
+ print('Warning: Force plate threshold not set, defaulting to 0.')
53
+ constant = 0.0
54
+
55
+ # --- Check sampling rates ---
56
+ AVR = data['zoosystem']['AVR']
57
+ if AVR != 1:
58
+ warnings.warn('Video and Analog channels must be at the same sampling rate or events will be incorrect.')
59
+
60
+ # --- Handle units ---
61
+ units = data['zoosystem']['Units']['Forces']
62
+ if units == 'N/kg':
63
+ m = data['zoosystem']['Anthro']['Bodymass']
64
+ else:
65
+ m = 1.0
66
+
67
+ # --- Extract force signal ---
68
+ if '_' not in channel:
69
+ yd = data_new[channel]['line'][:, 2] # looking for GRF Z
70
+ else:
71
+ yd = data_new[channel]['line']
72
+
73
+ # --- Determine peak sign ---
74
+ peak = peak_sign(yd) # user-defined function
75
+
76
+ # --- Find threshold crossing ---
77
+ threshold_signal = peak * yd * m
78
+ if 'fs' in etype:
79
+ exd_array = np.where(threshold_signal > constant)[0]
80
+ exd = exd_array[0] - 1 # MATLAB indexing correction
81
+ eyd = yd[exd]
82
+ else: # 'FO' type
83
+ exd_array = np.where(threshold_signal > constant)[0]
84
+ exd = exd_array[-1] + 1
85
+ eyd = yd[exd]
86
+
87
+ else:
88
+ raise ValueError(f'Unknown event type: {etype}')
89
+
90
+ # Add event to the channel's event dict
91
+ data_new[channel]['event'][ename] = [exd, eyd, 0]
92
+
93
+ return data_new
94
+
95
+ def max_stance(yd):
96
+ """ extracts max from first 40% of the gait cycle"""
97
+ raise NotImplementedError
98
+ return exd