System designTroubleshooting

Dark frames from cv2.VideoCapture: the wrong video node

Dark frames that still stream from cv2.VideoCapture usually mean the wrong /dev/video node. List your nodes with v4l2-ctl, open the capture node, then pin it.

LumaSpec Vision10 min read Share on LinkedIn

On this page 9 sections
  1. 01The decision: you opened a node, not your camera
  2. 02Why one camera owns several /dev/video nodes
  3. 03Find the node that carries pixels
  4. 04Worked example: two cameras, four nodes
  5. 05Other causes of black frames, in order
  6. 06Make the fix survive the next reboot
  7. 07The production answer: address the camera, not an index
  8. 08Checklist before you blame the camera
  9. 09Questions

The camera works in every app on the machine, but your Python script shows a black window: cv2.VideoCapture(0) opens, frames arrive on schedule, and every one of them is black. On Linux this is rarely a broken camera. A UVC device registers several /dev/video nodes — one per function — and the integer you pass to cv2.VideoCapture selects a node by position, not by purpose. This guide walks the diagnosis: enumerate the nodes, find the one that reports pixel formats, open that index, then make the assignment survive the next reboot. It closes with the choice that removes the problem on a production station: a camera addressed by IP address or serial number, where node order does not exist.

The decision: you opened a node, not your camera

When cv2.VideoCapture(0) opens without an error and hands you uniformly black frames while a desktop viewer shows a live image, the camera, the cable and the USB link are fine. The index selected a node that carries no pixels from your camera — a metadata sibling, or another camera entirely. Fix it by list, not by guess: run v4l2-ctl --list-devices, take the node printed under your camera's name, confirm it with a format query, and pass its position as the index.

The arithmetic explains why 0 keeps failing. Two UVC cameras on a bus register four nodes, and the second camera's capture node typically sits at /dev/video2 — so cv2.VideoCapture(2) is the answer far more often than chance suggests. The neighbouring failure modes are covered separately: when the node exists but your application shows nothing at all, read USB camera at /dev/video0 but no video in your app UI; when the camera never enumerated in the first place, start at UVC camera not detected: identify the device node first.

Why one camera owns several /dev/video nodes

Video4Linux2 gives every function of a device its own character node. For a UVC camera the driver registers a pair: a capture node that lists pixel formats and streams, and a metadata node that carries timestamps and per-frame flags. This is long-standing behaviour — kernel bug 199575, "V4L2 issues two video devices to each camera", describes the /dev/video0 and /dev/video1 pair exactly. Only one node of the pair captures: in one documented case, a USB camera with ID 046d:0893 appeared as /dev/video8 and /dev/video9, and only /dev/video8 delivered frames.

nodes = 2 × uvc_cameras capture node of camera n ≈ node 2 × (n − 1) index = position of that node in the list your OpenCV build opens

The capture node is usually the lower number of its pair; confirm with a format query instead of assuming. Embedded boards multiply the effect: a MIPI CSI-2 sensor uses the same V4L2 interface and can expose several nodes for one sensor, and its media pipeline must be routed before any of them produce frames — the media-ctl pipeline setup guide covers that path. Board-level cameras on SoCs hit it most often.

Find the node that carries pixels

Four commands, about a minute:

  1. Confirm the camera is on the bus: lsusb.
  2. Count the nodes: ls /dev/video*. Two per UVC camera is normal; a bigger count means another camera or a CSI pipeline is in the list.
  3. Map nodes to names: v4l2-ctl --list-devices. Each camera prints with the nodes it owns.
  4. Query a node: v4l2-ctl -d /dev/video2 --list-formats-ext. The capture node answers with pixel formats such as YUYV 4:2:2 or MJPEG and their frame sizes; a metadata node answers with an empty list.

Illustrative output from a two-camera host:

$ v4l2-ctl --list-devices
HD Camera (usb-0000:00:14.0-2):
        /dev/video0
        /dev/video1

USB 2.0 Camera (usb-0000:00:14.0-4):
        /dev/video2
        /dev/video3

Then open the node that answered:

import cv2

