Robotic manipulation · vision-language grounding · MuJoCo

minivla — language-conditioned pick-and-place

You type “put the red block to the left of the blue block.” A 7-DOF Franka Panda looks at the table through a single overhead camera, works out which block you mean and where it should go, picks it up, puts it there — then looks again to check it actually worked, and tries something different if it didn’t.

MuJoCo 3.12 Franka Emika Panda Python 3.11 · NumPy · OpenCV PyTorch (MPS) 679 evaluated episodes runs on a MacBook, CPU only
99.0%end-to-end success, 97 held-out episodes
1.4 mmmean pixel→world grounding error
92.6%across 582 episodes under degraded conditions
0.6 mmtool-tip tracking after the correction loop

What it does

01
Perceive
Overhead RGB → block detections in pixels, with colour and orientation
02
Locate
Backproject each pixel and intersect the table plane at the block’s height
03
Reason
Resolve “left of the blue block” into a target pose; check it for collisions
04
Act
Damped least-squares IK drives scripted pick and place primitives
05
Verify
Re-render, re-check the instruction, diagnose, retry the right stage

The loop closes at step 05 → step 01. The verifier is shown the instruction and the image, and nothing else.

The design rule for the whole project was modular pipeline first, learning second: build a fully engineered system that works and can be measured, then run a small learned-policy experiment against it. That ordering means there is a working fallback at every stage, and the learned policy has an honest baseline to be compared to rather than being graded on its own.

The rule that makes the numbers mean something

The pipeline may never read object positions out of the simulator. Perception, reasoning and motion see rendered images and the robot’s own joint angles — nothing else. Ground truth is used only to score episodes and to draw clearly-labelled debug overlays. A pytest case scans the pipeline packages on every run and fails the build if any of them import the ground-truth module or read an object pose directly.

Demo

Six randomised scenes, instructions of increasing difficulty: a single pick, a lateral relation, a depth relation (the hard one for a single camera), a stack, a three-object “between”, and finally a deliberate perception error that the system catches and corrects. Overlays are the pipeline’s actual beliefs — detection boxes, chosen target, computed placement — with ground truth marked separately in magenta.

The self-correction, on its own

A 17 cm error is injected into the reference block’s detection after the first look. The arm picks correctly but places the block on the wrong side. The verifier — which is given only the instruction and a fresh image — reports the block is 26 px on the wrong side of the reference. The executive diagnoses a perception fault rather than a grasp fault, looks again with clean perception, and the retry lands 12.0 cm on the correct side.

Results

Baseline — 20 seeded scenes × 5 instruction templates

Instruction templatenSuccess
pick up the X block20100%
put X to the left/right of Y20100%
put X in front of / behind Y20100%
stack X on top of Y20100%
put X between Y and Z1794.1%
overall9799.0%
Why the baseline is not the headline

99% on 97 episodes is one failure. One failure is not a failure distribution — it cannot tell you which stage is fragile or where the system’s operating envelope ends. So the baseline is reported as-is and the actual work is the stress sweep below, which degrades conditions until things break and reports the curve.

Stress sweep — 679 episodes across seven conditions

Bar chart of end-to-end success and grounding accuracy across seven degraded conditions
ConditionEnd-to-endGroundingWhat it probes
baseline99.0%100%
sensor noise σ=899.0%100%mild camera noise
sensor noise σ=2093.8%93.8%the palette’s weakest colour pair
block yaw ±45°67.0%97.9%the fixed-yaw grasp assumption
clutter, 7 cm spacing97.9%100%clearance search, occlusion
stale calibration, 4 mm99.0%100%error below the guard threshold
stale calibration, 30 mmrefusedthe startup guard fires on all 97

Excluding the 97 episodes the calibration guard refused up front, 582 episodes were attempted and 92.6% succeeded. The guard declining to drive a robot it knows is miscalibrated is correct behaviour, not a failure, so it is counted separately — folding 97 safety refusals into the taxonomy would bury every real failure underneath them.

Failure taxonomy

Horizontal bar chart of failure classes across attempted episodes

Grasp slip dominates (30 of 43 failures), and almost all of it is the ±45° yaw condition. The gripper approaches at a fixed yaw, so a block rotated 45° presents a corner to the finger pads instead of a face; the pads close on an edge and the block is dropped in transit. That is the limitation the project documented in week one — and the number that makes it real, rather than an assertion, is 67%.

Grounding failures were predicted before they happened. A colour calibration early on measured the rendered hue of every block material and found orange and yellow separated by 10 units out of 180 — the tightest pair in the palette. Under σ=20 noise, 5 of the 8 grounding failures involve orange. They also fail safely: the perceiver reports “multiple candidates” and declines rather than moving the wrong block.

