Metric Development

Custom metric scripts

When the supplied angle definitions are not enough, compute a metric in Python. Point PoseLab at a folder in Settings › Metric Scripts and drop *.py files in it. Each file becomes one read-only Custom metric — you can tag it and add ranges, but the file itself is the definition. Edits hot-reload automatically.

Isolation & imports

Each script runs in its own python3 process, so a slow or crashing script can't take down the app, and you may import any module installed in that interpreter. PoseLab bundles no packages — pip install numpy / pandas / scipy into the python3 on your PATH if you want them, and guard optional imports.

The contract

A script must define compute, and should declare a name:

def register():                 # or a bare NAME = "Trunk Lean"
    return {"name": "Trunk Lean", "unit": "°", "window_secs": 5}

def compute(frame, history, inputs, customDeviceData):
    return 12.3                  # a number shown/recorded, or None to skip

Arguments

  • frame — the current pose, {"t": <seconds>, "joints": {name: [x, y, z] or None}}. Joints are keyed by COCO-17 snake_case name. A joint the estimator didn't find is None; for 2D capture, z is 0.
  • history — a list of prior frame dicts within window_secs, oldest → newest, excluding the current frame.
  • inputs — operator values such as {"height": 1.80} (the client's height in metres) when known. Use inputs.get("height") defensively.
  • customDeviceData — the last five seconds of named values from running BLE plugins, grouped as {device_id: {timestamp_ms: {name: value}}}. Keys are strings; use .get(device_id, {}) defensively.

Return a single number (shown live, coloured by your ranges, and recorded into the session metric report) or None to skip the frame.

Windowing is yours

register()'s window_secs sizes the history buffer, and the script does its own aggregation — PoseLab applies no extra Mean/Min/Max on top.

Runtime paths

The host injects four absolute-path variables as module globals so a script can locate app directories — to load reference data, cache, or write per-session output. Reference them directly, no import:

Variable Points at
app_data_dir the app-data root
app_plugins_dir the device-plugin directory
app_metrics_dir the metric-script directory (where this file lives)
session_dir the active session's media dir, or "" if none

session_dir is "" until a capture session is active and updates live, so read it inside compute, not at import time.

The COCO-17 joints

Joint names available on frame["joints"]:

nose        left_eye     right_eye    left_ear     right_ear
left_shoulder  right_shoulder  left_elbow  right_elbow
left_wrist     right_wrist     left_hip    right_hip
left_knee      right_knee      left_ankle  right_ankle

A worked example

Trunk lean angle from vertical, smoothed over the window — abridged from the examples:

import math

def register():
    return {"name": "Trunk Lean", "unit": "°", "window_secs": 1.0}

def _lean(joints):
    ls, rs = joints.get("left_shoulder"), joints.get("right_shoulder")
    lh, rh = joints.get("left_hip"), joints.get("right_hip")
    if not (ls and rs and lh and rh):
        return None
    sx, sy = (ls[0] + rs[0]) / 2, (ls[1] + rs[1]) / 2
    hx, hy = (lh[0] + rh[0]) / 2, (lh[1] + rh[1]) / 2
    # image y grows downward, so "up" is -y
    return math.degrees(math.atan2(sx - hx, -(sy - hy)))

def compute(frame, history, inputs, customDeviceData):
    vals = [v for v in (_lean(f["joints"]) for f in history + [frame]) if v is not None]
    return sum(vals) / len(vals) if vals else None

Testing without a camera

print(), warnings, tracebacks, and the standard logging module appear in the Developer Console on the Metric Scripts page — prefer logging so severity is clear. Test Metrics feeds one deterministic synthetic standing pose to every loaded script, a fast way to validate imports, field access, and return types.