System designTroubleshooting

GenTL buffer padding_y not implemented: fix the fetch crash

GenTL producers answer buffer padding_y with GC_ERR_NOT_IMPLEMENTED when the query is not implemented; rebuild the stride and update the side that lags.

LumaSpec Vision10 min read Share on LinkedIn

On this page 10 sections
  1. 01What the error is telling you
  2. 02Why a producer may skip the padding query
  3. 03Fix it: update one side
  4. 04Recover the frame with stride math
  5. 05Worked example: a 1920 × 1200 Mono8 frame
  6. 06When the image comes out sheared
  7. 07Which side to fix: the decision table
  8. 08Checks before you redeploy
  9. 09When the capture side is the suspect
  10. 10Questions

fetch_buffer() dies with NotImplementedException, GC_ERR_NOT_IMPLEMENTED (ID: -1003), and the traceback stops at a line adding padding_y to a byte count. The camera itself looks fine: the vendor tool shows live frames. The fault sits one layer down, between the GenTL producer that delivered the buffer and the acquisition library that turns it into an image array. This guide decodes the error, shows why frames can arrive while conversion still crashes, gives the stride math that rebuilds the image — 1920 bytes per row and 2,304,000 bytes total for the 1920 × 1200 Mono8 case — and ends with the decision: which side to update, and what to change in your reshape code today.

What the error is telling you

Read the exception literally. GC_ERR_NOT_IMPLEMENTED (ID: -1003) is the GenTL status for a query this producer never implemented — the message from the source says "I do not offer that", not "the camera failed". The traceback shows exactly where it fires: inside the library's array-conversion step (_to_np_array), at the line nr_bytes += self._buffer.padding_y. Delivery has already succeeded by then. Three facts follow:

  • Frames are arriving. The buffer was delivered and queued before anything crashed.
  • The query for the end-of-buffer padding value was refused by the producer.
  • Harvester 1.2.3 reads that value unconditionally while sizing the NumPy array, so the refusal becomes a hard exception instead of a zero.

The camera working in the vendor tool confirms the split: sensor and transport are healthy, and the gap is one optional query. For contrast, a delivery problem looks different — the call hangs, then times out; that path is covered in Frame didn't arrive within timeout: one wait call per frame. And if the producer never loads at all, you are fighting a path problem, not a buffer one — start with GenTL XML file location errors: diagnose camera description file paths.

Why a producer may skip the padding query

Two padding values surround a GenTL image buffer. Per-row padding is extra bytes appended after each line so rows land on an alignment boundary; end-of-buffer padding (padding_y in the traceback) is extra bytes after the last row. A producer may implement either, both, or neither — and when a consumer asks for one that is missing, the designed answer is exactly the status you saw. Well-behaved consumers treat "not implemented" as "assume zero", which is what the library fix does.

The failure feels sudden because a working stack hides the gap until something moves:

  • An acquisition-library update that newly reads the query. That is the 1.2.3 pattern: older releases never asked, so the same producer "worked".
  • A producer or driver package update on the camera or frame-grabber side.
  • A pixel-format change that routes the frame through a different buffer path in the producer.

Log the pixel format at every session start; a silent switch to Mono8 changes the byte count as well, and that trap has its own write-up: Camera image encoding falls back to Mono8: request Bayer instead. The gap is not unique to one pairing either — producer reports describe buffer-information queries refused outright, or the same not-implemented status raised on every fetch, across different camera families. More GenTL bring-up faults, from discovery to XML paths, sit on the systems topic page.

Fix it: update one side

Decision first: update the acquisition library. The maintainers added a fallback for producers that refuse the padding query — in the reported case it went in on branch _issue_172 and was then merged to master — so any release after that merge converts the refusal into the zero-padding assumption for you. On the case's Python 3.7 / Windows 10 setup that is one command: pip install -U harvesters. Pin the version you validated afterwards.