Per-stage conditional success

Bar chart of conditional success by pipeline stage

An end-to-end number is a product of stages, so it hides which stage carries the risk. The conditional decomposition says grounding is not the bottleneck at baseline (100%), nor is the grasp given correct grounding (100%), and the residual sits downstream of a successful grasp. Under stress the picture inverts: grounding falls to 93.8% and drags everything after it. One aggregate number would have shown 92.6% and pointed nowhere.

Verifier reliability

Confusion matrix of the closed-loop verifier
A result I don’t trust, and why

The verifier scores almost perfectly. It shouldn’t be believed at face value: the verifier and the grounder share a detector, so when segmentation fails they fail the same way and the verifier never contradicts the grounder about what is on the table. Their errors are correlated by construction. This is the strongest argument in the project for a vision-language verifier — an independent perception path is what makes closed-loop checking worth having. A shared-backbone verifier only re-checks geometry, which is the easy half.

Things that went wrong, and what they taught

Perception · exposure

The detector was at 85% and every error was the same one

Orange kept reading as yellow. It wasn’t the thresholds: the rendered orange face was RGB (255, 238, 30) — hue 28, against yellow’s 30. Saturated materials under a bright light clip their dominant channel at 255, and the extra light lands in the other channels, sliding the measured hue toward neutral.

The fix was to expose the scene correctly, not to widen the detector’s bins — widening bins would have been tuning the simulator to make my code work. A calibration script now measures each material’s rendered hue at nine workspace positions and places the bin edges at the measured midpoints. Detection went to 100% on 48 blocks.

Motion · systematic error

Every grasp was aiming 7 mm high

Open-loop IK plus a position servo left a repeatable 6.7 mm droop under gravity — finite servo stiffness, exactly what a real position-controlled arm does. The fix is an outer loop on the tool pose: measure where the tip actually ended up from forward kinematics of the measured joint angles, then re-solve for a target shifted by the residual. It uses only the robot’s own state, and converges in one correction because the droop is systematic rather than random. Tracking error went from 6.7 mm to 0.6 mm.

Perception · the bug the tests were hiding

A 99% success rate was propped up by retries

Collecting demonstrations, only 56% of episodes succeeded — against a 99% measured success rate. The telemetry showed grasps that had clearly worked (motor effort 1.9, block held) being reported as failures, because the expected block width was 21% too large.

The cause: block size was estimated from the detection’s bounding box, and an axis-aligned box around a square rotated by θ has side s(|cos θ|+|sin θ|) — at 15°, exactly ×1.22. Switching to √(mask area), which is rotation-invariant, and calibrating out a measured residual bias fixed it. Demonstration yield went 56% → 99% and sweep wall-time dropped from 114 s to 25 s because the retries that had been masking it were no longer needed.

The uncomfortable part is the lesson: the headline number was right and the system underneath it was wrong. Retries were quietly absorbing a systematic defect, and it only surfaced when a different consumer used the same components without them.

Closed loop · retry policy

The robot retried the identical wrong action three times

The first self-correction run failed with the same grasp error three times at the same coordinate. The diagnosis routed a grasp failure to “retry the grasp”, which never re-examines the coordinate that produced it. The executive now escalates a twice-repeated grasp failure to a perception fault, on the principle that a physical action that fails the same way twice is evidence about its input, not about the action.

Verification · occlusion

Stacking looks like failure from directly overhead

The verifier marked correct stacks as failures: putting one block on another hides the lower one from a top-down camera, and “I can’t see the reference block” was being read as the instruction not being satisfied. It is the opposite — occlusion of the reference with the target present at that spot is the strongest evidence a monocular overhead view can give that the stack happened. The verifier now reasons that way and corroborates with the 45° camera.

How the geometry works

Pixel → world

A camera is specified by a vertical field of view, so focal length in pixels is f = (H/2) / tan(fovy/2) — 659.4 px for the overhead camera at 640×480, giving 1.82 mm per pixel at the table. To locate a block, backproject its pixel into a world ray and intersect the table plane: s = (zplane − oz) / dz.

Both a full camera model and a four-point planar homography were implemented and compared. On table-level points they are numerically identical — two algebraic inversions of the same noiseless projection — so the comparison was decided by something else entirely.

Bar chart of pixel-to-world error by camera and correction method
Mean error, ray–plane methodOverhead45° camera
raw parallax on a 40 mm cube0.46 cm4.24 cm
intersecting at the block’s height0.00 cm0.00 cm
…with the height wrong by 5 mm0.06 cm0.53 cm

