System designHow-to

Capture raw Bayer RG10 frames from six V4L2 cameras with v4l2-ctl

Capture raw Bayer RG10 frames from six V4L2 cameras with v4l2-ctl: the command, the 4,147,200-byte file check, and fixes for busy nodes and format refusals.

LumaSpec Vision8 min read Share on LinkedIn

On this page 9 sections
  1. 01What raw Bayer is, and what RG10 means
  2. 02What you need
  3. 03Ask the driver which Bayer code it offers
  4. 04Capture one raw frame from each camera
  5. 05Worked example: one RG10 frame from a 1920 × 1080 camera
  6. 06Verify the bytes are really Bayer
  7. 07Common failures and fixes
  8. 08Which cameras produce V4L2 nodes at all
  9. 09Questions

Six cameras come up as /dev/video0 through /dev/video5 on an embedded Linux board, and you want one untouched frame from each: the raw Bayer values as the sensor delivered them, before demosaicing, white balance or sharpening. v4l2-ctl, the control tool in the v4l-utils package, does this from the command line in minutes. This how-to gives you the exact capture command, the byte count that proves a complete RG10 frame from a 1920 × 1080 sensor, and fixes for the two failures you will meet first: a node held by another process, and a pixel format the driver refuses. It closes with the camera transports that produce V4L2 nodes at all, which is the decision to get right before the next hardware order.

What raw Bayer is, and what RG10 means

Each photosite on a color sensor sits under one red, green or blue filter in a repeating pattern — RGGB on the sensors that report RG10. Raw capture writes one 10-bit value per photosite in that pattern, with no demosaicing, white balance, gamma or noise reduction applied. All of that color processing happens later, in your pipeline, where you control it.

RG10 is the four-character code for that layout: 10-bit Bayer in RGRG/GBGB rows, each sample carried in a 16-bit word with the six high bits zero-filled, as the V4L2 pixel format documentation defines the 10-bit Bayer family. On a board with a hardware ISP the distinction matters: one video node can hand you ISP-processed RGB while another hands sensor values, and CSI camera with no ISP: what raw sensor output means explains that split. Boards with an ISP driver in the path still allow raw grabs; the BeagleY-AI setup that captures raw IMX219 frames is a worked reference configuration.

What you need

A Debian-class board with the six nodes up, root or video-group permission on them, and v4l-utils, which ships v4l2-ctl:

sudo apt install v4l-utils

Confirm the nodes exist before anything else:

ls /dev/video*

Six entries should print, /dev/video0 through /dev/video5. If fewer turn up, multiple USB cameras on Linux: stop device path conflicts covers the enumeration side.

Ask the driver which Bayer code it offers

Pick the code from the driver, never from memory or a second-hand list:

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

A node that offers the unpacked 10-bit format prints a line like this:

[0]: 'RG10' (10-bit Bayer RGRG/GBGB)
    Size: Discrete 1920x1080

Read that listing with two checks. The arrangement description must match the sensor's color-filter layout on its data sheet, because the same 10-bit depth ships in several arrangements under separate codes. And 1920x1080 must appear under that code, or --set-fmt-video will negotiate a resolution you did not ask for. When a driver log prints format codes you cannot place, V4L2 unknown pixelformat warnings in dmesg: FourCC decoded walks the decoding.

