Skip to main content

Lab 03-03: Full Humanoid FK/IK

Objectives​

  • Implement FK for multi-DOF humanoid limbs
  • Apply IK to position humanoid hands and feet
  • Handle kinematic chains with multiple joints
  • Create coordinated multi-limb poses

Materials​

TypeNameVersionTier
softwarePython3.10+required
softwareMuJoCo3.0+required
softwareNumPy1.24+required
softwareSciPy1.10+required

Background​

Humanoid robots have complex kinematic structures with multiple branching chains. This lab applies FK/IK concepts to realistic humanoid limbs.

Instructions​

Step 1: Load and Analyze the Humanoid​

import mujoco
import numpy as np
from scipy.optimize import minimize

# Load humanoid model
model = mujoco.MjModel.from_xml_path(
mujoco.util.get_resource_path("humanoid/humanoid.xml")
)
data = mujoco.MjData(model)

# Analyze kinematic structure
print("Humanoid Kinematic Structure")
print("=" * 50)
print(f"Bodies: {model.nbody}")
print(f"Joints: {model.njnt}")
print(f"DOF: {model.nv}")

# List all bodies
print("\nBody hierarchy:")
for i in range(model.nbody):
name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_BODY, i)
parent_id = model.body_parentid[i]
```python
```python
parent_name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_BODY, parent_id) if i > 0 else "world"
    print(f"  {i}: {name} (parent: {parent_name})")

# Identify end-effectors
end_effectors = ['right_hand', 'left_hand', 'right_foot', 'left_foot']
for ee in end_effectors:
body_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, ee)
```python
```python
if body_id >= 0:
        print(f"\n{ee} (body {body_id}):")
# Find joints in chain from root to this body
chain = []
current = body_id
```python
```python
while current > 0:
            for j in range(model.njnt):
```python
```python
if model.jnt_bodyid[j] == current:
                    jnt_name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_JOINT, j)
chain.append(jnt_name)
current = model.body_parentid[current]
print(f" Joint chain: {list(reversed(chain))}")

Understanding: Note the kinematic chains for each limb. Right arm chain typically: right_shoulder1 → right_shoulder2 → right_elbow

Step 2: Implement FK for a Limb​

def get_body_position(model, data, body_name):
"""Get world position of a body."""
body_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, body_name)
return data.xpos[body_id].copy()

def get_body_orientation(model, data, body_name):
"""Get world orientation (rotation matrix) of a body."""
body_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, body_name)
return data.xmat[body_id].reshape(3, 3).copy()

def compute_limb_fk(model, data, joint_angles, joint_names, end_effector):
"""
Compute FK for a limb by setting joint angles.

Args:
model, data: MuJoCo model and data
joint_angles: Dict mapping joint names to angles
joint_names: List of joint names in the limb
end_effector: Name of end-effector body

Returns:
End-effector position and orientation
"""
# Reset
mujoco.mj_resetData(model, data)

# Set joint angles
for jname, angle in joint_angles.items():
jnt_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, jname)
```python
```python
if jnt_id >= 0:
            qpos_addr = model.jnt_qposadr[jnt_id]
data.qpos[qpos_addr] = angle

# Forward kinematics
mujoco.mj_forward(model, data)

pos = get_body_position(model, data, end_effector)
ori = get_body_orientation(model, data, end_effector)

return pos, ori

# Test FK for right arm
right_arm_joints = ['right_shoulder1', 'right_shoulder2', 'right_elbow']

# Set some joint angles
test_angles = {
'right_shoulder1': np.radians(45),
'right_shoulder2': np.radians(-30),
'right_elbow': np.radians(60),
}

pos, ori = compute_limb_fk(model, data, test_angles, right_arm_joints, 'right_hand')

print(f"Right hand FK result:")
print(f" Position: {pos}")
print(f" Orientation:\n{ori}")

Verification: Change joint angles and observe how hand position changes.

Step 3: Implement Numerical IK for Humanoid​

