You asked the camera for color and the stream keeps coming back Y16. Frames arrive, the driver reports no errors, yet every pixel is gray and the format still reads Y16 no matter what your code requests. That combination is the signature of a negotiation that finished without you: an embedded camera module whose driver locked the pixel format when it initialized the sensor mode, and a capture loop asking for formats after the decision closed. Work through the checks below and you will find the formats the driver really advertises, set the format while it still counts, and decide between a mode change and a software debayer.
Why a color sensor streams Y16
On a V4L2 stack the pixel format is negotiated once, at stream start, not continuously while frames flow. The driver looks at the sensor mode it initialized, resolution and bit depth together, checks the format table for that mode, and answers your request with what you asked for or with a safe fallback. For many embedded module drivers that fallback is Y16, the 16-bit grayscale container. Bayer requests the table does not carry for the running mode are dropped without an error, and the stream keeps flowing in grayscale.
Shim drivers that translate between the sensor and V4L2 sharpen the problem: they advertise a very broad range of possible formats and resolutions, while the mode actually running accepts a far narrower set. The same lock keeps a pixel format from changing after the stream opens on any stack. The same family of failure returns Mono8 when your encoding request falls back, and generic V4L2 calls behave unevenly on these drivers for the same reason, which is why exposure and gain settings get ignored by a sensor driver. Nothing is wrong at the sensor; the negotiation simply ended at Y16.
Separate the display error from the gray image first
If your first failure was a window that never opens, with a message telling you to rebuild the library with GUI support, that is the viewer library missing its windowing backend, not a camera fault. Install the development package that message names and rebuild, or run a console viewer. Until something renders you cannot judge color at all. The gray stream is a second, independent problem, and it survives that fix: the same frames stay gray when you save them to disk. Judging from saved frames is the better test anyway, because it takes the viewer out of the chain entirely.
List the formats the driver really advertises
Enumerate the nodes, then dump each node's format table:
v4l2-ctl --list-devices
v4l2-ctl --list-formats-ext --device /dev/video0
The output is the driver's answer to the kernel's format enumeration, VIDIOC_ENUM_FMT: every fourcc and frame size the node accepts. A module often registers several /dev/video nodes with different tables, so run the listing on each and note which entries cover which sizes. On a color sensor, finding Y16 next to packed 10-bit Bayer entries such as BA10 confirms both formats the decisions below hinge on are in the driver. Decode any unknown pixelformat warnings in dmesg before trusting the names, as the FourCC decoder guide shows.
Treat the table as an advertisement, not a promise. A translation driver can list far more than the running mode will ever deliver, and the delivered buffer size is what tells the two apart.
Set the format before the stream starts
Set the format between opening the device and starting the stream, the ordering the format-negotiation startup guide walks through and the same ordering a USB camera rejecting modes needs. A request made inside the capture loop arrives after the driver has closed the negotiation, which is exactly the pattern that produces this failure: change the format field, re-run, still Y16. Test with v4l2-ctl alone. Request a Bayer format such as BA10 at your mode's size, then read the format and the buffer size back. If the format reads back as requested but the size matches Y16, the driver downgraded the request silently.
Worked example: a 2.3 MP Full HD color module that answers Y16
Inputs from your case: a 2.3 MP color global-shutter module running Full HD at 1920 × 1080 px, a requested RGB format at 3 bytes per pixel, and the driver's answer, Y16 at 2 bytes per pixel.
The rule: the delivered buffer measures width × height × bytes per pixel, and the driver reports that size together with the format it actually chose. Comparing the number with your request separates a misapplied request from a replaced one.
Pixels per frame: 1920 × 1080 = 2,073,600 px. Y16: 2,073,600 px × 2 B/px = 4,147,200 B ≈ 4.1 MB. RGB888: 2,073,600 px × 3 B/px = 6,220,800 B ≈ 6.2 MB.
Result: you request RGB and the delivered buffer measures 4,147,200 B, exactly the Y16 size. The driver replaced the request; had it applied RGB, the buffer would measure 6,220,800 B. As long as the size stays at 4,147,200 B whatever fourcc you send, no code inside the capture loop can change the format, and the mode-table work below is the fix. If any request comes back with a different size, the negotiation works and the original code had an ordering bug.
Decision rule: at Full HD, a delivered 4,147,200 B means Y16 was delivered, regardless of the fourcc in your request.
When every mode answers Y16
With ordering right, two cases remain. If other sensor modes carry Bayer entries in their tables, switch modes: sensor mode and format are one package, so a mode change can deliver BA10 or an RGB format directly. If every mode's table answers Y16 only, the color has one place left to hide — inside the 16-bit words themselves.
Test it in one frame. Aim the camera at a solid red card and capture. A true monochrome path returns uniform gray. A Bayer path returns a fine two-by-two brightness mosaic, lighter pixels in two of four positions repeated across the whole frame. Seeing the mosaic means the raw Bayer data is present and a software RGB conversion recovers full color; the guide on proving you have the raw Bayer stream gives further checks. Uniform gray means the driver dropped the color, and no conversion restores data that is no longer there — the mirror image of a Bayer format decoding to a blank preview. If the module's driver package ships an updated mode table, applying it is the vendor-documented fix; re-run --list-formats-ext afterwards to confirm the table grew.
Weigh the two fixes before committing. Software debayering costs CPU time on every frame, and small embedded cores may not hold the full frame rate with it, while a mode with native Bayer formats removes that cost entirely.
The formats side by side
| Format | What it carries | Bytes per pixel | 1920 × 1080 frame | Without processing |
|---|---|---|---|---|
| Mono8 | 8-bit mono | 1 | 2,073,600 B ≈ 2.1 MB | true grayscale |
| BA10 | packed 10-bit Bayer | 1.25 | 2,592,000 B ≈ 2.6 MB | gray-looking mosaic until debayered |
| Y16 | 16-bit mono | 2 | 4,147,200 B ≈ 4.1 MB | true grayscale, or Bayer in disguise |
| RGB888 | packed 24-bit color | 3 | 6,220,800 B ≈ 6.2 MB | color as captured |
The trap sits in rows two and three: both look gray on screen, but only one still contains the color. Byte arithmetic, not eyeballing, tells them apart, which is why the delivered buffer size is the diagnostic that matters.
Prevent it on the next project
Check a module driver's per-mode format tables before you design around the module, and run a board-camera evaluation with a color target in view. A gray image caught during evaluation costs an afternoon; caught in production it costs a redesign. Where the color guarantee matters more than the footprint, standard machine-vision interfaces are the sturdier path: GigE and USB3 cameras expose pixel formats through a standard control layer, and a GigE camera showing gray almost always has a default pixel format to blame rather than a locked driver table. Browse the area scan cameras and board-level cameras categories, use the camera finder, or start at the camera step in the camera configurator.
The five checks, in order
- Save one frame and confirm the gray image is real; fix a viewer-library GUI error separately if that is what first stopped you.
- Run v4l2-ctl --list-devices, then --list-formats-ext on every node the module registers, noting the fourcc list and sizes per node.
- Set the format between open and stream start: request a Bayer format such as BA10 and read the format back.
- Compare the delivered buffer size with the table above; 4,147,200 B at Full HD proves Y16 was delivered whatever you requested.
- If every mode still answers Y16, run the red-card test: a mosaic means debayer in software, uniform gray means change the mode, the driver package or the camera.
Other format and driver failures across desktop and embedded stacks are gathered on the systems topic page.
When the usual answer is wrong
Reinstalling the capture library does nothing, because the negotiation lives in the kernel driver's mode table, not in user space. Blaming the sensor is premature, because a color sensor streaming uniform Y16 is almost always negotiated that way, and the red-card test settles it in one frame. Post-hoc Y16-to-RGB conversion works only when the words still carry Bayer, so run the mosaic test before writing a converter. And a gray-looking preview is not a verdict: BA10 and Y16 both display gray until processed, so judge by delivered bytes and by the mosaic, never by the preview alone.
The decision, per case
- The mode table lists Bayer entries and your request applies: done — set the format at init and the color returns.
- The table lists Bayer but requests are ignored: an ordering bug — set the format between open and stream start.
- Every mode answers Y16 and the red card shows a mosaic: keep Y16 and debayer in software, budgeting CPU for it.
- Every mode answers Y16 and the card is uniform gray: apply the updated driver or mode table if one exists, or move the application to a camera with standard format negotiation.
If you are selecting hardware rather than debugging it, the camera configurator narrows the field, and the contact page reaches the applications team for a second opinion.
Questions
Why does a color camera stream grayscale when the sensor itself is color?
The driver answered the format request with its fallback instead of a Bayer format, and for many module drivers that fallback is Y16, a 16-bit grayscale container. The color was discarded during negotiation, not at the sensor. Prove it by size: at Full HD a Y16 frame is 4,147,200 bytes, so a delivered buffer of that size confirms the downgrade. Note that a viewer can also impose its own default format at open, overwriting what your code agreed with the driver.
What does the Y16 pixel format mean in V4L2?
Y16 is a FourCC, a four-character code the kernel uses to identify formats, standing for 16-bit luma with no color plane attached. It costs 2 bytes per pixel, so a 1920 × 1080 frame occupies 4,147,200 bytes. Some module drivers use Y16-shaped buffers to carry unpacked Bayer data, which is why a mosaic test on a solid color target shows whether color remains recoverable inside the words.
Does v4l2-ctl --list-formats-ext show what the sensor can really output?
It shows what the driver advertises for that node, not a guarantee for the mode currently running. Translation drivers for embedded modules advertise a broad generic list while the active sensor mode accepts a narrower set. Treat the entries as candidates: request one, read the format and the delivered buffer size back, and let the size confirm or refute each entry. Re-listing after a mode change also tells you whether the table is per-mode or fixed.
Can Y16 frames be converted to color after capture?
Only when the 16-bit words still carry the raw Bayer pattern, which the red-card mosaic test reveals. If the words hold 10-bit values left-aligned in 16 bits, shift each value right by 6 before the debayer to get an 8-bit working range. A uniform gray frame means the color was dropped in the driver, and no conversion can restore what is no longer in the data.
Why does changing the format inside the capture loop do nothing?
V4L2 negotiation closes when the stream starts: the format is part of the sensor mode the driver initialized, so requests made while frames flow arrive too late and are ignored. Set the format between opening the device and starting the stream. Reopening the device starts negotiation again from the driver's default, which is why the format set belongs in the open sequence rather than as a one-time fix.