Maker Β· Technical Deep-Dive Β· Closed Investigation

Camera Fix, Recording & Quantified Rollout Testing

Tracing a "loses the plot" 0-success policy back to the operator's own hand in frame, fixing it, then quantifying exactly how well the fix worked β€” down to the lux

Result: the earlier dense-dataset policy's total rollout failure was a camera-framing bug, not a training-pipeline problem β€” fixed by moving to a front-facing mount. The corrected policy hits 86.3% (69/80) on its trained position grid and 53.6% (30/56) on genuinely novel positions. Lighting sensitivity is now quantified in actual lux, not just observed. One sharp, reproducible anomaly (a near-total "blind spot" for one sample at one position) was investigated, not resolved, and knowingly closed in favor of the sim/SmolVLA roadmap.
Covers: 18–22 September 2026 Β· camera stand redesign β†’ pre-flight validation β†’ 3-batch recording (96 episodes) β†’ merge β†’ two training runs on the ML Desktop β†’ rollout testing β†’ quantified lux investigation β†’ extended confirmation passes

1. Starting problem: a policy that "loses the plot"

The prior 120-episode "dense" dataset (diagnosed in an earlier session note) trained an ACT policy that scored zero successes on rollout, on both the Pi 5 and the Mac β€” a stall-and-restart failure mode rather than a clean miss. Reviewing training footage frame-by-frame found the actual cause: the high-mounted overhead camera (an official LeRobot/XLeRobot design, originally chosen for hypothetical cross-hardware compatibility with LeKiwi/XLeRobot) captured the operator's own hand and arm in frame during teleoperation demonstrations β€” a genuine train/inference distribution mismatch, since no hand exists during autonomous rollout. The same high mount also caused a physical arm/camera-stand collision in some poses, and the wrist camera's fisheye lens added further distortion on top.

Decision that unblocked the fix: drop the cross-hardware-compatibility requirement for this camera mount entirely. LeKiwi and XLeRobot are planned to run SmolVLA (not ACT) and will get their own separate recording/validation pass on their own hardware β€” so the SO-101 rig's camera position never needed to match their mount design in the first place. This freed the mount to be optimized purely for this rig's own geometry.

2. Camera stand β€” resolution

A CAD detour into FreeCAD explored a custom-designed stand before concluding a purpose-built one wasn't worth the time given a working off-the-shelf option existed. Settled on a freestanding 3-leg stand (Printables model 648198) rather than a printer-frame-specific clip mount, since this rig isn't printer-frame-mounted. Front-facing vs. side-facing positions were compared via test stills β€” front-facing won on clean full-frame workspace coverage with minimal clutter. The wrist camera's fisheye lens was also swapped for a non-fisheye one during the pre-flight pass below.

3. Pre-flight validation

Before any new recording, a full reconfirmation pass was run rather than trusting the last-known configuration β€” ports, camera indices, and calibration are all known to drift between sessions on this rig.

Parked, not blocking: the spd-say/speech-dispatcher audio-cue setup for episode boundaries remains unresolved (chased through locale, HDMI routing, and synthesis-engine config in an earlier session). Recording proceeds fine on terminal text output alone.

4. Recording β€” three batches

Recorded in three separate batches, using the corrected camera setup and, this time, the streaming-encoding flags that had earlier fixed a BrokenProcessPool crash:

--dataset.streaming_encoding=true
--dataset.encoder_threads=2
--dataset.num_image_writer_processes=0
--dataset.num_image_writer_threads_per_camera=2
BatchRepo IDEpisodesNotes
1so101_pi5_act_v3_framefix32Clean
2so101_pi5_act_v3_batch231 (of 32)Teardown crash lost the last episode
3so101_pi5_act_v3_batch333One extra episode recorded to compensate

Batch 2 teardown crash: a TimeoutError: OpenCVCamera(/dev/video2) latest frame is too old: 1052.1 ms occurred during video-encoder finalization, after the full 32-episode cadence summary had already printed. Confirmed as a shutdown/finalization resource-contention crash, not mid-recording data loss β€” episodes 0–30 (31 of 32) were intact, only the final episode was lost. Rather than risk a --resume=true for one episode, the simpler fix was recording one extra episode in batch 3.

Grid: 8 positions Γ— 4 orientations Γ— 3 repetitions = 96 episodes, matching 32 + 31 + 33 = 96 exactly.

Merge

Merged with the same LeRobotDataset.create() / add_frame() / save_episode() pattern used for the earlier bgdiv and visual-trigger-backdoor merges. Three bugs in the draft merge script were caught and fixed: batch 1 missing from BASE_REPO_IDS, the wrong features schema (policy-schema format instead of the dataset-metadata format LeRobotDataset.create() actually needs β€” the same class of bug hit once before in the bgdiv merge), and a stale task string ("...box" instead of the actually-recorded "...target").

