💻

Webカメラ2台を使用した簡易3D映像プログラム(C#)

2023/04/27に公開

概要

令和3年度に実施した研究にて使用したC#のプログラムです。

PCに接続した2台のWebカメラからキャプチャした画像を、左右に結合してアプリケーション上に描画するプログラムです。

2台のWebカメラ
3D映像の例

当該研究では、Web会議システムを使用して、このアプリケーションを画面共有することで、簡易的に3D映像を遠方に送信しました。

また、受信側は、スマートホンとそれが取付可能な安価なVRゴーグルを使用して映像を閲覧しました。

スマートホンとVRゴーグル
構成イメージ

プログラム

作成したプロジェクト

今回のプログラム開発では、Visual Studio 2019 を使用して、以下のプロジェクトを作成しました。

  • 言語:C#
  • テンプレート:Windowsフォームアプリケーション
  • フレームワーク:.NET Framework 4.7.2
  • プロジェクト名:StereoCam

追加パッケージ

今回作成したプログラムは、NuGetにより以下のパッケージをインストールしました。

  • OpenCvSharp4.Windows
  • OpenCvSharp4.Extensions

大まかな流れ

カメラが2台接続されていた場合、BackgroundWorkerコンポーネントを使用して、以下を繰り返し実行します。

  1. 2台のカメラからキャプチャ
  2. 2つのキャプチャ画像を1つの表示用フレームに結合
  3. 表示用フレームをビットマップに変換してPictureBoxへ表示

Form1クラス

メンバ変数

OpenCV関連のメンバ変数は、3つあります。

  • 1つは、2画面を結合して表示するための表示用フレーム(Mat型)
  • 残り2つは、カメラ2台分のVideoCapture型変数
//表示用フレーム(左右2画面分)
private Mat frame_2pane;
//キャプチャ用
private VideoCapture cap0, cap1;

また、これ以外に主となるメンバ変数として、設定値管理用の変数があります。AppSettings型については、後ほど述べます。

//設定
private AppSettings mySettings;

キャプチャ関連のメンバ関数

表示用フレーム作成

createFrame() メソッドで、表示用フレームを作成します。
すでに作成済みの場合、一旦削除してから作成します。

作成時には、キャプチャする幅と高さに合わせたサイズで作成します。
キャプチャする幅と高さは、設定管理用変数mySettingsに保存されている値を使用します。

//表示用フレーム作成
private void createFrame()
{
    //キャプチャする幅と高さ
    int ww = this.mySettings.ResolutionWidth;
    int hh = this.mySettings.ResolutionHeight;

    //表示用フレームを一旦削除
    if (frame_2pane != null)
    {
        this.stopBgWorker(true);
        frame_2pane.Release();
        frame_2pane.Dispose();
    }

    //表示用フレームのMat作成(横幅は2画面分)
    frame_2pane = new Mat(hh, ww * 2, MatType.CV_8UC3);

    //PictureBoxを出力サイズに合わせる
    pictureBox1.Width = frame_2pane.Cols;
    pictureBox1.Height = frame_2pane.Rows;
}

カメラのオープン

openCamAll() メソッドと openCam() メソッドでカメラを開きます。

openCam() メソッドは、引数で指定されたIDのカメラを開きます。

openCamAll() メソッドは、設定として保存された2つのカメラIDに対して、openCam()メソッドを呼び出し、2台のカメラのオープンを試します。
2台の内1台でも開くことができたら、BackgroundWorkerコンポーネントにて、バックグラウンドスレッドを実行します。

//2つのカメラを開く
private void openCamAll()
{
    //ワーカースレッドを強制停止
    this.stopBgWorker(true);

    //カメラ開く
    bool check0 = this.openCam(ref this.cap0, this.mySettings.CamId0);
    bool check1 = this.openCam(ref this.cap1, this.mySettings.CamId1);

    //cam0 or cam1が開けた? → ワーカースレッド実行
    if (check0 || check1) this.backgroundWorker1.RunWorkerAsync();
}

//カメラを開く
//戻り値:true...成功、false...失敗
private bool openCam(ref VideoCapture cap, int cam_id = 0)
{
    //カメラ画像取得用のVideoCapture作成
    cap = new VideoCapture(cam_id);
    //オープン失敗?
    if (!this.capIsOpened(ref cap))
    {
        return false;
    }
    //幅と高さを設定
    cap.FrameWidth = this.mySettings.ResolutionWidth;
    cap.FrameHeight = this.mySettings.ResolutionHeight;

    return true;
}

カメラからのキャプチャと2つの画像の結合

BackgroundWorkerコンポーネントにて、バックグラウンドで動作するスレッドは backgroundWorker1_DoWork() メソッドとなります。

このメソッドでは、

  • 2台のカメラからキャプチャ
  • 表示用フレームへコピー

を、バックグラウンドスレッドの停止が指示されるまで繰り返し実行します。

//ワーカースレッド
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    BackgroundWorker bw = (BackgroundWorker)sender;

    //cam0,cam1のX座標の原点位置(コピー先の)
    int xx0, xx1;
    if (this.mySettings.IsCrossCam)
    {
        xx0 = this.mySettings.ResolutionWidth;
        xx1 = 0;
    }
    else
    {
        xx0 = 0;
        xx1 = this.mySettings.ResolutionWidth;
    }

    //停止待ちでないなら繰り返し
    while (!backgroundWorker1.CancellationPending)
    {
        //cam0 キャプチャしてコピー
        this.captureAndCopy(ref cap0, xx0);
        //cam1 キャプチャしてコピー
        this.captureAndCopy(ref cap1, xx1);

        //表示の実行
        bw.ReportProgress(0);
    }
}

