You set the camera to Mono16, grab a frame, and the array that comes back holds exactly half the elements you expected. The image is sharp, the ROI is right, the camera streams — only the count is wrong. The fault sits in one of three places: the pixel size the camera is actually sending, the buffer size announced to the GenTL producer, or the data type you read the buffer with.
This guide gives you the arithmetic that separates those three, four checks that localize the fault in about ten minutes, and the fix for each finding. It is written for anyone integrating an area scan camera through a GenTL producer — a frame grabber driver, a network card driver or a USB3 driver — in Python, C++ or C#.
What Mono16 owes you
Mono16 is a promise about bytes: 16 bits, 2 bytes per pixel. Three GenICam features fix the payload before a single frame moves — width, height and the pixel size of the chosen format:
A correct Mono16 unpack returns exactly pixels 16-bit elements, each 2 bytes. Mono8 returns the same element count at 1 byte each. Packed formats sit in between at fractional bytes per pixel:
| Format | Pixel size | Bytes per pixel | 640 × 256 payload | Unpack as |
|---|---|---|---|---|
| Mono8 | 8 bit | 1.00 | 163,840 bytes | 163,840 × uint8 |
| Mono10packed | 10 bit | 1.25 (4 pixels in 5 bytes) | 204,800 bytes | producer's packing layout |
| Mono12packed | 12 bit | 1.50 (2 pixels in 3 bytes) | 245,760 bytes | producer's packing layout |
| Mono16 | 16 bit | 2.00 | 327,680 bytes | 163,840 × uint16 |
Worked example. Say the ROI is 640 × 256 — a narrow strip that lifts a small camera to a 94.8 fps nominal rate. The pixel count is 640 × 256 = 163,840 pixels. In Mono16 the producer owes 163,840 × 2 = 327,680 bytes, and a correct unpack yields 327,680 ÷ 2 = 163,840 uint16 elements. Your debug print shows 81,920 elements, labelled Mono16. Run the arithmetic backwards: 81,920 × 2 = 163,840 bytes actually arrived — exactly width × height × 1. Something in the chain sized the buffer as if the format were Mono8, and the missing 163,840 bytes never left the camera.
That backward calculation is the whole diagnosis. Element count × element size gives the bytes the buffer really held; compare that with the payload you are owed and you know which side came up short.
Where the count halves
Rank the suspects by how often each is guilty:
- The consumer sized its buffers for 8 bits per pixel. The acquisition library allocated width × height bytes regardless of the pixel size, so a Mono16 stream fits only half its payload. GenTL consumers have shipped this exact fault for 16-bit formats and fixed it in later releases; if buffer bytes come up short while your own unpacking is right, read your library's release notes and update before debugging anything else.
- The camera is not actually in Mono16. PixelFormat still reads Mono8, the producer delivers width × height bytes, and code written for 16-bit divides by 2 — half the elements, values capped at 255.
- Your own unpack halves the view. The buffer is complete, but the array is built from a length computed for 1-byte elements, or the wrong dtype lands on a slice.
- Chunk data inflates the payload. With chunk data active, PayloadSize exceeds width × height × 2, buffers announced from your own product are too small, and the producer refuses to start or delivers short.
- A packed format is in play. Mono10packed and Mono12packed pack a whole number of pixels into a whole number of bytes — 4 into 5 and 2 into 3 — so 2-bytes-per-pixel arithmetic is wrong from the first row.
If the buffer is full but your array is short, your own code is the suspect. That application-side twin has its own write-up: Why your 16-bit image buffer has half the expected pixels.
Four checks that pin down the fault
Run them in order; each ends in a verdict.
- Read the camera's PixelFormat and PixelSize before allocating anything. You want Mono16 and a pixel size of 16. A pixel size of 8 confirms suspect 2 — set the format now, because pixel formats are locked while the camera is streaming (why pixel formats refuse to change, and how to set them).
- Read PayloadSize from the producer. For the 640 × 256 ROI in Mono16 it must read 327,680. A larger value means chunk data is on; a value near 163,840 means the camera is still in Mono8. Never substitute your own width × height × 2 for this number.
- Compare the delivered buffer's byte length with PayloadSize. Equal bytes: the sizing chain is healthy, go to step 4. Half the bytes: the consumer under-allocated — suspect 1 confirmed.
- Check dtype and element count on the array you build. Mono16 unpacks to exactly 163,840 uint16 elements. A uint8 read of full bytes returns 327,680 one-byte values — plausible-looking frames with destroyed data. A uint16 read of half-size bytes returns 81,920, the number in your debug print.
The fix for each finding
Each verdict has one fix:
| Verdict | Fix |
|---|---|
| Buffer bytes are half of PayloadSize | Allocate from PayloadSize, not width × height. If the library computes sizes itself and ignores 16-bit formats, that is a library bug: move to a release that fixes it. |
| Pixel size reads 8, you expected 16 | Set PixelFormat to Mono16 before acquisition starts; the payload must then double to 327,680 bytes. |
| Full bytes, wrong element count or values | Build the array as uint16 with a count of width × height. On x86 and ARM hosts the bytes are little-endian, so a plain 16-bit read is correct. |
| PayloadSize exceeds width × height × 2 | Switch chunk data off, or announce every buffer at the full PayloadSize. |
Two habits make the fix permanent. Size buffers from PayloadSize, because the producer already knows about chunk data and packing and your formula does not. And assert element count = width × height on the first frame of every new build, so this fault fails loudly at integration instead of quietly in production.
The producer here is the driver behind the camera: a frame grabber driver for CoaXPress or Camera Link, a network card driver for GigE, a USB3 driver. PixelSize, PayloadSize and buffer length read the same way on all three.
The 20 fps cap that arrives alongside
The same integrations often hit a second symptom: the ROI lifted the camera's nominal rate to 94.8 fps, yet about 20 fps arrive. Run the bandwidth math before touching camera settings. One Mono16 frame is 327,680 bytes, so 94.8 fps needs 327,680 × 94.8 = 31,064,064 bytes per second, about 31 MB/s. GigE's 125 MB/s line rate leaves roughly 110 MB/s of payload after protocol overhead, so the transport has around 3.5× headroom at this ROI — bandwidth is not the constraint.
A cap near a round number is a timing signature. 20 fps means one frame every 50 ms (1 ÷ 20 = 0.05 s). Look for a per-frame stage that takes about that long — a copy, a conversion, a file write — or for a buffer queue too shallow to absorb jitter, which turns any stall into dropped frames. Both failure modes have their own guides: Acquisition faster than processing: fix the frame rate gap and Camera burst capture faster than link bandwidth: use the buffer.
Fix the buffer sizing first, in any case: short deliveries stall a pipeline too and masquerade as a throughput problem.
Bake these checks into the next build
Five lines of discipline prevent a repeat:
- Read PixelSize together with PixelFormat, always as a pair.
- Take buffer size from PayloadSize at announce time, every time.
- Assert element count = width × height on the first frame of any new camera, driver or library build.
- Expect the payload to scale with the format: Mono8 to Mono16 doubles it, and the assertion catches the case where it does not.
- Pin the acquisition library version in the build record. This class of sizing bug is fixed in releases, and a pinned version makes the fix visible in the diff.
The rest of the camera-side fixes on this site sit on the cameras topic.
When Mono16 is not the format to pick
Mono16 is the safe default for a 10-, 12- or 14-bit sensor, and the trade is bandwidth. A 12-bit sensor carried in Mono16 costs 2 bytes per pixel; Mono12packed carries the same values in 1.5, a 25% saving on the link and the host. Mono8 costs 1 byte but discards the low bits, flattening scenes that need the sensor's full dynamic range — that trade is covered in Why 10-bit sensors ship 8-bit images.
The decision for the worked example: the full-rate Mono16 stream needs about 31 MB/s, comfortably inside GigE, so keep Mono16 and its simple 2-bytes-per-pixel math. Move to a packed format only when the link or the host actually binds, and accept that the unpack then follows the producer's packing layout instead of your own arithmetic.
If the count is still wrong after all four checks, change one hardware variable and re-run them: a second camera of the same model through the camera finder, or the whole chain — camera, lens, light, capture card — through the system configurator. For a case that resists all of this, the contact page reaches the applications team.
Questions
Why exactly half the elements and not some other count?
Mono16 is 2 bytes per pixel, so halving happens whenever something built for 1-byte elements meets a 16-bit payload. In the 640 × 256 example, the camera owes 327,680 bytes and a correct unpack gives 163,840 uint16 elements. If only 163,840 bytes arrive — a Mono8-sized buffer — a uint16 read yields exactly 81,920, the count in your debug print. Any other ratio points at chunk data or a packed format instead.
How do I confirm the camera is really sending Mono16?
Read the PixelFormat node and the PixelSize value beside it before you allocate buffers: Mono16 reports 16, Mono8 reports 8. Then cross-check the payload — width × height × 2 bytes for Mono16, width × height for Mono8. On the 640 × 256 ROI those are 327,680 and 163,840 bytes. If PixelFormat will not change, the camera is probably streaming: set the format before acquisition starts.
Acquisition refuses to start once the format switches to Mono16 — the same fault?
Closely related. A GenTL consumer announces buffers to the producer, and the GenICam GenTL standard lets the producer refuse to start acquisition when an announced buffer is smaller than the payload size. Size every buffer from the producer's PayloadSize rather than your own width × height × 2: active chunk data or a packed format can push the payload above that product.
My element count is right now but values stop at 255 — what is that?
The mirror-image fault: the unpack is correct but the camera is still transmitting Mono8, so every 16-bit read finds the low half empty and contrast collapses. Read PixelSize; if it says 8, set the pixel format to Mono16 before buffers are allocated. The payload check then proves it: the buffer must double from 163,840 to 327,680 bytes on the 640 × 256 ROI.
Does the interface matter — GigE, USB3 or CoaXPress?
Not for this fault. Buffer sizing and unpacking happen on the host behind the GenTL producer, so a GigE network driver, a USB3 driver and a CoaXPress frame grabber driver all behave the same. What the interface changes is throughput and driver tuning, which is where the separate 20 fps cap comes from — that one is about bandwidth and buffer queues, not about data width.
Do packed formats such as Mono12packed behave the same way?
They fail differently. Mono12packed fits 2 pixels into 3 bytes and Mono10packed fits 4 into 5, so width × height × 2 overshoots and a plain uint16 read scrambles values. Take the size from PayloadSize and unpack with the packing layout the producer specifies for that exact format. If the sensor supports it and bandwidth allows, plain Mono16 avoids the whole packing question.