From raw webcam pixels to a robot arm you control with your hand.
Jim Seelan · 3 hours · 6 modules · 1 closed loop
?view=scroll to the URL?print-pdf, then File → Print → Save as PDF| Part | Content | When to read |
|---|---|---|
| 1 — Concept foundations | 11 concepts with plain-language explanations, analogies, references, self-check | Before or after the session |
| 2 — The lab | Every module: scripts, exercises, diagrams, speaker notes | During and after |
| 3 — After today | 9 capstone projects across laptop and hardware tiers | After the session |
| 4 — Quick reference | Key numbers, all demo scripts, common issues, fallback behaviours | Keep open during the session |
Eleven concepts you need to understand the lab. Each has a plain-language explanation, an analogy, and links to go deeper. During a live session, skip to Part 2. On your own, read these first.
You give it an image, it returns a label. The world does not change because of what it said.
Examples: image classifier, chatbot, recommendation engine.
The agent acts → the world changes → those changes become the next input. A continuous conversation with the physical world.
Examples: robot arm, self-driving car, drone.
Reading a book is static — the book doesn't change based on what you understood. Having a conversation is dynamic — what you say changes what the other person says next. Physical AI systems are always in a conversation with the physical world.
cap.read() # returns numpy array, shape (480, 640, 3)
# 480 rows × 640 columns × 3 (B, G, R values)
# = 921,600 numbers per frame
A robot cannot act on 921,600 numbers directly. Too much, most of it irrelevant. The perception layer compresses this down to 3 or 4 joint angle values.
Camera frame (480×640×3 ints)
→ MediaPipe model
→ 21 landmark (x, y, z) coordinates
→ dot-product angle formula
→ 3–4 joint angles in degrees
→ numpy array shape (4,) float32 ← the state vector
Training from scratch: millions of images, weeks of GPU, deep CV expertise.
We use Google's model (hand_landmarker.task — 7.5 MB) and run inference on it.
The model was trained elsewhere. We just use it.
Landmark at x=0.5, y=0.3 means: halfway across the frame, 30% down from top. Always in [0.0, 1.0] regardless of camera resolution. Code is resolution-independent.
detect_async() → model calls your on_result callback when done.
Main loop keeps reading camera while model processes the previous frame in a background thread.
queue.Queue(maxsize=1) safely passes results between threads.
Neural networks require fixed-size inputs. The state vector must always have the same number of elements. CartPole: always (4,). Reacher: always (11,). Non-negotiable.
Exactly what's needed — no more. CartPole: cart position, cart velocity, pole angle, pole angular velocity. Not the pixel render. Not the time elapsed. Just those 4 numbers.
| Context | Shape | Contents |
|---|---|---|
| M1 exercise result | (4,) float32 | Index, middle, ring, thumb MCP angles |
| CartPole observation | (4,) float32 | cart pos, cart vel, pole angle, pole vel |
| Reacher observation | (11,) float32 | Joint cos/sin, target pos, velocities, fingertip pos + dist |
| M4 action | (2,) float32 | Joint 0 torque, joint 1 torque |
A standardised interface — the same three calls — for hundreds of different environments. Whether you're training on CartPole or a humanoid robot, your RL code stays the same. Only the environment changes.
obs, info = env.reset()
obs, reward, done, trunc, info = env.step(action)
env.close()
render_mode="rgb_array", each env.render() call returns
the simulation frame as a numpy array (height × width × 3 RGB) instead of opening a display window.
The FastAPI server in api/server.py uses this: render → encode to JPEG →
stream over HTTP to your browser. No display hardware, GPU, or desktop required.
| Symbol | Name | CartPole example |
|---|---|---|
| S | State space | All possible (cart_pos, cart_vel, pole_angle, pole_vel) combinations |
| A | Action space | {0, 1} — push left or push right |
| R(s,a) | Reward function | +1 if pole upright; episode ends if it falls |
| T(s′|s,a) | Transition function | Physics of the system — deterministic for CartPole |
| γ | Discount factor | 0.99 — near rewards worth slightly more than far rewards |
For CartPole: knowing current (position, velocity, angle, angular_velocity) is sufficient to predict the next state. No history needed. This is what makes the problem mathematically tractable.
Naive policy gradient methods: update too aggressively → policy "collapses" → unrecoverable. PPO's fix: a clipped objective function that constrains how much the policy can change in one update. "Proximal" = stay close to where you were.
n_steps timesteps, collect (s, a, r, s′)| Parameter | What it controls | Default |
|---|---|---|
n_steps | Timesteps collected per rollout before an update | 2048 |
batch_size | Mini-batch size for each gradient update | 64 |
clip_range | Max policy change per update (the "proximal" constraint) | 0.2 |
gamma | Discount factor γ — how much future rewards are worth | 0.99 |
Your hand moves slightly
→ camera captures new frame
→ MediaPipe detects landmark 8 at (x=0.6, y=0.4)
→ scale_landmark_to_action → action = [0.2, -0.2]
→ env.step([0.2, -0.2]) → Reacher arm moves
→ env.render() → new frame sent to browser
→ you see the arm respond
→ your hand adjusts
→ loop continues at ~20 Hz
50–100ms latency between hand movement and arm response. That latency is the loop time. When it works, you feel it.
Gemini is a VLM: it understands both images and text. Give it a JPEG and a question — it reasons about the image in context. Not labelling — reasoning.
In this lab: camera frame + prompt → JSON action dict
| System | Organisation | What it does |
|---|---|---|
| RT-2 | Google DeepMind | VLA on real robot hardware |
| OpenVLA | Stanford / Berkeley | Open-source VLA model |
| π0 | Physical Intelligence | Dexterous manipulation VLA |
| Our M5 | This lab | Camera → Gemini → JSON action |
2x − 1 do, and why is it the right formula for Module 4?render_mode="rgb_array" do, and why does the lab hub need it?Six modules. Every one builds one stage of the pipeline. By Module 4, your hand controls a robot arm in real time. That's the point.
| Module | Duration | What you build |
|---|---|---|
| M0 — Kickoff | 15 min | Draw the pipeline. Print your first state vector. |
| M1 — Perception | 30 min | Webcam → 21 landmarks → 4 joint angles → state vector |
| M2 — Simulation | 20 min | Step environments, explore Reacher's observation space |
| M3 — RL | 30 min | Watch PPO learn CartPole live. Reward curve climbs. |
| M4 — Loop closes | 40 min | Your hand controls the robot. The loop is closed. |
| M5 — Foundation Models | 25 min | Gemini sees the camera and outputs robot actions |
| M6 — Wrap-up | 15 min | Map everything to real-world research tools |
We're not building from scratch. We're wiring together pre-trained pieces. The glue code is what you're writing today.
modules/00_kickoff/overview.ipynb # kernel: Physical AI Lab — run all cells
import numpy as np
print(np.array([145.2, 163.8, 171.4, 158.0], dtype=np.float32))
# [145.2 163.8 171.4 158.0]
# That printed line is a state vector.
Allow ~3 minutes. Then draw the pipeline on the whiteboard and name the module for each stage.
Your webcam sees 921,600 numbers per frame. MediaPipe compresses that to 21 landmarks. The angle formula compresses that to 4 joint angles. That's your state vector.
python modules/01_perception/01_webcam_basics.py
# Prints frame shape: (480, 640, 3)
python modules/01_perception/02_hand_tracking.py
# 21 green dots appear. Hold your hand up.
python modules/01_perception/03_joint_angles.py
# Prints [145.2, 163.8, 171.4] once per second
# "That is a state vector."
mediapipe.tasks.vision.HandLandmarker.
The older mp.solutions.hands still appears in most tutorials but is deprecated.
Key: coordinates already normalised to [0,1].
fallback_hand_demo.mp4 loops silently.
Code path is identical. Don't spend time on this.
03_joint_angles.py, add landmarks 13→14 to produce a 4-element state vector.
Allow ~10 min. Ask 1–2 volunteers to share output.
CartPole: 4 observations, 2 discrete actions. Reacher: 11 observations, 2 continuous torques. Those torques are what your hand controls in 20 minutes.
python modules/02_simulation/01_gym_intro.py
# Observation space: Box(-4.8, 4.8, (4,), float32)
# Action space: Discrete(2)
python modules/02_simulation/02_mujoco_reacher.py
# Observation space: Box(-inf, inf, (11,), float64)
# Action space: Box(-1.0, 1.0, (2,), float32)
observation_space.low and observation_space.high for Reacher.
Understand the bounds before you try to send actions. ~8 min.
python modules/02_simulation/03_pretrained_agent.py before the lab starts.
It loads a pre-trained policy and runs HalfCheetah (a MuJoCo humanoid) for 5 seconds.
When participants ask "what does a million training steps look like?" — that video is the answer.
Don't run this live; it takes 30 seconds to load and derails the pace.
python modules/03_rl/02_train_cartpole.py
# ~2–3 min. Keep matplotlib window visible.
# Walk the MDP cycle diagram while it trains.
# STOP TALKING when the curve climbs — let everyone watch.
total_timesteps and re-run. ~10 min. Watch the curve shape change.
python modules/04_perception_to_action/01_hand_to_reacher.py
# Two windows: camera + Reacher sim.
# Move your index finger slowly.
# x position → joint 0 torque. y position → joint 1 torque.
action = 2 * norm_x − 1
That's the entire "intelligence" of the Module 4 controller.
Landmark x in [0, 1] → torque in [−1, +1].
ActionSliceWrapper is provided.
It wraps Reacher's 2-element action space so you can pass a 3-element array and it slices the first two
elements for the actual env.step() call — your extra channel controls a visual indicator
without breaking the environment interface.
python modules/05_foundation_models/01_gemini_vision.py
# Captures one frame, sends "Describe this scene." ~2 sec.
python modules/05_foundation_models/02_gemini_robot_brain.py
# "Calling Gemini..." → "Done." → {"action":"FORWARD","reason":"..."}
MY_PROMPT. Different prompts → different responses.
How you phrase the question dramatically changes reliability of the output.
cached_responses.json automatically. Exercise still works.
cached_responses.json is the simplified version of this: if the API is unavailable,
the robot keeps operating from pre-computed responses rather than stopping.
Offline-first is a hard requirement in physical systems.
| Module | Pipeline stage | Real-world equivalent |
|---|---|---|
| M1 — Perception | Webcam → State Vector | Industrial vision systems; MediaPipe in production robot demos |
| M2 — Simulation | State → Action → World | MuJoCo used in Boston Dynamics, DeepMind, academic labs |
| M3 — RL | Training the policy | PPO in real locomotion controllers and game-playing agents |
| M4 — Closed loop | Perception to Action | Teleoperation data collection for BC / ACT / Diffusion Policy |
| M5 — Foundation Models | Reasoning layer | RT-2 (Google), OpenVLA (Stanford/Berkeley), π0 (Physical Intelligence) |
Nine independent projects. Every one has a closed feedback loop — that's the Physical AI criterion. None are theory-only. Pick one from your hardware tier and start with the perception layer.
| # | Project | Level | Extends |
|---|---|---|---|
| 1 | Adaptive Posture Coach — Webcam detects shoulder/neck landmarks. Agent adapts alert timing from your correction history — not a fixed threshold. | Beginner | M1 · M3 |
| 2 | Sim-to-Sim Transfer — Train PPO on CartPole, deploy into modified env (heavier pole, wind noise). Measure degradation. Apply domain randomisation. | Intermediate | M2 · M3 |
| 3 | Behaviour Cloning Agent — Use hand control (M4) to play a Pygame environment. Record (obs, action) pairs. Train an MLP clone. Observe covariate shift. | Intermediate | M1 · M4 · M5 |
| 4 | Sign Language Recogniser + Corrective Loop — Real-time letter recognition from landmarks. Model confidence feeds back to prompt the user to re-sign ambiguous letters. | Beginner–Int | M1 · M5 |
| 5 | PID Tuner via RL — Simulated pendulum or water level. Use RL to learn PID gain parameters as the action space. Compare learned vs hand-tuned. | Intermediate | M2 · M3 |
| 6 | Multi-Step VLA Planner — Give Gemini a verbal instruction. It decomposes into sub-goals. Each executed via the M4 pipeline. Chain 3+ steps. | Intermediate | M4 · M5 |
~£20 total hardware. Projects 7–8 are the best introductions to real-world Physical AI.
| # | Project | Level |
|---|---|---|
| 7 |
Gaze-Tracked Servo Turret — Pi Camera + MediaPipe detects face or hand.
Two SG90 servos pan/tilt to track. Apply the M4 mapping (2x−1) directly to servo angle.
Servo moves → camera points somewhere new → perception updates → next command. Physically broken without the loop. |
Intermediate |
| 8 |
Imitation Learning Arm — 2-servo arm on Pi. Record (landmark state, servo angle) demonstrations. Train small MLP to replicate. Deploy. Document where it drifts.
Covariate shift is physically observable — the arm drifts into states it never saw. The failure mode is the educational content. |
Int–Advanced |
| 9 |
RL Balancer — Train PPO on MuJoCo pendulum. Deploy onto physical pendulum with Pi + servo + encoder. Measure the sim-to-real gap quantitatively. 4–6 week project.
This is what labs like Tedrake's at MIT study at scale. |
Advanced |
Key numbers, demo scripts, exercises at a glance, common issues. Keep this open during the session.
# Key landmarks
lm 0 = wrist
lm 4 = thumb tip (M4 exercise)
lm 8 = index tip (M4 control)
lm 13 = ring MCP (M1 exercise)
lm 14 = ring PIP (M1 exercise)
# State shapes
M1 exercise : (4,) float32
CartPole obs : (4,) float32
Reacher obs : (11,) float64
M4 action : (2,) float32
# Training
50k steps ≈ 2–3 min on laptop
CartPole clicks at 20k–40k steps — don't stop early
# CartPole terminal conditions
|pole angle| > 12° → episode ends
|cart pos| > 2.4m → episode ends
max reward = 500 (500 steps)
# Reacher action space
torque ∈ [−1, +1] for each joint
# M4 mapping
action = 2 * landmark_x − 1
# MDP components
S = state space
A = action space
R = reward function
T = transition function
γ = discount factor (0.99)
# Start the hub
.venv\Scripts\activate
python api/server.py
# → http://localhost:8000
# Verify install
python verify_install.py
# All 14 should be PASS or WARN
# M1 — Perception
python modules/01_perception/01_webcam_basics.py
python modules/01_perception/02_hand_tracking.py
python modules/01_perception/03_joint_angles.py
# M2 — Simulation
python modules/02_simulation/01_gym_intro.py
python modules/02_simulation/02_mujoco_reacher.py
# M3 — RL
python modules/03_rl/02_train_cartpole.py
# M4 — Loop closes
python modules/04_perception_to_action/01_hand_to_reacher.py
# M5 — Foundation Models
python modules/05_foundation_models/01_gemini_vision.py
python modules/05_foundation_models/02_gemini_robot_brain.py
# M6 — Wrap-up
modules/06_wrapup/concepts_map.ipynb
| Module | Exercise | Time |
|---|---|---|
| M0 | Print a state vector: np.array([145.2, 163.8, 171.4, 158.0], dtype=np.float32) | 3 min |
| M1 | Add ring finger angle (landmarks 13→14) to make a 4-element vector | 10 min |
| M2 | Print observation_space.low and observation_space.high for Reacher | 8 min |
| M3 | Change total_timesteps and re-run. Watch the curve shape change. | 10 min |
| M4 | Add landmark 4 (thumb tip) as a 3rd torque channel via ActionSliceWrapper | 15 min |
| M5 | Write your own MY_PROMPT. Observe how phrasing changes output reliability. | 10 min |
| Symptom | Fix |
|---|---|
| MediaPipe window black | assets/hand_landmarker.task missing — re-run setup.bat |
| MuJoCo window doesn't open | Normal — render fallback active. Check terminal for rgb_array message. |
| Reward curve never climbs | Let it run — CartPole clicks at 20k–40k steps. Don't stop early. |
| Port 8000 in use | netstat -ano | findstr :8000 → taskkill /PID [n] /F |
| Hub page blank | Chrome or Edge — not Firefox. |
| Gemini 429 rate limit | Cache fallback activates automatically. Continue. |
| ARM64 / mujoco fails | WSL2: wsl --install (restart) → run setup.sh inside Ubuntu |
| [WARN] Camera unavailable | Fallback video looping — code path identical. Not a problem. |
| What fails | What happens automatically |
|---|---|
| Webcam unavailable | Loops fallback_hand_demo.mp4 silently — code path identical |
| OpenGL / display | make_env tries "human" → "rgb_array" → None |
| Gemini key missing | Random entry from cached_responses.json |
| Gemini API call fails | Any exception → cache fallback |
| No internet during session | All assets pre-bundled — everything works |
You've touched every piece of the modern Physical AI stack — perception, simulation, reinforcement learning, the closed loop, foundation model reasoning. Clone the repo. Take it home. Keep building.