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.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 = 500; // UI更新時間(ms)
        readonly int CVCAMERA_ID = 0;       // 追加カメラのID
        readonly int CVCAMERA_WIDTH = 1920; // 追加カメラの画像幅
        readonly int CVCAMERA_HEIGHT = 1080; // 追加カメラの画像高さ

        List<ISCamera> _Cameras;        // ISカメラオブジェクト
        SensorData _Sensor;             // センサーオブジェクト
        CvCamera _CvCamera;             // Cvカメラオブジェクト
        Task _TriggerThread;            // トリガースレッド
        Task _SerialThread;             // シリアル通信スレッド
        Task _CvCameraThread;           // opencvカメラスレッド
        Task _RecodingThread;           // 録画スレッド
        List<SoundPlayer> _Sounds;      // 音源オブジェクト

        public Setting Setting { get; private set; }               // 設定ダイアログ
        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 OutputDir { get { return Path.Combine(Setting.SaveDir , $"rec{RecodingTimeStr}_{TxtSubjectName.Text}_{TxtCounter.Text}"); } } // データ保存フォルダ
        public bool IsReverseBlood { get; private set; } = false;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public MainForm() {
            MediaFoundationApi.Startup();
            InitializeComponent();
            _Cameras = new List<ISCamera>();
            _Sensor = new SensorData(this, PLOT_LENGTH);
            _CvCamera = new CvCamera(this);
            Setting = new Setting();
            _Sounds = new List<SoundPlayer>();
            _Sounds.Add(new SoundPlayer(@"StartRecoding.wav"));
            _Sounds.Add(new SoundPlayer(@"StopRecoding.wav"));
            _Sounds.Add(new SoundPlayer(@"EndSaving.wav"));
            _Sounds.Add(new SoundPlayer(@"ReverseBlood.wav"));
            _Sounds.Add(new SoundPlayer(@"SnapShot.wav"));
            _Sounds.ForEach(s => { s.Load(); });
        }

        /// <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());

            // タイマー&スレッド起動
            _TriggerThread = Task.Run(TriggerThread);
            _SerialThread = Task.Run(SerialThread);
            _CvCameraThread = Task.Run(CvCameraThread);

            // コントロール変数初期化
#if DEBUG
            TxtSubjectName.Text = "デバッグ";