//キャプチャして表示用フレームにコピー
private void captureAndCopy(ref VideoCapture cap, int copy_to_x)
{
    //開いている?
    if (this.capIsOpened(ref cap))
    {
        //キャプチャ用
        Mat frame = new Mat(this.mySettings.ResolutionHeight, this.mySettings.ResolutionWidth, MatType.CV_8UC3);

        //キャプチャ(画像取得)
        cap.Read(frame);

        //コピー先となるframe_2pane部分領域
        Mat roi_dst;
        roi_dst = new Mat(frame_2pane, new Rect(copy_to_x, 0, frame.Cols, frame.Rows));
        //キャプチャした画像をコピー先領域へコピー
        frame.CopyTo(roi_dst);
    }
}

OpenCVのフレームをPictureBoxへ

表示用フレームをPictureBoxへ表示するには、OpenCvSharp.Extensions.BitmapConverter.ToBitmap() メソッドを利用しています。

//キャプチャした2画面分をPictureBoxに表示
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    //描画
    if (backgroundWorker1.IsBusy && !backgroundWorker1.CancellationPending)
        pictureBox1.Image = OpenCvSharp.Extensions.BitmapConverter.ToBitmap(frame_2pane);
}

AppSettingsクラス

AppSettingsクラスは、設定値管理用クラスです。設定値をpublicメンバ変数やプロパティとして定義しています。

主なメンバを以下に示します。

メンバ 用途
int ResolutionWidth キャプチャする横幅
int ResolutionHeight キャプチャする高さ
int CamId0 1台目のカメラID
int CamId1 2台目のカメラID
bool IsCrossCam カメラを左右入れ替えるかどうか

このクラスをシリアライズすることでXML形式での保存も可能ですが、今回は、保存の処理は省略しています。

実際のコード

Form1.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using OpenCvSharp;
using Gitc;

namespace StereoCam
{
	public partial class Form1 : Form
	{
		//表示用フレーム(左右2画面分)
		Mat frame_2pane;
		//キャプチャ用
		VideoCapture cap0, cap1;

		/* ------------------------------ */

		//設定
		private AppSettings mySettings;
		//アプリケーションの情報
		private string appNameDisp;
		private string appVerStr;

		/* ============================================================ */

		public Form1()
		{
			InitializeComponent();
		}

