A 12-bit camera can focus, trigger, save and measure perfectly and still show a black window in a preview built for 16-bit data. Nothing is broken. The sensor stops at 4095 counts, the preview maps 65535 counts to white, and your brightest pixel lands about 6% up that scale — gray level 16 of 255, effectively black. This page gives the arithmetic, two fixes (set the display range or scale the pixels), a three-step test that confirms the sensor's true bit depth, and the checks that separate a scaling problem from a dead trigger or a capped lens. It is for anyone integrating a camera through MATLAB, Python or an SDK viewer.
Two numbers decide it: sensor depth and buffer depth
Every area scan sensor digitizes through an analog-to-digital converter with N bits, so it emits 2^N codes numbered 0 to 2^N - 1. The buffer your application receives is usually wider: most acquisition SDKs deliver mono streams into unsigned 16-bit integers (uint16), because one container type holds every common depth from 8 to 16 bits. Delivery copies values; it never rescales them. A 12-bit frame inside a 16-bit buffer therefore keeps its original counts, ceiling 4095, with everything above absent by construction.
For a 12-bit sensor: 4096 codes, ceiling 4095, fill factor 2^(16 - 12) = 16.
Why the preview paints black
A preview that assumes the container's full range maps 0 to black and 65535 to white across the 255 levels of an 8-bit display ramp. Your brightest pixel lands at 4095 / 65535 × 255 = 15.9, so the whole frame paints at about gray level 16 of 255, roughly 6% brightness. Where the preview splits mono values unevenly across RGB channels, the frame shows a dark blue cast instead of pure black. Either way the camera sits at full signal; the display scale is 16× taller than the data.
The same arithmetic makes saved 10-bit PNG files look black in ordinary image viewers; why 10-bit PNG images look black in image viewers covers that file case. This page is the live-stream case: frames straight from the camera into the preview.
What each sensor depth loses in a 16-bit stream
| Sensor depth | Codes | Brightest count | Share of 16-bit range | Multiply by to fill | Brightest pixel shows |
|---|---|---|---|---|---|
| 8-bit | 256 | 255 | 0.4% | ×256 | gray 1 of 255 |
| 10-bit | 1024 | 1023 | 1.6% | ×64 | gray 4 of 255 |
| 12-bit | 4096 | 4095 | 6.2% | ×16 | gray 16 of 255 |
| 14-bit | 16384 | 16383 | 25% | ×4 | gray 64 of 255 |
| 16-bit | 65536 | 65535 | 100% | ×1 | gray 255 of 255 |
The last column assumes the viewer maps the container linearly onto the 8-bit display ramp, the common default. A 14-bit sensor already reaches gray 64, dark but visibly alive; at 12-bit the illusion is complete.
Fix one: point the display range at the sensor's real range
The clean fix touches no pixels. Set the preview's intensity window to minimum 0 and maximum 4095 and mark the image as scaled, so 4095 stretches to white. In MATLAB these are the axes CLim property and the image CDataMapping property; most SDK viewer controls expose the same thing as an intensity-range or auto-range setting. MATLAB's Image Acquisition Toolbox also documents a full-bit-depth preview switch: set imaqmex('feature', '-previewFullBitDepth', true) before you create the videoinput object, and preview renders the frames at their real depth.
Saved frames keep native counts either way, so measurements are untouched; saving 12-bit camera images without losing intensity data covers keeping that depth on disk.
Fix two: scale the pixels when you must
When the viewer cannot be configured — a generic image control, a display library that expects full-range values — scale the buffer itself. Multiply by 16, equivalently shift left 4 bits, to fill a 16-bit container; divide by 16, shift right 4 bits, to feed an 8-bit display, which sends the ceiling 4095 to 255. The upward scale is lossless below saturation: 4095 × 16 = 65520, inside the 65535 ceiling with 15 counts of headroom. If you build images from raw buffers by hand, convert a raw camera buffer to a processing-ready image shows that step first.
# Worked example: variable names invented here, not a camera SDK's API.
pixel = 4095 # int, initial value: brightest count a 12-bit ADC emits (2^12 - 1)
scale = 2 ** (16 - 12) # int, initial value: 16, the factor that fills a 16-bit container
to_16bit = pixel * scale # int: 65520, fills 99.98% of the 16-bit range
to_8bit = pixel >> 4 # int: 255, full white on an 8-bit display ramp
assert pixel == 2**12 - 1
assert to_16bit == 65520 and to_16bit <= 65535
assert to_8bit == 255
assert round(pixel / 65535 * 255) == 16 # what the unscaled preview showed
A three-step test for the sensor's true bit depth
Before changing code, establish the real ceiling:
- Read one histogram. Grab a single normally exposed frame of a plain, evenly lit target. Typical values under 4000 with a hard ceiling at 4095 say 12-bit counts; a ceiling near 65535 says 16-bit.
- Find the clipping ceiling. Raise exposure time or lamp intensity until the histogram flattens against a hard ceiling. That ceiling equals the converter maximum: 4095 confirms 12-bit, 65535 confirms 16-bit. A few seconds of overexposure is enough to find it; do not leave the sensor pinned.
- Check the alignment. Histogram spikes every 16 counts with a top at 65520 mean the camera left-aligns 12-bit data in the 16-bit container: the image looks correct and needs interpretation, not brightening. Arbitrary values capped at 4095 are raw right-aligned counts, the case that paints black.
Where the pixel format decision lives
Over GigE and USB3 the camera publishes its formats as GenICam features: the PixelFormat enumeration carries symbolics such as Mono8, Mono10, Mono12 and Mono16, and your SDK sets one before acquisition starts. Framing and focusing in Mono8 while measuring in Mono12 splits the problem cleanly, because 255 fills an 8-bit display directly. If setting the format throws an invalid-value error, the symbolic is not in that camera's enumeration; GenICam enumeration invalid value: check symbolics before you set walks through that failure.
The area scan cameras category and the cameras finder filter by interface and sensor format. Two examples: the LS-CMR-0053 runs 9 fps over GigE at 12 MP with a 1/1.7-inch IMX226 rolling-shutter sensor, and the LS-CMR-0166 reaches 30 fps at 4000 × 3000 over USB3, also on the IMX226 sensor.
- Sensor format
- 1/1.7"
- Lens mount
- C-mount
- Sensor format
- 1/1.7"
- Interface
- USB3
When the chain runs over CoaXPress or Camera Link, frames cross a frame grabber before they reach your buffer, and the grabber's SDK adds its own pixel-format and tap packing settings; check them too when the preview is black only in your application. The accessories finder lists the GigE, 10GigE and CoaXPress cards. One example: the MV-GX1008 is an octo CXP-6 frame grabber that delivers camera taps into host buffers whose format your code requests.
- Interface
- CoaXPress
When scaling is not the problem
Let the histogram decide before you touch code. A ceiling of 0 on every frame means no light reached the sensor: lens cap, closed aperture, a strobe firing outside the exposure window, or a trigger that never arrived; the trigger side is wired up in hardware triggered image acquisition in Python. Frames arriving empty or timing out point at acquisition, not display. A frame with visible content but washed-out contrast points to a gamma or LUT stage in the SDK, which no scaling repairs.
Before you change anything
- Read the PixelFormat symbolic list from the camera itself, not from a spec sheet: the enumeration is the authority on which depths the camera streams.
- Run the clipping-ceiling test once and write the ceiling down; every scaling decision hangs on that number.
- Note the alignment: combing every 16 counts means left-aligned data that needs interpretation, not brightening.
- Apply the fix at the display stage first; rescale pixels only when the viewer cannot be configured.
- After any pixel-format or firmware change, repeat the ceiling test, because alignment can move with the format.
Fix the range, not the camera: display window 0 to 4095 for the one-line cure, multiply by 16 for a 16-bit path, divide by 16 for an 8-bit preview. If you are specifying hardware for 12-bit work, the camera step of the configurator pairs camera, mount and controller, and the systems topic page collects more acquisition-level guides like this one. If a histogram still refuses to make sense, send its ceiling number through the contact page — it is usually all we need to name the cause.
Questions
Is multiplying 12-bit values by 16 safe for measurements?
Yes arithmetically: 4095 × 16 = 65520 fits inside a 16-bit word, so nothing saturates and every ratio between pixels is preserved. It is not safe for calibrated software: any threshold, gain or flat-field correction expressed in counts must be multiplied by the same 16, and radiometric work should stay in native counts with scaling applied only at display.
Why does the camera's own viewer show the image while a custom preview stays black?
The vendor viewer reads the camera's PixelFormat feature and sets its display range to the real ceiling, 4095 for Mono12, before rendering. A generic preview assumes the container's full 16-bit range, 65535, and maps the brightest pixel to gray level 16. Same frames, different display ranges; the fix belongs on the preview side, not in the camera.
Why does a 12-bit sensor stop at 4095 instead of 4096?
A 12-bit converter produces 4096 distinct codes, and because counting starts at zero they run 0 to 4095. The value 4096 does not exist in the stream. The same one-off applies everywhere: a 16-bit buffer tops out at 65535, which is why 4095 × 16 leaves 15 counts of headroom below the container ceiling.
How do I show 12-bit data on an 8-bit display or in an 8-bit file?
Divide by 16, equivalently shift right by four bits: the ceiling 4095 becomes 255. That discards the four least significant bits, which costs display detail but never pixel ratios. For a 16-bit path multiply by 16 instead and write the 8-bit image only at the last step, so the processing copy keeps native counts.
Can the camera send 8-bit data while the pipeline is being debugged?
Yes, if Mono8 appears in the camera's PixelFormat enumeration: select it and the preview brightens immediately because 255 fills the 8-bit display range directly. Mono8 keeps only 256 gray levels, so it is fine for framing, focus and trigger checks but too coarse for sub-1% intensity work; switch back to Mono12 before measuring.