def ik_cost_function(joint_values, model, data, joint_names, target_pos,
target_body, weight_pos=1.0, weight_ori=0.0, target_ori=None):
"""
Cost function for IK optimization.

Args:
joint_values: Array of joint angles to optimize
target_pos: Desired end-effector position
target_body: Name of end-effector body
weight_pos, weight_ori: Weights for position and orientation error
target_ori: Optional target orientation (3x3 matrix)
"""
# Set joint values
mujoco.mj_resetData(model, data)

for i, jname in enumerate(joint_names):
jnt_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, jname)
```python
```python
if jnt_id >= 0:
            qpos_addr = model.jnt_qposadr[jnt_id]
data.qpos[qpos_addr] = joint_values[i]

mujoco.mj_forward(model, data)

# Position error
current_pos = get_body_position(model, data, target_body)
pos_error = np.sum((current_pos - target_pos)**2)

# Orientation error (optional)
ori_error = 0
```python
```python
if target_ori is not None and weight_ori > 0:
        current_ori = get_body_orientation(model, data, target_body)
ori_error = np.sum((current_ori - target_ori)**2)

return weight_pos * pos_error + weight_ori * ori_error

def solve_limb_ik(model, data, target_pos, joint_names, end_effector,
initial_guess=None, bounds=None):
"""
Solve IK for a limb using numerical optimization.

Returns:
Dictionary of joint names to angles, or None if failed
"""
n_joints = len(joint_names)

if initial_guess is None:
initial_guess = np.zeros(n_joints)

if bounds is None:
# Default bounds: ±180 degrees
bounds = [(-np.pi, np.pi)] * n_joints

# Optimize
result = minimize(
ik_cost_function,
initial_guess,
args=(model, data, joint_names, target_pos, end_effector),
method='L-BFGS-B',
bounds=bounds,
options={'maxiter': 200, 'ftol': 1e-8}
)

```python
```python
if result.fun < 0.001: # Converged close enough
        return {jname: angle for jname, angle in zip(joint_names, result.x)}
else:
print(f"IK failed to converge. Final error: {result.fun:.6f}")
return None

# Test IK: Move right hand to specific position
target_position = np.array([0.3, -0.3, 1.2])

solution = solve_limb_ik(
model, data,
target_pos=target_position,
joint_names=right_arm_joints,
end_effector='right_hand'
)

if solution:
print(f"\nIK Solution for target {target_position}:")
for jname, angle in solution.items():
print(f" {jname}: {np.degrees(angle):.1f}°")

# Verify
pos, _ = compute_limb_fk(model, data, solution, right_arm_joints, 'right_hand')
error = np.linalg.norm(pos - target_position)
print(f"\nVerification:")
print(f" Achieved position: {pos}")
print(f" Position error: {error:.6f} m")

Verification: IK solution should place the hand close to target position.

Step 4: Multi-Limb Coordination​

def create_coordinated_pose(model, data, pose_spec):
"""
Create a coordinated pose with multiple limb targets.

Args:
pose_spec: Dictionary with structure:
{
'right_hand': {'pos': [x,y,z], 'joints': [...]},
'left_hand': {'pos': [x,y,z], 'joints': [...]},
...
}

Returns:
Dictionary of all joint angles
"""
all_joints = {}

for limb_name, spec in pose_spec.items():
if 'pos' in spec and 'joints' in spec:
solution = solve_limb_ik(
model, data,
target_pos=np.array(spec['pos']),
joint_names=spec['joints'],
end_effector=limb_name
)
if solution:
all_joints.update(solution)
else:
print(f"Warning: Could not solve IK for {limb_name}")

return all_joints

# Define a reaching pose
reaching_pose = {
'right_hand': {
'pos': [0.4, -0.2, 1.3],
'joints': ['right_shoulder1', 'right_shoulder2', 'right_elbow']
},
'left_hand': {
'pos': [0.2, 0.3, 1.0],
'joints': ['left_shoulder1', 'left_shoulder2', 'left_elbow']
}
}

print("Solving coordinated reaching pose...")
pose_joints = create_coordinated_pose(model, data, reaching_pose)

if pose_joints:
print("\nCoordinated pose joint angles:")
for jname, angle in sorted(pose_joints.items()):
print(f" {jname}: {np.degrees(angle):.1f}°")

