IMX900のQuad HDRを使った6画面表示
概要
本サンプルは、IMX900センサーを搭載したモノクロカメラのQuad HDR機能を使い、異なる露光時間で撮影した画像を比較できるアプリです。
Quad HDRでは、2×2画素の各位置に対応する4つの画素グループに、個別の露光時間を設定します。本サンプルは、1枚の受信画像から4枚の個別露光画像を取り出し、その輝度平均画像と補正前のRAW画像を合わせた計6枚を表示します。
露光時間を1つだけ設定する撮影では、明るい部分に合わせると暗い部分が黒つぶれし、暗い部分に合わせると明るい部分が白飛びすることがあります。露光時間を変えて複数フレームを撮影・合成する方法もありますが、被写体が動くとフレーム間で位置がずれることがあります。
Quad HDRは、異なる露光条件の情報を1フレームにまとめて取得します。今回確認したカメラの動作では、4つのグループの露光終了時刻がそろい、露光時間が長いグループほど早く露光を開始します。
本サンプルでは、カメラのプロパティから露光時間を設定し、受信したMono8画像の画素分離、輝度平均化、表示、保存を行います。
また、遮光して取得した暗画像(Dark Frame)をOpenCVのCv2.Subtractで差し引く補正も行えます。暗画像に含まれる固定パターンノイズを低減するための処理です。必要に応じてご利用ください。
サンプルプログラム
| Software | IC Imaging Control 4.0, Visual Studio™ 2022 |
|---|---|
| IC4関連パッケージ | ic4dotnet、ic4dotnet.System.Windows.Forms、ic4dotnet.OpenCvSharp |
| IC4関連パッケージサンプル(C#) | IMX900_HDR.zip |
| IC4関連パッケージ実行ファイル | IMX900_HDR_App.zip |
| IC4関連パッケージ対応カメラ | DMM47GX900-ML、DMM47GX900-MLP、DMM47GX900、DMM47GX900-P、ViM43GX900、ViM47GX900-I67 |
| ファームウェア | 4370以上で、使用機種のQuad HDR個別露光に対応するもの |
Quad HDRの仕組み
Quad HDRでは、2×2画素の各位置に異なる露光時間を割り当てます。同じ位置関係にある画素が、画像全体で1つの露光グループを構成します。個別露光は1~5000 µs(マイクロ秒)の範囲で設定できます。
露光開始と露光終了のタイミング
Quad HDRの各グループは、露光の終了時刻がそろうように制御されます。上図のように、露光時間が長いグループ(例:T4)ほど早く露光を開始し、短いグループ(例:T1)ほど後から開始します。これにより、動きのある被写体に対する時間的なずれを最小限に抑えつつ、異なる露光条件のデータを同時に取得します。
画素グループの割り当て
画素グループは、行(Row)と列(Column)の偶数(Even)・奇数(Odd)の組み合わせで決まります。 ExposureTimeSelectorで操作するグループを選び、そのグループの露光時間を設定します。
露光モード
ExposureTimeModeには、次の2つのモードがあります。
- Common(共通露光):すべての画素を同じ露光時間で撮影します。
- Individual(個別露光):Quad HDRの4グループに、それぞれ別の露光時間を設定します。
個別露光の設定手順
Properties画面では、次の順番で設定します。
- ExposureTimeModeをIndividualに設定します。
- ExposureTimeSelectorで、設定したいグループを選びます。例えば、Odd Row, Even Columnは奇数行・偶数列のグループです。
- ExposureTimeに露光時間を入力します。
- 他のグループも設定する場合は、2と3を繰り返します。
カメラから届くのは、4グループの画素が混在する1枚のモノクロ画像です。本アプリでは、同じグループの画素だけを取り出し、4枚の画像へ並べ直します。これは画像を4つの領域に切り分ける処理ではありません。4枚の個別露光画像と平均画像の幅・高さは、それぞれRAW画像の半分になります。
実行結果と操作方法
撮影対象
反射率の高いアルミ包装(白飛びしやすい)と、反射率の低い黒色ゴムマット(黒つぶれしやすい)を同時に撮影し、効果を確認できます。
6画面の見方
Individualモードでは、同じ受信フレームから作った4枚の個別露光画像、平均画像、補正前RAWを表示します。左列と中央列の4枚が露光グループに対応し、右上が平均、右下がRAWです。
Commonモードでも6画面表示を続けます。この場合は全グループが共通露光になるため、露光時間による差はなくなります。
基本の操作手順
- カメラを選択し、Commonで被写体を確認します。
- Individual (Quad HDR)へ切り替えます。
- 4つの露光時間を調整し、短時間露光で明部、長時間露光で暗部の見え方を比較します。
- 動く被写体では、明るさだけでなくブレや位置差も確認します。
Dark Frame補正の使い方
この補正は、取得した暗画像の画素値をそのまま差し引きます。まず露光時間とゲインを決めてから、次の手順で取得してください。
- レンズを覆い、光が入らない状態にします。
- Snap Dark Frame を押します。
- 表示が「Dark Frame: 取得済み」に変わったら、レンズの覆いを外します。
- 補正を解除する場合は、 Reset Dark Frame を押します。
露光時間やゲインを変更しても、取得済みのDark Frameは保持します。ただし、新しい条件に合わせて補正量を自動調整するわけではありません。条件に合わせ直す場合は、再度遮光して取得してください。 なお、RAWには暗画像補正を適用しません。平均画像は、補正が有効な場合には補正後の4枚の画像から計算します。
各ファイルについて
| ファイル | 役割 |
|---|---|
| Program.cs | IC4を初期化し、Form1を起動します。 |
| Form1.Designer.cs | 画面の部品を配置し、操作イベントを接続します。 |
| Form1.cs | カメラ設定・受信・表示・保存の処理をつなぎます。 |
| Imx900HdrProcessor.cs | Dark Frame減算、画素分離、単純平均を行います。 |
| SixPanelView.cs | 6画面表示と同期ズーム・ドラッグ・座標表示を行います。 |
| CameraParameterControl.cs | 数値入力とスライダーを同期し、Min/Maxを表示します。 |
| SixFrameExporter.cs | 表示中の6枚をBMP形式で保存します。 |
| CameraSettingsFile.cs | カメラ設定をXML形式で保存・読み込みします。 |
| ImageSaveSettings.cs | 画像の保存先を、カメラ設定とは別のXMLファイルに記憶します。 |
Form1.csとForm1.Designer.csは、1つのForm1クラスを分担するファイルです。画像を受信してから表示するまでの処理の流れを、以下に示します。
カメラからの高速な連続画像受信や画像処理を、画面を描画する処理(GUIスレッド)と同じスレッドで行うと、アプリの操作がカクついたりフリーズしたりしてしまいます。そのため本サンプルでは、画像を受信して処理する「裏側の処理(受信スレッド)」と、画面を書き換える「表側の処理(GUIスレッド)」を分け、両者の間で安全にデータ(_pendingFrames)を受け渡す設計にしています。
-
カメラから画像を受信(受信スレッド) → Form1.Sink_FramesQueued()が呼ばれます。
-
画像処理の実行(受信スレッド) → Imx900HdrProcessor.Process()で画像処理を行います。 → SplitAndAverage() メソッド内で、RAW画像から個別露光4枚と平均画像を生成します。 → RAW画像を含めた計6枚のセット(ExposureFrameSet)を _pendingFrames 変数に格納し、表示の準備を完了します。
-
画面表示の更新(GUIスレッド) → Form1.PreviewTimer_Tick()がタイマー(約30fps)で定期的に実行されます。 → _pendingFrames に新しい画像があれば取り出し、DisplayFrames()へ渡します。 → SixPanelView.ShowFrames()により、6つの領域に画像が描画されます。
コード解説
状態を保持する変数と露光グループの定義
Form1.cs
// 画素の行・列の偶奇に対応させるための文字列です。カメラのExposureTimeSelectorに対応。
// 画面の4つの入力欄と同じ順序にして、書き込み先を対応付けます。
private static readonly string[] ExposureSelectors =
{
"EvenRowEvenColumn",
"EvenRowOddColumn",
"OddRowEvenColumn",
"OddRowOddColumn"
};
private readonly Grabber _grabber = new Grabber();
private readonly Imx900HdrProcessor _hdrProcessor = new Imx900HdrProcessor();
private readonly CameraParameterControl[] _individualExposureControls;
private QueueSink _sink;
private bool _updatingControls;
private bool _cameraControlsReady;
private volatile bool _hdrEnabled;
private volatile bool _darkFrameActive;
private int _darkFrameCommand;
private ExposureFrameSet _displayedFrames;
private bool _snapshotMode;
private string _imageSaveDirectory;
private int _lastFrameErrorTick;
private ExposureFrameSet _pendingFrames;
private readonly System.Windows.Forms.Timer _previewTimer = new System.Windows.Forms.Timer();
private volatile bool _closing;
// ユーザーごとのアプリデータ領域にカメラ設定を保存します。
private string DeviceStatePath
{
get { return Path.Combine(Application.UserAppDataPath, "imx900hdr.xml"); }
}
private string ImageSaveSettingsPath
{
get { return Path.Combine(Application.UserAppDataPath, "image-save-settings.xml"); }
}
ここでは、カメラへの接続や画像処理、画面表示で使う情報をForm1クラスの変数として準備しています。関数の中だけで使う一時的な変数とは異なり、これらはボタン操作や画像受信など、別々のタイミングで呼ばれる処理の間でも状態を共有するために必要です。ExposureSelectorsには4つの画素グループを並べ、画面の入力欄とカメラの設定先を同じ順序で対応付けています。
画像については、次に表示する_pendingFramesと、現在確認している_displayedFramesを分けて管理しています。これにより、カメラから次の画像が届いても、Snapで固定した画像を保存対象として保持できます。また、_darkFrameCommandは暗画像の取得・解除要求を受信側へ伝えるための変数です。設定ファイルの保存先もプロパティとしてまとめることで、起動時の読み込みと終了時の保存で同じ場所を参照できるようにしています。
画面の初期化と起動時のカメラ接続
Form1.cs
//
public Form1()
{
InitializeComponent();
_individualExposureControls = new[]
{
exposureEvenEven,
exposureEvenOdd,
exposureOddEven,
exposureOddOdd
};
// 露光時間を変更したときにカメラに対して反映するイベントを登録します。個別露光は4つの入力欄が同じイベントハンドラを共有します。
foreach (CameraParameterControl control in _individualExposureControls)
control.ValueChanged += individualExposure_ValueChanged;
_sink = new QueueSink(PixelFormat.Mono8); //PixelFormatはMono8で固定します。
_sink.FramesQueued += Sink_FramesQueued;
_previewTimer.Interval = 33;// 約30fpsでGUIスレッドへ表示更新します。
_previewTimer.Tick += PreviewTimer_Tick;
}
// Form1_Loadは、フォームのロード時に呼び出されます。
// 保存先設定を読み込み、カメラの復元または選択を行い、ライブ表示を開始します。
private void Form1_Load(object sender, EventArgs e)
{
try { _imageSaveDirectory = ImageSaveSettings.Load(ImageSaveSettingsPath); }
catch (Exception ex) { SetStatus("保存先設定を読み込めませんでした: " + ex.Message); }
_previewTimer.Start();
// 前回のカメラ設定が残っていれば復元を試みます。失敗した場合は選択ダイアログを開きます。
if (!TryRestoreDevice())
SelectDevice();
// 復元または選択に成功した場合は、カメラの現在値をGUIへ反映し、ライブ表示を開始します。
if (_grabber.IsDeviceValid)
{
SetupCameraControls(); // 露光値の入力欄をカメラの現在値で初期化します。
StartStreaming(); // 6画面ライブ表示を開始します。
}
UpdateGUIState(); // 接続・取得・露光モードに応じて、GUI上で操作できるボタンや入力欄を切り替えます。
}
ここでは、フォームの生成時に画面部品とイベントを準備し、その後のフォームのロード処理でカメラの接続とライブ表示を開始しています。4つの個別露光入力欄を配列_individualExposureControlsにまとめ、どの入力欄を操作しても共通のイベント関数へ通知されるようにしています。
QueueSinkはカメラから届く画像を受け取るためのシンクです。このサンプルでは、コールバック関数として Sink_FramesQueued を登録し、ピクセルフォーマットにはMono8を指定しています。また、表示更新には33ミリ秒間隔のタイマーを使い、受信処理と画面の描画を分担させています。フォームを開いた後は保存先設定を読み込み、前回のカメラを復元できなければ選択画面を開きます。
終了処理と前回のカメラ状態の復元
Form1.cs
// Form1_FormClosingは、フォームが閉じられるときに呼び出されます。
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
_closing = true;
_previewTimer.Stop();
_previewTimer.Dispose();
Interlocked.Exchange(ref _darkFrameCommand, 0); // Dark Frameの取得待ちやリセット待ちを解除します。
try
{
if (_grabber.IsDeviceValid)
{
StopStreaming(); // 6画面ライブ表示を停止します。
Directory.CreateDirectory(Path.GetDirectoryName(DeviceStatePath)); // 保存先フォルダーがない場合は作成します。
_grabber.DeviceSaveState(DeviceStatePath); // 現在のカメラ設定をIC4固有形式で保存します。
_grabber.DeviceClose(); // カメラを閉じます。
}
}
catch { }
if (_sink != null)
{
_sink.FramesQueued -= Sink_FramesQueued; // 受信スレッドからの通知を解除します。コールバック関数が呼ばれないようになります。
_sink.Dispose(); // QueueSinkの内部バッファを解放します。
_sink = null;
}
ClearPreview();
_hdrProcessor.Dispose();
_grabber.Dispose();
}
// 前回のカメラ設定が残っていれば復元します。失敗した場合は選択ダイアログを開きます。
private bool TryRestoreDevice()
{
if (!File.Exists(DeviceStatePath))
return false;
try
{
_grabber.DeviceOpenFromState(DeviceStatePath); // 前回のカメラ設定を復元します。
if (_grabber.IsDeviceValid)
Text = "IMX900 HDR - " + _grabber.DeviceInfo.ModelName;
return _grabber.IsDeviceValid;
}
catch (Exception ex)
{
SetStatus("前回のカメラを開けませんでした: " + ex.Message);
return false;
}
}
ここでは、アプリケーションを終了するときの解放処理と、次回起動時に前回のカメラを復元する処理を行っています。FramesQueuedのイベント登録を解除することで、使い終わった受信処理への通知を止めています。
また、起動時の TryRestoreDevice では、保存ファイルの存在を確認してからDeviceOpenFromStateで復元しています。ファイルがない場合や復元できない場合はfalseを返し、呼び出し元がカメラ選択へ進めるようにしています。ここで扱う起動用ファイルはIC4の状態保存形式です。後述するSave XMLボタンの独自XMLファイルとは役割と形式が異なります。
カメラを選択する
Form1.cs
// カメラ選択ダイアログを開き、ユーザーが選択したカメラを接続します。
private bool SelectDevice()
{
try
{
StopStreaming(); // 6画面ライブ表示を停止します。
ClearPreview(); // 表示をクリアします。
ResetDarkFrameImmediately(); // Dark Frameの取得待ちやリセット待ちを解除します。
if (_grabber.IsDeviceValid)
_grabber.DeviceClose(); // カメラを閉じます。
ic4.WinForms.Dialogs.ShowDeviceDialog(_grabber, this); // カメラ選択ダイアログを開きます。ユーザーが選択したカメラを接続します。
if (!_grabber.IsDeviceValid)
return false;
Directory.CreateDirectory(Path.GetDirectoryName(DeviceStatePath)); // 保存先フォルダーがない場合は作成します。
_grabber.DeviceSaveState(DeviceStatePath); // 現在のカメラ設定をIC4固有形式で保存します。
Text = "IMX900 HDR - " + _grabber.DeviceInfo.ModelName;
return true;
}
catch (Exception ex)
{
ShowError("カメラの選択に失敗しました。", ex);
return false;
}
}
ここでは、使用するカメラを選び直す前に、現在のカメラの状態を確認しています。まずライブ取得を停止し、表示中と表示待ちの画像をクリアします。その後でDark Frameを解除し、接続済みのカメラを閉じてから、IC4のShowDeviceDialogでカメラ選択ダイアログを開きます。接続できた場合は、次回起動時の復元に使う状態を保存します。
ライブ取得を開始・停止する
Form1.cs
// 6画面ライブ表示を開始します。
private void StartStreaming()
{
if (!_grabber.IsDeviceValid) return;
_snapshotMode = false;
if (_grabber.IsStreaming)
{
SetStatus("6画面ライブ表示に戻りました");
UpdateGUIState(); // 接続・取得・露光モードに応じて、GUI上で操作できるボタンや入力欄を切り替えます。
return;
}
try
{
// Mono8以外のPixelFormatでは6画面表示はサポートしません。PropertiesでMono8に変更してください。
if (_grabber.DevicePropertyMap.GetValueString(PropId.PixelFormat) != "Mono8")
throw new NotSupportedException("6画面表示はMono8専用です。PropertiesでPixel FormatをMono8にしてください。");
_grabber.StreamSetup(_sink); // QueueSinkを使用してライブスタートします。
SetStatus("6画面ライブ表示中: 個別露光4枚 / 平均 / RAW");
}
catch (Exception ex)
{
ShowError("ライブ表示を開始できませんでした。", ex);
}
UpdateGUIState();
}
// 6画面ライブ表示を停止します。
private void StopStreaming()
{
if (!_grabber.IsDeviceValid || !_grabber.IsStreaming)
return;
try { _grabber.StreamStop(); } // 6画面ライブ表示を停止します。
catch (Exception ex) { SetStatus("ライブ停止エラー: " + ex.Message); }
UpdateGUIState();
}
ここでは、カメラの画像取得を開始する処理と停止する処理を、それぞれ共通の関数にまとめています。StartStreamingでは接続状態を確認した後、Snapによる表示固定を解除します。すでに取得中であればStreamSetupを呼び出さずにライブ表示へ戻しています。
新しく取得を始める場合は、画素形式がMono8であることを確認します。条件を満たした場合にQueueSinkを使って取得を開始し、最後にボタンの操作可否を更新します。StopStreamingも、接続と取得の両方が有効な場合だけ停止を実行します。
コールバック関数
Form1.cs
// コールバック関数。フレームを受信したタイミングで呼ばれます。
// ここではGUIで表示処理をしません。
private void Sink_FramesQueued(object sender, EventArgs e)
{
ImageBuffer cameraImage = null;
ExposureFrameSet frames = null;
try
{
// QueueSinkの内部バッファに残っている最新のフレームだけを取得し、古いフレームは破棄します。
while (_sink != null && !_sink.IsCancelRequested &&
_sink.TryPopOutputBuffer(out ImageBuffer queuedImage))
{
cameraImage?.Dispose();
cameraImage = queuedImage;
}
if (cameraImage == null || _sink == null || _sink.IsCancelRequested || _closing)
return;
ApplyPendingDarkFrameCommand(cameraImage); // Dark Frameの反映をします。
frames = _hdrProcessor.Process(cameraImage); // 6画面表示するための準備をします。
ExposureFrameSet previous = Interlocked.Exchange(ref _pendingFrames, frames); // 受信スレッドからGUIスレッドへ表示更新を通知します。
frames = null;
previous?.Dispose();
}
catch (Exception ex)
{
SetFrameErrorStatusThrottled("画像処理エラー: " + ex.Message);
}
finally
{
frames?.Dispose();
cameraImage?.Dispose();
}
}
// IC4受信スレッドから直接GUIを操作せず、1組の6枚をまとめて表示します。
private void PreviewTimer_Tick(object sender, EventArgs e)
{
// 固定中は表示を更新しません。受信側は最新の1組だけを保ち、古い組を解放します。
if (_snapshotMode || _closing) return;
ExposureFrameSet frames = Interlocked.Exchange(ref _pendingFrames, null); // 受信スレッドからGUIスレッドへ表示更新を通知します。
if (frames != null) DisplayFrames(frames); // 生成されたframesをDisplayFramesに渡し、表示と保存ができるようにします。
}
ここでは、画像を受信する処理と、その画像を画面に表示する処理を分けています。Sink_FramesQueuedは画像到着時に呼ばれるコールバック関数です。キューに複数の画像があれば古いものを解放して最新の1枚を残します。
取得した画像には保留中のDark Frameの処理を反映し、画像処理クラスで6枚の画像セットを作ります。その結果を_pendingFramesへ渡し、古い表示待ちの画像を解放します。画面側のタイマーで呼ばれるPreviewTimer_Tick は、この受け渡し先から1組を取り出してDisplayFramesを呼びます。コールバック関数 Sink_FramesQueued 側から直接GUIを操作しないように、画像処理とGUI描画の役割を分けています。
6枚を表示して保存用に保持する
Form1.cs
// 受信スレッドからの通知で、最新の1組だけを表示します。古い組は解放します。
private void DisplayFrames(ExposureFrameSet frames)
{
try
{
preview.ShowFrames(frames); // 6画面表示用の処理を行います。
ExposureFrameSet old = _displayedFrames; // 古いフレームを保持します。Disposeする前にGUI表示を更新します。
_displayedFrames = frames; // 新しいフレームを保持します。Disposeする前にGUI表示を更新します。
old?.Dispose();
if (old == null) UpdateGUIState(); // 初回表示時にボタンや入力欄の操作可否を更新します。
}
catch (Exception ex)
{
frames.Dispose();
preview.ClearImages();
_displayedFrames?.Dispose();
_displayedFrames = null;
SetFrameErrorStatusThrottled("表示エラー: " + ex.Message);
UpdateGUIState();
}
}
ここでは、処理済みの6枚を画面へ表示し、その画像セットを保存用として保持しています。preview.ShowFramesに表示を依頼した後、新しい画像セットを_displayedFramesへ登録し、これまで表示していた古いセットを解放します。単に画面へ描くだけでなく元の画像データを保持しておくことで、保存ボタンを押したときに、ユーザーが確認している6枚をそのまま保存処理へ渡せます。
Snap・表示クリア・Dark Frame処理の反映
Form1.cs
private void btnSnap_Click(object sender, EventArgs e)
{
if (_displayedFrames == null) return;
_snapshotMode = true;
ExposureFrameSet latest = Interlocked.Exchange(ref _pendingFrames, null); // 受信スレッドからGUIスレッドへ表示更新を通知します。
if (latest != null) DisplayFrames(latest); // 最新のフレームを表示します。
if (_displayedFrames == null) return;
SetStatus("Snap: 6枚の静止画を表示中です。BMP保存、またはLive Startでライブ表示へ戻れます。");
UpdateGUIState();
}
// 呼び出し前に取得を停止します。カメラを変更した後に古い画像を残しません。
private void ClearPreview()
{
Interlocked.Exchange(ref _pendingFrames, null)?.Dispose();
preview.ClearImages();
_displayedFrames?.Dispose();
_displayedFrames = null;
_snapshotMode = false;
UpdateGUIState();
}
// 受信スレッドからの通知で、Dark Frameの取得やリセットを反映します。
private void ApplyPendingDarkFrameCommand(ImageBuffer cameraImage)
{
int command = Interlocked.Exchange(ref _darkFrameCommand, 0);
if (command > 0)
{
_hdrProcessor.SetDarkFrame(cameraImage);
SetDarkFrameState(true, "Dark Frame: 取得済み");
SetStatus("Dark Frameを取得しました");
}
else if (command < 0)
{
_hdrProcessor.ResetDarkFrame();
SetDarkFrameState(false, "Dark Frame: なし");
SetStatus("Dark Frameをリセットしました");
}
}
ここでは、Snapによる静止画確認、表示のクリア、Dark Frame処理の実行など、画像の状態を切り替える処理をまとめています。Snapでは_snapshotModeを有効にし、表示待ちの最新画像があればその1組を表示してから固定します。新しい撮影命令をカメラへ送るのではなく、受信済みの画像を確認用に止めています。
ClearPreviewは、表示待ちと表示中の両方の画像を解放し、Snap状態も解除します。カメラの切り替えなどで呼ぶ際は、先に取得を停止することが前提です。一方、Dark Frameの取得・解除は、画面操作の時点では要求として記録し、画像受信時にApplyPendingDarkFrameCommandで実行します。
カメラの設定を読み込む
Form1.cs
// カメラの現在値と設定範囲を読み取り、共通露光・個別露光の入力欄を初期化します。
private bool SetupCameraControls()
{
_cameraControlsReady = false;
if (!_grabber.IsDeviceValid) return false;
_updatingControls = true;
try
{
PropertyMap properties = _grabber.DevicePropertyMap;
// XMLやProperties画面の値を優先します。同期時にAutoや露光値を上書きしません。
string originalMode = properties.GetValueString(PropId.ExposureTimeMode); // Common / Individual
string originalSelector = properties.GetValueString(PropId.ExposureTimeSelector); // EvenRowEvenColumn / EvenRowOddColumn / OddRowEvenColumn / OddRowOddColumn
try
{
ConfigureParameter(gainControl, "Gain", properties.Find(PropId.Gain), false, false); // ゲインは小数点を表示します。
properties.SetValue(PropId.ExposureTimeMode, "Common"); // 共通露光の範囲を読み取るためにモードを切り替えます。
properties.SetValue(PropId.ExposureTimeSelector, "Common");
ConfigureParameter(commonExposureControl, "Common Exposure",
properties.Find(PropId.ExposureTime), true, false);
properties.SetValue(PropId.ExposureTimeMode, "Individual"); // 個別露光の範囲を読み取るためにモードを切り替えます。
string[] titles = { "Even / Even", "Even / Odd", "Odd / Even", "Odd / Odd" };
for (int i = 0; i < ExposureSelectors.Length; i++)
{
properties.SetValue(PropId.ExposureTimeSelector, ExposureSelectors[i]);// 個別露光の範囲を読み取るためにSelectorを切り替えます。
ConfigureParameter(_individualExposureControls[i], titles[i],
properties.Find(PropId.ExposureTime), false, true);
}
}
finally
{
// 読み取りのため切り替えたモードとSelectorを、元の選択に戻します。
properties.SetValue(PropId.ExposureTimeMode, originalMode);
properties.SetValue(PropId.ExposureTimeSelector, originalSelector);
}
_hdrEnabled = string.Equals(originalMode, "Individual", StringComparison.OrdinalIgnoreCase);
radioIndividual.Checked = _hdrEnabled;
radioCommon.Checked = !_hdrEnabled;
radioIndividual.Enabled = true;
_cameraControlsReady = true;
}
catch (Exception ex)
{
radioIndividual.Enabled = false;
SetStatus("露光設定を画面に読み込めませんでした: " + ex.Message);
}
finally
{
_updatingControls = false;
}
// 設定の読み直しでは取得済みのDark Frameを保持し、補正を継続します。
UpdateGUIState();
return _cameraControlsReady;
}
ここでは、カメラが保持している現在値と設定範囲を読み取り、共通露光・個別露光・ゲインの入力欄へ反映しています。ExposureTimeはSelectorによって参照先が変わるため、4つの値を読むには対象グループを順番に選ぶ必要があります。この処理ではCommonとIndividualを切り替えながら、各入力欄に必要な情報を取得します。_updatingControlsを有効にしている間は、入力欄の更新をユーザーの操作として扱わないようにします。
入力範囲・単位を設定する
Form1.cs
// カメラが返す範囲・刻み・単位を入力欄に設定します。
// 個別露光では範囲を整数にそろえ、小数点を表示しません。
private static void ConfigureParameter(CameraParameterControl control, string title,
PropFloat property, bool preferLogarithmic, bool forceInteger)
{
double minimum = property.Minimum;
double maximum = property.Maximum;
double value = property.Value;
double increment = property.IncrementMode == PropertyIncrementMode.Increment
? property.Increment
: 0.0;
int precision = forceInteger
? 0
: (int)Math.Max(0, Math.Min(6, property.DisplayPrecision));
if (forceInteger)
{
// カメラの範囲からはみ出さないよう、下限を切り上げ、上限を切り下げます。
minimum = Math.Ceiling(minimum);
maximum = Math.Floor(maximum);
if (maximum < minimum)
maximum = minimum;
value = Math.Max(minimum, Math.Min(maximum, Math.Round(value)));
increment = Math.Max(1.0, Math.Round(increment));
}
// 個別露光は整数・線形で操作します。
// カメラが対数表現を指定していても、個別露光では線形変換を使用します。
bool logarithmic = !forceInteger && (preferLogarithmic ||
property.Representation == FloatRepresentation.Logarithmic);
string unit = string.IsNullOrWhiteSpace(property.Unit) ? string.Empty : " (" + property.Unit + ")";
control.Configure(title + unit, minimum, maximum, value,
increment, logarithmic, precision,
!forceInteger && property.IncrementMode == PropertyIncrementMode.ValueSet
? property.ValidValueSet
: null);
}
ここでは、モードや撮影条件によって設定範囲が変わる可能性があるため、カメラの数値プロパティが持つ下限・上限・現在値・刻み・単位を、画面の入力部品へ渡しています。DisplayPrecisionも参照し、小数を何桁まで表示するかを調整しています。ユーザーは入力欄の数値だけでなく、設定可能な範囲と単位も合わせて確認できます。
個別露光ではforceIntegerを指定し、下限を切り上げ、上限を切り下げて整数の範囲へそろえます。現在値も丸めて範囲内に収め、小数点を表示しない構成にしています。また、個別露光のスライダーには指数ではなく線形の対応を使い、同じ移動量に対して値がほぼ一定量ずつ増減するようにしています。
CommonとIndividualを切り替える
Form1.cs
// 露光モードを切り替え、選択したモードの画面上の値をカメラへ適用します。
private void SetExposureMode(bool individual)
{
if (_updatingControls || !_grabber.IsDeviceValid)
return;
try
{
PropertyMap properties = _grabber.DevicePropertyMap;
properties.SetValue(PropId.ExposureAuto, "Off"); // 自動露光を無効にします。
properties.SetValue(PropId.ExposureTimeMode, individual ? "Individual" : "Common"); // 露光モードを切り替えます。
_hdrEnabled = individual;
if (individual)
{
for (int i = 0; i < ExposureSelectors.Length; i++)
WriteIndividualExposure(i); // 個別露光の4つの入力欄の値をカメラへ適用します。
}
else
{
properties.SetValue(PropId.ExposureTimeSelector, "Common"); // 共通露光の入力欄の値をカメラへ適用します。
properties.SetValue(PropId.ExposureTime, commonExposureControl.Value); // 共通露光の入力欄の値をカメラへ適用します。
PropFloat exposure = properties.Find(PropId.ExposureTime); // 適用後の実値を読み戻し、GUI入力欄へ反映します。
commonExposureControl.SetValueSilently(exposure.Value);
}
SetStatus(individual ? "Individual: 4系統の個別露光を6画面で表示" :
"Common: 共通露光の画像を6画面で表示");
}
catch (Exception ex)
{
_hdrEnabled = false;
SetStatus("露光モードの変更に失敗しました: " + ex.Message);
}
UpdateGUIState();
}
ここでは、ユーザーが選んだ露光モードをカメラへ設定しています。最初に画面の初期化中ではないこととカメラの接続を確認し、自動露光を無効にしてからExposureTimeModeを変更します。
Individualを選んだ場合は、4つの入力欄に対応するWriteIndividualExposureを順に呼び出します。Commonの場合は共通のSelectorを選択し、共通露光時間を書き込んでから実際の値を読み戻します。
最後にUpdateGUIStateを呼び、選択したモードで使える入力欄を有効にします。ここで切り替えるのは共通露光と個別露光であり、露光開始・終了・中点のどこをそろえるかを選択する機能ではありません。
個別露光時間を書き込み、カメラの実際の値を確認する
Form1.cs
// 対象の画素グループを選択し、露光値を書き込んで結果を読み戻します。
private void WriteIndividualExposure(int index)
{
if (!_grabber.IsDeviceValid || !_hdrEnabled || index < 0 || index >= ExposureSelectors.Length)
return;
try
{
PropertyMap properties = _grabber.DevicePropertyMap; // 個別露光の4つの入力欄の値をカメラへ適用します。
try { properties.SetValue(PropId.ExposureAuto, "Off"); } catch { } // 自動露光を無効にします。
properties.SetValue(PropId.ExposureTimeMode, "Individual"); // 個別露光モードに切り替えます。
// 要求したモードが実際に適用されたことを確認してから個別露光を操作します。
string appliedMode = properties.GetValueString(PropId.ExposureTimeMode);
if (!string.Equals(appliedMode, "Individual", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("ExposureTimeModeをIndividualへ変更できませんでした。");
properties.SetValue(PropId.ExposureTimeSelector, ExposureSelectors[index]); // 個別露光の範囲を読み取るためにSelectorを切り替えます。
double requestedValue = Math.Round(_individualExposureControls[index].Value); // 要求された露光値を取得します。
PropFloat exposure = properties.Find(PropId.ExposureTime);
// 取得中のロックなどで設定できない状態を確認し、失敗理由を表示できるようにします。
if (!exposure.IsAvailable)
throw new InvalidOperationException("選択した個別露光プロパティを利用できません。");
if (exposure.IsReadonly || exposure.IsLocked)
throw new InvalidOperationException("選択した個別露光プロパティは現在変更できません。");
// 変更前後を比較するため現在値を保持し、書き込み後の実値を読み戻します。
double valueBeforeWrite = exposure.Value;
exposure.Value = requestedValue;
double actualAppliedValue = exposure.Value;
// 読み戻した値も整数表示にそろえます。通知を抑えて書き込みの再発を防ぎます。
double displayedAppliedValue = Math.Round(actualAppliedValue);
_individualExposureControls[index].SetValueSilently(displayedAppliedValue);
// 要求値と適用値を表示します。単位が空文字の場合は表示しません。
SetStatus(string.Format("{0}: 要求 {1:N0} → 適用 {2:N0} {3}",
GetExposureDisplayName(index), requestedValue, actualAppliedValue,
string.IsNullOrWhiteSpace(exposure.Unit) ? "" : exposure.Unit));
// 丸め誤差を許容しつつ、変更を要求したのに値が変わらない場合を検出します。
double tolerance = Math.Max(0.5, Math.Abs(requestedValue) * 0.000001);
if (Math.Abs(requestedValue - valueBeforeWrite) > tolerance &&
Math.Abs(actualAppliedValue - valueBeforeWrite) <= tolerance)
{
throw new InvalidOperationException(
"カメラが個別露光値の変更を受け付けませんでした。カメラ設定とPixelFormatを確認してください。");
}
// 露光変更後も、ユーザーが取得したDark Frameによる補正を維持します。
}
catch (Exception ex)
{
SetStatus("個別露光の設定に失敗しました: " + ex.Message);
}
}
ここでは、指定された画素グループの露光時間を書き込み、カメラが実際に受け付けた値を確認しています。
最初に接続・モード・配列番号を確認し、Individualへの切り替え結果も読み戻します。その後、ExposureTimeSelectorで対象を選んでからExposureTimeへアクセスします。
書き込み前にはプロパティが利用可能か、読み取り専用やロック状態ではないかを確認します。書き込んだ後は実値を読み直し、通知を出さずに入力欄へ戻します。さらに、変更を要求したのに値が変化しなかった場合は、エラーとして知らせます。
接続・取得状態に応じて画面を更新する
Form1.cs
// 接続・取得・露光モードに応じて、操作できるボタンや入力欄を切り替えます。
// 受信スレッドから呼ばれた場合は、画面を操作するGUIスレッドへ処理を渡します。
private void UpdateGUIState()
{
if (InvokeRequired)
{
try { BeginInvoke((MethodInvoker)UpdateGUIState); } catch (InvalidOperationException) { }
return;
}
bool valid = _grabber.IsDeviceValid;
bool streaming = valid && _grabber.IsStreaming;
btnProperties.Enabled = valid;
btnSaveSettings.Enabled = valid;
btnSaveImage.Text = "Save All 6 BMP";
labelCaptureState.Text = _displayedFrames == null ? "画像なし" :
(_snapshotMode ? "SNAP:静止画を確認中" : (streaming ? "LIVE" : "STOP:静止画を確認中"));
labelSaveFolder.Text = string.IsNullOrEmpty(_imageSaveDirectory)
? "保存先:未指定(初回保存時に選択)" : "保存先:" + _imageSaveDirectory;
radioCommon.Enabled = valid && _cameraControlsReady;
radioIndividual.Enabled = valid && _cameraControlsReady;
btnLiveStart.Enabled = valid && (!streaming || _snapshotMode);
btnLiveStop.Enabled = streaming;
btnSnap.Enabled = _displayedFrames != null && streaming;
btnSaveImage.Enabled = _displayedFrames != null;
btnSnapDarkFrame.Enabled = streaming && !_snapshotMode;
btnResetDarkFrame.Enabled = _darkFrameActive || Volatile.Read(ref _darkFrameCommand) != 0;
gainControl.Enabled = valid && _cameraControlsReady;
commonExposureControl.Enabled = valid && _cameraControlsReady && !_hdrEnabled;
// 個別露光の入力欄は、カメラが接続されていて、カメラの設定が読み込まれ、individual露光モードが有効な場合に操作可能。
foreach (CameraParameterControl control in _individualExposureControls)
control.Enabled = valid && _cameraControlsReady && _hdrEnabled;
}
ここでは、カメラとアプリの現在の状態を調べ、操作できるボタンや入力欄を切り替えています。接続していなければプロパティ操作を無効にし、画像がなければ保存ボタンを無効にします。一方、取得を停止していても表示画像が残っていれば、BMP保存は有効なままです。接続の有無だけではなく、「取得中か」「確認用の画像があるか」を分けて判定することで、実際にできる操作と画面の表示を一致させています。
露光の入力欄も、Commonでは共通露光だけ、Individualでは4つの個別露光だけを操作可能にします。また、Snap中はLive Startで表示を再開できるようにし、Dark Frameの取得ボタンには別の条件を設けています。状態変更のたびに各イベント関数へ同じ判定を書くと条件が食い違いやすいため、この関数へまとめています。受信側から呼ばれた場合はBeginInvokeで画面を扱うスレッドへ処理を渡し、別スレッドから直接ボタンやラベルを更新しないようにしています。
カメラ選択ボタンの処理
Form1.cs
// カメラを選択し直したら、入力範囲を更新してライブ表示を開始します。
private void btnDevice_Click(object sender, EventArgs e)
{
// カメラ選択ダイアログを開きます。
if (SelectDevice())
{
SetupCameraControls(); // 露光値の入力欄をカメラの現在値で初期化します。
StartStreaming(); // 6画面ライブ表示を開始します。
}
UpdateGUIState(); // 接続・取得・露光モードに応じて、GUI上で操作できるボタンや入力欄を切り替えます。
}
ここでは、カメラ選択ボタンが押されたことを受けて、選択・入力欄の初期化・ライブ開始を順番に呼び出しています。イベント関数とは、ボタンのクリックなどの操作をきっかけに実行される入口です。この関数自体に接続や画像取得の詳細を書かず、SelectDevice、SetupCameraControls、StartStreamingへ役割を分けることで、ユーザーの操作から何が起きるのかを分かりやすくしています。
SelectDeviceがtrueを返した場合だけ後続の処理へ進むようにし、接続できたときは、まずカメラの現在値を入力欄へ読み込み、その後でライブ表示を開始します。
最後のUpdateGUIStateではGUI上で操作できるボタンや入力欄を切り替えます。
Properties画面を開く
Form1.cs
// 標準設定画面を開きます。閉じた後に値を読み直し、画面とカメラを同期します。
private void btnProperties_Click(object sender, EventArgs e)
{
if (!_grabber.IsDeviceValid) return;
bool wasStreaming = _grabber.IsStreaming;
try
{
StopStreaming();
if (_grabber.IsStreaming)
throw new InvalidOperationException("カメラの取得を停止できませんでした。");
ClearPreview();
ic4.WinForms.Dialogs.ShowDevicePropertyDialog(_grabber, this);
SetupCameraControls();
Directory.CreateDirectory(Path.GetDirectoryName(DeviceStatePath));
_grabber.DeviceSaveState(DeviceStatePath);
}
catch (Exception ex)
{
ShowError("カメラプロパティを開けませんでした。", ex);
}
finally
{
if (wasStreaming) StartStreaming();
}
}
ここでは、IC4のShowDevicePropertyDialogで標準のProperties画面を開き、そこで変更した設定をアプリの入力欄へ反映しています。最初に取得中かどうかを記録し、ストリームを停止してから設定画面を開きます。
Properties画面を閉じると、SetupCameraControlsで露光値などを読み直し、次回起動用のカメラ状態を保存します。設定を読み直しても取得済みのDark Frameは保持しています。
カメラ設定をXML保存する
Form1.cs
// xml形式でカメラ設定を保存します。保存先はユーザーが選択します。
private void btnSaveSettings_Click(object sender, EventArgs e)
{
if (!_grabber.IsDeviceValid) return;
using (var dialog = new SaveFileDialog
{
Title = "カメラのプロパティ設定をXML保存",
Filter = "Camera settings (*.xml)|*.xml", DefaultExt = "xml",
AddExtension = true, FileName = "imx900_settings.xml"
})
{
if (dialog.ShowDialog(this) != DialogResult.OK) return;
try
{
CameraSettingsFile.Save(dialog.FileName, _grabber.DeviceSaveState());
SetStatus("カメラ設定を保存しました: " + dialog.FileName);
}
catch (Exception ex) { ShowError("XML設定を保存できませんでした。", ex); }
}
}
ここでは、現在のカメラ設定を、ユーザーが指定したXMLファイルへ保存しています。まず有効なカメラ接続があることを確認し、SaveFileDialogで保存先とファイル名を選びます。拡張子の指定や候補のファイル名を用意することで、設定ファイルであることを分かりやすくしています。選択をキャンセルした場合は、保存処理を実行せずに終了します。
保存時にはDeviceSaveStateでIC4の状態データを取得し、CameraSettingsFile.Saveへ渡します。画面上の4つの露光値だけを手作業で列挙するのではなく、IC4が保存対象とするカメラ状態を独自XMLへ格納する構成です。
XMLファイルを検査して読み込む
Form1.cs
// xml形式でカメラ設定を読み込みます。読み込んだ設定はカメラに適用されます。
private void btnLoadSettings_Click(object sender, EventArgs e)
{
using (var dialog = new OpenFileDialog
{
Title = "カメラのプロパティ設定をXML読込",
Filter = "Camera settings (*.xml)|*.xml", CheckFileExists = true
})
{
if (dialog.ShowDialog(this) != DialogResult.OK) return;
byte[] state;
try { state = CameraSettingsFile.Load(dialog.FileName); }
catch (Exception ex)
{
ShowError("XML設定を読み込めませんでした。現在の設定は変更していません。", ex);
return;
}
LoadCameraState(state);
}
}
ここでは、読み込み対象のXMLファイルを選択し、カメラへ適用する前に内容を読み取っています。OpenFileDialogではXMLを選びやすいフィルターを指定し、実在するファイルを選ぶようにしています。ユーザーがキャンセルした場合は何も変更せず終了します。ファイルが選ばれた後にCameraSettingsFile.Loadを呼び、復元に必要な状態データを取り出すようになっています。
読み込んだ設定をカメラへ反映する
Form1.cs
// 読み込んだXML設定をカメラに適用します。
private void LoadCameraState(byte[] state)
{
bool wasStreaming = _grabber.IsDeviceValid && _grabber.IsStreaming;
string originalDevice = _grabber.IsDeviceValid ? _grabber.DeviceInfo.UniqueName : null;
byte[] backup = null;
bool deviceChanged = false;
try
{
StopStreaming(); // ライブ表示を停止します。
if (_grabber.IsStreaming)
throw new InvalidOperationException("カメラの取得を停止できませんでした。");
if (_grabber.IsDeviceValid)
backup = _grabber.DeviceSaveState(); // 現在のカメラ設定をバックアップします。
ClearPreview(); // 表示をクリアします。
if (_grabber.IsDeviceValid) _grabber.DeviceClose(); // 接続済みのカメラを閉じてから、設定を復元します。
deviceChanged = true;
_grabber.DeviceOpenFromState(state); // XML設定を読み込み、カメラを開きます。
if (!SetupCameraControls()) // 露光値の入力欄をカメラの現在値で初期化します。
throw new InvalidOperationException("読み込んだカメラ設定の露光値を確認できませんでした。");
// 同じカメラへのXML設定読み込みではDark Frameの補正値を保持します。別カメラへのXML設定読み込みでは補正値を解除します。
if (!string.Equals(originalDevice, _grabber.DeviceInfo.UniqueName, StringComparison.Ordinal))
ResetDarkFrameImmediately(); // 別カメラへのXML設定読み込みでは補正値を解除します。
Text = "IMX900 HDR - " + _grabber.DeviceInfo.ModelName;
if (wasStreaming) StartStreaming();
SetStatus("XMLのカメラ設定を読み込みました。停止中の場合はLive Startで表示します。");
}
catch (Exception ex)
{
string message = "カメラ設定の読み込みに失敗しました。";
if (deviceChanged)
{
try
{
if (_grabber.IsDeviceValid) _grabber.DeviceClose();
if (backup != null)
{
_grabber.DeviceOpenFromState(backup);
SetupCameraControls();
Text = "IMX900 HDR - " + _grabber.DeviceInfo.ModelName;
message += " 元のカメラ設定へ戻しました。";
}
}
catch (Exception restoreError)
{
message += " 元の設定の復元にも失敗しました: " + restoreError.Message;
}
}
if (wasStreaming && _grabber.IsDeviceValid) StartStreaming();
ShowError(message, ex);
}
finally { UpdateGUIState(); }
}
ここでは、読み込んだ状態データをカメラへ適用し、失敗した場合には元の状態への復元を試みています。まず取得中かどうかと接続先の状態を記録し、取得を止めてから現在のカメラ設定をバックアップします。その後、表示をクリアしてカメラを閉じ、DeviceOpenFromStateで設定を復元します。接続し直した後に入力欄も更新することで、カメラの値と画面の表示をそろえています。同じカメラに設定を読み込んだ場合はDark Frameを保持し、別のカメラへ変わった場合は解除します。
ここで作るデータは復元に備えたメモリ上の一時データであり、ユーザーが後から選び直せる設定ファイルとして保存するものではありません。継続して残したい設定はSave XMLで別途保存してください。
取得ボタンと露光モードのイベントを接続する
Form1.cs
// 開始・停止ボタンから共通のストリーム制御処理を呼び出します。
private void btnLiveStart_Click(object sender, EventArgs e) { StartStreaming(); }
private void btnLiveStop_Click(object sender, EventArgs e) { StopStreaming(); }
// 共通露光が選択された場合に、カメラを共通露光モードへ切り替えます。
private void radioCommon_CheckedChanged(object sender, EventArgs e)
{
if (radioCommon.Checked)
SetExposureMode(false); // 共通露光モードへ切り替えます。
}
// 個別露光が選択された場合に、カメラを個別露光モードへ切り替えます。
private void radioIndividual_CheckedChanged(object sender, EventArgs e)
{
if (radioIndividual.Checked)
SetExposureMode(true);
}
ここでは、開始・停止ボタンと露光モードのラジオボタンを、実際の処理を行う共通関数へ接続しています。Live Startを押すとStartStreaming、Live Stopを押すとStopStreamingを呼びます。ボタンごとに詳細な取得処理を書くのではなく、操作の入口だけを短く記述することで、取得手順を変更するときの修正箇所をまとめられます。
ラジオボタンについては、CheckedChangedが「選択されたとき」だけでなく「選択が外れたとき」にも発生する点に注意が必要です。例えばCommonからIndividualへ切り替えると、両方の状態が変化します。そこでCheckedを確認し、選択された側だけがSetExposureModeを呼ぶようにしています。この確認がないと、1回の操作で両方のモード設定を実行してしまう可能性があります。
ゲインを変更する
Form1.cs
// 手動ゲインを適用して読み戻します。取得済みのDark Frameは保持します。
private void gainControl_ValueChanged(object sender, EventArgs e)
{
if (_updatingControls || !_grabber.IsDeviceValid)
return;
try
{
PropertyMap properties = _grabber.DevicePropertyMap; // ゲインの入力欄の値をカメラに適用します。
try { properties.SetValue(PropId.GainAuto, "Off"); } catch { } // 自動ゲインを無効にします。
properties.SetValue(PropId.Gain, gainControl.Value); // GUIでの設定値をカメラに適用します。
gainControl.SetValueSilently(properties.Find(PropId.Gain).Value);
}
catch (Exception ex) { SetStatus("ゲイン設定エラー: " + ex.Message); }
}
ここでは、ゲインの入力欄で指定した値をカメラへ書き込み、その結果を画面へ読み戻しています。最初に画面の初期化中ではないこととカメラの接続を確認し、自動ゲインの無効化を試みてから手動値を適用します。
書き込み後にプロパティの実値を読み直すのは、要求した値とカメラが受け付けた値をそろえて表示するためです。SetValueSilentlyを使うことで、表示を戻す操作が再びValueChangedを発生させ、同じ書き込みを繰り返すことを防ぎます。エラーは状態欄へ表示します。また、この処理ではDark Frameを解除しないため、ゲインを変更しても補正の利用状態は維持されます。ただし、暗画像の値をゲイン比で自動調整するわけではありません。変更後の条件に合った補正が必要な場合は、遮光した状態で改めて暗画像を取得してください。
共通露光時間を変更する
Form1.cs
// Commonモードでのみ共通露光時間を書き込み、設定した値を入力欄へ戻します。
private void commonExposureControl_ValueChanged(object sender, EventArgs e)
{
if (_updatingControls || !_grabber.IsDeviceValid || _hdrEnabled)
return;
try
{
PropertyMap properties = _grabber.DevicePropertyMap;
try { properties.SetValue(PropId.ExposureAuto, "Off"); } catch { }
properties.SetValue(PropId.ExposureTimeSelector, "Common");
properties.SetValue(PropId.ExposureTime, commonExposureControl.Value);
commonExposureControl.SetValueSilently(properties.Find(PropId.ExposureTime).Value);
}
catch (Exception ex) { SetStatus("共通露光設定エラー: " + ex.Message); }
}
ここでは、Commonモードで使用する共通露光時間をカメラへ設定しています。最初の条件判定では、画面の初期化中、カメラ未接続、Individualモードのいずれかに該当する場合に処理を終了します。共通露光の入力欄が変更されたという通知だけで書き込まず、現在のモードも確認することで、個別露光を使っているときに共通設定を誤って変更することを避けています。
処理を進める場合は自動露光の無効化を試み、ExposureTimeSelectorをCommonにしてからExposureTimeへ入力値を書き込みます。ExposureTimeという同じプロパティ名を使っていても、Selectorによって参照する対象が変わりますので注意してください。最後にカメラの値を読み戻し、通知を出さずに入力欄へ反映します。
Commonでは全グループが共通の露光条件になりますが、表示される4枚はそれぞれ異なる位置の画素から作られています。そのため、露光条件が同じでも細部の画素値まで一致するとは限りません。露光条件が共通であることと、画像そのものが同一であることは別だと理解すると、6画面の比較結果を読み取りやすくなります。
Dark Frameを取得する要求を登録する
Form1.cs
// 次のフレームをDark Frameにする要求を登録します。取得済みになるまで遮光します。
private void btnSnapDarkFrame_Click(object sender, EventArgs e)
{
Interlocked.Exchange(ref _darkFrameCommand, 1); // 次のフレームをDark Frameとして取得する要求を登録します。
labelDarkFrame.Text = "Dark Frame: 取得待ち";
SetStatus("次のフレームをDark Frameとして取得します...");
UpdateGUIState(); // 取得待ちの状態に合わせて画面を更新します。
}
ここでは、次に受信する画像をDark Frameとして使うための処理をしています。ボタンのクリック処理では暗画像を直接作成せず、Interlocked.Exchangeで_darkFrameCommandを「1」に変更します。この値を画像受信側が読み取り、実際に届いた画像を補正用として保持します。画面操作と画像受信は別のタイミングで起きるため、要求を受け渡す仕組みが必要です。
Dark Frameのリセットを要求する
Form1.cs
// 取得中は次のフレームでリセットします。停止中はフレームを待たずに解除します。
private void btnResetDarkFrame_Click(object sender, EventArgs e)
{
if (_grabber.IsDeviceValid && _grabber.IsStreaming)
{
Interlocked.Exchange(ref _darkFrameCommand, -1);
labelDarkFrame.Text = "Dark Frame: リセット待ち";
SetStatus("次のフレームでDark Frameをリセットします...");
}
else
{
ResetDarkFrameImmediately(); // 停止中はフレームを待たずに解除します。
SetStatus("Dark Frameをリセットしました");
}
UpdateGUIState();
}
ここでは、Dark Frame補正を解除しています。画像取得中は_darkFrameCommandに「-1」を設定し、次の画像受信時に解除を実行するよう要求します。画像処理が動いている間に画面側から直接補正画像を切り替えるのではなく、受信側の処理手順の中で補正を解除するようにしています。
Dark Frameを直ちに解除する
Form1.cs
// 保留中の要求を取り消し、Dark Frameを解放して状態表示も更新します。
private void ResetDarkFrameImmediately()
{
Interlocked.Exchange(ref _darkFrameCommand, 0); // 保留中の要求を取り消します。
_hdrProcessor.ResetDarkFrame(); // Dark Frameを解放します。
SetDarkFrameState(false, "Dark Frame: なし"); // 状態表示も更新します。
}
ここでは、次の画像受信を待たずにDark Frameを解除する共通処理を行っています。最初に_darkFrameCommandを0へ戻し、保留中の取得要求やリセット要求を取り消します。その後、画像処理クラスのResetDarkFrameを呼んで保持している暗画像を解放し、SetDarkFrameStateで補正が無効になったことを画面へ伝えます。要求・画像データ・表示の3つをまとめてリセットしています。
Dark Frameの状態を画面へ反映する
Form1.cs
// Dark Frameの状態を共有し、表示変更はGUIスレッドで行います。
private void SetDarkFrameState(bool active, string text)
{
_darkFrameActive = active;
if (_closing || IsDisposed)
return;
if (InvokeRequired)
{
try { BeginInvoke((MethodInvoker)delegate { SetDarkFrameState(active, text); }); } // GUIスレッドで表示変更します。
catch (InvalidOperationException) { }
return;
}
labelDarkFrame.Text = text;
UpdateGUIState();
}
ここでは、Dark Frameが有効かどうかを共有変数へ記録し、その状態をラベルとボタンへ反映しています。この関数は、暗画像の取得が完了した受信側からも呼ばれます。画面部品はGUIスレッドで操作する必要があるため、呼び出し元が画面側かどうかをInvokeRequiredを使って確認しています。
別スレッドから呼ばれた場合は、BeginInvoke(画面更新を予約する仕組み)で同じ処理を画面側へ依頼していったん戻ります。依頼先で再び実行されたときにラベルの文字を変更し、UpdateGUIStateでリセットボタンなどの操作可否を更新します。
現在表示している6枚をBMP保存する
Form1.cs
// 確認中の6枚を保存します。取得停止中でも、保持した画像があれば保存できます。
private void btnSaveImage_Click(object sender, EventArgs e)
{
if (_displayedFrames == null) return;
if (string.IsNullOrEmpty(_imageSaveDirectory) && !ChooseImageSaveDirectory()) return;
if (_displayedFrames == null || _closing) return;
if (!Directory.Exists(_imageSaveDirectory))
{
SetStatus("保存先フォルダーが見つかりません。Save Folderで保存先を変更してください。");
return;
}
// 保存中はGUI側の画像更新が入らないため、確認中の同じ6枚を最後まで保存できます。
UseWaitCursor = true;
try
{
string savedDirectory = SixFrameExporter.Save(_imageSaveDirectory, _displayedFrames);
SetStatus("確認中の6枚のBMPを保存しました: " + savedDirectory);
}
catch (Exception ex) { SetStatus("BMP保存に失敗しました: " + ex.Message); }
finally { UseWaitCursor = false; UpdateGUIState(); }
}
ここでは、現在表示している6枚の画像セットをBMP形式で保存しています。まず保存対象の有無を確認し、保存先が未指定であればフォルダー選択へ進みます。選択画面を閉じた後にも画像と終了状態を確認し、保存先フォルダーが存在するかを調べてからSixFrameExporter.Saveを呼びます。保存先がなくなっている場合は、別の場所へ勝手に変更せず、ユーザーへ再指定を案内します。
保存に使うのは_displayedFramesが保持している画像データです。デジタルズームした表示範囲だけを切り出して保存するわけではなく、十字マークなどのオーバーレイ情報もBMPには含まれません。
露光グループの表示名を取得する
Form1.cs
// 配列の番号を画面と同じ露光グループ名に変換します。
private static string GetExposureDisplayName(int index)
{
switch (index)
{
case 0: return "Even / Even";
case 1: return "Even / Odd";
case 2: return "Odd / Even";
case 3: return "Odd / Odd";
default: return "Exposure";
}
}
ここでは、プログラム内部で使う配列番号を、画面上で分かりやすい露光グループ名へ変換しています。4つの入力欄やSelectorは配列で管理しているため、内部処理では0から3の番号を使います。しかし、状態欄に番号だけを表示しても、ユーザーにはどの露光時間を変更したのか分かりにくくなります。そこで、switch文でEven / Evenなどの表記へ置き換えています。
暗画像を保持し、補正前RAWと補正用画像を分ける
Imx900HdrProcessor.cs
public void SetDarkFrame(ImageBuffer cameraImage)
{
// Mono8で偶数幅・偶数高さの画像であることを確認します。
ValidateInput(cameraImage);
// OpenCVのMatに変換してコピーします。カメラの受信バッファは再利用されるため、参照を保持してはいけません。
using (Mat source = cameraImage.CreateOpenCvWrap())
{
Mat replacement = source.Clone(); // コピーして保持します。
lock (_sync)
{
_darkFrame?.Dispose();
_darkFrame = replacement;
}
}
}
public ExposureFrameSet Process(ImageBuffer cameraImage)
{
// Mono8で偶数幅・偶数高さの画像であることを確認します。
ValidateInput(cameraImage);
lock (_sync)
{
// 6画面表示用の画像を生成します。途中で例外が発生した場合は、確保したバッファを破棄します。
var result = new ExposureFrameSet();// 0~3:行・列の偶奇順、4:平均、5:補正前RAW
try
{
using (Mat source = cameraImage.CreateOpenCvWrap()) // OpenCVのMatに変換します。
using (Mat corrected = source.Clone())// 補正前RAWをコピーします。
{
// RAWはDark Frame補正・平均化・明るさ調整をせず、元の画素値をコピーします。
result.Images[5] = CopyToImageBuffer(source);
if (_darkFrame != null &&
_darkFrame.Size() == corrected.Size() &&
_darkFrame.Type() == corrected.Type())
Cv2.Subtract(corrected, _darkFrame, corrected);// Dark Frame補正を行います。
// 4つの露光画像と平均を同じ走査で作り、全画面の撮影時刻をそろえます。
Mat[] separated = new Mat[5];
try
{
for (int i = 0; i < separated.Length; i++)
separated[i] = new Mat(source.Rows / 2, source.Cols / 2, MatType.CV_8UC1);
SplitAndAverage(corrected, separated); // 隣り合う2×2画素から各グループを抜き出し、平均化します。
for (int i = 0; i < separated.Length; i++)
result.Images[i] = CopyToImageBuffer(separated[i]); // 0~3:行・列の偶奇順、4:平均
}
finally
{
foreach (Mat image in separated)
image?.Dispose();
}
}
return result;
}
catch
{
result.Dispose();
throw;
}
}
}
ここでは、カメラから届いた1フレームをもとに、補正前のRAWと、Dark Frame補正を適用する作業用画像を分けて準備しています。Form1の受信処理からProcessが呼ばれると、まず入力がMono8で、幅と高さが偶数であることを確認します。 SetDarkFrameでは、受信画像をCreateOpenCvWrapでOpenCVのMatとして扱い、Cloneで独立したコピーを作ります。 減算後の画像から4つの個別露光画像と平均画像を生成し、RAWと合わせてExposureFrameSetで返します。Mono8の減算結果は負の値を保持できないため、0未満になる部分は0となります。
2×2画素から個別露光画像と単純平均を作る
Imx900HdrProcessor.cs
private static unsafe void SplitAndAverage(Mat source, Mat[] output)
{
// sourceは偶数幅・偶数高さのMono8画像であることを前提とします。
for (int y = 0; y < source.Rows / 2; y++)
{
byte* row0 = (byte*)source.Ptr(y * 2).ToPointer();
byte* row1 = (byte*)source.Ptr(y * 2 + 1).ToPointer();
byte* ee = (byte*)output[0].Ptr(y).ToPointer();
byte* eo = (byte*)output[1].Ptr(y).ToPointer();
byte* oe = (byte*)output[2].Ptr(y).ToPointer();
byte* oo = (byte*)output[3].Ptr(y).ToPointer();
byte* average = (byte*)output[4].Ptr(y).ToPointer();
// 2×2画素の偶奇位置に応じて4つのグループに分け、平均を計算します。
for (int x = 0; x < source.Cols / 2; x++)
{
ee[x] = row0[x * 2];
eo[x] = row0[x * 2 + 1];
oe[x] = row1[x * 2];
oo[x] = row1[x * 2 + 1];
// intで加算して255を超える中間値を保持し、4で割って四捨五入します。
// 露光時間による重み付けや自動正規化はしない単純平均です。
average[x] = (byte)((ee[x] + eo[x] + oe[x] + oo[x] + 2) / 4);
}
}
}
Quad HDRのRAW画像から4つの個別画像を作る処理は、画像を上下左右の4つのエリアに切り分けるわけではありません。画像全体から、行・列ともに「1画素飛ばし」でデータを抽出します。そのため、写っている範囲(画角)を保ったまま、画像の縦横のピクセル数(解像度)だけがそれぞれ半分になります。
実際のコードの SplitAndAverage メソッドでは、2行×2列(4画素)のブロックごとにループを回し、左上、右上、左下、右下の値をそれぞれee、eo、oe、ooへ格納します。添字の偶奇は0始まりの画像座標に対応します。
例えば、ある2×2画素の値が40、80、120、160なら、4枚の出力の同じ座標にはそれぞれの値が入り、平均画像には100が入ります。加算時は整数として計算するため、合計が255を超えても途中で8bitの範囲へ切り詰められません。
※コード内の平均化計算の「+2」について
実際のソースコードでは、平均画像の画素値を求める際に「 (4つの画素の合計 + 2) / 4」という計算をしています。この「+2」は明るさを加えるための補正ではなく、整数の割り算で小数点以下を「四捨五入」するためのテクニックです。そのまま4で割ると小数点以下が切り捨てられるため、先に2を加えています。なお、本サンプルでは露光時間の違いによる重み付け補正などは行わず、画素値の単純平均を計算しています。
画像セットを6つの表示領域へ反映する
SixPanelView.cs
internal void ShowFrames(ExposureFrameSet frames)
{
// 撮影サイズが変わった場合は、どれか1画面だけでなく全画面をリセットします。
for (int i = 0; i < _displays.Length; i++)
{
var type = frames.Images[i].ImageType; // フレームの幅・高さを取得します。
if (_viewports[i].ImageSize != new Size((int)type.Width, (int)type.Height))
{
ResetAllZoom(); // 撮影サイズが変わった場合は、全画面のズーム・位置をリセットします。
break;
}
}
// 6画面の表示を更新します。各画面のズーム・位置は共有されます。
for (int i = 0; i < _displays.Length; i++)
{
var type = frames.Images[i].ImageType; // フレームの幅・高さを取得します。
_viewports[i].SetImageSize(new Size((int)type.Width, (int)type.Height)); // 画像サイズを設定します。
_viewports[i].Resize(_displays[i].ClientSize); // Displayのクライアントサイズに合わせて表示位置を計算します。
_viewports[i].SetView(_sharedZoom, _sharedCenterX, _sharedCenterY); // 共有されたズーム・位置を設定します。
ApplyViewport(i); // Displayに表示位置を反映します。
_displays[i].DisplayBuffer(frames.Images[i]); // Displayに画像を表示します。
}
RefreshCursorPosition(); // マウスが画像の上にある場合は、カーソル表示を更新します。
}
ここでは、Form1から渡されたExposureFrameSetを、6つの表示部品へ順番に反映しています。画像処理クラスが画素データを作るのに対し、この関数はそのデータを画面のどこに、どの大きさで表示するかを担当します。最初のループでは各画像のサイズを以前の表示状態と比較し、どれかが変わっていれば全画面のズームをリセットします。古い画像サイズを前提とした表示位置を使い続けないための処理です。
続くループでは、各ZoomViewportへ画像サイズと表示枠のサイズを設定し、共有しているズーム倍率と中心位置を適用します。その計算結果をApplyViewportでIC4の表示位置へ反映してから、DisplayBufferで画像を表示します。
ズーム倍率とドラッグ位置を全画面で共有する
SixPanelView.cs
internal void ZoomAllAt(int sourceIndex, Point point, int wheelDelta)
{
ZoomViewport source = _viewports[sourceIndex]; // 操作元の画面のズーム倍率と中心位置を取得します。
if (source.Bounds.IsEmpty) return; // 画像がない場合はズーム操作を無視します。
source.ZoomAt(point, wheelDelta); // 操作元の画面でズームを実行します。
_sharedZoom = source.Zoom; // 共有されたズーム倍率と中心位置を保持します。
_sharedCenterX = source.CenterX; // 共有されたズーム倍率と中心位置を保持します。
_sharedCenterY = source.CenterY; // 共有されたズーム倍率と中心位置を保持します。
ApplySharedView(); // 共有されたズーム倍率と中心位置を他の5画面に適用します。
}
internal void PanAllBy(int sourceIndex, int deltaX, int deltaY)
{
if (deltaX == 0 && deltaY == 0) return; // ドラッグしていない場合は何もしません。
ZoomViewport source = _viewports[sourceIndex]; // 操作元の画面のズーム倍率と中心位置を取得します。
if (source.Bounds.IsEmpty || source.Zoom <= 1.0) return;
source.PanBy(deltaX, deltaY); // 操作元の画面でパンを実行します。
_sharedCenterX = source.CenterX; // 共有された中心位置を保持します。
_sharedCenterY = source.CenterY; // 共有された中心位置を保持します。
ApplySharedView(); // 共有されたズーム倍率と中心位置を他の5画面に適用します。
}
private void ApplySharedView()
{
for (int i = 0; i < _viewports.Length; i++)
{
_viewports[i].SetView(_sharedZoom, _sharedCenterX, _sharedCenterY); // 共有されたズーム倍率と中心位置を適用します。
ApplyViewport(i); // Displayに表示位置を反映します。
}
RefreshCursorPosition(); // マウスが画像の上にある場合は、カーソル表示を更新します。
}
ここでは、どれか1つの画面で行ったズームやドラッグを、残りの画面にも反映しています。ZoomAllAtは操作元のZoomViewportでマウス位置を基準に拡大し、結果の倍率と中心位置を_sharedZoom、_sharedCenterX、_sharedCenterYへ記録します。PanAllByはドラッグ量から操作元の中心位置を更新し、その結果を同じ共有変数へ渡します。どちらも最後にApplySharedViewを呼ぶため、共通の手順で6画面を更新できます。
カーソル位置を画像座標へ変換して全画面へ伝える
SixPanelView.cs
internal void UpdateCursorPosition(Point screenPoint)
{
// 6画面のうち、マウスが乗っている画面を探します。見出しや余白の上ではカーソル位置をクリアします。
for (int i = 0; i < _displays.Length; i++)
{
var display = _displays[i];
if (!display.IsHandleCreated || !display.Visible) continue;
Point local = display.PointToClient(screenPoint); // 画面座標をDisplayのクライアント座標に変換します。
Point pixel;
// Displayのクライアント座標を画像内の画素位置に変換します。見出しや余白の上ではカーソル位置をクリアします。
if (_viewports[i].TryGetImagePixel(local, out pixel))
{
Rectangle bounds = _viewports[i].Bounds;
// 画素単位で丸める前の位置を共有します。
SetCursorPosition(i, (local.X - (double)bounds.Left) / bounds.Width,
(local.Y - (double)bounds.Top) / bounds.Height);
return;
}
}
ClearCursorPosition(); // マウスが画像の外にある場合は、カーソル表示をクリアします。
}
private void SetCursorPosition(int index, double x, double y)
{
_hoverIndex = index;
_cursorX = x;
_cursorY = y;
for (int i = 0; i < _viewports.Length; i++)
{
if (_labels[i] == null) continue;
UpdateLabel(i); // 6画面の見出しラベルに、解像度・ズーム倍率・カーソル位置を表示します。
var display = _displays[i];
var view = _viewports[i];
if (index < 0 || view.Bounds.IsEmpty)
_cursorOverlays[i].Hide(); // マウスが画像の外にある場合は、カーソル表示をクリアします。
else
// 6画面のうち、マウスが乗っている画面のインデックスと、共有されたズーム倍率と中心位置を使って、カーソル表示を更新します。
_cursorOverlays[i].ShowAt(display, view.GetClientPoint(x, y), view.Bounds);
}
}
public Point GetImagePixel(double x, double y)
{
if (ImageSize.IsEmpty) return Point.Empty;
return new Point(Math.Max(0, Math.Min(ImageSize.Width - 1, (int)(x * ImageSize.Width))),
Math.Max(0, Math.Min(ImageSize.Height - 1, (int)(y * ImageSize.Height))));
}
public Point GetClientPoint(double x, double y)
{
return new Point(Bounds.Left + (int)Math.Round(x * Bounds.Width),
Bounds.Top + (int)Math.Round(y * Bounds.Height));
}
ここでは、マウスの画面上の位置を画像内の位置へ変換し、その位置を6画面の座標表示と十字マークへ伝えています。UpdateCursorPositionは6つのDisplayを調べ、PointToClientでマウス位置を各表示部品の座標に直します。そのうえでTryGetImagePixelを呼び、画像の上にあるかを判定します。見出しや黒い余白の上では有効な画像座標として扱わず、位置表示を解除しています。
6枚のBMPを1組として保存する
SixFrameExporter.cs
public static string Save(string parentDirectory, ExposureFrameSet frames)
{
if (frames == null) throw new ArgumentNullException(nameof(frames));
foreach (ImageBuffer image in frames.Images)
if (image == null) throw new InvalidOperationException("保存する6枚の画像がそろっていません。");
if (!Directory.Exists(parentDirectory))
throw new DirectoryNotFoundException("保存先フォルダーが見つかりません。");
// GUIDを付けて連続保存でも名前が重ならないようにし、既存の画像は上書きしません。
string folderName = "imx900_" + DateTime.Now.ToString("yyyyMMdd_HHmmss_fff") +
"_" + Guid.NewGuid().ToString("N");
string finalDirectory = Path.Combine(Path.GetFullPath(parentDirectory), folderName);
string temporaryDirectory = finalDirectory + ".saving";
Directory.CreateDirectory(temporaryDirectory);
try
{
// 表示用ズームではなく、同じフレームから生成済みの6枚の画素データを保存します。
// Mono8は8bitモノクロBMPになり、JPEGのような非可逆圧縮は行いません。
for (int i = 0; i < FileNames.Length; i++)
frames.Images[i].SaveAsBitmap(Path.Combine(temporaryDirectory, FileNames[i]));
// 全ファイルが完成した後でフォルダー名を確定し、途中の保存を完了扱いにしません。
Directory.Move(temporaryDirectory, finalDirectory);
return finalDirectory;
}
catch (Exception saveError)
{
try
{
// 今回生成した一時ファイルだけを消します。既存の保存フォルダーには触れません。
foreach (string name in FileNames)
{
string file = Path.Combine(temporaryDirectory, name);
if (File.Exists(file)) File.Delete(file);
}
Directory.Delete(temporaryDirectory);
}
catch (Exception cleanupError)
{
throw new IOException("6枚の保存に失敗しました。一時フォルダーが残っています: " +
temporaryDirectory + " (" + cleanupError.Message + ")", saveError);
}
throw;
}
}
ここでは、表示中の画像セットを6つのBMPファイルとして、1つの保存フォルダーへ書き出しています。Form1は保存先とExposureFrameSetを渡し、このクラスがSaveAsBitmapを使ってファイル出力を担当します。最初に6枚がそろっていることと親フォルダーの存在を確認し、不完全な画像セットのまま保存を開始しないようにしています。保存するのは各ImageBufferの画素データであり、ズーム表示や十字マークを含む画面のキャプチャではありません。
IC4の状態データをXMLへ格納して復元する
CameraSettingsFile.cs
public static void Save(string path, byte[] deviceState)
{
if (deviceState == null || deviceState.Length == 0)
throw new ArgumentException("カメラ設定データが空です。", nameof(deviceState));
var document = new XDocument(
new XElement("Imx900CameraSettings", new XAttribute("version", "1"),
new XElement("DeviceState", new XAttribute("encoding", "base64"),
Convert.ToBase64String(deviceState))));
// 一時ファイルが完成してから置き換え、書き込み失敗で既存の設定を壊さないようにします。
string fullPath = Path.GetFullPath(path);
string temporaryPath = fullPath + "." + Guid.NewGuid().ToString("N") + ".tmp";
try
{
document.Save(temporaryPath);
if (File.Exists(fullPath))
File.Replace(temporaryPath, fullPath, null);
else
File.Move(temporaryPath, fullPath);
}
finally
{
if (File.Exists(temporaryPath)) File.Delete(temporaryPath);
}
}
public static byte[] Load(string path)
{
// 外部参照を含むXMLを処理せず、読み込み前にファイル形式を検査します。
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null,
MaxCharactersInDocument = 16 * 1024 * 1024
};
using (XmlReader reader = XmlReader.Create(path, settings))
{
var document = XDocument.Load(reader);
XElement root = document.Root;
if (root == null || root.Name != "Imx900CameraSettings" ||
(string)root.Attribute("version") != "1")
throw new InvalidDataException("このアプリのSave XMLで保存した設定ファイルを選択してください。");
XElement state = root.Element("DeviceState");
if (state == null || (string)state.Attribute("encoding") != "base64")
throw new InvalidDataException("XMLにカメラ設定データがありません。");
byte[] result = Convert.FromBase64String(state.Value);
if (result.Length == 0)
throw new InvalidDataException("カメラ設定データが空です。");
return result;
}
}
ここでは、IC4が返すカメラ状態のバイト列をXMLに格納し、後から元のバイト列へ戻しています。Form1の保存ボタンの処理ではDeviceSaveStateで得たデータをSaveへ渡し、読み込みボタンの処理ではLoadの結果をLoadCameraStateへ渡します。このクラスはカメラへ接続したり露光値を書き込んだりせず、設定データとファイルの変換を行っています。バイト列はBase64という文字列形式へ変換し、DeviceState要素へ格納します。Base64はXMLで扱いやすくするためのものです。
画像保存先をXMLへ記録する
ImageSaveSettings.cs
public static string Load(string settingsPath)
{
if (!File.Exists(settingsPath)) return null;
using (var reader = XmlReader.Create(settingsPath, new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null,
MaxCharactersInDocument = 65536
}))
{
XElement root = XElement.Load(reader);
if (root.Name != "ImageSaveSettings")
throw new InvalidDataException("画像保存先の設定形式が正しくありません。");
string path = (string)root.Element("Directory");
return string.IsNullOrWhiteSpace(path) ? null : Path.GetFullPath(path);
}
}
public static void Save(string settingsPath, string directory)
{
string path = Path.GetFullPath(directory);
if (!System.IO.Directory.Exists(path))
throw new DirectoryNotFoundException("画像の保存先フォルダーが見つかりません。");
System.IO.Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(settingsPath)));
var xml = new XElement("ImageSaveSettings", new XElement("Directory", path));
string temporary = settingsPath + "." + Guid.NewGuid().ToString("N") + ".tmp";
try
{
xml.Save(temporary);
if (File.Exists(settingsPath)) File.Replace(temporary, settingsPath, null);
else File.Move(temporary, settingsPath);
}
finally { if (File.Exists(temporary)) File.Delete(temporary); }
}
ここでは、BMPの保存先フォルダーだけを、カメラ設定とは別のXMLファイルへ保存しています。Form1のChooseImageSaveDirectoryからSaveを呼び、起動時にはLoadで前回の場所を読み込みます。露光設定を読み込んだりカメラを変更したりしても、パソコン側の画像保存先は独立して管理するため、CameraSettingsFileとは別のクラスに分けています。このファイルに撮影画像やカメラの状態データは格納しません。


