Skip to content

Body Tracking & Movement Metrics (body_tracking_metrics)

A reusable SightLab helper that, with three calls per trial, will:

  1. Save 6DOF (position + orientation) for body trackers (Vive, Mocopi, etc.) and both avatar
    hands into the standard trial_data file.
  2. Log derived movement metrics for the trackers, head, and hands to a
    separate body_metrics CSV.

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>.csv
  • COMBINED → a single appended file: <date>_<pid>_body_metrics_combined.csv
    (includes a trial number column)

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 the head x/y/z/... columns in trial_data.
If no avatar head is available, it falls back to viz.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 with bodyTracking.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 length resets 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'])