System designExplainer

Multiple GenTL cameras in one process: one acquisition object each

Why one GenTL acquisition object per camera is the right way to run multiple cameras in one Python process: modules, threads, bandwidth math and a checklist.

LumaSpec Vision9 min read Share on LinkedIn

On this page 9 sections
  1. 01The decision: one object per camera
  2. 02How the transport layer is shaped for it
  3. 03What each object owns
  4. 04One fetch thread per stream
  5. 05The bandwidth arithmetic that picks your ports
  6. 06Ports, cables and stable device order
  7. 07When one object per camera is the wrong shape
  8. 08Checks before you commit
  9. 09Questions

Running several cameras in one program is routine in spectral imaging, multi-view inspection and stereo rigs. If your acquisition stack talks GenTL, the question "can one acquisition object own all my cameras?" has a definite answer: it should not, and it does not need to. Give every camera its own acquisition object and its own fetch thread, let every object share one producer module and one process, and match objects to cameras by serial number rather than by position in the enumeration. This explainer covers the module structure that makes the pattern work, the threading that keeps frame rates independent, the bandwidth arithmetic that decides your ports, and the checks to run before you commit.

The decision: one object per camera

A GenTL stack is already multi-camera below your code. The producer module enumerates every device it can reach; it is your application object that maps to a single camera. So the architecture that fits is one acquisition object per camera, one fetch thread per stream, and one producer module per transport shared by the whole process. A single object that owns several cameras adds a second, redundant layer the transport layer never asked for.

The payoffs are concrete. Each object is line-for-line the code you already run for one camera, so a bench test with a single device exercises it. A timeout or a pulled cable parks one thread instead of freezing the whole inspection. And the pattern is transport-agnostic: the same structure carries GigE, USB3, 10GigE and CoaXPress devices, which is usually why a spectral or multi-view rig wants one process at all.

How the transport layer is shaped for it

GenTL, the transport-layer interface in the GenICam standard family, organizes everything as a chain of modules. One system module per producer; interface modules beneath it; device modules beneath an interface; and under each device, its data stream modules and their buffers.

The standard allows an interface module to hold zero, one or many device modules, and every device belongs to exactly one transport technology. Multi-camera is native to the producer; it is the application-side object that is single-camera by design.

Two facts settle the architecture. A data stream belongs to one device — no stream serves two cameras — so "one object owning many cameras" can only mean one object juggling many streams, which is the code you would write across objects with none of the isolation. And one producer file covers one transport: a GigE Vision producer and a USB3 Vision producer are separate modules, so a mixed rig loads one of each.

Before you trust a producer with a given transport, work through GenTL producer GigE Vision support: check compatibility. For how the modules hand frames to your code, Depth camera GenTL producer setup: how the data path works traces the same path on a 3D device.

What each object owns

Every acquisition object wraps the same chain for one camera: attach to the loaded producer, walk system → interface → its device, open one data stream, and announce a private buffer pool. Everything below the device is per-object; everything above it is shareable.

# Illustrative pseudocode: every name here is ours; your GenTL
# front-end will name these steps its own way.
class CameraAcquisition:
    def __init__(self, cti_path: str, serial: str):
        self.cti_path = cti_path    # producer module, e.g. "gige_producer.cti"
        self.serial = serial        # serial string from the device info list
        self.frames = 0             # frames pulled from this object's stream

acq_a = CameraAcquisition("gige_producer.cti", "left-camera-serial")
acq_b = CameraAcquisition("gige_producer.cti", "right-camera-serial")

assert acq_a.serial != acq_b.serial   # two objects, two devices, never one

The test that proves the wiring is three asserts at startup: the serials are unique, each object opened a different device, and both streams report announced buffers. Fail any one of them and you stop on the bench in a second, not mid-shift.

After each fetch, return the buffer to that stream's pool before waiting on the next frame. Requeuing a buffer that is already queued breaks acquisition in ways that surface much later; Requeue an already queued camera buffer: why it breaks acquisition dissects the failure.

One fetch thread per stream

A fetch call blocks until its camera delivers a frame or the timeout expires. Share one thread across two streams and the second camera waits behind the first: its buffers fill, the pool runs dry, frames drop. N cameras therefore means N fetch threads, each pushing payloads into one bounded queue that your processing stage drains.

Fetch threads cost almost nothing because each spends its life blocked in the transport layer rather than executing. That holds in Python too: a thread blocked on a buffer consumes no interpreter time. Budget cores for the processing that consumes frames, not for the fetch loops. When processing cannot keep up with acquisition, set the policy at the queue — drop oldest, batch, or throttle — instead of letting the stream stall; Acquisition faster than processing: fix the frame rate gap lays out the options.

The bandwidth arithmetic that picks your ports

The software pattern never rescues an undersized link, so size the links before the threads:

bytes_per_frame = width_px × height_px × bytes_per_pixel data_rate = bytes_per_frame × frame_rate

Work at 1 byte per pixel (a Mono8 frame); an RGB frame triples the rate. A 12 MP frame is 12 MB, so one 12 MP camera at 30 fps produces 12 MB × 30 = 360 MB/s, and two of them 720 MB/s.

Over USB3: the LS-CMR-0166 outputs 4000 × 3000 frames at 30 fps. Two such cameras total 720 MB/s against a USB 3.0 link's 5 Gbps ≈ 625 MB/s at the wire — it does not fit, even before overhead. Decision: one USB3 host controller per camera, with the power draw checked as well — Multiple USB cameras on one host: bandwidth and power budget sizes both — or move the rig to 10GigE.

