biomechzoo 0.7.1__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.
- biomechzoo-0.7.1/LICENSE +21 -0
- biomechzoo-0.7.1/PKG-INFO +49 -0
- biomechzoo-0.7.1/README.md +29 -0
- biomechzoo-0.7.1/pyproject.toml +35 -0
- biomechzoo-0.7.1/setup.cfg +4 -0
- biomechzoo-0.7.1/src/__init__.py +33 -0
- biomechzoo-0.7.1/src/biomechzoo/__init__.py +0 -0
- biomechzoo-0.7.1/src/biomechzoo/__main__.py +6 -0
- biomechzoo-0.7.1/src/biomechzoo/biomech_ops/__init__.py +0 -0
- biomechzoo-0.7.1/src/biomechzoo/biomech_ops/continuous_relative_phase_data.py +31 -0
- biomechzoo-0.7.1/src/biomechzoo/biomech_ops/continuous_relative_phase_line.py +36 -0
- biomechzoo-0.7.1/src/biomechzoo/biomech_ops/filter_data.py +58 -0
- biomechzoo-0.7.1/src/biomechzoo/biomech_ops/filter_line.py +86 -0
- biomechzoo-0.7.1/src/biomechzoo/biomech_ops/movement_onset.py +131 -0
- biomechzoo-0.7.1/src/biomechzoo/biomech_ops/normalize_data.py +37 -0
- biomechzoo-0.7.1/src/biomechzoo/biomech_ops/normalize_line.py +51 -0
- biomechzoo-0.7.1/src/biomechzoo/biomech_ops/phase_angle_data.py +39 -0
- biomechzoo-0.7.1/src/biomechzoo/biomech_ops/phase_angle_line.py +48 -0
- biomechzoo-0.7.1/src/biomechzoo/biomechzoo.py +518 -0
- biomechzoo-0.7.1/src/biomechzoo/conversion/__init__.py +0 -0
- biomechzoo-0.7.1/src/biomechzoo/conversion/c3d2zoo_data.py +95 -0
- biomechzoo-0.7.1/src/biomechzoo/conversion/mvnx2zoo_data.py +113 -0
- biomechzoo-0.7.1/src/biomechzoo/conversion/opencap2zoo_data.py +23 -0
- biomechzoo-0.7.1/src/biomechzoo/conversion/table2zoo_data.py +112 -0
- biomechzoo-0.7.1/src/biomechzoo/imu/__init__.py +0 -0
- biomechzoo-0.7.1/src/biomechzoo/imu/kinematics.py +66 -0
- biomechzoo-0.7.1/src/biomechzoo/imu/step_detection.py +118 -0
- biomechzoo-0.7.1/src/biomechzoo/imu/tilt_algorithm.py +124 -0
- biomechzoo-0.7.1/src/biomechzoo/linear_algebra_ops/__init__.py +0 -0
- biomechzoo-0.7.1/src/biomechzoo/linear_algebra_ops/compute_magnitude_data.py +36 -0
- biomechzoo-0.7.1/src/biomechzoo/linear_algebra_ops/rectify.py +25 -0
- biomechzoo-0.7.1/src/biomechzoo/mvn/__init__.py +0 -0
- biomechzoo-0.7.1/src/biomechzoo/mvn/load_mvnx.py +514 -0
- biomechzoo-0.7.1/src/biomechzoo/mvn/main_mvnx.py +75 -0
- biomechzoo-0.7.1/src/biomechzoo/mvn/mvn.py +232 -0
- biomechzoo-0.7.1/src/biomechzoo/mvn/mvnx_file_accessor.py +464 -0
- biomechzoo-0.7.1/src/biomechzoo/processing/__init__.py +0 -0
- biomechzoo-0.7.1/src/biomechzoo/processing/addchannel_data.py +62 -0
- biomechzoo-0.7.1/src/biomechzoo/processing/addevent_data.py +149 -0
- biomechzoo-0.7.1/src/biomechzoo/processing/explodechannel_data.py +87 -0
- biomechzoo-0.7.1/src/biomechzoo/processing/partition_data.py +47 -0
- biomechzoo-0.7.1/src/biomechzoo/processing/removechannel_data.py +50 -0
- biomechzoo-0.7.1/src/biomechzoo/processing/removeevent_data.py +57 -0
- biomechzoo-0.7.1/src/biomechzoo/processing/renamechannel_data.py +78 -0
- biomechzoo-0.7.1/src/biomechzoo/processing/renameevent_data.py +62 -0
- biomechzoo-0.7.1/src/biomechzoo/processing/split_trial_data.py +44 -0
- biomechzoo-0.7.1/src/biomechzoo/statistics/__init__.py +0 -0
- biomechzoo-0.7.1/src/biomechzoo/statistics/eventval.py +118 -0
- biomechzoo-0.7.1/src/biomechzoo/utils/__init__.py +0 -0
- biomechzoo-0.7.1/src/biomechzoo/utils/batchdisp.py +21 -0
- biomechzoo-0.7.1/src/biomechzoo/utils/common_substring.py +24 -0
- biomechzoo-0.7.1/src/biomechzoo/utils/compute_sampling_rate_from_time.py +25 -0
- biomechzoo-0.7.1/src/biomechzoo/utils/engine.py +93 -0
- biomechzoo-0.7.1/src/biomechzoo/utils/findfield.py +21 -0
- biomechzoo-0.7.1/src/biomechzoo/utils/get_split_events.py +33 -0
- biomechzoo-0.7.1/src/biomechzoo/utils/peak_sign.py +24 -0
- biomechzoo-0.7.1/src/biomechzoo/utils/set_zoosystem.py +66 -0
- biomechzoo-0.7.1/src/biomechzoo/utils/update_channel_list.py +35 -0
- biomechzoo-0.7.1/src/biomechzoo/utils/version.py +5 -0
- biomechzoo-0.7.1/src/biomechzoo/utils/zload.py +57 -0
- biomechzoo-0.7.1/src/biomechzoo/utils/zplot.py +61 -0
- biomechzoo-0.7.1/src/biomechzoo/utils/zsave.py +54 -0
- biomechzoo-0.7.1/src/biomechzoo/visualization/__init__.py +0 -0
- biomechzoo-0.7.1/src/biomechzoo/visualization/ensembler.py +491 -0
- biomechzoo-0.7.1/src/biomechzoo/visualization/qc_app.py +149 -0
- biomechzoo-0.7.1/src/biomechzoo.egg-info/PKG-INFO +49 -0
- biomechzoo-0.7.1/src/biomechzoo.egg-info/SOURCES.txt +69 -0
- biomechzoo-0.7.1/src/biomechzoo.egg-info/dependency_links.txt +1 -0
- biomechzoo-0.7.1/src/biomechzoo.egg-info/entry_points.txt +2 -0
- biomechzoo-0.7.1/src/biomechzoo.egg-info/requires.txt +9 -0
- biomechzoo-0.7.1/src/biomechzoo.egg-info/top_level.txt +2 -0
biomechzoo-0.7.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [2025] McGill MOTION Lab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: biomechzoo
|
|
3
|
+
Version: 0.7.1
|
|
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
|
+
Requires-Dist: plotly>=6.4.0
|
|
17
|
+
Requires-Dist: kaleido>=1.2.0
|
|
18
|
+
Requires-Dist: dash>=3.3.0
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# BiomechZoo for Python
|
|
22
|
+
This is a development version of the biomechzoo toolbox for python.
|
|
23
|
+
|
|
24
|
+
## How to install
|
|
25
|
+
- biomechZoo for python is now an official package, you can simply add biomechZoo to your environment using
|
|
26
|
+
``pip install biomechzoo``
|
|
27
|
+
|
|
28
|
+
## Usage notes
|
|
29
|
+
- If you need to install a specific version, run ``pip install biomechzoo==x.x.x`` where x.x.x is the version number.
|
|
30
|
+
- If you need to update biomechzoo to the latest version in your env, run ``pip install biomechzoo --upgrade``
|
|
31
|
+
|
|
32
|
+
## Dependencies notes
|
|
33
|
+
- We use Python 3.11 for compatibility with https://github.com/stanfordnmbl/opencap-processing
|
|
34
|
+
- We use Numpy 2.2.6 for compatibility with https://pypi.org/project/numba/
|
|
35
|
+
|
|
36
|
+
See also http://www.github.com/mcgillmotionlab/biomechzoo or http://www.biomechzoo.com for more information
|
|
37
|
+
|
|
38
|
+
## Developer notes
|
|
39
|
+
|
|
40
|
+
### Installing a dev environment
|
|
41
|
+
conda create -n biomechzoo-dev python=3.11
|
|
42
|
+
conda activate biomechzoo-dev
|
|
43
|
+
cd biomechzoo root folder
|
|
44
|
+
pip install -e ".[dev]"
|
|
45
|
+
|
|
46
|
+
### import issues
|
|
47
|
+
if using PyCharm:
|
|
48
|
+
- Right-click on src/.
|
|
49
|
+
- Select Mark Directory as → Sources Root.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# BiomechZoo for Python
|
|
2
|
+
This is a development version of the biomechzoo toolbox for python.
|
|
3
|
+
|
|
4
|
+
## How to install
|
|
5
|
+
- biomechZoo for python is now an official package, you can simply add biomechZoo to your environment using
|
|
6
|
+
``pip install biomechzoo``
|
|
7
|
+
|
|
8
|
+
## Usage notes
|
|
9
|
+
- If you need to install a specific version, run ``pip install biomechzoo==x.x.x`` where x.x.x is the version number.
|
|
10
|
+
- If you need to update biomechzoo to the latest version in your env, run ``pip install biomechzoo --upgrade``
|
|
11
|
+
|
|
12
|
+
## Dependencies notes
|
|
13
|
+
- We use Python 3.11 for compatibility with https://github.com/stanfordnmbl/opencap-processing
|
|
14
|
+
- We use Numpy 2.2.6 for compatibility with https://pypi.org/project/numba/
|
|
15
|
+
|
|
16
|
+
See also http://www.github.com/mcgillmotionlab/biomechzoo or http://www.biomechzoo.com for more information
|
|
17
|
+
|
|
18
|
+
## Developer notes
|
|
19
|
+
|
|
20
|
+
### Installing a dev environment
|
|
21
|
+
conda create -n biomechzoo-dev python=3.11
|
|
22
|
+
conda activate biomechzoo-dev
|
|
23
|
+
cd biomechzoo root folder
|
|
24
|
+
pip install -e ".[dev]"
|
|
25
|
+
|
|
26
|
+
### import issues
|
|
27
|
+
if using PyCharm:
|
|
28
|
+
- Right-click on src/.
|
|
29
|
+
- Select Mark Directory as → Sources Root.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "biomechzoo"
|
|
3
|
+
version = "0.7.1"
|
|
4
|
+
description = "Python implementation of the biomechZoo toolbox"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11,<3.12" # max version for opencap-process tools is 3.11
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICEN[CS]E*"]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"ezc3d>=1.5.19", # for reading c3d files
|
|
11
|
+
"matplotlib>=3.10.6",
|
|
12
|
+
"numpy==2.2.6", # max version for opencap-process tools
|
|
13
|
+
"pandas>=2.3.2",
|
|
14
|
+
"scipy>=1.16.2",
|
|
15
|
+
"pyarrow>=19.0.0", # for parquet support
|
|
16
|
+
"plotly>=6.4.0", # for ensembler
|
|
17
|
+
"kaleido>=1.2.0", # for ensembler (saving images)
|
|
18
|
+
"dash>=3.3.0", # for ensembler (quality check)
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/mcgillmotionlab/biomechzoo"
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
biomechzoo = "main:main"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["setuptools>=78.1.0", "wheel>=0.45.1"]
|
|
29
|
+
build-backend = "setuptools.build_meta"
|
|
30
|
+
|
|
31
|
+
[[tool.uv.index]]
|
|
32
|
+
name = "testbiomechzoo"
|
|
33
|
+
url = "https://test.pypi.org/simple/"
|
|
34
|
+
publish-url = "https://test.pypi.org/legacy/"
|
|
35
|
+
explicit = true
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BiomechZoo: A Python toolbox for processing and analyzing human movement data.
|
|
3
|
+
|
|
4
|
+
This package provides functions for converting, processing, analyzing,
|
|
5
|
+
and visualizing biomechanical data (e.g., motion capture, EMG, kinetics).
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
from biomechzoo import BiomechZoo
|
|
9
|
+
from biomechzoo.conversion import c3d2zoo
|
|
10
|
+
|
|
11
|
+
zoo = BiomechZoo()
|
|
12
|
+
zoo.conversion.c3d2zoo('path/to/data')
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# Import main class or entry point
|
|
16
|
+
from .biomechzo import BiomechZoo
|
|
17
|
+
|
|
18
|
+
# Import commonly used submodules
|
|
19
|
+
from . import conversion
|
|
20
|
+
from . import processing
|
|
21
|
+
from . import plotting
|
|
22
|
+
from . import utils
|
|
23
|
+
|
|
24
|
+
# Define what gets exposed with "from biomechzoo import *"
|
|
25
|
+
__all__ = [
|
|
26
|
+
"BiomechZoo",
|
|
27
|
+
"conversion",
|
|
28
|
+
"processing",
|
|
29
|
+
"plotting",
|
|
30
|
+
"utils",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
__version__ = "0.4.4"
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from biomechzoo.biomech_ops.continuous_relative_phase_line import continuous_relative_phase_line
|
|
2
|
+
from biomechzoo.processing.addchannel_data import addchannel_data
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def continuous_relative_phase_data(data, ch_dist, ch_prox):
|
|
6
|
+
""" This function determines the CRP on a 0-180 scale, correcting for
|
|
7
|
+
discontinuity in the signals >180.
|
|
8
|
+
See Also phase_angle_data.py and phase_angle_line.py
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
data_new = data.copy()
|
|
12
|
+
prox = data[ch_prox]['line']
|
|
13
|
+
dist = data[ch_dist]['line']
|
|
14
|
+
crp = continuous_relative_phase_line(dist, prox)
|
|
15
|
+
data_new = addchannel_data(data_new, ch_new_name=ch_dist + '_' + ch_prox + '_' + 'crp', ch_new_data=crp)
|
|
16
|
+
return data_new
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == '__main__':
|
|
20
|
+
# -------TESTING--------
|
|
21
|
+
import os
|
|
22
|
+
from biomechzoo.utils.zload import zload
|
|
23
|
+
from biomechzoo.utils.zplot import zplot
|
|
24
|
+
# note: crp should be computed on phase angle data. Here we just demonstrate that it works.
|
|
25
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
26
|
+
project_root = os.path.dirname(current_dir)
|
|
27
|
+
fl = os.path.join(project_root, 'data', 'other', 'HC032A18_exploded.zoo')
|
|
28
|
+
data = zload(fl)
|
|
29
|
+
data = continuous_relative_phase_data(data, ch_dist='RKneeAngles_x', ch_prox='RHipAngles_x')
|
|
30
|
+
zplot(data, 'RKneeAngles_x_RHipAngles_x_crp')
|
|
31
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
def continuous_relative_phase_line(dist, prox):
|
|
2
|
+
""" This function determines the CRP on a 0-180 scale, correcting for
|
|
3
|
+
discontinuity in the signals >180.
|
|
4
|
+
|
|
5
|
+
Arguments
|
|
6
|
+
dist, ndarray: data of distal segment or joint
|
|
7
|
+
prox, ndarray: data of proximal segment or joibt
|
|
8
|
+
|
|
9
|
+
Returns
|
|
10
|
+
crp, ndarray: continous relative phase betweeen dist and prox data
|
|
11
|
+
"""
|
|
12
|
+
temp_CRP = abs(dist - prox)
|
|
13
|
+
idx = temp_CRP > 180 # This corrects discontinuity in the data and puts everything on a 0-180 scale.
|
|
14
|
+
temp_CRP[idx] = 360 - temp_CRP[idx]
|
|
15
|
+
crp = temp_CRP
|
|
16
|
+
return crp
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == '__main__':
|
|
20
|
+
# -------TESTING--------
|
|
21
|
+
import os
|
|
22
|
+
from biomechzoo.utils.zload import zload
|
|
23
|
+
from biomechzoo.biomech_ops.phase_angle_line import phase_angle_line
|
|
24
|
+
from matplotlib import pyplot as plt
|
|
25
|
+
# note: crp should be computed on phase angle data. Here we just demonstrate that it works.
|
|
26
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
27
|
+
project_root = os.path.dirname(current_dir)
|
|
28
|
+
fl = os.path.join(project_root, 'data', 'other', 'HC032A18_exploded.zoo')
|
|
29
|
+
data = zload(fl)
|
|
30
|
+
knee = data['RKneeAngles_x']['line']
|
|
31
|
+
hip = data['RHipAngles_x']['line']
|
|
32
|
+
knee_pa = phase_angle_line(knee)
|
|
33
|
+
hip_pa = phase_angle_line(hip)
|
|
34
|
+
crp = continuous_relative_phase_line(knee_pa, hip_pa)
|
|
35
|
+
plt.plot(crp)
|
|
36
|
+
plt.show()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from biomechzoo.biomech_ops.filter_line import filter_line
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def filter_data(data, ch, filt=None):
|
|
5
|
+
"""
|
|
6
|
+
Filter one or more channels from a zoo data dictionary using specified filter parameters.
|
|
7
|
+
|
|
8
|
+
Arguments
|
|
9
|
+
----------
|
|
10
|
+
data : dict
|
|
11
|
+
The zoo data dictionary containing signal channels.
|
|
12
|
+
ch : str or list of str
|
|
13
|
+
The name(s) of the channel(s) to filter.
|
|
14
|
+
filt : dict, optional
|
|
15
|
+
Dictionary specifying filter parameters. Keys may include:
|
|
16
|
+
- 'ftype': 'butter' (default)
|
|
17
|
+
- 'order': filter order (default: 4)
|
|
18
|
+
- 'cutoff': cutoff frequency or tuple (Hz)
|
|
19
|
+
- 'btype': 'low', 'high', 'bandpass', 'bandstop' (default: 'lowpass')
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
dict
|
|
24
|
+
The updated data dictionary with filtered channels.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
if filt is None:
|
|
28
|
+
filt = {'ftype': 'butter',
|
|
29
|
+
'order': 4,
|
|
30
|
+
'cutoff': 10,
|
|
31
|
+
'btype': 'lowpass',
|
|
32
|
+
'filtfilt': True}
|
|
33
|
+
|
|
34
|
+
if isinstance(ch, str):
|
|
35
|
+
ch = [ch]
|
|
36
|
+
|
|
37
|
+
# loop through all channels and filter
|
|
38
|
+
for c in ch:
|
|
39
|
+
if c not in data:
|
|
40
|
+
raise KeyError('Channel {} not found in data'.format(c))
|
|
41
|
+
|
|
42
|
+
if 'fs' not in filt:
|
|
43
|
+
|
|
44
|
+
video_channels = data['zoosystem']['Video']['Channels']
|
|
45
|
+
analog_channels = data['zoosystem']['Analog']['Channels']
|
|
46
|
+
|
|
47
|
+
if c in analog_channels:
|
|
48
|
+
filt['fs'] = data['zoosystem']['Analog']['Freq']
|
|
49
|
+
elif c in video_channels:
|
|
50
|
+
filt['fs'] = data['zoosystem']['Video']['Freq']
|
|
51
|
+
else:
|
|
52
|
+
raise ValueError('Channel not analog or video')
|
|
53
|
+
|
|
54
|
+
signal_raw = data[c]['line']
|
|
55
|
+
signal_filtered = filter_line(signal_raw=signal_raw, filt=filt)
|
|
56
|
+
data[c]['line'] = signal_filtered
|
|
57
|
+
|
|
58
|
+
return data
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
[b, a] = sgl.butter(N=order, Wn=norm_cutoff, btype=btype, )
|
|
46
|
+
signal_filtered = sgl.filtfilt(b, a, signal_raw)
|
|
47
|
+
else:
|
|
48
|
+
raise NotImplementedError(f"Filter type '{ftype}' not implemented.")
|
|
49
|
+
|
|
50
|
+
return signal_filtered
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def kt_butter(ts, fc, fs, order=2, btype='lowpass', filtfilt=True):
|
|
54
|
+
"""
|
|
55
|
+
Apply a Butterworth filter to data.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
ts, ndarray, 1d.
|
|
60
|
+
fc, Cut-off frequency in Hz. This is a float for single-frequency filters
|
|
61
|
+
(lowpass, highpass), or a tuple of two floats (e.g., (10., 13.)
|
|
62
|
+
for two-frequency filters (bandpass, bandstop)).
|
|
63
|
+
order, Optional. Order of the filter. Default is 2.
|
|
64
|
+
btype, Optional. Can be either "lowpass", "highpass", "bandpass" or
|
|
65
|
+
"bandstop". Default is "lowpass".
|
|
66
|
+
filtfilt, Optional. If True, the filter is applied two times in reverse direction
|
|
67
|
+
to eliminate time lag. If False, the filter is applied only in forward
|
|
68
|
+
direction. Default is True.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
ts_f, A copy of the input data which each data being filtered.
|
|
73
|
+
|
|
74
|
+
Notes:
|
|
75
|
+
- This code was adapted from kineticstoolkit Thanks @felxi
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
sos = sgl.butter(order, fc, btype, analog=False, output="sos", fs=fs)
|
|
79
|
+
|
|
80
|
+
# Filter
|
|
81
|
+
if filtfilt:
|
|
82
|
+
ts_f = sgl.sosfiltfilt(sos, ts, axis=0)
|
|
83
|
+
else:
|
|
84
|
+
ts_f = sgl.sosfilt(sos,ts, axis=0)
|
|
85
|
+
|
|
86
|
+
return ts_f
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy.signal as signal
|
|
3
|
+
|
|
4
|
+
def movement_onset(yd, fsamp, constants):
|
|
5
|
+
"""
|
|
6
|
+
Extracts movement onset based on the average and standard deviation of a sliding window
|
|
7
|
+
Standard thresholds for running are mean_thresh=1.2, std_thresh=0.2. For walking mean_thresh=0.6, std_thresh=0.2.
|
|
8
|
+
|
|
9
|
+
yd: 1d array of the vector
|
|
10
|
+
fsamp: sampling frequency
|
|
11
|
+
constants: [mean_thresh, std_thresh]
|
|
12
|
+
etype: 'movement_onset' or 'movement_offset'
|
|
13
|
+
"""
|
|
14
|
+
acc_mag = yd.copy()
|
|
15
|
+
acc_mag_filtered = bw_filter(data=acc_mag, fsamp=fsamp, N=4, fc=20, btype="low")
|
|
16
|
+
features, timestamps = sliding_window_features(ch_data=acc_mag_filtered, fsamp=fsamp)
|
|
17
|
+
|
|
18
|
+
mean_thresh, std_thresh = constants[0], constants[1]
|
|
19
|
+
min_thresh = 0.1
|
|
20
|
+
onset_time = None
|
|
21
|
+
while onset_time is None and mean_thresh > min_thresh:
|
|
22
|
+
# ----Check if already moving----
|
|
23
|
+
if check_already_moving(features=features, mean_thresh=mean_thresh, std_thresh=std_thresh):
|
|
24
|
+
onset_time = timestamps[0]
|
|
25
|
+
|
|
26
|
+
# ----Try detecting onset----
|
|
27
|
+
else:
|
|
28
|
+
onset_index = detect_movement_onset(features, fsamp, mean_thresh, std_thresh)
|
|
29
|
+
if onset_index is not None:
|
|
30
|
+
onset_time = timestamps[onset_index]
|
|
31
|
+
else:
|
|
32
|
+
# relax thresholds for next iteration
|
|
33
|
+
mean_thresh /=2
|
|
34
|
+
std_thresh /= 2
|
|
35
|
+
|
|
36
|
+
return onset_time
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def movement_offset(yd, fsamp, constants):
|
|
40
|
+
|
|
41
|
+
# ----extract the constants----
|
|
42
|
+
mean_thresh, std_thresh = constants[0], constants[1]
|
|
43
|
+
min_thresh = 0.1
|
|
44
|
+
onset_time = None
|
|
45
|
+
|
|
46
|
+
# ----Reverse, filter and extract features from the signa;----
|
|
47
|
+
acc_mag = yd.copy()
|
|
48
|
+
acc_mag = acc_mag[::-1]
|
|
49
|
+
|
|
50
|
+
acc_mag_filtered = bw_filter(data=acc_mag, N=4, fc=20, btype="low", fsamp=fsamp)
|
|
51
|
+
features, timestamps = sliding_window_features(ch_data=acc_mag_filtered, fsamp=fsamp)
|
|
52
|
+
|
|
53
|
+
# ----reverse timestamps----
|
|
54
|
+
timestamps = timestamps[::-1]
|
|
55
|
+
|
|
56
|
+
while onset_time is None and mean_thresh > min_thresh:
|
|
57
|
+
# ----Check if already moving----
|
|
58
|
+
if check_already_moving(features=features, mean_thresh=mean_thresh, std_thresh=std_thresh):
|
|
59
|
+
onset_time = timestamps[0]
|
|
60
|
+
|
|
61
|
+
# ----Try detecting onset----
|
|
62
|
+
else:
|
|
63
|
+
onset_index = detect_movement_onset(features, fsamp, mean_thresh, std_thresh)
|
|
64
|
+
if onset_index is not None:
|
|
65
|
+
onset_time = timestamps[onset_index]
|
|
66
|
+
else:
|
|
67
|
+
# relax thresholds for next iteration
|
|
68
|
+
mean_thresh /= 2
|
|
69
|
+
std_thresh /= 2
|
|
70
|
+
|
|
71
|
+
return onset_time
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def sliding_window_features(ch_data, fsamp):
|
|
75
|
+
# ----sliding window features----
|
|
76
|
+
features = []
|
|
77
|
+
timestamps = []
|
|
78
|
+
window_size = 2 * fsamp # windows van 2 seconds
|
|
79
|
+
step_size = 1 * fsamp # with an overlap of 1 seconds
|
|
80
|
+
|
|
81
|
+
for start in range(0, len(ch_data) - window_size, step_size):
|
|
82
|
+
segment = ch_data[start:start + window_size]
|
|
83
|
+
mean_val = segment.mean()
|
|
84
|
+
std_val = segment.std()
|
|
85
|
+
# entropy = -np.sum((segment / np.sum(segment)) * np.log2(segment / np.sum(segment) + 1e-12))
|
|
86
|
+
timestamps.append(start)
|
|
87
|
+
features.append((mean_val, std_val))
|
|
88
|
+
|
|
89
|
+
return np.array(features), np.array(timestamps)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def check_already_moving(features, mean_thresh=1.2, std_thresh=0.2):
|
|
93
|
+
initial_window = features[:5] # First few seconds
|
|
94
|
+
if np.all(initial_window[:, 0] > mean_thresh) and np.all(initial_window[:, 1] > std_thresh):
|
|
95
|
+
return True
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
def detect_movement_onset(features, fs, mean_thresh=1.2, std_thresh=0.2, min_duration=3):
|
|
99
|
+
movement_flags = (features[:, 0] > mean_thresh) & (features[:, 1] > std_thresh)
|
|
100
|
+
onset_index = None
|
|
101
|
+
for i in range(len(movement_flags) - int(min_duration * fs / 50)):
|
|
102
|
+
if np.all(movement_flags[i:i + int(min_duration * fs / 50)]):
|
|
103
|
+
onset_index = i
|
|
104
|
+
break
|
|
105
|
+
return onset_index if onset_index is not None else None
|
|
106
|
+
|
|
107
|
+
def bw_filter(data, fsamp, N, fc, btype, output="ba"):
|
|
108
|
+
"""
|
|
109
|
+
Basic zero-phase butterworth filter.
|
|
110
|
+
:param data: 1xN array
|
|
111
|
+
:param N: filter order
|
|
112
|
+
:param fc: cutoff frequency
|
|
113
|
+
:param btype: filter type
|
|
114
|
+
:return: filtered data 1xN.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
output:
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
answer = fc
|
|
121
|
+
Fn = fsamp / 2
|
|
122
|
+
Fnrad = 2 * np.pi * Fn
|
|
123
|
+
Fc = 2 * np.pi * answer
|
|
124
|
+
Wn = [Fc / Fnrad]
|
|
125
|
+
# [b, a] = butter(4, Wn, 'low');
|
|
126
|
+
# local_breast = -(filtfilt(b, a, local_breast_raw(:,:)));
|
|
127
|
+
|
|
128
|
+
[b, a] = signal.butter(N, Wn=Wn, btype=btype, output=output)
|
|
129
|
+
filtered_data = signal.filtfilt(b=b, a=a, x=data)
|
|
130
|
+
|
|
131
|
+
return filtered_data
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
import copy
|
|
3
|
+
from biomechzoo.biomech_ops.normalize_line import normalize_line
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def normalize_data(data, nlength=101):
|
|
7
|
+
"""normalize all channels in the loaded zoo dict to nlen.
|
|
8
|
+
Arguments
|
|
9
|
+
data: dict, loaded zoo file
|
|
10
|
+
nlength: int: new length of data. Default = 101, usually a movement cycle
|
|
11
|
+
Returns:
|
|
12
|
+
None
|
|
13
|
+
Notes:
|
|
14
|
+
-It is often needed to partition data to a single cycle first (see partition_data)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# normalize channel length
|
|
18
|
+
data_new = copy.deepcopy(data)
|
|
19
|
+
for ch_name, ch_data in data_new.items():
|
|
20
|
+
if ch_name != 'zoosystem':
|
|
21
|
+
ch_data_line = ch_data['line']
|
|
22
|
+
# ch_data_event = ch_data['event']
|
|
23
|
+
ch_data_event = ch_data.setdefault('event', {})
|
|
24
|
+
ch_data_normalized = normalize_line(ch_data_line, nlength)
|
|
25
|
+
data_new[ch_name]['line'] = ch_data_normalized
|
|
26
|
+
data_new[ch_name]['event'] = ch_data_event
|
|
27
|
+
warnings.warn('event data have not been normalized')
|
|
28
|
+
|
|
29
|
+
# update zoosystem
|
|
30
|
+
# todo: update all relevant zoosystem meta data related to data lengths
|
|
31
|
+
warnings.warn('zoosystem data have not been fully updated')
|
|
32
|
+
if 'Video' in data['zoosystem']:
|
|
33
|
+
data['zoosystem']['Video']['CURRENT_END_FRAME'] = nlength
|
|
34
|
+
if 'Analog' in data['zoosystem']:
|
|
35
|
+
data['zoosystem']['Analog']['CURRENT_END_FRAME'] = nlength
|
|
36
|
+
|
|
37
|
+
return data_new
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.interpolate import interp1d
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def normalize_line(channel_data, nlength=101):
|
|
6
|
+
"""
|
|
7
|
+
Channel-level: interpolate channel data to target length.
|
|
8
|
+
Assumes channel_data is a 1D or 2D numpy array.
|
|
9
|
+
"""
|
|
10
|
+
original_length = channel_data.shape[0]
|
|
11
|
+
|
|
12
|
+
if original_length == nlength:
|
|
13
|
+
return channel_data
|
|
14
|
+
|
|
15
|
+
x_original = np.linspace(0, 1, original_length)
|
|
16
|
+
x_target = np.linspace(0, 1, nlength)
|
|
17
|
+
|
|
18
|
+
if channel_data.ndim == 1:
|
|
19
|
+
f = interp1d(x_original, channel_data, kind='linear')
|
|
20
|
+
channel_data_norm = f(x_target)
|
|
21
|
+
else:
|
|
22
|
+
channel_data_norm = np.zeros((nlength, channel_data.shape[1]))
|
|
23
|
+
for i in range(channel_data.shape[1]):
|
|
24
|
+
f = interp1d(x_original, channel_data[:, i], kind='linear')
|
|
25
|
+
channel_data_norm[:, i] = f(x_target)
|
|
26
|
+
|
|
27
|
+
return channel_data_norm
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == '__main__':
|
|
33
|
+
# --- 1D TESTS ---
|
|
34
|
+
data_1d = np.array([0, 1, 2, 3, 4])
|
|
35
|
+
print("Original 1D shape:", data_1d.shape)
|
|
36
|
+
|
|
37
|
+
# Case 1: same length
|
|
38
|
+
same = normalize_line(data_1d, nlength=5)
|
|
39
|
+
print("Same length test passed:", np.allclose(same, data_1d))
|
|
40
|
+
|
|
41
|
+
# Case 2: upsample
|
|
42
|
+
upsampled = normalize_line(data_1d, nlength=10)
|
|
43
|
+
print("Upsampled 1D shape:", upsampled.shape)
|
|
44
|
+
print("Upsampled 1D first/last values:", upsampled[0], upsampled[-1])
|
|
45
|
+
|
|
46
|
+
# Case 3: downsample
|
|
47
|
+
downsampled = normalize_line(data_1d, nlength=3)
|
|
48
|
+
print("Downsampled 1D shape:", downsampled.shape)
|
|
49
|
+
print("Downsampled 1D first/last values:", downsampled[0], downsampled[-1])
|
|
50
|
+
|
|
51
|
+
print("\nAll tests completed.")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from biomechzoo.biomech_ops.phase_angle_line import phase_angle_line
|
|
2
|
+
from biomechzoo.processing.addchannel_data import addchannel_data
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def phase_angle_data(data, channels):
|
|
6
|
+
"""Compute phase angle using Hilbert Transform.
|
|
7
|
+
Arguments
|
|
8
|
+
data: dict, zoo data to operate on
|
|
9
|
+
channels, list. Channel names on which to apply calculations
|
|
10
|
+
Returns:
|
|
11
|
+
data: dict, zoo data with calculations appended to new channel(s)
|
|
12
|
+
"""
|
|
13
|
+
data_new = data.copy()
|
|
14
|
+
for ch in channels:
|
|
15
|
+
if ch not in data_new:
|
|
16
|
+
raise ValueError('Channel {} not in data. Available keys: {}'.format(ch, list(data_new.keys())))
|
|
17
|
+
r = data_new[ch]['line']
|
|
18
|
+
phase_angle = phase_angle_line(r)
|
|
19
|
+
ch_new = ch + '_phase_angle'
|
|
20
|
+
data_new = addchannel_data(data_new, ch_new_name=ch_new, ch_new_data=phase_angle)
|
|
21
|
+
return data_new
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == '__main__':
|
|
25
|
+
# -------TESTING--------
|
|
26
|
+
import os
|
|
27
|
+
from biomechzoo.utils.zload import zload
|
|
28
|
+
from biomechzoo.utils.zplot import zplot
|
|
29
|
+
# get path to sample zoo file
|
|
30
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
31
|
+
project_root = os.path.dirname(current_dir)
|
|
32
|
+
fl = os.path.join(project_root, 'data', 'other', 'HC032A18_exploded.zoo')
|
|
33
|
+
|
|
34
|
+
# load zoo file
|
|
35
|
+
data = zload(fl)
|
|
36
|
+
data = data['data']
|
|
37
|
+
data = phase_angle_data(data, channels=['RKneeAngles_x', 'RHipAngles_x'])
|
|
38
|
+
zplot(data, 'RKneeAngles_x_phase_angle')
|
|
39
|
+
|