System designGuide

Unify multi-brand camera SDKs in C# with one acquisition layer

Unify multi-brand camera SDKs in C#: one GenTL consumer plus a callback-and-semaphore grab loop, with timeout and bandwidth numbers to size each link.

LumaSpec Vision11 min read Share on LinkedIn

On this page 9 sections
  1. 01One wrapper per brand, or one GenTL consumer for all?
  2. 02What should the unified C# interface look like?
  3. 03Why callback plus semaphore, not a thread per camera?
  4. 04How long should the image timeout be?
  5. 05How much bandwidth does each camera need?
  6. 06What breaks multi-brand acquisition in practice?
  7. 07When is the standards route the wrong answer?
  8. 08What do you check before locking the design?
  9. 09Questions

Cameras from different brands rarely agree on callbacks, buffers, pixel formats or even units. If your C# project has to run a mixed fleet — a GigE camera on one station, a USB3 camera on the next — the expensive path is one vendor SDK per brand. The economical path is one acquisition layer: a GenICam GenTL consumer that loads any vendor's transport DLL, wrapped in a small ICamera interface driven by a callback-plus-semaphore grab engine. This guide gives you the interface shape, the timeout and bandwidth numbers to size it, and the checks to run before you commit the design.

One wrapper per brand, or one GenTL consumer for all?

Use one GenICam GenTL consumer for every GigE Vision and USB3 Vision camera in the plant, and write per-brand adapters only for what sits outside those standards; the consumer loads each vendor's transport-layer DLL, so brand becomes configuration, not code. That is the whole decision, and it is worth being strict about it.

GenICam splits the problem in two. GenApi defines camera features by node name — ExposureTime, Gain, TriggerMode, TriggerSource, AcquisitionStart — and GenTL defines the C transport interface that enumerates, opens and streams. Each vendor ships a producer DLL; your consumer loads whichever producers you install. A camera that takes ExposureTime in microseconds on one brand takes the same node, same unit, on another. The unified SDK you were about to write by hand already exists as the standard; your work narrows to a grab engine and a station-to-serial-number config file.

Image data follows the same rule: pixel format names like Mono8 and RGB8 mean the same bytes on every compliant device. GigE Vision and USB3 Vision are the compliance marks that make this true end to end, so check for them before buying — the interface filter in area scan cameras answers it in one click. For the standards themselves, see what each machine vision connectivity standard does, and for the software side of mixing makers, how to mix camera brands in one vision software stack.

What should the unified C# interface look like?

Thin enough to memorize: enumerate serial numbers, open one, set features by name, start in a mode with a callback, get an image with a timeout, close — eight to twelve members, nothing brand-specific in the signature. The class names below are this guide's own; the strings passed to Set are the standard feature names.

public enum GrabMode { Continuous, SoftTrigger, HardTrigger }

public interface ICamera
{
    List<string> Enumerate();                 // serial numbers, not IP addresses
    bool Open(string serialNumber);
    bool Set(string feature, string value);   // "TriggerSource", "Line1"
    bool Set(string feature, double value);   // "ExposureTime", 10000.0 (µs)
    bool Start(GrabMode mode, Action<byte[]> onFrame);
    bool GetImage(out Bitmap image, int timeoutMs);
    bool SoftTrigger();
    void Close();
}

The implementation gives timeoutMs a default of 3000, so casual callers write GetImage(out var bmp, 3000) and slow stations override it. Two choices are deliberate. Serial numbers, not IP addresses, identify cameras, because DHCP renumbers stations and serials never move. A Bitmap leaves the interface because every vision library reads one, but the byte array stays native inside — copying pixels per frame is the hidden cost, and reusing image buffers instead of per-frame allocation covers the pattern.

The same interface drives both sides of a mixed fleet, a GigE model and a USB3 model:

Area scan camerasHikrobot MV-CS200-10GC 20 MP Area Scan Camera, GigE, IMX183, Color, BasicMV-CS200-10GC
Sensor format
1"
Lens mount
C-mount
$739.00View part
Area scan camerasHikrobot MV-CS200-10UM 20 MP Area Scan Camera, USB3.0, IMX183, 2nd Gen., MonoMV-CS200-10UM
Sensor format
1"
Lens mount
C-mount
$759.00View part

A matched pair like this — same sensor family, one on GigE, one on USB3.0 — is the fastest way to prove the layer is genuinely brand- and transport-agnostic on the bench.

Why callback plus semaphore, not a thread per camera?

Because one callback plus one semaphore per stream moves frames with no polling loop: the transport thread runs the callback, the callback stores the newest pixels and releases, and GetImage blocks on the semaphore until a frame is ready or the timeout expires. A camera running at 100 fps uses one callback and one wait call, not a 100 Hz loop burning a core.

