Newer
Older
ISCamRecorder / ISCamRecorder / MainForm.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 System.Diagnostics;
using System.Reflection;
using System.IO;
using NAudio.MediaFoundation;
using System.Windows.Forms.DataVisualization.Charting;
using System.Threading;
using System.Media;

namespace ISCamRecorder {

    /// <summary>
    /// ソフトウェアの状態
    /// </summary>
    public enum STATE {
        Init,       // 初期化中 
        Idle,       // 待機中
        Recoding,   // 録画中
        Saving,     // 保存中
        Exit        // 終了
    }

    /// <summary>
    /// メインフォームクラス
    /// </summary>
    public partial class MainForm : Form {

        readonly int PLOT_LENGTH = 200;     // センサー値表示サンプル数
        readonly int UI_UPDATE_INTERVAL = 2000; // UI更新時間
        readonly int CVCAMERA_ID = 0;       // 追加カメラのID
        readonly int CVCAMERA_WIDTH = 1920; // 追加カメラの画像幅
        readonly int CVCAMERA_HEIGHT = 1080; // 追加カメラの画像高さ

        private System.Threading.Timer _UITimer;    // UI更新タイマー
        List<ISCamera> _Cameras = new List<ISCamera> ();    // ISカメラオブジェクト
        SensorData _Sensor;             // センサーオブジェクト
        CvCamera _CvCamera;             // Cvカメラオブジェクト
        Task _TriggerThread;            // トリガースレッド
        Task _SerialThread;             // シリアル通信スレッド
        Task _CvCameraThread;           // opencvカメラスレッド
        Task _RecodingThread;           // 録画スレッド
        float _TriggerFrameRate = 30.0F;    // トリガーフレームレート
        float _RecodingDulation = 0;    // 録画時間
        private ulong _availablePhysicalMemory; //合計物理メモリ
        SubjectList _SubjectList;       // 非測定者一覧フォーム

        public Chart SensorChart { get { return chart1; } }             // 外部からのアクセス用
        public PictureBox CvCameraPic { get { return PicCvCamera; } }   // 外部からのアクセス用
        public STATE State { get; private set; } = STATE.Init;          // ソフトウェアの状態
        public DateTime RecodingTime { get; private set; } = DateTime.Now; // 録画開始時間
        public string RecodingTimeStr { get { return RecodingTime.ToString("yyyyMMdd_HHmmss"); } }  // 録画開始時間文字列
        public string OutputBaseDir { get { return TxtOutputDir.Text; } } // データ保存親フォルダ
        public string OutputDir { get { return Path.Combine(OutputBaseDir, $"rec{RecodingTimeStr}_{TxtSubjectName.Text}_{TxtCounter.Text}"); } } // データ保存フォルダ
        public string ImageType { get; private set; }                   // 画像保存形式
        public int MovieRate { get; private set; }                      // 動画レート
        public bool SwitchEnabled { get; private set; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public MainForm() {
            MediaFoundationApi.Startup();
            InitializeComponent();
            _Sensor = new SensorData(this, PLOT_LENGTH);
            _CvCamera = new CvCamera(this);
            _SubjectList = new SubjectList();
        }

        /// <summary>
        /// フォームロード時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MainForm_Load(object sender, EventArgs e) {
            SetTitle(this.Text);

            // カメラ設定
            _Cameras.Add(new ISCamera(this, icTop, "9220016"));
            _Cameras.Add(new ISCamera(this, icLeft, "9220018"));
            _Cameras.Add(new ISCamera(this, icFront, "9220021"));
            _Cameras.Add(new ISCamera(this, icRight, "9220025"));
            _Cameras.ForEach(c => c.Connect());

            // タイマー&スレッド起動
            _UITimer = new System.Threading.Timer(UITimerCB, this, 0, UI_UPDATE_INTERVAL);
            _TriggerThread = Task.Run(TriggerThread);
            _SerialThread = Task.Run(SerialThread);
            _CvCameraThread = Task.Run(CvCameraThread);

            // コントロール変数初期化
            ImageType = CboImageType.Text;
            MovieRate = int.Parse(TxtMovieRate.Text);
            _RecodingDulation = float.Parse(TxtRecodingDulation.Text);
            SwitchEnabled = ChkStartSW.Checked;
#if DEBUG
            TxtSubjectName.Text = "デバッグ";
            TxtRecodingDulation.Text = "3";
#endif

            State = STATE.Idle;
        }

        /// <summary>
        /// ソフトウェアトリガー生成スレッド
        /// </summary>
        private void TriggerThread() {
            var swatch = new Stopwatch();
            swatch.Start();

            while (State != STATE.Exit) {
                var interval = (long)(1000F / _TriggerFrameRate);
                while (State != STATE.Exit) {
                    if (swatch.ElapsedMilliseconds >= interval) break;
                }
                swatch.Restart();
                _Cameras.ForEach(c => c.SWTrigger());
            }
        }