		private void Form1_Load(object sender, EventArgs e)
		{
			//コマンドライン引数
			String[] args = Environment.GetCommandLineArgs();

			//アプリケーション名
			this.appNameDisp = System.IO.Path.GetFileNameWithoutExtension(args[0]);

			//バージョン
			System.Diagnostics.FileVersionInfo verInfo = System.Diagnostics.FileVersionInfo.GetVersionInfo(
					System.Reflection.Assembly.GetExecutingAssembly().Location
				);
			if (verInfo == null) this.appVerStr = "0.0";
			else this.appVerStr = verInfo.FileMajorPart.ToString()
					+ "." + verInfo.FileMinorPart.ToString()
					+ "." + verInfo.FileBuildPart.ToString()
					;
			//キャプション
			this.Text = this.appNameDisp + " ver." + this.appVerStr;

			//------------------------------
			//設定

			//設定
			this.mySettings = new AppSettings();
			//最小サイズ
			this.MinimumSize = this.Size;

			//------------------------------

			//表示用フレーム作成
			this.createFrame();
			//2つのカメラを開く
			this.openCamAll();
			//コントロール有効無効
			this.ctrlEnable();
		}

		private void Form1_FormClosing(object sender, FormClosingEventArgs e)
		{
			//バックグラウンドワーカーを強制停止
			this.stopBgWorker(true);
		}

		//コントロール有効無効
		private void ctrlEnable()
		{
			//ステータスバーにCAM0/1の状態表示
			string lr0 = this.mySettings.IsCrossCam ? "R" : "L";
			string lr1 = this.mySettings.IsCrossCam ? "L" : "R";
			string stat0 = "CAM0(" + lr0 + "):" + (this.capIsOpened(ref this.cap0) ? "OK" : "ng");
			string stat1 = "CAM1(" + lr1 + "):" + (this.capIsOpened(ref this.cap1) ? "OK" : "ng");
			this.slblCam0.Text = stat0;
			this.slblCam1.Text = stat1;

			//メニュー項目のチェックマーク
			this.cnmCrossCam.Checked = this.mySettings.IsCrossCam;
		}

		//表示用フレーム作成
		private void createFrame()
		{
			//キャプチャする幅と高さ
			int ww = this.mySettings.ResolutionWidth;
			int hh = this.mySettings.ResolutionHeight;

			//表示用フレームを一旦削除
			if (frame_2pane != null)
			{
				this.stopBgWorker(true);
				frame_2pane.Release();
				frame_2pane.Dispose();
			}

			//表示用フレームのMat作成(横幅は2画面分)
			frame_2pane = new Mat(hh, ww * 2, MatType.CV_8UC3);

			//PictureBoxを出力サイズに合わせる
			pictureBox1.Width = frame_2pane.Cols;
			pictureBox1.Height = frame_2pane.Rows;
		}

		//[設定]メニュー
		private void cnmSettings_Click(object sender, EventArgs e)
		{
			int res_id = this.mySettings.ResolutionId;

			//設定ダイアログ
			FormSettings frm = new FormSettings(this.mySettings);
			if (frm.ShowDialog() == DialogResult.OK)
			{
				//解像度変更あり?
				if (res_id != this.mySettings.ResolutionId)
				{
					this.createFrame();
				}
				//カメラ開く
				this.openCamAll();
				//コントロール有効無効
				this.ctrlEnable();
			}
		}

		//[カメラを入れ替える]メニュー
		private void cnmCrossCam_Click(object sender, EventArgs e)
		{
			//フラグ反転
			this.mySettings.IsCrossCam = !this.mySettings.IsCrossCam;
			//カメラ開く
			this.openCamAll();
			//コントロール有効無効
			this.ctrlEnable();
		}

		//2つのカメラを開く
		private void openCamAll()
		{
			//ワーカースレッドを強制停止
			this.stopBgWorker(true);

			//カメラ開く
			bool check0 = this.openCam(ref this.cap0, this.mySettings.CamId0);
			bool check1 = this.openCam(ref this.cap1, this.mySettings.CamId1);

			//cam0 or cam1が開けた? → ワーカースレッド実行
			if (check0 || check1) this.backgroundWorker1.RunWorkerAsync();
		}

		//カメラを開く
		//戻り値:true...成功、false...失敗
		private bool openCam(ref VideoCapture cap, int cam_id = 0)
		{
			//カメラ画像取得用のVideoCapture作成
			cap = new VideoCapture(cam_id);
			//オープン失敗?
			if (!this.capIsOpened(ref cap))
			{
				return false;
			}
			//幅と高さを設定
			cap.FrameWidth = this.mySettings.ResolutionWidth;
			cap.FrameHeight = this.mySettings.ResolutionHeight;

			return true;
		}

