The vendor viewer delivers the camera's rated frame rate. Then your own program connects, and the counter collapses to 5 fps. The hardware is usually innocent here: the viewer just proved the sensor, driver and link all work. The real suspects are a setting your program makes differently, and an architecture that grabs, processes and draws in one loop. This guide runs the four checks in the order that clears them fastest — exposure time, link bandwidth, the camera's own frame-rate setting, then loop structure — and shows the producer/consumer split that turns a 5 fps grab-process loop back into the 30 fps the camera is rated for. Every step carries its arithmetic, so you can substitute your own timings and confirm the fix on your bench.
Where the frames stop: the two-minute triage
Run the vendor viewer first and read its frame-rate counter. If the viewer also crawls, the limit is exposure, a camera setting or the link: work through checks 1 to 3 below. If the viewer holds the rated rate, the camera and link are proven and the problem lives in your program's settings or structure: go straight to check 4.
Either way, instrument before changing anything. Timestamp every frame as your code receives it, average over two seconds, and compare three numbers: the rated rate on the data sheet, the camera's computed rate in its feature tree, and your delivered rate. The camera computes the rate its current settings allow; your code reports the rate it actually gets. The gap between those two tells you which side of the boundary to work on.
| Reading | Meaning | Next |
|---|---|---|
| Viewer slow, computed rate below rated | Exposure is eating the frame budget | Check 1 |
| Viewer slow, computed rate at rated, link near saturation | Bandwidth limit | Check 2 |
| Computed rate low with normal exposure | A frame-rate cap left in the camera | Check 3 |
| Viewer at rated rate, your program slow | Processing serialized with acquisition | Check 4 |
Check 1: exposure time against the frame budget
A camera cannot start the next exposure until readout of the previous frame finishes, so every camera carries this ceiling. A 30 fps target gives a total budget of 33.3 ms. Set 40 ms of exposure against a 12 ms readout — an illustrative figure for a small global shutter sensor; larger rolling shutter sensors take longer — and the cap falls to 1000 / 52 = 19.2 fps. No software change raises it.
The accidental version is auto exposure. In a dim lab it drifts toward 100 ms, and on a 12 MP rolling shutter sensor rated at 30 fps the readout alone takes roughly 30 ms — the 33.3 ms period at its rating implies it — so the cap lands at 1000 / 130 = 7.7 fps. The same 100 ms smears a moving part across many pixels, which is why low fps and an apparent focus problem so often arrive together.
Test it in ten seconds: fix exposure at 5 ms in the viewer and watch the counter jump. The fix is light, not code: add illumination, set a fixed exposure, leave auto off. The frame-rate-versus-exposure trade-off on GigE and the exposure-plus-readout arithmetic on a hardware trigger work through this budget in detail.
Check 2: does the link carry the pixels?
Frame size is pure arithmetic: a 4000 × 3000 mono 8-bit frame is 4000 × 3000 × 1 B = 12,000,000 B = 12 MB, and color triples it to 36 MB. A GigE link moves 125 MB/s on the wire; after protocol overhead expect about 100 MB/s with default 1500-byte packets and about 120 MB/s with jumbo frames enabled. Twelve-megabyte frames therefore cap at 100 / 12 = 8.3 fps to 120 / 12 = 10 fps. The catalog shows the boundary directly, because the same IMX226 sensor ships on both links:
- Sensor format
- 1/1.7"
- Lens mount
- C-mount
- Sensor format
- 1/1.7"
- Interface
- USB3
The printed ratings agree with the math. Twelve MB at 9 fps is 108 MB/s delivered — right at the GigE ceiling, so the GigE card's 9 fps is the link's doing, not the camera's. The same frames on USB3: 12 MB at 30 fps is 360 MB/s, which fits the roughly 400 MB/s a USB3 host sustains in practice, which is why the USB3 card prints 30 fps on identical silicon.
Now the trap: if your program negotiates color where the viewer used mono, every frame triples. Thirty-six MB at 30 fps is 1080 MB/s, which no GigE or USB3 link carries, and even the GigE link alone falls to 100 / 36 = 2.8 fps. Request mono 8-bit unless the inspection needs color, and check which pixel format each program actually negotiates.
Then count what else uses the wire. A second camera on the same switch, or several USB devices behind one root hub, divides a fixed budget. Host-side USB trouble has a well-known usbfs memory fix; link-level caps look identical when a camera sits stuck at a fixed rate; and a busy host controller starving a USB camera is the same story in miniature — see why a USB camera misses frames on a busy host.
The decision is one multiplication: your frame size times your target rate, compared with roughly 120 MB/s for GigE and 400 MB/s for USB3. Over the line, cut pixels at the camera with ROI or binning, drop to mono, or change the interface — no thread trick changes the wire.
Check 3: a frame-rate cap left in the camera
Every machine-vision camera carries an explicit target frame rate in its feature tree, usually with a separate enable switch, alongside a computed estimate of the rate the current settings can deliver. Driver defaults, saved user sets and previous sessions can leave that target at 5 fps on hardware that would happily run 30.
Read the computed estimate first. If the camera itself says 5 fps, your loop is innocent: set the target, enable it, re-read the estimate, and confirm it lands at the rated rate. Do this explicitly at program start rather than trusting power-on defaults, and re-check packet size after any network change — a lost jumbo-frame setting quietly costs tens of MB/s of the GigE budget.
Break one more habit while you are there: two programs on one camera negotiate badly. A viewer left open holds settings while your program runs, and the one-client-at-a-time problem applies to stored settings as much as to streams. Close every other session before benchmarking.
Check 4: one loop doing grab, process and draw
If the viewer holds the rated rate while your program sits at 5 fps, suspect the loop before the hardware. The signature is steady: not bursts, not random drops, just a constant rate equal to one grab plus one processing pass plus one draw.
Run the numbers for a text-recognition loop on a 30 fps camera. The grab waits up to 33.3 ms for the next frame, decode and match take 150 ms, the overlay draw takes 15 ms: 33.3 + 150 + 15 = 198.3 ms, and 1000 / 198.3 = 5.0 fps. That is the 5 fps symptom, rebuilt from three ordinary timings — time each stage in your own program with a stopwatch around the calls.
The driver cannot rescue this. Its buffer pool holds a few frames — three to ten in a typical setup — and at 30 fps three buffers cover only 100 ms of a 165 ms processing cycle, so the next frame arrives with nowhere to land and the camera stalls. When the symptom is buffers filling rather than a slow counter, the buffers-full diagnosis maps it step by step. If profiling shows the decode or match itself as the tail, work the decode-step profile and the search-region shrink; if a GPU stage is the tail, the GPU frame-rate diagnosis is the one to follow.
The fix: producer and consumer loops
Split the work so acquisition never waits for processing. A producer loop grabs each frame, timestamps it and pushes it into a bounded queue — nothing else. A consumer loop pops frames, processes them and draws results, then hands each buffer back to the driver's pool so the camera never runs out. Most machine-vision SDKs ship this pattern as their grab-with-processing or producer/consumer example, so start from that skeleton instead of writing one from scratch.
# Illustrative skeleton: these names are yours to choose, not a vendor API.
acquire_loop: # own thread
while running:
frame: Image = camera.Grab(timeout_ms = 1000)
queue.Push(frame) # no decode, no draw here, ever
dropped = queue.Dropped() # counter you display
process_loop: # second thread
while running:
frame: Image = queue.Pop(timeout_ms = 1000)
result: str = Decode(frame.ROI(1280, 960))
DrawOverlay(result)
camera.Return(frame) # buffer back to the driver pool
Three rules keep the split healthy. Bound the queue at three to ten frames; five is a good starting depth. Decide now what happens when the consumer falls behind: for guidance, keep only the newest frame and let older ones drop; for measurement, set the camera's frame rate just under what processing sustains so nothing queues. And never let the queue grow without a limit.
Verify with the counters from the triage: a sixty-second run should hold the rated rate with the dropped counter at zero. When the consumer still cannot keep up, the acquisition-versus-processing gap continues from there.
Worked example: 5 fps back to 30 fps
Splitting alone is not magic: the pipeline now runs at the slower of the two loops.
Keep the check-4 timings and only split: the consumer still spends 150 + 15 = 165 ms per frame, so the rate rises just to 1000 / 165 = 6.1 fps. The consumer remains five times slower than the 33.3 ms frame period.
Cut the consumer next. Search a 1280 × 960 region instead of the full frame: 1,280 × 960 = 1,228,800 pixels against 12,000,000, so pixel-count scaling puts decode at 150 × (1,228,800 / 12,000,000) = 15.4 ms. The consumer period becomes 15.4 + 5 = 20.4 ms, a capability of 1000 / 20.4 = 49 fps, and the camera's own 30 fps becomes the limit — delivered in full. Pixel-count scaling is a good first estimate for region operations but not exact: time your decode once at full frame and once at the ROI, and use your measured ratio instead.
Re-check bandwidth while you are there: 12 MB at 30 fps is 360 MB/s, inside USB3's practical 400 MB/s and far outside GigE's roughly 120 MB/s ceiling, so on GigE the same code tops out near 10 fps no matter what the threads do.
The decision, stated plainly: get processing under the frame period and you receive the rated rate. If you cannot, set the camera's rate about 20 percent below what processing sustains — that keeps the queue shallow and latency bounded instead of trading frame rate for lag.
Keep it fixed: the settings and checks that stop a relapse
Start-up settings:
- Fixed exposure with auto exposure off: a drifting exposure re-imposes the check-1 ceiling with no code change at all.
- Target frame rate set explicitly with its enable switch on, and the computed rate re-read at startup.
- Mono 8-bit pixel format unless the inspection needs color, and a note of what each program negotiates.
- Packet size confirmed after every network change.
Architecture:
- Producer and consumer separated from the first commit, with the queue bounded at three to ten frames.
- Delivered fps, dropped frames and per-stage timings — grab, decode, draw — visible on your status screen, so a relapse shows within seconds.
- One client per camera, viewer closed before benchmarking.
Hardware:
- The camera link reserved for camera traffic: no plant network on the same port, no USB devices stacked behind one root hub.
- A host sized for the consumer loop. When the machine also runs other duties, a controller with dedicated ports isolates the streams: the MV-VC3301H-128G66 puts 5+ LAN ports and 4 built-in light channels in one vision controller, so each camera link keeps its own budget.
- LAN ports
- 5+ LAN ports
- USB 3 ports
- 3-4 USB 3 ports
- Light outputs
- 4 built-in channels
- Memory
- 8 GB
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.
If the triage ends at the hardware and the computed rate says your pixels cannot move at your rate, choose by the multiplication rather than by hope: the area scan camera finder filters by resolution, interface and speed, and the vision controllers page starts the host side. The systems topic page gathers the rest of the frame-rate fixes, and if you would like a second pair of eyes on your timings, tell us what your counters show.
Questions
How deep should the queue between acquisition and processing be?
Start at five frames. At 30 fps that is a 167 ms cushion, enough to absorb a processing spike without adding noticeable latency. Past about ten frames the queue stops protecting you and starts hiding a consumer that cannot keep up: rates look fine while latency grows until frames arrive stale. For guidance tasks that only need the newest image, use depth one with overwrite and never queue history at all.
Can auto exposure cause the frame rate drop on its own?
Yes, and it is the most common hardware-side cause. Left on in dim light it drifts exposure upward; at 100 ms of exposure against roughly 30 ms of readout, no software change gets a 12 MP rolling shutter camera past about 7.7 fps. Work the fix in order: disable auto exposure, set a fixed value, re-read the camera's computed rate in its feature tree, and only then add light if the image turned dark. Light added while auto exposure stays on simply moves the camera to a new, equally slow equilibrium.
Will a faster PC or controller raise the frame rate?
Only when processing is the bottleneck, and the viewer test separates the cases. If the vendor viewer also slows down, the limit is exposure, a camera setting or link bandwidth, and no computer fixes it. If the viewer runs at the rated rate, splitting acquisition from processing usually recovers it on the hardware you already have. Buy a dedicated controller when the host also carries plant communication or several cameras, so each link keeps its own budget.
Is it acceptable to process only every second or third frame?
For guidance and presence checks, yes: keep the newest frame and drop the rest, which costs nothing if the part moves less than your tolerance between processed frames. For counting, measurement or robot guidance paths, no: a dropped frame is a missed part. In that case lower the camera's frame-rate setting until processing keeps pace, so every frame is processed in order and latency stays bounded.
Do jumbo frames increase GigE camera throughput?
They help at the margin, not the ceiling. Default 1500-byte packets spend more wire time on headers and interrupt the host far more often per frame; jumbo frames lift usable payload toward roughly 120 MB/s and cut CPU load noticeably. Enable them on the camera and the network interface together, and check every switch in the path passes the larger packets. Expect tens of MB/s, not a new limit: the 125 MB/s wire speed still stands.


