Is it possible to get USB camera working with SO101 arm through WSL Ubuntu?

Hello,

I just got a SO101 Arm kit, finished building it and managed to get teleop working. I wanted to continue with the tutorials that are recommended before trying to do anything on my own, but I’ve come to a issue with trying to get cameras working.

I have a Logitech C270 usb webcam (wsl passthrough has been enabled), and when running the lerobot-find-cameras opencv command I received this error message. I tried a couple things like changing the format to V4L2, but haven’t gotten this command to work as it does in the example. The only thing I found, but probably isn’t what i’m looking for is `OpenCVCameraConfig` settings fail under `backend=ANY` on Linux; the same settings work with `backend=V4L2` · Issue #3198 · huggingface/lerobot · GitHub

Thank you

Here is the error listed below:

ioctl(VIDIOC_QBUF): Bad file descriptor
[video4linux2,v4l2 @ 0x5b2d2bd784c0] ioctl(VIDIOC_G_INPUT): Inappropriate ioctl for device

-– Detected Cameras —
Camera #0:
Name: OpenCV Camera @ /dev/video0
Type: OpenCV
Id: /dev/video0
Backend api: FFMPEG
Default stream profile:
Format: 0.0
Fourcc:
Width: 640
Height: 480
Fps: 30.0

WARNING:lerobot.cameras.opencv.camera_opencv:Error reading frame in background thread for OpenCVCamera(/dev/video0): OpenCVCamera(/dev/video0) read failed (status=False).
WARNING:lerobot.cameras.opencv.camera_opencv:Error reading frame in background thread for OpenCVCamera(/dev/video0): OpenCVCamera(/dev/video0) read failed (status=False).
ERROR:lerobot.scripts.lerobot_find_cameras:Failed to connect or configure OpenCV camera /dev/video0: Timed out waiting for frame from camera OpenCVCamera(/dev/video0) after 1000 ms. Read thread alive: True.
WARNING:lerobot.cameras.opencv.camera_opencv:Error reading frame in background thread for OpenCVCamera(/dev/video0): OpenCVCamera(/dev/video0) read failed (status=False).
ioctl(VIDIOC_QBUF): Bad file descriptor
WARNING:lerobot.scripts.lerobot_find_cameras:No cameras could be connected. Aborting image save.

Hmm… I suspect asking in the LeRobot Discord would be more reliable.
For now, seems possible, but looks like several known failure modes may be overlapping:


Yes, using a USB webcam through WSL should be possible in principle. However, your log does not yet show a successful video stream. It shows that /dev/video0 can be discovered and at least partly queried, but every frame read returns status=False, followed by a 1000 ms timeout.

The issue you found, backend=ANY failing while backend=V4L2 works, is therefore quite relevant. There is also a very similar LeRobot forum report with:

  • an SO-101 setup,
  • lerobot-find-cameras,
  • Backend api: FFMPEG,
  • the same VIDIOC_QBUF and VIDIOC_G_INPUT messages,
  • successful camera use outside LeRobot,
  • and successful LeRobot capture after explicitly selecting V4L2.

That case was native Ubuntu rather than WSL, though, so I would not assume it has exactly the same root cause. WSL adds an independent USB/IP and UVC layer.

The quickest route is to split the problem into layers before changing LeRobot itself:

Can V4L2 read real frames directly?
├─ No
│  └─ Investigate WSL / usbipd / UVC / device node / camera mode
└─ Yes
   └─ Compare OpenCV CAP_ANY with CAP_V4L2
      ├─ Only CAP_V4L2 works
      │  └─ OpenCV backend selection is the leading candidate
      ├─ Both work
      │  └─ Investigate LeRobot version / finder lifecycle / warm-up
      └─ Neither works
         └─ Investigate the OpenCV build, package conflicts, or device contention

1. First identify the actual capture node and its supported modes

Install the V4L2 tools if necessary:

sudo apt update
sudo apt install v4l-utils

Then inspect every node rather than assuming /dev/video0 is necessarily the usable image stream:

v4l2-ctl --list-devices

for dev in /dev/video*; do
    echo "=== $dev ==="
    v4l2-ctl -d "$dev" --all
    v4l2-ctl -d "$dev" --list-formats-ext
done

For the node you test, look for:

  • Video Capture or Video Capture Multiplanar
  • Streaming
  • an advertised combination of pixel format, resolution, and frame rate

One physical UVC webcam can expose multiple /dev/video* nodes. Some may be image-capture nodes, while others can be metadata or other logical interfaces. The kernel’s VIDIOC_QUERYCAP documentation explains the relevant capability flags.

It may also help to distinguish these terms:

Setting Examples Meaning
Capture backend/API V4L2, FFMPEG The route OpenCV uses to access the device
Pixel format / FOURCC MJPG, YUYV The image representation sent by the camera
Stream mode 640×480 at 30 fps Resolution and frame rate

So if you “changed the format to V4L2,” the exact place where you changed it matters: V4L2 is normally the backend/API, whereas MJPG or YUYV would be the pixel format.

2. Test the exact advertised mode without LeRobot

Choose one combination that actually appears in --list-formats-ext.

For example, only if the camera advertises MJPG at 640×480 and 30 fps:

v4l2-ctl -d /dev/video0 \
    --set-fmt-video=width=640,height=480,pixelformat=MJPG \
    --set-parm=30 \
    --stream-mmap \
    --stream-count=30 \
    --stream-to=/dev/null \
    --verbose

For another advertised mode, replace the node, FOURCC, size, and frame rate accordingly:

v4l2-ctl -d <capture-node> \
    --set-fmt-video=width=<advertised-width>,height=<advertised-height>,pixelformat=<advertised-fourcc> \
    --set-parm=<advertised-fps> \
    --stream-mmap \
    --stream-count=30 \
    --stream-to=/dev/null \
    --verbose

If possible, run this in another terminal at the same time:

sudo dmesg -w

That can reveal whether capture triggers a USB reset, UVC timeout, disconnect, or USB/IP/VHCI error.

How to interpret this test

  • If this direct V4L2 test fails, the problem is below LeRobot. Changing the LeRobot camera configuration is unlikely to fix it yet.
  • If it reads 30 frames successfully, the WSL-to-V4L2 path is basically functioning, and the next useful comparison is the OpenCV backend.

3. Compare OpenCV’s automatic backend with explicit V4L2

This keeps the device and Python environment the same and changes only the OpenCV backend:

import cv2

device = "/dev/video0"

for name, backend in (
    ("ANY", cv2.CAP_ANY),
    ("V4L2", cv2.CAP_V4L2),
):
    cap = cv2.VideoCapture(device, backend)

    print(f"\n{name}")
    print("opened:", cap.isOpened())

    if cap.isOpened():
        print("actual backend:", cap.getBackendName())
        print(
            "reported mode:",
            int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
            int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),
            cap.get(cv2.CAP_PROP_FPS),
        )

    first_frame = None

    for attempt in range(30):
        ok, frame = cap.read()
        if ok and frame is not None:
            first_frame = {
                "attempt": attempt,
                "shape": frame.shape,
            }
            break

    print("first valid frame:", first_frame)
    cap.release()