If the library cannot move — a validated production line, a frozen environment — update the other side: the producer. The camera or frame-grabber vendor ships its GenTL producer inside its SDK or driver package, and a newer package may answer the query. Get it from the vendor or its distributor, never from a copy found elsewhere. If your frames come through a frame grabber, the producer travels with that card's driver, so the card vendor is the address.

If neither side can move, compute the stride yourself; the next two sections give the math and the worked case.

Recover the frame with stride math

The reshape you can defend in code review sizes the array from the geometry you know and treats both paddings as zero bytes when their queries are refused:

row_stride = width × bytes_per_pixel + row_padding frame_size = height × row_stride + end_padding

Here width and height are in pixels and rows, and every padding term is in bytes. Bytes per pixel for the common unpacked formats: Mono8 and the 8-bit Bayer variants carry 1 byte per pixel, Mono16 carries 2, RGB8 carries 3, YUV422 carries 2. Packed formats need the bit count: bytes per row = width × bits per pixel ÷ 8, rounded up. And remember the bytes you receive are not pixels until the layout is decoded — that distinction is the subject of Image bytes vs pixel data: decode before processing.

The code, with the test that proves the result:

# Documented names, from the payload component: component.width,
# component.height and component.data, as used in the traceback.
# The remaining variables are ours: ints, initial values shown.
width_px: int = int(component.width)     # 1920 in the worked case
height_px: int = int(component.height)   # 1200
bytes_per_px: int = 1                    # Mono8
row_padding: int = 0                     # query refused -> assume 0
end_padding: int = 0                     # query refused -> assume 0

row_stride = width_px * bytes_per_px + row_padding   # 1920 bytes
frame_bytes = height_px * row_stride + end_padding   # 2304000 bytes

assert len(component.data) >= frame_bytes            # delivered count backs it
img = component.data.reshape(height_px, row_stride)[:, :width_px]
assert img.shape == (1200, 1920)
assert img.nbytes == 2304000                         # 1200 * 1920 * 1

With both paddings genuinely zero this is the same reshape as component.data.reshape(component.height, component.width) — the assert is what makes the assumption visible. Hand the result to functions that need contiguous memory through numpy.ascontiguousarray(img).

Worked example: a 1920 × 1200 Mono8 frame

Inputs, with their sources: width 1920 px and height 1200 px come from the frame the code reshapes in the reported case; Mono8 gives 1 byte per pixel — a 2-D reshape into height × width only lands on that value for the delivered count, and Mono8 is what these stacks commonly negotiate; both padding values are unknown, because the query is the thing being refused, so they enter as 0 bytes.

Rule, in words: multiply width by bytes per pixel for the row stride, multiply by rows for the frame size, then compare against the delivered count. Arithmetic:

  • Row stride: 1920 px × 1 B/px = 1920 B.
  • Frame size: 1200 rows × 1920 B = 2,304,000 B, about 2.2 MiB (2,304,000 ÷ 1,048,576).
  • Test: the delivered count must equal 2,304,000 B. If it came back 2,313,600 B instead, the real stride is 2,313,600 ÷ 1200 = 1928 B, meaning 8 B of per-row padding the producer never reported — reshape to (1200, 1928) and slice [:, :1920].

Decision: a refused query means reshape straight to (1200, 1920) when delivered bytes equal 2,304,000. Any excess means the hardware pads after all, so divide the delivered count by the row count and slice. Never add an unreported padding to the byte count before reshaping — that is the original failure mode, just with your guess in place of the producer's answer.

When the image comes out sheared

The visual signature of a stride mismatch is a diagonal tear: each row lands one padding-width further out of place than the row above. Reshape with 1920 bytes per row when delivery actually carries 1928, and row 100 is 800 px (100 × 8) off. The count check in the worked example catches this before you ever see the tear, which is why it belongs in code rather than in your eyes.

If a shear does slip through, the repair takes one line of arithmetic: take the delivered byte count, divide by the row count, reshape with that stride, slice off the padding columns, and wrap with numpy.ascontiguousarray.

