I’m working on analyzing soccer kicking motion using mocopi BVH data in Blender. My goal is to extract two joint events:
- MHE (Maximum Hip Extension) → the timing when the hip joint is most extended
- MKF (Maximum Knee Flexion) → the timing when the knee joint is most flexed
The problem
I want to always detect MHE as the maximum hip extension angle. However, depending on the trial, my calculation sometimes detects it at the minimum angle instead of the maximum.
My question
Why does MHE sometimes come out as a minimum angle instead of the maximum? Is my trunk reference (torso1–6) the wrong choice? Is there a better way to define the hip angle so that MHE consistently corresponds to the maximum?
Any advice on how to fix this would be greatly appreciated!
Here is the code I’m using right now (simplified but runnable):
import bpy
import math
import numpy as np
# ==========================
# User settings
# ==========================
armature_name = "" # Armature name
frame_start =
frame_end =
# ==========================
# Utility functions
# ==========================
def signed_angle_2d(v1, v2):
"""Signed 2D angle (atan2, deg)"""
dot = np.dot(v1, v2)
det = v1[0]*v2[1] - v1[1]*v2[0]
return math.degrees(math.atan2(det, dot))
def hip_signed_by_body(root, hip, knee, torso_top, torso_bottom):
"""
Hip flexion/extension angle relative to the trunk
(extension = positive, flexion = negative)
Trunk axis is defined by torso_1 → torso_6
"""
v1 = hip - root # pelvis→hip
v2 = knee - hip # hip→knee
# trunk forward vector
forward = torso_top - torso_bottom
if np.linalg.norm(forward) == 0:
forward = np.array([0.0, 0.0, 1.0])
forward = forward / np.linalg.norm(forward)
# right / up vectors
up = np.array([0.0, 1.0, 0.0])
right = np.cross(forward, up)
if np.linalg.norm(right) == 0:
right = np.array([1.0, 0.0, 0.0])
right = right / np.linalg.norm(right)
up2 = np.cross(right, forward)
R = np.vstack([right, up2, forward]).T
# transform to local coordinates
v1_local = R.T @ v1
v2_local = R.T @ v2
# project onto XZ plane
v1_proj = np.array([v1_local[0], v1_local[2]])
v2_proj = np.array([v2_local[0], v2_local[2]])
ang = signed_angle_2d(v1_proj, v2_proj)
# extension is positive
return -ang
def knee_3d_angle(hip, knee, foot):
"""3D knee angle (always positive)"""
v1 = hip - knee
v2 = foot - knee
dot = np.dot(v1, v2)
norm = np.linalg.norm(v1) * np.linalg.norm(v2)
if norm == 0:
return 0.0
return math.degrees(math.acos(np.clip(dot/norm, -1.0, 1.0)))
# ==========================
# Calculate MHE / MKF
# ==========================
def analyze_right_leg():
hip_bone = "r_up_leg"
knee_bone = "r_low_leg"
foot_bone = "r_foot"
root_bone = "root"
torso_top_bone = "torso_1"
torso_bottom_bone = "torso_6"
armature = bpy.data.objects[armature_name]
hip_angles = []
knee_angles = []
for f in range(frame_start, frame_end+1):
bpy.context.scene.frame_set(f)
root = np.array(armature.pose.bones[root_bone].head)
hip = np.array(armature.pose.bones[hip_bone].head)
knee = np.array(armature.pose.bones[knee_bone].head)
foot = np.array(armature.pose.bones[foot_bone].head)
torso1 = np.array(armature.pose.bones[torso_top_bone].head)
torso6 = np.array(armature.pose.bones[torso_bottom_bone].head)
hip_angle = hip_signed_by_body(root, hip, knee, torso1, torso6)
knee_angle = knee_3d_angle(hip, knee, foot)
hip_angles.append((f, hip_angle))
knee_angles.append((f, knee_angle))
# MHE = maximum hip extension (positive = extension)
mhe_frame, mhe_val = max(hip_angles, key=lambda x: x[1])
# MKF = minimum knee angle (maximum flexion)
mkf_frame, mkf_val = min(knee_angles, key=lambda x: x[1])
return (mhe_frame, mhe_val, mkf_frame, mkf_val)
# ==========================
# Run
# ==========================
mhe_f, mhe_v, mkf_f, mkf_v = analyze_right_leg()
print("=== Right leg kick ===")
print(f"MHE (hip_y max, torso1-6 reference): Frame {mhe_f}, Angle {mhe_v:.2f}°")
print(f"MKF (knee 3D min): Frame {mkf_f}, Angle {mkf_v:.2f}°")