biomechzoo 0.1.1__py3-none-any.whl
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.
- biomechzoo/__init__.py +5 -0
- biomechzoo/__main__.py +6 -0
- biomechzoo/biomech_ops/continuous_relative_phase_data.py +31 -0
- biomechzoo/biomech_ops/continuous_relative_phase_line.py +36 -0
- biomechzoo/biomech_ops/filter_data.py +56 -0
- biomechzoo/biomech_ops/filter_line.py +88 -0
- biomechzoo/biomech_ops/normalize_data.py +35 -0
- biomechzoo/biomech_ops/normalize_line.py +27 -0
- biomechzoo/biomech_ops/phase_angle_data.py +39 -0
- biomechzoo/biomech_ops/phase_angle_line.py +48 -0
- biomechzoo/biomechzoo.py +349 -0
- biomechzoo/conversion/__init__.py +0 -0
- biomechzoo/conversion/c3d2zoo_data.py +65 -0
- biomechzoo/conversion/csv2zoo_data.py +78 -0
- biomechzoo/conversion/mvnx2zoo_data.py +71 -0
- biomechzoo/conversion/opencap2zoo_data.py +23 -0
- biomechzoo/mvn/load_mvnx.py +514 -0
- biomechzoo/mvn/main_mvnx.py +75 -0
- biomechzoo/mvn/mvn.py +232 -0
- biomechzoo/mvn/mvnx_file_accessor.py +463 -0
- biomechzoo/processing/add_channel_data.py +71 -0
- biomechzoo/processing/addchannel_data.py +71 -0
- biomechzoo/processing/addevent_data.py +46 -0
- biomechzoo/processing/explodechannel_data.py +46 -0
- biomechzoo/processing/partition_data.py +51 -0
- biomechzoo/processing/removechannel_data.py +36 -0
- biomechzoo/processing/renamechannel_data.py +79 -0
- biomechzoo/processing/renameevent_data.py +68 -0
- biomechzoo/processing/split_trial_by_gait_cycle.py +52 -0
- biomechzoo/utils/batchdisp.py +21 -0
- biomechzoo/utils/compute_sampling_rate_from_time.py +25 -0
- biomechzoo/utils/engine.py +68 -0
- biomechzoo/utils/findfield.py +11 -0
- biomechzoo/utils/get_split_events.py +33 -0
- biomechzoo/utils/split_trial.py +23 -0
- biomechzoo/utils/zload.py +46 -0
- biomechzoo/utils/zplot.py +61 -0
- biomechzoo/utils/zsave.py +50 -0
- biomechzoo-0.1.1.dist-info/METADATA +48 -0
- biomechzoo-0.1.1.dist-info/RECORD +44 -0
- biomechzoo-0.1.1.dist-info/WHEEL +5 -0
- biomechzoo-0.1.1.dist-info/entry_points.txt +2 -0
- biomechzoo-0.1.1.dist-info/licenses/LICENSE +21 -0
- biomechzoo-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def add_channel_data(data, ch_new_name, ch_new_data, section='Video'):
|
|
5
|
+
"""
|
|
6
|
+
Add a new channel to zoo data.
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
----------
|
|
10
|
+
data : dict
|
|
11
|
+
Zoo file data.
|
|
12
|
+
ch_new_name : str
|
|
13
|
+
Name of the new channel.
|
|
14
|
+
ch_new_data : array-like
|
|
15
|
+
New data to be added to the channel (should be n x 1 or n x 3).
|
|
16
|
+
section : str
|
|
17
|
+
Section of zoo data ('Video' or 'Analog').
|
|
18
|
+
|
|
19
|
+
Returns
|
|
20
|
+
-------
|
|
21
|
+
dict
|
|
22
|
+
Updated zoo data with new channel added.
|
|
23
|
+
|
|
24
|
+
Notes
|
|
25
|
+
-----
|
|
26
|
+
- If the channel already exists, it will be overwritten.
|
|
27
|
+
- Adds channel name to the list in data['zoosystem'][section]['Channels'].
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# Warn if overwriting
|
|
31
|
+
if ch_new_name in data:
|
|
32
|
+
print('Warning: channel {} already exists, overwriting...'.format(ch_new_name))
|
|
33
|
+
|
|
34
|
+
# Assign channel data
|
|
35
|
+
data[ch_new_name] = {
|
|
36
|
+
'line': ch_new_data,
|
|
37
|
+
'event': {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Update channel list
|
|
41
|
+
ch_list = data['zoosystem'][section].get('Channels', [])
|
|
42
|
+
|
|
43
|
+
# If the channel list is a NumPy array, convert it to a list
|
|
44
|
+
if isinstance(ch_list, np.ndarray):
|
|
45
|
+
ch_list = ch_list.tolist()
|
|
46
|
+
|
|
47
|
+
# Ensure it's a flat list of strings
|
|
48
|
+
if isinstance(ch_list, list) and ch_new_name not in ch_list:
|
|
49
|
+
ch_list.append(ch_new_name)
|
|
50
|
+
data['zoosystem'][section]['Channels'] = ch_list
|
|
51
|
+
|
|
52
|
+
return data
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == '__main__':
|
|
56
|
+
# -------TESTING--------
|
|
57
|
+
import os
|
|
58
|
+
from src.biomechzoo.utils.zload import zload
|
|
59
|
+
from src.biomechzoo.utils.zplot import zplot
|
|
60
|
+
# get path to sample zoo file
|
|
61
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
62
|
+
project_root = os.path.dirname(current_dir)
|
|
63
|
+
fl = os.path.join(project_root, 'data', 'other', 'HC030A05.zoo')
|
|
64
|
+
|
|
65
|
+
# load zoo file
|
|
66
|
+
data = zload(fl)
|
|
67
|
+
data = data['data']
|
|
68
|
+
r = data['RKneeAngles']['line']*3
|
|
69
|
+
data= add_channel_data(data, ch_new_name='blah', ch_new_data=r)
|
|
70
|
+
zplot(data, 'blah')
|
|
71
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def addchannel_data(data, ch_new_name, ch_new_data, section='Video'):
|
|
5
|
+
"""
|
|
6
|
+
Add a new channel to zoo data.
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
----------
|
|
10
|
+
data : dict
|
|
11
|
+
Zoo file data.
|
|
12
|
+
ch_new_name : str
|
|
13
|
+
Name of the new channel.
|
|
14
|
+
ch_new_data : array-like
|
|
15
|
+
New data to be added to the channel (should be n x 1 or n x 3).
|
|
16
|
+
section : str
|
|
17
|
+
Section of zoo data ('Video' or 'Analog').
|
|
18
|
+
|
|
19
|
+
Returns
|
|
20
|
+
-------
|
|
21
|
+
dict
|
|
22
|
+
Updated zoo data with new channel added.
|
|
23
|
+
|
|
24
|
+
Notes
|
|
25
|
+
-----
|
|
26
|
+
- If the channel already exists, it will be overwritten.
|
|
27
|
+
- Adds channel name to the list in data['zoosystem'][section]['Channels'].
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# Warn if overwriting
|
|
31
|
+
if ch_new_name in data:
|
|
32
|
+
print('Warning: channel {} already exists, overwriting...'.format(ch_new_name))
|
|
33
|
+
|
|
34
|
+
# Assign channel data
|
|
35
|
+
data[ch_new_name] = {
|
|
36
|
+
'line': ch_new_data,
|
|
37
|
+
'event': {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Update channel list
|
|
41
|
+
ch_list = data['zoosystem'][section].get('Channels', [])
|
|
42
|
+
|
|
43
|
+
# If the channel list is a NumPy array, convert it to a list
|
|
44
|
+
if isinstance(ch_list, np.ndarray):
|
|
45
|
+
ch_list = ch_list.tolist()
|
|
46
|
+
|
|
47
|
+
# Ensure it's a flat list of strings
|
|
48
|
+
if isinstance(ch_list, list) and ch_new_name not in ch_list:
|
|
49
|
+
ch_list.append(ch_new_name)
|
|
50
|
+
data['zoosystem'][section]['Channels'] = ch_list
|
|
51
|
+
|
|
52
|
+
return data
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == '__main__':
|
|
56
|
+
# -------TESTING--------
|
|
57
|
+
import os
|
|
58
|
+
from src.biomechzoo.utils.zload import zload
|
|
59
|
+
from src.biomechzoo.utils.zplot import zplot
|
|
60
|
+
# get path to sample zoo file
|
|
61
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
62
|
+
project_root = os.path.dirname(current_dir)
|
|
63
|
+
fl = os.path.join(project_root, 'data', 'other', 'HC030A05.zoo')
|
|
64
|
+
|
|
65
|
+
# load zoo file
|
|
66
|
+
data = zload(fl)
|
|
67
|
+
data = data['data']
|
|
68
|
+
r = data['RKneeAngles']['line']*3
|
|
69
|
+
data= addchannel_data(data, ch_new_name='blah', ch_new_data=r)
|
|
70
|
+
zplot(data, 'blah')
|
|
71
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def addevent_data(data, ch, ename, etype):
|
|
5
|
+
if isinstance(ch, str):
|
|
6
|
+
ch = [ch]
|
|
7
|
+
|
|
8
|
+
if len(ch) == 1 and ch[0].lower() == 'all':
|
|
9
|
+
ch = [key for key in data if key != 'zoosystem']
|
|
10
|
+
|
|
11
|
+
for channel in ch:
|
|
12
|
+
if ename == '':
|
|
13
|
+
data[channel]['event'] = {}
|
|
14
|
+
continue
|
|
15
|
+
|
|
16
|
+
if channel not in data:
|
|
17
|
+
print(f'Channel {channel} does not exist')
|
|
18
|
+
continue
|
|
19
|
+
|
|
20
|
+
yd = data[channel]['line'] # 1D array
|
|
21
|
+
etype = etype.lower()
|
|
22
|
+
if etype == 'absmax':
|
|
23
|
+
exd = int(np.argmax(np.abs(yd)))
|
|
24
|
+
eyd = float(yd[exd])
|
|
25
|
+
elif etype == 'first':
|
|
26
|
+
exd = 0
|
|
27
|
+
eyd = float(yd[exd])
|
|
28
|
+
elif etype == 'last':
|
|
29
|
+
exd = len(yd) - 1
|
|
30
|
+
eyd = float(yd[exd])
|
|
31
|
+
elif etype == 'max':
|
|
32
|
+
exd = int(np.argmax(yd))
|
|
33
|
+
eyd = float(yd[exd])
|
|
34
|
+
elif etype == 'min':
|
|
35
|
+
exd = int(np.argmin(yd))
|
|
36
|
+
eyd = float(yd[exd])
|
|
37
|
+
elif etype == 'rom':
|
|
38
|
+
eyd = float(np.max(yd) - np.min(yd))
|
|
39
|
+
exd = 0 # dummy index (like MATLAB version)
|
|
40
|
+
else:
|
|
41
|
+
raise ValueError(f'Unknown event type: {etype}')
|
|
42
|
+
|
|
43
|
+
# Add event to the channel's event dict
|
|
44
|
+
data[channel]['event'][ename] = [exd, eyd, 0]
|
|
45
|
+
|
|
46
|
+
return data
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
def explodechannel_data(data, channels=None):
|
|
2
|
+
""" Explodes 3D channels (n x 3 arrays) into separate X, Y, Z channels.
|
|
3
|
+
|
|
4
|
+
Arguments:
|
|
5
|
+
data (dict): Zoo data loaded from a file
|
|
6
|
+
channels (list of str or None): Channels to explode.
|
|
7
|
+
If None, explode all channels with 'line' shaped (n x 3).
|
|
8
|
+
|
|
9
|
+
Returns:
|
|
10
|
+
data_new (dict): Modified zoo dictionary with exploded channels.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
data_new = data.copy()
|
|
14
|
+
|
|
15
|
+
# Find default channels if none provided
|
|
16
|
+
if channels is None:
|
|
17
|
+
channels = []
|
|
18
|
+
for ch in data_new:
|
|
19
|
+
if ch == 'zoosystem':
|
|
20
|
+
continue
|
|
21
|
+
ch_data = data_new[ch]['line']
|
|
22
|
+
if ch_data.ndim == 2 and ch_data.shape[1] == 3:
|
|
23
|
+
channels.append(ch)
|
|
24
|
+
|
|
25
|
+
# Explode each channel
|
|
26
|
+
for ch in channels:
|
|
27
|
+
if ch not in data_new:
|
|
28
|
+
print('Warning: channel {} not found, skipping.'.format(ch))
|
|
29
|
+
continue
|
|
30
|
+
|
|
31
|
+
ch_data = data_new[ch]['line']
|
|
32
|
+
if ch_data.ndim != 2 or ch_data.shape[1] != 3:
|
|
33
|
+
print(f"Warning: channel '{ch}' 'line' is not n x 3 shape, skipping.")
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
x, y, z = ch_data[:, 0], ch_data[:, 1], ch_data[:, 2]
|
|
37
|
+
for axis, line in zip(['_x', '_y', '_z'], [x, y, z]):
|
|
38
|
+
key = ch + axis
|
|
39
|
+
data_new[key] = {
|
|
40
|
+
'line': line,
|
|
41
|
+
'event': data[ch]['event']}
|
|
42
|
+
|
|
43
|
+
# Remove original channel
|
|
44
|
+
del data_new[ch]
|
|
45
|
+
|
|
46
|
+
return data_new
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from biomechzoo.utils.findfield import findfield
|
|
2
|
+
import warnings
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def partition_data(data, evt_start, evt_end):
|
|
6
|
+
""" partition data for all channels between events evt_start and evt_end"""
|
|
7
|
+
|
|
8
|
+
# extract event values
|
|
9
|
+
e1, _ = findfield(data,evt_start)
|
|
10
|
+
e2, _ = findfield(data,evt_end)
|
|
11
|
+
|
|
12
|
+
data_new = data.copy()
|
|
13
|
+
for ch_name, ch_data in data_new.items():
|
|
14
|
+
if ch_name != 'zoosystem':
|
|
15
|
+
try:
|
|
16
|
+
data_new[ch_name]['line'] = ch_data[e1[0]:e2[0], :]
|
|
17
|
+
except (IndexError, ValueError) as e:
|
|
18
|
+
# IndexError: if e1[0]:e2[0] goes beyond the available indices
|
|
19
|
+
# ValueError: less likely, but may arise with shape mismatches
|
|
20
|
+
warnings.warn(f"Skipping {ch_name} due to error: {e}")
|
|
21
|
+
|
|
22
|
+
# partition events
|
|
23
|
+
events = ch_data['event']
|
|
24
|
+
for event_name, value in events.items():
|
|
25
|
+
original_frame = value[0]
|
|
26
|
+
if original_frame == 1:
|
|
27
|
+
continue # leave index 1 as is (per MATLAB version)
|
|
28
|
+
elif original_frame == 999:
|
|
29
|
+
continue # do not change outlier markers
|
|
30
|
+
else:
|
|
31
|
+
new_frame = original_frame - e1[0] + 1
|
|
32
|
+
data_new[ch_name]['event'][event_name][0] = new_frame
|
|
33
|
+
|
|
34
|
+
return data_new
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _partition_line(arr, evt_start, evt_end):
|
|
38
|
+
arr_new = arr[evt_start:evt_end, :]
|
|
39
|
+
return arr_new
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _partition_event(event_dict, evt_start, evt_end, arr_len):
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
# event_dict_new = {}
|
|
45
|
+
# for event, event_val in event_dict:
|
|
46
|
+
# event_val_new =
|
|
47
|
+
# event_dict_new[event] =
|
|
48
|
+
#
|
|
49
|
+
#
|
|
50
|
+
#
|
|
51
|
+
# return event_dict_new
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
def removechannel_data(data, channels, mode='remove'):
|
|
2
|
+
"""
|
|
3
|
+
File-level processing: Remove or keep specified channels in a single zoo dictionary.
|
|
4
|
+
|
|
5
|
+
Parameters:
|
|
6
|
+
- data (dict): Zoo data loaded from a file
|
|
7
|
+
- channels (list of str): List of channels to remove or keep
|
|
8
|
+
- mode (str): 'remove' or 'keep'
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
- dict: Modified zoo dictionary with updated channels
|
|
12
|
+
"""
|
|
13
|
+
if mode not in ['remove', 'keep']:
|
|
14
|
+
raise ValueError("mode must be 'remove' or 'keep'.")
|
|
15
|
+
|
|
16
|
+
zoosystem = data.get('zoosystem', {})
|
|
17
|
+
all_channels = [ch for ch in data if ch != 'zoosystem']
|
|
18
|
+
|
|
19
|
+
# Check for missing channels
|
|
20
|
+
missing = [ch for ch in channels if ch not in all_channels]
|
|
21
|
+
if missing:
|
|
22
|
+
print('Warning: the following channels were not found {}'.format(missing))
|
|
23
|
+
|
|
24
|
+
if mode == 'remove':
|
|
25
|
+
keep_channels = [ch for ch in all_channels if ch not in channels]
|
|
26
|
+
elif mode == 'keep':
|
|
27
|
+
keep_channels = [ch for ch in all_channels if ch in channels]
|
|
28
|
+
else:
|
|
29
|
+
raise ValueError("Mode must be 'remove' or 'keep'.")
|
|
30
|
+
|
|
31
|
+
# Build new zoo dictionary
|
|
32
|
+
data_new = {'zoosystem': zoosystem}
|
|
33
|
+
for ch in keep_channels:
|
|
34
|
+
data_new[ch] = data[ch]
|
|
35
|
+
|
|
36
|
+
return data_new
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def renamechannel_data(data, ch_old_names, ch_new_names, section='Video'):
|
|
5
|
+
"""
|
|
6
|
+
Rename channels in a zoo data.
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
----------
|
|
10
|
+
data : dict
|
|
11
|
+
Zoo file data.
|
|
12
|
+
ch_old_names : str or list
|
|
13
|
+
Name of the old channels.
|
|
14
|
+
ch_new_names : str or list
|
|
15
|
+
Name of the new channels.
|
|
16
|
+
|
|
17
|
+
section : str
|
|
18
|
+
Section of zoo data ('Video' or 'Analog').
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
dict
|
|
23
|
+
Updated zoo data with new channel added.
|
|
24
|
+
|
|
25
|
+
Notes
|
|
26
|
+
-----
|
|
27
|
+
- If the channel already exists, it will be overwritten.
|
|
28
|
+
- Adds channel name to the list in data['zoosystem'][section]['Channels'].
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
for i, ch_old_name in enumerate(ch_old_names):
|
|
32
|
+
ch_new_name = ch_new_names[i]
|
|
33
|
+
# Warn if overwriting
|
|
34
|
+
if ch_new_name in data:
|
|
35
|
+
print('Warning: channel {} already exists, overwriting...'.format(ch_new_name))
|
|
36
|
+
|
|
37
|
+
# Assign new channel
|
|
38
|
+
data[ch_new_name] = {
|
|
39
|
+
'line': data[ch_old_name]['line'],
|
|
40
|
+
'event': data[ch_old_name]['event']
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# remove old channel
|
|
44
|
+
data.pop(ch_old_name)
|
|
45
|
+
|
|
46
|
+
# Update channel list
|
|
47
|
+
ch_list = data['zoosystem'][section].get('Channels', [])
|
|
48
|
+
|
|
49
|
+
# If the channel list is a NumPy array, convert it to a list
|
|
50
|
+
if isinstance(ch_list, np.ndarray):
|
|
51
|
+
ch_list = ch_list.tolist()
|
|
52
|
+
|
|
53
|
+
# Ensure it's a flat list of strings
|
|
54
|
+
if isinstance(ch_list, list) and ch_new_name not in ch_list:
|
|
55
|
+
ch_list.append(ch_new_name)
|
|
56
|
+
data['zoosystem'][section]['Channels'] = ch_list
|
|
57
|
+
|
|
58
|
+
return data
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == '__main__':
|
|
62
|
+
# -------TESTING--------
|
|
63
|
+
import os
|
|
64
|
+
from src.biomechzoo.utils.zload import zload
|
|
65
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
66
|
+
project_root = os.path.dirname(current_dir)
|
|
67
|
+
fl = os.path.join(project_root, 'data', 'other', 'HC030A05.zoo')
|
|
68
|
+
|
|
69
|
+
# load zoo file
|
|
70
|
+
data = zload(fl)
|
|
71
|
+
ch_old_name = 'RKneeAngles'
|
|
72
|
+
ch_new_name = 'RightKneeAngles'
|
|
73
|
+
data = renamechannel_data(data, ch_old_name=ch_old_name, ch_new_name=ch_new_name)
|
|
74
|
+
|
|
75
|
+
if ch_old_name not in data:
|
|
76
|
+
print('RkneeAngles removed')
|
|
77
|
+
if ch_new_name in data:
|
|
78
|
+
print('RightKneeAngles added')
|
|
79
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
def renameevent_data(data, evt, nevt):
|
|
2
|
+
"""
|
|
3
|
+
Rename events in the Zoo data structure.
|
|
4
|
+
|
|
5
|
+
Parameters
|
|
6
|
+
----------
|
|
7
|
+
data : dict
|
|
8
|
+
The Zoo-formatted dictionary.
|
|
9
|
+
evt : str or list of str
|
|
10
|
+
Names of existing events to rename.
|
|
11
|
+
nevt : str or list of str
|
|
12
|
+
Names of new events to apply.
|
|
13
|
+
|
|
14
|
+
Returns
|
|
15
|
+
-------
|
|
16
|
+
data : dict
|
|
17
|
+
Updated Zoo data with renamed events.
|
|
18
|
+
"""
|
|
19
|
+
# Convert to list if passed as single string
|
|
20
|
+
if isinstance(evt, str):
|
|
21
|
+
evt = [evt]
|
|
22
|
+
if isinstance(nevt, str):
|
|
23
|
+
nevt = [nevt]
|
|
24
|
+
|
|
25
|
+
if len(evt) != len(nevt):
|
|
26
|
+
raise ValueError("`evt` and `nevt` must have the same length.")
|
|
27
|
+
|
|
28
|
+
# Get all data channels except 'zoosystem'
|
|
29
|
+
channels = [ch for ch in data if ch != 'zoosystem']
|
|
30
|
+
for old_name, new_name in zip(evt, nevt):
|
|
31
|
+
eventsRenamed = False
|
|
32
|
+
for ch in channels:
|
|
33
|
+
events = data[ch].get('event', {})
|
|
34
|
+
if old_name in events:
|
|
35
|
+
print('Renaming event {} in channel {} to {}'.format(old_name, ch, new_name))
|
|
36
|
+
data[ch]['event'][new_name] = events[old_name]
|
|
37
|
+
del data[ch]['event'][old_name]
|
|
38
|
+
eventsRenamed = True
|
|
39
|
+
|
|
40
|
+
if not eventsRenamed:
|
|
41
|
+
print('no event {} found in any channel'.format(old_name))
|
|
42
|
+
|
|
43
|
+
return data
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == '__main__':
|
|
47
|
+
# -------TESTING--------
|
|
48
|
+
import os
|
|
49
|
+
from src.biomechzoo.utils.zload import zload
|
|
50
|
+
# get path to sample zoo file
|
|
51
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
52
|
+
project_root = os.path.dirname(current_dir)
|
|
53
|
+
fl = os.path.join(project_root, 'data', 'other', 'HC030A05.zoo')
|
|
54
|
+
|
|
55
|
+
# load zoo file
|
|
56
|
+
data = zload(fl)
|
|
57
|
+
evt = ['Left_FootStrike1', 'Left_FootStrike2', 'NonExistingEvent']
|
|
58
|
+
|
|
59
|
+
# see existing keys
|
|
60
|
+
missing = [k for k in evt if k not in data['SACR']['event']]
|
|
61
|
+
print("Missing keys:", missing) # expected behavior is missing 'NonExistingEvent'
|
|
62
|
+
|
|
63
|
+
nevt = ['LFS_1', 'LFS2', 'NE_1']
|
|
64
|
+
data = renameevent_data(data, evt=evt, nevt=nevt)
|
|
65
|
+
|
|
66
|
+
# after applying renameevent_data
|
|
67
|
+
missing = [k for k in nevt if k not in data['SACR']['event']]
|
|
68
|
+
print("Missing keys:", missing) # expected behavior is missing 'NE_1'
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import os
|
|
3
|
+
import scipy.io as sio
|
|
4
|
+
from biomechzoo.utils.findfield import findfield
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def split_trial_by_gait_cycle(fl, event_name):
|
|
8
|
+
""" splits lengthy trials containing n cycles into n trials based on side"""
|
|
9
|
+
|
|
10
|
+
# extract folder and file name
|
|
11
|
+
fld = os.path.dirname(fl)
|
|
12
|
+
fl_name = os.path.splitext(os.path.basename(fl))[0]
|
|
13
|
+
|
|
14
|
+
# load file
|
|
15
|
+
data = zload(fl)
|
|
16
|
+
|
|
17
|
+
# find all events, events should follow style name1, name2, etc..
|
|
18
|
+
split_events = []
|
|
19
|
+
i = 1
|
|
20
|
+
_, channel_name = findfield(data, event_name)
|
|
21
|
+
event_name_root = event_name[0:-1]
|
|
22
|
+
while True:
|
|
23
|
+
key = f"{event_name_root}{i}"
|
|
24
|
+
if key in data[channel_name]['event']:
|
|
25
|
+
split_events.append(data[channel_name]['event'][key])
|
|
26
|
+
i += 1
|
|
27
|
+
else:
|
|
28
|
+
break
|
|
29
|
+
|
|
30
|
+
n_segments = len(split_events) - 1
|
|
31
|
+
if n_segments < 1:
|
|
32
|
+
print("Not enough {} events to split.".format(event_name_root))
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
for i in range(n_segments):
|
|
36
|
+
|
|
37
|
+
start = split_events[i][0]
|
|
38
|
+
end = split_events[i + 1][0]
|
|
39
|
+
fl_new = os.path.join(fld, fl_name + '_' + str(i+1) + '.zoo')
|
|
40
|
+
data_new = _split_trial(fld, fl_new, start, end)
|
|
41
|
+
|
|
42
|
+
def _split_trial(fld, fl_new, start, end):
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
|
|
45
|
+
if __name__ == '__main__':
|
|
46
|
+
""" testing: load a single zoo file from the other subfolder in data"""
|
|
47
|
+
# -------TESTING--------
|
|
48
|
+
from src.biomechzoo.utils.zload import zload
|
|
49
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
50
|
+
project_root = os.path.dirname(current_dir)
|
|
51
|
+
fl = os.path.join(project_root, 'data', 'other', 'HC030A05.zoo')
|
|
52
|
+
split_trial_by_gait_cycle(fl, event_name='Right_FootStrike1')
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
def batchdisp(msg, level=1, verbose='none'):
|
|
2
|
+
""" utility to control verbosity level during batch processing"""
|
|
3
|
+
level = _normalize_verbose(level)
|
|
4
|
+
verbose = _normalize_verbose(verbose)
|
|
5
|
+
if verbose >= level:
|
|
6
|
+
print(msg)
|
|
7
|
+
|
|
8
|
+
def _normalize_verbose(verbose):
|
|
9
|
+
if isinstance(verbose, int):
|
|
10
|
+
if verbose not in (0, 1, 2):
|
|
11
|
+
raise ValueError("Integer verbose level must be 0 (none), 1 (minimal), or 2 (all)")
|
|
12
|
+
return verbose
|
|
13
|
+
elif isinstance(verbose, str):
|
|
14
|
+
verbose_map = {'none': 0, 'minimal': 1, 'all': 2}
|
|
15
|
+
if verbose.lower() not in verbose_map:
|
|
16
|
+
raise ValueError("String verbose level must be 'none', 'minimal', or 'all'")
|
|
17
|
+
return verbose_map[verbose.lower()]
|
|
18
|
+
else:
|
|
19
|
+
raise TypeError("Verbose must be an int (0–2) or str ('none', 'minimal', 'all')")
|
|
20
|
+
|
|
21
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def compute_sampling_rate_from_time(t, verbose=False):
|
|
5
|
+
""" computes sampling rate from time column
|
|
6
|
+
Arguments
|
|
7
|
+
t, 1D numpy array, recorded times
|
|
8
|
+
|
|
9
|
+
Returns
|
|
10
|
+
fsamp, int: sampling rate of capture
|
|
11
|
+
"""
|
|
12
|
+
# Calculate differences between consecutive time points
|
|
13
|
+
dt = np.diff(t)
|
|
14
|
+
|
|
15
|
+
# Average time difference (seconds)
|
|
16
|
+
avg_dt = np.mean(dt)
|
|
17
|
+
|
|
18
|
+
# Sampling frequency (Hz)
|
|
19
|
+
# Sampling frequency (Hz)
|
|
20
|
+
fsamp = int(np.round(1 / avg_dt))
|
|
21
|
+
|
|
22
|
+
if verbose:
|
|
23
|
+
print('Inferred sampling rate: {} Hz'.format(fsamp))
|
|
24
|
+
|
|
25
|
+
return fsamp
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def engine(root_folder, extension='.zoo', subfolders=None, name_contains=None, verbose=False):
|
|
5
|
+
"""
|
|
6
|
+
Recursively search for files with a given extension, optionally filtering by
|
|
7
|
+
specific subfolders and substrings in filenames.
|
|
8
|
+
|
|
9
|
+
This function walks through every directory and subdirectory starting at
|
|
10
|
+
'root_folder'. If 'subfolders' is provided, it limits the search only to those
|
|
11
|
+
folders (or any of their subfolders) whose names appear in 'subfolders'. For
|
|
12
|
+
example, if subfolders=['Straight'], it will only consider files inside any
|
|
13
|
+
folder named 'Straight' at any depth within the root folder.
|
|
14
|
+
|
|
15
|
+
For each file found, it checks whether the file's extension matches the given
|
|
16
|
+
'extension' (case-insensitive). If 'name_contains' is specified, it also
|
|
17
|
+
requires the filename to contain that substring (case-insensitive).
|
|
18
|
+
|
|
19
|
+
Arguments:
|
|
20
|
+
root_folder (str): The root directory path where the search begins.
|
|
21
|
+
extension (str): File extension to search for (e.g., '.zoo', '.c3d'). Default .zoo
|
|
22
|
+
subfolders (list of str, optional): List of folder names to restrict the search to.
|
|
23
|
+
Only files inside these folders (or their subfolders) are included.
|
|
24
|
+
If None, search all subfolders.
|
|
25
|
+
name_contains (str, optional): Substring that must be present in the filename
|
|
26
|
+
(case-insensitive). If None, no substring filtering is applied.
|
|
27
|
+
verbose (bool, optional): If true, displays additional information to user
|
|
28
|
+
Returns:
|
|
29
|
+
list of str: List of full file paths matching the criteria.
|
|
30
|
+
"""
|
|
31
|
+
matched_files = []
|
|
32
|
+
|
|
33
|
+
subfolders_set = set(subfolders) if subfolders else None
|
|
34
|
+
|
|
35
|
+
for dirpath, _, filenames in os.walk(root_folder):
|
|
36
|
+
if subfolders_set is not None:
|
|
37
|
+
rel_path = os.path.relpath(dirpath, root_folder)
|
|
38
|
+
if rel_path == '.':
|
|
39
|
+
continue
|
|
40
|
+
# Split the relative path into all folder parts
|
|
41
|
+
parts = rel_path.split(os.sep)
|
|
42
|
+
# Check if any folder in the path matches one in subfolders_set
|
|
43
|
+
if not any(part in subfolders_set for part in parts):
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
for file in filenames:
|
|
47
|
+
if not file.lower().endswith(extension.lower()):
|
|
48
|
+
continue
|
|
49
|
+
if name_contains and name_contains.lower() not in file.lower():
|
|
50
|
+
continue
|
|
51
|
+
matched_files.append(os.path.join(dirpath, file))
|
|
52
|
+
|
|
53
|
+
if verbose:
|
|
54
|
+
print("Found {} {} files in subfolders named {} with substring {}:".format(len(matched_files), extension, subfolders, name_contains))
|
|
55
|
+
for f in matched_files:
|
|
56
|
+
print(" - {}".format(f))
|
|
57
|
+
|
|
58
|
+
return matched_files
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == '__main__':
|
|
62
|
+
""" testing: use engine to search for files in any subfolder called 'Straight' for files with the substring 'HC03'
|
|
63
|
+
with extension .c3d in the sample study folder (data)"""
|
|
64
|
+
# -------TESTING--------
|
|
65
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
66
|
+
project_root = os.path.dirname(current_dir)
|
|
67
|
+
sample_dir = os.path.join(project_root, 'data', 'sample_study', 'raw c3d files')
|
|
68
|
+
c3d_files = engine(sample_dir, '.c3d', subfolders=['Straight'], name_contains='HC03', verbose=True)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
def findfield(data, target_event):
|
|
2
|
+
""" searches in zoo data for the event value and channel name associated with target_event"""
|
|
3
|
+
for channel, content in data.items():
|
|
4
|
+
if channel == 'zoosystem':
|
|
5
|
+
continue
|
|
6
|
+
events = content.get('event', {})
|
|
7
|
+
if target_event in events:
|
|
8
|
+
return events[target_event], channel
|
|
9
|
+
return None, None
|
|
10
|
+
|
|
11
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from biomechzoo.utils.findfield import findfield
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_split_events(data, first_event_name):
|
|
5
|
+
""" splits lengthy trials containing n cycles into n trials based on side"""
|
|
6
|
+
|
|
7
|
+
# find all events, events should follow style name1, name2, etc..
|
|
8
|
+
split_events = []
|
|
9
|
+
_, channel_name = findfield(data, first_event_name)
|
|
10
|
+
if channel_name is None:
|
|
11
|
+
return None
|
|
12
|
+
|
|
13
|
+
event_name_root = first_event_name[0:-1]
|
|
14
|
+
first_event_number = int(first_event_name[-1])
|
|
15
|
+
i = 1
|
|
16
|
+
if first_event_number > 1:
|
|
17
|
+
i = first_event_number
|
|
18
|
+
|
|
19
|
+
while True:
|
|
20
|
+
key = f"{event_name_root}{i}"
|
|
21
|
+
if key in data[channel_name]['event']:
|
|
22
|
+
split_events.append(key)
|
|
23
|
+
i += 1
|
|
24
|
+
else:
|
|
25
|
+
break
|
|
26
|
+
|
|
27
|
+
n_segments = len(split_events) - 1
|
|
28
|
+
if n_segments < 1:
|
|
29
|
+
print("Not enough {} events to split.".format(event_name_root))
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
return split_events
|
|
33
|
+
|