System designHow-to

Stream variable image sizes over GigE Vision

Why a GigE Vision sender declares one fixed image size, how to size the buffer for your largest frame, and how to repack a variable image stream into it safely.

LumaSpec Vision12 min read Share on LinkedIn

On this page 10 sections
  1. 01Can a GigE Vision sender stream images of different sizes?
  2. 02Why does the GenICam node map lock the image size?
  3. 03What declared width and height should I pick?
  4. 04How do I repack variable frames into the fixed buffer?
  5. 05How much bandwidth does a padded stream need?
    1. What does the worked example cost on one GigE port?
    2. What does the same stream cost in RGB8?
  6. 06What breaks when the receiver sees the black padding?
  7. 07Why does my recreated server vanish from discovery?
  8. 08When is one fixed size the wrong answer?
  9. 09What should I check before shipping it?
  10. 10Questions

You are on the sender side of GigE Vision: your application takes an incoming image stream — from a camera, a file set or your own processing — and exposes it to the network as a GigE Vision device. The frames arriving at your input change size; the device you expose cannot. Its GenICam node map declares one Width and one Height, and every receiver on the network allocates its buffers from that single declaration. This guide gives you the decision, the arithmetic and the failure modes: declare the largest frame once, repack every smaller frame into the declared geometry, budget the link for the declared payload rather than the live one, and treat any geometry change as an application restart. The worked example is a Mono8 stream mixing 1280 × 256 and 2048 × 1024 frames at 30 fps over one GigE port.

Can a GigE Vision sender stream images of different sizes?

Yes on the wire, no through the node map: the sender can fill each transmit buffer with different content, but it publishes one fixed Width and Height at creation, so the working design is to repack every incoming frame into that declared geometry instead of resizing the stream.

What the sender must not do is advertise new geometry mid-stream. GigE Vision frames carry pixels, not per-frame dimensions, and the receiver signed a contract with the node map it read at discovery: buffer count and buffer size are fixed from that moment. Rewriting Width or Height under a connected receiver either fails, tears frames, or both.

The tempting escape — stop the server, rebuild it with a bigger declaration — usually makes the device disappear from discovery until the whole process restarts, which is the trap most integrators hit first. Declare generously once, then vary the content: one buffer, one geometry, every frame packed into it.

Why does the GenICam node map lock the image size?

Because the receiver sizes its memory from the node map it read when it connected: it allocates fixed buffers of Width × Height × bytes per pixel, and the protocol carries no per-frame geometry, so a size that changes under a connected receiver desynchronizes or tears the stream.

Walk through the receiver's startup and the lock is obvious:

  • At discovery it reads the device's declared Width, Height and PixelFormat.
  • It allocates a pool of buffers, each exactly payload_bytes deep, and hands them to the transport.
  • It fills each buffer from incoming packets and passes full buffers to the application; nothing in the packet stream flags the live geometry of that frame.
  • If the device sends more bytes than a buffer holds, packets spill and bands go missing; fewer, and the receiver stalls or pads.
payload_bytes = width × height × bytes_per_pixel link_load = payload_bytes × frame_rate / link_budget_bytes_per_s

Those two lines drive everything below: the first sizes every buffer on the network, the second decides whether the link survives the worst-case frame. The same memory discipline applies on your side — keep one reusable transmit buffer per stream instead of allocating per frame, as in reuse image buffers instead of per-frame allocation.

What declared width and height should I pick?

Declare the largest width and height any incoming frame will ever have, measured not remembered: a stream mixing 1280 × 256 ROI windows with 2048 × 1024 full frames declares 2048 × 1024, because the largest frame sets the buffer and every smaller one simply arrives pre-packed inside it.

Three refinements:

  • Take the maximum across every source, mode and roadmap item you can name; a declaration you have to raise later is a restart on every receiver.
  • Round width so each row is a multiple of 4 bytes for Mono8 (a multiple of 4 pixels), which keeps receiver-side copies word-aligned; packed 10- and 12-bit formats want wider alignment.
  • Resist extra margin beyond that: every declared row is transmitted on every frame, so margin is a permanent bandwidth tax — the overhead numbers in the section on the wrong answer show what it costs.

In the worked example the incoming stream is ROI windows from a GigE area scan camera on the capture host, running between 1280 × 256 and 2048 × 1024, so the declaration is 2048 × 1024. The camera feeding that stream:

Area scan camerasHikrobot MV-CT0MG0060/R/NN/NN 6 MP Area Scan Camera, 3rd Gen., GigE, Mono, BASEMV-CT0MG0060/R/NN/NN
Sensor format
1/1.8"
Lens mount
C-mount
Interface
GigE
Resolution
6-10 MP
$909.00View partBuild a system with it

