Stabilizing Basler Camera Acquisition with Event-Driven Capture and Async Saving (C# / pylon SDK)
In previous articles, we covered burst capture (continuous acquisition) and performance tuning using ROI.
As a follow-up, this article explains how to make your acquisition loop more robust by combining event-driven image grabbing with asynchronous save processing.
✅ Why Event-Driven Acquisition Helps
Key characteristics:
- Your handler is called only when a frame arrives
- It integrates cleanly with UI frameworks like WPF
For single-frame capture, you can simply use GrabOne().
However, for GUI applications (live previews, start/stop capture buttons, etc.), using the ImageGrabbed event often results in a cleaner design and more responsive UI.
🔧 Minimal Event-Based Implementation
We will extend the same BaslerCameraSample class used in previous articles:
- ROI article: Using ROI to Boost Burst Capture Performance
- Single-frame capture article: How to Capture a Single Image with Basler pylon SDK in C#
First, we register an ImageGrabbed event handler and start streaming.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
/// <summary> /// Starts continuous image acquisition until StopGrabbing is called. /// Each time an image is grabbed, the OnImageGrabbed event handler is invoked. /// </summary> public void StartGrabbing() { if (Camera == null || !IsConnected) throw new InvalidOperationException(“Camera is not connected.”); if (IsGrabbing) throw new InvalidOperationException(“Camera is already grabbing.”); Camera.StreamGrabber!.ImageGrabbed += OnImageGrabbed; // Set acquisition mode to continuous SetPLCameraParameter(PLCamera.AcquisitionMode, PLCamera.AcquisitionMode.Continuous); Camera.StreamGrabber.Start(GrabStrategy.OneByOne, GrabLoop.ProvidedByStreamGrabber); } /// <summary> /// Event handler called whenever an image is grabbed. /// Converts the image to BitmapSource and saves it with a timestamp-based file name. /// </summary> void OnImageGrabbed(object? sender, ImageGrabbedEventArgs e) { using IGrabResult result = e.GrabResult.Clone(); Console.WriteLine($“Image grabbed successfully: {result.Timestamp} ms”); if (result.GrabSucceeded) { var bmp = ConvertGrabResultToBitmap(result); string filename = $“frame_{DateTime.Now:HHmmss_fff}.bmp”; SaveBitmap(bmp, filename); } } |
It’s convenient to expose an IsGrabbing property on BaslerCameraSample:
|
1 2 3 4 |
/// <summary> /// Indicates whether the camera is currently grabbing images. /// </summary> public bool IsGrabbing => Camera?.StreamGrabber?.IsGrabbing ?? false; |
To stop continuous acquisition, detach the event handler and stop the stream grabber:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/// <summary> /// Stops continuous image acquisition and removes the event handler. /// </summary> public void StopGrabbing() { if (Camera == null || !IsConnected) throw new InvalidOperationException(“Camera is not connected.”); if (!IsGrabbing) throw new InvalidOperationException(“Camera is not grabbing.”); Camera.StreamGrabber!.ImageGrabbed -= OnImageGrabbed; Camera.StreamGrabber.Stop(); } |
Whether you attach/detach the event handler every time depends on your application design. For simple apps, you might register the handler once (e.g., in the constructor) and keep it for the lifetime of the object.
Example: 1 Second of Continuous Capture
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
[TestMethod()] public async Task StartGrabbingTest() { if (!_baslerCameraSample.IsConnected) _baslerCameraSample.Connect(); try { _baslerCameraSample.StartGrabbing(); Assert.IsTrue(_baslerCameraSample.IsGrabbing, “Camera should be grabbing after StartGrabbing is called.”); await Task.Delay(1000); // Capture for 1 second } catch (InvalidOperationException ex) { Assert.Fail($“StartGrabbing failed: {ex.Message}”); } finally { _baslerCameraSample.StopGrabbing(); Assert.IsFalse(_baslerCameraSample.IsGrabbing, “Camera should not be grabbing after StopGrabbing is called.”); } } |
⚠️ Problem: Saving Overhead Can Stall the Pipeline
Depending on your PC and image size, saving each image may take several to tens of milliseconds.
At higher frame rates, a simple event handler that saves directly inside ImageGrabbed may not keep up, causing:
- Increased latency
- Dropped frames
- Unstable frame intervals
🧭 Solution: Buffer with ConcurrentQueue and Save on a Separate Thread
Overall flow:
|
1 2 3 4 5 6 7 8 |
[Basler SDK] ↓ [OnImageGrabbed event] ↓ [Enqueue into ConcurrentQueue] ↓ [Background task dequeues and saves frames] |
First, modify OnImageGrabbed to push cloned results into a queue instead of saving immediately.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/// <summary> /// Queue used to buffer grabbed images for asynchronous processing. /// Each ImageGrabbed event enqueues one cloned grab result with a timestamp. /// </summary> readonly ConcurrentQueue<(DateTime Timestamp, IGrabResult Result)> _bufferedImageQueue = new(); /// <summary> /// Event handler called whenever an image is grabbed. /// Instead of saving immediately, we enqueue a cloned IGrabResult. /// </summary> void OnImageGrabbed(object? sender, ImageGrabbedEventArgs e) { IGrabResult result = e.GrabResult.Clone(); if (result.GrabSucceeded) _bufferedImageQueue.Enqueue((DateTime.Now, result)); } |
Next, run a separate asynchronous loop that dequeues items and saves them:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/// <summary> /// Asynchronously saves buffered images until cancellation is requested /// and the queue is drained. Intended to run on a background task. /// </summary> public async Task SaveBufferedImages(CancellationTokenSource cts) { // Loop while grabbing is active or there are still items in the queue while (!cts.IsCancellationRequested && (IsGrabbing || _bufferedImageQueue.Count > 0)) { if (_bufferedImageQueue.TryDequeue(out var item)) { var bmp = ConvertGrabResultToBitmap(item.Result); SaveBitmap(bmp, $“frame_{item.Timestamp:HHmmss_fff}.bmp”); item.Result.Dispose(); } else { // Avoid busy-waiting when the queue is empty await Task.Delay(1); } } // Optionally clear the queue (or leave it for the next run, depending on your design) _bufferedImageQueue.Clear(); } |
Example: Combining Event-Driven Capture with Async Saving
We can now update the test to run acquisition and saving concurrently:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
[TestMethod()] public async Task StartGrabbingTest() { if (!_baslerCameraSample.IsConnected) _baslerCameraSample.Connect(); Task? saveTask = null; var cts = new CancellationTokenSource(); try { _baslerCameraSample.StartGrabbing(); Assert.IsTrue(_baslerCameraSample.IsGrabbing, “Camera should be grabbing after StartGrabbing is called.”); // Start background save loop saveTask = _baslerCameraSample.SaveBufferedImages(cts); await Task.Delay(1000); // Capture for 1 second } catch (InvalidOperationException ex) { Assert.Fail($“StartGrabbing failed: {ex.Message}”); } finally { _baslerCameraSample.StopGrabbing(); Assert.IsFalse(_baslerCameraSample.IsGrabbing, “Camera should not be grabbing after StopGrabbing is called.”); // Stop the save loop and wait for it to finish cts.Cancel(); if (saveTask is not null) await saveTask; } } |
Depending on your requirements:
- If real-time streaming is critical and your CPU has enough headroom, you can save while grabbing.
- If your CPU is heavily loaded but RAM is sufficient, you can buffer during capture and save after stopping (by starting
SaveBufferedImagesonly afterStopGrabbing).
✅ Summary
- Using the
ImageGrabbedevent yields a clean, C#-idiomatic structure for continuous acquisition. - Directly saving in the event handler can become a bottleneck at higher frame rates.
- A robust pattern is: Clone → Enqueue → Async Save on a background task using
ConcurrentQueue. - This separation helps stabilize continuous acquisition and makes your pipeline more resilient under load.
In the next article, we’ll look at integrating Basler cameras with OpenCV, enabling more advanced image processing workflows.
Author: @MilleVision
📦 Full Sample Project(With Japanese Comment)
A complete C# project that includes event-driven acquisition, ROI, burst capture, and asynchronous saving is available here(With Japanese Comment):
コメントを残す