メモリーレコーディング (高速カメラで撮影した現象をRAMメモリー領域に保存)
概要
本プログラムは、高速撮影した画像をPCのメインメモリに直接記録するメモリーレコーディングのサンプルです。
数百〜数千fpsで取得した画像をHDDやSSDへ直接保存すると、ストレージの書き込み帯域がボトルネックとなり、フレーム落ちが発生します。本プログラムでは、事前にPCのメモリ上へ確保したバッファに画像データをいったん格納することで、フレーム落ちすることなく保存します。バッファに保存された画像データは、画面右側のビューアで再生およびコマ送りでの確認が可能です。
サンプルプログラム
| Software | IC Imaging Control 4.0, Visual Studio™ 2022 |
|---|---|
| サンプル(C#) | WinForm_MemoryRecording_cs_4.0.zip |
実行結果

アプリを起動すると最初にカメラ選択ダイアログが開き、使用するカメラを選ぶとメイン画面が表示されます。画面は左右2つの領域に分かれており、左側にカメラのライブ映像が、右側に記録したバッファの再生映像が表示されます。下部のコントロールでバッファサイズや再生間隔(再生速度)を設定し、LiveStart / LiveStop ボタンで記録の開始・停止を行います。
クラスフィールドの定義(バッファと状態管理)
private ic4.Grabber grabber = new ic4.Grabber();
static QueueSink sink;
private static int counter = 0; // 画像保存用のカウンター
// 画像表示の状態管理用のフィールド
private int CAPTURE_WIDTH = 640;
private int CAPTURE_HEIGHT = 480;
private const double ZoomStep = 1.05; // 1段あたりの倍率
private const double ZoomMin = 0.2;
private const double ZoomMax = 10.0;
// 最新1000枚の画像を保持するリスト
private readonly List<Bitmap> _bufferList = new List<Bitmap>();
// PictureBox 内で表示している現在の番号
private int _currentIndex = 0;
// 排他制御(複数スレッドから触られるため)
private readonly object _lockObj = new object();
// バッファ再生用のタイマー
private System.Threading.Timer playbackTimer;
ここでは、プログラム全体で使い回すグローバル変数等を宣言しています。grabberはカメラを操作するオブジェクトで、カメラを開いたり、ストリームを開始・停止したりする役割を持ちます。sink(QueueSink)は、カメラから届いた画像を受け取るためのシンクです。メモリーレコーディングで一時保存するのが_bufferListです。これは記録した画像を1枚ずつBitmapとして貯めていくリストで、RAMメモリー上に展開されます。ハードディスクやSSDではなくメモリー上のリストへ保存することで、高速なフレーム到着にも追従できるようになっています。
ライブラリの初期化
static void Main()
{
// Initialize IC4 library
ic4.Library.Init();
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
ここでは、アプリケーションが最初に実行する処理を記述しています。ic4.Library.Init()はIC Imaging Control4.0のライブラリ本体を初期化するための呼び出しで、SDKを使うために必ず一度実行しておく必要があります。
アプリ起動(カメラオープン+ライブ表示+メモリー記録の開始)
private void Form1_Load(object sender, EventArgs e)
{
// ユーザーに接続カメラを選択してもらうダイアログを表示
ic4.WinForms.Dialogs.ShowDeviceDialog(grabber, this);
// ユーザーがキャンセルした/有効なデバイスが無いときは安全にアプリ終了。
if (!grabber.IsDeviceValid)
{
Close();
return;
}
settingDomainUpDownControl();
_displayStates[display1] = new ViewState();
display1.MouseWheel += Display_MouseWheel;
display1.MouseDown += Display_MouseDown;
display1.MouseMove += Display_MouseMove;
display1.MouseUp += Display_MouseUp;
// QueueSinkを作成。
sink = new ic4.QueueSink(ic4.PixelFormat.BGR8);
// フレームが届いたときに通知されるイベント
sink.FramesQueued += FramesQueued;
sink.SinkConnected += SinkConnected;
tbImageIndex.Maximum = (int)numericUpDownBuffer.Value - 1;
// ライブスタート開始
grabber.StreamSetup(sink, display1);
sink.AllocAndQueueBuffers((int)numericUpDownBuffer.Value);
pictureBox1.Image = Image.FromFile("BufferRecording.png");
checkBoxBufferRecording.Checked = true;
checkBoxRecordingLoop.Checked = true;
SwitchBtnState();
if (!checkBoxBufferRecording.Checked) return;
btnPlay.Enabled = false;
button2.Enabled = true;
numericUpDownPlaySpeed.Enabled = false;
trackBarPlaySpeed.Enabled = false;
playbackTimer = new System.Threading.Timer(_ =>
{
PlaybackFrame();
}, null, 0, (int)numericUpDownPlaySpeed.Value);
}
btnDeviceProperties_Clickは、ボタンを押したときにカメラ設定用のダイアログを開く処理です。ShowDevicePropertyDialogはICImagingControlが用意している便利なメソッドで、呼ぶと露光時間・ゲイン・トリガーモードなどカメラの全プロパティを一覧できる標準ダイアログが表示されます。
1.カメラの選択とビュー状態の初期化
まずアプリ起動時にカメラと接続する準備をしています。 ic4.WinForms.Dialogs.ShowDeviceDialog(grabber, this);を呼ぶと、PCに接続されているカメラを選ぶためのダイアログが表示され、任意のカメラを選択できます。
次にsettingDomainUpDownControl()でカメラ設定を行い、ライブ表示用のdisplay1にマウス操作のイベントを登録して、デジタルズームできるように準備しています。
2.QueueSinkの作成とイベント登録
カメラが撮影した画像を受け取るための仕組みを準備しています。ここではQueueSinkというタイプのシンク(画像受信のための受け皿)を、ピクセルフォーマットBGR8を指定して作成しています。QueueSinkは、必要なときに1枚だけ取得するSnapSinkとは異なり、カメラから届くすべてのフレームを次々にキューにいれるシンクです。sink.FramesQueued += FramesQueued;によって、新しいフレームがキューに届くたびにFramesQueuedメソッドが呼び出されるよう登録しています。
3.ストリーム開始とバッファの確保
grabber.StreamSetup(sink, display1);で、カメラからの映像をシンク(sink)と画面の表示エリア(display1)の両方へ流すように設定し、ライブ表示と画像取得を同時に開始します。
sink.AllocAndQueueBuffers((int)numericUpDownBuffer.Value);では、画面で指定したバッファサイズ(記録したい枚数)の分だけ、画像を受け取るためのバッファをあらかじめPCメモリー上に確保しています。撮影が始まる前に必要な枚数のメモリーを先に確保しておくことで、フレームが高速に届いても、その都度メモリーを割り当てる無駄をなくし、安定して受信できるようにしています。
4.再生タイマーの起動
最後に、記録したバッファを繰り返し再生表示するためのタイマー(playbackTimer)を起動しています。System.Threading.Timerを使い、numericUpDownPlaySpeed.Value(再生間隔・ミリ秒)ごとに PlaybackFrame()の関数を呼び出すよう設定します。
高速カメラのプロパティ設定
void settingDomainUpDownControl()
{
trackBarPlaySpeed.Value = (int)numericUpDownPlaySpeed.Value;
var propertyMap = grabber.DevicePropertyMap;
propertyMap.SetValue(ic4.PropId.Width, 720);
propertyMap.SetValue(ic4.PropId.Height, 540);
propertyMap.SetValue(ic4.PropId.AcquisitionFrameRate, 500);
propertyMap.SetValue(ic4.PropId.ExposureAuto, "Off");
propertyMap.SetValue(ic4.PropId.ExposureTime, 2000);
propertyMap.SetValue(ic4.PropId.GainAuto, "Off");
propertyMap.SetValue(ic4.PropId.Gain, 7);
}
ここでは、高速撮影を行うためのカメラ側のプロパティ(設定値)をまとめて指定しています。
grabber.DevicePropertyMapは、カメラが持つ各種プロパティへアクセスするためのオブジェクトで、ここに値を書き込むことでカメラの動作を変更できます。解像度を幅720、高さ540に設定したうえで、AcquisitionFrameRate(フレームレート)を「500」に指定しています。自動露光(ExposureAuto)と自動ゲイン(GainAuto)をどちらも「OFF」にして自動調整を切り、ExposureTimeを「2000ms」、Gainを「7」に固定化します。
その他、プロパティ設定をしたい場合には下記を参照してください。
GenICam SFNCに基づくプロパティ一覧
IC Imaging Control Ver4.0(C#/VB.NET) サンプルプログラム
フレーム到着イベント(全フレームをRAMメモリーに記録)
// フレーム到着イベント(QueueSink.FramesQueued)
private void FramesQueued(object sender, EventArgs e)
{
if (!checkBoxBufferRecording.Checked) return;
this.BeginInvoke(new Action(ProcessFrames));
}
private void ProcessFrames()
{
while (sink.TryPopOutputBuffer(out ic4.ImageBuffer buffer))
{
try
{
bool needStore = false;
lock (_lockObj)
{
if (_bufferList.Count < (int)numericUpDownBuffer.Value)
{
needStore = true;
}
}
if (needStore)
{
tbImageIndex.Enabled = false;
using (var wrap = buffer.CreateBitmapWrap())
{
var bmp = new Bitmap(wrap);
lock (_lockObj)
{
_bufferList.Add(bmp);
}
}
}
}
finally
{
buffer.Dispose();
tbImageIndex.Enabled = true;
}
}
}
この関数FramesQueuedでは、QueueSinkに新しいフレームが積まれたときに通知されるイベントハンドラです。このイベントはカメラの内部スレッドから呼び出されるため、GUI上のコントロールを落ちてしまうことがありますのでthis.BeginInvokeを使い、ProcessFramesをGUIを操作するためのスレッドへ転送して実行しています。ProcessFramesでは、sink.TryPopOutputBufferでキューに溜まったフレームを1枚ずつ取り出し、キューが空になるまで whileループで処理します。取り出したフレームは、_bufferList の枚数が設定したバッファサイズに達していなければ記録します。
CreateBitmapWrap()は、カメラが持つ画像メモリーをそのまま参照するBitmapを作りますが、この元のバッファは後でbuffer.Dispose()によってカメラへ返却され、次のフレーム用に再利用されてしまいます。そのまま参照を保持すると中身が次々に書き換わってしまうため、new Bitmap(wrap)で独立したコピーを作り、それを _bufferList に追加しています。こうすることで、バッファが再利用されても記録した画像はRAMメモリー上に残り続けます。 最後のfinallyブロックで必ずbuffer.Dispose()を呼び、使い終わったバッファをシンクへ返却するようにします。これをしないと、再利用できるバッファが枯渇し、新しいフレームを受け取れなくなってしまいます。
バッファの再生処理
private void PlaybackFrame()
{
if (!checkBoxBufferRecording.Checked) return;
Bitmap bmpToShow = null;
lock (_lockObj)
{
if (_bufferList.Count == 0) return;
if (_bufferList.Count < (int)numericUpDownBuffer.Value)
return;
_currentIndex++;
if (_currentIndex >= _bufferList.Count)
{
_currentIndex = 0;
if (checkBoxRecordingLoop.Checked && grabber.IsStreaming)
{
_bufferList.Clear();
pictureBox1.Image = Image.FromFile("BufferRecording.png");
return;
}
}
bmpToShow = (Bitmap)_bufferList[_currentIndex].Clone();
}
// UI スレッドで描画
this.BeginInvoke(new Action(() =>
{
if (pictureBox1.Image != null)
pictureBox1.Image.Dispose();
pictureBox1.Image = bmpToShow;
tbImageIndex.Value = _currentIndex;
}));
}
このプログラムは、メモリに保存した画像データを1枚ずつ順番に表示し、録画した映像をゆっくり再生する処理です。再生用のタイマーから一定間隔で呼び出されます。
最後の画像まで再生した後は、番号を最初に戻してループ再生を行います。このとき「ループ録画」が有効かつカメラがストリーミング中であれば、保存した画像を一度すべて消去し、画面を「記録中」の画像に切り替えます。これにより、「記録 → 再生 → クリア → 新規記録」という順番で繰り返し処理を行っています。
また、別の処理が裏側で画像を消去した際に表示中のデータが壊れないよう、排他制御(lock)の中で画像を複製(Clone)してから使用しています。実際の画面更新は、安全性を確保するためUIスレッド上で行います。その際、古い画像データを放置するとメモリが枯渇してしまうため、明示的に破棄(Dispose)してメモリリークを防ぎつつ、新しい画像の表示とスライダーの同期を行っています。
スライダーによるコマ送り表示
private void tbImageIndex_Scroll(object sender, EventArgs e)
{
Bitmap bmpToShow = null;
lock (_lockObj)
{
if (_bufferList.Count == 0) return;
int idx = tbImageIndex.Value;
if (idx < 0 || idx >= _bufferList.Count) return;
_currentIndex = idx;
bmpToShow = (Bitmap)_bufferList[idx].Clone();
}
// PictureBox に表示
if (pictureBox1.Image != null)
pictureBox1.Image.Dispose();
try
{
pictureBox1.Image = bmpToShow;
}
catch (Exception ex)
{
}
}
このプログラムは、ユーザーがスライダー(tbImageIndex)を動かしたときに、その位置に応じた画像を画面に表示する処理です。バッファに保存した画像から、見たい瞬間を手動でコマ送りの機能です。
まず、スライダーの現在位置を画像の番号として読み取ります。このとき、指定された番号が実際に保存されている画像の範囲内(0から最大枚数の間)に正しく収まっているかをチェックしてます。
次に他の処理によるデータ書き換えと競合しないように排他制御(lock)を行い、対象の画像を複製して画面(pictureBox1)に表示します。その際、それまで表示していた古い画像データを明示的に破棄(Dispose)することで、メモリの無駄遣いやパソコンの動作低下を防いでいます。なお、スライダーが動かせる最大値は、保存されている画像枚数に合わせて自動的に更新される仕組みになっています。
private void numericUpDownBuffer_ValueChanged(object sender, EventArgs e)
{
tbImageIndex.Maximum = (int)numericUpDownBuffer.Value - 1;
}
tbImageIndex.Maximum を「バッファサイズ − 1」に設定しているのは、記録番号が「0」から始まるためです。たとえば200枚記録する場合、番号は「0~199」となるので、スライダーの最大値も「199」に合わせています。
ライブ(記録)の開始・停止
private void btnLiveStart_Click(object sender, EventArgs e)
{
lock (_lockObj)
{
_bufferList.Clear();
}
if (grabber.IsDeviceValid && !grabber.IsStreaming) grabber.StreamSetup(sink, display1);
SwitchBtnState();
if (!checkBoxBufferRecording.Checked)
{
checkBoxRecordingLoop.Enabled = false;
checkBoxRecordingLoop.Checked = false;
groupBoxDisplay.Enabled = false;
pictureBox1.Image = Image.FromFile("wait.png");
}
else
{
sink.AllocAndQueueBuffers((int)numericUpDownBuffer.Value);
pictureBox1.Image = Image.FromFile("BufferRecording.png");
}
if (!checkBoxBufferRecording.Checked) return;
btnPlay.Enabled = false;
button2.Enabled = true;
numericUpDownPlaySpeed.Enabled = false;
trackBarPlaySpeed.Enabled = false;
playbackTimer = new System.Threading.Timer(_ =>
{
PlaybackFrame();
}, null, 0, (int)numericUpDownPlaySpeed.Value);
}
ここでは、LiveStartボタンを押したときに、新たに記録を開始する処理を行っています。まず_bufferList.Clear()で前回の記録をすべて消去し、まっさらな状態から記録を始めます。続いて、まだストリームが動いていなければ 、grabber.StreamSetup(sink, display1)でライブ表示と画像取得を開始します。このあと、Buffer Recordingのチェック状態によって動作が分かれます。チェックが入っていれば、AllocAndQueueBuffersで記録用バッファを確保し、「記録中」の案内画像を表示したうえで再生タイマーを起動します。チェックが入っていない場合は、メモリー記録を行わない単なるライブ表示モードとなり、再生関連のコントロールを無効化して待機画像「wait.png」を表示します。
private void btnLiveStop_Click(object sender, EventArgs e)
{
if (grabber.IsStreaming) grabber.StreamStop();
SwitchBtnState();
numericUpDownPlaySpeed.Enabled = true;
trackBarPlaySpeed.Enabled = true;
btnPlay.Enabled = true;
button2.Enabled = false;
playbackTimer?.Dispose();
playbackTimer = null;
}
ここでは、LiveStop ボタンを押したときの停止処理を行っています。grabber.IsStreamingでストリームが動作中であることを確認してからgrabber.StreamStop()で映像の取得を停止します。あわせて、再生速度のコントロールを操作可能に戻し、再生タイマーをDispose()で破棄してからnullを代入しています。タイマーを確実に破棄しておくことで、停止後に再生処理が動き続けてしまうのを防いでいます。
ボタンの有効・無効の切り替え
void SwitchBtnState()
{
groupBoxDisplay.Enabled = true;
if (grabber.IsStreaming)
{
btnLiveStart.Enabled = false;
btnLiveStop.Enabled = true;
btnDeviceProperties.Enabled = false;
numericUpDownBuffer.Enabled = false;
checkBoxBufferRecording.Enabled = false;
checkBoxRecordingLoop.Enabled = false;
}
else
{
btnLiveStart.Enabled = true;
btnLiveStop.Enabled = false;
btnDeviceProperties.Enabled = true;
numericUpDownBuffer.Enabled = true;
checkBoxBufferRecording.Enabled = true;
checkBoxRecordingLoop.Enabled = true;
}
}
ここでは、現在カメラがストリーミング中かどうかに応じて、画面上のボタンや設定項目の有効・無効をまとめて切り替えています。ストリーミング中はLiveStartボタンやバッファサイズの変更を無効化し、LiveStopのみ押せるようにします。逆に停止中はLiveStartや各設定を操作できるようにします。
再生・一時停止と再生速度の調整生・一時停止と再生速度の調整
private void btnPlay_Click(object sender, EventArgs e)
{
if (!checkBoxBufferRecording.Checked) return;
btnPlay.Enabled = false;
button2.Enabled = true;
numericUpDownPlaySpeed.Enabled = false;
trackBarPlaySpeed.Enabled = false;
playbackTimer = new System.Threading.Timer(_ =>
{
PlaybackFrame();
}, null, 0, (int)numericUpDownPlaySpeed.Value);
}
private void button2_Click(object sender, EventArgs e)
{
btnPlay.Enabled = true;
button2.Enabled = false;
numericUpDownPlaySpeed.Enabled = true;
trackBarPlaySpeed.Enabled = true;
playbackTimer?.Dispose();
playbackTimer = null;
}
ここでは、記録したバッファの再生(Play)と一時停止(Stop)を行っています。btnPlay_Click では再生タイマーを起動し、設定した間隔ごとにPlaybackFrame()を呼び出して再生を開始します。button2_Click(Stop)ではタイマーを破棄して再生を止めます。
private void trackBarPlaySpeed_Scroll(object sender, EventArgs e)
{
numericUpDownPlaySpeed.Value = trackBarPlaySpeed.Value;
}
private void numericUpDownPlaySpeed_ValueChanged(object sender, EventArgs e)
{
trackBarPlaySpeed.Value = (int)numericUpDownPlaySpeed.Value;
}
ここでは、再生速度をスライダー(trackBarPlaySpeed)と数値入力欄(numericUpDownPlaySpeed)の2つの方法で調整できるようにし、両者の値を常に同期させています。スライダーを動かせば数値欄に反映され、数値欄を変えればスライダーにも反映されます。どちらで操作しても同じ再生間隔(ミリ秒)が設定され、見た目と実際の値が食い違わないようにしています。この値が大きいほど再生はゆっくりになり、高速現象をより詳しく観察できます。
デバイスプロパティダイアログの表示と設定の保存
private void btnDeviceProperties_Click(object sender, EventArgs e)
{
if (!grabber.IsDeviceValid)
return;
ic4.WinForms.Dialogs.ShowDevicePropertyDialog(grabber,this);
grabber.DeviceSaveState("device.xml");
}
ここでは、Propertiesボタンを押したときに、カメラの設定用ダイアログを開くようにしています。ShowDevicePropertyDialogはIC Imaging Controlが用意している便利なメソッドで、呼び出すと露光時間・ゲイン・フレームレートといったカメラの各種プロパティを一覧・変更できる標準ダイアログが表示されます。ダイアログを閉じたあと、grabber.DeviceSaveState("device.xml")で現在のカメラ設定をdevice.xmlというファイルに保存しています。設定をファイルに残しておくことで、あとから同じ条件を復元したり、設定内容を確認したりできるようにしています。
ライブ映像のズーム(Ctrl+マウスホイール)
private void Display_MouseWheel(object sender, MouseEventArgs e)
{
ic4.WinForms.Display d = display1;
// Ctrlキーが押されていない場合は何もしない (Ctrl + ホイールのみズーム)
if ((ModifierKeys & Keys.Control) == 0) return;
var s = _displayStates[d];
s.Panning = false;
// ズーム倍率を計算し、許容範囲内に収まるように制限する
double factor = (e.Delta > 0) ? ZoomStep : (1.0 / ZoomStep);
double calculatedZoom = s.Zoom * factor;
double newZoom = Math.Max(ZoomMin, Math.Min(ZoomMax, calculatedZoom));
// ズームの中心をマウスカーソル位置に保つ
int oldW = d.RenderWidth;
int oldH = d.RenderHeight;
int oldL = d.RenderLeft;
int oldT = d.RenderTop;
s.Zoom = newZoom;
int newW = (int)(CAPTURE_WIDTH * s.Zoom);
int newH = (int)(CAPTURE_HEIGHT * s.Zoom);
// マウスカーソルが画像内のどこに位置するか(相対座標 0.0~1.0)
double rx = (e.X - oldL) / (double)oldW;
double ry = (e.Y - oldT) / (double)oldH;
// 新しい矩形位置を計算し、マウス位置を保つ
int newL = e.X - (int)(rx * newW);
int newT = e.Y - (int)(ry * newH);
d.RenderPosition = DisplayRenderPosition.Custom;
d.RenderLeft = newL;
d.RenderTop = newT;
d.RenderWidth = newW;
d.RenderHeight = newH;
}
ここでは、ライブ表示(display1)の上で「Ctrlキーを押しながらマウスホイールを回す」ことで、映像を拡大・縮小する処理を行っています。ホイールを上に回せば拡大、下に回せば縮小となるよう倍率(factor)を決め、Math.Max と Math.Minを組み合わせて倍率が ZoomMin~ZoomMax の範囲に収まるよう制限しています。これにより、極端に拡大・縮小しすぎて映像が見えなくなるのを防ぎます。
再生画像の描画(アスペクト比を保った表示)
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
if (pictureBox1.Image == null)
return;
e.Graphics.Clear(pictureBox1.BackColor);
Image img = pictureBox1.Image;
float boxWidth = pictureBox1.Width;
float boxHeight = pictureBox1.Height;
float imgWidth = img.Width;
float imgHeight = img.Height;
// 拡大率を計算 (アスペクト比を維持し、全体が表示される最大倍率を求める)
float ratioX = boxWidth / imgWidth;
float ratioY = boxHeight / imgHeight;
float ratio = Math.Min(ratioX, ratioY);
// 描画される画像の新しいサイズ
int newWidth = (int)(imgWidth * ratio);
int newHeight = (int)(imgHeight * ratio);
int x = 0;
int y = ((int)boxHeight - (int)newHeight) / 2;
// 画像を描画
e.Graphics.DrawImage(img, new Rectangle(x, y, newWidth, newHeight));
}
ここでは、再生用のpictureBox1に画像を描画する際、縦横の比率(アスペクト比)を崩さずに表示する処理を行っています。表示枠の横方向・縦方向それぞれに収まる倍率を計算し、そのうち小さいほう(Math.Min)を採用することで、画像全体が枠内に収まる最大サイズを求めています。横と縦で別々の倍率をそのまま使うと画像が引き伸ばされて歪んでしまいますが、同じ倍率を両方に適用することで、アスペクト比を保ったまま表示できます。
終了処理
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
saveSettingFile();
if (grabber.IsStreaming) grabber.StreamStop();
}
ここでは、アプリケーションを閉じるときの処理を行っています。saveSettingFile() で設定を保存したあと、カメラがまだストリーミング中であればgrabber.StreamStop() で映像の取得を確実に停止しています。