How do I repack variable frames into the fixed buffer?

Copy each incoming frame into the top-left corner of the declared buffer, fill the remaining right and bottom region with the pad value, and queue the buffer unchanged — resize only when the receiver displays images, never when it measures them.

  1. At startup, survey every incoming geometry and declare Width, Height and PixelFormat once, before any receiver can discover the device.
  2. Allocate one reusable output buffer at the declared payload — 2,097,152 bytes in the worked example — and reuse it every frame instead of allocating per frame.
  3. Copy rows using the source's row stride, the value your capture SDK reports, not width × bytes per pixel.
  4. Fill the pad region — right of each row and below the last row — on every frame, including the bottom rows; never transmit stale bytes left by a previous larger frame.
  5. Choose the pad value deliberately: black (0) for measurement, edge-replication when the receiver runs smoothing or morphology that would treat a black border as a real edge.
  6. Skip resizing unless the receiver only displays; resizing changes the millimetres-per-pixel scale that every downstream measurement relies on.
  7. Copy before you queue: if the pipeline reuses or rewrites the incoming buffer, hand the server its own bytes — the torn frames in a camera callback failure is the receiver-side twin of this mistake.
  8. Leave Width and Height untouched while streaming; a genuinely larger source later is a new declaration, a restart and a receiver re-discovery, not a runtime patch.

How much bandwidth does a padded stream need?

Budget the declared size, not the live one: at 2048 × 1024 in Mono8 and 30 fps the stream moves 62.9 MB/s, about 55 % of what one GigE port reliably carries, so the declaration absorbs the ROI savings and the link plan must respect that.

What does the worked example cost on one GigE port?

  • Payload per frame: 2048 × 1024 × 1 byte = 2,097,152 bytes (2 MiB).
  • Per second: 2,097,152 × 30 = 62,914,560 bytes/s ≈ 62.9 MB/s ≈ 503 Mb/s.
  • Budget per port: a 1 Gb/s link moves 125 MB/s of raw bits; after Ethernet, IP/UDP and GigE Vision headers plus inter-packet gaps, budget 100-115 MB/s of image payload per port — the top of the range with 9000-byte jumbo frames, the bottom with 1500-byte frames.
  • Load: 62.9 / 115 ≈ 55 %, leaving about 52 MB/s of headroom. Hold the worst case under 70-80 % of budget so bursts and packet resends have somewhere to go.
  • Ceiling: 115 MB/s ÷ 2,097,152 bytes ≈ 54.8, so the same link carries the declared Mono8 frame up to roughly 54 fps before it saturates.

One clean port carries the Mono8 example with headroom, and a second port keeps control traffic and the capture feed off the stream:

Frame grabbersHikrobot MV-GE2002P Dual-port PoE NIC, full height V2.0MV-GE2002P
Interface
GigE
Ports
2 ports
$249.00View partBuild a system with it

What does the same stream cost in RGB8?

Payload triples: 2048 × 1024 × 3 = 6,291,456 bytes per frame, which is 188.7 MB/s at 30 fps — over the 115 MB/s budget of one GigE port. The port tops out at 115 MB/s ÷ 6,291,456 bytes ≈ 18.3, so about 18 fps is the most it carries. To keep 30 fps, move the path to 10GigE, where 188.7 MB/s sits under 20 % of a 1000-1150 MB/s budget. That path moves to 10GigE:

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

Whatever the sender budget says, the receiving host needs its own port for the stream: a GigE frame grabber with four ports keeps vision traffic off the office LAN:

Frame grabbersHikrobot MV-GE1104 4-port GigE frame grabberMV-GE1104
Interface
GigE
Ports
4 ports
$699.00View partBuild a system with it

Overrun never raises an error at the sender — it shows up downstream as missing bands inside frames, the signature described in GigE Vision packet loss horizontal lines in live images.

What breaks when the receiver sees the black padding?

Anything that assumes the frame edge is the part edge: histogram normalization picks up the black bars, edge-to-edge measurements report the declaration instead of the part, and classifiers that never saw padding drift on every frame, so the receiver needs to know the live extent.

You own the node map, so publish the extent in it: add two read-only integer features that your sender updates per frame — the names are yours to choose, for example live width and live height — and agree with the receiver that they describe the last transmitted frame. Pair that with a placement convention: always put the live image at the top-left corner, so a receiver can crop deterministically without a second coordinate channel. Log the extent per frame on the sender too; when a measurement goes wrong three weeks later, that log is the only record of what the receiver actually saw.

Why does my recreated server vanish from discovery?

Because discovery presence belongs to the process that first created the server: stopping and recreating it inside the same run frequently leaves the device invisible to receivers until the whole application restarts, so plan geometry changes as restarts, not as live rebuilds.