#endif
            this.UpdateControlState();
        }

        /// <summary>
        /// ソフトウェアトリガー生成スレッド
        /// UI更新も行う
        /// </summary>
        private void TriggerThread() {
            var swatch = new Stopwatch();
            swatch.Start();
            var lastUIUpdate = DateTime.Now;

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

                // UI更新
                var now = DateTime.Now;
                if ((now - lastUIUpdate).TotalMilliseconds > UI_UPDATE_INTERVAL) {
                    this.UpdateTextInfo();
                    lastUIUpdate = now;
                }
            }
        }

        /// <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.Connect();
            _Sensor.Loop();
        }

        /// <summary>
        /// UI更新
        /// </summary>
        public void UpdateTextInfo() {
            if (State == STATE.Exit) return;
            this.Invoke((MethodInvoker)delegate {
                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 検出対象:{Setting.RBDetectCh}";
                TxtCvCamera.Text = $"追加カメラ {_CvCamera.CameraInfo()}";
                if (State == STATE.Init && _CvCamera.CurrentFPS > 0) {
                    State = STATE.Idle;
                    this.UpdateControlState();
                }
                if (State == STATE.Recoding) {
                    var elapse = DateTime.Now - RecodingTime;
                    LblState.Text = $"録画中 {elapse.TotalSeconds:0}s";
                }
            });
        }

        /// <summary>
        /// 状態表示の更新
        /// </summary>
        private void UpdateControlState() {
            switch (State) {
            case STATE.Init:
                LblState.Text = "初期化中";
                PnlState.BackColor = Color.LightBlue;
                BtnRecodeMovie.Text = $"{ButtonSymbol(Setting.ACTION.movie)}録画";
                BtnRecodeMovie.Enabled = false;
                BtnSnapImage.Enabled = false;
                BtnSetProperty.Enabled = true;
                BtnNextSubject.Enabled = true;
                BtnPrevSubject.Enabled = true;
                BtnSetting.Enabled = true;
                TxtSubjectName.ReadOnly = false;
                TxtCounter.ReadOnly = false;
                break;
            case STATE.Idle:
                LblState.Text = "待機中";
                PnlState.BackColor = Color.MediumSpringGreen;
                BtnRecodeMovie.Text = $"{ButtonSymbol(Setting.ACTION.movie)}録画 {Setting.RecodingLimit:0}s";
                BtnRecodeMovie.Enabled = true;
                BtnSnapImage.Enabled = true;
                BtnSetProperty.Enabled = true;
                BtnNextSubject.Enabled = true;
                BtnPrevSubject.Enabled = true;
                BtnSetting.Enabled = true;
                TxtSubjectName.ReadOnly = false;
                TxtCounter.ReadOnly = false;
                break;
            case STATE.Recoding:
                LblState.Text = $"録画中";
                PnlState.BackColor = Color.OrangeRed;
                BtnRecodeMovie.Text = $"{ButtonSymbol(Setting.ACTION.movie)}停止";
                BtnRecodeMovie.Enabled = true;
                BtnSnapImage.Enabled = false;
                BtnSetProperty.Enabled = false;
                BtnNextSubject.Enabled = false;
                BtnPrevSubject.Enabled = false;
                BtnSetting.Enabled = false;
                TxtSubjectName.ReadOnly = true;
                TxtCounter.ReadOnly = true;
                break;
            case STATE.Saving:
                LblState.Text = "保存中";
                PnlState.BackColor = Color.Goldenrod;
                BtnRecodeMovie.Text = $"{ButtonSymbol(Setting.ACTION.movie)}停止";
                BtnRecodeMovie.Enabled = false;
                BtnSnapImage.Enabled = false;
                BtnSetProperty.Enabled = false;
                BtnNextSubject.Enabled = false;
                BtnPrevSubject.Enabled = false;
                BtnSetting.Enabled = false;
                TxtSubjectName.ReadOnly = true;
                TxtCounter.ReadOnly = true;
                break;
            }
            BtnSnapImage.Text = $"{ButtonSymbol(Setting.ACTION.image)}撮影";
        }

        /// <summary>
        /// 録画開始・中断
        /// </summary>
        private 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()
        {
            // 録画開始
            _Sounds[0].Play();
            this.Invoke((MethodInvoker)delegate {
                for (var i = 0; i < _Cameras.Count; i++) {
                    if (Setting.RecCameras[i]) _Cameras[i].ChangeSink(true);
                }
                RecodingTime = DateTime.Now;
                Directory.CreateDirectory(OutputDir);
                State = STATE.Recoding;
                this.UpdateControlState();
            });
            _Sensor.StartRecoding(TxtSubjectName.Text);

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

            // 保存準備
            _Sounds[1].Play();
            _Sensor.StopRecoding();
            IsReverseBlood = false;
            this.Invoke((MethodInvoker)delegate {
                State = STATE.Saving;
                this.UpdateControlState();
            });

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

            // プレビューモードへ変更
            this.Invoke((MethodInvoker)delegate {
                for (var i = 0; i < _Cameras.Count; i++) {
                    if (Setting.RecCameras[i]) _Cameras[i].ChangeSink(false);
                }
                TxtCounter.Text = (int.Parse(TxtCounter.Text) + 1).ToString();
                State = STATE.Idle;
                this.UpdateControlState();
            });
            _Sounds[2].Play();
            Debug.WriteLine("_RecodingThread ends");
        }

        /// <summary>
        /// 逆血時
        /// </summary>
        public void ReverseBlood() {
            IsReverseBlood = true;
            _Sounds[3].Play();
        }

        /// <summary>
        /// 静止画撮影
        /// </summary>
        private void SnapImage() {
            if (State != STATE.Idle) return;

            _Sounds[4].Play();
            var outDir = Path.Combine(Setting.SaveDir, "image");
            Directory.CreateDirectory(outDir);
            this.Invoke((MethodInvoker)delegate {
                for (var i = 0; i < _Cameras.Count; i++) {
                    if (Setting.SnapCameras[i]) _Cameras[i].SnapImage();
                }
            });
            if (Setting.SnapCameras[4]) _CvCamera.Snap();
        }

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

        /// <summary>
        /// 静止画撮影ボタン
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void BtnSnapImage_Click(object sender, EventArgs e) {
            this.SnapImage();
        }

        /// <summary>
        /// ハードウェアボタン押下時
        /// </summary>
        public void HWButton() {
            if (Setting.ButtonAction == Setting.ACTION.movie) {
                StartRecoding();
            } else {
                SnapImage();
            }
        }

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

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

            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 BtnSetting_Click(object sender, EventArgs e) {
            for (int i = 0; i < _Cameras.Count; i++) {
                Setting.ConsumedMemoryPerSecond[i] = _Cameras[i].ConsumedMemoryPerSecond();
            }
            Setting.ConsumedMemoryPerSecond[4] = _CvCamera.ConsumedMemoryPerSecond();
            var lastTriggerSetting = Setting.SWTrigger;

            Setting.ShowDialog();

            this.UpdateControlState();
            if (Setting.SWTrigger != lastTriggerSetting) {
                _Cameras.ForEach(c => c.SetTriggerMode(Setting.SWTrigger));
            }
        }

        /// <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>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void BtnNextSubject_Click(object sender, EventArgs e) {
            TxtSubjectName.Text = Setting.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 = Setting.Previous(TxtSubjectName.Text);
            TxtCounter.Text = "1";
        }

        /// <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> 
        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="action"></param>
        /// <returns></returns>
        private string ButtonSymbol(Setting.ACTION action) {
            return Setting.ButtonAction == action ? "● " : "";

        }

        /// <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>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void SplitterMoved(object sender, SplitterEventArgs e) {
            _Cameras.ForEach(c => c.SetDisplaySize());
        }
    }
}