Pyomeca is a python library allowing you to carry out a complete biomechanical analysis; in a simple, logical and concise way.
See Pyomeca's documentation site.
Here is an example of a complete EMG pipeline in just one command:
from pyomeca import Analogs3d
emg = (
Analogs3d.from_c3d("your_c3d.c3d", names=['anterior_deltoid', 'biceps'])
.band_pass(freq=2000, order=4, cutoff=[10, 425])
.center()
.rectify()
.low_pass(freq=2000, order=4, cutoff=5)
.normalization()
.time_normalize()
)
Object-oriented architecture where each class is associated with common and specialized functionalities:
Specialized functionalities include signal processing routine commonly used in biomechanics: filters, normalization, onset detection, outliers detection, derivative, etc.
Each functionality can be chained. In addition to making it easier to write and read code, it allows you to add and remove analysis steps easily (such as Lego blocks).
Each class inherits from a numpy array, so you can create your own analysis step easily.
Easy reading and writing interface to common files in biomechanics (c3d, csv, mat, sto, trc, mot, xlsx)
Common linear algebra routine implemented: get Euler angles to/from roto-translation matrix, create a system of axes, set a rotation or translation, transpose or inverse, etc.
First, install miniconda or anaconda. Then type:
conda install pyomeca -c conda-forge
Pyomeca is designed to work well with other libraries that we have developed:
Pyomeca is Apache-licensed and the source code is available on GitHub. If any questions or issues come up as you use pyomeca, please get in touch via GitHub issues. We welcome any input, feedback, bug reports, and contributions.
Type | Reading example | Writing example | Class | Description |
---|---|---|---|---|
c3d |
Markers3d.from_c3d() |
Markers3d and Analogs3d |
C3d file | |
csv |
Markers3d.from_csv() |
Markers3d.to_csv() |
Markers3d and Analogs3d |
Csv file |
excel |
Markers3d.from_excel() |
Markers3d and Analogs3d |
Excel file | |
sto |
Analogs3d.from_sto() |
Analogs3dOsim.to_sto() (pyosim needed) |
Analogs3d |
Analogs file used in Opensim |
mot |
Analogs3d.from_mot() |
Analogs3dOsim.to_mot() (pyosim needed) |
Analogs3d |
Joint angles file used in Opensim |
trc |
Markers3d.from_trc() |
Markers3dOsim.to_trc() (pyosim needed) |
Markers3d |
Markers positions file used in OpenSim |
from pathlib import Path
from pyomeca import Markers3d, Analogs3d
from utils import describe_data
%load_ext lab_black
data_path = Path("..") / "data" / "markers_analogs.c3d"
analogs = Analogs3d.from_c3d(data_path)
describe_data(analogs)
Shape: (1, 38, 11600)
Rate: 2000.0
Labels:
['Voltage.1', 'Voltage.2', 'Voltage.3', 'Voltage.4', 'Voltage.5', 'Voltage.6', 'Delt_ant.EMG1', 'Infra.EMG10', 'Subscap.EMG11', '12.EMG12', '13.EMG13', '14.EMG14', '15.EMG15', '16.EMG16', 'Delt_med.EMG2', 'Delt_post.EMG3', 'Biceps.EMG4', 'Triceps.EMG5', 'Trap_sup.EMG6', 'Trap_inf.EMG7', 'Gd_dent.EMG8', 'Supra.EMG9', 'EMG1', 'EMG10', 'EMG11', 'EMG12', 'EMG13', 'EMG14', 'EMG15', 'EMG16', 'EMG2', 'EMG3', 'EMG4', 'EMG5', 'EMG6', 'EMG7', 'EMG8', 'EMG9']
Time Frames:
[0.0000e+00 5.0000e-04 1.0000e-03 ... 5.7890e+00 5.7895e+00 5.7900e+00]
First Frame:
[[-2.20515728e-02 -1.03979707e-02 -1.54441223e-02 -3.72455120e-02 7.31565058e-03 1.04316175e-02 -2.60891229e-05 2.23239495e-05 2.87022194e-05 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 -4.02107289e-05 -1.36110339e-05 9.72763337e-06 3.79955077e-06 4.21132700e-06 -9.91835077e-06 -2.29653051e-06 2.97530321e-04 0.00000000e+00 0.00000000e+00 0.00000000e+00 -1.78752043e-06 3.85072917e-06 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00]]
markers = Markers3d.from_c3d(
data_path, prefix=":", names=["EPICm", "LARMm", "LARMl", "LARM_elb"]
)
describe_data(markers)
Shape: (4, 4, 580)
Rate: 100.0
Labels:
['EPICm', 'LARMm', 'LARMl', 'LARM_elb']
Time Frames:
[0. 0.01 0.02 0.03 0.04 0.05 0.06 0.07 0.08 0.09 0.1 0.11 0.12 0.13 0.14 0.15 0.16 0.17 0.18 0.19 0.2 0.21 0.22 0.23 0.24 0.25 0.26 0.27 0.28 0.29 0.3 0.31 0.32 0.33 0.34 0.35 0.36 0.37 0.38 0.39 0.4 0.41 0.42 0.43 0.44 0.45 0.46 0.47 0.48 0.49 0.5 0.51 0.52 0.53 0.54 0.55 0.56 0.57 0.58 0.59 0.6 0.61 0.62 0.63 0.64 0.65 0.66 0.67 0.68 0.69 0.7 0.71 0.72 0.73 0.74 0.75 0.76 0.77 0.78 0.79 0.8 0.81 0.82 0.83 0.84 0.85 0.86 0.87 0.88 0.89 0.9 0.91 0.92 0.93 0.94 0.95 0.96 0.97 0.98 0.99 1. 1.01 1.02 1.03 1.04 1.05 1.06 1.07 1.08 1.09 1.1 1.11 1.12 1.13 1.14 1.15 1.16 1.17 1.18 1.19 1.2 1.21 1.22 1.23 1.24 1.25 1.26 1.27 1.28 1.29 1.3 1.31 1.32 1.33 1.34 1.35 1.36 1.37 1.38 1.39 1.4 1.41 1.42 1.43 1.44 1.45 1.46 1.47 1.48 1.49 1.5 1.51 1.52 1.53 1.54 1.55 1.56 1.57 1.58 1.59 1.6 1.61 1.62 1.63 1.64 1.65 1.66 1.67 1.68 1.69 1.7 1.71 1.72 1.73 1.74 1.75 1.76 1.77 1.78 1.79 1.8 1.81 1.82 1.83 1.84 1.85 1.86 1.87 1.88 1.89 1.9 1.91 1.92 1.93 1.94 1.95 1.96 1.97 1.98 1.99 2. 2.01 2.02 2.03 2.04 2.05 2.06 2.07 2.08 2.09 2.1 2.11 2.12 2.13 2.14 2.15 2.16 2.17 2.18 2.19 2.2 2.21 2.22 2.23 2.24 2.25 2.26 2.27 2.28 2.29 2.3 2.31 2.32 2.33 2.34 2.35 2.36 2.37 2.38 2.39 2.4 2.41 2.42 2.43 2.44 2.45 2.46 2.47 2.48 2.49 2.5 2.51 2.52 2.53 2.54 2.55 2.56 2.57 2.58 2.59 2.6 2.61 2.62 2.63 2.64 2.65 2.66 2.67 2.68 2.69 2.7 2.71 2.72 2.73 2.74 2.75 2.76 2.77 2.78 2.79 2.8 2.81 2.82 2.83 2.84 2.85 2.86 2.87 2.88 2.89 2.9 2.91 2.92 2.93 2.94 2.95 2.96 2.97 2.98 2.99 3. 3.01 3.02 3.03 3.04 3.05 3.06 3.07 3.08 3.09 3.1 3.11 3.12 3.13 3.14 3.15 3.16 3.17 3.18 3.19 3.2 3.21 3.22 3.23 3.24 3.25 3.26 3.27 3.28 3.29 3.3 3.31 3.32 3.33 3.34 3.35 3.36 3.37 3.38 3.39 3.4 3.41 3.42 3.43 3.44 3.45 3.46 3.47 3.48 3.49 3.5 3.51 3.52 3.53 3.54 3.55 3.56 3.57 3.58 3.59 3.6 3.61 3.62 3.63 3.64 3.65 3.66 3.67 3.68 3.69 3.7 3.71 3.72 3.73 3.74 3.75 3.76 3.77 3.78 3.79 3.8 3.81 3.82 3.83 3.84 3.85 3.86 3.87 3.88 3.89 3.9 3.91 3.92 3.93 3.94 3.95 3.96 3.97 3.98 3.99 4. 4.01 4.02 4.03 4.04 4.05 4.06 4.07 4.08 4.09 4.1 4.11 4.12 4.13 4.14 4.15 4.16 4.17 4.18 4.19 4.2 4.21 4.22 4.23 4.24 4.25 4.26 4.27 4.28 4.29 4.3 4.31 4.32 4.33 4.34 4.35 4.36 4.37 4.38 4.39 4.4 4.41 4.42 4.43 4.44 4.45 4.46 4.47 4.48 4.49 4.5 4.51 4.52 4.53 4.54 4.55 4.56 4.57 4.58 4.59 4.6 4.61 4.62 4.63 4.64 4.65 4.66 4.67 4.68 4.69 4.7 4.71 4.72 4.73 4.74 4.75 4.76 4.77 4.78 4.79 4.8 4.81 4.82 4.83 4.84 4.85 4.86 4.87 4.88 4.89 4.9 4.91 4.92 4.93 4.94 4.95 4.96 4.97 4.98 4.99 5. 5.01 5.02 5.03 5.04 5.05 5.06 5.07 5.08 5.09 5.1 5.11 5.12 5.13 5.14 5.15 5.16 5.17 5.18 5.19 5.2 5.21 5.22 5.23 5.24 5.25 5.26 5.27 5.28 5.29 5.3 5.31 5.32 5.33 5.34 5.35 5.36 5.37 5.38 5.39 5.4 5.41 5.42 5.43 5.44 5.45 5.46 5.47 5.48 5.49 5.5 5.51 5.52 5.53 5.54 5.55 5.56 5.57 5.58 5.59 5.6 5.61 5.62 5.63 5.64 5.65 5.66 5.67 5.68 5.69 5.7 5.71 5.72 5.73 5.74 5.75 5.76 5.77 5.78 5.79]
First Frame:
[[662.2623291 641.40118408 582.25305176 678.54089355] [450.61416626 564.45166016 524.54125977 500.98504639] [321.36239624 281.25653076 271.89208984 304.51834106] [ 1. 1. 1. 1. ]]
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style="ticks", context="talk")
raw = analogs["Delt_ant.EMG1"].abs()
def create_plots(data, labels):
_, ax = plt.subplots(figsize=(12, 6))
for datum, label in zip(data, labels):
datum.plot(label=label, lw=3, ax=ax)
plt.legend()
sns.despine()
moving_average = raw.moving_average(window_size=100)
create_plots(data=[raw, moving_average], labels=["raw", "moving average"])
From the raw
array:
Render the same plot using the moving_median
and moving_rms
methods
Plot the three kind of smoothing methods together
import numpy as np
# fake data
freq = 100
time = np.arange(0, 1, 0.01)
w = 2 * np.pi * 1
y = np.sin(w * time) + 0.1 * np.sin(10 * w * time)
y = Analogs3d(y.reshape(1, 1, -1))
low_pass = y.low_pass(freq=freq, order=2, cutoff=5)
create_plots(data=[y, low_pass], labels=["raw", "low-pass @ 5Hz"])
From the raw
array:
band_pass
(4th order with 10-200Hz cutoff), band_stop
(2nd order with 40-60Hz cutoff) and high_pass
(2nd order with 100Hz cutoff) methodsamp, freqs = y.fft(freq=freq)
amp_filtered, freqs_filtered = low_pass.fft(freq=freq)
_, ax = plt.subplots(2, 1, figsize=(12, 6))
y.plot("k-", ax=ax[0], label="raw")
low_pass.plot("r-", ax=ax[0], label="low-pass @ 5Hz")
ax[0].set_title("Temporal domain")
ax[1].plot(freqs, amp.squeeze(), "k-", label="raw")
ax[1].plot(freqs_filtered, amp_filtered.squeeze(), "r-", label="low-pass @ 5Hz")
ax[1].set_title("Frequency domain")
ax[1].legend()
plt.tight_layout()
sns.despine()
# if `ref` is not specified (MVC), take normalize with signal max
raw.rectify().normalization().plot()
sns.despine()
raw.normalization??
Signature: raw.normalization(ref=None, scale=100) Source: def normalization(self, ref=None, scale=100): """ Normalize a signal against `ref` (x's max if empty) on a scale of `scale` Parameters ---------- ref : np.ndarray reference value scale Scale on which to express x (100 by default) Returns ------- FrameDependentNpArray """ if not np.any(ref): ref = np.nanmax(self, axis=-1) # add one dimension ref = np.expand_dims(ref, axis=-1) return self / (ref / scale) File: ~/miniconda3/envs/tutorials/lib/python3.8/site-packages/pyomeca/frame_dependent.py Type: method
raw.moving_rms(100).time_normalization().plot()
sns.despine()
# insert some 0 in the signal
signal = moving_average.copy()
signal[..., 6000:6500] = 0
mu = signal[..., : int(signal.get_rate)].mean()
onset = signal.detect_onset(
threshold=mu + mu * 0.1, # mean of the first second + 10%
above=int(signal.get_rate) / 2, # we want at least 1/2 second above the threshold
below=int(signal.get_rate) / 2, # we accept point below threshold for 1/2 second
)
onset
array([[1429, 8593]])
_, ax = plt.subplots(figsize=(12, 6))
signal.plot(ax=ax)
for (inf, sup) in onset:
ax.axvline(x=inf, color="g")
ax.axvline(x=sup, color="r")
sns.despine()
Apply the following pipeline on the raw
channel in the analogs
array:
Then, plot the result