Baslerカメラの画像取得をイベント駆動+非同期保存で安定化する【C# / pylon SDK】
前回までの記事では、バーストキャプチャ(連続撮影)やROIによる高速化を紹介してきました。
今回はその続編として、画像取得をイベント駆動にし、非同期で保存処理を行う方法を解説します。
✅ なぜイベント駆動が有効なのか?
特徴
- 画像が届いたときだけ処理が呼ばれる
- WPFなどのUIアプリとも相性が良い
通常、画像を1枚ずつ取得する場合は GrabOne() を使いますが、
GUIに画面を表示させたいときや、任意のタイミングで連続撮影を開始・停止したいときは ImageGrabbed イベントを使うと構造がシンプルになります。
🔧 最低限のイベントベース実装
今回も前回記事(ROIを活用してバースト撮影を高速化する)で紹介したBaslerCameraSampleクラスに、以下の機能を追加していきます。
BaslerCameraSampleクラスはBaslerのpylon SDKでカメラ画像を1枚取得する方法で紹介しているので、こちらも参考にしてください。
まずは、ImageGrabbed イベントにイベントハンドラを追加して、ストリーミングを開始します。
/// <summary>
/// StopGrabbing メソッドを呼び出すまで、カメラからの画像取得を開始します。
/// 画像が取得されると、OnImageGrabbed イベントハンドラが呼び出されます。
/// </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;
// 画像取得モードを連続モードに設定
SetPLCameraParameter(PLCamera.AcquisitionMode, PLCamera.AcquisitionMode.Continuous);
Camera.StreamGrabber.Start(GrabStrategy.OneByOne, GrabLoop.ProvidedByStreamGrabber);
}
/// <summary>
/// イベントハンドラ。画像が取得されると呼び出されます。
/// 取得された画像は BitmapSource に変換され、ファイル名に現在の時刻を付加して保存されます。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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);
}
}
また、カメラを扱うBaslerCameraSampleクラスにIsGrabbingを実装しておくと、実用上便利なことが多いです。
public bool IsGrabbing => Camera?.StreamGrabber?.IsGrabbing ?? false;
これで連続撮影が開始されます。停止したいときは、イベントハンドラを削除し、ストリーミングを停止します。
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();
}
イベントハンドラを毎回追加、削除するかはアプリケーションの仕様によって使い分けます。
シンプルなアプリケーションであれば、コンストラクタでイベントハンドラを一度だけ登録することも多いです。
実行例
以下は1秒間連続撮影するテストコードです。
[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); // 1秒間連続撮影を続ける
}
catch (InvalidOperationException ex)
{
Assert.Fail($"StartGrabbing failed: {ex.Message}");
}
finally
{
// StopGrabbingを呼び出して、撮影を停止する。
_baslerCameraSample.StopGrabbing();
Assert.IsFalse(_baslerCameraSample.IsGrabbing, "Camera should not be grabbing after StopGrabbing is called.");
}
}
⚠️ 問題点:保存処理が重いとフレームが詰まる
PCの性能や画像の大きさによっては保存に数〜数十msかかるため、シンプルな実装では フレームレートが高いと ImageGrabbed の処理が間に合わないこともあります。
🧭 解決策:ConcurrentQueueを活用し非同期で保存するスレッドを起動する
以下のような流れで処理していきます。
[Basler SDK]
↓
[OnImageGrabbed イベント]
↓
[ConcurrentQueue に追加]
↓
[非同期スレッドで保存]
まずは、OnImageGrabbedを書き換えます。
GrabResultの保存が処理に間に合わなくなるのを防ぐために、
イベント内で IGrabResult.Clone() してキューに追加します。
/// <summary>
/// 画像取得結果をバッファリングするためのキューです。
/// 画像が取得されると、OnImageGrabbed イベントハンドラでこのキューに追加されます。
/// </summary>
readonly ConcurrentQueue<(DateTime, IGrabResult)> _bufferedImageQueue = new();
/// <summary>
/// 画像が取得されたときに呼び出されるイベントハンドラです。
/// 取得された画像は、キューに追加されます。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void OnImageGrabbed(object? sender, ImageGrabbedEventArgs e)
{
// 保存や各種処理が終わったらDisposeが必要
IGrabResult result = e.GrabResult.Clone();
if (result.GrabSucceeded)
_bufferedImageQueue.Enqueue((DateTime.Now, result));
}
次に別スレッドで queue.TryDequeue(out var item) を繰り返して非同期で保存します。
例えば以下のメソッドのようにすることができます。
public async Task SaveBufferedImages(CancellationTokenSource cts)
{
// キューが空になるまで、またはキャンセル要求があるまでループします。
// 撮影中はキューに追加される可能性があるため、ループを続けます。
while (!cts.IsCancellationRequested && (IsGrabbing || _bufferedImageQueue.Count > 0))
{
if (_bufferedImageQueue.TryDequeue(out var item))
{
var bmp = ConvertGrabResultToBitmap(item.Item2);
SaveBitmap(bmp, $"frame_{item.Item1:HHmmss_fff}.bmp");
item.Item2.Dispose();
}
else
{
await Task.Delay(1); // 空ループ対策
}
}
// キューをクリア(次のStartGrabbingで実行する実装でもよい)
_bufferedImageQueue.Clear();
}
実行例
StartGrabbingTestを以下のように書き換えます。
StartGrabbing()を呼んだ後に、SaveBufferedImages()タスクを非同期で実行します。保存タスクは、処理能力に余裕がありリアルタイム性が求められる場合は撮影中に、処理能力が厳しくメモリに余裕がある場合は撮影終了後に実行するのが効果的です。
[TestMethod()]
public async Task StartGrabbingTest()
{
if (!_baslerCameraSample.IsConnected)
_baslerCameraSample.Connect();
Task? saveTask = null;
try
{
_baslerCameraSample.StartGrabbing();
Assert.IsTrue(_baslerCameraSample.IsGrabbing, "Camera should be grabbing after StartGrabbing is called.");
// 撮影開始後に画像を保存するタスクを開始する。
// 保存をキャンセルしたい場合は、cts.Cancel()を呼び出します。
var cts = new CancellationTokenSource();
saveTask = _baslerCameraSample.SaveBufferedImages(cts);
await Task.Delay(1000); // 1秒間連続撮影を続ける
}
catch (InvalidOperationException ex)
{
Assert.Fail($"StartGrabbing failed: {ex.Message}");
}
finally
{
// StopGrabbingを呼び出して、グラブを停止する。
_baslerCameraSample.StopGrabbing();
Assert.IsFalse(_baslerCameraSample.IsGrabbing, "Camera should not be grabbing after StopGrabbing is called.");
if (saveTask is not null)
await saveTask; // 保存タスクが完了するのを待つ
}
}
✅ まとめ
-
ImageGrabbedイベントを使うとC#らしくコードが直感的に書ける - 保存処理が重い場合は
Clone + Queue + 非同期保存が効果的 - 安定した連続撮影を実現するためには、非同期処理が重要
次回は、OpenCVとの連携について解説予定です。
筆者:@MilleVision