[ on ] chu .tech
← Blog

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

  1. Producer (grabber). The GigE camera’s SDK callback fires on its own thread. It copies the frame into a shared, bounded queue (BlockingCollection<HImage> with BoundedCapacity) and returns immediately. The producer never processes, never touches the UI.

  2. 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.

  3. UI (WPF dispatcher). Workers do not touch controls. They publish immutable result objects (InspectionResult with image refs, defect classes, timestamps) through a channel. The view model consumes them on the dispatcher via IProgress<InspectionResult> or a System.Threading.Channels.Channel and 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.

✉ Book a call