A live view that runs for ten frames and then locks is not a flaky camera; it is a buffer that never went home. When you drive a GenICam camera from Python through a GenTL producer, every buffer fetch_buffer hands you is on loan: the producer wrote the frame into memory it owns, and only buffer.queue() puts that memory back into circulation. Skip the queue call and the stream dies at a fixed frame count — the number of buffers announced at start. Here you get the pool arithmetic, the four causes ranked by how often they bite, the fetch-copy-queue fix as code, the traceback reading that confirms it, and the checks that prove the copy is real.
The freeze count is your buffer pool size
The signature is a stall at a constant frame count. Acquisition starts, frames arrive, the display updates, then fetch_buffer stops returning and the image freezes. Restart and it locks at the same count: ten, fifteen, whatever the producer announced at start. That determinism is the tell. A cable fault, a driver hiccup or a network collision freezes the stream at random times; a drained pool freezes at the same frame every run.
Prove it in one run. Print a counter each time fetch_buffer returns and note the number where the stall lands. If the pool holds ten buffers, you can fetch ten, and frame 11 has nowhere to land — so the view dies at frame 11, run after run. That count is also your first fact for any support conversation: a freeze that moves between runs is a different fault with a different fix.
Why the pool drains: fetch takes, queue returns
A GenTL producer owns the frame memory. When acquisition starts it prepares a fixed set of buffers and feeds them to the camera's acquisition engine; the GenICam GenTL standard defines each buffer as the destination the engine writes a frame into. A completed frame fills one buffer and joins the filled queue. fetch_buffer hands you the head of that queue. Nothing returns a buffer to the free set except queue() — that is the whole contract, and keeping it is your job, not the library's.
Worked example: a 5 MP Mono8 stream at 2456 × 2058 pixels. frame_bytes = 2456 × 2058 × 1 = 5,054,448 bytes ≈ 4.8 MiB. A ten-buffer pool holds 10 × 5,054,448 = 50,544,480 bytes ≈ 48 MiB, and at 20 fps it is exhausted at frame 11, i.e. 11/20 = 0.55 s after start. Double the frame rate and the same leak kills the stream twice as fast.
Four causes, tested in order
Ranked by how often each explains the freeze, with the test that separates them:
- The fetch never queues. A helper function fetches, reshapes, returns the array and lets the buffer go out of scope — no queue() anywhere in the path. The pool shrinks by one per frame until it is empty. Test: the counter from the previous section; a constant stall count at the pool depth is this cause.
- The queue is skipped on the error path. An exception between fetch and queue — an incomplete frame is the classic trigger — leaks that buffer. One reported producer ran dry after fifteen incomplete-frame errors because every dropped buffer stayed unqueued. Test: raise deliberately every tenth iteration; if the stall count falls below the announced depth, the error path leaks.
- You queue before you copy. The stream survives but frames come back black, because the producer has already written the next exposure into the memory your variable points at. Test: print image.sum() on the kept array twice; a value that changes between reads means shared storage.
- The copy is an alias. buf = buffer binds the same object; copy.copy(buffer) duplicates the wrapper, not the pixel storage. Both leave the image pointing into producer memory. Test: queue, then inspect the kept image — zeros or tearing mean alias.
| Symptom | Cause | Fix |
|---|---|---|
| Freezes at a constant frame count | Pool never refilled | One queue() per fetch, or the with-form |
| Stall count falls run over run | Error path skips queue() | Queue in finally |
| Black frames, drifting values | Copy made after queue() | Copy inside the hold |
| Reshape error with shape (0,0) | Dimensions read after queue() | Read width and height inside the hold |
| Freezes at frame 1, or at random times | Not the pool | Route through the section below |
The fix: fetch, copy, queue
Three rules cover every case: every fetch pairs with exactly one queue(); the copy happens while you still hold the buffer; width and height are read inside the hold too. The with-form makes the first rule automatic — the library queues the buffer when the with-scope exits, which is that construct's documented purpose:
from harvesters.core import Harvester
h = Harvester()
h.add_cti_file(r'C:\GenTL\your_producer.cti') # your producer's .cti file
h.update_device_info_list()
ia = h.create_image_acquirer(0)
ia.start_image_acquisition()
while True:
with ia.fetch_buffer() as buffer:
component = buffer.payload.components[0]
height = int(component.height) # row count, int
width = int(component.width) # column count, int
image = component.data.reshape(height, width).copy() # own storage
# queueing already happened on scope exit; image is yours to keep
# display or store image here
height and width are ints taken from the payload; image is a fresh uint8 array (Mono8) that owns its memory. Without the with-form, pair fetch and queue by hand with try/finally so an exception cannot skip the queue:
buffer = ia.fetch_buffer()
try:
component = buffer.payload.components[0]
image = component.data.reshape(component.height, component.width).copy()
finally:
buffer.queue()
One-line proof that the copy is real: print(image.base). A copied array owns its storage and reports None; a view over component.data reports that array as its base, which means the producer can still write through your variable.
If you only display inside the hold and never keep the frame, you can skip the copy: reshape returns a view and the with-form queues it on exit. The moment a frame outlives the hold, the copy is not optional.
What the traceback tells you: (0,0) and black frames
The all-zero case produces a reshape error worth reading closely: cannot reshape array of size 5054448 into shape (0,0). The target (0,0) is the diagnosis — width and height were read from a buffer that queue() had already recycled, and the producer had reset them. The size in the same message is the real frame: 2456 × 2058 = 5,054,448 bytes, a 5 MP Mono8 frame. One message therefore carries both the cause and the geometry your reshape handled fine one frame earlier.
Queue first and copy afterwards, and there is no exception at all — just frames of zeros, because the producer has already written the next exposure into the storage your variable references. copy.copy(buffer) fails the same way: it builds a second wrapper around the same storage, so the alias queues along with the original and reads back whatever landed in that memory last. The only copy that survives queue() is an array with its own allocation: reshape(...).copy(), or np.array(component.data) if you prefer the explicit constructor.
Size the pool and the RAM first
Fixing the queue discipline is the cure; sizing the pool is the prevention. Ask what the worst stall your loop can suffer is, then announce:
With a 30 fps loop and a 200 ms processing spike, 30 × 0.2 = 6 frames can be in flight, and the two spares absorb a second back-to-back stall: 6 + 2 = 8 buffers. Announce fewer and one slow iteration drains the pool mid-run; announce more and the only cost is host RAM: a 4000 × 3000 Mono8 frame is 4000 × 3000 × 1 = 12,000,000 bytes ≈ 11.4 MiB, so eight buffers hold 8 × 12,000,000 = 96,000,000 bytes ≈ 92 MiB.
The copy itself costs almost nothing. One 12,000,000-byte memcpy at a typical 5 GB/s takes about 2.4 ms (an estimate; desktop memory bandwidth usually runs higher), so 30 fps adds about 360 MB/s of copy traffic — a rounding error for a desktop memory bus.
A 12 MP, 1/1.7" rolling-shutter GigE camera that runs 9 fps makes the pace concrete:
- Sensor format
- 1/1.7"
- Lens mount
- C-mount
- Interface
- GigE
- Resolution
- 12 MP
At 9 fps, a ten-buffer pool is exhausted at frame 11, which lands 11/9 ≈ 1.2 s after start — barely a second of grace. At 170 fps the same leak ends the stream in 11/170 ≈ 65 ms. The faster the camera, the less time a missing queue() leaves you to notice.
When the freeze is not the pool
The constant stall count points at the pool; other freeze signatures point elsewhere:
- Locks at frame 1, every run: format or channel negotiation, not the pool — work through the checks in camera preview freezes after first frame.
- The interface locks up while acquisition keeps running: fetch is blocking your UI thread — move it off with GUI freezes during image capture.
- fetch_buffer times out from the first frame: nothing is arriving — the stages to probe are in Getdata timeout: no frames available.
- The producer never enumerates at all: the .cti side is the suspect, as in GenICam producer loading errors in Linux applications.
- Start/stop cycles hang instead of the stream: acquisition calls must pair like fetch and queue, as single frame acquisition still busy explains.
Two queue mistakes sit either side of the fix. Queueing a buffer twice corrupts the pool in its own way — see requeue an already queued buffer. Keeping every copied frame in an unbounded Python list trades a freeze for a memory leak, which bounding the Python frame queue prevents. The callback variant of the same discipline — copy, then requeue — is in copy the buffer before you queue it, and multi-camera hosts add one pool per device, as multi-camera acquisition freezes covers.
Prevention checklist
Run through this before you blame the camera:
- Count fetches and queues separately; the two totals must advance in lock-step, one queue per fetch.
- Keep every fetch and its queue in one scope — the with-form or try/finally, never a hand-off across a return.
- Copy inside the hold and check image.base is None on the first frame.
- Read width, height and pixel format inside the hold; never touch a buffer after queue().
- Size the pool as ceil(fps × worst stall) + 2 and budget width × height × bytes per pixel of RAM per buffer.
- If frames can arrive incomplete, queue the buffer even on the error path, then handle the bad payload.
A bench that keeps the numbers honest
Nothing above is hardware-specific: any GenICam GigE camera through any GenTL producer follows the same contract, so the GigE area scan cameras category and the camera finder are where a second unit for comparison comes from. On the host side, the frame grabbers category and the accessories finder carry the interface cards.
A 2.5 MP global-shutter GigE camera covers the classic bench case:
- Sensor format
- 1/1.7"
- Lens mount
- C-mount
- Interface
- GigE
- Resolution
- Up to 3 MP
The host side of the contract is the network card; a dual-port PoE unit is a typical pick:
- Interface
- GigE
- Ports
- 2 ports
New build rather than a rescue? The configurator pairs camera, lens and host interface in one pass:
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.
More on the acquisition lifecycle lives on the systems topic page.
The decision, per case: a freeze at a constant frame count means queue discipline — fetch, copy, queue, with the error path queuing too; black frames or a (0,0) reshape error mean the copy and the dimensions move inside the hold; a freeze at frame 1 or at random times is not the pool, and the routing above sends it to the right fix. If the counters still will not pair, /contact/ reaches the applications team with your traceback, frame counter and camera model.
Questions
How many buffers should the GenTL producer have?
Size the pool from the worst stall your loop can suffer, not the average pass: buffers = ceil(fps × stall_seconds) + 2. At 30 fps with a 200 ms processing spike that is ceil(30 × 0.2) + 2 = 8. Fewer, and one slow iteration drains the pool mid-run; more only costs host RAM, about 11.4 MiB per buffer for a 4000 × 3000 Mono8 stream.
Does the with-statement queue the buffer if my processing raises?
Yes — the buffer returns to the pool when the with-block exits, exception or not, so the pool stays healthy through a crashing iteration. What the with-form cannot do is copy for you: if the exception lands before your .copy() line, that frame is lost while the pipeline survives. Put the copy as the first statement inside the block.
Why does copy.copy(buffer) not protect my image?
It duplicates the wrapper object, not the pixel storage: the copy holds references to the same producer-owned memory, so the next frame lands inside the object you kept. Plain assignment (buf = buffer) binds the same object outright. The only copy that counts is a new array with its own allocation — component.data.reshape(height, width).copy() — taken before queue().
Does every camera on a host get its own buffer pool?
Yes: each device on each producer announces an independent pool, so a three-camera host runs three drain clocks and freezes at three different frame counts. When every view stalls together at the same count, suspect a shared host-side cause such as the network interface or a starved consumer process; differing counts point at the loop with the smallest number.
How do I measure the pool depth without guessing?
Count fetches. Print a counter each time fetch_buffer returns and note the frame where the stream stalls; that count is the announced depth and it repeats run after run. Reported cases freeze at ten frames on one set-up and drain after fifteen dropped buffers on another, so a fixed count of that order is the signature — a count that changes between runs has a different cause.


