Body Tracking & Movement Metrics (body_tracking_metrics)
A reusable SightLab helper that, with three calls per trial, will:
- Save 6DOF (position + orientation) for body trackers (Vive, Mocopi, etc.) and both avatar
hands into the standardtrial_datafile. - Log derived movement metrics for the trackers, head, and hands to a
separatebody_metricsCSV.
The module lives in the core SightLab location, so it can be imported from any
project:
import sightlab_utils.body_tracking_metrics as btm
What gets recorded
1. trial_data additions (when saveTrialData=True)
| Body | Columns added to trial_data |
|---|---|
| Tracker1…N | Tracker1 x/y/z/yaw/pitch/roll (via SightLab's 6DOF logging) |
| Right hand | RHand X/Y/Z and RHand Yaw/Pitch/Roll |
| Left hand | LHand X/Y/Z and LHand Yaw/Pitch/Roll |
The head is already logged in 6DOF by SightLab, so it is not duplicated here.
2. body_metrics CSV (when bodyMetrics=True)
A separate file is written to a body_metrics subfolder of the experiment
data folder, alongside trial_data. It mirrors SightLab's data-saving mode:
PER_TRIAL→ one file per trial:<date>_<pid>_body_metrics_<trial>.csvCOMBINED→ a single appended file:<date>_<pid>_body_metrics_combined.csv
(includes atrial numbercolumn)
For each body (Tracker1…N, Head, RHand, LHand), the following scalar metrics
are recorded every frame:
| Metric | Unit | Meaning |
|---|---|---|
speed (m/s) |
m/s | Linear speed = distance moved / frame time |
accel (m/s^2) |
m/s² | Change in linear speed / frame time |
ang speed (deg/s) |
deg/s | Angular speed from the orientation change / frame time |
ang accel (deg/s^2) |
deg/s² | Change in angular speed / frame time |
path length (m) |
m | Cumulative distance travelled this trial |
state |
text | Active or Idle (see thresholds below) |
Leading columns: p_id, date/time, trial number, timestamp (secs).
Idle / Active rule: a body is Active when its linear speed ≥
idleThreshold or its angular speed ≥ angularIdleThreshold; otherwise
Idle. This means rotating in place (e.g. mouse-look on the head) still counts
as Active.
Note on the head source: head metrics use
sightlab.getAvatarHead()— the
same avatar head node that feeds thehead x/y/z/...columns intrial_data.
If no avatar head is available, it falls back toviz.MainView.
Quick start: add it to an existing experiment
Inside your viztask-scheduled experiment function, add the highlighted lines.
import sightlab_utils.sightlab as sl
from sightlab_utils.settings import *
import sightlab_utils.body_tracking_metrics as btm # 1. import
sightlab = sl.SightLab(gui=False)
# ... environment setup ...
bodyTracking = btm.BodyTracking(sightlab) # 2. create once
def sightLabExperiment():
yield viztask.waitEvent(EXPERIMENT_START)
for trial in range(sightlab.getTrialCount()):
yield viztask.waitKeyDown(' ')
bodyTracking.addTrialDataColumns() # 3. BEFORE startTrial
yield sightlab.startTrial()
bodyTracking.start() # 4. AFTER trial starts
yield viztask.waitKeyDown(' ')
bodyTracking.stop() # 5. BEFORE endTrial
yield sightlab.endTrial()
viztask.schedule(sightlab.runExperiment)
viztask.schedule(sightLabExperiment)
The three lifecycle calls (order matters)
| Call | When | Why |
|---|---|---|
bodyTracking.addTrialDataColumns() |
Before sightlab.startTrial() |
Tracker columns must be registered before the trial starts, or SightLab won't log them. Also fetches the trackers. |
bodyTracking.start() |
After startTrial() and hand setup |
Begins per-frame hand logging (to trial_data) and metrics logging (to the CSV). Hands must exist first. |
bodyTracking.stop() |
Before sightlab.endTrial() |
Stops the per-frame updates and flushes/closes the metrics file. |
Configuration
All options are constructor arguments and are enabled by sensible defaults.
bodyTracking = btm.BodyTracking(
sightlab,
saveTrialData=True, # save tracker + hand 6DOF into trial_data
bodyMetrics=True, # write the separate body_metrics CSV
idleThreshold=0.05, # linear speed (m/s) at/above which = Active
angularIdleThreshold=5.0, # angular speed (deg/s) at/above which = Active
trackerCount=3, # number of Vive trackers to read
noTrackerConfigs=('Desktop',) # config names that have no trackers
)
| Argument | Default | Description |
|---|---|---|
saveTrialData |
True |
Save tracker + hand 6DOF into the standard trial_data file. |
bodyMetrics |
True |
Write the separate body_metrics CSV with derived metrics. |
idleThreshold |
0.05 |
Linear speed (m/s) at or above which a body is Active. |
angularIdleThreshold |
5.0 |
Angular speed (deg/s) at or above which a body is Active. |
trackerCount |
3 |
How many Vive trackers to read from steamvr.getTrackerList(). |
noTrackerConfigs |
('Desktop',) |
Config names (from sightlab.getConfig()) that have no trackers. |
Common variations
# Only save tracker/hand 6DOF to trial_data (no separate metrics file)
btm.BodyTracking(sightlab, bodyMetrics=False)
# Only write the metrics CSV (don't touch trial_data)
btm.BodyTracking(sightlab, saveTrialData=False)
# Make Idle/Active less sensitive
btm.BodyTracking(sightlab, idleThreshold=0.1, angularIdleThreshold=10)
# GUI projects that also expose a desktop full-body mode
btm.BodyTracking(sightlab, noTrackerConfigs=['Desktop', 'Desktop Full Body'])
How it adapts to the setup
- Desktop / no-tracker configs: if
sightlab.getConfig()is in
noTrackerConfigs, no trackers are fetched and only the head/hands are used.
Check withbodyTracking.hasTrackers()if you need to branch on this. - Missing hands: if an avatar hand isn't set, it is simply skipped (no
columns, no metrics for it). - No data folder: if SightLab isn't writing data files (e.g. logging
disabled), the metrics logger prints a notice and safely does nothing instead
of crashing.
Output locations
data/
└── <date>_<pid>_experiment_data/
├── trial_data/
│ └── <date>_<pid>_trial_data_<trial>.csv ← tracker + hand 6DOF added here
└── body_metrics/
└── <date>_<pid>_body_metrics_<trial>.csv ← derived metrics (PER_TRIAL)
# or <date>_<pid>_body_metrics_combined.csv (COMBINED)
Notes & tips
- Acceleration columns are noisy by nature. Acceleration is the frame-to-frame
change of an already per-frame-differenced velocity, so it amplifies jitter. If
you need smooth accel/angular-accel, apply a short moving average in post. - Discrete inputs read as high speed. Inputs that teleport a body a fixed
distance in one frame (e.g. scroll-wheel hand movement) produce momentarily high
speeds. Real tracked motion is continuous and reads smoother. - First frame of each trial is zero for all rates (no previous sample yet);
path lengthresets to 0 at the start of every trial.
Reference: example scripts
See these scripts in ExampleScripts/ViveTrackers/ for working usage:
ViveTrackers_FullBody.py(non-GUI)ViveTrackers_FullBody_GUI.py(GUI; passes
noTrackerConfigs=['Desktop', 'Desktop Full Body'])