You switch your stereo depth camera's output to a 16-bit disparity format, and the numbers that arrive are 0 and 255 — a posterized, two-color map instead of a smooth depth image. That symptom is a data-path fault, not a broken camera: something between the sensor and your variable is discarding the low byte. This guide shows what a Disparity16 pixel actually holds, why exactly two values betray eight-bit data, the four places where bit depth usually gets lost, and the formula that turns a correct raw value into millimeters. If you write the capture loop yourself — a driver node, an SDK frame callback or a file reader — you can find and fix this in an afternoon.
What a Disparity16 pixel holds
A Disparity16 pixel is a 16-bit unsigned integer that carries one stereo disparity in fixed subpixel steps. The standard packing is 16 LSBs per pixel of disparity, so the arithmetic is simple: a raw value of 80 is 80 / 16 = 5 px of disparity, a raw 33 is 2.0625 px, and the full 0 to 65,535 range would span about 4,096 px. Real stereo pairs sit far below that ceiling, so the format itself is never the bottleneck. Zero is reserved: it marks pixels where the stereo match found no correspondence, not a distance. The format is one entry in the depth stream's pixel-format selector, an enumeration in the driver or capture configuration that takes effect when the stream starts.
Why exactly 0 and 255 means eight bits
An eight-bit unsigned integer offers 256 values, 0 through 255; a 16-bit unsigned integer offers 65,536, as Microsoft's native pixel formats overview spells out. If your buffer holds only the two end points, one bit of information per pixel survived the trip: every measurement was reduced to a yes or a no.
Three mechanisms do this in practice. A display copy mistaken for the data stream: many drivers publish a colorized or thresholded image beside the raw one. A copy into an eight-bit container, which truncates or clamps every value above 255. Or a viewer that rescales the 16-bit frame into an eight-bit window, flattening the histogram on screen while the numbers stay intact. A stream spread across 0 to 255 is a different clue: that is an eight-bit scaled copy of real data, not a mask.
The four likely causes, in order
Work down the list; each item carries a test that settles it in minutes.
-
You tapped the display copy. Drivers commonly publish a preview next to the raw stream. Test: read the encoding field of the message you actually receive — if it declares eight bits per pixel, you are on the preview. Fix: subscribe to the stream whose encoding reads 16-bit unsigned.
-
Sixteen-bit data landed in an eight-bit container. The camera sends unsigned 16-bit; your image class or copy target is unsigned 8-bit, so values wrap or clamp at 255. Test: print the container type and the frame maximum — an 8-bit type cannot exceed 255 by construction. Fix: declare the buffer 16-bit and size every row at two bytes per pixel.
-
The format change never took effect. Some drivers lock the pixel-format selector once streaming, so the configuration says disparity while the wire still carries the old eight-bit encoding. Test: query the active format back from the driver after start and log it; the same lock-up appears in a pixel-format selector stuck read-only until the scan type is set. Fix: set the format before the stream opens, or stop, set, restart.
-
The data is fine and the viewer is flattening it. Test: histogram on the raw buffer, never on the render. Fix: widen the display range or use a 16-bit-aware renderer, and quote measurements from code only.
Read the buffer as 16-bit and prove what arrived
Take the buffer straight from the frame callback, read it as 16-bit words, and count what is inside before trusting any rendering. The loop below uses our own variable names; map them to your SDK's frame fields.
// Our names — map them to your SDK's frame fields.
uint32_t width = 640; // pixels per row
uint32_t height = 480; // rows
uint32_t stride = width * 2; // bytes per row, 16-bit data
const uint8_t* base = frame_bytes; // first byte the SDK handed you
uint16_t vmax = 0; // highest raw value seen
uint32_t distinct = 0; // distinct non-zero raw values
bool seen[65536] = {}; // one flag per possible raw value
for (uint32_t y = 0; y < height; ++y) {
const uint16_t* row = reinterpret_cast<const uint16_t*>(base + y * stride);
for (uint32_t x = 0; x < width; ++x) {
uint16_t v = row[x]; // one 16-bit read, not two bytes
if (v == 0) continue; // zero = no match; skip it
if (!seen[v]) { seen[v] = true; ++distinct; }
if (v > vmax) vmax = v;
}
}
// Healthy stream: vmax far above 255, distinct in the hundreds.
A corrected stream shows a maximum far above 255 and hundreds of distinct values on any textured scene. If the count stays at two, the flattening happened upstream of this loop, and the causes above tell you where. Copy the frame into your own storage inside the callback: drivers reuse buffers, and a frame read too late comes back torn or gray — the same mechanism behind frames that save wrong until you copy the Mat data out of the SDK buffer.
Convert disparity to distance
With real 16-bit values flowing, distance is one division:
Worked example, with numbers you can check in your head. Raw value 80, so d = 80 / 16 = 5 px. With a calibrated focal length of 500 px and a 2 mm baseline, Z = 500 × 2 / 5 = 200 mm. Units: f_px carries no length, so Z inherits the unit of B; divide by 1,000 for meters.
Quantization follows the third line. One LSB is 1/16 px = 0.0625 px, so at 200 mm: ΔZ = 200² × 0.0625 / (500 × 2) = 2.5 mm. Move the same point out to 400 mm: the disparity halves to 2.5 px, and the same LSB now costs 160,000 × 0.0625 / 1,000 = 10 mm. Equal disparity steps are therefore not equal millimeter steps — the bands compress with distance, as disparity map to depth conversion: bands are not fixed mm works through.
Decide the LSB factor from one plane target
The 16-LSB packing is the default in stereo toolkits, but a stream can also carry floating-point disparity or a different fixed-point factor, and quoting meters with the wrong factor scales every answer you publish. One measurement settles it. Aim the camera square at a plane at a known distance — 500 mm here — and compute the expected disparity: d = 500 × 2 / 500 = 2 px. Read the raw value on that plane. Raw 32 confirms the standard packing, because 2 × 16 = 32. Raw 500 would mean 500 / 2 = 250 LSB per pixel, and you would divide by 250 from then on. Whatever raw divided by expected turns out to be is your factor. Where calibration noise clouds the reading, stereo disparity scale and calibration for depth estimates extends the method.
When the encoding already says 16-bit
Two places hide a flattening after the format looks right. Stride: a 16-bit row occupies 2 × width bytes, and a reader that advances one byte per pixel, or trusts an assumed width, rebuilds a smeared image that can look two-valued — compare the buffer's stride against 2 × width before anything else. Valid masks: a frame of nothing but zeros is not a two-valued depth map, it is a missing one, usually a stream-schema fault; the SDK returns an empty depth image buffer traces that path. And when values are plausible but the map tears row to row, the fault sits upstream in stereo rectification, a different failure with its own fix.
Checks before you trust the stream
- The encoding field of the received message reads 16-bit unsigned — proves you tapped the data stream, not the preview.
- Your container is 16-bit and the stride equals 2 × width — proves the copy path keeps both bytes.
- The histogram, zeros excluded, spans hundreds of distinct values — proves nothing flattened the frame.
- Zeros are counted separately — proves invalid pixels are not dragging min, max or means.
- The plane-target quotient returns your assumed LSB factor — proves the scale before any distance is quoted.
- One caliper check on that plane lands within a few millimeters — proves the chain end to end.
When you need depth, not disparity
Debugging the raw stream pays off when your algorithm consumes disparity itself. If the task only needs metric results — heights, volumes, pick poses — binocular stereo and structured-light sensors do the matching internally and publish depth or point clouds in real units, so this converter never enters your code. The MV-DPS200P-02 projects structured light; the MV-DLS300P-04 scans a laser line across a stereo pair. Both sit in the 3D sensors range.
- Type
- Stereo / structured light
- Type
- Stereo / structured light
To compare the whole range, open the camera finder; if the open question is which camera class fits the task at all, 2D or 3D camera for conveyor pick and place walks that decision. From a corrected stream, the next building block is geometry: applying per-axis scale factors turns a depth image into a point cloud, and the 3D camera topic page collects the rest of the fault-finding guides.
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.
Stuck on a particular SDK's behavior? Tell us the camera model and what your histogram shows, and we will go through the numbers with you.
Questions
What does a Disparity16 value of 0 mean?
Zero means the stereo match failed or the pixel was masked: there is no measurement there, not a distance of 0 mm. Treat every zero as invalid — exclude it from statistics and color maps, and count it separately. A small fraction of zeros is normal in a textured scene; a frame that is mostly zeros points at weak texture, exposure or matching settings rather than a conversion bug.
How do I convert a raw disparity value to meters by hand?
Divide the raw value by the subpixel scale to get pixels, then divide focal length times baseline by it: Z = f_px × B / d, with B and Z in the same unit. Example: raw 96 at 1/16 px scaling is 6 px of disparity; with a 500 px focal length and a 30 mm baseline, Z = 500 × 30 / 6 = 2,500 mm, or 2.5 m.
How can I reduce stereo depth error at long range?
Error per LSB scales with distance squared divided by focal length times baseline, so widen the geometry: doubling the baseline halves the error. At 400 mm range with a 500 px focal length, a 2 mm baseline gives 10 mm per 1/16 px LSB; a 4 mm baseline gives 5 mm. A longer focal length helps the same way, at the cost of field of view.
Does every stereo camera pack disparity as 1/16 pixel steps?
It is the default in most stereo toolkits, but not universal: some SDKs emit floating-point disparity — use those values as pixels directly, no divide by 16 — and a few pack fixed-point factors other than 16. Settle it with one plane target at a known distance and divide the raw value by the disparity you expect; the quotient is your stream's factor.
How many distinct values should a healthy disparity frame show?
A textured scene a few hundred thousand pixels in size typically yields thousands of distinct raw values after subpixel packing. Single digits mean a mask or an eight-bit path; a few dozen suggests a flat, low-texture scene or heavy smoothing; values crowding against the maximum hint at clipping. Track the count per frame — it is the fastest single health metric a disparity stream offers.

