Level 11 (after Lessons 11.1 and 11.2; Project 8) · Time 4 to 6 hours · Difficulty 4 of 5
Repeat Project 8 with the cube positions estimated from the front camera’s depth and segmentation images instead of read from mjData. Your routine receives a Robot, not mjData. A Robot offers what a real robot would have: joint encoders, motor commands, a jaw-opening sensor, tactile pad forces and the front camera. Every decision, including whether a cube really landed in the tray, has to come from those.
vision_scene.py holds Project 8’s layouts, hold and judge, plus the Robot interface. Read it; do not edit it.starter.py asks for two functions:
estimate_cubes(model, depth, seg, cam_pos, cam_mat), a pure function of one frame. It cannot see the simulation, so its accuracy is measured fairly.pick_and_place(robot), which returns a category per cube.Rendering needs an OpenGL context. On a machine without a display, set MUJOCO_GL=egl or osmesa; without one, the tests skip.
Estimator. The reference back-projects a cube’s pixels and keeps the top face (points within 4 mm of the highest). It returns the midpoint of that face’s extent along the face’s own principal axes. On the 300 cube views of 100 layouts from the tuning seed, the methods compare as follows:
| Method | Median error | Within 5 mm |
|---|---|---|
| centroid of every visible pixel | 9.6 mm | 0 of 300 |
| centroid of the top face | 2.4 mm | all |
| midpoint of the top face’s extent (reference) | 0.45 mm | all (95th percentile 1.2 mm) |
The visible side faces pull the first centroid toward the camera. Nearer pixels of the top face cover less of it, so the second leans toward the camera too, by less. On the test seed (13), the reference’s median error is 0.49 mm, with all 300 within 5 mm. In the first frame a cube covers 493 to 860 pixels at 640 × 480 (median 671), and none was hidden by another.
Task. The reference is Project 8’s state machine, with three changes:
mjData;It runs its dynamics (bias forces, mass matrix) on its own MjData copy, filled from the joint encoders. It placed 300 of 300 cubes on the test seed, every report matched the judge, and 100 episodes took 22 s on 16 processes. Project 8’s reference placed 300 of 300 as well, so the gap the specification allows (10 points) is unused.
MJC_IMPL=starter MUJOCO_GL=osmesa pytest projects/p09_vision_pick
MUJOCO_GL=osmesa pytest projects/p09_vision_pick
Four tests, about 30 s:
python tools/mutate_project.py p09_vision_pick checks the mutants in mutants.json; run it with MUJOCO_GL set.
Parallel episodes. The scene runs episodes in worker processes started with spawn. Workers forked from a process that has created an OpenGL context (pytest’s GL check does) inherit Mesa’s locks in an unusable state. In this project’s first test run they hung without using any CPU. With spawn, a script of yours that calls run_many must keep its top-level code under if __name__ == "__main__":.
mjOBJ_SITE. Matching the id alone is harmless here only because no site’s id (0 to 5) equals a cube’s geom id; the control mutant confirms it. Check the type anyway.solution.py, about 140 lines.