		//カメラが開かれているかどうか
		private bool capIsOpened(ref VideoCapture cap)
		{
			return cap != null && !cap.IsDisposed && cap.IsOpened();
		}

		//カメラを閉じる
		private void closeCam(ref VideoCapture cap)
		{
			//開いている? → 閉じる
			if (this.capIsOpened(ref cap))
			{
				cap.Release();
				cap.Dispose();
			}
		}

		//ワーカースレッド停止(強制 or すべてのカメラが閉じている場合)
		private void stopBgWorker(bool force_close = false)
		{
			//強制クローズ?
			if (force_close)
			{
				this.closeCam(ref cap0);
				this.closeCam(ref cap1);
			}
			else
			{
				//どちらかオープンしてる?
				if (this.capIsOpened(ref cap0) || this.capIsOpened(ref cap1)) return;
			}

			//スレッドの終了を待機
			backgroundWorker1.CancelAsync();

			while (backgroundWorker1.IsBusy)
			{
				Application.DoEvents();
			}
		}

		//ワーカースレッド
		private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
		{
			BackgroundWorker bw = (BackgroundWorker)sender;

			//cam0,cam1のX座標の原点位置(コピー先の)
			int xx0, xx1;
			if (this.mySettings.IsCrossCam)
			{
				xx0 = this.mySettings.ResolutionWidth;
				xx1 = 0;
			}
			else
			{
				xx0 = 0;
				xx1 = this.mySettings.ResolutionWidth;
			}

			//停止待ちでないなら繰り返し
			while (!backgroundWorker1.CancellationPending)
			{
				//cam0 キャプチャしてコピー
				this.captureAndCopy(ref cap0, xx0);
				//cam1 キャプチャしてコピー
				this.captureAndCopy(ref cap1, xx1);

				//表示の実行
				bw.ReportProgress(0);
			}
		}

		//キャプチャして表示用フレームにコピー
		private void captureAndCopy(ref VideoCapture cap, int copy_to_x)
		{
			//開いている?
			if (this.capIsOpened(ref cap))
			{
				//キャプチャ用
				Mat frame = new Mat(this.mySettings.ResolutionHeight, this.mySettings.ResolutionWidth, MatType.CV_8UC3);

				//キャプチャ(画像取得)
				cap.Read(frame);

				//コピー先となるframe_2pane部分領域
				Mat roi_dst;
				roi_dst = new Mat(frame_2pane, new Rect(copy_to_x, 0, frame.Cols, frame.Rows));
				//キャプチャした画像をコピー先領域へコピー
				frame.CopyTo(roi_dst);
			}
		}

		//キャプチャした2画面分をPictureBoxに表示
		private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
		{
			//描画
			if (backgroundWorker1.IsBusy && !backgroundWorker1.CancellationPending)
				pictureBox1.Image = OpenCvSharp.Extensions.BitmapConverter.ToBitmap(frame_2pane);
		}

		/* ============================================================ */


	}
}

Form1.Designer.cs

namespace StereoCam
{
	partial class Form1
	{
		/// <summary>
		/// 必要なデザイナー変数です。
		/// </summary>
		private System.ComponentModel.IContainer components = null;

		/// <summary>
		/// 使用中のリソースをすべてクリーンアップします。
		/// </summary>
		/// <param name="disposing">マネージド リソースを破棄する場合は true を指定し、その他の場合は false を指定します。</param>
		protected override void Dispose(bool disposing)
		{
			if (disposing && (components != null))
			{
				components.Dispose();
			}
			base.Dispose(disposing);
		}

		#region Windows フォーム デザイナーで生成されたコード

