The formula #
RAW_STRAIGHT_ANGLE = 3.10 # ~177.6° — open finger
RAW_CURLED_ANGLE = 1.60 # ~91.7° — fully curled finger
flexion = (straight_limit - avg_angle) / (straight_limit - curled_limit)
flexions[finger_name] = float(np.clip(flexion, 0.0, 1.0))The three pieces #
Denominator — the range. \(3.10 - 1.60 = 1.50\) rad of travel. It sets the scale.
Numerator — the distance travelled away from straight. At \(\bar\theta = 2.35\): \(3.10 - 2.35 = 0.75\) rad.
Division — the percentage. \(0.75 / 1.50 = 0.5\): half the range used.
Why “backwards” #
Textbook min-max normalization is \((x - \min)/(\max - \min)\). This one subtracts from the maximum, because the interior angle (Part 5) runs opposite to the output we want:
| Finger | Interior angle | Wanted flexion |
|---|---|---|
| Open | large (3.10) | 0.0 |
| Closed | small (1.60) | 1.0 |
Check the extremes:
$$\frac{3.10 - 3.10}{1.50} = 0.0 \qquad \frac{3.10 - 2.35}{1.50} = 0.5 \qquad \frac{3.10 - 1.60}{1.50} = 1.0$$Why clip #
Real hands overshoot the window: hyperextended fingers exceed 3.10 rad, a hard fist dips under 1.60.
Unclipped, flexion leaves \([0, 1]\) and the force mapping would command beyond the actuator’s range.
MuJoCo would clamp it anyway (ctrlrange), but clipping at the source keeps the topic honest for
every other subscriber.
A side effect you get for free: the flat regions of the chart are small dead zones at both ends, which hide jitter when the hand is already fully open or closed.
Where 3.10 and 1.60 come from #
They are measured, not derived — read off a live system with one person’s open hand and fist in front of one webcam. They silently absorb:
- that person’s hand anatomy,
- MediaPipe’s model bias (a straight finger rarely reads exactly \(\pi\)),
- the aspect-ratio skew of normalized coordinates (Part 4),
- the orientation the hand is usually held at.
Change any of those and the window drifts. A recorded live session shows exactly how far: that operator’s fist only reached flexion 0.67–0.76 — Part 9.
Normalize here, interpolate there #
Normalization and linear interpolation are inverses, and the pipeline puts one on each side of the network:
| Step | Operation | In → out | Runs in |
|---|---|---|---|
| Vision | normalize | radians → 0..1 | vision_tracker |
| Actuation | lerp | 0..1 → newtons | mujoco_twin |
Sending 0..1 over ROS 2 is what keeps the sides independent: recalibrate vision without touching the simulator, retune the simulator (or swap in real servos) without touching vision. Part 10 is the other half.