Challenge: Try creating different coordinated poses (arms up, reaching forward, etc.)

Step 5: Visualize the Pose​

def apply_pose(model, data, joint_angles):
"""Apply joint angles to the model."""
mujoco.mj_resetData(model, data)

for jname, angle in joint_angles.items():
jnt_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, jname)
```python
```python
if jnt_id >= 0:
            qpos_addr = model.jnt_qposadr[jnt_id]
data.qpos[qpos_addr] = angle

# Lift humanoid off ground
data.qpos[2] = 1.4 # Z position

mujoco.mj_forward(model, data)

# Apply the coordinated pose and visualize
if pose_joints:
apply_pose(model, data, pose_joints)

# Record end-effector positions
print("\nEnd-effector positions after applying pose:")
for ee in ['right_hand', 'left_hand']:
pos = get_body_position(model, data, ee)
print(f" {ee}: {pos}")

# Launch viewer (if available)
try:
print("\nLaunching viewer - close window when done...")
with mujoco.viewer.launch_passive(model, data) as viewer:
while viewer.is_running():
mujoco.mj_step(model, data)
viewer.sync()
except Exception as e:
print(f"Viewer not available: {e}")
print("Pose applied successfully (run in environment with display for visualization)")

Step 6: Animate a Trajectory​

def interpolate_poses(pose1, pose2, num_steps=50):
"""Linearly interpolate between two poses."""
poses = []
for t in np.linspace(0, 1, num_steps):
interp_pose = {}
all_joints = set(pose1.keys()) | set(pose2.keys())
for jname in all_joints:
angle1 = pose1.get(jname, 0)
angle2 = pose2.get(jname, 0)
interp_pose[jname] = angle1 + t * (angle2 - angle1)
poses.append(interp_pose)
return poses

# Create start and end poses
start_pose = create_coordinated_pose(model, data, {
'right_hand': {
'pos': [0.3, -0.4, 0.8],
'joints': right_arm_joints
}
})

end_pose = create_coordinated_pose(model, data, {
'right_hand': {
'pos': [0.5, -0.2, 1.4],
'joints': right_arm_joints
}
})

if start_pose and end_pose:
# Generate trajectory
trajectory = interpolate_poses(start_pose, end_pose, num_steps=100)

print(f"Generated trajectory with {len(trajectory)} waypoints")

# Record hand positions along trajectory
hand_positions = []
for pose in trajectory:
apply_pose(model, data, pose)
pos = get_body_position(model, data, 'right_hand')
hand_positions.append(pos.copy())

hand_positions = np.array(hand_positions)

# Plot trajectory
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')

ax.plot(hand_positions[:, 0], hand_positions[:, 1], hand_positions[:, 2],
'b-', linewidth=2, label='Hand trajectory')
ax.scatter(*hand_positions[0], c='g', s=100, marker='o', label='Start')
ax.scatter(*hand_positions[-1], c='r', s=100, marker='^', label='End')

ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
ax.set_title('Right Hand Trajectory (Joint Space Interpolation)')
ax.legend()

plt.savefig('hand_trajectory.png', dpi=150)
plt.show()

Observation: Joint-space interpolation produces smooth joint motion but may not produce straight-line Cartesian motion.

Expected Outcomes​

  1. Understanding of humanoid kinematic structure
  2. Working FK for limb chains
  3. Numerical IK for positioning end-effectors
  4. Multi-limb coordinated poses
  5. Animated trajectories with visualization

Rubric​

CriterionPointsDescription
Kinematic Analysis15Correct identification of joint chains
FK Implementation20Accurate forward kinematics
IK Solver25Working numerical IK with good convergence
Multi-Limb Coordination20Successful coordinated poses
Trajectory Generation20Smooth animated movement

Total: 100 points

Extensions​

  1. Cartesian-Space Interpolation: Implement straight-line motion in Cartesian space
  2. Full-Body IK: Include torso and legs for whole-body reaching
  3. Obstacle Avoidance: Add constraints to avoid self-collision
  4. Real-Time IK: Optimize for speed to enable interactive control