		/// <summary>
		/// デザイナー サポートに必要なメソッドです。このメソッドの内容を
		/// コード エディターで変更しないでください。
		/// </summary>
		private void InitializeComponent()
		{
			this.components = new System.ComponentModel.Container();
			this.pictureBox1 = new System.Windows.Forms.PictureBox();
			this.backgroundWorker1 = new System.ComponentModel.BackgroundWorker();
			this.panel1 = new System.Windows.Forms.Panel();
			this.contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(this.components);
			this.cnmSettings = new System.Windows.Forms.ToolStripMenuItem();
			this.statusStrip1 = new System.Windows.Forms.StatusStrip();
			this.slblCam0 = new System.Windows.Forms.ToolStripStatusLabel();
			this.slblCam1 = new System.Windows.Forms.ToolStripStatusLabel();
			this.cnmCrossCam = new System.Windows.Forms.ToolStripMenuItem();
			((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
			this.panel1.SuspendLayout();
			this.contextMenuStrip1.SuspendLayout();
			this.statusStrip1.SuspendLayout();
			this.SuspendLayout();
			//
			// pictureBox1
			//
			this.pictureBox1.Location = new System.Drawing.Point(-2, -2);
			this.pictureBox1.Name = "pictureBox1";
			this.pictureBox1.Size = new System.Drawing.Size(81, 80);
			this.pictureBox1.TabIndex = 0;
			this.pictureBox1.TabStop = false;
			//
			// backgroundWorker1
			//
			this.backgroundWorker1.WorkerReportsProgress = true;
			this.backgroundWorker1.WorkerSupportsCancellation = true;
			this.backgroundWorker1.DoWork += new System.ComponentModel.DoWorkEventHandler(this.backgroundWorker1_DoWork);
			this.backgroundWorker1.ProgressChanged += new System.ComponentModel.ProgressChangedEventHandler(this.backgroundWorker1_ProgressChanged);
			//
			// panel1
			//
			this.panel1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
            | System.Windows.Forms.AnchorStyles.Left)
            | System.Windows.Forms.AnchorStyles.Right)));
			this.panel1.AutoScroll = true;
			this.panel1.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D;
			this.panel1.ContextMenuStrip = this.contextMenuStrip1;
			this.panel1.Controls.Add(this.pictureBox1);
			this.panel1.Location = new System.Drawing.Point(0, 0);
			this.panel1.Name = "panel1";
			this.panel1.Size = new System.Drawing.Size(284, 238);
			this.panel1.TabIndex = 1;
			//
			// contextMenuStrip1
			//
			this.contextMenuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
            this.cnmSettings,
            this.cnmCrossCam});
			this.contextMenuStrip1.Name = "contextMenuStrip1";
			this.contextMenuStrip1.Size = new System.Drawing.Size(280, 74);
			//
			// cnmSettings
			//
			this.cnmSettings.Name = "cnmSettings";
			this.cnmSettings.Size = new System.Drawing.Size(279, 24);
			this.cnmSettings.Text = "設定(&S) ...";
			this.cnmSettings.Click += new System.EventHandler(this.cnmSettings_Click);
			//
			// statusStrip1
			//
			this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
            this.slblCam0,
            this.slblCam1});
			this.statusStrip1.Location = new System.Drawing.Point(0, 236);
			this.statusStrip1.Name = "statusStrip1";
			this.statusStrip1.Size = new System.Drawing.Size(284, 25);
			this.statusStrip1.TabIndex = 2;
			this.statusStrip1.Text = "statusStrip1";
			//
			// slblCam0
			//
			this.slblCam0.Name = "slblCam0";
			this.slblCam0.Size = new System.Drawing.Size(17, 20);
			this.slblCam0.Text = "0";
			//
			// slblCam1
			//
			this.slblCam1.Name = "slblCam1";
			this.slblCam1.Size = new System.Drawing.Size(17, 20);
			this.slblCam1.Text = "0";
			//
			// cnmCrossCam
			//
			this.cnmCrossCam.Name = "cnmCrossCam";
			this.cnmCrossCam.Size = new System.Drawing.Size(279, 24);
			this.cnmCrossCam.Text = "カメラ0とカメラ1を入れ替える(&C)";
			this.cnmCrossCam.Click += new System.EventHandler(this.cnmCrossCam_Click);
			//
			// Form1
			//
			this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
			this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
			this.ClientSize = new System.Drawing.Size(284, 261);
			this.Controls.Add(this.statusStrip1);
			this.Controls.Add(this.panel1);
			this.Name = "Form1";
			this.Text = "Form1";
			this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing);
			this.Load += new System.EventHandler(this.Form1_Load);
			((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
			this.panel1.ResumeLayout(false);
			this.contextMenuStrip1.ResumeLayout(false);
			this.statusStrip1.ResumeLayout(false);
			this.statusStrip1.PerformLayout();
			this.ResumeLayout(false);
			this.PerformLayout();

		}

		#endregion

		private System.Windows.Forms.PictureBox pictureBox1;
		private System.ComponentModel.BackgroundWorker backgroundWorker1;
		private System.Windows.Forms.Panel panel1;
		private System.Windows.Forms.ContextMenuStrip contextMenuStrip1;
		private System.Windows.Forms.ToolStripMenuItem cnmSettings;
		private System.Windows.Forms.StatusStrip statusStrip1;
		private System.Windows.Forms.ToolStripStatusLabel slblCam0;
		private System.Windows.Forms.ToolStripStatusLabel slblCam1;
		private System.Windows.Forms.ToolStripMenuItem cnmCrossCam;
	}
}

