WCB26 / utils.py
tian-tan's picture
Initial commit
0b3b5de
Raw
History Blame Contribute Delete
3.18 kB
"""Shared helpers (no app/viewer imports → safe for both app.py and viewer/builder.py to import).
One source of truth for the things the 2D gait-cycle plots (app.py) and the 3D skeleton viewer
(viewer/builder.py) must agree on: left heel-strike detection, .mot file reading, and the
low/high-cadence condition colors.
"""
import io
import numpy as np
import pandas as pd
WCB_GC_FRAMES = 101 # 0-100% gait cycle, inclusive
WCB_GC_DURATION = 1.0 # seconds of animation per gait cycle (shared by all skeletons)
WCB_GC_VGRF_PEAK_FRAC = 0.05 # heel-strike threshold = max(FLOOR, frac * peak vGRF)
WCB_GC_VGRF_FLOOR = 20.0 # N; lower bound on the heel-strike threshold
WCB_GC_MIN_DURATION = 0.4 # s; refractory guard rejecting sub-stride crossings
def heel_strike_threshold(vgrf):
"""Stance-level rising-edge threshold: ~5% of the peak vGRF, floored at 20 N. Not ~0 N,
because the generated/resampled vGRF has small swing-phase bumps a 1 N threshold would
mistake for heel strikes (truncating the cycle to a sub-stride blip)."""
return max(WCB_GC_VGRF_FLOOR, WCB_GC_VGRF_PEAK_FRAC * float(np.nanmax(vgrf)))
def first_gait_cycle_indices(vgrf, time=None, dt=None, min_duration=WCB_GC_MIN_DURATION):
"""First left heel-strike-to-heel-strike index pair (i0, i1) spanning >= min_duration
seconds, or None. Heel strikes are rising-edge crossings of heel_strike_threshold(); the
min_duration guard skips sub-stride crossings. Pass either per-sample `time` or uniform `dt`."""
thd = heel_strike_threshold(vgrf)
hs = np.where((vgrf[:-1] < thd) & (vgrf[1:] >= thd))[0]
for k in range(len(hs) - 1):
a, b = hs[k], hs[k + 1]
gap = (time[b] - time[a]) if time is not None else (b - a) * dt
if gap >= min_duration:
return a, b
return None
def left_vy_column(cols):
"""Left-foot (calcn_l) vertical-GRF column name in a .mot column list, or None.
'force_l_vy' (generated, convertDfToGRFMot) or 'ground_force_2_vy' (experimental)."""
for c in cols:
if c.endswith('_l_vy') or 'ground_force_2' in c:
return c
return None
def read_mot(path):
"""Read a .mot file -> (header lines before the column row, DataFrame). Returns (None, None)
if path is None or no 'time' column row is found. Drops pandas 'Unnamed' overflow columns."""
if path is None:
return None, None
with open(path) as f:
lines = f.readlines()
hdr_idx = next((i for i, l in enumerate(lines) if l.strip().startswith('time')), None)
if hdr_idx is None:
return None, None
meta = lines[:hdr_idx]
data = ''.join(lines[hdr_idx:])
try:
df = pd.read_csv(io.StringIO(data), sep='\t')
except Exception:
df = pd.read_csv(io.StringIO(data), delim_whitespace=True) # whitespace-delimited fallback
df = df.loc[:, ~df.columns.str.contains('^Unnamed')]
return meta, df
# Condition colors shared verbatim by the 2D plots, the 3D overlay, and the HTML legend.
WCB_COLOR_LO = ('steelblue', [0.27, 0.51, 0.71]) # (matplotlib/CSS name, glTF RGB 0-1)
WCB_COLOR_HI = ('tomato', [1.00, 0.39, 0.28])