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.
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 isNone; for 2D capture,zis0. -
history— a list of priorframedicts withinwindow_secs, oldest → newest, excluding the current frame. -
inputs— operator values such as{"height": 1.80}(the client's height in metres) when known. Useinputs.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.
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.