OpenCV allows selecting a capture backend explicitly, and getBackendName() reports which backend was actually opened.

The most informative outcomes are:

  • CAP_ANY selects FFMPEG and fails, while CAP_V4L2 succeeds
    This would make the backend path the strongest explanation and would closely resemble issue #3198 and the similar HF Forum case.

  • Both backends succeed
    The camera, WSL, V4L2, and basic OpenCV path are working. The remaining difference is likely in lerobot-find-cameras, its version, initialization timing, or device lifecycle.

  • Both fail, although v4l2-ctl succeeds
    Check how OpenCV was built and whether multiple OpenCV packages are installed.

  • The direct V4L2 test also fails
    Stay on the WSL/UVC branch rather than modifying LeRobot.

If explicit V4L2 works, the corresponding LeRobot camera configuration would be along these lines, using a mode that the camera actually advertised:

from lerobot.cameras.configs import Cv2Backends
from lerobot.cameras.opencv.configuration_opencv import OpenCVCameraConfig

camera_config = OpenCVCameraConfig(
    index_or_path="/dev/video0",
    width=640,
    height=480,
    fps=30,
    fourcc="MJPG",
    backend=Cv2Backends.V4L2,
)

Again, MJPG, 640×480, and 30 fps are only examples; they should match the output from v4l2-ctl --list-formats-ext.

Why the issue you found may be relevant

LeRobot supports an explicit OpenCV backend in OpenCVCameraConfig. In the LeRobot v0.6.0 configuration source, however, the default is still:

backend: Cv2Backends = Cv2Backends.ANY

The v0.6.0 lerobot-find-cameras implementation constructs an OpenCVCameraConfig for each detected camera without passing an explicit backend. Therefore, the finder can still use ANY, which can resolve to FFMPEG for a Linux /dev/video* path—as your output shows.

The related PR #3208 proposes automatically selecting V4L2 on Linux for /dev/video* paths. At the time of writing, that PR is still open.

The important scope limitation from issue #3198 is that it does not claim that FFMPEG is universally broken. Its narrower observation is:

  • ANY selected FFMPEG on that machine,
  • requested settings such as MJPG did not apply through that path,
  • and the same settings worked with explicit V4L2.

Your case has one camera rather than two, and WSL introduces another possible failure layer. So this is a strong candidate, not a confirmed diagnosis.

WSL and usbipd checks if direct V4L2 capture fails

Microsoft’s WSL USB documentation describes USB passthrough through usbipd-win. A useful basic state check is:

Windows PowerShell:

wsl --version
usbipd --version
usbipd list

WSL:

uname -r
lsusb
lsusb -t
ls -l /dev/video*

The webcam should appear as Attached in usbipd list and as a Logitech USB device in lsusb.

A few details that are easy to miss:

  • usbipd bind sharing is persistent.
  • usbipd attach --wsl is not permanent across unplugging the device or restarting WSL.
  • While the USB device is attached to WSL, Windows cannot use it.
  • A Windows Camera, Teams, Zoom, browser, or OBS process should therefore not be expected to use the same physical webcam simultaneously.

The official sequence is roughly:

usbipd list
usbipd bind --busid <busid>
usbipd attach --wsl --busid <busid>

Then in WSL:

lsusb

Also check that the normal Linux camera modules exist:

modinfo uvcvideo

lsmod | grep -E 'uvcvideo|videodev|vhci_hcd|usbip'

A custom WSL kernel would not be my first step. First try:

wsl --update
wsl --shutdown

Then re-open WSL and re-attach the camera. A custom kernel becomes relevant only if the installed WSL kernel genuinely lacks a required driver or module.

If the camera node exists but direct streaming fails, the most useful evidence is the V4L2 command output together with the kernel messages produced at the same moment:

sudo dmesg -w
LeRobot version and the camera-finder lifecycle

It is worth recording the exact LeRobot version:

lerobot-info
python -c "import lerobot; print(getattr(lerobot, '__version__', 'unknown'))"

The latest PyPI release at the time of writing is LeRobot 0.6.0, released on July 6, 2026.

A separate camera-finder issue, #3592, identified cases where one physical webcam exposed multiple logical V4L2 nodes and the finder opened them in a way that could cause hardware contention. It also identified a fixed one-second warm-up as insufficient for some cameras.

The corresponding PR #3593 was merged into main on July 29, 2026. It changes the finder to use a sequential lifecycle:

connect -> warm up -> test -> disconnect

and adds a configurable --warmup-s option.

Because that merge happened after the 0.6.0 PyPI release, a normal 0.6.0 installation will not contain that finder change.

This branch becomes relevant mainly if:

  • direct V4L2 streaming works,
  • the simple OpenCV test works,
  • but lerobot-find-cameras still fails.

Trying current main could then test the newer finder lifecycle and a longer warm-up. However, it should not be treated as a complete backend fix: the separate automatic-V4L2 PR, #3208, is still unmerged.

Optional second reader: FFmpeg directly

If the V4L2 result is unclear, FFmpeg can serve as another reader independent of LeRobot and OpenCV’s wrapper.

First list the modes FFmpeg sees:

ffmpeg -f video4linux2 \
    -list_formats all \
    -i /dev/video0

Then select one mode from that list:

ffmpeg \
    -f video4linux2 \
    -input_format <advertised-format> \
    -video_size <advertised-width>x<advertised-height> \
    -framerate <advertised-fps> \
    -i /dev/video0 \
    -frames:v 30 \
    -f null -

The FFmpeg Video4Linux2 documentation notes that V4L2 devices generally support only specific combinations of format, size, and frame rate.

This gives another useful split:

  • v4l2-ctl and FFmpeg both fail: likely below OpenCV/LeRobot.
  • v4l2-ctl works but FFmpeg fails: inspect FFmpeg’s format negotiation.
  • FFmpeg works but OpenCV with the FFMPEG backend fails: the OpenCV wrapper/build path becomes more interesting.
Lower-priority environment checks

These are worth checking only after the main split tests:

python -m pip list | grep -i opencv

python - <<'PY'
import cv2
print("OpenCV:", cv2.__version__)
print(cv2.getBuildInformation())
PY

The official opencv-python packaging repository recommends installing only one of:

  • opencv-python
  • opencv-contrib-python
  • opencv-python-headless
  • opencv-contrib-python-headless

They all provide the same cv2 namespace, so multiple variants in one environment can produce an ambiguous installation.

Also check whether another Linux process has the node open:

fuser -v /dev/video*
lsof /dev/video* 2>/dev/null

And check permissions:

ls -l /dev/video*
id

For deeper OpenCV diagnostics:

OPENCV_LOG_LEVEL=DEBUG \
OPENCV_VIDEOIO_DEBUG=1 \
python opencv_camera_probe.py

Set those variables before Python imports cv2.

I would keep these below the direct V4L2 and backend tests, because the current log already shows that OpenCV can discover and partially query the device.

A useful information bundle for Discord or a GitHub issue

If none of the branches resolves it, this would be a strong evidence bundle for the official LeRobot Discord or a GitHub issue:

Windows version:
wsl --version:
usbipd --version:
usbipd list:

uname -r:
LeRobot version:
OpenCV version:
OpenCV build information:

lsusb:
lsusb -t:
v4l2-ctl --list-devices:

For every /dev/video* node:
  v4l2-ctl --all
  v4l2-ctl --list-formats-ext

Exact node tested:
Exact FOURCC:
Exact resolution:
Exact frame rate:

Direct v4l2-ctl streaming result:
OpenCV CAP_ANY result and actual backend:
OpenCV CAP_V4L2 result and actual backend:
Kernel messages during the failed capture:

Exact location/configuration where "V4L2" was selected:

That should let someone distinguish a WSL/USB-IP problem from a UVC/V4L2 problem, an OpenCV backend problem, and a LeRobot finder problem without repeating the whole setup process.

So my default route would be:

  1. Find the node with Video Capture and Streaming.
  2. Select one mode actually advertised by the C270.
  3. Confirm 30 frames with v4l2-ctl.
  4. If that works, compare CAP_ANY and CAP_V4L2.
  5. Only then change the LeRobot camera backend or investigate the newer finder lifecycle.

That should narrow the problem substantially before any source modification or custom WSL kernel work.

Hi,

I went through your checklist, and everything seemed to work. when I got to step 3 I got the result:
**CAP_ANY selects FFMPEG and fails, while CAP_V4L2 succeeds
**
But I’m not sure what you mean by:
If explicit V4L2 works, the corresponding LeRobot camera configuration would be along these lines, using a mode that the camera actually advertised:

from lerobot.cameras.configs import Cv2Backends
from lerobot.cameras.opencv.configuration_opencv import OpenCVCameraConfig

camera_config = OpenCVCameraConfig(
    index_or_path="/dev/video0",
    width=640,
    height=480,
    fps=30,
    fourcc="MJPG",
    backend=Cv2Backends.V4L2,
)

I should mention that when I did change backend=cv2.V4L2 it seemed to give less errors when running the lerobot-find-cameras opencv command, and the image was saved. That image however was glitchy on the top half and compeletly green on the bottom.

I know you said it should work in theory, but would it be best to turn my laptop into a dualboot? Most of the videos I’ve seen where people started with WSL ended up moving to linux.

Edit:
Found this review/tutorial on the arms: https://www.youtube.com/watch?v=59JTCvpG_Ec (6:49)
and they mentioned they also had issues with getting cameras working, which they ended up just switching to a native linux OS.

Thanks for narrowing it down. With that symptom, WSL may still be viable. Based on that result, there is actually an option I had not mentioned yet, because it is fairly edge-case. If this still does not work, I would lean toward native Linux or the LeRobot Discord:


I would not switch to dual boot solely because CAP_ANY failed.

Your result establishes an important difference:

CAP_ANY  -> FFMPEG -> fails
CAP_V4L2 -> V4L2  -> returns a frame

That means WSL, USB/IP, UVC, and V4L2 are working far enough to return image data. It does not prove that the whole path is stable—the green/corrupted image still matters—but it makes “USB cameras simply do not work in WSL” much less likely.

It also makes the backend selected by CAP_ANY the clearest confirmed difference. This is closely related to LeRobot issue #3198, although the corrupted frame may be a second problem rather than part of the same one.

There are now two small tests that should decide whether WSL remains worth using:

  1. Make the unmodified LeRobot finder choose V4L2 without falling back to FFMPEG.
  2. Check whether a standalone CAP_V4L2 capture produces normal images after several warm-up frames.

Try forcing the finder through OpenCV’s runtime backend registry

LeRobot v0.6.0 creates its finder camera configuration with the default backend, ANY. Since OpenCV then selects FFMPEG on your system, one narrow workaround is to change OpenCV’s backend priority only for this command:

OPENCV_VIDEOIO_PRIORITY_LIST=V4L2 \
OPENCV_VIDEOIO_PRIORITY_FFMPEG=0 \
OPENCV_VIDEOIO_DEBUG=1 \
OPENCV_LOG_LEVEL=INFO \
lerobot-find-cameras opencv

This does four useful things:

  • places V4L2 first in OpenCV’s runtime backend list;
  • disables the FFMPEG video-I/O backend for this process;
  • leaves the LeRobot source unmodified;
  • enables enough logging to check which path was attempted.

The variables are attached only to this command, so they do not permanently change your shell or system configuration.

Before running it, you can optionally confirm that your OpenCV build exposes a V4L2 stream backend:

python - <<'PY'
import cv2

print(
    [
        cv2.videoio_registry.getBackendName(backend)
        for backend in cv2.videoio_registry.getStreamBackends()
    ]
)
PY

You should see V4L2 in the output.

OpenCV documents OPENCV_VIDEOIO_PRIORITY_LIST and the generic OPENCV_VIDEOIO_PRIORITY_<backend> controls in its environment-variable reference.

Interpret that result first

V4L2-limited finder saves a normal image
└─ WSL is probably usable for this setup.
   Use explicit V4L2 in the real robot/recording camera configuration.

V4L2-limited finder still saves a green/corrupted image
└─ Save several images with standalone CAP_V4L2.
   ├─ Standalone images are normal
   │  └─ The remaining problem is probably in the finder/config/lifecycle path.
   └─ Standalone images are also corrupted
      └─ The remaining problem is probably the selected camera mode
         or the WSL/usbipd/UVC transfer path.

Check whether the green image is produced below LeRobot

Your report says that CAP_V4L2 “succeeds,” but it is not clear whether that means only that read() returned True, or that the resulting image was visually correct.

A frame can have a valid array shape and still contain incomplete or incorrectly decoded image data.

Use one combination that the camera actually advertises through:

v4l2-ctl -d /dev/video0 --list-formats-ext

Then save multiple frames after discarding the initial frames:

import time
import cv2

device = "/dev/video0"

cap = cv2.VideoCapture(device, cv2.CAP_V4L2)

if not cap.isOpened():
    raise RuntimeError("Could not open the camera with CAP_V4L2")

# Replace these only with a combination shown by:
# v4l2-ctl -d /dev/video0 --list-formats-ext
fourcc = cv2.VideoWriter_fourcc(*"<advertised-fourcc>")
cap.set(cv2.CAP_PROP_FOURCC, fourcc)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, <advertised-width>)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, <advertised-height>)
cap.set(cv2.CAP_PROP_FPS, <advertised-fps>)

print("backend:", cap.getBackendName())
print(
    "actual mode:",
    int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
    int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),
    cap.get(cv2.CAP_PROP_FPS),
)

# Give the camera time to initialize and discard early frames.
for _ in range(30):
    cap.read()

for i in range(5):
    ok, frame = cap.read()
    print(i, ok, None if frame is None else frame.shape)

    if ok and frame is not None:
        cv2.imwrite(f"v4l2_probe_{i}.png", frame)

    time.sleep(0.1)

cap.release()

Do not assume that MJPG at 640×480 and 30 fps is valid just because it is common for the C270. Use the exact FOURCC, resolution, and frame rate reported by your device.

The outcomes are fairly informative:

Observation More likely direction
All five standalone images are normal LeRobot finder/configuration/lifecycle
Only the first images are corrupted Camera initialization or warm-up
Every standalone image has the same corruption Mode negotiation, pixel format, or decoding path
Corruption changes from frame to frame Incomplete or unstable frame transfer becomes more plausible
v4l2-ctl and standalone OpenCV both produce corruption Below LeRobot; WSL/usbipd/UVC or camera mode
Only the LeRobot-saved image is corrupt LeRobot finder/configuration path

The appearance alone—glitches on top and green on the bottom—is not enough to identify the cause. It can occur when a frame is incomplete, when a compressed frame is damaged, or when the producer and consumer disagree about the format or frame layout.

Why this runtime workaround applies to the finder

The earlier configuration example:

camera_config = OpenCVCameraConfig(...)

was not intended as a standalone script that changes lerobot-find-cameras.

OpenCVCameraConfig is a configuration object that can be inserted into the actual robot, teleoperation, or recording configuration.

In LeRobot v0.6.0’s OpenCV configuration, the default is:

backend: Cv2Backends = Cv2Backends.ANY

The v0.6.0 finder implementation creates its test camera roughly like this:

OpenCVCameraConfig(
    index_or_path=cam_id,
    color_mode=ColorMode.RGB,
)

It does not supply a backend there, so the default remains ANY.

That explains why changing a separate camera_config object does not automatically change the finder.

The runtime environment variables operate one layer lower: they change the backend registry that OpenCV consults when LeRobot asks for ANY.

The related automatic-V4L2 PR #3208 proposes making LeRobot choose V4L2 automatically for Linux /dev/video* paths. At the time of writing, it is still open, so the current finder should not be assumed to contain that behavior.

Also, if backend=cv2.V4L2 was literal code rather than shorthand, the standard names are:

# Direct OpenCV:
cv2.CAP_V4L2

# LeRobot configuration:
Cv2Backends.V4L2
Using explicit V4L2 in the actual SO-101 configuration

Once V4L2 produces normal images, the explicit backend belongs in the camera configuration used by the robot—not in a separate unused variable.

A Python configuration would look approximately like this:

from lerobot.cameras.configs import Cv2Backends
from lerobot.cameras.opencv import OpenCVCameraConfig
from lerobot.robots.so_follower import SO101FollowerConfig

robot_config = SO101FollowerConfig(
    port="<follower-port>",
    id="<follower-id>",
    cameras={
        "front": OpenCVCameraConfig(
            index_or_path="/dev/video0",
            width=<advertised-width>,
            height=<advertised-height>,
            fps=<advertised-fps>,
            fourcc="<advertised-fourcc>",
            backend=Cv2Backends.V4L2,
        )
    },
)

The official LeRobot examples similarly place OpenCVCameraConfig inside the cameras dictionary of SO101FollowerConfig. See the real-world robot guide and camera documentation.

For CLI-based teleoperation or recording, the same camera is normally supplied through --robot.cameras. The exact accepted representation of the backend can be version-sensitive, so I would first confirm the working Python configuration or inspect:

lerobot-teleoperate --help
lerobot-record --help

before copying a numeric enum into a long CLI command.

The important distinction is:

lerobot-find-cameras
    Discovery/test utility; v0.6.0 internally uses backend=ANY.

SO101FollowerConfig / --robot.cameras
    Configuration used for actual teleoperation and recording;
    this is where explicit V4L2 belongs.
If only the finder remains unreliable

A separate finder problem may still be relevant if:

  • standalone CAP_V4L2 images are normal;
  • the runtime backend override selects V4L2;
  • but the finder image is still corrupted or intermittent.

PR #3593 was merged into LeRobot main on July 29, 2026. It changes camera discovery to a sequential lifecycle:

connect -> warm up -> test -> disconnect

It also adds a configurable --warmup-s argument.

That fix was merged after the LeRobot 0.6.0 release, so a normal 0.6.0 installation will not include it.

If the standalone images become normal only after some discarded frames, testing current main with a longer warm-up becomes reasonable:

OPENCV_VIDEOIO_PRIORITY_LIST=V4L2 \
OPENCV_VIDEOIO_PRIORITY_FFMPEG=0 \
lerobot-find-cameras opencv --warmup-s 5

That command assumes a version containing PR #3593. Check first:

lerobot-find-cameras --help

and verify that --warmup-s exists.

This newer finder lifecycle and the automatic-V4L2 work are separate changes. PR #3593 is merged, while PR #3208 is still open. Updating to main therefore should not be described as automatically fixing the backend selection.

When native Linux becomes the sensible choice

I would lean toward native Linux if the corruption is also visible in direct, explicit V4L2 capture—especially if it persists across more than one low-load mode advertised by the camera.

For example:

  • standalone CAP_V4L2 images are repeatedly corrupted;
  • v4l2-ctl or FFmpeg shows the same corruption;
  • reducing resolution and frame rate does not help;
  • multiple advertised FOURCC modes fail;
  • dmesg reports USB resets, UVC timeouts, or VHCI/USB-IP errors;
  • or you plan to add multiple cameras and do not gain much from retaining the USB/IP layer.

At that point, native Linux removes an entire transport and virtualization boundary:

Windows
  -> usbipd
    -> USB/IP
      -> WSL virtual kernel
        -> UVC/V4L2
          -> OpenCV
            -> LeRobot

Native Linux does not guarantee that every camera configuration will work, but it removes the Windows-to-WSL USB forwarding layer and usually makes low-level camera diagnosis more direct.

Conversely, WSL remains a reasonable option if:

  • standalone V4L2 images are normal;
  • the runtime backend override makes the finder normal;
  • or the remaining problem is limited to LeRobot’s finder warm-up/configuration.

So I would use the standalone five-image probe as the boundary:

Standalone CAP_V4L2 images normal
    -> keep investigating the LeRobot side in WSL.

Standalone CAP_V4L2 images consistently corrupted
    -> native Linux or the LeRobot Discord is probably the better use of time.

In other words: your result has not ruled WSL out. It has shown that FFMPEG selected through CAP_ANY is the wrong path on this setup, while explicit V4L2 gets substantially farther.

I would try the command-scoped V4L2-only finder once, then compare its output with several standalone CAP_V4L2 images. If both paths still produce corrupted frames, moving to native Linux is a very reasonable decision rather than continuing indefinitely through increasingly obscure WSL camera edge cases.

I should also mention that I was able to get a python script running with my usb webcam, and that worked great. I think there are some things that are automatically applied to the usb webcam when the lerobot-find-cameras opencv is entered

here is that script for reference

import cv2

camera = cv2.VideoCapture(0, cv2.CAP_V4L2)

camera.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'MJPG'))
camera.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

if not camera.isOpened():
    print("Could not open webcam")
    exit()

print("Press q to quit")

while True:
    ret, frame = camera.read()

    if not ret:
        print("Failed to grab frame")
        break

    cv2.imshow("USB Webcam", frame)

    if cv2.waitKey(1) & 0xFF == ord("q"):
        break

camera.release()
cv2.destroyAllWindows()

Ah. If so, that narrows the candidates down quite a lot:


That standalone script is strong evidence because it does not merely open the camera or obtain one frame—it continuously displays valid frames under the current WSL environment.