        /// <summary>
        /// OpenCVカメラスレッド
        /// </summary>
        private void CvCameraThread() {
            if (!_CvCamera.Open(CVCAMERA_ID, CVCAMERA_WIDTH, CVCAMERA_HEIGHT)) return;
            _CvCamera.CameraLoop();
        }

        /// <summary>
        /// シリアル通信スレッド
        /// </summary>
        private void SerialThread() {
            if (!_Sensor.Connect()) return;
            _Sensor.Loop();
        }

        /// <summary>
        /// タイマーイベント
        /// </summary>
        /// <param name="obj"></param>
        static void UITimerCB(object obj) {
            if (((MainForm)obj).State == STATE.Exit) return;
            ((MainForm)obj).UpdateForm();
        }

        /// <summary>
        /// UI更新
        /// </summary>
        public void UpdateForm() {
            if (this.InvokeRequired) {
                this.Invoke((MethodInvoker)delegate { UpdateForm(); });
                return;
            }
            if (State != STATE.Exit) {
                TxtTop.Text = $"上方カメラ {_Cameras[0].CameraInfo()}";
                TxtLeft.Text = $"左方カメラ {_Cameras[1].CameraInfo()}";
                TxtFront.Text = $"前方カメラ {_Cameras[2].CameraInfo()}";
                TxtRight.Text = $"右方カメラ {_Cameras[3].CameraInfo()}";
                TxtSensor.Text = $"逆血センサー {_Sensor.FrameRate:0.0} Hz";
                TxtCvCamera.Text = $"追加カメラ {_CvCamera.CameraInfo()}";
                TotalPhysicalMemory();
                MemoryToUse();
                _TriggerFrameRate = float.Parse(TxtTriggerFPS.Text);
            }
        }

        /// <summary>
        /// カメラプロパティの設定
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void SetProperty_Click(object sender, EventArgs e) {
            var config =_Cameras[0].SetProperty();
            _Cameras.ForEach(c => c.SetProperty(config));
        }

        /// <summary> 
        /// フォームタイトルを設定 
        /// </summary> 
        private void SetTitle(string title) {
            var version = Assembly.GetExecutingAssembly().GetName().Version;
#if DEBUG
            var debug = " [DEBUG]";
#else
            var debug = ""; 
#endif
            Text = $"{title} ver {version.Major}.{version.Minor}{debug}";
        }

        /// <summary>
        /// フォームを閉じる時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MainForm_FormClosing(object sender, FormClosingEventArgs e) {
            if (State == STATE.Idle) {
                Task.Run(ExitProgram);
                e.Cancel = true;
            }else if(State != STATE.Exit) {
                e.Cancel = true;
            }
        }