Result: 96 episodes, 56,856 frames β€” verified against the batch sum (18,956 + 18,355 + 19,545 = 56,856, exact match).

5. Training β€” two runs on the ML Desktop

RunDatasetStepsWall timeFinal loss
Quick validation32-episode batch 1 only20,00040m 27s~0.128
Full run96-episode merged dataset50,0001h 40m 52s~0.112–0.116

Both runs on the RTX 5060 Ti β€” roughly 8Γ— faster than Mac MPS for this workload (~8.3 steps/sec vs. ~1 step/sec previously observed). The quick 32-episode run served as a fast sanity check on the new camera setup before committing to the full training time. Both completed cleanly with no crashes β€” a clean contrast with the earlier dense-dataset run, which had trained without error yet produced a policy with zero rollout successes, confirming the camera fix (not the training pipeline) was the actual problem all along.

6. Rollout testing β€” the two headline findings

Session-start hiccup: the Pi 5 briefly dropped off the network and needed its WiFi password re-entered locally β€” back up at the same address, 192.168.2.110, within ~30 minutes.

Finding 1 β€” lighting mismatch

Rollout under the room's default lighting produced poor results across the board; testing improved dramatically once the room light was matched to the actual recording-time condition. This reconfirms the original background-diversity experiment's lighting/glare finding as a general, recurring property of this rig, not a one-off β€” any future rollout session should deliberately match recording-time lighting before judging policy quality.

Finding 2 β€” object generalization vs. positional grasp-margin

Testing across three physical objects at the 8 trained positions initially looked confusing until separated into two independent variables:

A mid-session concern that some tested positions might be novel (unseen during training) was checked and ruled out β€” all 8 were confirmed part of the actual training grid. So there is no evidence of a spatial-generalization failure in this specific finding; it's an object/position-margin interaction.

7. Quantifying the lighting effect: reflected lux by sample

Measured with a phone light-meter app (back-camera variant, chosen because it lets the sample be visually framed and confirmed before reading), each sample filled the measurement area so only its own reflected light was captured, at a constant camera-to-sample distance.

Sample colorNo added light+1 light source+2 light sources
Black25–30100–110140–150
Blue40–45120–140200–220
Red45–50130–150210–230
Gray55–60160–170270–290
Yellow80–90180–200310–320
White100–110240–260340–380

Position dependence, discovered mid-measurement: the table above was measured near position 5 β€” but reflected lux also depends strongly on table position, since the two LED sources sit physically closer to positions 5–8. At those positions, white's reflected lux rises further, to 400–420, versus its 340–380 baseline. A separate retest at position 3 (chosen as roughly equidistant from both lights, for a fairer cross-sample comparison) put black at only 70 lux there β€” resolving what first looked like a measurement discrepancy against the general table's 140–150 reading; it was two different positions, not an error.

Two distinct lighting failure modes, from direct observation β€” not the same story with a sign flip

Shape surfaced as a third, independent confound: egg-like samples (blue, gray) both scored 1/2 at moderate, unremarkable lux β€” more likely a grip-difficulty issue than a lighting one. The thick-disk red sample scored 2/2 at similarly moderate lux, better than expected β€” reframing some of the apparent color/lux effect as partly a shape effect for the mid-range samples, while lux still cleanly explains the black and white extremes.

8. Extended confirmation β€” trained positions, 10 trials each (yellow)

Yellow β€” the trained-on object, and the sample left standing after black (localization failure) and white (glare failure) were ruled out β€” was run 10 trials per trained position, up from the initial ~4-trial pass.

PositionSuccess / 10
17
210
310
48
58
68
79
89
Total69 / 80 (86.3%)

No position collapses to near-zero; the weakest (position 1) still clears 70%. This is a substantially stronger, more statistically reliable confirmation than the initial pass, and confirms the camera fix + lighting correction together produce a genuinely solid policy across the full trained grid.

9. Spatial generalization β€” untrained (novel) positions

9a. First pass (1 trial/point, yellow)

CoordinatesResult
(5, 18)Success
(11, 17)Fail
(12, 7)Success
(16, 10)Fail
(21, 13)Success
(30, 15)Fail
(33, 13)Success
(35, 20)Fail

4/8 (50%) β€” flagged at the time as too small a sample to trust.

9b. Confirmation pass (7 trials/point, yellow)

CoordinatesSuccess / 71-trial result (9a)
(5, 18)4/7Success
(11, 17)6/7Fail
(12, 7)5/7Success
(16, 10)5/7Fail
(21, 13)2/7Success
(30, 15)4/7Fail
(33, 13)3/7Success
(35, 20)1/7Fail
Total30 / 56 (53.6%)4/8 (50%)