The known-working combination is:

backend: V4L2
FOURCC:  MJPG
size:    640x480
FPS:     not explicitly overridden

So a general WSL → usbipd → UVC/V4L2 failure now looks much less likely. The narrower remaining difference is probably how lerobot-find-cameras opens and configures the camera.

In LeRobot v0.6.0, discovery opens each Linux device approximately as:

cv2.VideoCapture(target)

That means no explicit V4L2 backend is supplied while discovering the default profile. The normal OpenCVCameraConfig defaults are also:

backend = ANY
fourcc = None

Therefore, forcing only V4L2 may still not reproduce your successful script: MJPG and 640×480 are also part of the known-working condition.

The cleanest next test would be to bypass the finder and test LeRobot’s own OpenCVCamera class with that exact combination:

import cv2

from lerobot.cameras.configs import Cv2Backends
from lerobot.cameras.opencv.camera_opencv import OpenCVCamera
from lerobot.cameras.opencv.configuration_opencv import OpenCVCameraConfig

config = OpenCVCameraConfig(
    index_or_path="/dev/video0",
    width=640,
    height=480,
    fourcc="MJPG",
    backend=Cv2Backends.V4L2,
)

camera = OpenCVCamera(config)

try:
    camera.connect()

    capture = camera.videocapture
    print("backend:", capture.getBackendName())

    fourcc_value = int(capture.get(cv2.CAP_PROP_FOURCC))
    actual_fourcc = "".join(
        chr((fourcc_value >> (8 * i)) & 0xFF) for i in range(4)
    )
    print("FOURCC:", actual_fourcc)
    print(
        "mode:",
        int(capture.get(cv2.CAP_PROP_FRAME_WIDTH)),
        int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT)),
        capture.get(cv2.CAP_PROP_FPS),
    )

    for i in range(5):
        frame = camera.read()
        print(i, frame.shape)

        # OpenCVCamera returns RGB by default; OpenCV writes BGR.
        cv2.imwrite(
            f"lerobot_v4l2_{i}.png",
            cv2.cvtColor(frame, cv2.COLOR_RGB2BGR),
        )
finally:
    camera.disconnect()

I would initially leave FPS unspecified, since your working script does not override it.

The result should distinguish the remaining cases fairly cleanly:

Minimal LeRobot OpenCVCamera produces normal images
└─ WSL is viable.
   The problem is mostly the finder/default-profile path.
   Reuse V4L2 + MJPG + 640x480 in the actual SO-101 camera config.

Raw OpenCV works, but minimal LeRobot OpenCVCamera is corrupted
└─ The difference is inside LeRobot's configuration or camera lifecycle,
   rather than a general WSL webcam failure.

Both explicit paths work, but lerobot-find-cameras does not
└─ Treat the finder as the problematic utility rather than as a requirement
   that must succeed before teleoperation or recording can work.

This also makes issue #3198 look more directly relevant. In that report, requested MJPG and resolution settings failed to apply under the default ANY backend path, while the same settings worked with explicit V4L2.

I would still avoid calling it the identical root cause, but the next useful test is now quite narrow: reproduce the already-working V4L2 + MJPG + 640×480 tuple inside LeRobot itself.

If that succeeds, I do not think native Linux is required merely because lerobot-find-cameras chooses a bad default path. If that exact tuple still fails inside LeRobot, the minimal raw-OpenCV-versus-LeRobot pair would also be an excellent compact reproduction to bring to the Discord or a GitHub issue.

HI,

I found this link: Lerobot Camera Backend Issues
and originally tried their solution with changing

self.videocapture = cv2.VideoCapture(self.index_or_path, self.backend)

to

self.videocapture = cv2.VideoCapture(self.index_or_path, cv2.CAP_V4L2)

which helped a tiny bit? some of the red errors were not there. I then undid that back to

self.videocapture = cv2.VideoCapture(self.index_or_path, self.backend)

I then tried downgrading my opencv to 4.12.0.88, and then ran the lerobot-find-cameras opencv command giving me the following output.

--- Detected Cameras ---
Camera #0:
  Name: OpenCV Camera @ /dev/video0
  Type: OpenCV
  Id: /dev/video0
  Backend api: V4L2
  Default stream profile:
     Format: 0.0
     Fourcc: YUYV
     Width: 640
     Height: 480
     Fps: 30.0

ERROR:lerobot.scripts.lerobot_find_cameras:Failed to connect or configure OpenCV camera /dev/video0: Timed out waiting for frame from camera OpenCVCamera(/dev/video0) after 1000 ms. Read thread alive: True.WARNING:lerobot.scripts.lerobot_find_cameras:No cameras could be connected. Aborting image save.

Oh. In that case, there are a couple of details that stand out:


The downgrade result is useful evidence rather than just a workaround:

OpenCV 4.13 -> CAP_ANY selected FFMPEG
OpenCV 4.12 -> CAP_ANY selected V4L2

OpenCV 4.13’s VideoIO changes include new support for opening camera devices through FFmpeg, so a version-dependent change in automatic backend selection is plausible. I would not call it a confirmed OpenCV regression from this alone, especially because the changelog specifically mentions opening by device index, but the version boundary is worth recording.

However, the new log also shows that changing the backend did not yet reproduce the known-working camera mode:

lerobot-find-cameras:
V4L2 + YUYV + 640x480 + 30 fps -> timeout

working standalone script:
V4L2 + MJPG + 640x480, with no explicit FPS override -> normal video

So it looks as though the downgrade removed one problem—the unexpected FFMPEG path—but left a second difference: the finder is still accepting the camera’s YUYV/default profile instead of the MJPG profile that worked.

That makes issue #3198 look even more relevant: its important point was not simply “V4L2 works,” but that the requested FOURCC and mode were actually applied through the V4L2 path.

I would therefore treat OpenCV 4.12 as a useful diagnostic condition, not necessarily the final fix. The next clean comparison remains an explicit LeRobot camera configuration using the already validated tuple:

backend=Cv2Backends.V4L2
fourcc="MJPG"
width=640
height=480

I would initially leave FPS unspecified, since the working standalone script did not override it.

If that explicit LeRobot configuration works, then WSL itself is probably fine and the narrow problem is the finder’s automatic backend/profile selection. If it still fails while the equivalent raw OpenCV script works, the remaining difference is inside LeRobot’s camera configuration or lifecycle rather than the WSL camera transport in general.

within the lerobot_find_cameras.py file, I set fourcc=“MJPG” and had my opencv libraries 4.12. this seemed to work sort of? this was the result from running that command. It still shows “YUYV” as the format, so not sure if thats just a UI thing or if its still setting it as YUYV. Additionally the image it saves seems to be correct, but clearly there is some data that is corrupt in that image.

I was also wondering if there is a site that shows all of the packages that lerobot requires and their recommended versions?


--- Detected Cameras ---
Camera #0:
  Name: OpenCV Camera @ /dev/video0
  Type: OpenCV
  Id: /dev/video0
  Backend api: V4L2
  Default stream profile:
    Format: 0.0
    Fourcc: YUYV
    Width: 640
    Height: 480
    Fps: 30.0