AppSettings.cs

using System;
using System.Xml.Serialization;

namespace Gitc
{
	/// <summary>
	/// アプリケーションの設定情報を保存するためのクラス。
	/// </summary>
	public class AppSettings
	{
		//解像度配列の初期ID
		public const int DEF_RESOLUTION_ID = 1;
		//解像度配列
		public static string[] ResolutionArr = { "320x240", "640x480", "800x600", "1024x768" };

		//解像度配列ID
		private int _ResolutionId;
		public int ResolutionId
		{
			get { return this.getResolutionId(this._ResolutionId); }
			set { this._ResolutionId = this.getResolutionId(value); this._ResolutionWidth = -1; }
		}
		private int getResolutionId(int res_id)
		{
			if (res_id < 0 || res_id >= ResolutionArr.Length) return DEF_RESOLUTION_ID;
			return res_id;
		}

		//解像度文字列
		[XmlIgnore()]
		public string ResolutionStr
		{
			get { return ResolutionArr[this.ResolutionId]; }
		}

		//解像度の幅と高さ
		private int _ResolutionWidth, _ResolutionHeight = -1;
		[XmlIgnore()]
		public int ResolutionWidth
		{
			get
			{
				if (this._ResolutionWidth < 0 || this._ResolutionHeight < 0) this.getResolutionWH();
				return this._ResolutionWidth;
			}
		}
		[XmlIgnore()]
		public int ResolutionHeight
		{
			get
			{
				if (this._ResolutionWidth < 0 || this._ResolutionHeight < 0) this.getResolutionWH();
				return this._ResolutionHeight;
			}
		}

		//解像度文字列から幅と高さを求める
		private void getResolutionWH()
		{
			string wh = this.ResolutionStr;
			string[] sep = { "x" };
			string[] split = wh.Split(sep, StringSplitOptions.None);
			int w = int.Parse(split[0]);
			int h = int.Parse(split[1]);

			this._ResolutionWidth = w;
			this._ResolutionHeight = h;
		}

		//カメラ0,1のID
		public int CamId0;
		public int CamId1;

		//カメラを入れ替えるかどうか
		public bool IsCrossCam;

		//============================================================

		/// <summary>
		/// 引数なしコンストラクタ。
		/// </summary>
		public AppSettings()
		{
			this.ResolutionId = DEF_RESOLUTION_ID;
			this.CamId0 = 0;
			this.CamId1 = 1;
			this.IsCrossCam = false;
		}

		//============================================================
	}
}

FormSettings.cs

