Your 10-bit RGB camera grabs normally and the viewer shows a clean image, yet every attempt to read pixels in code fails: the linear-access call returns false, and the plane table comes back all zeros. Nothing is broken and the buffer is not empty. A packed format such as RGB10p32 stores three 10-bit channels bit-packed into 32 bits, so the plane has no byte-addressable layout and pointer arithmetic has nothing valid to walk. This guide shows how to confirm which pixel format the frame buffer actually holds, switch the acquisition to an unpacked 16-bit conversion, and read every channel with the right increments and casts. It applies to any color area scan camera delivering more than 8 bits per channel through a GenICam or GenTL acquisition stack.
Symptom: frames arrive, pixel access returns nothing
Three signs travel together. The linear-access call on the image plane returns false instead of a base pointer and increments. The plane-table call returns true, but every x and y entry reads 0, so a read at any coordinate lands at offset 0 and returns 0. And the behavior is identical whether the conversion stage is set to Raw or to a 16-bit RGB format.
A clean viewer image does not contradict this. Display paths convert into their own buffer, so what you see says nothing about the layout of the buffer your code touches. The failure lives in that buffer's pixel format, not in the camera's data.
Likely causes, most probable first
- The camera delivers a packed format. In RGB10p32 a channel value can start at bit 10 or bit 20, in the middle of a byte. Test: read the pixel format the stream reports for the grabbed buffer, not the setting you wrote. Any name ending in p, RGB10p32 or Mono10p for example, means packed.
- The conversion stage is set to Raw. The driver passes the camera bits through untouched, so the host buffer stays packed whatever you intended to receive. Test: set the conversion to RGB16 explicitly, re-grab, and call linear access again.
- The calls run on an invalid or stale image object. Pointer calls fail on an image that is not bound to the opened stream. Test: discover devices, open one, grab a single snapshot and access it in the same scope. If access suddenly works, the earlier object was dead.
- You dereference bytes instead of 16-bit words. The increments are byte counts, so a byte read returns only part of a channel and caps near 255 on a white target. Test: aim at a white target; a correct ushort read lands at 65,472 or above, never below 256.
Packed versus unpacked: why RGB10p32 defeats pointer arithmetic
The Windows imaging documentation's native pixel formats overview defines a pixel format as the memory layout of each pixel: how many bits each channel gets and where they sit. Packed 10-bit variants squeeze channels into 32-bit containers; unpacked variants give each channel its own 16-bit word.
RGB10p32 lays out one pixel like this:
| Bits in the pixel | Content | Width |
|---|---|---|
| 0-9 | channel 0 | 10 bits |
| 10-19 | channel 1 | 10 bits |
| 20-29 | channel 2 | 10 bits |
| 30-31 | padding | 2 bits, reads 0 |
Channel 1 starts at bit 10, inside the second byte. A byte read at the pixel address returns a fragment of two adjacent channels, so the code runs without errors and yields meaningless values — worse than a clean failure. RGB16 stores one 16-bit little-endian word per channel at offsets +0, +2 and +4 inside a 6-byte pixel, which byte stepping addresses cleanly and a ushort cast decodes.
Packed formats raise the same layout problem in every GenTL consumer and SDK, for Mono10p as much as RGB10p32, and support for decoding them transparently has arrived release by release. If your library decodes packed data on its own, linear access can succeed even on RGB10p32 — one more reason to read back the delivered format instead of assuming it.
The fix: convert to RGB16, then read 16-bit words
- Set the conversion, not the camera. The parameter that decides what lands in host memory is the stream or device conversion, commonly an enumeration named PixelFormat on the consumer side. Choose its RGB16 entry; Raw is the entry that caused this failure. Leave the camera at RGB10p32 when link bandwidth is tight, because the conversion happens after the frame arrives.
- Grab one frame and read back the delivered format. It must now report RGB16. If the value refuses to change, that is a different failure with its own fix — see the guide on pixel formats that stay fixed, linked below.
- Ask the plane for linear access in 16-bit unsigned elements. You receive a byte base pointer plus two increments in bytes. Interleaved RGB16 gives xInc = 6 bytes per pixel and yInc = 6 × image width per line; channel-per-plane stacks give three planes with xInc = 2.
- Walk to the pixel in bytes and read each channel as an unsigned 16-bit word:
// Example names: your SDK's linear-access call fills these in.
byte* pBase; // first byte of the plane
long xInc = 6; // bytes per pixel = 3 channels x 2 bytes
long yInc = 2448 * 6; // 14,688 bytes per line at 2448 px wide
int x = 640, y = 480; // pixel to read
byte* pPixel = pBase + y * yInc + x * xInc; // offset in bytes
ushort r = *(ushort*)(pPixel + 0);
ushort g = *(ushort*)(pPixel + 2);
ushort b = *(ushort*)(pPixel + 4);
ushort r10 = (ushort)(r >> 6); // 10-bit range 0-1023 when left-aligned
- Prove the path with two snapshots before any processing loop. A white diffuse target must read about 65,472 or more per channel, or near 65,535 if your driver rescales. A lens-capped dark frame must read near 0. When both pass, the pointer arithmetic is proven.
Worked example on a 2448 × 2048 frame:
- bytes per pixel: 3 × 2 = 6; bytes per line: 2448 × 6 = 14,688
- pixel (640, 480): offset = 480 × 14,688 + 640 × 6 = 7,050,240 + 3,840 = 7,054,080 bytes
- the three channel words sit at +0 (R), +2 (G) and +4 (B), two bytes each
- frame memory: 2448 × 2048 = 5,013,504 pixels; 5,013,504 × 6 = 30,081,024 bytes ≈ 30.1 MB unpacked, against 5,013,504 × 4 = 20,054,016 bytes ≈ 20.1 MB packed
Where the 10-bit value sits in the 16-bit word
The GenICam convention for unpacked containers holds lower-depth data left-aligned: the 10 bits occupy bits 15 down to 6, and bits 5 to 0 read zero. Full scale, 1,023, therefore arrives as 1,023 × 64 = 65,472, and a right shift by 6 recovers the original range. Some driver conversions rescale instead, mapping 1,023 to 65,535, a factor of 65,535 ÷ 1,023 ≈ 64.07. One white-target snapshot decides which convention your stack uses: near 65,472 means shift, near 65,535 means scale.
The 10-bit and 16-bit YUV capture guidance in the Windows media docs recommends the same container approach: keep 10-bit data in a 16-bit sample for processing. Do the same here, and keep those words when you record to disk rather than saving a squeezed 8-bit copy — the guide on saving 12-bit camera images without losing intensity data covers the same trap one depth higher.
What unpacking costs, and when packed is still right
Memory grows by a fixed 1.5×: 6 bytes per pixel instead of 4 — 30.1 MB instead of 20.1 MB on the 5,013,504-pixel frame above. Converting on the fly also writes the unpacked frame once, so a 30 fps stream moves 30,081,024 × 30 = 902,430,720 bytes/s ≈ 0.9 GB/s through host memory, against 601,620,480 bytes/s ≈ 0.6 GB/s packed.
Link bandwidth is a separate ledger, and it only moves if you ask the camera itself for a 16-bit format. A GigE link carries 1,000 Mbit/s ÷ 8 = 125 MB/s line rate. This 5 MP 10-bit RGB stream needs 20.1 MB per frame packed, so GigE tops out at 125,000,000 ÷ 20,054,016 ≈ 6 fps packed and 125,000,000 ÷ 30,081,024 ≈ 4 fps unpacked; at 30 fps you are on 10GigE or CoaXPress either way. Pixel format and exposure together set the real ceiling — the guide on RGB camera frame rate limits works that calculation.
Decision: default to the consumer-side RGB16 conversion. It is the only layout in this set that byte pointer arithmetic can walk without a custom decoder. Keep the packed format when a GPU or FPGA pipeline consumes the 4-byte words directly, or when the link is the constraint and you can validate a decoder bit by bit — the guide on packed Bayer 12-bit conversion to a 16-bit image shows that decode path for raw sensors.
Format options at a glance
| Format | Bytes per pixel | Byte-addressable | Linear access | Choose it when |
|---|---|---|---|---|
| RGB10p32 | 4 | No, channels straddle bytes | Fails or returns fragments | The link is the bottleneck and you decode yourself |
| RGB16 | 6 | Yes, words at +0, +2, +4 | Works with a ushort cast | Default for processing in code |
| RGB8 | 3 | Yes | Works | The 2 extra bits carry no detail you inspect |
| Raw | varies | No | Needs your own decode | You demosaic or unpack in GPU or FPGA |
RGB8 halves memory again, but watch what actually arrives: conversion stages can hand an 8-bit buffer to a camera whose sensor is 10-bit — the guide on ADC bit depth versus pixel format explains that pipeline.
Checks before you write the processing loop
- Read the delivered pixel format from the grabbed buffer and log it. The setting you wrote is a request; the buffer is the fact.
- Confirm both increments are byte counts and check for line padding. Some allocations round each line up, so yInc can exceed 6 × width; padding mismatches surface as skew or illegal GPU mappings, which the guide on GPU frame mapping, pitch and planes walks through.
- Count the planes. Interleaved RGB16 is one plane with three words per pixel; channel-per-plane stacks hand you three planes with xInc = 2 and their own channel order.
- Watch the sign. Some stacks expose 16-bit data as signed words, which turns values above 32,767 negative; the I16 to U16 conversion fix covers that conversion.
- If formats are negotiated at stream start and the wrong one wins, the guide on camera pipeline format negotiation lists the usual losers and fixes.
- When the conversion runs inside a frame grabber driver, the same rule applies: the format the card delivers decides the pointer layout, exactly as a software consumer does.
Prevention: two habits that keep pixel access honest
- Run a one-frame self-test at startup — delivered format, increments, white-target read, dark read — before the application's processing loops start.
- Re-run it after every library or firmware change. Packed-format decode support in GenTL consumers has grown release by release, so an upgrade can silently change the buffer you receive from the same camera.
If the set format refuses to stick at all, that failure has its own diagnosis in the guide on camera pixel formats that stay fixed.
To size the hardware around a 10-bit RGB stream, do the arithmetic in this order: bytes per frame, frames per second, then interface line rate. The camera configurator walks interface, sensor and lens in that order, the camera finder filters by interface and resolution, and the systems topic page collects the rest of the acquisition guides.
Questions
Why does my pixel table come back all zeros when the viewer image looks fine?
The viewer applies its own display conversion in a separate buffer, so a clean image only proves frames are arriving. Your code failed because the host buffer holds packed RGB10p32: three 10-bit channels bit-packed into 4 bytes per pixel, a layout no byte-addressable table can describe. Set the acquisition conversion to RGB16, re-grab, and linear access should return a base pointer with xInc = 6. Prove it on a white target: a center pixel should read about 65,472 or more per channel.
How do I recover the original 0-1023 range from RGB16 channel values?
Read the channel as an unsigned 16-bit word and shift right by 6 when the data is left-aligned: 65,472 ÷ 64 = 1,023. A few driver conversions rescale instead, so full scale reads 65,535; the factor there is 65,535 ÷ 1,023 ≈ 64.07. One snapshot of a white target tells you which convention your stack uses, and you code the shift or the scale once for all channels.
Can I keep the packed RGB10p32 format and decode it myself?
Yes. Read each 4-byte pixel as one 32-bit little-endian word, extract the three 10-bit fields with shifts and masks, and scale them into your processing type. You save the 50% memory increase and the conversion pass, at the cost of decode code you must validate bit by bit. Choose this when a GPU or FPGA pipeline consumes packed words directly or the link is the bottleneck; otherwise the RGB16 conversion is faster to get right.
Do the last 2 bits in each RGB10p32 pixel hold image data?
No. Three 10-bit channels fill 30 of the 32 bits; the remaining 2 are padding that reads zero. Treat them as absent: masking them as a fourth channel yields noise, not signal. If a task truly needs more than 10 bits per channel, select a camera and conversion that deliver 12-bit unpacked RGB instead of reading padding.
Why did my frame memory grow by half after switching to RGB16?
Unpacking trades memory for addressability: a pixel costs 6 bytes in RGB16 against 4 packed. On a 2448 × 2048 frame that is 5,013,504 pixels × 6 = 30,081,024 bytes, against 20,054,016 packed — exactly 1.5×. Link bandwidth does not change when the camera still sends RGB10p32, because the conversion happens after the frame arrives; only host memory and one write pass grow.