--------------------
Corrupt JPEG data: 3 extraneous bytes before marker 0xd1
Corrupt JPEG data: 4 extraneous bytes before marker 0xd5
Corrupt JPEG data: 1 extraneous bytes before marker 0xd4
Corrupt JPEG data: 5 extraneous bytes before marker 0xd2
Corrupt JPEG data: 1 extraneous bytes before marker 0xd4
Corrupt JPEG data: 5 extraneous bytes before marker 0xd1
Corrupt JPEG data: 3 extraneous bytes before marker 0xd2
Corrupt JPEG data: 1 extraneous bytes before marker 0xd0
Corrupt JPEG data: 2 extraneous bytes before marker 0xd5
Corrupt JPEG data: 1 extraneous bytes before marker 0xd7
Corrupt JPEG data: 1 extraneous bytes before marker 0xd6
Corrupt JPEG data: 4 extraneous bytes before marker 0xd3
Corrupt JPEG data: 4 extraneous bytes before marker 0xd2
Corrupt JPEG data: 4 extraneous bytes before marker 0xd4
Corrupt JPEG data: 4 extraneous bytes before marker 0xd2
Corrupt JPEG data: 3 extraneous bytes before marker 0xd3
Corrupt JPEG data: 2 extraneous bytes before marker 0xd6
Corrupt JPEG data: 2 extraneous bytes before marker 0xd4
Corrupt JPEG data: 1 extraneous bytes before marker 0xd1
Corrupt JPEG data: 1 extraneous bytes before marker 0xd2
Corrupt JPEG data: 2 extraneous bytes before marker 0xd7
Corrupt JPEG data: 2 extraneous bytes before marker 0xd0
Corrupt JPEG data: 1 extraneous bytes before marker 0xd6
Corrupt JPEG data: 5 extraneous bytes before marker 0xd7
Corrupt JPEG data: 2 extraneous bytes before marker 0xd6
Corrupt JPEG data: 1 extraneous bytes before marker 0xd2
Corrupt JPEG data: 4 extraneous bytes before marker 0xd6
Corrupt JPEG data: 5 extraneous bytes before marker 0xd2
Corrupt JPEG data: 1 extraneous bytes before marker 0xd5
Corrupt JPEG data: 2 extraneous bytes before marker 0xd2
Corrupt JPEG data: 2 extraneous bytes before marker 0xd0
Corrupt JPEG data: 1 extraneous bytes before marker 0xd7
Corrupt JPEG data: 1 extraneous bytes before marker 0xd2
Corrupt JPEG data: 2 extraneous bytes before marker 0xd2
Corrupt JPEG data: 2 extraneous bytes before marker 0xd0
Corrupt JPEG data: 8 extraneous bytes before marker 0xd7
Corrupt JPEG data: 2 extraneous bytes before marker 0xd7
Corrupt JPEG data: 3 extraneous bytes before marker 0xd4
Corrupt JPEG data: 1 extraneous bytes before marker 0xd3
Corrupt JPEG data: 11 extraneous bytes before marker 0xd4
Corrupt JPEG data: 13 extraneous bytes before marker 0xd3
Corrupt JPEG data: 4 extraneous bytes before marker 0xd4
Corrupt JPEG data: 3 extraneous bytes before marker 0xd7
Corrupt JPEG data: 1 extraneous bytes before marker 0xd7
Corrupt JPEG data: 1 extraneous bytes before marker 0xd5
Corrupt JPEG data: 1 extraneous bytes before marker 0xd2
Corrupt JPEG data: 4 extraneous bytes before marker 0xd7
Corrupt JPEG data: 3 extraneous bytes before marker 0xd1
Corrupt JPEG data: 4 extraneous bytes before marker 0xd3
Corrupt JPEG data: 1 extraneous bytes before marker 0xd2
Corrupt JPEG data: 4 extraneous bytes before marker 0xd5
Corrupt JPEG data: 13 extraneous bytes before marker 0xd5
Corrupt JPEG data: 1 extraneous bytes before marker 0xd6
Corrupt JPEG data: 4 extraneous bytes before marker 0xd5
Corrupt JPEG data: 1 extraneous bytes before marker 0xd4
Corrupt JPEG data: 3 extraneous bytes before marker 0xd1
Corrupt JPEG data: 2 extraneous bytes before marker 0xd1
Corrupt JPEG data: 3 extraneous bytes before marker 0xd4
Corrupt JPEG data: 4 extraneous bytes before marker 0xd4
Corrupt JPEG data: 2 extraneous bytes before marker 0xd4
Corrupt JPEG data: 4 extraneous bytes before marker 0xd1
Corrupt JPEG data: 5 extraneous bytes before marker 0xd2
Corrupt JPEG data: 1 extraneous bytes before marker 0xd1
Corrupt JPEG data: 3 extraneous bytes before marker 0xd4
Corrupt JPEG data: 5 extraneous bytes before marker 0xd7
Corrupt JPEG data: 4 extraneous bytes before marker 0xd5
Corrupt JPEG data: 2 extraneous bytes before marker 0xd2
Corrupt JPEG data: 4 extraneous bytes before marker 0xd4
Corrupt JPEG data: 4 extraneous bytes before marker 0xd3
Corrupt JPEG data: 2 extraneous bytes before marker 0xd7
Corrupt JPEG data: 5 extraneous bytes before marker 0xd6
Corrupt JPEG data: 12 extraneous bytes before marker 0xd7
Corrupt JPEG data: 4 extraneous bytes before marker 0xd0
Corrupt JPEG data: 2 extraneous bytes before marker 0xd6
Corrupt JPEG data: 1 extraneous bytes before marker 0xd3
Corrupt JPEG data: 4 extraneous bytes before marker 0xd7
Corrupt JPEG data: 1 extraneous bytes before marker 0xd2
Corrupt JPEG data: 4 extraneous bytes before marker 0xd0
Corrupt JPEG data: 1 extraneous bytes before marker 0xd0
Corrupt JPEG data: 2 extraneous bytes before marker 0xd0
Corrupt JPEG data: 9 extraneous bytes before marker 0xd2
Corrupt JPEG data: 2 extraneous bytes before marker 0xd3
Corrupt JPEG data: 1 extraneous bytes before marker 0xd3
Corrupt JPEG data: 4 extraneous bytes before marker 0xd3
Corrupt JPEG data: 1 extraneous bytes before marker 0xd2
Corrupt JPEG data: 2 extraneous bytes before marker 0xd7
Corrupt JPEG data: 5 extraneous bytes before marker 0xd2
Corrupt JPEG data: 2 extraneous bytes before marker 0xd3
Corrupt JPEG data: 7 extraneous bytes before marker 0xd3
Corrupt JPEG data: 1 extraneous bytes before marker 0xd3
Corrupt JPEG data: 1 extraneous bytes before marker 0xd1
Corrupt JPEG data: 2 extraneous bytes before marker 0xd0
Corrupt JPEG data: 2 extraneous bytes before marker 0xd5
Corrupt JPEG data: 7 extraneous bytes before marker 0xd7
Corrupt JPEG data: 1 extraneous bytes before marker 0xd0
Corrupt JPEG data: 5 extraneous bytes before marker 0xd2
Corrupt JPEG data: 4 extraneous bytes before marker 0xd2
Corrupt JPEG data: 9 extraneous bytes before marker 0xd4
Corrupt JPEG data: 1 extraneous bytes before marker 0xd3
Corrupt JPEG data: 8 extraneous bytes before marker 0xd3
Corrupt JPEG data: 1 extraneous bytes before marker 0xd3
Corrupt JPEG data: 2 extraneous bytes before marker 0xd5
Corrupt JPEG data: 3 extraneous bytes before marker 0xd6
Corrupt JPEG data: 5 extraneous bytes before marker 0xd3
Corrupt JPEG data: 1 extraneous bytes before marker 0xd2
Corrupt JPEG data: 5 extraneous bytes before marker 0xd6
Corrupt JPEG data: 3 extraneous bytes before marker 0xd3
Corrupt JPEG data: 1 extraneous bytes before marker 0xd5
Corrupt JPEG data: 3 extraneous bytes before marker 0xd5
Corrupt JPEG data: 4 extraneous bytes before marker 0xd7
Corrupt JPEG data: 1 extraneous bytes before marker 0xd7
Corrupt JPEG data: 2 extraneous bytes before marker 0xd6
Corrupt JPEG data: 1 extraneous bytes before marker 0xd6
Corrupt JPEG data: 9 extraneous bytes before marker 0xd0
Corrupt JPEG data: 3 extraneous bytes before marker 0xd0
Corrupt JPEG data: 1 extraneous bytes before marker 0xd2
Corrupt JPEG data: 4 extraneous bytes before marker 0xd2
Corrupt JPEG data: 3 extraneous bytes before marker 0xd2
Corrupt JPEG data: 2 extraneous bytes before marker 0xd4
Corrupt JPEG data: 1 extraneous bytes before marker 0xd1
Corrupt JPEG data: 1 extraneous bytes before marker 0xd4
Corrupt JPEG data: 9 extraneous bytes before marker 0xd1
Corrupt JPEG data: 1 extraneous bytes before marker 0xd0
Corrupt JPEG data: 2 extraneous bytes before marker 0xd5
Corrupt JPEG data: 3 extraneous bytes before marker 0xd2
Corrupt JPEG data: 2 extraneous bytes before marker 0xd3
Corrupt JPEG data: 2 extraneous bytes before marker 0xd3
Corrupt JPEG data: 1 extraneous bytes before marker 0xd5
Corrupt JPEG data: 1 extraneous bytes before marker 0xd6
Corrupt JPEG data: 1 extraneous bytes before marker 0xd0
Corrupt JPEG data: 5 extraneous bytes before marker 0xd2
Corrupt JPEG data: 8 extraneous bytes before marker 0xd6
Corrupt JPEG data: 8 extraneous bytes before marker 0xd4
Corrupt JPEG data: 2 extraneous bytes before marker 0xd3
Corrupt JPEG data: 1 extraneous bytes before marker 0xd7
Corrupt JPEG data: 2 extraneous bytes before marker 0xd3
Corrupt JPEG data: 3 extraneous bytes before marker 0xd7
Corrupt JPEG data: 3 extraneous bytes before marker 0xd2
Corrupt JPEG data: 1 extraneous bytes before marker 0xd3
Corrupt JPEG data: 5 extraneous bytes before marker 0xd7
Corrupt JPEG data: 6 extraneous bytes before marker 0xd0
Corrupt JPEG data: 3 extraneous bytes before marker 0xd2
Corrupt JPEG data: 2 extraneous bytes before marker 0xd3
Corrupt JPEG data: 2 extraneous bytes before marker 0xd1
Corrupt JPEG data: 1 extraneous bytes before marker 0xd2
Corrupt JPEG data: 1 extraneous bytes before marker 0xd0
Corrupt JPEG data: 1 extraneous bytes before marker 0xd7
Corrupt JPEG data: 5 extraneous bytes before marker 0xd1
Corrupt JPEG data: 1 extraneous bytes before marker 0xd2
Corrupt JPEG data: 1 extraneous bytes before marker 0xd2
Corrupt JPEG data: 3 extraneous bytes before marker 0xd5
Corrupt JPEG data: 1 extraneous bytes before marker 0xd7
Corrupt JPEG data: 1 extraneous bytes before marker 0xd4
Corrupt JPEG data: 3 extraneous bytes before marker 0xd6
Corrupt JPEG data: 6 extraneous bytes before marker 0xd7
Corrupt JPEG data: 1 extraneous bytes before marker 0xd3
Corrupt JPEG data: 2 extraneous bytes before marker 0xd1
Corrupt JPEG data: 1 extraneous bytes before marker 0xd0
Corrupt JPEG data: 2 extraneous bytes before marker 0xd3
Corrupt JPEG data: 3 extraneous bytes before marker 0xd6
Corrupt JPEG data: 1 extraneous bytes before marker 0xd2
Corrupt JPEG data: 3 extraneous bytes before marker 0xd0
Corrupt JPEG data: 1 extraneous bytes before marker 0xd7
Corrupt JPEG data: 2 extraneous bytes before marker 0xd0
Corrupt JPEG data: 1 extraneous bytes before marker 0xd0
Corrupt JPEG data: 1 extraneous bytes before marker 0xd4
Corrupt JPEG data: 4 extraneous bytes before marker 0xd4
Corrupt JPEG data: 3 extraneous bytes before marker 0xd4
Corrupt JPEG data: 7 extraneous bytes before marker 0xd5
Corrupt JPEG data: 3 extraneous bytes before marker 0xd0
Corrupt JPEG data: 4 extraneous bytes before marker 0xd5
Corrupt JPEG data: 4 extraneous bytes before marker 0xd5
Corrupt JPEG data: 1 extraneous bytes before marker 0xd5
Corrupt JPEG data: 2 extraneous bytes before marker 0xd3
Corrupt JPEG data: 1 extraneous bytes before marker 0xd2
Corrupt JPEG data: 5 extraneous bytes before marker 0xd7
Corrupt JPEG data: 2 extraneous bytes before marker 0xd6
Corrupt JPEG data: 1 extraneous bytes before marker 0xd4
Corrupt JPEG data: 4 extraneous bytes before marker 0xd1
Corrupt JPEG data: 2 extraneous bytes before marker 0xd3
Corrupt JPEG data: 4 extraneous bytes before marker 0xd7
Corrupt JPEG data: 9 extraneous bytes before marker 0xd5
Corrupt JPEG data: 2 extraneous bytes before marker 0xd1
Corrupt JPEG data: 1 extraneous bytes before marker 0xd4
Corrupt JPEG data: 7 extraneous bytes before marker 0xd2

