Working with optical mocap data in Blender, I kept running into the same problem: somewhere in a 200-frame take, one or two frames where a marker occlusion or ID swap caused a bone to snap to a completely wrong position. One bad frame in 200 is easy to miss on a visual pass, especially mid-session when you're trying to process 20+ takes.
The usual approach is scrubbing through the timeline or eyeballing the fcurve editor. Both are slow and inconsistent. So I wrote a script that checks every rotation and location fcurve in the active action and flags any frame-to-frame delta above a threshold:
import bpy
import math
THRESHOLD_DEG = 25.0 # rotation jump threshold
THRESHOLD_LOC = 0.3 # location jump threshold (Blender units)
def detect_curve_jumps(action):
threshold_rot = math.radians(THRESHOLD_DEG)
violations = []
for fcurve in action.fcurves:
dp = fcurve.data_path
is_rot = 'rotation_euler' in dp
is_loc = 'location' in dp
if not (is_rot or is_loc):
continue
label = dp
if 'pose.bones' in dp:
try:
label = dp.split('"')[1]
except IndexError:
pass
threshold = threshold_rot if is_rot else THRESHOLD_LOC
kps = fcurve.keyframe_points
for i in range(1, len(kps)):
delta = abs(kps[i].co[1] - kps[i - 1].co[1])
if delta > threshold:
frame = int(kps[i].co[0])
deg = round(math.degrees(delta), 2) if is_rot else None
violations.append((frame, label, fcurve.array_index, deg, round(delta, 4)))
return sorted(violations, key=lambda v: v[0])
obj = bpy.context.object
if obj and obj.animation_data and obj.animation_data.action:
results = detect_curve_jumps(obj.animation_data.action)
print(f"Scan complete — {len(results)} jump(s) flagged")
for frame, bone, axis, deg, raw in results:
val = f"{deg}°" if deg is not None else f"delta {raw}"
print(f" f{frame:5d} {bone:25s} axis[{axis}] {val}")
else:
print("Select an armature with an active action first.")
Thresholds need tuning per project. 25° rotation and 0.3 units location works for my capture volume, but anything involving fast motion (slaps, kicks) will generate false positives. It also only catches single-frame spikes, not gradual drift over 10–20 frames. And note that it targets rotation_euler only, so if your rig bakes to quaternions you'd need to adapt it.
The part I'm still unsure about: reporting. Console output feels throwaway when you're mid-cleanup and want to jump to specific frames. I'm thinking about dumping flagged frames into a text block or using fcurve.color to visually tag suspect curves in the editor, but that might be too heavy for a quick scan tool. Curious what people actually want out of something like this.