using Gitc;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace StereoCam
{
    public partial class FormSettings : Form
    {
        private AppSettings mySettings = null;

        public FormSettings(AppSettings ms)
        {
            InitializeComponent();
            this.mySettings = ms;
        }

        private void FormSettings_Load(object sender, EventArgs e)
        {
            //各設定をコントロールへ反映

            this.nudCamId0.Value = this.mySettings.CamId0;
            this.nudCamId1.Value = this.mySettings.CamId1;

            this.cbResolution.Items.AddRange(AppSettings.ResolutionArr);
            this.cbResolution.SelectedIndex = this.mySettings.ResolutionId;

            this.chkIsCrossCam.Checked = this.mySettings.IsCrossCam;
        }

        //OKボタンクリック
        private void btnOk_Click(object sender, EventArgs e)
        {
            //カメラID
            int cid0 = (int)this.nudCamId0.Value;
            int cid1 = (int)this.nudCamId1.Value;

            //IDが同じ?
            if (cid0 == cid1)
            {
                MessageBox.Show("カメラ0とカメラ1が同じ", "エラー");
                return;
            }

            //コントロールの値を設定へ反映

            this.mySettings.CamId0 = cid0;
            this.mySettings.CamId1 = cid1;

            this.mySettings.ResolutionId = this.cbResolution.SelectedIndex;
            this.mySettings.IsCrossCam = this.chkIsCrossCam.Checked;

            //フォームクローズ
            this.DialogResult = DialogResult.OK;
            this.Close();
        }
    }
}

FormSettings.Designer.cs