Discovery is a small control service bound to a socket your process owns; tearing the server down does not always release that binding cleanly, and receivers that cached the device's identity keep asking a ghost. The reliable pattern is boring:

  • Create the server once per process start, with the final declaration.
  • Set the user-defined device name (GenICam's DeviceUserID) at creation, so discovery lists show which geometry and source each instance serves.
  • If the geometry must grow, ship it as a versioned restart: new declaration, new process, receivers re-discover.
  • If you must rebuild in the same process during development, restart the process between attempts instead of debugging why the device stopped answering.

When is one fixed size the wrong answer?

When most frames are far smaller than the declaration, or the receiver must archive exact pixels with no padding: padding 1280 × 256 live frames into 2048 × 1024 buffers sends 6.4× the useful bytes, so shrink the declaration, split the stream across two servers, or leave GigE Vision for that path.

Run the arithmetic before you commit:

  • Overhead factor: 2,097,152 ÷ 327,680 = 6.4 when the live frame is 1280 × 256 inside a 2048 × 1024 declaration.
  • If 90 % of frames are that size, you send 0.9 × 6.4 + 0.1 × 1 ≈ 5.9× the useful payload all day, and link load, copy time and archive volume all scale with it — the knock-on costs are exactly what this processing time versus image size walkthrough works through.
  • Two fixed servers beat one oversized declaration when frames fall into two clean classes: two declarations, two device names, and each receiver subscribes to the class it wants.
  • If the downstream tool needs exact geometry and will not crop, stream your own headered format over TCP or UDP instead; a per-frame header is far smaller than padding.

What should I check before shipping it?

Run the smallest and the largest incoming frame through the full chain before deployment: stride mistakes, discovery races and links sized for the old smaller payload all hide until the first real shift, and this list catches the ones that reach production.

Declaration

  • Declaration equals the measured maximum across every source and mode, logged for a shift rather than remembered from a datasheet.
  • Width is a multiple of 4 pixels in Mono8, so rows land word-aligned at the receiver.

Repacking

  • Row stride comes from the capture SDK's stride value, never from width × bytes per pixel.
  • The pad region is written every frame, bottom rows included.
  • The incoming buffer is copied before it is queued to the server.

Transport

Discovery and recovery

  • The server is created once per process, the restart path for a geometry change is written down and tested.
  • DeviceUserID is set, receiver buffer counts are sized for burst — 16 × 2 MiB in the worked example — and the live-width and live-height features read correctly at the receiver.

If the arithmetic says one declaration covers your stream, build it that way and stop; if the overhead factor hurts, split the servers before you write the repacker. Size the ports and cameras for the worst case with the camera finder, the frame grabber finder or the system configurator — and if you want a second pair of eyes on the bandwidth budget before you commit the software design, contact the applications team.

Questions

How many buffers must the receiver hold for a padded stream?

Each receiver buffer holds the declared payload, not the live frame: 2048 × 1024 Mono8 is 2 MiB, so 16 buffers cost 32 MiB of host memory — negligible. Size the count for burst, not average: if your sender can emit 5 frames back to back after a scheduling stall, hold at least twice that. Memory only stops being free at 10GigE rates, where 64 × 2 MiB buffers smooth the gaps between receive interrupts.

What does a padded Mono8 stream cost in archive storage?

At the worked example's 62.9 MB/s, one hour archives 62,914,560 × 3600 ≈ 226 GB, and an 8-hour shift fills about 1.8 TB. If most live frames are 1280 × 256, the useful content is only 327,680 × 30 × 3600 ≈ 35 GB per hour — a 6.4× inflation. Crop to the live extent before writing to disk and storage, backup and retrieval all shrink by the same factor.

Can I declare the size once and change only PixelFormat later?

PixelFormat is part of the same declared contract: it sets bytes per pixel, so the receiver's buffer size changes with it. Swap Mono8 for RGB8 without a restart and every buffer is 3× too small. Choose the final pixel format at server creation — if receivers may want colour later, declare RGB8 from day one and pay the 3× bandwidth from the start.

Can the receiver write Width and Height over the control protocol?

Only if you let it: as the device builder you decide which features are writable. Leave Width and Height read-only after creation, and a receiver that tries to shrink the frame gets an access error instead of a stream your buffers cannot fill. Expose the live extent through your own read-only integer features instead.

Does repacking touch the source camera's settings?

No. The source camera keeps its own ROI, exposure and pixel format on its own link; the repack happens inside your sender process, between the capture buffer and the transmit buffer. That decoupling is the point: you can change the source's ROI freely, because the declared transmit geometry absorbs the difference — until a source grows past the declaration, which is a restart.