The aggregate rate is reassuringly consistent between passes (50% vs. 53.6%), even though several individual points flipped: (11,17) and (16,10) went fail→mostly-succeed, (21,13) went succeed→mostly-fail — a clean demonstration of why the single-trial pass was explicitly labeled preliminary. (35, 20) stands out as the weakest point (1/7, 14%), consistent across both passes — the first appearance, with the standard yellow sample, of the same "not seen, unresponsive to added light" signature later found with red at trained position 4 (§10).

Qualitative "hunting" behavior observed at novel positions β€” the arm visibly tracking toward the sample, retrying on failure rather than returning to a default pose β€” is attributed on review to a mechanical consequence of the camera reposition itself: under the old overhead mount, the arm's own body frequently occluded the sample during approach; under the new front-facing camera, the sample stays continuously visible, which is what actually enables the retry behavior. A cleaner, more mechanistic explanation than "emergent closed-loop tracking."

9c. Red-sample novel-position pass (5 trials/point) β€” completes a 2Γ—2 design

CoordinatesSuccess / 5 (red)
(5, 18)0/5
(11, 17)0/5
(12, 7)0/5
(16, 10)4/5
(21, 13)1/5
(30, 15)0/5
(33, 13)1/5
(35, 20)1/5
Total7 / 40 (17.5%)
The full 2Γ—2 design:
Trained positionsNovel positions
Yellow (trained sample)86.3% (69/80)53.6% (30/56)
Red (untrained sample)58.75% (47/80)17.5% (7/40)

Matches the predicted ordering: yellow-trained best, red-novel worst (stacking both disadvantages), with the two single-disadvantage cells (yellow-novel, red-trained) landing close together (53.6% vs. 58.75%) β€” plausible, since each carries one but not both sources of difficulty.

(16, 10) is the most surprising single data point in the whole investigation: 4/5 (80%) for red β€” dramatically higher than every neighboring red result, including trained position 4 just ~2cm away, which showed near-total non-detection (Β§10). Directly repeated and confirmed: the sample is essentially invisible to the policy at position 4 but reliably detected a couple of centimeters away. A sharp, small-scale spatial discontinuity, not a gradual gradient.

A second, independent hypothesis raised for red's broader novel-position failures (explicitly flagged as a feeling, not a confirmed measurement): the gripper may be closing a little too high and too early β€” a grasp height/timing problem distinct from the "not seen at all" story that applies to position 4 and (35,20).

10. Red sample β€” full 10-trial run at trained positions, and the position-4 anomaly

Following up on red's promising 2/2 spot-check, a full 10-trials-per-position run was done across all 8 trained positions. Caveat: the policy was trained on yellow only, never red β€” some overall drop is expected on that basis alone; what's notable is the shape of the pattern, not just the overall rate.

Testing began under direct morning sunlight, producing unquantified "awful" results β€” the window was closed and artificial lighting restored to match the calibrated setup before any numbers below were collected.

PositionRed / 10Yellow / 10 (Β§8, for comparison)
197
2810
3510
418
548
638
799
889
Total47 / 80 (58.75%)69 / 80 (86.3%)

The pattern is essentially inverted relative to yellow: yellow's weakest point was an edge case (position 1, 70%) with everything else 80–100%; red's four outer positions (1,2,7,8) are strong (80–90%) while the four inner positions (3–6) collapse, with position 4 nearly failing outright.

Refined failure-mode split by region:

The corners are a shape problem, the middle is a vision problem β€” two independent issues that happen to overlap spatially into what first looked like one smooth pattern.

Position-4 diagnostics, same day

Working theory, unconfirmed: not light placement or surface lux (both checked and ruled out), but the camera's own viewing geometry interacting with the disk's flat, reflective face β€” a specific camera-sample-light angular relationship present at position 4 (and possibly (35,20)) and not at adjacent points, without necessarily showing up as a large change in surface-reflected lux. The single most diagnostic remaining test β€” inspecting a raw camera frame with the sample at position 4 β€” was identified but not run.

11. Closing decision

With three obvious levers (light position, light intensity, sample orientation) each tried and none cleanly explaining the position-4 anomaly, and red not being the standardized sample going forward, this is closed as a documented, partially-unresolved anomaly rather than a solved question β€” an explicit user decision. Retraining to specifically fix weak positions was considered and declined: not worth the time relative to the sim/SmolVLA roadmap that follows this investigation.

Summary

What's next

The planned next step is installing MuJoCo on the ML Desktop, moving from pure real-hardware ACT training toward a parallel simulation track and SmolVLA. Full plan: SO-101 Sim-to-Real Integration Roadmap.