Capture one raw frame from each camera

  1. Free every node first. A preview or streaming daemon holding a node open makes v4l2-ctl fail with a device-busy error; a quick file-usage check (fuser on the node path) names the holder. Stop that service, then capture — freeing the node is part of the job, not cleanup.

  2. Grab one test frame from the first node:

    v4l2-ctl -d /dev/video0 --set-fmt-video=width=1920,height=1080,pixelformat='RG10' --stream-mmap --stream-count=1 --stream-to=camera_0.raw
    

    Flag by flag: --set-fmt-video sets width, height and the four-character code in one call; --stream-mmap streams through kernel-mapped buffers, the dependable route for a raw dump; --stream-count=1 stops after exactly one frame; --stream-to sends the bytes to the named file.

  3. Weigh the result: ls -l camera_0.raw must read 4,147,200 bytes on a 1920 × 1080 RG10 sensor. The worked example below derives that number; the same check guards every other camera.

  4. Repeat for the remaining nodes, one filename per camera:

    v4l2-ctl -d /dev/video1 --set-fmt-video=width=1920,height=1080,pixelformat='RG10' --stream-mmap --stream-count=1 --stream-to=camera_1.raw
    v4l2-ctl -d /dev/video2 --set-fmt-video=width=1920,height=1080,pixelformat='RG10' --stream-mmap --stream-count=1 --stream-to=camera_2.raw
    v4l2-ctl -d /dev/video3 --set-fmt-video=width=1920,height=1080,pixelformat='RG10' --stream-mmap --stream-count=1 --stream-to=camera_3.raw
    v4l2-ctl -d /dev/video4 --set-fmt-video=width=1920,height=1080,pixelformat='RG10' --stream-mmap --stream-count=1 --stream-to=camera_4.raw
    v4l2-ctl -d /dev/video5 --set-fmt-video=width=1920,height=1080,pixelformat='RG10' --stream-mmap --stream-count=1 --stream-to=camera_5.raw
    

    A shell loop over the node numbers 0 to 5 collapses this to one line; the six explicit commands skip a round of loop debugging.

  5. Set exposure and gain on the node as driver controls before you capture, so all six files share one response to the scene. Raw capture skips the ISP's color processing, not the sensor's timing. When a set value seems to do nothing, V4L2 exposure and gain settings ignored by a sensor driver lists the usual causes.

  6. Mind the timing: these six frames land seconds apart, not on one instant. If the sensors must see the same moment, fire one trigger pulse and grab per node — Hardware trigger grab loop in Python: acquire and save each frame shows the triggered side.

Worked example: one RG10 frame from a 1920 × 1080 camera

Inputs: width 1920 px and height 1080 px, the sensor resolution this rig runs; two bytes per pixel, because RG10 carries each 10-bit sample inside a 16-bit word (kernel pixel format documentation, cited above).

Rule in words: an unpacked raw frame file stores one value per photosite, so its size is width × height × bytes per sample.

bytes = width × height × 2 4,147,200 B = 1920 × 1080 × 2 B

Arithmetic: 1920 × 1080 = 2,073,600 photosites; 2,073,600 × 2 B = 4,147,200 B, about 4.15 MB (3.96 MiB).

Result and decision: each of the six files must weigh exactly 4,147,200 bytes — confirm with ls -l camera_*.raw. A file of exactly 2,073,600 bytes, half the target, means the driver substituted an 8-bit format: the fourcc did not take, and the data is not the 10-bit depth you intended. Any size in between means a truncated stream, most often a node that was reopened mid-capture. Six files at full size, and you hold six complete raw frames ready for your demosaic.

Verify the bytes are really Bayer

Import camera_0.raw as a 16-bit grey image, 1920 × 1080, little-endian. Pixel values should run 0 to 1023 — ten bits, never 4095 or 65535 — and the undemosaiced picture reads as a fine dark weave rather than a photo. A hex editor shows the 4,147,200-byte run. If the file opens as finished color, the node handed you ISP output, not raw; Bayer raw stream from an RGB camera: how to know you have it gives the tells and the checks.

Common failures and fixes

  • Device busy on one node. A daemon still holds it: stop the service and rerun that camera. Inside containers the holder is usually the device passthrough — Camera in Docker: pass V4L2 media nodes, not just /dev/video0 shows the node and media-controller passing that fixes it.
  • The node refuses RG10 with an invalid-argument error. Take the code --list-formats-ext actually prints for your sensor's Bayer arrangement and use it unchanged in the capture command.
  • The file comes up short. Exactly half size is an 8-bit substitution; anything else is truncation from a node reopened mid-stream. Stop the other user, recapture, re-weigh.
  • Format accepted, capture then fails anyway. Some sensor drivers stream more lines than the visible height — data and blanking lines — and the requested frame must cover them; raw capture fails when frame height is undersized is the reference case.
  • Five nodes instead of six. Re-scan after checking power and cabling; at this count, enumeration drops are most often a supply or cable fault, not a driver one. If the cabling is suspect, the cables range covers the camera-side connection hardware.

