A camera stream that pushes all 32 cores to 40% while motion control and PLC comms starve is not a camera fault. It is OpenCV's default thread pool using every logical core it can find. Any per-frame operation with a parallel implementation wakes the whole pool. The fix is one call, cv::setNumThreads(N), plus a sizing rule that protects the frame rate while you bound the load. This page gives you the mechanics of where the threads come from, the cap call with a readback that proves it took, a decision table for N, the arithmetic that predicts what a cap does to frame rate, and the measurements that confirm the result on a running line.
Why does a camera stream load every CPU core at once?
OpenCV sizes its internal worker pool to every logical core on the machine unless you tell it otherwise. On a 32-thread host, a single 60 fps stream from an area scan camera can hold all 32 cores near 40% each.
The pool belongs to OpenCV's parallel framework, the machinery behind parallel_for_. Every function with a parallel implementation uses it: color conversion, resize, thresholding, and the internal conversion some camera wrappers run before handing you a frame. The pool is built once from the hardware's logical core count and keeps that size until you set it.
The measured case behind the numbers in this guide: one small preview stream at 60 fps held 32 threads at roughly 40% each. Multiply it out: 32 × 40% = 1,280% of one core's capacity, about 12.8 cores of demand for a picture a few hundred pixels tall. Most of it is not pixel math. Idle pool workers spin for a short while for the next job instead of sleeping immediately, so per-frame overhead wakes all of them, which is why the load spreads instead of concentrating. Dropping the same stream to 30 fps cut it to about 15% per thread: 32 × 15% = 480%, roughly 4.8 cores.
What are all those extra threads and apparent processes?
The swarm you see in htop is usually one program's thread pool, not many processes. Linux tools list each thread of a process as its own row by default, and the pool wakes because the frame loop reaches OpenCV functions that parallelize internally.
htop shows threads as rows, so a streaming program with a 32-slot pool looks like 32 entries; task managers that hide threads give the opposite impression. Either way the count is a pool you size, not a runaway program you kill.
What wakes it: every per-frame call with a parallel implementation. The usual suspects in a streaming loop are color conversion, resizing to a preview, and any conversion a camera wrapper performs inside its own get-frame function before it returns an image. Each job is small; the pool's size, not the work's size, decides how many cores light up. That is why the same test collapsed onto a single core as soon as the preview shrank.
How to cap OpenCV's threads with cv::setNumThreads
Call cv::setNumThreads(N) once at process start with N as the worker count you will allow. The setting applies to OpenCV's pool for the whole process, and N = 0 is a documented special case that disables threading optimizations so every function runs sequentially.
Zero is the diagnostic setting, not a production one; production caps are small positive numbers. In C++:
#include <opencv2/core.hpp>
cv::setNumThreads(2); // documented API: cap the pool at 2 workers
int pool = cv::getNumThreads(); // documented API: readback, pool == 2 proves it took
In Python the same calls are cv2.setNumThreads() and cv2.getNumThreads():
import cv2
cv2.setNumThreads(2) # cap before the first OpenCV call
pool = cv2.getNumThreads() # returns 2 when the cap took effect
Read the pool back right after startup: getNumThreads() returns the current worker count and should equal the value you set.
What thread count to set on a shared host
Start at 2 worker threads per camera process on any host that shares CPU with time-critical code, keep the sum of caps at or below the physical core count, and leave the default pool in place only where the vision application owns the whole machine.
Two threads per process bounds the worst case at roughly 200% CPU plus a main-thread share of about 50%. On an 8-core machine with 800% capacity that leaves about 800% − 250% = 550%, some 5.5 cores, for acquisition, the OS and everything else you run.
| Situation | Setting | Why it works |
|---|---|---|
| Dedicated controller, one vision application | default pool | fastest per frame; nothing else needs the cores |
| One camera process, shared host | cv::setNumThreads(2) |
worst case about 2.5 cores, most of the machine free |
| Four camera processes, 8-core host | 1 per process to start | 4 × 1 = 4 workers, half the cores; raise one process at a time while fps holds |
| Hunting a load spike | cv::setNumThreads(0) |
sequential mode exposes each operation's own cost |
Multi-camera hosts stay clean when each camera owns a process with its own cap; one camera per process is the pattern to follow when several programs share a machine.
When the host itself is the root cause — an office desktop doing line duty — a purpose-built controller removes the conflict at the source. The same family's three tiers make the sizing ladder visible:
| Specification | MV-VC2140-128G40-1T | MV-VC3301H-128G66 | MV-VC5525-256G70-NN |
|---|---|---|---|
| CPU | Intel® Processor N97 | Intel® Core i3-8100 | Intel® Core i5-12400 |
| Memory | 8 GB | 8 GB | 32 GB |
| Storage | 128 GB SSD + 1 TB HDD | 128G SSD | 256 GB SSD |
| GPU | Intel® UHD Graphics | Intel® HD Graphics 630 | Intel® UHD Graphics 730 |
| Operating system | Unactivated 64-bit Windows 10 LTSC 21H2 | Windows 10 64-bit, Not activated | Unactivated Windows 10 |
| Encryption method | Not support | VM6200Pro license key | – |
| Video output | HDMI interface × 1, VGA interface × 1; Supports independent display outputs at the same time; Max. resolution 1920 × 1080 @ 60 Hz | HDMI interface × 1, VGA interface × 1; Supports independent display output at the same time; Max. resolution 1920 × 1080 @ 60 Hz | HDMI interface × 1, VGA interface × 1; Supports independent display output at the same time; Max. resolution 1920 × 1080 @ 60 Hz |
| Digital I/O | Opto-isolated input × 8; Opto-isolated output × 8 | 8 digital isolated inputs supported; 16 opto-isolated outputs supported (NPN/PNP switchable) | Opto-isolated input × 8; Opto-isolated output × 12 ( supports NPN/PNP switching), digital-isolated low-side driver output × 8; Non-isolated bi-directional differential I/O × 4 |
| Light interface | Light interface with 24 VDC constant voltage × 4; (Brightness is adjustable. Max. current 3 A for single light interface, and total max. power consumption 80 W. Supports light mode switching between strobe and solid via external control) | 4-channel 24 V constant-voltage light drive, Max. power per channel: 72 W, Max. power (4 channels): 150 W | 24 VDC constant voltage × 8; (Max. power consumption of single channel is 72 W, total max. power consumption 190 W) |
| Network interface | GigE interface × 4 | 6× Gigabit Ethernet ports | GigE interface × 7, including interface for PoE × 4 |
| USB Interface | USB 2.0 × 2; USB 3.0 × 2 | USB 2.0 × 4; USB 3.0 × 4; Built-in USB 2.0 × 1 | USB 2.0 × 4, USB 3.0 × 4, built-in USB 2.0 × 1 |
| Serial Port | RS-232 × 1, supports switching to RS-485 or RS-422 | 2× full-function RS-232, RS-485/RS-422 switchable | RS-232 × 2 by default, supports switching to RS-485 and RS-422 |
| Price | $1,679.00 | $2,419.00 | $4,479.00 · quoted |
All 3 share: Certification CE, KC. Values as printed in each product's data sheet; open a part for its drawing and full specifications.
Choose by stream count and cabling: the entry tier carries one or two GigE streams, the mid tier suits a multi-camera cell around an Intel Core i3, and the top tier adds PoE camera ports and an Intel Core i5 for heavier pipelines. The mid tier covers the common single-camera shared cell — Intel Core i3 processor, 8 GB of memory, 5+ LAN ports, 4 built-in light channels and PCIe expansion in the MV-VC3301H-128G66:
- LAN ports
- 5+ LAN ports
- USB 3 ports
- 3-4 USB 3 ports
- Light outputs
- 4 built-in channels
- Memory
- 8 GB
Size the cap on a controller exactly as on any host: count physical cores, subtract acquisition and OS needs, and keep every process's cap inside the remainder.
Why did the frame rate drop after capping the threads?
A cap smaller than the stream's real demand turns a CPU problem into a frame-rate problem. Against 12.8 cores of demand, a 2-thread pool delivers about 60 × 2 / 12.8 ≈ 9 fps from a 60 fps camera.
The decision rule: cut demand first — fewer pixels per frame, fewer conversions, lower preview rate — until a small cap meets it with a thread to spare. Otherwise raise N until delivered fps holds with 20-30% headroom on the capped cores. Lowering demand is the only option that also frees cores for your other processes. When processing still cannot keep pace with acquisition, that is the acquisition-versus-processing frame rate gap, and late frames surface as wait timeouts before anything else notices.
How to verify the cap on a live stream
Three numbers prove the fix: a getNumThreads() readback equal to the value you set, total process CPU at or below cap × 100% plus a few percent of main thread, and a delivered frame rate equal to the camera setting held over a ten-minute run at worst-case resolution.
- Read the pool back with cv2.getNumThreads() immediately after startup — expect 2, or your N. If it returns the logical core count, the cap never ran in this process.
- Watch the process total in htop for five minutes at worst-case resolution and frame rate: two busy threads plus a small main-thread share, not a wall of 40% rows.
- Count delivered frames over ten minutes and divide by time; compare with the configured camera fps. Read the rate from the API rather than guessing from a display timer — read the frame rate reliably in OpenCV covers the method.
- Run the other time-critical process beside the stream and confirm its loop timing is back to normal.
- Repeat the whole set after any software update; pool behavior differs between OpenCV versions and builds.
When is the default thread pool the right choice?
Leave the pool at its default on a machine that runs nothing but the vision application. One worker per logical core is the fastest configuration there, and capping it costs frame rate while protecting nobody.
The cap exists to protect co-residents: an HMI, database writes, PLC communications, motion loops on the same box. If any of those share the machine, it is a shared host — cap it. If nothing shares it, the default pool gives the shortest processing time per frame, and the right move is sizing the machine rather than tuning the pool. The vision controllers category exists for exactly that step.
How to keep the CPU load low for the long run
Prevention is arithmetic and hygiene: keep per-frame pixels and conversions to what the inspection actually needs, run one process per camera with its own cap, and pull the interface-level levers before you give any pool more threads.
- Convert once: request the pixel format your algorithm needs at capture instead of converting, resizing and converting again down the chain.
- Right-size the region of interest: demand scales with pixels × frame rate, so a smaller ROI is the cheapest CPU on offer.
- Convert at the rate you consume: if the consumer runs at 30 fps, do not convert 60.
- Bound the frame queue so a slow consumer drops frames instead of growing memory: bound the queue and reuse frames.
- Trade latency, not cores: GigE interrupt moderation cuts per-packet CPU at a small latency cost.
- Replace spin loops with blocking waits: replace busy-wait acquisition removes a full core of constant burn by itself.
- Pin the OpenCV version on validated machines and re-measure CPU and fps after upgrades.
More host-side tuning guides sit on the systems topic page. If the audit ends with a host that was undersized from the start, bring your stream settings and measured demand to the contact page and we will size it with you.
Questions
Does cv::setNumThreads(0) switch threading off completely?
Yes. Zero disables threading optimizations, so every OpenCV function runs sequentially in the calling thread. That is a useful diagnostic when you need to isolate which operation drives the load, but it removes parallel throughput entirely. On a production stream prefer a cap of 1 or 2, and reserve 0 for short tests.
Do I have to call setNumThreads again for each frame or each camera?
No. The setting belongs to the process and persists until you change it, so one call at startup is enough for the application's lifetime. It is not shared across processes: every camera program you launch carries its own pool and its own cap, which is why one process per camera with its own setting is the clean multi-camera pattern.
Does the cap also limit threads opened by camera drivers and codecs?
No. cv::setNumThreads sizes OpenCV's internal worker pool only. Capture backends, codec libraries and the camera SDK each keep their own pools with their own sizing rules. If a process still spawns more threads than you asked for, audit those components' thread settings before suspecting the cap.
How should threads be split across several camera processes on one host?
Add the caps across processes and keep the sum at or below the physical core count, leaving a core or two for the OS and acquisition. On an 8-core machine, four camera processes at 2 threads each already total 8 workers, so start each process at 1 and raise one at a time while the delivered frame rate holds.
Can an OpenCV version change how many threads a small operation spawns?
Yes. Pool sizing and how long idle workers spin before sleeping differ between versions and builds, so identical code can show a different CPU profile after an upgrade. Pin the OpenCV version on validated machines and re-measure CPU usage and delivered frame rate as a standard step in every software update.