capture_index = 2   # int: /dev/video2 is the node that listed formats
cap = cv2.VideoCapture(capture_index)

capture_index is the illustrative variable here — set it to whichever node answered step 4 on your machine. The test that proves the fix: point the camera at a lamp. The preview window shows the lamp within about a second; a wrong node stays uniformly black whatever the light does.

One behaviour to know: current OpenCV builds check for capture capability and skip nodes without it, so the index counts only usable nodes; older builds hand the index to whatever node it lands on, metadata included, which is where error-free black frames come from. Measure the number once per machine with the two queries above; do not carry it between machines or OpenCV builds.

Worked example: two cameras, four nodes

Take a host with one internal camera and one USB inspection camera. ls /dev/video* prints:

/dev/video0 /dev/video1 /dev/video2 /dev/video3

The arithmetic: 2 cameras × 2 nodes each = 4 nodes, numbered 0 through 3. The internal camera took the first pair at boot, the USB camera the second, so the capture node you want is /dev/video2 and the index on this boot is 2.

Node Owner Function What cv2.VideoCapture returns
/dev/video0 internal camera capture the internal camera's image, not yours
/dev/video1 internal camera metadata an open failure, or black frames
/dev/video2 USB inspection camera capture the image you want
/dev/video3 USB inspection camera metadata an open failure, or black frames

On a three-node machine with this same symptom, ls /dev/video* printed /dev/video0 through /dev/video2, only /dev/video2 answered the format query, and cv2.VideoCapture(2) showed a live image where index 0 had been black. One line changed and the problem closed.

Note what an OpenCV upgrade does to the table: if the build skips metadata nodes, the indices compress to 0 → /dev/video0 and 1 → /dev/video2, and your camera moves from index 2 to index 1 without any hardware change. That is the failure the next section prevents.

Other causes of black frames, in order

The wrong node is the most common cause, not the only one. Work down the table:

Likelihood Cause Quick test Fix
High index opened a metadata node format query lists nothing open the node that answers
High index opened another camera — an IR sensor streams valid, genuinely dark frames camera name in --list-devices; light changes nothing pick the node under the right name
Medium format negotiation paired the stream badly image improves when one format is forced USB camera black image on Linux: fix UVC format negotiation
Medium a container hides the sibling nodes works on the host, black in Docker Camera in Docker: pass V4L2 media nodes, not just /dev/video0
Low device permissions the open fails rather than going black Linux camera device permissions
Low CSI pipeline not routed node exists, no formats, no frames BBAI-64 CSI camera video0 is not a capture device

Two neighbouring reads: when the controls query comes back empty, v4l2-ctl list-ctrls returns nothing: controls live on the right node applies the same node logic to camera settings; when you need to script discovery across buses, enumerate cameras on USB and Firewire buses in one list builds that list.

Make the fix survive the next reboot

Node numbers are not stable: the ordering of the /dev/video devices decides which camera an index receives, and issue 3003, "Webcam ordering is not persistent in Linux", tracks the behaviour. Three levels of durability, in order of effort:

  1. Derive the index at startup. Run v4l2-ctl --list-devices, take the node under your camera's name, confirm it with the format query, and convert it to the index in code. Never store the number in a config file.
  2. Use the stable path where a path is accepted. udev also exposes each node under /dev/v4l2/by-id/, with the camera's identity in the symlink name, and v4l2-ctl -d plus GStreamer pipelines take that path directly. cv2.VideoCapture keeps its integer index, so use this for diagnostics and for non-OpenCV tools.
  3. Remove the node list from the design — next section.

The production answer: address the camera, not an index

For a station that boots unattended, UVC index selection is the wrong foundation. Industrial cameras answer to an identity, not a position. A GigE camera is opened by its IP address through its SDK — OpenCV's VideoCapture cannot open one at all, and why OpenCV VideoCapture cannot open a GigE Vision camera plus opening a GigE camera on a link-local address by IP from Linux walk that path. A machine-vision USB3 camera is enumerated by the vendor SDK or a GenTL producer, which lists cameras by serial number, so node order never enters the question.