Which side to fix: the decision table

Symptom Test Action
Vendor tool shows frames; traceback dies at padding_y Query refused, delivery fine Update the acquisition library; its fallback assumes 0
Library pinned, newer producer offered Version list from the vendor Update the producer package from the vendor or distributor
Both sides pinned Delivered count vs computed count Stride fallback in your reshape with the byte-count assert
Sheared image after the fallback Delivered ÷ rows vs your stride Reshape with the real stride and slice the padding off
Call hangs, then times out No delivery at all Treat as a delivery fault, not a padding fault

Checks before you redeploy

Each item earns its place by preventing a repeat:

  • Pin both versions — the acquisition library and the producer package. "Worked yesterday" only means something when you can name yesterday's versions.
  • Log the pixel format at session start. The stride is a function of it, and silent fallbacks change it.
  • Assert delivered bytes against computed bytes once per session, on the first frame. One comparison catches format switches and hidden padding for the whole run.
  • Query optional buffer information once, not per frame. If refused, lock in the zero assumption for the session.
  • Allocate image buffers once per session instead of per frame; per-frame allocation is its own slowdown, covered in Reuse image buffers instead of per-frame allocation.
  • Wrap camera teardown in a finally block so a mid-run crash cannot leak device handles — the pattern is in Camera handle cleanup after Python script errors.
  • After any update on either side, run the reshape asserts against one real frame before trusting the stream.

When the capture side is the suspect

If several producers on the same machine refuse basic queries, or you are specifying hardware for a rebuild anyway, keep the bug in proportion: it is not hardware-specific, because any producer can lag on an optional query. What shortens the fix cycle is current, vendor-shipped driver packages and a consumer that tolerates refusals.

For a rebuild, size the camera side with the camera finder, and if the plan includes a capture card, start the configurator at the extras step. The two categories below cover the capture path end to end.

CamerasArea scan cameras

GigE, USB3, 10GigE and CoaXPress

1,287 models · from $149.00
AccessoriesFrame grabbers

CoaXPress, Camera Link and GigE cards

58 models · from $189.00

If a traceback still refuses to make sense, contact us; expert support is free with every order.

Questions

Does GC_ERR_NOT_IMPLEMENTED mean the camera is broken?

No. The status code says the producer refused one optional query, not that the sensor failed. If the vendor tool shows live frames and the traceback dies after delivery, the camera and the transport are healthy. Treat it as a version mismatch between the acquisition library and the producer, and close it by updating one of the two sides.

What is the difference between per-row and end-of-buffer padding?

Per-row padding is extra bytes appended after each image line so every row starts on an alignment boundary. End-of-buffer padding is extra bytes after the last row. Producers are not obliged to report either, which is why a consumer must handle the refusal. Assume 0 bytes, then prove the assumption by comparing your computed frame size against the delivered byte count.

How do I size a row for a packed 10- or 12-bit format?

Multiply width by bits per pixel and divide by 8: Mono10p packs 10 bits per pixel, so a 1920 px row is 1920 × 10 ÷ 8 = 2400 bytes and a 1200-row frame is 2,880,000 bytes; Mono12p gives 2880 bytes per row, 3,456,000 bytes per frame. Round up per row, then check the delivered count before reshaping.

Why did fetch_buffer work yesterday and fail today?

Something in the software stack moved, not the camera. The usual triggers are an acquisition-library update that newly reads the padding query, a producer or driver package update, or a pixel-format change that routes frames through a different buffer path. Pin the working pair of versions, and test any update against one real frame before the line sees it.

Is this failure specific to Python?

No. The refused query belongs to the GenTL interface every producer exposes, so a C++ or C# consumer that reads padding hits the same refusal. Python surfaces it earlier because building a NumPy array demands an exact byte count up front. The fix is identical everywhere: tolerate the refusal, assume zero padding, and check the delivered byte count.