ズーム・フォーカス・アイリスの自動切替
概要
このプログラムは、電動レンズ(フォーカス・ズーム・アイリス)を搭載したカメラのライブ映像を表示しながら、あらかじめ登録した3組のレンズ設定を、タイマーで一定間隔ごとに自動で切り替えるサンプルです。
サンプルプログラム
| Software | IC Imaging Control 4.0, Visual Studio™ 2022 |
|---|---|
| サンプル(C#) | WinForm_Zoom_cs_4.0.zip |
| 備考 | 電動ズーム/フォーカス/アイリス対応レンズを搭載したカメラ |
実行結果
IC4オブジェクトと状態管理の定義
// ===== IC4 objects =====
private readonly ic4.Grabber _grabber = new ic4.Grabber();
private ic4.QueueSink _sink;
// ===== Timer =====
private System.Timers.Timer _presetSwitchTimer;
private int _currentPresetIndex = 1; // 1..3
// ===== Capture size used for Display zoom/pan =====
private int _captureWidth = 640;
private int _captureHeight = 480;
// ===== Display view state =====
private readonly Dictionary<ic4.WinForms.Display, ViewState> _displayStates
= new Dictionary<ic4.WinForms.Display, ViewState>();
private const double ZoomStep = 1.05;
private const double ZoomMin = 0.2;
private const double ZoomMax = 10.0;
private class ViewState
{
public double Zoom = 1.0;
public bool Panning = false;
public Point DragStart;
public Point RenderStart;
}
// ===== Exposure slider scale =====
private const double ExposureMin = 2;
private const double ExposureMax = 30000000;
private const double ExposureTrackMax = 1000.0;
ここでは、他の関数などで使う変数等をまとめて定義しています。_grabberはカメラの制御をするためのオブジェクトで、_sinkは映像フレームを受け取る受け皿です。これらは複数のメソッドから繰り返し使うため、グローバル変数として関数の外に置いています。
_presetSwitchTimer はプリセットを自動で切り替えるためのタイマー、_currentPresetIndex は「今どのプリセットを適用しているか(1〜3)」を保存するための変数です。_captureWidthと_captureHeightはカメラから取得している画像の幅と高さで、後述する表示ズームの計算に使います。
_displayStates と ViewState クラスは、表示ズーム・パンの状態(現在の倍率、ドラッグ中かどうか、ドラッグ開始位置など)を保持するために用意しておきます。ZoomStep(1回の拡大率)・ZoomMin / ZoomMax(倍率の下限・上限)を定数として持つことで、ズームしすぎ・縮小しすぎを防ぎます。
アプリ起動(カメラを開く → ライブ表示 → プリセット適用 → タイマー準備)
private void Form1_Load(object sender, EventArgs e)
{
if (!SelectAndOpenDevice())
{
Close();
return;
}
ApplyInitialCameraSettings();
LoadUiSettingsFromAppSettings();
SaveDeviceState();
SetupDisplayInteraction(display1);
SetupSinkAndStartStreaming();
// 起動時に一度、現在のプリセットを適用
ApplyPreset(_currentPresetIndex);
SetupTimer((int)numericUpDownInterval.Value);
// 自動開始はしない(ボタンでStart)
}
ここでは、アプリとカメラを動作させるのに必要な準備をしています。まずSelectAndOpenDevice()でカメラ選択ダイアログを表示し、カメラが選ばれなければCloseでアプリを終了します。カメラをオープンしたら、ApplyInitialCameraSettingsでオート機能をオフにするなどの初期設定を行い、LoadUiSettingsFromAppSettingsで前回保存したGUIの値を復元します。SaveDeviceStateは現在のカメラ設定をdevice.xmlに保存しておきます。続いてSetupDisplayInteraction(display1)でマウス操作(ズーム/パン)のイベントを登録し、SetupSinkAndStartStreamingでライブ表示を開始します。
最後に、起動直後に一度ApplyPreset(_currentPresetIndex)を呼んで現在のプリセットをカメラに反映し、SetupTimer でタイマーを準備します。なお、ここではタイマーを準備するだけで自動開始はしません。プリセットの自動切替は、ユーザーが「Timer Start」ボタンを押したときに始まるようにしています。
カメラを開いてライブ表示を開始する
private bool SelectAndOpenDevice()
{
ic4.WinForms.Dialogs.ShowDeviceDialog(_grabber, this);
return _grabber.IsDeviceValid;
}
private void SetupSinkAndStartStreaming()
{
_sink = new ic4.QueueSink(ic4.PixelFormat.BGR8);
_sink.FramesQueued += Sink_FramesQueued;
// Loadでライブ開始
_grabber.StreamSetup(_sink, display1);
UpdateCaptureSizeFromDevice();
}
private void UpdateCaptureSizeFromDevice()
{
var pm = _grabber.DevicePropertyMap;
_captureWidth = (int)pm.GetValueDouble(ic4.PropId.Width);
_captureHeight = (int)pm.GetValueDouble(ic4.PropId.Height);
}
ここでは、カメラを開いて映像を流し始める部分を準備しています。ShowDeviceDialogはIC Imaging Control4.0が用意しているデバイス選択ダイアログで、接続されているカメラの一覧から使用するカメラを選べます。
SetupSinkAndStartStreaming では、まずQueueSinkをBGR8(カラー8bit)形式で作成します。QueueSinkは受信したフレームをキューに溜めるタイプのシンクで、FramesQueuedイベントを使えば全フレームに対して画像処理を行えます。今回は画像処理を行わない構成ですが、後から処理を追加できる土台として用意しています。_grabber.StreamSetup(_sink, display1)を呼ぶと、カメラの映像がdisplay1コントロールに表示されると同時に、シンクにも送られてライブ表示が始まります。
UpdateCaptureSizeFromDeviceは、カメラのWidth・Heightプロパティから実際の画像サイズを読み取り、_captureWidthと_captureHeight に保存します。
カメラの初期設定(オート機能をオフにする)
private void ApplyInitialCameraSettings()
{
var pm = _grabber.DevicePropertyMap;
// オートOFF
SetManualMode(pm);
// WBは機種によって無いので try/catch
try
{
pm.SetValue(ic4.PropId.BalanceWhiteAuto, "Off");
pm.SetValue(ic4.PropId.BalanceRatioSelector, "Red");
pm.SetValue(ic4.PropId.BalanceRatio, 1.8);
pm.SetValue(ic4.PropId.BalanceRatioSelector, "Blue");
pm.SetValue(ic4.PropId.BalanceRatio, 2.46);
pm.SetValue(ic4.PropId.BalanceRatioSelector, "Green");
pm.SetValue(ic4.PropId.BalanceRatio, 1.0);
}
catch { }
UpdateCaptureSizeFromDevice();
}
private static void SetManualMode(ic4.PropertyMap pm)
{
pm.SetValue(ic4.PropId.ExposureAuto, "Off");
pm.SetValue(ic4.PropId.GainAuto, "Off");
pm.SetValue(ic4.PropId.IrisAuto, false);
}
ここでは、カメラの自動制御機能(オート露光・オートゲイン・オートアイリス・オートホワイトバランス)をオフにしています。ホワイトバランスについては、BalanceRatioSelectorで「赤・青・緑」を1つずつ選びながら、それぞれの倍率(BalanceRatio)を設定しています。ここでは一例として「赤=1.8、青=2.46、緑=1.0」という固定値でホワイトバランスを調整しています。
タイマーによるプリセットの自動切替
private void SetupTimer(int intervalMs)
{
StopTimer();
_presetSwitchTimer = new System.Timers.Timer(intervalMs);
_presetSwitchTimer.AutoReset = true;
_presetSwitchTimer.Elapsed += PresetSwitchTimer_Elapsed;
}
private void PresetSwitchTimer_Elapsed(object sender, ElapsedEventArgs e)
{
// UIスレッドへ
if (IsDisposed) return;
try
{
BeginInvoke((MethodInvoker)delegate
{
SwitchPreset();
});
}
catch
{
// フォーム終了時のレースを避けるため握りつぶし
}
}
private void SwitchPreset()
{
// 1→2→3→1
ApplyPreset(_currentPresetIndex);
_currentPresetIndex++;
if (_currentPresetIndex > 3) _currentPresetIndex = 1;
}
ここでは、一定間隔ごとにプリセットを自動で切り替える仕組みを作っています。SetupTimerでは、まず StopTimerで既存のタイマーを止めてから、新しいSystem.Timers.Timerを指定ミリ秒(画面の Interval(ms) の値)で作成します。SwitchPresetでは、現在のプリセットを適用したうえでインデックスを1つ進め、3を超えたら1に戻します。これにより、プリセットが 1 → 2 → 3 → 1 → 2 → 3 … と循環して切り替わります。「Timer Start」ボタンでこのタイマーを開始し、「Timer Stop」ボタンで停止できます。
マウスホイールでライブ映像を拡大・縮小(表示ズーム)
private void Display_MouseWheel(object sender, MouseEventArgs e)
{
var d = display1; // 現状仕様:display1固定
if ((ModifierKeys & Keys.Control) == 0) return;
var s = _displayStates[d];
s.Panning = false;
double factor = (e.Delta > 0) ? ZoomStep : (1.0 / ZoomStep);
double newZoom = Clamp(s.Zoom * factor, ZoomMin, ZoomMax);
// 以前のRender矩形
int oldW = d.RenderWidth;
int oldH = d.RenderHeight;
int oldL = d.RenderLeft;
int oldT = d.RenderTop;
// new size
s.Zoom = newZoom;
int newW = (int)(_captureWidth * s.Zoom);
int newH = (int)(_captureHeight * s.Zoom);
// マウス位置を中心に見せる
if (oldW <= 0) oldW = _captureWidth;
if (oldH <= 0) oldH = _captureHeight;
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;
}
ここでは、Ctrlキーを押しながらマウスホイールを回したときに、ライブ映像を画面上で拡大・縮小する処理を行っています。これはレンズを動かす光学ズームではなく、受信済みの映像を表示上で拡大するデジタルの表示ズームです。ホイールを上に回す(e.Delta > 0)と拡大、下に回すと縮小になるよう、倍率factorを切り替えます。新しい倍率はClampで ZoomMin~ZoomMax(0.2~10倍)の範囲に収め、拡大・縮小しすぎを防ぎます。表示サイズは、カメラの画像サイズに倍率を掛けて求めています。
マウスドラッグで表示位置を移動
private void Display_MouseDown(object sender, MouseEventArgs e)
{
var d = display1;
if (e.Button != MouseButtons.Left) return;
var s = _displayStates[d];
s.Panning = true;
s.DragStart = e.Location;
s.RenderStart = new Point(d.RenderLeft, d.RenderTop);
}
private void Display_MouseMove(object sender, MouseEventArgs e)
{
var d = display1;
var s = _displayStates[d];
if (!s.Panning) return;
int dx = e.X - s.DragStart.X;
int dy = e.Y - s.DragStart.Y;
d.RenderLeft = s.RenderStart.X + dx;
d.RenderTop = s.RenderStart.Y + dy;
d.RenderPosition = DisplayRenderPosition.Custom;
}
private void Display_MouseUp(object sender, MouseEventArgs e)
{
var d = display1;
_displayStates[d].Panning = false;
}
このプログラムは、拡大表示したライブ映像をマウスのドラッグ操作で上下左右に動かし、見たい部分へ移動させる「パン」機能を実装しています。まずDisplay_MouseDownでは、マウスの左ボタンが押された際にドラッグ開始の合図としてフラグ(Panning)を有効にします。このとき、クリックした瞬間のマウス座標と、その時点での映像の描画位置を記憶しておきます。
続いてDisplay_MouseMoveは、マウスが動くたびに連続して呼び出されます。ここではドラッグ操作中である場合のみ、現在のマウス位置と記憶しておいた開始位置との差分を計算し、マウスがどれだけ動いたかを割り出します。その移動量をもとの映像描画位置に足し合わせて座標を更新することで、マウスの動きにぴったり追従して映像がスムーズに移動する仕組みです。このとき、描画モードをカスタム(Custom)に設定し、プログラム側で指定した座標が正しく画面に反映されるようにしています。
露光値とスライダーの対数変換
// 露光値 → スライダー位置
public int ConvertFinalValueToTrackValue(double finalValue, double minValue, double maxValue, double trackBarMax)
{
double clampedValue = Math.Max(minValue, Math.Min(maxValue, finalValue));
double logMin = Math.Log10(minValue);
double logMax = Math.Log10(maxValue);
double logRange = logMax - logMin;
if (logRange == 0) return 0;
double logValue = Math.Log10(clampedValue);
double logRatio = (logValue - logMin) / logRange;
double trackValue = logRatio * trackBarMax;
return (int)Math.Round(trackValue);
}
// スライダー位置 → 露光値
private long ConvertTrackValueToFinalExposure(int trackValue, double minValue, double maxValue, double trackMax)
{
double logMin = Math.Log10(minValue);
double logMax = Math.Log10(maxValue);
double logValue = logMin + (logMax - logMin) * (trackValue / trackMax);
double finalValue = Math.Pow(10, logValue);
return (long)Math.Round(finalValue);
}
このプログラムは、露光時間とスライダー位置を相互変換する処理です。露光時間の設定範囲(2~30,000,000マイクロ秒)は6桁以上の差があるため、単純な比例計算では短い露光時間の領域がスライダー上で潰れ、微調整が不可能になります。スライダー位置への変換には対数を、露光値への復元には指数を使用することで、極端に短い露光から長い露光まで、全域で直感的な操作を実現しています。
設定の保存と終了処理
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
SaveUiSettingsToAppSettings();
StopTimer();
try
{
if (_grabber.IsDeviceValid && _grabber.IsStreaming)
_grabber.AcquisitionStop();
}
catch { }
try { _grabber.DeviceClose(); } catch { }
}
ここでは、アプリ終了時の後始末をまとめて行っています。SaveUiSettingsToAppSettingsで、3つのプリセットに入力した値やタイマー間隔をアプリ設定に保存し、次回起動時にLoadUiSettingsFromAppSettingsで復元できるようにしています。次にStopTimer でプリセット自動切替のタイマーを止め、ライブ表示中であればAcquisitionStopで映像の取得を停止し、最後にDeviceCloseでカメラを閉じます。