A detector sees a cube’s top face, not its footprint, so intersecting at z = 0 is wrong by the parallax. Intersecting at the block’s height instead is exact for an upright cube. The decisive row is the last one: block height is estimated, and sensitivity to getting it wrong is tan θ of the viewing ray — about 9° overhead versus 45° for the angled camera, a 9× difference. The overhead camera and the ray–plane method were chosen for that, plus one operational reason: the ray–plane model reads the live camera pose, so a startup check catches a camera that has moved. A cached homography would keep returning confident, wrong numbers — which is exactly what the 30 mm stress condition confirms it does.

Inverse kinematics

Damped least squares, hand-implemented on MuJoCo’s Jacobian: dq = Jᵀ(JJᵀ + λ²I)⁻¹e. In the SVD basis this scales each direction by σ/(σ²+λ²) rather than 1/σ, so near-singular directions are abandoned instead of chased. Converges in 5–10 iterations with sub-millimetre residual across the workspace.

How to actually catch a singularity

A test that probes with an arbitrary error direction will not find the problem: with the arm fully extended, an arbitrary 1 mm error produces an undamped step only ~3× larger than the damped one, because the well-conditioned directions dominate. Aim the same 1 mm along the smallest left-singular vector and the undamped step exceeds 1 radian while the damped step stays near 10⁻⁴. That asymmetry is why singularity bugs get reported as “intermittent” on real robots — and why the test now probes deliberately.

Learned-policy experiment

With the engineered pipeline measured, the same task was posed as behaviour cloning: 524 demonstrations from the scripted expert, a small MLP predicting which primitive comes next and with what target coordinates. Observations are detected blocks in the table frame plus gripper state — not raw pixels. That is a deliberate narrowing, and it is the honest thing to say about it: this experiment does not learn perception, it learns the policy layer. A pixel-to-action policy would have to learn colour constancy and projective geometry from scratch, which is what makes real VLA work need enormous, diverse data.

A negative result, kept

The first encoding could not be learned at all — validation accuracy froze at 46%. The reason was mine, not the model’s: two of the five primitives shared an identical observation while carrying different labels, so the dataset was not a function and no model could fit it. The network was correctly learning the marginal distribution. Re-posing the action space so each observation determines one action fixed it, and moved the interesting question to where it belonged — the placement regression.

Training and validation curves for the behaviour cloning policy
Training curves after the action space was re-posed. Primitive classification is essentially free by construction; the quantity actually being learned is where to pick and place.

It memorises — and more data doesn’t fix it

The policy fits its training scenes to 0.45 cm and generalises at 7.3 cm. Two explanations fit that, and they call for opposite responses: too little data, or the wrong inductive bias. A learning curve tells them apart.

Learning curve: held-out error versus number of training scenes, with an attention baseline
Training scenesRowsTrainHeld-out
253380.50 cm9.33 cm
506670.43 cm8.06 cm
1001,3370.75 cm7.99 cm
2002,6740.45 cm7.28 cm
200, slot-attention2,6740.88 cm4.03 cm

Eight times the data buys 2 cm. The curve flattens well above the training error — the signature of an inductive-bias problem, not a data problem. Swapping the MLP for a model given the shape of the task (score each object slot against the instruction, attend, then predict an offset relative to the attended slot) nearly halves the error at the same data, and on the selection subtask specifically it is 2.9× better: 11.4 cm → 3.9 cm.

That is the concrete, local version of why production VLA systems are large pretrained attention models over tokens rather than small per-task policies. “Find the thing the instruction refers to, then act relative to it” is the operation attention is. An MLP over a flat scene vector has to discover it as an arbitrary function of 45 inputs.

Rolled out closed-loop: 16.7% against the expert’s 100%

TemplatenCloned policyScripted expert
pick2015%100%
place lateral2035%100%
place depth2020%100%
stack200%100%
between1612.5%100%
overall9616.7%100%
The two results explain each other

77 of the 80 failures are “grasp failed”. The grasp tolerates about 2 cm of position error; the policy localises the target block to 11.4 cm. It isn’t failing at manipulation — it never gets to manipulate. It fails at selection, one step earlier, and the closed-loop number is just the open-loop error seen through a threshold. stack at 0% is the sharpest case, because stacking needs the tightest lateral accuracy of any template.

It is also a clean demonstration of compounding error under distribution shift: the expert only ever demonstrates states along successful trajectories, so once the policy’s first action is 11 cm off, it is in a state the training data never described and has no way back. That is the standard argument for interactive correction, which this experiment does not implement.

Read honestly, 16.7% vs 100% is not “the expert is better at robotics”. The expert has structural access to the answer — it computes reference + 12 cm in a direction resolved from the relation, by an exact geometric rule. The policy has to infer that rule from examples. The engineered version winning is the expected outcome, and precisely why the project was sequenced pipeline-first: it is both the fallback and the yardstick.

Honest limitations