biomechzoo 0.5.9__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.
- __init__.py +33 -0
- biomechzoo/__init__.py +0 -0
- biomechzoo/__main__.py +6 -0
- biomechzoo/biomech_ops/__init__.py +0 -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 +58 -0
- biomechzoo/biomech_ops/filter_line.py +85 -0
- biomechzoo/biomech_ops/movement_onset.py +53 -0
- biomechzoo/biomech_ops/normalize_data.py +36 -0
- biomechzoo/biomech_ops/normalize_line.py +51 -0
- biomechzoo/biomech_ops/phase_angle_data.py +39 -0
- biomechzoo/biomech_ops/phase_angle_line.py +48 -0
- biomechzoo/biomechzoo.py +447 -0
- biomechzoo/conversion/__init__.py +0 -0
- biomechzoo/conversion/c3d2zoo_data.py +95 -0
- biomechzoo/conversion/mvnx2zoo_data.py +113 -0
- biomechzoo/conversion/opencap2zoo_data.py +23 -0
- biomechzoo/conversion/table2zoo_data.py +114 -0
- biomechzoo/imu/__init__.py +0 -0
- biomechzoo/imu/kinematics.py +0 -0
- biomechzoo/imu/tilt_algorithm.py +112 -0
- biomechzoo/linear_algebra_ops/__init__.py +0 -0
- biomechzoo/linear_algebra_ops/compute_magnitude_data.py +43 -0
- biomechzoo/mvn/__init__.py +0 -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 +464 -0
- biomechzoo/processing/__init__.py +0 -0
- biomechzoo/processing/addchannel_data.py +71 -0
- biomechzoo/processing/addevent_data.py +116 -0
- biomechzoo/processing/explodechannel_data.py +69 -0
- biomechzoo/processing/partition_data.py +46 -0
- biomechzoo/processing/removechannel_data.py +46 -0
- biomechzoo/processing/removeevent_data.py +57 -0
- biomechzoo/processing/renamechannel_data.py +79 -0
- biomechzoo/processing/renameevent_data.py +62 -0
- biomechzoo/processing/split_trial_data.py +40 -0
- biomechzoo/statistics/eventval.py +118 -0
- biomechzoo/utils/__init__.py +0 -0
- biomechzoo/utils/batchdisp.py +21 -0
- biomechzoo/utils/compute_sampling_rate_from_time.py +25 -0
- biomechzoo/utils/engine.py +88 -0
- biomechzoo/utils/findfield.py +11 -0
- biomechzoo/utils/get_split_events.py +33 -0
- biomechzoo/utils/peak_sign.py +24 -0
- biomechzoo/utils/set_zoosystem.py +66 -0
- biomechzoo/utils/version.py +5 -0
- biomechzoo/utils/zload.py +57 -0
- biomechzoo/utils/zplot.py +61 -0
- biomechzoo/utils/zsave.py +54 -0
- biomechzoo-0.5.9.dist-info/METADATA +46 -0
- biomechzoo-0.5.9.dist-info/RECORD +58 -0
- biomechzoo-0.5.9.dist-info/WHEEL +5 -0
- biomechzoo-0.5.9.dist-info/entry_points.txt +2 -0
- biomechzoo-0.5.9.dist-info/licenses/LICENSE +21 -0
- biomechzoo-0.5.9.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from biomechzoo.utils.engine import engine
|
|
4
|
+
from biomechzoo.utils.zload import zload
|
|
5
|
+
from biomechzoo.utils.findfield import findfield # assuming this exists
|
|
6
|
+
|
|
7
|
+
def eventval(fld, dim1=None, dim2=None, ch=None, localevts=None, globalevts=None, anthroevts=None):
|
|
8
|
+
"""
|
|
9
|
+
Extract event values from .zoo files and compile into a pandas DataFrame.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
fld : str
|
|
14
|
+
Path to the root data folder containing .zoo files.
|
|
15
|
+
dim1 : list of str, optional
|
|
16
|
+
List of conditions (subfolder names under fld).
|
|
17
|
+
dim2 : list of str, optional
|
|
18
|
+
List of participant identifiers.
|
|
19
|
+
ch : list of str
|
|
20
|
+
List of channels to extract events from.
|
|
21
|
+
localevts : list of str, optional
|
|
22
|
+
List of local events.
|
|
23
|
+
globalevts : list of str, optional
|
|
24
|
+
List of global events.
|
|
25
|
+
anthroevts : list of str, optional
|
|
26
|
+
List of events stored in the metadata.Usually anthropometric data
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
pd.DataFrame
|
|
31
|
+
Columns: ['condition', 'subject', 'file', 'event_name', 'event_value']
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
zoo_files = engine(fld, extension='.zoo')
|
|
35
|
+
results = []
|
|
36
|
+
files_excluded = []
|
|
37
|
+
for fl in zoo_files:
|
|
38
|
+
data = zload(fl)
|
|
39
|
+
fname = os.path.basename(fl)
|
|
40
|
+
condition = next((c for c in (dim1 or []) if c in fl), '')
|
|
41
|
+
subject = next((s for s in (dim2 or []) if s in fl), '')
|
|
42
|
+
|
|
43
|
+
# Skip file if condition or subject is empty
|
|
44
|
+
|
|
45
|
+
if not condition or not subject:
|
|
46
|
+
files_excluded.append(fl)
|
|
47
|
+
continue
|
|
48
|
+
else:
|
|
49
|
+
print('processing {}'.format(fl))
|
|
50
|
+
|
|
51
|
+
# --- Local events ---
|
|
52
|
+
if localevts and ch:
|
|
53
|
+
print('extracting local events...')
|
|
54
|
+
for channel in ch:
|
|
55
|
+
if channel not in data:
|
|
56
|
+
print('channel {} not found'.format(channel))
|
|
57
|
+
continue
|
|
58
|
+
for evt in localevts:
|
|
59
|
+
try:
|
|
60
|
+
exd = int(data[channel]['event'][evt][0]) # xdata
|
|
61
|
+
eyd = data[channel]['event'][evt][1] # ydata
|
|
62
|
+
results.append({
|
|
63
|
+
'condition': condition,
|
|
64
|
+
'subject': subject,
|
|
65
|
+
'file': fname,
|
|
66
|
+
'event_name': f"{channel}_{evt}",
|
|
67
|
+
'event_index': exd,
|
|
68
|
+
'event_value': eyd
|
|
69
|
+
})
|
|
70
|
+
except (KeyError, IndexError, TypeError) as e:
|
|
71
|
+
print(f"Local event '{evt}' not found in channel '{channel}' for file '{fname}'")
|
|
72
|
+
|
|
73
|
+
# --- Global events ---
|
|
74
|
+
if globalevts and ch:
|
|
75
|
+
print('extracting global events...')
|
|
76
|
+
for evt in globalevts:
|
|
77
|
+
# use findfield to locate where the global event is stored
|
|
78
|
+
evt_val, evt_ch = findfield(data, evt)
|
|
79
|
+
if not evt_ch:
|
|
80
|
+
print(f"Global event '{evt}' not found in any channel for file '{fname}'")
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
exd = int(evt_val[0])
|
|
84
|
+
for channel in ch:
|
|
85
|
+
if channel not in data:
|
|
86
|
+
print(f"Skipping {evt}: channel {channel} not in data")
|
|
87
|
+
continue
|
|
88
|
+
try:
|
|
89
|
+
eyd = data[channel]['line'][exd]
|
|
90
|
+
results.append({
|
|
91
|
+
'condition': condition,
|
|
92
|
+
'subject': subject,
|
|
93
|
+
'file': fname,
|
|
94
|
+
'event_name': f"{channel}_{evt}",
|
|
95
|
+
'event_index': exd,
|
|
96
|
+
'event_value': eyd
|
|
97
|
+
})
|
|
98
|
+
except (IndexError, KeyError, TypeError) as e:
|
|
99
|
+
print(f"Global event '{evt}' index out of range in channel '{channel}' for file '{fname}': {e}")
|
|
100
|
+
|
|
101
|
+
if anthroevts and ch:
|
|
102
|
+
raise NotImplementedError
|
|
103
|
+
print('extracting anthropometric events...')
|
|
104
|
+
for evt in globalevts:
|
|
105
|
+
# use findfield to locate where the global event is stored
|
|
106
|
+
evt_val, _ = findfield(data, evt)
|
|
107
|
+
|
|
108
|
+
if evt_val:
|
|
109
|
+
results.append({
|
|
110
|
+
'condition': condition,
|
|
111
|
+
'subject': subject,
|
|
112
|
+
'file': fname,
|
|
113
|
+
'event_name': evt,
|
|
114
|
+
'event_index': evt_val[0],
|
|
115
|
+
'event_value': evt_val[1]
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
return pd.DataFrame(results)
|
|
File without changes
|
|
@@ -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,88 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def engine(root_folder, extension='.zoo', subfolders=None, name_contains=None, verbose=False):
|
|
6
|
+
"""
|
|
7
|
+
Recursively search for files with a given extension, optionally filtering by
|
|
8
|
+
specific subfolders and substrings in filenames.
|
|
9
|
+
|
|
10
|
+
This function walks through every directory and subdirectory starting at
|
|
11
|
+
'root_folder'. If 'subfolders' is provided, it limits the search only to those
|
|
12
|
+
folders (or any of their subfolders) whose names appear in 'subfolders'. For
|
|
13
|
+
example, if subfolders=['Straight'], it will only consider files inside any
|
|
14
|
+
folder named 'Straight' at any depth within the root folder.
|
|
15
|
+
|
|
16
|
+
For each file found, it checks whether the file's extension matches the given
|
|
17
|
+
'extension' (case-insensitive). If 'name_contains' is specified, it also
|
|
18
|
+
requires the filename to contain that substring (case-insensitive).
|
|
19
|
+
|
|
20
|
+
Arguments:
|
|
21
|
+
root_folder (str): The root directory path where the search begins.
|
|
22
|
+
extension (str): File extension to search for (e.g., '.zoo', '.c3d'). Default .zoo
|
|
23
|
+
subfolders (list or str, optional): List of folder names to restrict the search to.
|
|
24
|
+
Only files inside these folders (or their subfolders) are included.
|
|
25
|
+
If None, search all subfolders.
|
|
26
|
+
name_contains (str, or list; optional): Substring that must be present in the filename
|
|
27
|
+
(case-insensitive). If None, no substring filtering is applied.
|
|
28
|
+
verbose (bool, optional): If true, displays additional information to user
|
|
29
|
+
Returns:
|
|
30
|
+
list of str: List of full file paths matching the criteria.
|
|
31
|
+
"""
|
|
32
|
+
# check format of subfolder (string or list)
|
|
33
|
+
if subfolders is not None:
|
|
34
|
+
if type(subfolders) is str:
|
|
35
|
+
subfolders = [subfolders]
|
|
36
|
+
|
|
37
|
+
# check format of name_contants (str or list)
|
|
38
|
+
if name_contains is not None:
|
|
39
|
+
if type(name_contains) is str:
|
|
40
|
+
name_contains = [name_contains]
|
|
41
|
+
|
|
42
|
+
matched_files = []
|
|
43
|
+
|
|
44
|
+
subfolders_set = set(subfolders) if subfolders else None
|
|
45
|
+
for dirpath, _, filenames in os.walk(root_folder):
|
|
46
|
+
if subfolders_set is not None:
|
|
47
|
+
rel_path = os.path.relpath(dirpath, root_folder)
|
|
48
|
+
if rel_path == '.':
|
|
49
|
+
continue
|
|
50
|
+
# Split the relative path into all folder parts
|
|
51
|
+
parts = rel_path.split(os.sep)
|
|
52
|
+
# Check if any folder in the path matches one in subfolders_set
|
|
53
|
+
if not any(part in subfolders_set for part in parts):
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
for file in filenames:
|
|
57
|
+
if not file.lower().endswith(extension.lower()):
|
|
58
|
+
continue
|
|
59
|
+
full_path = os.path.join(dirpath, file)
|
|
60
|
+
if name_contains is not None:
|
|
61
|
+
match = False
|
|
62
|
+
for name_contain in name_contains:
|
|
63
|
+
if name_contain and name_contain.lower() in full_path.lower(): # <-- check full path
|
|
64
|
+
match = True
|
|
65
|
+
break
|
|
66
|
+
if not match:
|
|
67
|
+
continue
|
|
68
|
+
matched_files.append(full_path)
|
|
69
|
+
|
|
70
|
+
# sort list
|
|
71
|
+
matched_files = np.sort(matched_files)
|
|
72
|
+
|
|
73
|
+
if verbose:
|
|
74
|
+
print("Found {} {} files in subfolder(s) named {} with substring {}:".format(len(matched_files), extension, subfolders, name_contains))
|
|
75
|
+
for f in matched_files:
|
|
76
|
+
print('{}'.format(f))
|
|
77
|
+
|
|
78
|
+
return matched_files
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == '__main__':
|
|
82
|
+
""" testing: use engine to search for files in any subfolder called 'Straight' for files with the substring 'HC03'
|
|
83
|
+
with extension .c3d in the sample study folder (data)"""
|
|
84
|
+
# -------TESTING--------
|
|
85
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
86
|
+
project_root = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
|
|
87
|
+
sample_dir = os.path.join(project_root, 'data', 'sample_study', 'raw c3d files')
|
|
88
|
+
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
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def peak_sign(r):
|
|
4
|
+
"""
|
|
5
|
+
Determine whether the largest absolute peak in the signal is positive or negative.
|
|
6
|
+
|
|
7
|
+
Parameters
|
|
8
|
+
----------
|
|
9
|
+
r : array-like
|
|
10
|
+
Signal vector.
|
|
11
|
+
|
|
12
|
+
Returns
|
|
13
|
+
-------
|
|
14
|
+
sign : int
|
|
15
|
+
1 if the maximum peak is positive, -1 if negative.
|
|
16
|
+
"""
|
|
17
|
+
r = np.asarray(r)
|
|
18
|
+
max_val = np.max(r)
|
|
19
|
+
min_val = np.min(r)
|
|
20
|
+
|
|
21
|
+
if abs(max_val) > abs(min_val):
|
|
22
|
+
return 1
|
|
23
|
+
else:
|
|
24
|
+
return -1
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from biomechzoo.utils.version import get_biomechzoo_version
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def set_zoosystem(fl=None):
|
|
7
|
+
"""
|
|
8
|
+
Create the 'zoosystem' branch for data being imported to BiomechZoo.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
fl: str
|
|
12
|
+
Path to the source file (e.g., C3D or CSV).
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
zoosystem: dict
|
|
16
|
+
Dictionary containing default BiomechZoo system parameters.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Default top-level fields
|
|
20
|
+
zch = ['Analog', 'Anthro', 'AVR', 'CompInfo', 'SourceFile',
|
|
21
|
+
'Units', 'Version', 'Video']
|
|
22
|
+
|
|
23
|
+
# Initialize top-level dict
|
|
24
|
+
zoosystem = {key: {} for key in zch}
|
|
25
|
+
|
|
26
|
+
# Section-specific defaults
|
|
27
|
+
section = ['Video', 'Analog']
|
|
28
|
+
for sec in section:
|
|
29
|
+
zoosystem[sec]['Channels'] = []
|
|
30
|
+
zoosystem[sec]['Freq'] = []
|
|
31
|
+
zoosystem[sec]['Indx'] = []
|
|
32
|
+
zoosystem[sec]['ORIGINAL_START_FRAME'] = []
|
|
33
|
+
zoosystem[sec]['ORIGINAL_END_FRAME'] = []
|
|
34
|
+
zoosystem[sec]['CURRENT_START_FRAME'] = []
|
|
35
|
+
zoosystem[sec]['CURRENT_END_FRAME'] = []
|
|
36
|
+
|
|
37
|
+
# Processing and AVR defaults
|
|
38
|
+
zoosystem['Processing'] = ''
|
|
39
|
+
zoosystem['AVR'] = 0
|
|
40
|
+
|
|
41
|
+
# Force plates defaults
|
|
42
|
+
zoosystem['Analog']['FPlates'] = {
|
|
43
|
+
'CORNERS': [],
|
|
44
|
+
'NUMUSED': 0,
|
|
45
|
+
'LOCALORIGIN': [],
|
|
46
|
+
'LABELS': []
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Version and source file
|
|
50
|
+
zoosystem['Version'] = get_biomechzoo_version()
|
|
51
|
+
if fl is None:
|
|
52
|
+
zoosystem['SourceFile'] = 'None' # ensure string
|
|
53
|
+
else:
|
|
54
|
+
zoosystem['SourceFile'] = str(Path(fl)) # ensure string
|
|
55
|
+
|
|
56
|
+
# Units defaults
|
|
57
|
+
zoosystem['Units'] = {
|
|
58
|
+
'Markers': 'mm',
|
|
59
|
+
'Angles': 'deg',
|
|
60
|
+
'Forces': 'N',
|
|
61
|
+
'Moments': 'Nmm',
|
|
62
|
+
'Power': 'W/kg',
|
|
63
|
+
'Scalars': 'mm'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return zoosystem
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from scipy.io import loadmat
|
|
2
|
+
import os
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def zload(filepath):
|
|
7
|
+
if not filepath.endswith('.zoo'):
|
|
8
|
+
raise ValueError(f"{filepath} is not a .zoo file")
|
|
9
|
+
|
|
10
|
+
if not os.path.exists(filepath):
|
|
11
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
12
|
+
|
|
13
|
+
mat_data = loadmat(filepath, struct_as_record=False, squeeze_me=True)
|
|
14
|
+
|
|
15
|
+
# Remove default MATLAB metadata fields
|
|
16
|
+
mat_data = {k: v for k, v in mat_data.items() if not k.startswith('__')}
|
|
17
|
+
|
|
18
|
+
# Convert MATLAB structs to Python dicts (recursively)
|
|
19
|
+
def mat_struct_to_dict(obj):
|
|
20
|
+
if isinstance(obj, dict):
|
|
21
|
+
return {k: mat_struct_to_dict(v) for k, v in obj.items()}
|
|
22
|
+
elif hasattr(obj, '_fieldnames'):
|
|
23
|
+
return {field: mat_struct_to_dict(getattr(obj, field)) for field in obj._fieldnames}
|
|
24
|
+
elif isinstance(obj, list):
|
|
25
|
+
return [mat_struct_to_dict(item) for item in obj]
|
|
26
|
+
else:
|
|
27
|
+
return obj
|
|
28
|
+
data = {k: mat_struct_to_dict(v) for k, v in mat_data.items()}
|
|
29
|
+
|
|
30
|
+
if 'data' in data:
|
|
31
|
+
data = data['data']
|
|
32
|
+
|
|
33
|
+
# Convert Video and Analog channel arrays to Python lists
|
|
34
|
+
for sys in ['Video', 'Analog']:
|
|
35
|
+
if 'zoosystem' in data and sys in data['zoosystem']:
|
|
36
|
+
channels = data['zoosystem'][sys].get('Channels', [])
|
|
37
|
+
# Convert to list and strip spaces
|
|
38
|
+
if isinstance(channels, np.ndarray):
|
|
39
|
+
channels = channels.tolist()
|
|
40
|
+
channels = [str(ch).strip() for ch in channels]
|
|
41
|
+
data['zoosystem'][sys]['Channels'] = channels
|
|
42
|
+
|
|
43
|
+
return data
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == '__main__':
|
|
47
|
+
""" testing: load a single zoo file from the other subfolder in data"""
|
|
48
|
+
# -------TESTING--------
|
|
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
|
+
data = zload(fl)
|
|
53
|
+
|
|
54
|
+
channels = [k for k in data.keys()]
|
|
55
|
+
print('{} channels found'.format(len(channels)))
|
|
56
|
+
for ch in channels:
|
|
57
|
+
print({ch})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def zplot(data, ch, xlabel='frames', ylabel='angles (deg)'):
|
|
5
|
+
"""
|
|
6
|
+
Plot a single channel of a zoo file, along with any existing events.
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
----------
|
|
10
|
+
data : dict
|
|
11
|
+
Loaded zoo file.
|
|
12
|
+
ch : str
|
|
13
|
+
Name of the channel to plot, e.g., 'RkneeAngles'.
|
|
14
|
+
xlabel : str
|
|
15
|
+
Label for x-axis. Default is 'frames'.
|
|
16
|
+
ylabel : str
|
|
17
|
+
Label for y-axis. Default is 'angles (deg)'.
|
|
18
|
+
|
|
19
|
+
Returns
|
|
20
|
+
-------
|
|
21
|
+
None
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
if ch not in data:
|
|
25
|
+
raise KeyError(f"Channel '{ch}' not found in data.")
|
|
26
|
+
|
|
27
|
+
y = data[ch]['line']
|
|
28
|
+
x = range(len(y))
|
|
29
|
+
|
|
30
|
+
plt.figure(figsize=(10, 4))
|
|
31
|
+
plt.plot(x, y, label='Signal', linewidth=2)
|
|
32
|
+
plt.title(ch)
|
|
33
|
+
plt.xlabel(xlabel)
|
|
34
|
+
plt.ylabel(ylabel)
|
|
35
|
+
plt.grid(True)
|
|
36
|
+
|
|
37
|
+
# Plot events if available
|
|
38
|
+
events = data[ch].get('event', {})
|
|
39
|
+
for name, coords in events.items():
|
|
40
|
+
evtx, evty = coords[0], coords[1]
|
|
41
|
+
plt.plot(evtx, evty, 'ro')
|
|
42
|
+
plt.text(evtx, evty, name, fontsize=8, color='red', ha='left', va='bottom')
|
|
43
|
+
|
|
44
|
+
plt.tight_layout()
|
|
45
|
+
plt.show()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == '__main__':
|
|
49
|
+
# -------TESTING--------
|
|
50
|
+
import os
|
|
51
|
+
from biomechzoo.utils.zload import zload
|
|
52
|
+
|
|
53
|
+
# get path to sample zoo file
|
|
54
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
55
|
+
project_root = os.path.dirname(current_dir)
|
|
56
|
+
fl = os.path.join(project_root, 'data', 'other', 'HC030A05.zoo')
|
|
57
|
+
|
|
58
|
+
# load zoo file
|
|
59
|
+
data = zload(fl)
|
|
60
|
+
ch = 'SACR'
|
|
61
|
+
zplot(data, ch)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from scipy.io import savemat
|
|
2
|
+
import inspect
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from biomechzoo.utils.batchdisp import batchdisp
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def zsave(fl, data, inplace=True, out_folder=None, root_folder=None, verbose=False):
|
|
9
|
+
"""
|
|
10
|
+
Save zoo data to .zoo file (MAT format)
|
|
11
|
+
|
|
12
|
+
Arguments:
|
|
13
|
+
fl (str): Full path to original .zoo file
|
|
14
|
+
data (dict): Zoo data to save
|
|
15
|
+
inplace (bool): Whether to overwrite original file
|
|
16
|
+
out_folder (str or None): If not inplace, output folder name (relative to root_folder or file location)
|
|
17
|
+
root_folder (str or None): Optional base directory for saving when inplace=False
|
|
18
|
+
"""
|
|
19
|
+
# Get caller function name for logging
|
|
20
|
+
caller_name = inspect.stack()[1].function
|
|
21
|
+
|
|
22
|
+
# Initialize zoosystem and processing history
|
|
23
|
+
zoosystem = data.get('zoosystem', {})
|
|
24
|
+
processing = zoosystem.get('Processing', [])
|
|
25
|
+
if not isinstance(processing, list):
|
|
26
|
+
processing = [processing]
|
|
27
|
+
processing.append(caller_name)
|
|
28
|
+
zoosystem['Processing'] = processing
|
|
29
|
+
data['zoosystem'] = zoosystem
|
|
30
|
+
|
|
31
|
+
# Determine save path
|
|
32
|
+
if inplace:
|
|
33
|
+
fl_new = fl
|
|
34
|
+
out_dir = os.path.dirname(fl)
|
|
35
|
+
else:
|
|
36
|
+
if out_folder is None:
|
|
37
|
+
out_folder = 'processed'
|
|
38
|
+
|
|
39
|
+
if root_folder is None:
|
|
40
|
+
root_folder = os.path.dirname(fl)
|
|
41
|
+
|
|
42
|
+
root_path = os.path.dirname(root_folder)
|
|
43
|
+
in_folder = os.path.basename(root_folder)
|
|
44
|
+
out_dir = os.path.join(root_path, out_folder)
|
|
45
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
46
|
+
fl_new = fl.replace(in_folder, out_folder)
|
|
47
|
+
save_folder = os.path.dirname(fl_new)
|
|
48
|
+
os.makedirs(save_folder, exist_ok=True)
|
|
49
|
+
|
|
50
|
+
# Save the .zoo file
|
|
51
|
+
savemat(fl_new, data)
|
|
52
|
+
batchdisp('all files saved to ' + out_dir, level=1, verbose=verbose)
|
|
53
|
+
|
|
54
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: biomechzoo
|
|
3
|
+
Version: 0.5.9
|
|
4
|
+
Summary: Python implementation of the biomechZoo toolbox
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/mcgillmotionlab/biomechzoo
|
|
7
|
+
Requires-Python: <3.12,>=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: ezc3d>=1.5.19
|
|
11
|
+
Requires-Dist: matplotlib>=3.10.6
|
|
12
|
+
Requires-Dist: numpy==2.2.6
|
|
13
|
+
Requires-Dist: pandas>=2.3.2
|
|
14
|
+
Requires-Dist: scipy>=1.16.2
|
|
15
|
+
Requires-Dist: pyarrow>=19.0.0
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# BiomechZoo for Python
|
|
19
|
+
This is a development version of the biomechzoo toolbox for python.
|
|
20
|
+
|
|
21
|
+
## How to install
|
|
22
|
+
- biomechZoo for python is now an official package, you can simply add biomechZoo to your environment using
|
|
23
|
+
``pip install biomechzoo``
|
|
24
|
+
|
|
25
|
+
## Usage notes
|
|
26
|
+
- If you need to install a specific version, run ``pip install biomechzoo==x.x.x`` where x.x.x is the version number.
|
|
27
|
+
- If you need to update biomechzoo to the latest version in your env, run ``pip install biomechzoo --upgrade``
|
|
28
|
+
|
|
29
|
+
## Dependencies notes
|
|
30
|
+
- We use Python 3.11 for compatibility with https://github.com/stanfordnmbl/opencap-processing
|
|
31
|
+
- We use Numpy 2.2.6 for compatibility with https://pypi.org/project/numba/
|
|
32
|
+
|
|
33
|
+
See also http://www.github.com/mcgillmotionlab/biomechzoo or http://www.biomechzoo.com for more information
|
|
34
|
+
|
|
35
|
+
## Developer notes
|
|
36
|
+
|
|
37
|
+
### Installing a dev environment
|
|
38
|
+
conda create -n biomechzoo-dev python=3.11
|
|
39
|
+
conda activate biomechzoo-dev
|
|
40
|
+
cd biomechzoo root folder
|
|
41
|
+
pip install -e ".[dev]"
|
|
42
|
+
|
|
43
|
+
### import issues
|
|
44
|
+
if using PyCharm:
|
|
45
|
+
- Right-click on src/.
|
|
46
|
+
- Select Mark Directory as → Sources Root.
|