魚眼レンズを使ったパノラマ表示
概要
このプログラムは、IC Imaging Controlで取得したカメラ画像をOpenCvSharpで処理し、魚眼カメラ画像をパノラマ風に展開して表示するWinFormsサンプルです。なお、下記のアルゴリズムをリストボックスで切り替えられるようにしています。
- DisplayMode1:魚眼画像を円周方向に展開してパノラマ表示する
- DisplayMode2:画像中央の魚眼領域を切り出し座標変換する
- DisplayMode3:前後左右の4方向画像を連結して一部領域を表示する
サンプルプログラム
| 利用した開発環境 | Visual Studio™ 2019 |
|---|---|
| SDK | IC Imaging Control 3.5, Visual Studio™ 2022 |
| デバイスドライバ | Cam33U_setup,gigecam_setup,usbcam,AFU420_setup,usb2pro_drv |
| デバイス | TISカメラ(USB or GigE)+M12 220レンズ |
| サンプル(C#) | WinForm_FishEye_3.5.zip |
| サンプル(VB.NET) | ー |
| exeファイル アプリケーション |
ー |
| 別途ファイル | ー |
| 関連参照URL | ー |
実行結果

クラスフィールドの定義
private FrameQueueSink _frameQueueSink;
private string filePath = "DefaultDevice.xml";
private int DisplayMode = 1;
private int intfirstimage = 0;
private int srcW = 1400;
private int srcH = 1400;
// マッピング用の変数
private FishEyeMapper mapper;
private Mat mapX_front, mapY_front;
private Mat mapX_right, mapY_right;
private Mat mapX_back, mapY_back;
private Mat mapX_left, mapY_left;
private static System.Drawing.Bitmap _image;
private Bitmap originalImage;
private bool _isClosing = false;
ここでは、アプリケーション全体で使用するグローバル変数を定義しています。
_frameQueueSinkは、カメラから送られてくるフレームを受け取るためのシンクです。ライブ表示だけでなく、取得した画像をリアルタイムに処理したい場合は、FrameQueueSinkのようなフレーム取得用の仕組みを使用します。filePathには、カメラ設定を保存・読み込みするXMLファイル名を指定しています。カメラの解像度、ピクセルフォーマット、露光などの設定を毎回手動で行うのは手間がかかるため、設定ファイルとして保存しておくことで、次回起動時の復元がしやすくなります。
このサンプルでは、下記のように選ぶアルゴリズムによって変わります。
| DisplayMode1: WarpPolarによるパノラマ展開 |
画像中央を基点として、Cv2.WarpPolarで魚眼映像の円周方向を直線状に極座標展開します。360度の映像を1枚のパノラマ画像として広く確認したい場合に有効です。 |
|---|---|
| DisplayMode2: FishEyeMapperによるRemap |
画像中央の魚眼領域(例: 1400×1400ピクセル)だけを切り出し、FishEyeMapperを使って変換します。事前に作成した座標マップ(LUT)を適用するCv2.Remapを使用するため、毎フレームの計算負荷を抑えられます。 |
| DisplayMode3: 前後左右のLUT処理を想定した表示 |
中心領域に対して、前後左右の4方向に対応する座標マップをそれぞれ適用(ApplyLUT)し、4枚の画像を生成します。その後、Cv2.HConcatで横一列に連結してパノラマ画像を作り出します。 |
srcWとsrcHは、魚眼画像として切り出す領域サイズです。たとえば、カメラ画像全体が1920×1200で、その中央に直径1400ピクセル程度の魚眼円が写っている場合、その中心部分だけを切り出して処理するために使用します。
アプリ起動時の保存先設定
private void Form1_Load(object sender, EventArgs e)
{
// ユーザーのドキュメントフォルダへのパスを取得
string documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
// 設定から読み込み
string savedPath = Properties.Settings.Default.txtFilePath;
// 未設定 または 存在しない場合 → ドキュメントフォルダにフォールバック
if (string.IsNullOrWhiteSpace(savedPath) || !Directory.Exists(savedPath))
{
// 設定にも保存しておく(次回起動を安定させる)
Properties.Settings.Default.txtFilePath = documentsPath;
Properties.Settings.Default.Save();
}
ここでは、アプリケーションで使用する保存先フォルダを確認しています。
Properties.Settings.Default.txtFilePathに保存済みのフォルダパスがある場合は、そのパスを使用します。
カメラ設定ファイルの読み込みとデバイス選択
try
{
if (File.Exists(filePath))
icImagingControl1.LoadDeviceStateFromFile(filePath, true);
}
catch (Exception ex)
{
if (!icImagingControl1.LoadShowSaveDeviceState(filePath))
{
MessageBox.Show("カメラデバイス1が見つかりませんでした。", this.Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
this.Close();
return;
}
}
if (!icImagingControl1.DeviceValid)
{
if (!icImagingControl1.LoadShowSaveDeviceState(filePath))
{
MessageBox.Show("デバイスが見つかりませんでした。", this.Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
this.Close();
return;
}
}
ここでは、前回保存したカメラ設定をXMLファイルから読み込んでいます。
LoadDeviceStateFromFileは、保存済みのデバイス状態を読み込むための処理です。カメラの種類、ビデオフォーマット、フレームレートなどを前回と同じ状態に復元できます。
FrameQueueSinkの設定
icImagingControl1.LiveDisplay = false;
_frameQueueSink = new FrameQueueSink(
Callback_FrameQueueSink,
MediaSubtypes.RGB32,
5
);
icImagingControl1.Sink = _frameQueueSink;
ここでは、カメラから取得したフレームを受け取るためにFrameQueueSinkを設定しています。
LiveDisplay = falseにしているのは、IC Imaging Control標準のライブ表示ではなく、取得した画像をOpenCvSharpで処理してからPictureBoxへ表示するためです。
FrameQueueSinkの第1引数には、フレーム受信時に呼び出されるコールバック関数Callback_FrameQueueSinkを指定します。第2引数のMediaSubtypes.RGB32は、カラー8bitのピクセルフォーマットです。OpenCvSharpのBitmap変換や画面表示を扱いやすくするため、このサンプルではRGB32としています。
第3引数の「5」はキューに保持するバッファ数です。処理待ちの画像をカメラとプログラムの間でPCメモリ上で最大5枚保持しています。プログラムでは、このメモリ上のバッファから画像を1枚ずつ取り出して処理を行います。
表示領域について
comboBoxMode.SelectedIndex = 1;
// ディスプレイサイズを変更してビデオをフルコントロールサイズにストレッチします
icImagingControl1.LiveDisplayDefault = false;
icImagingControl1.LiveDisplaySize = icImagingControl1.Size;
icImagingControl1.LiveDisplay = false;
panel1.Controls.Add(pictureBox1);
InitializeFishEyeMapper();
icImagingControl1.LiveStart();
ここでは、画面表示の初期設定と魚眼画像変換用の初期化を行っています。
LiveDisplayDefault = falseは、IC Imaging Control側の標準表示サイズに任せず、アプリケーション側で表示サイズを制御するための設定です。ただし、このサンプルでは最終的な表示はPictureBoxで行っているため、実際の表示更新は後述のUpdateImageAsyncで行われます。panel1.Controls.Add(pictureBox1)では、スクロール可能なパネル上にPictureBoxを追加しています。魚眼画像をパノラマ展開すると横長の画像になる場合があるため、パネル上に配置しておくことで、拡大表示やスクロール表示に対応しやすくなります。
InitializeFishEyeMapper()では、Remap処理で使用するパノラマかするためのテーブルを初期化しています。最後にLiveStart()を呼び出すことで、カメラからのライブ取得を開始します。
FishEyeMapperの初期化
private void InitializeFishEyeMapper()
{
try
{
mapper = new FishEyeMapper(srcW, srcH);
mapX_front = new Mat();
mapY_front = new Mat();
mapX_right = new Mat();
mapY_right = new Mat();
mapX_back = new Mat();
mapY_back = new Mat();
mapX_left = new Mat();
mapY_left = new Mat();
}
catch (Exception ex)
{
MessageBox.Show($"魚眼変換テーブルの初期化に失敗しました: {ex.Message}", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
ここでは、魚眼画像の座標変換に使用するFishEyeMapperと、方向別のマップ変数を初期化しています。FishEyeMapper(srcW, srcH)では、魚眼画像として扱う幅と高さを指定して変換テーブルを作成しています。このサンプルでは、srcWとsrcHがどちらも1400に設定されているため、中央の1400×1400ピクセル領域を魚眼画像として処理します。mapX_front、mapY_frontなどは、前後左右それぞれの方向に対応した座標変換マップを格納するための変数です。OpenCVのRemapでは、出力画像の各画素が入力画像のどの座標を参照するかを、mapXとmapYで指定します。
LUTを使ったRemap処理
private Mat ApplyLUT(Mat src, Mat mapX, Mat mapY)
{
Mat result = new Mat();
if (mapX != null && mapY != null && !mapX.Empty() && !mapY.Empty())
{
Cv2.Remap(src, result, mapX, mapY, InterpolationFlags.Linear);
}
else
{
result = src.Clone();
}
return result;
}
ここでは、OpenCVのRemapを使って画像の座標変換を行っています。Cv2.Remapは、入力画像srcに対して、mapXとmapYで定義された座標マップを適用し、変換後の画像をresultに出力します。魚眼補正、パノラマ展開、方向別の切り出しなどでは、あらかじめ作成した座標変換マップを使って高速に変換する方法がつかわれます。
フレーム受信コールバックの開始処理
private FrameQueuedResult Callback_FrameQueueSink(IFrameQueueBuffer buffer)
{
if (_isClosing)
{
// フォームが閉じられている場合は何もせずに戻る
return FrameQueuedResult.ReQueue;
}
_image = buffer.CreateBitmapWrap();
// BitmapをMatに変換
Mat srcMatBase = BitmapConverter.ToMat(_image);
Mat srcdisplay = new Mat();
srcdisplay = srcMatBase;
// 魚眼の中心
int cx = srcMatBase.Width / 2;
int cy = srcMatBase.Height / 2;
int maxRadius = Math.Min(cx, cy);
int outH = 2048;
int outW = 512;
ここでは、カメラから1フレーム受信したときの処理を行っています。受信した画像データをWindows標準の形式(Bitmap)として読み込みます。ただし、この後の工程でOpenCVによる高度な画像処理(WarpPolarやRemapなど)を行うため、データ形式をOpenCV専用のMatへ変換(ToMat)しておく必要があります。
続いて、魚眼レンズ特有の円形画像を正確に処理するための基準値を計算します。画像の縦横サイズから中心座標(cx, cy)を割り出し、中心から画像の端までの「短い方の距離」を最大半径(maxRadius)として設定します。これにより、処理の過程で画像の外側の無効なエリアを誤って参照してしまうのを防いでいます。
DisplayMode 1:WarpPolarによるパノラマ展開
if (DisplayMode == 1)
{
Mat pano = new Mat();
Cv2.WarpPolar(
srcMatBase,
pano,
new OpenCvSharp.Size(outW, outH),
new Point2f(cx, cy),
maxRadius,
InterpolationFlags.Cubic,
WarpPolarMode.Linear
);
// 回転(上下反転)
Cv2.Rotate(pano, srcdisplay, RotateFlags.Rotate90Clockwise);
}
ここでは円形に写った魚眼レンズの映像を、OpenCVのWarpPolar(極座標変換)を用いてパノラマ状の四角い画像に引き伸ばしています。変換の中心点(cx, cy)と最大半径(maxRadius)を基準に、画像を角度と半径の方向へ切り開きます。このとき、指定した中心位置が実際のレンズの光学中心からズレていると仕上がりが歪んでしまうため、実運用では正確な中心座標を測定して指定するようにしてください。円形から四角形へ引き伸ばす際、ピクセルの隙間を自然に埋めるためCubic補間という手法を指定しています。パソコンへの負荷はやや高くなりますが、画像を滑らかにきれいに表示できます。
DisplayMode 2:中央の魚眼領域を切り出してRemapする
else if (DisplayMode == 2)
{
// もしカメラ画像が 1920x1200 で、その中に直径 1400 の円がある場合は
// そこを切り出してから渡す
Rect roi = new Rect(
(srcMatBase.Cols - srcW) / 2,
(srcMatBase.Rows - srcH) / 2,
srcW,
srcH
);
Mat fisheyeCrop = new Mat(srcMatBase, roi);
// パノラマに変換(ここは Remap だけなので軽い)
if (mapper != null)
{
srcdisplay = mapper.Remap(fisheyeCrop);
Cv2.Rotate(srcdisplay, srcdisplay, RotateFlags.Rotate180);
}
else
{
srcdisplay = fisheyeCrop;
}
}
ここでは、カメラの映像全体から魚眼レンズが捉えた円形の部分だけを効率よく切り出し、あらかじめ用意した座標マップを使って画像を変換しています。まず、カメラの画像全体から魚眼の映像が写っている中央の範囲(指定した幅と高さの矩形)だけを計算対象(ROI)として指定します。無駄な余白部分の計算を省き、本当に必要な部分だけを抜き出して処理に渡すことで、パソコンへの負荷を減らしています。切り出された画像データは、専用の変換クラス(FishEyeMapper)に渡されます。ここで使用されるOpenCVのRemapという機能は、「事前に作成しておいた変換ルール(マップ)」に従ってピクセルを移動させる仕組みです。フレームごとに複雑な計算をやり直す必要がないため、非常に軽い動作で映像を変換できるのが大きな特徴です。
DisplayMode 3:4方向画像を作成して横方向に連結する
else if (DisplayMode == 3)
{
Rect roi = new Rect(
(srcMatBase.Cols - srcW) / 2,
(srcMatBase.Rows - srcH) / 2,
srcW,
srcH
);
Mat fisheyeCrop = new Mat(srcMatBase, roi);
Mat front = ApplyLUT(fisheyeCrop, mapX_front, mapY_front);
Mat right = ApplyLUT(fisheyeCrop, mapX_right, mapY_right);
Mat back = ApplyLUT(fisheyeCrop, mapX_back, mapY_back);
Mat left = ApplyLUT(fisheyeCrop, mapX_left, mapY_left);
Cv2.HConcat(new Mat[] { left, front, right, back }, srcdisplay);
Rect roi2 = new Rect(700, 0, 1200, srcdisplay.Height);
srcdisplay = new Mat(srcdisplay, roi2);
}
ここでは、円形の魚眼画像から「前後左右」の4つの視点を持つ映像を作り出し、横一列に並べて表示する処理です。360度カメラの映像から任意の方向を平面として見渡すような用途を想定しています。まず、前のモードと同様にカメラ映像の中央から不要な余白を省いて魚眼部分だけを切り出します。次に、その画像に対して前後左右それぞれの専用テーブルマップを適用し、各方向を向いた4枚の画像を生成します。これらをOpenCVの結合機能(HConcat)を使って「左・前・右・後」の順番で1枚の横長画像に繋ぎ合わせます。最後に、連結した画像が長すぎて画面に収まらない場合を考慮し、本当に表示したい指定の範囲だけをさらに切り出しています。実運用で正しい4方向の映像を得るには、カメラのレンズに合わせたキャリブレーション済みのLUTを事前に読み込ませておく必要があります。
処理画像をBitmapに戻してUIへ渡す
// 各部分をBitmapに変換
Bitmap quarter1Image = BitmapConverter.ToBitmap(srcdisplay);
originalImage = BitmapConverter.ToBitmap(srcdisplay);
// UIスレッドでの操作が安全であることを確認し、Invokeを使用
if (this.InvokeRequired)
{
try
{
this.BeginInvoke((MethodInvoker)delegate
{
if (!_isClosing && !this.IsDisposed)
{
UpdateImageAsync(sldZoom1.Value, quarter1Image, pictureBox1);
if (intfirstimage == 4)
{
Task.Delay(1000);
panel1.AutoScrollPosition = new System.Drawing.Point(
Properties.Settings.Default.Panel1ScrollX,
Properties.Settings.Default.Panel1ScrollY
);
}
}
});
}
catch (ObjectDisposedException)
{
// フォームが既に破棄されている場合は例外をキャッチして無視
}
catch (InvalidOperationException)
{
// フォームが既に閉じられている場合は例外をキャッチして無視
}
}
else
{
if (!_isClosing && !this.IsDisposed)
{
UpdateImageAsync(sldZoom1.Value, quarter1Image, pictureBox1);
}
}
intfirstimage++;
return FrameQueuedResult.ReQueue;
ここでは、OpenCVで処理した画像(Mat)をWindowsの画面に表示できる形式(Bitmap)に戻し、安全に画面を更新しています。カメラの画像受信は、画面を動かすメインの処理(UIスレッド)とは別の裏側の処理で実行されます。裏側から直接画面を書き換えるとエラーになってしまうため、InvokeRequiredで状態を確認し、BeginInvokeを使って正規のUIスレッドへ安全に画面の更新するようにしています。最後に、使い終わったデータの受け皿をReQueueでカメラ側に返却し、次の画像を受け取る準備をしています。
画像の拡大表示処理
private async void UpdateImageAsync(int scale, Bitmap bitmapImage, PictureBox pictureBoxCtrl)
{
if (bitmapImage == null || scale <= 0)
{
return;
}
await Task.Run(() =>
{
double zoomscale = scale * 0.1;
int newWidth = (int)(bitmapImage.Width * zoomscale);
int newHeight = (int)(bitmapImage.Height * zoomscale);
Bitmap resizedImage = new Bitmap(newWidth, newHeight);
using (Graphics g = Graphics.FromImage(resizedImage))
{
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
g.DrawImage(bitmapImage, 0, 0, newWidth, newHeight);
}
this.Invoke((Action)(() =>
{
pictureBoxCtrl.Image = resizedImage;
pictureBoxCtrl.Width = newWidth;
pictureBoxCtrl.Height = newHeight;
pictureBoxCtrl.Top = (panel1.Height - newHeight) / 2;
}));
});
}
ここでは、指定されたズーム倍率に合わせて画像をリサイズし、画面(PictureBox)に表示する処理です。倍率はトラックバーの値を10分の1にして計算します。大きな画像の変形処理を画面の描画処理(UIスレッド)と同時に行うと、アプリの動作が一時的にフリーズしてしまいますので、Task.Runを用いて別スレッドでリサイズ計算を行っています。
デバイス選択ボタンの処理
private void cmdDevice_Click(object sender, EventArgs e)
{
if (icImagingControl1.DeviceValid)
{
if (icImagingControl1.LiveVideoRunning)
{
icImagingControl1.LiveStop();
}
}
// xmlファイルを読み込む
if (!icImagingControl1.LoadShowSaveDeviceState(filePath))
{
MessageBox.Show("No device was selected.", this.Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
this.Close();
return;
}
if (icImagingControl1.DeviceValid)
{
sldZoom1.Enabled = true;
icImagingControl1.LiveDisplayDefault = false;
icImagingControl1.LiveDisplaySize = icImagingControl1.VideoFormatCurrent.Size;
}
}
ここでは、使用するカメラを途中で選び直すための処理をしています。映像を取得中のままデバイスを切り替えるとエラーや動作不良の原因になるため、すでにカメラが動作している場合は必ず事前にストリーミングを停止(LiveStop)させてから処理を進めます。その後、デバイス選択画面を表示してユーザーにカメラを選ばせ、その設定内容をXMLファイルに保存します。この保存処理によって、次回アプリを起動した際にも同じカメラ状態を自動的に復元できるようになります。
カメラプロパティダイアログの表示
private void cmdImageSettings_Click(object sender, EventArgs e)
{
icImagingControl1.ShowPropertyDialog();
icImagingControl1.SaveDeviceStateToFile(filePath);
}
このプログラムは、IC Imaging Control標準のカメラ設定画面(プロパティダイアログ)を表示する処理です。
ShowPropertyDialogを呼び出すことで、露光時間やゲイン、ホワイトバランスといったカメラ固有の詳細な設定を、画面上から直感的に調整できます。設定画面が閉じられた直後には、変更された内容をXMLファイルへ自動的に保存(SaveDeviceStateToFile)してます。
ライブ開始・停止処理
private void cmdStart_Click(object sender, EventArgs e)
{
if (icImagingControl1.DeviceValid)
{
icImagingControl1.LiveStart();
trackBarSwitch();
}
}
private void cmdStop_Click(object sender, EventArgs e)
{
icImagingControl1.LiveStop();
trackBarSwitch();
}
private void trackBarSwitch()
{
// ライブストリーミングの状態に応じてトラックバーを有効/無効にする
bool isLive = icImagingControl1.LiveVideoRunning;
sldZoom1.Enabled = isLive;
}
ここでは、カメラからのライブ映像の開始と停止をボタン操作で制御しています。開始時の処理(cmdStart_Click)では、カメラが正しく接続されているか(DeviceValid)を事前に確認してからストリーミングを開始し、未接続時にエラーが発生するのを防いでいます。また、開始・停止の操作に合わせて、ズーム操作用スライダーの有効・無効を自動で切り替える機能(trackBarSwitch)も同時に呼び出しています。
フォーム終了時の処理
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
icImagingControl1.LiveStop();
_isClosing = true; // フォームが閉じるフラグをセット
Properties.Settings.Default.sldZoom1 = sldZoom1.Value;
Properties.Settings.Default.Save();
}
アプリケーションを終了する際の処理です。まず、アプリを閉じる前にカメラの映像取得を明示的に停止(LiveStop)します。次に、終了処理中であることを示すフラグ(_isClosing = true)を立てます。これにより、アプリの終了中に裏側で新しい画像が届いても画面の更新処理がブロックされ、すでに破棄された画面パーツ(PictureBoxなど)へアクセスして強制終了する事故を未然に防ぎます。最後に、現在のズーム倍率をアプリの設定として保存(Save)し、次回起動時に前回と同じ拡大率を自動的に復元できるようにしています。
FishEyeMapperクラス
public class FishEyeMapper
{
private int width;
private int height;
private Mat mapX, mapY;
public FishEyeMapper(int w, int h)
{
width = w;
height = h;
InitializeMaps();
}
private void InitializeMaps()
{
// 基本的なマッピングの初期化
// 実際のキャリブレーションデータがある場合は、それを使用してください
mapX = new Mat(height, width, MatType.CV_32FC1);
mapY = new Mat(height, width, MatType.CV_32FC1);
// デフォルトのマッピング(恒等変換)
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
mapX.Set(y, x, (float)x);
mapY.Set(y, x, (float)y);
}
}
}
public Mat Remap(Mat src)
{
Mat dst = new Mat();
Cv2.Remap(src, dst, mapX, mapY, InterpolationFlags.Linear);
return dst;
}
}
OpenCVのRemap(座標変換)で使うための専用マップテーブルを作成・管理するクラスです。mapXとmapYは、変換後の画像の各ピクセルが、元の画像のどの座標(X, Y)を引っぱってくるかを指定しています。このサンプルコードでは、元の座標をそのまま設定しているため、映像が変形しないの状態になっていますが実際の魚眼補正では、ここにカメラの焦点距離やレンズの歪み係数などのキャリブレーション結果を計算して格納します。実際に映像を変換するのがRemapメソッドです。複雑な歪み補正の計算であっても、「最初にマップを1回だけ作成し、毎フレームの映像にはそのマップを適用するだけ」という構成にすることで、パソコンの負荷を大幅に抑えながらリアルタイムで滑らかな映像変換が可能になります。