Over GigE: the LS-CMR-0053 is a 12 MP GigE camera at 9 fps. At 1 byte per pixel that is 12 MB × 9 = 108 MB/s, or 108/125 ≈ 86% of a 1 GbE port's 125 MB/s line rate. One camera fits a port, barely; two never fit one port through a switch. Decision: one port per camera, keeping each port's summed image rate under 80% of its line rate as the working budget.

Both are 12 MP cameras built on the same sensor:

Specification LS-CMR-0166 LS-CMR-0053
Sensor model Sony IMX226 IMX 226
Resolution 4000 × 3000 (12MP) 4000 px x 3000 px
Max. frame rate 30 fps 9 fps
Pixel size 1.85 μm 1.85 μm x 1.85 μm
Shutter rolling shutter Rolling Shutter
Data interface USB 3.0 GigE
Pixel bit depth 10-bit 8 / 10 / 12 bits
Price $560.00 $560.00

All 2 share: Sensor size 1/1.7". Values as printed in each product's data sheet; open a part for its drawing and full specifications.

On a 10GigE link the same pair totals 720/1,250 ≈ 58% of the 1,250 MB/s line rate, inside the 80% budget with headroom — that is the port choice once frame rates climb. The card and cabling side of that budget is worked through in Two GigE cameras on one host: NIC ports and bandwidth budget.

Ports, cables and stable device order

Give every camera its own port on the host. The MV-GE1104 is a 4-port GigE frame grabber; the MV-GT2104 is a quad-port copper 10GbE NIC for the higher-rate case. Both live in the frame grabbers category.

Frame grabbersHikrobot MV-GE1104 4-port GigE frame grabberMV-GE1104
Interface
GigE
Ports
4 ports
$699.00View partBuild a system with it
Frame grabbersHikrobot MV-GT2104 Quad-port copper 10GbE NICMV-GT2104
Interface
10GigE
Ports
4 ports
$1,429.00View partBuild a system with it

Cable reach often decides the transport before software does: passive USB3 cables hold roughly 3 to 5 m, while GigE carries 100 m on Cat5e, which is why rigs with cameras spread along a line standardize on GigE. Drag-chain-rated versions and locking connectors are listed under cables.

The device information list is an enumeration, not an assignment. Its order follows what the producer found first, and a reboot or a replug can swap two entries. Match each object to the camera's serial number, or set a user-defined device ID on each camera once and match on that. The hardware-side version of that discipline is in Assign cameras to ports reliably in a multi-camera system, and Multiple depth cameras on one host: select each by serial number shows it for devices with no user-settable ID.

When one object per camera is the wrong shape

Three cases bend the rule. A device that exposes several streams — intensity and depth from one 3D sensor, say — keeps one object per device, but the thread rule is unchanged: one thread per stream, inside that object. When you need fault isolation rather than convenience, invert the pattern and give each camera its own process; Multiple vision software instances: one camera per process is that architecture. And software objects synchronize nothing: if frames must align, share a hardware trigger line, because the pulse is what aligns them — Multiple GigE cameras, same trigger: why frames arrive incomplete covers what breaks when the trigger path is wrong.

One more limit: a container sees only the producers installed inside it, so discovery returns an empty list if the module stayed on the host. GenTL camera discovery in containers: diagnose missing devices walks that diagnosis.

Checks before you commit

Run this list on the bench with every camera connected:

  • Enumerate once at startup; assert each object bound a different serial.
  • One producer module per transport, loaded once, shared by every object.
  • One fetch thread per stream; one bounded queue feeding processing.
  • 3 to 5 announced buffers per stream, requeued after each payload handoff.
  • A per-fetch timeout, so one stalled camera parks one thread only.
  • Summed image rate per port under 80% of that port's line rate.
  • Camera order fixed by serial or user-set device ID, never by enumeration position.
  • Frames from different cameras compared by timestamp only when they share a trigger.

Pick the cameras and ports first — the software pattern is the easy half:

Build the whole station

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.

Start with the camera

The area scan cameras category and the camera finder narrow the sensor and interface choice. With the parts fixed, the structure above is wiring, not a redesign.

Questions

Can one acquisition object ever own two cameras?

Not usefully. A GenTL data stream belongs to exactly one device, so a second camera would need its own stream anyway, and every fetch would serialize behind the first camera's blocking wait. The only object worth sharing is the producer module and its enumeration. The exception is one device exposing several streams — keep one object per device and one thread per stream.

How do I stop two objects grabbing the same camera?

Match each object to the camera's serial number, never to its position in the enumeration, and assert at startup that all serials in your list are unique before opening any device. The transport layer opens a device exclusively: the second open attempt fails. An assertion at construction turns that into a one-second startup error instead of a fault that appears mid-shift.

How many buffers should each data stream announce?

Size them from your worst processing pause: two buffers cover the one in transit and the one your code holds, then add one for every frame that arrives during the pause. A 100 ms hiccup at 30 fps is 3 frames, so 2 + 3 = 5 buffers per stream. Announcing far more than that only hides a queue you have not sized on purpose.

Can two GigE cameras share one port through a switch?

No. A switch adds ports, not bandwidth: both cameras still contend for the one 125 MB/s line rate of the uplink into the host, and two 108 MB/s cameras do not fit. Give each camera its own port on a multi-port card and keep the summed image rate of the cameras on each port under about 80% of that port's line rate.

Do the fetch threads need one core each?

At machine-vision frame rates, no. Each thread spends almost all of its time blocked in the transport layer waiting for the next buffer, so two or four fetch threads cost a fraction of one core between them — spend your core budget on the processing that eats frames, not on fetch loops. If your stack exposes frame-arrived callbacks, you can drop the explicit threads entirely.