Which cameras produce V4L2 nodes at all

Plain v4l2-ctl works on what the kernel exposes as capture nodes: CSI camera modules with a V4L2 driver — the usual case on boards like this — and UVC-class USB devices. Machine-vision cameras speak USB3 Vision and GigE Vision instead: the kernel sees the transport, frames arrive through a GenICam stack (vendor SDK or GenICam library), and that stack carries its own pixel-format control for the same raw Bayer data. No /dev/video node for an industrial camera means that transport, not a fault.

If you are specifying hardware for an embedded multi-camera build, the board-level cameras range is where those builds start: its USB3 modules carry raw Bayer over a standard USB3 connection, with the capture stack supplied by the camera's SDK. The three modules below cover the usual shutter and resolution picks:

Board-level camerasHikrobot MV-CB060-10UM-B 6 MP Board Level Camera, USB3, IMX178, Mono, Bare Board, V4MV-CB060-10UM-B
Sensor format
1/1.8"
Interface
USB3
$239.00View part
Board-level cameras3.1 MP USB3 board-level camera, IMX252, 1/1.8" global shutter, 120 fps (LS-BCM-0003)LS-BCM-0003
Sensor format
1/1.8"
Lens mount
C-mount
$690.00View part
Board-level camerasUSB3.0 board camera / module, 1.3 MP, Color (LS-BCM-0047)LS-BCM-0047
Sensor format
1/2.7"
Lens mount
C-mount
$179.00View part

When the build wants a lens-mounted industrial body instead of a bare board, the C-mount side of the area scan cameras range covers it:

Area scan camerasHikrobot MV-CT0MU0060/R/NN/NN 6 MP Area Scan Camera, 3rd Gen., USB3.0, Mono, BASEMV-CT0MU0060/R/NN/NN
Sensor format
1/1.8"
Lens mount
C-mount
Interface
USB3
Resolution
6-10 MP
$969.00View partBuild a system with it

For the full selection criteria — mount, shutter type, sensor size, interface — board level camera selection for embedded vision machines works them through, and the board camera evaluation checklist turns a data sheet into a bench test. The camera finder filters by interface, shutter and sensor format.

Build the whole station

The configurator matches a camera, lens, light, light controller and cables to each other, step by step, and lists only parts that fit what you already picked.

Start with the camera

Capture first, weigh every file, and build the demosaic and measurement pipeline on the frames you have verified. For capture-side reading beyond this workflow, the systems topic page collects the camera and capture guides, and the project configurator specs a complete station.

Questions

Why is my RG10 file 4,147,200 bytes instead of 2,073,600?

RG10 stores every 10-bit sample inside a 16-bit word, so a 1920 × 1080 frame holds 2,073,600 photosites × 2 bytes = 4,147,200 bytes. A file of exactly 2,073,600 bytes means the driver wrote one byte per pixel, an 8-bit format, so the code you set did not take. Re-run --list-formats-ext and copy the code it prints.

v4l2-ctl rejects RG10 — where does the correct code come from?

From the node itself. --list-formats-ext prints every fourcc the driver supports with its resolutions; pick the code whose Bayer arrangement — RGGB, GRBG, GBRG or BGGR — matches the sensor's color-filter layout on its data sheet, then use that code unchanged in --set-fmt-video. If only packed 10-bit formats are listed, the unpacked 16-bit container is not offered and your reader must unpack the stream itself.

Do exposure and gain settings still apply to a raw capture?

Yes. Raw bypasses the ISP's color processing, not the sensor's timing: exposure time and analog gain remain driver controls applied as each frame is read out. Set them on the same node before streaming so all six files share one response to the lighting; otherwise cameras viewing one scene produce files of different brightness and your comparisons wander.

Can all six cameras capture on the same instant?

Not with a sequential loop: each v4l2-ctl run opens, streams and closes one node, so frames land seconds apart. For a shared instant, wire the six sensors to one trigger pulse, run one capture process per node that waits for it, and read out six frames taken together. Single frames never stress bandwidth; continuous simultaneous streaming is where the host controller becomes the limit.