        /// <summary>
        /// サイズ変更時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MainForm_SizeChanged(object sender, EventArgs e) {
            _Cameras.ForEach(c => c.SetDisplaySize());
        }

        /// <summary>
        /// 利用可能なメモリ量をラベルに表示
        /// </summary>
        private void TotalPhysicalMemory() {

            Microsoft.VisualBasic.Devices.ComputerInfo info =
                new Microsoft.VisualBasic.Devices.ComputerInfo();
            ulong bcs = 1024 * 1024;
            _availablePhysicalMemory = info.AvailablePhysicalMemory / bcs; //利用可能な物理メモリ
            LblAvailableMemory.Text = "利用可能メモリ:" + _availablePhysicalMemory.ToString("#,0") + "MB";
        }

        /// <summary>
        /// 消費メモリ量をラベルに表示
        /// </summary>
        private void MemoryToUse() {
            var memoryConsumption = (_Cameras.Select(c => c.MemoryFor1SecRecoding()).Sum() 
                + _CvCamera.MemoryFor1SecRecoding()) * float.Parse(TxtRecodingDulation.Text);
            LblRecodingMemory.Text = "消費するメモリ:" + memoryConsumption.ToString("#,0") + "MB";
            if (memoryConsumption > _availablePhysicalMemory && _availablePhysicalMemory != 0) LblRecodingMemory.ForeColor = Color.Red;
            else LblRecodingMemory.ForeColor = Color.Black;
        }

        /// <summary>
        /// 動画撮影
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void BtnRecodeMovie_Click(object sender, EventArgs e) {
            StartRecoding();
        }

        /// <summary>
        /// 録画開始・中断
        /// </summary>
        public void StartRecoding() {
            if (State == STATE.Idle) {
                _RecodingThread = Task.Run(RecodingThread);
            } else if (State == STATE.Recoding) {
                // 中断
                Debug.WriteLine("Stop recoding");
                _Cameras.ForEach(c => c.StopRecoding());
            }
        }

        /// <summary>
        /// 録画スレッド
        /// </summary>
        private void RecodingThread()
        {
            // 録画開始サウンド
            var player = new SoundPlayer(@"start.wav");
            player.Play();

            // 録画モードへ変更
            Color originalButtonColor = Color.Gray;
            this.Invoke((MethodInvoker)delegate {
                _Cameras.ForEach(c => c.ChangeSink(true));

                BtnSetProperty.Enabled = false;
                BtnRecodeMovie.Text = "撮影中";
                originalButtonColor = BtnRecodeMovie.BackColor;
                BtnRecodeMovie.BackColor = Color.Orange;
            });

            // 録画開始
            var frameRate = _Cameras.Select(c => c.FrameRate).Average();
            var framesToCapture = (int)(_RecodingDulation * frameRate + 1.0F);
            RecodingTime = DateTime.Now;
            Directory.CreateDirectory(OutputDir);
            State = STATE.Recoding;

            // 録画
            Task[] tasks = new Task[4];
            for (var i = 0; i < _Cameras.Count; i++) {
                var cam = _Cameras[i];
                tasks[i] = Task.Run(() => cam.RecordToMemory(framesToCapture));
            }
            Task.WaitAll(tasks);

            // 保存準備
            State = STATE.Saving;
            player = new SoundPlayer(@"end.wav");
            player.Play();
            this.Invoke((MethodInvoker)delegate {
                BtnRecodeMovie.Enabled = false;
                BtnRecodeMovie.Text = "保存中";
                BtnRecodeMovie.BackColor = Color.Aqua;
            });

            // 保存
            for (var i = 0; i < _Cameras.Count; i++)
            {
                var cam = _Cameras[i];
                tasks[i] = Task.Run(() => cam.SaveToFile(frameRate));
            }
            Task.WaitAll(tasks);
            _CvCamera.SaveToFile();

            // プレビューモードへ変更
            this.Invoke((MethodInvoker)delegate {
                _Cameras.ForEach(c => c.ChangeSink(false));
                BtnRecodeMovie.Text = "動画撮影";
                BtnRecodeMovie.BackColor = originalButtonColor;
                BtnRecodeMovie.Enabled = true;
                BtnSetProperty.Enabled = true;
                TxtCounter.Text = (int.Parse(TxtCounter.Text) + 1).ToString();
            });
            State = STATE.Idle;
        }

        /// <summary>
        /// 静止画撮影ボタン
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void BtnSnapImage_Click(object sender, EventArgs e) {
            if (State != STATE.Idle) return;

            var player = new SoundPlayer(@"snap.wav");
            player.Play();

            var outDir = Path.Combine(OutputBaseDir, "image");
            Directory.CreateDirectory(outDir);
            _Cameras.ForEach(c => c.SnapImage());
            _CvCamera.Snap();
        }

        /// <summary>
        /// プログラム終了
        /// </summary>
        private void ExitProgram() {
            State = STATE.Exit;
            _UITimer.Dispose();

            const int timeout = 2000;
            _SerialThread.Wait(timeout);
            Debug.WriteLine("_SerialThread stop");
            _CvCameraThread.Wait(timeout);
            Debug.WriteLine("_CvCameraThread stop");
            _TriggerThread.Wait(timeout);
            Debug.WriteLine("_TriggerThread stop");

            this.Invoke((MethodInvoker)delegate {
                this.Close();
            });
        }

        /// <summary>
        /// 録画時間テキストボックスでキー押下時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void NumberKeyOnly(object sender, KeyPressEventArgs e) {
            //バックスペースが押された時は有効(Deleteキーも有効)
            if (e.KeyChar == '\b' || e.KeyChar == '.') {
                return;
            }

            //数値0~9以外が押された時はイベントをキャンセルする
            if ((e.KeyChar < '0' || '9' < e.KeyChar)) {
                e.Handled = true;
            }
        }

        /// <summary>
        /// トリガー使用チェック変更時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ChkTrigger_CheckedChanged(object sender, EventArgs e) {
            _Cameras.ForEach(c => c.SetTriggerMode(((CheckBox)sender).Checked));
        }

        /// <summary>
        /// 画像形式選択時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void CboImageType_SelectedIndexChanged(object sender, EventArgs e) {
            ImageType = CboImageType.Text;
        }

        /// <summary>
        /// 動画レート変更時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void TxtMovieRate_TextChanged(object sender, EventArgs e) {
            MovieRate = int.Parse(TxtMovieRate.Text);
        }

        /// <summary>
        /// 記録時間変更時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void TxtRecodingDulation_TextChanged(object sender, EventArgs e) {
            _RecodingDulation = float.Parse(TxtRecodingDulation.Text);
        }

        /// <summary>
        /// SW連動チェック変更
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ChkStartSW_CheckedChanged(object sender, EventArgs e) {
            SwitchEnabled = ChkStartSW.Checked;
        }

        /// <summary>
        /// 非測定者の一覧を開く
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void BtnOpenSubjList_Click(object sender, EventArgs e) {
            _SubjectList.ShowDialog();
        }

        /// <summary>
        /// 非測定者の「次」をクリック
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void BtnNextSubject_Click(object sender, EventArgs e) {
            TxtSubjectName.Text = _SubjectList.Next(TxtSubjectName.Text);
            TxtCounter.Text = "1";
        }

        /// <summary>
        /// 非測定者の「前」をクリック
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void BtnPrevSubject_Click(object sender, EventArgs e) {
            TxtSubjectName.Text = _SubjectList.Previous(TxtSubjectName.Text);
            TxtCounter.Text = "1";
        }
    }
}