Finalizing image saving...
Corrupt JPEG data: 3 extraneous bytes before marker 0xd7
Image capture finished. Images saved to outputs/captured_images

Oh. In that case, I think it is fair to say that the chances of getting this working under WSL have increased quite a bit:


The important result is that LeRobot now:

  • opens the camera through V4L2,
  • captures frames after fourcc="MJPG" was added,
  • and completes image saving with an image that appears visually correct.

That means the main path now appears to be functioning:

WSL
-> usbipd / USB-IP
-> UVC / V4L2
-> OpenCV
-> LeRobot OpenCVCamera
-> saved image

I would no longer treat native Linux as necessary just because lerobot-find-cameras previously failed. The next question is mainly whether this configuration remains stable during actual teleoperation or recording.

Why the output can still say YUYV

The displayed line is probably not reporting the stream used by the later configured camera instance.

In LeRobot v0.6.0’s camera discovery code, the finder first performs an unconfigured discovery pass:

camera = cv2.VideoCapture(target)

It reads and stores that camera’s default profile:

backend
FOURCC
width
height
FPS

and then releases that VideoCapture.

The finder script prints this previously collected default stream profile, then creates a separate OpenCVCamera instance for image capture.

So the two stages are approximately:

Discovery instance
-> opens the unconfigured default profile
-> observes YUYV
-> saves YUYV in discovery metadata
-> releases the camera
-> prints that metadata

