Skip to content

Integrating MAVIS

Install the base package and call HandTracker.process inside your own image loop. The API owns its model and filters; your application owns capture, display, threads, and scheduling. Use one tracker per sequential source. A tracker is not safe for simultaneous calls from multiple threads.

from mavis_track import HandTracker

with HandTracker(max_hands=2, source_id="camera-a", smoothing=True) as tracker:
    for timestamp, frame_bgr in my_frame_source():
        result = tracker.process(frame_bgr, timestamp=timestamp)
        for hand in result.hands:
            my_application.update(hand.track_id, hand.landmarks)

my_frame_source and my_application belong to your project. A runnable OpenCV example is available in the repository at examples/01_basic_tracking/mavis_webcam.py.

Frame and result contract

Input is a nonempty three-channel uint8 NumPy image. BGR is the default; use color="rgb" for RGB. Input arrays are not modified. Set mirrored=True when the incoming image has been horizontally flipped, so handedness labels account for MediaPipe's mirror convention.

Timestamps are finite, nonnegative, strictly increasing seconds. With no supplied timestamp, MAVIS uses time.perf_counter(). Do not mix clocks within a tracker. FrameResult.timestamp_clock is source for supplied timestamps or monotonic for the default; source clock meaning is the caller's responsibility.

Each result owns its read-only arrays. result.hands is an empty tuple when no hands are detected. Each hand contains:

Field Meaning
track_id Short-lived association within this tracker/source
handedness Left, Right, or Unknown
handedness_score Model classification probability; not landmark accuracy
landmarks, raw_landmarks (21, 3) image-normalized x/y and wrist-relative model z
world_landmarks Optional (21, 3) hand-centered model estimates in meters
angles 14 named geometric flexion angles, degrees; NaN for degenerate geometry
angle_space hand_world_meters or image_scaled

Image x/y use width/height normalization. For image-based angle fallback, x and z are scaled by width and y by height before geometric angles are computed. World estimates are uncalibrated model predictions; separate hands cannot be combined as a global scene.

Association matches handedness and nearby wrists in adjacent frames. It resets after disappearance, a long timestamp gap, or a large jump. Reordering detections preserves IDs in ordinary cases. Crossings and ambiguous same-label detections are not a guarantee of continuous identity.

close() is idempotent. A context manager closes on success or exceptions. Create a new tracker after closing.

Loading recordings

from mavis_track import load_session

session = load_session("recordings/demo")
points = session.landmarks   # (frames, hand_slots, 21, 3)
present = session.valid      # (frames, hand_slots)
timestamps = session.time_vector
image_sizes = session.image_sizes  # Per-frame width/height, None in older files

Hand slots are storage positions; consult track_ids for identity. Missing hands have ID -1 and NaN coordinates. Fractional sampling rates are preserved. Numeric legacy single-hand files are expanded to one slot. Object arrays are rejected; loading never enables pickle. Archives are limited to 256 MiB uncompressed and 256 fields.

UDP and LSL

from handtrack.io.broadcast import UDPBroadcaster

sender = UDPBroadcaster(coordinate_space="image_normalized",
                        timestamp_clock="source")
try:
    sender.send_landmarks(result.frame_id, result.timestamp, [
        {"hand_index": slot, "track_id": hand.track_id,
         "landmarks": hand.landmarks}
        for slot, hand in enumerate(result.hands)
    ])
finally:
    sender.close()

Send every frame, including an empty list when hands disappear. UDP packets include schema version, coordinate space, clock label, and hand count; missing numeric coordinates become JSON null. Configure the receiver to clear absent hands. UDP itself provides no delivery guarantee.

LSL is optional: install mavis-track[stream], or .[stream] for source development. Construct LSLBroadcaster(coordinate_space="image_normalized") for frame API coordinates. Its legacy default is calibrated meters for stereo applications. LSL timestamps must use pylsl.local_clock(); do not pass video-relative, Unix, or arbitrary source timestamps directly. Empty detections produce an all-NaN sample. Fixed hand slots in the legacy LSL stream do not convey track IDs.

Unity compatibility assets

The packaged Left/Right Hand Listener C# files are legacy bone-rotation CSV receivers. They do not consume MAVIS's JSON landmark packets. A new Unity project needs a receiver matching the JSON contract above; copying those legacy scripts alone does not establish that integration.

Compatibility

handtrack.tracker.HandTracker retains its source-owning legacy interface. Prefer mavis_track.HandTracker for new embedding; it avoids capture and GUI ownership and supplies explicit presence, timing, and identity metadata. The desktop face/pose/holistic modes use an additional MediaPipe interface; the initial supported embedding API is hand tracking.