QueueSinkでMetaDataをCSV出力しながらシングルスレッドで画像処理
概要
このサンプルプログラムは、2台のカメラから同時に映像を取得し、各フレームに付加されたタイムスタンプやフレーム番号などのMetaDataを、CSVファイルに記録するものです。本サンプルでは、QueueSinkのFramesQueuedコールバックがシングルスレッドで同期的に処理されるという仕様を確認できます。具体的には、カメラの出力を20fps(50ms間隔)に設定し、コールバック内に200msの Sleep を挿入することで、処理中は次のフレームが届かない状態が疑似的に再現されています。
この動作を、別途用意された「マルチスレッド構成」のサンプルと比較することで、プログラム設計の違いがフレーム処理の遅延や取りこぼしにどのように影響するかを評価できます。
CSVファイルは、Debugモードで実行した場合、以下のディレクトリに保存されます:
ImageBufferOpenCVLive\bin\Debug\net6.0\logs
サンプルプログラム
Software | IC Imaging Control 4.0, Visual Studio™ 2022 |
---|---|
サンプル(C#) | ImageBufferOpenCVLive_SingleTheading_cs_4.0.zip |
解説
ログファイルの準備
Directory.CreateDirectory(LogDir);
_logCam1 = new StreamWriter(Path.Combine(LogDir, "cam1_log.csv"), true, Encoding.UTF8) { AutoFlush = true };
_logCam2 = new StreamWriter(Path.Combine(LogDir, "cam2_log.csv"), true, Encoding.UTF8) { AutoFlush = true };
_logCam1.WriteLine("TimeStamp,Elapsed_ns,FrameNumber");
_logCam2.WriteLine("TimeStamp,Elapsed_ns,FrameNumber");
この部分では、ログファイルを出力するためのディレクトリとCSVファイルを準備しています。LogDirは実行ディレクトリ配下の logs フォルダを想定しており、存在しない場合には自動で作成しています。
ImageBufferOpenCVLive\bin\Debug\net6.0\logs
続いて、cam1_log.csvおよびcam2_log.csvの2つのファイルを StreamWriter で開きます。trueを指定しているため追記モードとなり、すでにファイルが存在していても上書きされず内容が追加される形式になります。
IC4ライブラリの初期化
ic4.Library.Init(apiLogLevel: ic4.LogLevel.Info, logTargets: ic4.LogTarget.WinDebug);
IC4のすべての機能を使用可能にするためには、プログラムの冒頭でこの初期化処理が必要です。この一行でIC4の内部モジュールが読み込まれ、APIが正しく動作するようになります。ここではログ出力レベルにInfoを指定しており、通常の情報・警告・エラーのログが出力されます。また、ログ出力先にWinDebugを指定することで、Visual Studioの「出力」ウィンドウなど、Windowsのデバッグ環境でログを即時確認できるようになっています。
デバイスの選択
var deviceInfo1 = ic4.ConsoleHelper.PresentUserChoice(ic4.DeviceEnum.Devices, d => d.ModelName, "Select device #1:");
var deviceInfo2 = ic4.ConsoleHelper.PresentUserChoice(ic4.DeviceEnum.Devices, d => d.ModelName, "Select device #2:");
if (deviceInfo1 == null || deviceInfo2 == null) return;
DeviceEnum.Devicesによって取得された接続中のカメラの一覧をコンソールに表示し、それぞれのモデル名をもとにユーザーが選択できるようになっています。
ここでは1台目と2台目のカメラを個別に選ぶ構成となっており、どちらか一方でも選択がキャンセルされた場合には return により処理を中断しています。
Grabber(画像取得用オブジェクト)の生成とカメラ接続
var grabber1 = new ic4.Grabber();
var grabber2 = new ic4.Grabber();
grabber1.DeviceOpen(deviceInfo1);
grabber2.DeviceOpen(deviceInfo2);
ここでは各々のカメラとソフトウェアの間を接続するためにGrabberオブジェクトを2つ生成し、選択されたカメラデバイスにそれぞれ接続します。GrabberはIC4における中心的な役割を担うクラスであり、画像の取得、ストリームの制御、プロパティ設定などをすべてこのオブジェクトを通じて行います。DeviceOpen()メソッドによって、Grabberがカメラに接続されると、以後の各々のカメラの画像取得や制御処理が有効になります。
カメラの撮影条件(フレームレートとトリガーモード)の設定
var propertyMap1 = grabber1.DevicePropertyMap;
var propertyMap2 = grabber2.DevicePropertyMap;
propertyMap1.SetValue(ic4.PropId.AcquisitionFrameRate, 20);
propertyMap1.SetValue(ic4.PropId.TriggerMode, false);
propertyMap2.SetValue(ic4.PropId.AcquisitionFrameRate, 20);
propertyMap2.SetValue(ic4.PropId.TriggerMode, false);
Grabberが接続された状態であれば、各カメラの撮影条件を変更することが可能です。ここではフレームレートとトリガーモードの2点が設定されています。まず、DevicePropertyMapを通じてカメラのパラメータにアクセスし、フレームレートを「20fps」に設定します。これは1秒間に20枚のフレームが出力される設定で、1フレームあたりおよそ50ミリ秒の間隔に相当します。
次に、トリガーモードをfalseに設定しています。これにより、カメラは外部信号によって撮影タイミングを制御されるのではなく、設定フレームレートに基づいて自動的にフレームを出力し続ける「フリーラン」動作になります。
映像表示用ウィンドウの作成と「画面を閉じる」で処理を終了する設定
var display1 = new ic4.FloatingDisplay { RenderPosition = ic4.DisplayRenderPosition.StretchCenter };
var display2 = new ic4.FloatingDisplay { RenderPosition = ic4.DisplayRenderPosition.StretchCenter };
var windowClosed = new ManualResetEventSlim(false);
display1.WindowClosed += (_, __) => windowClosed.Set();
display2.WindowClosed += (_, __) => windowClosed.Set();
それぞれのカメラの映像を表示するためのウィンドウのAPI(FloatingDisplay)を使用します。ユーザーがどちらかの表示ウィンドウを閉じた際に、メイン処理を終了させるための仕組みとしてManualResetEventSlimを使用しています。WindowClosedイベントが発火するとSet()が呼ばれ、これによりメインスレッドが待機から解放され、プログラムの終了処理するようになっています。
QueueSink(画像受信バッファ)の作成
var sink1 = new ic4.QueueSink(ic4.PixelFormat.BGR8, maxOutputBuffers: 1);
var sink2 = new ic4.QueueSink(ic4.PixelFormat.BGR8, maxOutputBuffers: 1);
各Grabberから取得される画像データを一時的に保持し、後述する処理に渡すための受信バッファであるQueueSinkを作成します。ここではOpenCVと互換性のあるカラーフォーマットとしてPixelFormat.BGR8(8bit・BGR順)を指定します。また、maxOutputBuffersに「1」を指定することで、QueueSinkが新しいフレームをリングバッファに保持するのは最大1枚としています。
FramesQueued イベント(フレーム受信時の処理)
// --- CAM1 ---
sink1.FramesQueued += (s, e) =>
{
using (var buffer1 = sink1.PopOutputBuffer())
{
var wrap = buffer1.CreateOpenCvWrap();
Cv2.PutText(wrap, "OverLay Single Thread",
new Point(100, 100), HersheyFonts.HersheySimplex,
1, new Scalar(255, 0, 0), 2);
display1.DisplayBuffer(buffer1); // ← 表示
// --- CSV ログ ---
var elapsed = (buffer1.MetaData.DeviceTimestampNs - start1).ToString();
lock (Lock1)
{
_logCam1.WriteLine($"{buffer1.MetaData.DeviceTimestampNs.ToString():O},{elapsed:F3},{buffer1.MetaData.DeviceFrameNumber.ToString():O}");
}
start1 = buffer1.MetaData.DeviceTimestampNs;
Thread.Sleep(200); // 200msの重い処理の例
}
};
この部分では、1台目のカメラに対応するQueueSink(sink1)に画像が届いたときに自動的に呼び出されるコールバック処理が定義されています。FramesQueuedはIC4におけるイベントで、カメラからの新しいフレームがPCに入ってきたタイミングで非同期に発火します。関数内ではまず、PopOutputBuffer()を呼び出して最新のフレームを取り出します。その後、OpenCVの画像処理を施すためにCreateOpenCvWrap()メソッドを使ってOpenCVのMatオブジェクトに変換し、OpenCVの画像処理の一例として画像上に青色のテキスト「OverLay Single Thread」をオーバーレイで描画しています。次に、display1.DisplayBuffer(buffer1)によって、取得したバッファが画面に表示されます。
その後、各フレームにはMetaDataとして、DeviceTimestampNs(デバイス内部クロックによるナノ秒単位の時刻)およびDeviceFrameNumber(カメラ内部で付加された連番)を取得し、それをCSVファイルに書き込んでいます。なお、lock (Lock1)で排他制御をしているのは、他のスレッドからの同時アクセスによるログ破損を防いでいます。
また、start1という変数には前回のタイムスタンプを記録しておき、そこから現在のフレームまでに何ナノ秒経過したかを計算しています。この差分(Elapsed_ns)がCSVの2列目に記録されることで、フレームごとの間隔(=実効的なフレームレート)を後から確認できるようになっています。
最後にThread.Sleep(200)を入れて200ミリ秒の疑似的な重い処理を挿入しています。これにより、QueueSinkの処理が完了するまで次のフレームが受信できないという、シングルスレッド構成の挙動が再現されます。これはQueueSinkの仕様によるもので、バッファが空くまで次の画像を受け取らないため、CPUの性能や並列処理の構造によって処理能力が大きく左右される部分です。なお、このイベント処理はカメラ2(sink2)に対しても同様の構造で記述されており、処理内容も同じです。
ストリームの開始
grabber1.StreamSetup(sink1);
grabber2.StreamSetup(sink2);
それぞれのGrabberに対してQueueSinkをストリームの出力先として設定しています。StreamSetup()は、カメラから送られてくる映像ストリームがどのように処理されるかを構成するための重要な関数です。
ここで指定されたSink(sink1とsink2)がGrabberに関連付けられると、以降はカメラからの映像がこのSinkに流れ込み、そこでFramesQueuedのコールバックの処理が行われるようになります。
ウィンドウが閉じられるまで待機
windowClosed.Wait();
windowClosed は、ウィンドウが閉じられたことを検知するための同期オブジェクトで、ウィンドウが閉じられるまで待機しています。
終了処理
grabber1.StreamStop();
grabber2.StreamStop();
_logCam1?.Dispose();
_logCam2?.Dispose();
ウィンドウが閉じられ、Wait()が解除されると、すぐに終了処理が始まります。ここではまず、両方のGrabberに対してStreamStop()を呼び出し、カメラからの映像取得を停止させています。これにより、カメラ側のフレーム出力は中断され、QueueSinkへのフレーム流入も止まります。その後、ログファイルのStreamWriterに対してDispose()を呼び出すことで、開かれていたファイルリソースを安全に閉じます。