Configured capture instance
-> opens the camera again
-> applies fourcc="MJPG"
-> reads frames
-> saves images

Therefore, the printed YUYV is not necessarily evidence that the later MJPG setting failed. It is discovery metadata from the earlier, unconfigured instance.

Also, this line:

Format: 0.0

is not another representation of YUYV or MJPG. OpenCV’s CAP_PROP_FORMAT describes the format/type of the returned Mat; the pixel-format code is reported separately through CAP_PROP_FOURCC.

There are two additional clues that MJPG was probably applied:

  1. LeRobot v0.6.0 reads CAP_PROP_FOURCC back after setting it and normally logs a warning like this if the requested value was not accepted:

    failed to set fourcc=MJPG (...)
    Continuing with default format.
    
  2. The subsequent output is coming from a JPEG decoder:

    Corrupt JPEG data: ... extraneous bytes before marker ...
    

Assuming the posted log includes the relevant warnings, the absence of LeRobot’s “failed to set fourcc” message, combined with the JPEG decoder output, strongly suggests that the configured capture entered the MJPG path.

If you want to verify it directly, print the properties from the configured instance after camera.connect(), rather than from the discovery metadata:

import cv2

capture = camera.videocapture

fourcc_value = int(capture.get(cv2.CAP_PROP_FOURCC))
actual_fourcc = "".join(
    chr((fourcc_value >> (8 * i)) & 0xFF)
    for i in range(4)
)

print("Actual backend:", capture.getBackendName())
print("Actual FOURCC:", actual_fourcc)
print(
    "Actual mode:",
    int(capture.get(cv2.CAP_PROP_FRAME_WIDTH)),
    int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT)),
    capture.get(cv2.CAP_PROP_FPS),
)
How I would interpret the JPEG warnings

The warning text is alarming, but it does not automatically mean that the decoded image is unusable.

The same extraneous bytes before marker warning has previously been reported with a Logitech C270 and OpenCV. Similar libjpeg warnings can occur while the decoder still recovers and returns an apparently normal frame.

I would judge it by observable behavior rather than by the warning count alone:

Images look correct
+ reads continue successfully
+ no growing frame loss or timeouts
+ recording remains stable
------------------------------------------------
The warning may be noisy but non-blocking in this environment.

Visible blocks, missing image regions, color corruption,
intermittent read failures, or repeated recording failures
------------------------------------------------
The warning may correspond to a real capture/transport problem
and should not be ignored.

The most useful next test is therefore not another finder modification. It is a short actual teleoperation or recording run using the known-working configuration:

backend=Cv2Backends.V4L2
fourcc="MJPG"
width=640
height=480

I would initially avoid forcing FPS, since the standalone script that worked did not explicitly set it.

For example:

Teleoperate/record runs for several minutes with normal images
└─ WSL is probably viable for this setup.
   Record the working dependency and camera settings.

Images remain normal but JPEG warnings continue
└─ Check for read failures or dropped frames.
   If capture is stable, preserve the warning in the environment notes
   rather than treating it as the main blocker.

Visible corruption or read failures return
└─ Compare raw OpenCV and LeRobot using the exact same
   V4L2 + MJPG + 640x480 tuple.

I would not suppress the decoder warnings until stability has been checked, because they are currently useful evidence.

Where to find LeRobot’s dependency versions

I do not think there is one page containing a single universally recommended version for every package. There are several different sources, each answering a slightly different question:

Source What it tells you
Installation documentation Normal installation route and optional feature groups
Release tag’s pyproject.toml Declared compatible version ranges
Release tag’s uv.lock Exact versions resolved for the repository’s reproducible development/CI environment
pip freeze or uv pip freeze Versions actually installed in your environment

For LeRobot v0.6.0, pyproject.toml declares:

opencv-python-headless >= 4.9.0, < 4.14.0

So OpenCV 4.12 and 4.13 are both inside the declared dependency range.

However, the v0.6.0 uv.lock resolves:

opencv-python-headless == 4.13.0.92

That means:

  • OpenCV 4.12 is not outside LeRobot v0.6.0’s declared range.
  • OpenCV 4.12 is not the version locked for the repository’s reference development environment.
  • In this particular WSL + C270 environment, 4.12 appears to be a practical compatibility pin because it avoids the unwanted automatic FFMPEG path.

I would record it as an environment-specific working constraint rather than call it the general recommended LeRobot version.

A useful environment record would be:

python --version
python -m pip freeze > lerobot-wsl-working-environment.txt

python - <<'PY'
import cv2
import lerobot

print("LeRobot:", getattr(lerobot, "__version__", "unknown"))
print("OpenCV:", cv2.__version__)
print("cv2 loaded from:", cv2.__file__)
PY
One package-management detail worth checking

Your standalone test used cv2.imshow(). If that test was run in the same Python environment, a GUI-enabled package such as opencv-python may be installed.

LeRobot v0.6.0 declares opencv-python-headless as its base dependency. The official opencv-python packaging documentation warns that these packages all install the same cv2 namespace:

opencv-python
opencv-contrib-python
opencv-python-headless
opencv-contrib-python-headless

Only one variant should normally be installed in an environment.

It is therefore worth checking:

python -m pip list | grep -i opencv
python -m pip check
python -c "import cv2; print(cv2.__version__, cv2.__file__)"

If both opencv-python and opencv-python-headless appear, that does not prove they caused this camera problem, but it makes the effective OpenCV binary and future upgrades harder to reason about.

For a reproducible LeRobot environment, I would either:

  • keep one OpenCV wheel variant in that environment and save test images instead of using cv2.imshow(), or
  • perform GUI-based camera experiments in a separate environment.

I would not change the currently working environment before saving its full pip freeze.

So my current interpretation is:

The YUYV line
    = the default profile observed during the earlier discovery pass.

The later JPEG warnings
    = evidence that the configured capture likely entered the MJPG decode path.

Successful image saving
    = strong evidence that the main WSL camera path can work.

The remaining question
    = stability during actual teleoperation/recording.

If a short real recording works with explicit V4L2 + MJPG + 640×480, I would consider WSL a viable setup here and preserve OpenCV 4.12 plus the camera tuple as part of the project’s environment specification.