namespace StereoCam
{
    partial class FormSettings
    {
        /// <summary>
        /// Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            this.label1 = new System.Windows.Forms.Label();
            this.nudCamId0 = new System.Windows.Forms.NumericUpDown();
            this.nudCamId1 = new System.Windows.Forms.NumericUpDown();
            this.label2 = new System.Windows.Forms.Label();
            this.tabControl1 = new System.Windows.Forms.TabControl();
            this.tabPage1 = new System.Windows.Forms.TabPage();
            this.label3 = new System.Windows.Forms.Label();
            this.cbResolution = new System.Windows.Forms.ComboBox();
            this.chkIsCrossCam = new System.Windows.Forms.CheckBox();
            this.btnOk = new System.Windows.Forms.Button();
            this.btnCancel = new System.Windows.Forms.Button();
            ((System.ComponentModel.ISupportInitialize)(this.nudCamId0)).BeginInit();
            ((System.ComponentModel.ISupportInitialize)(this.nudCamId1)).BeginInit();
            this.tabControl1.SuspendLayout();
            this.tabPage1.SuspendLayout();
            this.SuspendLayout();
            // 
            // label1
            // 
            this.label1.AutoSize = true;
            this.label1.Location = new System.Drawing.Point(12, 14);
            this.label1.Name = "label1";
            this.label1.Size = new System.Drawing.Size(44, 12);
            this.label1.TabIndex = 0;
            this.label1.Text = "カメラ0:";
            // 
            // nudCamId0
            // 
            this.nudCamId0.Location = new System.Drawing.Point(62, 12);
            this.nudCamId0.Name = "nudCamId0";
            this.nudCamId0.Size = new System.Drawing.Size(78, 19);
            this.nudCamId0.TabIndex = 1;
            // 
            // nudCamId1
            // 
            this.nudCamId1.Location = new System.Drawing.Point(62, 37);
            this.nudCamId1.Name = "nudCamId1";
            this.nudCamId1.Size = new System.Drawing.Size(78, 19);
            this.nudCamId1.TabIndex = 3;
            // 
            // label2
            // 
            this.label2.AutoSize = true;
            this.label2.Location = new System.Drawing.Point(12, 39);
            this.label2.Name = "label2";
            this.label2.Size = new System.Drawing.Size(44, 12);
            this.label2.TabIndex = 2;
            this.label2.Text = "カメラ1:";
            // 
            // tabControl1
            // 
            this.tabControl1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 
            | System.Windows.Forms.AnchorStyles.Left) 
            | System.Windows.Forms.AnchorStyles.Right)));
            this.tabControl1.Controls.Add(this.tabPage1);
            this.tabControl1.Location = new System.Drawing.Point(4, 4);
            this.tabControl1.Name = "tabControl1";
            this.tabControl1.SelectedIndex = 0;
            this.tabControl1.Size = new System.Drawing.Size(357, 188);
            this.tabControl1.TabIndex = 0;
            // 
            // tabPage1
            // 
            this.tabPage1.Controls.Add(this.label3);
            this.tabPage1.Controls.Add(this.cbResolution);
            this.tabPage1.Controls.Add(this.chkIsCrossCam);
            this.tabPage1.Controls.Add(this.nudCamId0);
            this.tabPage1.Controls.Add(this.nudCamId1);
            this.tabPage1.Controls.Add(this.label1);
            this.tabPage1.Controls.Add(this.label2);
            this.tabPage1.Location = new System.Drawing.Point(4, 22);
            this.tabPage1.Name = "tabPage1";
            this.tabPage1.Padding = new System.Windows.Forms.Padding(3);
            this.tabPage1.Size = new System.Drawing.Size(349, 162);
            this.tabPage1.TabIndex = 0;
            this.tabPage1.Text = "設定";
            this.tabPage1.UseVisualStyleBackColor = true;
            // 
            // label3
            // 
            this.label3.AutoSize = true;
            this.label3.Location = new System.Drawing.Point(12, 71);
            this.label3.Name = "label3";
            this.label3.Size = new System.Drawing.Size(47, 12);
            this.label3.TabIndex = 4;
            this.label3.Text = "解像度:";
            // 
            // cbResolution
            // 
            this.cbResolution.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
            this.cbResolution.FormattingEnabled = true;
            this.cbResolution.Location = new System.Drawing.Point(62, 68);
            this.cbResolution.Name = "cbResolution";
            this.cbResolution.Size = new System.Drawing.Size(121, 20);
            this.cbResolution.TabIndex = 5;
            // 
            // chkIsCrossCam
            // 
            this.chkIsCrossCam.AutoSize = true;
            this.chkIsCrossCam.Location = new System.Drawing.Point(14, 100);
            this.chkIsCrossCam.Name = "chkIsCrossCam";
            this.chkIsCrossCam.Size = new System.Drawing.Size(160, 16);
            this.chkIsCrossCam.TabIndex = 6;
            this.chkIsCrossCam.Text = "カメラ0とカメラ1を入れ替える";
            this.chkIsCrossCam.UseVisualStyleBackColor = true;
            // 
            // btnOk
            // 
            this.btnOk.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
            this.btnOk.Location = new System.Drawing.Point(253, 206);
            this.btnOk.Name = "btnOk";
            this.btnOk.Size = new System.Drawing.Size(95, 23);
            this.btnOk.TabIndex = 1;
            this.btnOk.Text = "OK";
            this.btnOk.UseVisualStyleBackColor = true;
            this.btnOk.Click += new System.EventHandler(this.btnOk_Click);
            // 
            // btnCancel
            // 
            this.btnCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
            this.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
            this.btnCancel.Location = new System.Drawing.Point(152, 206);
            this.btnCancel.Name = "btnCancel";
            this.btnCancel.Size = new System.Drawing.Size(95, 23);
            this.btnCancel.TabIndex = 2;
            this.btnCancel.Text = "キャンセル";
            this.btnCancel.UseVisualStyleBackColor = true;
            // 
            // FormSettings
            // 
            this.AcceptButton = this.btnOk;
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.CancelButton = this.btnCancel;
            this.ClientSize = new System.Drawing.Size(364, 241);
            this.Controls.Add(this.btnCancel);
            this.Controls.Add(this.btnOk);
            this.Controls.Add(this.tabControl1);
            this.Name = "FormSettings";
            this.Text = "設定";
            this.Load += new System.EventHandler(this.FormSettings_Load);
            ((System.ComponentModel.ISupportInitialize)(this.nudCamId0)).EndInit();
            ((System.ComponentModel.ISupportInitialize)(this.nudCamId1)).EndInit();
            this.tabControl1.ResumeLayout(false);
            this.tabPage1.ResumeLayout(false);
            this.tabPage1.PerformLayout();
            this.ResumeLayout(false);

        }

        #endregion

        private System.Windows.Forms.Label label1;
        private System.Windows.Forms.NumericUpDown nudCamId0;
        private System.Windows.Forms.NumericUpDown nudCamId1;
        private System.Windows.Forms.Label label2;
        private System.Windows.Forms.TabControl tabControl1;
        private System.Windows.Forms.TabPage tabPage1;
        private System.Windows.Forms.Button btnOk;
        private System.Windows.Forms.Button btnCancel;
        private System.Windows.Forms.CheckBox chkIsCrossCam;
        private System.Windows.Forms.Label label3;
        private System.Windows.Forms.ComboBox cbResolution;
    }
}

Discussion