wrote a Blender script to auto-detect foot plant frames from mocap velocity, no more hand-marking contacts

406 views 6 replies

Marking foot contacts by hand has been the most tedious part of my mocap cleanup workflow for years. Scrub to where the foot stops, drop a marker, move on, repeat across 40 takes. Did it again last week and finally got annoyed enough to write something.

The approach: sample world-space foot bone positions frame by frame, compute per-frame velocity, flag anything below a threshold as a plant candidate. Very dumb, very effective for the rough pass.

import bpy

def sample_bone_positions(obj, bone_name, frame_start, frame_end):
    scene = bpy.context.scene
    positions = {}
    for f in range(frame_start, frame_end + 1):
        scene.frame_set(f)
        bone = obj.pose.bones.get(bone_name)
        if bone:
            positions[f] = (obj.matrix_world @ bone.matrix).translation.copy()
    return positions

def detect_plant_frames(positions, threshold=0.008):
    frames = sorted(positions.keys())
    plants = []
    for i in range(1, len(frames)):
        delta = (positions[frames[i]] - positions[frames[i-1]]).length
        if delta < threshold:
            plants.append(frames[i])
    return plants

def mark_contacts(bone_names=('foot_L', 'foot_R'), threshold=0.008):
    obj = bpy.context.active_object
    if not obj or obj.type != 'ARMATURE':
        print('Select an armature.')
        return
    scene = bpy.context.scene
    for m in list(scene.timeline_markers):
        if m.name.startswith('PLANT_'):
            scene.timeline_markers.remove(m)
    for bone_name in bone_names:
        positions = sample_bone_positions(obj, bone_name, scene.frame_start, scene.frame_end)
        plants = detect_plant_frames(positions, threshold)
        label = bone_name.replace('foot_', '').upper()
        for f in plants:
            scene.timeline_markers.new(f'PLANT_{label}', frame=f)
        print(f'{bone_name}: {len(plants)} contacts detected')

mark_contacts()

Threshold of 0.008 world units works for Rokoko data at 30fps with 1 unit = 1 meter. You'll need to tune it for your scale and frame rate. At that setting I'm catching roughly 85% of contacts cleanly on the first pass. False positives tend to cluster during slow foot swing where the bone is nearly stopped but not actually planted yet.

What I haven't solved: consecutive plant frames just stack as individual markers, which is still useful but messy. I want to add a merge pass that collapses runs of adjacent frames into a single range marker so you can see "L foot: frames 12–18" as one thing instead of seven identical markers in a pile.

Also thinking about incorporating foot height from the ground plane as a secondary filter. That should help with the slow-swing false positives that raw velocity can't distinguish from actual contacts. Has anyone done something similar? Curious if there's a smarter heuristic than just velocity + height together.

the velocity threshold for "is this foot planted" is way more context-sensitive than it looks. fast action clips and slow walks need different values, and if you're pulling from multiple sessions or multiple actors the baseline noise in the foot tracks can vary a lot. been thinking about per-clip threshold calibration using the minimum velocity percentile of each foot track rather than a global hardcoded value: slow clips don't miss contacts, fast clips don't false-positive on every footfall. have you gone that direction, or are you just exposing it as a parameter and tuning per-clip manually?

Worth adding hysteresis to the threshold check if you haven't already. Checking velocity against a single value frame-by-frame gives you flickering at the boundary. Foot registers as planted for 2 frames, lifting for 1, planted again. Requiring N consecutive frames below threshold before marking contact, and M frames above before releasing it, smooths this out without much extra code. You're basically tracking a frame counter per foot instead of a raw bool. Output is also more stable across clips captured at different framerates.

Replying to SolarRay: The hysteresis framing is right, but I've landed on a slightly different impleme...

The consecutive-frame window also handles something the raw threshold approach doesn't: brief near-zero velocity frames mid-stride as the foot decelerates through a step. A single-value threshold check registers those transient dips as plants. They're short, so they don't visually affect the contact markers much, but if you're using the output to drive constraint weights or bake IK targets downstream, you end up with a one- or two-frame flicker that's genuinely difficult to trace back to its source.

Even a minimum window of 3–4 consecutive frames cleans this up almost entirely. Worth exposing it as a tunable parameter since sprinting needs a tighter window than a walk cycle, the foot is just on the ground for fewer frames at speed.

Worth being explicit about in the velocity calculation: make sure you're using world-space position delta between frames, not local-space transforms. Sounds obvious but local-space velocity on a foot bone gives you wrong readings during any hip-translated move where the foot is tracking correctly relative to the ground but still moving in world space. You'll get false "not planted" readings on otherwise clean data.

Also, if you're working from imported FBX rather than a live capture stream, double-check your frame rate assumption in the delta calculation. If the baked curve is at 120fps but your script treats it as 30fps, your velocity values are off by 4x. Ask me how I know. slow painful realization

Replying to GlitchPulse: Worth adding hysteresis to the threshold check if you haven't already. Checking ...

The hysteresis framing is right, but I've landed on a slightly different implementation that's been easier to tune: instead of tracking a velocity band, I track a minimum consecutive planted-frame count. If foot velocity stays below threshold for fewer than N frames (I use 3 or 4), I don't commit the contact marker and just keep waiting. It handles the same flickering problem without needing to calibrate two separate threshold values, and it's much simpler to explain to someone else on the team when they ask why a specific frame isn't getting flagged.

The real edge case is very fast footwork, quick shuffles where a genuine contact might only last 2 frames. In practice those almost always fall inside a transition and don't need clean markers anyway. Worth validating against your specific clip library before locking in a number though.

Replying to HexRunner: The consecutive-frame window also handles something the raw threshold approach d...

This is exactly what broke my first implementation. Threshold was dialed for walk cycles, looked clean, then I threw a run at it and started getting planted-frame markers mid-stride. The foot decelerates hard right before ground contact and I was flagging that deceleration valley as a plant.

Fixed it by adding a second condition alongside the consecutive-frame count: the foot's vertical position also has to be within some epsilon of its minimum Y across the clip. Small bit of extra setup, but it basically eliminates mid-stride false positives without needing to retune the velocity threshold per clip speed. The two conditions together are way more robust than either one alone.

Moonjump
Forum Search Shader Sandbox
Sign In Register