Your sensor is 12-bit, your frames arrive in 16-bit buffers, and the histogram matches neither: the brightest pixel sits at 32759, above the 12-bit ceiling of 4095 and far below the 16-bit ceiling of 65535. Nothing is broken. The driver delivered 12-bit counts inside a 16-bit container, left-justified, and your code (a CV_16UC1 frame, for example) is showing you the container, not the sensor's numbers. This guide gives you the alignment rule, the single shift that recovers the true 0–4095 counts, a test that tells padding bits from copied bits, and what a ceiling near 32759 says about your exposure.
The 16-bit frame is a container, not the range
A camera with a 12-bit converter produces counts from 0 to 4095 — 12 bits, full stop. Memory transfers, DMA engines and most image libraries prefer 16-bit words: byte-aligned buffers move cleanly and downstream pipelines want even word sizes. So the driver places the 12 data bits in the top 12 bit positions of each 16-bit word — MSB-aligned, often called left-justified — and fills the bottom four bits with padding. The 16-bit number is therefore the 12-bit count times 16. Reading the container as if it were the measurement range is what makes a healthy image look wrong.
What a pixel value means physically — photons, voltage, gain — is its own subject, kept separate in camera pixel values vs voltage: what USB output means.
SoC and driver memory-format documentation describes the second arrangement as sample data in the most significant bits, with the least significant bits repeated from them. Either way, one right-shift by four returns the sensor's own count.
Why the histogram stops near 32759
Take the number from the opening paragraph: 32759. In hex that is 0x7FF7 — nibbles 7, F, F, 7. The bottom nibble equals the top nibble, the copied-bits signature. Shift right four places: 32759 >> 4 = 2047. Your brightest pixel holds 2047 of 4095 counts, almost exactly half of full scale. The arithmetic closes cleanly: 2047 × 16 = 32752, plus the copied nibble 7 gives 32759 back.
So the ceiling is not a format fault. It is a scene that never reached full well. For measurement work, place the brightest feature you must evaluate at 70–90% of counts — roughly 2900–3700 counts, displayed as 46,400–59,200 in the container. That leaves headroom for a gain change without clipping, and clipping is unrecoverable: once a pixel saturates, no software can tell bright from brighter.
How much usable range the sensor really has, before noise eats the low counts, is what EMVA 1288 camera comparison: reading performance data explains.
Three ways 12 bits travel, and how to tell them apart
| Container style | Histogram appearance | Ceiling | Recovery |
|---|---|---|---|
| Zero-filled 16-bit word | values step in 16s | 65520 | value >> 4 |
| Low bits copied from the top nibble | odd values, low nibble mirrors high nibble | 65535 | value >> 4 |
| Packed 12-bit, 2 pixels per 3 bytes | scattered values, checkerboard pattern | none | fix the format first |
The first two rows carry the 12 data bits in the same bit positions, so a single shift by four decodes both. The third row is different: packed 12-bit stores two pixels in three bytes, 1.5 bytes per pixel, and reading that stream as 16-bit words produces plausible-looking nonsense. The fix there is the correct pixel format or an unpacking step, not arithmetic.
A buffer-size cross-check catches the packed case early. A 1920 × 1200 frame in a 16-bit container needs 1920 × 1200 × 2 = 4,608,000 bytes; the same frame packed needs 3,456,000 bytes. If your buffer math matches the packed number while the driver delivers the container, capture tears or fails — the same failure family as V4L2 raw capture failing when the frame height undersizes the buffer.
Recovering counts in code
Once the frame sits in your CV_16UC1 buffer, one integer operation per pixel returns the sensor's own numbers: value >> 4, or an integer divide by 16, identical on unsigned 16-bit data. If the pipeline converts to float anyway, a scale factor of 1/16 does the same job.
Work thresholds in counts, not container values. A pass limit of 8000 in the 16-bit frame is 8000 >> 4 = 500 counts. Written that way, the threshold ports to a camera that pads with zeros or one that replicates; raw container thresholds can drift by up to 15 counts between the two styles.
For a quick display view, value >> 8 keeps the top 8 bits: 32759 >> 8 = 127, mid-grey on a monitor, consistent with a pixel at half scale.
Confirm what the driver actually delivers
Formats are negotiated, and negotiation can quietly fail. Some drivers accept a set-format call for a format they do not support and keep running the previous one without an error. The read-back is the only proof.
- List what the driver offers (
--list-formats-extin your V4L2 tools) and note the advertised bits per pixel. - Set the 12-bit format, then read the active format back (the get-format call, VIDIOC_G_FMT) and compare fourccs.
- Expose a bright uniform target just below saturation and histogram the frame: steps of 16 mean zero-filled; odd values with mirrored nibbles mean replicated; a smooth spread to 65535 means genuine 16-bit, so do not shift.
- Decode the maximum with
>> 4and divide by 4095. That fraction is your exposure headroom.
When exposure and gain changes refuse to take effect the same way, the pattern is the same set-versus-actual mismatch, and V4L2 exposure and gain settings ignored by a sensor driver walks through that case.
When the shift is the wrong fix
A smooth histogram that reaches 65535 with no 16-step comb and no nibble mirroring means the frame carries more than 12 real bits — some sensors and depth devices do. Shifting then discards four genuine bits, so leave the data alone.
If the frame passed through an ISP stage — gamma, a look-up table, white balance — pixel values no longer map linearly to exposure or counts. Ask the driver for the raw format when you need linearity; CSI camera with no ISP: what raw sensor output means explains what raw bypasses.
A driver that scales exactly to fill the container multiplies by 65535 / 4095 ≈ 16.0004 instead of shifting. Dividing by 16 afterward still lands within one count of the true value, so the decode above survives that difference too.
Checks before you trust the numbers
- Histogram combs at 16: zero-filled container. Low nibble mirrors the high nibble: copied bits. Smooth spread to 65535: real 16-bit, leave it alone.
- Maximum decoded: max >> 4, then ÷ 4095. Keep the measured feature between 70% and 90% of that range.
- Format read back after every set: a fourcc of GREY means 8-bit and a ceiling of 255.
- Buffer size: width × height × 2 bytes for a 16-bit container (1920 × 1200 → 4,608,000 bytes); width × height × 1.5 for packed 12-bit.
- Thresholds stored in counts, not container values, so they survive padding differences of up to 15 counts.
- One decode point, right after capture. Shifting twice by four leaves you with 8 real bits and no record of where the range went.
Choosing a camera that streams 12-bit cleanly
Bit depth is a per-camera configuration, not a given. Most machine-vision area scan and board-level cameras offer 12-bit or 12-bit-packed formats alongside 8-bit, and the cost is bandwidth: every extra bit per pixel is frame rate you cannot spend elsewhere, which is why many cameras default to 8-bit out of the box.
If you are selecting hardware for a measurement task that needs the extra depth, the area scan camera category and the board-level camera category group by interface and resolution, the camera finder narrows by sensor, shutter and resolution, and the camera step of the configurator walks the whole chain.
GigE, USB3, 10GigE and CoaXPress
1,287 models · from $149.00Compact cameras to build in
120 models · from $179.00More explainers on sensor output sit on the cameras topic page. If a format still refuses to behave, contact the applications team with your format list and one sample frame — the decode above usually settles the question in minutes.
Questions
Why does full brightness read 65520 and not 65535?
4095 counts shifted four bits left is 4095 × 16 = 65520, and the remaining 15 container values are padding, so a zero-filled container tops out at 65520. If the driver replicates the top nibble into the bottom four bits, the same 4095 counts read 65535. Both are correct deliveries of the same measurement: decode with a shift right by four either way, and treat 65535 as proof of replication, not of a wider sensor range.
How many bits of real information does the frame carry?
Twelve from the converter, and effectively fewer after noise: temporal noise of a few counts makes the last 12-bit bit marginal on many sensors, which is exactly what EMVA 1288 figures quantify. The container's extra four bits are always padding or copies, never new data, so a 16-bit file from a 12-bit stream stores one third more bytes while adding nothing to the measurement.
Why did the driver keep the old pixel format after I set the new one?
Some drivers accept a set-format call and only apply formats they actually support at the current resolution and frame rate, keeping the previous format without throwing an error. Always read the active format back immediately after setting it and compare the fourcc. Check the format list first, and request 12-bit only at resolutions and frame rates where the driver lists it.
Can I divide by 65535 and work in floats instead?
You can — normalising by 65535 maps the container to 0–1 and is fine for display or model input. It is not the same as recovering counts: 32759 / 65535 ≈ 0.4999 tells you the fill level, while 32759 >> 4 = 2047 tells you the sensor count. Keep integer counts wherever a threshold, a ratio or a flat-field correction needs to trace back to photons.
Do thresholds tuned on one camera transfer to another?
Not as raw container values. A zero-filled camera shows 2047 counts as 32752 while a replicating camera shows 32759, so container thresholds drift by up to 15 counts between the styles, and an 8-bit fallback collapses the whole scale by 256. Decode to counts right after capture and store thresholds in counts; they then transfer across cameras and padding styles unchanged.