private readonly SemaphoreSlim _frameReady = new SemaphoreSlim(0, 1);
private readonly object _gate = new object();
private byte[] _latest;                      // one buffer, reused

private void OnFrame(byte[] pixels)          // transport thread — keep it short
{
    lock (_gate) { _latest = pixels; }
    if (_frameReady.CurrentCount == 0)       // never let the count pass one
        _frameReady.Release();
}

public bool GetImage(out Bitmap image, int timeoutMs = 3000)
{
    image = null;
    if (!_frameReady.Wait(timeoutMs))        // exactly one wait per frame
        return false;
    byte[] frame;
    lock (_gate) { frame = _latest; }
    image = ToBitmap(frame);                 // the one copy, made here
    return true;
}

Capping the semaphore at one makes the pattern drop stale frames when the consumer falls behind — the same behaviour as setting stream buffer handling to newest-only on the transport layer. Make exactly one wait call per frame you expect, never several, or you will report a timeout for a frame that already arrived; that is the failure mode behind frame didn't arrive within timeout errors.

Prove the pattern with a soak count, not by eye:

int delivered = 0;
for (int i = 0; i < 100; i++)
    if (cam.GetImage(out _, 3000))
        delivered++;
// pass: delivered >= 99 with the camera streaming at 5 fps (20 s soak)

One missed frame in a hundred at steady rate is the pass line. The check-then-release pair above can drop a single release under extreme load, which shows up as exactly one missed frame — that is why the test tolerates one. More than one means the consumer, not the camera, is the bottleneck.

How long should the image timeout be?

Make 3000 ms the standard value: it is ten times the 210 ms worst-case budget of a 20 MB frame over GigE with a 10 ms exposure, so a timeout means a real fault — no trigger, no link, no light — not a slow frame. Raise it to 5000 ms only when a PLC or mechanism can legally take seconds between triggers.

frame_MB = pixels × bytes_per_pixel ÷ 1,000,000 transfer_ms = frame_MB ÷ link_MB_per_s × 1000 timeout_ms = (exposure_ms + transfer_ms) × 10

Apply it to the 20 MP mono camera: 20 MP at one byte per pixel is 20 MB per frame. On GigE at about 100 MB/s, transfer is 20 ÷ 100 = 0.2 s = 200 ms; add a 10 ms exposure and the budget is 210 ms, so ten-times gives 2100 ms and 3000 ms clears it with room. On USB3 at about 350 MB/s, transfer is 20 ÷ 350 ≈ 57 ms, budget 67 ms, ten-times 670 ms — the same 3000 ms covers both transports without a config change.

How much bandwidth does each camera need?

Budget about 100 MB/s per GigE camera, about 350 MB/s per USB3 camera and about 250 MB/s per 2.5GigE camera, and keep 25 percent headroom on every link; the pixel math picks the interface before any SDK does.

Transport Line rate Usable payload Practical reach Host hardware
GigE 1 Gbit/s ~100 MB/s 100 m PoE network card
2.5GigE 2.5 Gbit/s ~250 MB/s 100 m 2.5G network card
5GBASE-T 5 Gbit/s ~500 MB/s 100 m 5G network card
10GigE 10 Gbit/s ~1000 MB/s 100 m on Cat6a 10G NIC or grabber
USB3 5 Gbit/s ~350–400 MB/s 3–8 m passive USB3 controller
CoaXPress CXP-12 12.5 Gbit/s per link ~1200 MB/s per link ~35 m CXP frame grabber
Camera Link — up to ~850 MB/s ~10 m Camera Link grabber

The middle row of that table has its own story: 5GBASE-T bandwidth, cables and distance splits the GigE/10GigE gap over ordinary cabling. For reliability comparisons beyond throughput, read the most reliable camera interface: GigE, USB3, Camera Link or CoaXPress.

Worked example, same 20 MP camera in Mono8. One frame is 20 MP × 1 byte = 20 MB. On GigE that is 200 ms of transfer per frame, a hard 5 fps ceiling. On USB3 at 350 MB/s it is about 57 ms, so 15 fps asks for 20 MB × 15 = 300 MB/s = 2.4 Gbit/s, which fits the 5 Gbit/s line with its encoding overhead intact. Color triples the bytes — RGB8 makes 60 MB and about 171 ms on USB3 — so run measurement stations in mono and pay for color only where the process needs it.

The host side unifies the same way, one card class per transport:

Frame grabbersHikrobot MV-GE2002P Dual-port PoE NIC, full height V2.0MV-GE2002P
Interface
GigE
Ports
2 ports
$249.00View part
Frame grabbersHikrobot MV-GU2204 MV-GU2200 Series Quad USB3.0 AdaptorMV-GU2204
Interface
USB3
Ports
4 ports
$469.00View part

