LeRobot camera errors: the camera never opens, or the keys do not match
Your LeRobot run stops at the camera step, in one of two ways. Either the camera never opens: find_cameras lists nothing, or teleoperate and record quit with a ConnectionError at startup. Or the camera opens fine and the failure moves downstream: the policy cannot find the image keys it expects, and a ValueError names the missing features. This page walks both families using the exact text LeRobot prints, then the cause, the fix, and the prevention for each message. The first move is always the same: run lerobot-find-cameras opencv and read what it prints. The second move is to compare the camera key names in your config against the keys the policy expects. Most reports close at one of those two checks.
Two families, one pipeline
Every camera in LeRobot passes through three steps. Discovery: lerobot-find-cameras opens every candidate device and prints a table. Connection: the camera class you configured opens the one device your config points at. Keyed capture: the frames land in your dataset under a key built from the name in your config. Family one strikes at discovery and connection. The camera never opens at all. Family two strikes at keyed capture. The camera works, but the name on the frames does not match the name the policy expects.
LeRobot camera errors come in two families: the camera never opens at all, or the camera opens and its key does not match what the policy expects. Every string on this page belongs to one of the two.
| What you see | Family | First move |
|---|---|---|
| No cameras (OpenCV or RealSense) were detected. | Discovery | Check cable, port, and OS camera access |
Failed to open OpenCVCamera(0). Run lerobot-find-cameras opencv to find available cameras. | Connection | Re-run discovery, use the fresh index |
| Error creating camera front with config ... | Config | Read the tail after the last colon |
| Error finding RealSense cameras: failed to set power state | Connection, macOS | Run the same command with sudo |
| frame width=640 or height=480 do not match configured width=1920 | Streaming | Fix the virtual camera resolution |
| SmolVLA requires at least one camera. | Keys | Give the run at least one camera |
| All image features are missing from the batch. | Keys | Align key names, or use a rename map |
Family one: the camera never opens
What find_cameras does before it prints anything
On Linux, discovery scans the /dev/video* device paths. On Windows and macOS it tries every index from 0 up to 59, one at a time. Each candidate gets opened, asked for its default width, height, fps, and format, and the full command also captures a test image after a one second warmup. The test images land in outputs/captured_images, so you can see which index is which view before you commit a config (camera_opencv.py, lerobot_find_cameras.py).
The official cameras page shows the output you should expect: a --- Detected Cameras --- block with one entry per camera, its index, its backend, and its default stream profile (LeRobot cameras documentation).
No cameras at all
No cameras (OpenCV or RealSense) were detected.
Cause. Discovery ran and every candidate refused to open. On Windows, check system camera access for desktop apps first. On Linux, look for a missing /dev/video* node: an unpowered hub, a bad cable, or a driver that did not bind. If you ran the RealSense flavor and that Python package is absent, you also see the skip notice Skipping RealSense camera search: pyrealsense2 library not found or not importable. and that search never ran.
Fix. Run plain lerobot-find-cameras with no type filter, so both searches run. Turn on camera access for desktop apps in your OS privacy settings. Plug the camera into a direct port, not a hub. For RealSense, install the extra first: pip install -e ".[realsense]".
Prevention. Re-run discovery before every session. The official docs warn that identifiers can change after a reboot or a re-plug. Keep cameras on direct ports.
The camera was there, then the index moved
Failed to open OpenCVCamera(0). Run
lerobot-find-cameras opencvto find available cameras.
Cause. The connect() call tried the device and the device said no. LeRobot prints the camera as its class name plus the index you gave it, so OpenCVCamera(0) means your config points at index 0 and index 0 did not open. The classic cause is an index shift: you re-plugged a camera or rebooted, and what was 0 is now 2. The other classic cause is a second program holding the camera: a browser tab, a chat app, another teleop session. A Windows report in the tracker has exactly this shape: discovery lists the laptop camera, then opening it fails (issue 1456).
Fix. Run lerobot-find-cameras opencv again. Put the fresh index into index_or_path. Close every other app that might hold the camera. On Linux you can pass a stable device path instead of an index, which survives re-plugs better than any number.
Prevention. Pin the device, not the number. A path under /dev/v4l/by-id/ keeps its meaning across reboots. RealSense cameras take a serial number instead, which is the strongest pin there is.
The config itself is rejected
Error creating camera front with config OpenCVCameraConfig(index_or_path=0, ...): ...
The line is long. The part that matters is the tail after the last colon. The wrapper comes from the camera factory and it re-raises the real error (cameras/utils.py). Two of the validators print their own text:
fourccmust be a 4-character string (e.g., 'MJPG', 'YUYV'), but 'MJPEG' is provided.
backendis expected to be in [<Cv2Backends.ANY: 0>, <Cv2Backends.V4L2: 200>, ...], but 699 is provided.
Cause. A typo in the cameras block: a fourcc code that is not four characters, a backend value that is not one of the codes, or a color mode that is neither rgb nor bgr.
Fix. Read the tail. Fix the one field it names. The fourcc codes that matter most are MJPG and YUYV.
Prevention. Keep one known-good cameras block and copy it forward. Never retype it from memory.
RealSense on macOS refuses to power up
Error finding RealSense cameras: failed to set power state
Cause. A RealSense camera on macOS fails to reach its power state during discovery. The official cameras page documents this exact string and links the underlying report (librealsense issue 12307).
Fix. Run the same command with sudo, as the official page instructs.
Prevention. The same page is blunt about it: RealSense on macOS is unstable. If you build a data pipeline on it, plan a move to Linux or Windows for the long run.
The camera opens, then the sizes disagree
Error reading frame in background thread for OpenCVCamera(X): OpenCVCamera(X) frame width=640 or height=480 do not match configured width=1920 or height=1080.
Cause. A virtual camera advertises one resolution and delivers another. The official troubleshooting entry names the exact case: OBS Virtual Camera advertising 1920 by 1080 while it actually sends 640 by 480, because the source was rescaled behind the scenes (LeRobot cameras documentation). The check that fires lives in the frame post-processing step of the camera class.
Fix. Recreate the virtual camera source at the resolution you actually stream. The official note says a virtual camera's resolution cannot be changed after creation, so delete the source and make it again. On Linux, confirm what the device really delivers with v4l2-ctl -d /dev/videoX --get-fmt-video. Or leave width and height unset in the camera config and accept what the device sends.
Prevention. Before a recording session, set the canvas and the output resolution to the same numbers, then start the virtual camera. The default stream profile that discovery prints is what the device will be held to.
The camera opens, then delivers nothing
OpenCVCamera(1) failed to capture frames during warmup.
Timed out waiting for frame from camera OpenCVCamera(0) after 5000 ms. Read thread alive: True.
Cause. The device opened but no frames arrive. USB bandwidth is the usual suspect: two or more cameras sharing one controller, or an uncompressed stream at a resolution the link cannot carry. The vendor course for the SO-101 arm puts numbers on it: with the MJPG format the setup carries three cameras at 1920 by 1080 at 30 fps, while the uncompressed YUYV format cannot, and depth cameras are heavier still (Seeed chapter 12).
Fix. Request compressed frames with fourcc MJPG. Drop to 640 by 480. Move cameras to separate direct ports. Replug the camera: the same course lists unplug and reconnect as the fix when depth camera frames time out.
Prevention. Treat the USB bus as a budget. Compressed format by default. Direct ports, no hubs. Modest resolutions: 640 by 480 at 30 fps is enough for most manipulation tasks, and the official cameras page says exactly that.
The backend knob you almost never touch
The config field backend takes a code from the list LeRobot ships: ANY is 0, V4L2 is 200, DSHOW is 700, AVFOUNDATION is 1200, MSMF is 1400 (configs.py). The default, ANY, lets OpenCV pick per platform. The time to override it is Windows: the tracker record cited above ends with the camera detected but not opened, and the fix direction there was the backend used to open the device, carried into the project as a per-platform backend switch (issue 1456). Forcing the DirectShow code, 700, is the lever when the default path will not open a camera that discovery can see. A value that is not one of the codes is rejected with the backend message above.
Family two: the camera opens, the keys do not match
A camera key is a name you chose
Nothing discovers a camera key. You type it. It is the name on the left of your cameras config, and the dataset stores each frame under observation.images. plus that name:
lerobot-teleoperate \
--robot.type=so101_follower \
--robot.cameras='{
front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30},
side: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30}
}'
Record with that block and your dataset carries observation.images.front and observation.images.side. The vendor course that uses this pattern states its half of the rule in one line: "The index_or_path values come from the last digit of each camera ID reported by lerobot-find-cameras opencv" (Seeed chapter 12). The names are the page's own gloss on top of that, not the vendor's wording.
A camera key is not discovered. It is the name you typed on the left of your cameras config, and the dataset stores it under observation.images plus that name.
The policy wants at least one camera
ValueError: SmolVLA requires at least one camera.
Cause. The policy validated its inputs and found no visual feature at all. The check sits in the policy code itself (modeling_smolvla.py). Two neighbors in the same validator name nearby mistakes. SmolVLA requires all nonvisual inputs and only known camera features. fires when a key is unknown to the checkpoint. SmolVLA requires RGB camera inputs: {name}. fires when a feature exists but is not an RGB image.
Fix. Give the run at least one camera, and give it the keys the checkpoint knows. Evaluating without cameras on purpose is a different path: that is the empty camera setting below.
Prevention. Carry the same cameras block from recording into evaluation. The key set at eval time should be the key set the dataset was recorded with.
The batch has none of the expected images
ValueError: All image features are missing from the batch. At least one expected. (batch: dict_keys(['observation.state', 'action'])) (image_features:['observation.images.top'])
Cause. The keys exist, just not under the names this policy expects. The message is helpful on purpose: it prints what the batch carries and what the policy wanted. A common source is a checkpoint trained on other key names, or a dataset recorded with other names.
Fix. Bridge the names instead of editing data. The rename map is a command line dictionary from source key to policy key, and the official page shows it end to end (rename map documentation):
--rename_map='{"observation.images.front": "observation.images.image", "observation.images.eagle": "observation.images.image2", "observation.images.glove": "observation.images.image3"}'
It works on train, eval, and rollout commands, for PI0, PI0.5, PI0Fast, SmolVLA, and XVLA. The same page offers the alternative: edit the policy config.json so its keys match your data, at the cost of tying that checkpoint to one dataset.
Prevention. Choose key names once. Use them in every recording, every training run, every eval. A rename map is a bridge, not a habit.
One byte of detail that saves an hour: SmolVLA prints this message with no space after image_features: while PI0.5 prints the same sentence with a space (modeling_pi05.py). If you search your logs for one form and find nothing, try the other.
The dataset predates the current namespace
Older LeRobot datasets carry images under pixels.top. The current namespace is observation.images.top. The processor maps old forms to new on read: pixels.{cam} or observation.pixels.{cam} becomes observation.images.{cam} (observation_processor.py, the mapping lines). When a tool reads raw features and skips that mapping, the mismatch lands as the missing-features error above. For the older normalization and calibration migrations there is a backward compatibility page; the pixels namespace mapping lives in the processor source, not there.
Fix. The rename map handles it, or you re-record. Inspect first. The edit tool prints a dataset's full feature list, with the command and flags straight from its own documentation (lerobot_edit_dataset.py):
lerobot-edit-dataset \
--repo_id YOUR_USER/YOUR_DATASET \
--operation.type info \
--operation.show_features true
Fewer cameras than the policy expects
Some checkpoints want more views than your robot has. The official answer is the empty cameras setting: --policy.empty_cameras=1 adds a placeholder feature named observation.images.empty_camera_0, and the policy masks it out instead of failing on it (rename map documentation). The same page is explicit that masked placeholders are the intended path for PI0, PI0.5, PI0Fast, SmolVLA, and XVLA. Go past the budget and the error names the overshoot, in the strictest form in the repo:
ValueError: Missing camera features ['observation.images.wrist'] in batch; at most empty_cameras=0 may be absent.
Cause. More configured views are absent than the empty camera budget allows.
Fix. Raise the budget to cover the absent views, or supply the missing camera.
Prevention. Record with the number of views you will deploy with. A policy that masked a view during training expects to mask it forever.
The ten-minute diagnosis
Run these in order. Each one takes minutes and needs no GPU.
First, lerobot-find-cameras opencv. Read the table. Open the images in outputs/captured_images and confirm which index is which view. If the table is empty, work the discovery causes above.
Second, put the fresh indices into your cameras block. Use direct ports. Request MJPG. Set width and height to the default profile the discovery output printed, or leave them unset.
Third, teleoperate with --display_data=true and watch both views live. This catches framing and lighting problems while the arm is still safe.
Fourth, record two episodes. Then run the dataset info command and read the feature list. You want observation.images.<name> entries, one per camera, under the exact names you chose.
Fifth, match the policy side. Compare the policy's expected keys against the feature list. Where they differ, pass a rename map, or set empty cameras for views you do not have.
Sixth, the sanity run. Train a small ACT policy on the dataset. It is cheap and forgiving. If it cannot learn the task either, suspect the data before the big model.
Camera indices are not stable across reboots and re-plugs. Camera key names are stable, because you chose them. Trust names, not indices.
Prevention: the two laws
The index law. Discovery before every session. A fresh index into the config each time. Pin devices by path or serial where the platform allows it. Direct ports, compressed frames, modest resolutions.
The name law. Pick camera names once, at project start. The same names go into record, train, eval, and rollout. The dataset, the stats, and the checkpoint all carry them. Rename nothing between steps. If a name must change, the rename map is the tool, and it stays on the command line where it is visible.
After any LeRobot upgrade, re-check the second family before blaming hardware. Namespace changes are quiet. The error they raise looks like a camera failure and is not one.
When the policy runs somewhere else
The camera must sit on the machine that drives the robot. The policy compute does not have to. That split is the normal shape of a small robot project: capture local, compute anywhere. This site carries model pages for the policies named on this page, SmolVLA, PI0.5, and ACT, each stating its status with a date. What any metered route costs lives in one place, the pricing guide, never in prose. If a first metered call is your next step, you can sign up.
Common questions
Why does find_cameras see my camera but record cannot open it? Two commands, two moments. The index moved between them, or another program grabbed the device after discovery. Re-run discovery right before the run, close other camera apps, and use the index the fresh table prints. On Windows, if discovery still lists the camera and opening keeps failing, the backend used to open the device is the next thing to change.
What is the difference between a camera index and a camera key? The index is a number the operating system hands out, and it changes. The key is a name you chose, and it does not. The index goes into index_or_path. The key becomes observation.images.<name> in your dataset.
My error mentions pixels instead of observation.images. What changed? Your dataset predates the current namespace. The processor maps pixels.{cam} to observation.images.{cam} on read. Tools that read raw features skip that mapping. Inspect with the dataset info command, then bridge with a rename map.
How do I use a checkpoint whose key names differ from my dataset? Pass a rename map on the command line. It maps source keys to policy keys for train, eval, and rollout. Supported policies are PI0, PI0.5, PI0Fast, SmolVLA, and XVLA.
Do I need every camera the policy was trained with? No. Set empty cameras to the number of missing views. The policy masks the placeholders. Go past the budget and the error tells you by how much.
The camera opens but frames time out. Is the camera broken? Probably not. It is usually USB bandwidth. Switch to MJPG, drop the resolution, use direct ports, and replug. Depth cameras are the heaviest case: one can occupy three consecutive device nodes.
The short version
LeRobot camera errors come in two families. Family one: the camera never opens. Run discovery, trust its fresh index, pin the device where you can, and check OS camera access. Family two: the camera opens but the keys do not match. The name on the left of your cameras config becomes observation.images.<name>. Hold that name constant from record to eval, bridge old or foreign names with a rename map, and use empty cameras for views you do not have. Every string on this page is quoted from the current source or the official docs, each with its cause, its fix, and its prevention. For the policies themselves, start from the SmolVLA, PI0.5, and ACT pages. Costs live on the pricing guide. You can sign up when you are ready for a first call.
Last verified: 2026-10-26