Industrial HMI & Vision · 2026-03-30
WPF Industrial HMI Architecture: Producer-Consumer Pattern with HALCON
The threading architecture behind a real-time inspection HMI: a grabber thread feeding a bounded queue, HALCON workers consuming frames, and a WPF UI that never touches the pipeline.
An industrial HMI that freezes during inspection is not an HMI. The architecture that prevents it is the same one I used for the GuanRong label inspection system: producer-consumer, with three layers that never block each other.
The three threads
-
Producer (grabber). The GigE camera’s SDK callback fires on its own thread. It copies the frame into a shared, bounded queue (
BlockingCollection<HImage>withBoundedCapacity) and returns immediately. The producer never processes, never touches the UI. -
Workers (HALCON). A fixed pool of
Tasks consumes frames from the queue and runs the inspection — preprocessing, region segmentation, classification. Slow inspection never stalls the camera, because the queue absorbs the difference. -
UI (WPF dispatcher). Workers do not touch controls. They publish immutable result objects (
InspectionResultwith image refs, defect classes, timestamps) through a channel. The view model consumes them on the dispatcher viaIProgress<InspectionResult>or aSystem.Threading.Channels.Channeland updates the screen.
The two rules that keep it correct
Rule 1 — the queue is the only shared memory. No locking inside the inspection code. Frames and results move through the bounded queue and the result channel; anything else (a shared List, a cached Dictionary) reintroduces the race you were trying to remove.
Rule 2 — cancellation is explicit. Industrial operators stop lines mid-inspection. Pass a CancellationToken from the view model through to the workers, and have workers check it between frames — not inside a single long HOperatorSet call. This is what makes “stop” feel instant instead of “stops after the current label.”
var queue = new BlockingCollection<HImage>(boundedCapacity: 4);
var workers = Enumerable.Range(0, Environment.ProcessorCount - 1).Select(_ => Task.Run(() =>
{
foreach (var img in queue.GetConsumingEnumerable(ct))
{
var result = Inspect(img, ct); // pure HALCON work
_channel.Writer.TryWrite(result); // async result delivery
}
}, ct)).ToArray();
What this buys you
- Camera throughput decoupled from inspection speed — a slower algorithm pass doesn’t drop frames
- The UI stays at 60 fps while inspection runs at full speed
- A stopped line is a bounded queue that drains in milliseconds, not a crash
The pattern transfers: any real-time line where acquisition speed and classification must not fight each other — label inspection, bottle inspection, web inspection — gets the same skeleton, with only the Inspect function swapped.