The MV-GE2002P is a dual-port PoE NIC, so power and data for a GigE pair ride one card. The MV-GU2204 is a quad USB3.0 adaptor with 4 ports, keeping a four-camera USB3 station's traffic off the motherboard controller.

Cables unify too, because both transports use standard cordage:

The LS-CBL-0199 is a 3 m high-flex GigE cable with screw locks; the LS-CBL-0219 is its 3 m USB3.0 counterpart, both rated for drag-chain use — see cables for the full range of lengths and connector styles.

What breaks multi-brand acquisition in practice?

Five failures account for nearly every support case: DLL bitness, two clients on one camera, the same camera listed twice by two transport layers, shared state between cameras in one process, and buffer queues filling behind a slow consumer. Each has a one-line fix.

When is the standards route the wrong answer?

When the camera is not a GigE Vision or USB3 Vision device: Camera Link and CoaXPress need their grabber's own driver layer, analog cameras need decode hardware, and onboard-processing 3D profilers return processed profiles rather than raw frames — each of those gets a hand-written adapter class behind the same ICamera interface.

That adapter is exactly where per-brand code belongs: one class per family, the station code never sees the brand. Vendor features beyond the standard names are still reachable as nodes by name, but that line of code is not portable, so quarantine it inside the adapter. The grabber hardware for those families is in frame grabbers; the speed story of onboard processing is in how onboard data reduction enables 3D profiler profile rates, and the legacy case is covered by analog versus digital cameras for machine vision.

What do you check before locking the design?

Five bench checks, in this order: an x64 build loading every producer DLL, single ownership of each camera, newest-only buffering, 25 percent bandwidth headroom per link, and a 100-frame soak at production frame rate that misses at most one frame.

Design:

  • Every camera is GigE Vision or USB3 Vision; anything else gets an adapter class.
  • The interface holds at eight to twelve members; brand strings appear only in adapters.
  • Config maps station to serial number, never to an IP address.

Bring-up:

  • Platform target x64; every DLL the same bitness; no viewer left running.
  • One acquisition object per camera; one transport layer per interface.
  • Stream buffer handling set to newest-only before any timing work.

Sign-off:

  • Bandwidth per link at 75 percent of usable payload or less.
  • 100-frame soak passes, drop counter zeroed afterwards.
  • Timeout 3000 ms unless a measured trigger cycle set it higher.

From here, filter the camera finder by interface, start the system configurator at the camera step for a matched camera, NIC and cable set, or bring the station list to the applications team through the contact page — expert support comes with every order. More integration write-ups sit on the systems topic page.

Questions

Does a GenTL consumer expose every feature a vendor SDK does?

The standard feature names cover exposure, gain, trigger mode and source, I/O line control and pixel format on any compliant camera, so day-to-day control is fully portable. A feature a vendor invented outside the standard still appears as a node you reach by name, but that line of code is brand-specific; keep it inside the adapter class so the shared interface stays clean.

How many cameras can one C# process acquire through one consumer?

One acquisition object per camera, with no static state shared between them; the practical ceiling is link bandwidth, not object count. Two 6 MP cameras at 15 fps in Mono8 are 90 MB/s each, so both sit on their own GigE port of one card. Four such cameras total 360 MB/s, which a GigE uplink cannot carry: move the uplink to 10GigE or split it across 2.5GigE ports.

Why does GetImage return false while the camera is streaming?

Three usual causes, in order: the timeout is shorter than the trigger cycle, another client still owns the device, or the buffer queue delivers oldest-first behind a slow consumer. Time one full trigger cycle and set the timeout to ten times it, close any viewer that has the camera open, and switch the stream to newest-only so stale frames drop instead of stacking up.

Should the unified layer return Bitmap objects?

A Bitmap is the easiest interchange format when several libraries consume the images, and it converts losslessly to any of them. It also copies pixel data on every frame: about 20 MB per copy for a 20 MP mono frame, which at 15 fps adds 300 MB/s of memory traffic. Return the byte array by default and build a Bitmap only where a consumer actually needs one.

Do GigE and USB3 cameras need a frame grabber?

No acquisition card is required for either: a network card with PoE ports powers and carries GigE Vision cameras, and any USB3 controller with headroom carries USB3 Vision cameras. Cards still earn their slot on multi-camera stations, where they give each camera a dedicated controller, locked connectors and trigger breakout. Camera Link and CoaXPress cameras are the ones that cannot run without a grabber.

What platform target should the C# project use?

x64, matching the producer DLLs you load. A 32-bit runtime cannot load 64-bit DLLs and fails at start-up with a load error rather than at the first camera call. Keep every DLL in the output directory at the same bitness and set the platform target in the project file, so a clean clone of the repository builds and runs without manual steps.