Two cameras from the catalog make the point. The LS-CMR-0053 is a 12 MP GigE area scan camera on a 1/1.7" sensor. The LS-CMR-0166 is a 12 MP USB3 area scan camera that runs at 30 fps on the same 1/1.7" format. Neither is opened by node position, so what /dev/video0 happens to be at boot never enters the question.

Area scan cameras12 MP USB3 area scan camera, IMX226, 1/1.7" rolling shutter, 30 fps, 4000 × 3000 resolution (LS-CMR-0166)LS-CMR-0166
Sensor format
1/1.7"
Interface
USB3
$560.00View part
Area scan cameras12 MP GigE area scan camera, IMX226, 1/1.7" rolling shutter, 9 fps (LS-CMR-0053)LS-CMR-0053
Sensor format
1/1.7"
Lens mount
C-mount
$560.00View part

Give a USB camera on a moving fixture a link built for motion: the LS-CBL-0219 is a 3 m USB3.0 camera cable rated for drag-chain use.

Cables3 m USB3.0 camera cable, high-flex (drag chain) (LS-CBL-0219)LS-CBL-0219$62.00View partBuild a system with it

Checklist before you blame the camera

  • lsusb lists the camera, and a desktop viewer shows a live image: hardware, cable and USB link are fine, and the fault is in node selection.
  • ls /dev/video* prints about two nodes per UVC camera; a surprising count means another camera or a CSI pipeline is in the list.
  • v4l2-ctl --list-devices maps every node to a camera name before you trust any index.
  • v4l2-ctl -d /dev/video2 --list-formats-ext answers with YUYV or MJPEG frame sizes on the capture node only.
  • The lamp test: a bright target appears in the preview within about a second of opening the right index.
  • The index is derived at startup on every boot, never carried over from a previous one.
  • An unattended station addresses its cameras by IP address or serial number, and a camera on a mover uses a drag-chain-rated USB3 cable.

When the diagnosis points to hardware instead, the area scan camera finder and the system configurator narrow the choice, the systems topic page collects the Linux-side guides, and the contact page reaches us for a specific set-up.

Questions

Why does one USB camera create two /dev/video nodes?

V4L2 creates one node per function, and the UVC driver registers a capture node plus a metadata node for each camera, so one camera occupies a pair such as /dev/video0 and /dev/video1. Only the capture node answers a format query with pixel formats like YUYV or MJPEG. The count grows by two for every camera you plug in, and which camera gets the lower pair follows plug and boot order — which is why the numbers move.

Is the cv2.VideoCapture index the same as the /dev/video number?

It matches often, but the index is a position, not a name. Current OpenCV builds skip nodes without capture capability, so the index counts only usable nodes and shifts when the node list changes; older builds hand the index to whatever node it lands on, metadata nodes included, which is one source of error-free black frames. Measure the index with v4l2-ctl --list-formats-ext at startup and treat it as a per-boot value.

Do /dev/video numbers stay the same across reboots?

No. Enumeration order decides the numbering, so any hardware or boot change can reorder it; issue 3003, "Webcam ordering is not persistent in Linux", tracks the behaviour. Derive the index at startup from v4l2-ctl --list-devices, or use the /dev/v4l2/by-id/ path where a tool accepts a path. On an unattended station, choose a camera addressed by IP address or serial number so the question disappears.

My machine has an infrared sensor too — could that be the black image?

Yes. Machines with an infrared face-login sensor expose it as its own capture node, and it streams valid, genuinely dark frames that no scene light will brighten — the node is doing its job. The RGB node sits beside it. Check the camera names in v4l2-ctl --list-devices and open the RGB node; if the name mentions infrared, you are on the wrong sibling.

Does this index problem affect GigE industrial cameras?

No. A GigE Vision camera never enters the UVC node list; it is opened by IP address through its SDK, and OpenCV's VideoCapture cannot open it at all. Machine-vision USB3 cameras are enumerated by the vendor SDK or a GenTL producer, by serial number. So a working industrial camera with no /dev/video node is not missing — it is addressed through a different door.