Newer
Older
PrismSoftware / EcomAnalysis / 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 System.IO;
using System.Diagnostics;
using MathNet.Numerics.Interpolation;
using System.Numerics;
using MathNet.Numerics.IntegralTransforms;

namespace EcomAnalysis {
    public partial class Form1 : Form {

        const int BLOCKS = 4;
        static private string[] _HeaderItems = {
            "time", "stimNo", "sceneNo", "stimTime", "target", "contact time",
            "Feedback", "RR", "gazeVx", "gazeVy", "pupilR", "pupilL" };
        static private Dictionary<int, int> _SceneNoAssign = new Dictionary<int, int>();
        private List<LogData> _Data = new List<LogData>();
        private List<SceneData> _SceneTable = new List<SceneData>();
        private SceneMean _ScMean = new SceneMean();
        private double[] LF = new double[BLOCKS];
        private double[] HF = new double[BLOCKS];
        private LogFileInfo _LogFileInfo = new LogFileInfo();
        private FormCompareLogs _FormCompareLogs = new FormCompareLogs();
        private bool _NewLog = true;

        /// <summary>
        /// コンストラクタ
        /// 基本データ設定
        /// </summary>
        public Form1() {
            InitializeComponent();
            for (int scene = 1, stim = 5; scene <= 10; scene++, stim += 2) {
                _SceneNoAssign.Add(stim, scene);
            }
            for (int scene = 11, stim = 26; scene <= 20; scene++, stim += 2) {
                _SceneNoAssign.Add(stim, scene);
            }
            for (int scene = 21, stim = 47; scene <= 30; scene++, stim += 2) {
                _SceneNoAssign.Add(stim, scene);
            }
            for (int scene = 31, stim = 68; scene <= 40; scene++, stim += 2) {
                _SceneNoAssign.Add(stim, scene);
            }
        }

        /// <summary>
        /// フォームロード時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Form1_Load(object sender, EventArgs e) {
            var args = Environment.GetCommandLineArgs();
            if (args.Length > 1) OpenLog(args[1]);
        }

        /// <summary>
        /// ログファイルを開く
        /// </summary>
        /// <param name="filename"></param>
        /// <returns></returns>
        private bool OpenLog(string filename) {
            // ファイルの存在確認
            if (!File.Exists(filename)) {
                MessageBox.Show("ファイルがありません", "Error",
                    MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }

            // ログファイル情報の処理
            if (!_LogFileInfo.Set(filename)) {
                MessageBox.Show("Logファイル名が正しくありません", "Error",
                    MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }

            // CSV読込
            ReadCSV(_LogFileInfo, _Data);
            TxtLogFilename.Text = _LogFileInfo.Filepath;
            TxtDateTime.Text = _LogFileInfo.ExecDate.ToString("yyyy年MM月dd日 HH時mm分");
            TxtSubject.Text = _LogFileInfo.Subject;
            TxtVisit.Text = _LogFileInfo.Visit.ToString();

            // フィードバック処理
            _LogFileInfo.FB.Calc(_Data);
            listView3.Items.Clear();
            var fbstrs = new string[] { "OK", "Good", "Nice", "Great", "Excellent", "合計" };
            for (var i=0; i< fbstrs.Length; i++) {
                listView3.Items.Add(new ListViewItem(new string[]{
                    fbstrs[i],
                    _LogFileInfo.FB.Count((Feedback.FBITEM)i).ToString(),
                    _LogFileInfo.FB.Point((Feedback.FBITEM)i).ToString(),
                    }));
            }

            Analyze();

            _NewLog = true;
            return true;
        }

        /// <summary>
        /// 解析
        /// </summary>
        private void Analyze() {
            // 定数
            const double RR2HB = 60000.0;  // RR間隔(ms)から心拍数への変換係数
            const int HB_MIN = 20;          // 心拍数の変動範囲の最小値
            const int HB_MAX = 200;         // 心拍数の変動範囲の最大値
            const int RR_MIN = (int)RR2HB / HB_MAX; // RR間隔の変動範囲の最小値
            const int RR_MAX = (int)RR2HB / HB_MIN; // RR間隔の変動範囲の最大値

            // シーン別解析
            _SceneTable.Clear();
            listView1.Items.Clear();
            if (_Data.Where(s => s.SceneNo > 0).Count() > 0) {
                var sceneBegin = _Data.Where(s => s.SceneNo > 0).Min(s => s.SceneNo);
                var sceneEnd = _Data.Where(s => s.SceneNo > 0).Max(s => s.SceneNo);

                for (int sc = sceneBegin; sc <= sceneEnd; sc++) {
                    var scDat = new SceneData();
                    scDat.SceneNo = sc;
                    var dat = _Data.Where(s => s.SceneNo == sc);
                    if (dat.Count() == 0) continue;
                    scDat.FBCount = dat.Count(c => c.Feedback > 0);
                    scDat.ECTime = dat.Count(c => c.Feedback == 5) * 15.0 +
                        dat.OrderBy(s => s.SceneTime).Select(s=>s.ContactTime).Last();

                    scDat.Latency = dat.Where(c => c.Feedback > 0).Count()==0 ? 0 : 
                        dat.Where(c => c.Feedback > 0).Select(s => s.SceneTime).First();
                    var validRR = dat.Where(s => s.RR >= RR_MIN && s.RR <= RR_MAX);
                    scDat.Beat = validRR.Count() > 0 ? RR2HB / validRR.Select(s => s.RR).Average() : 0;
                    scDat.Blink = CountEyeBlink(dat);
                    scDat.Pupil.R = dat.Select(s => s.Pupil.R).Average();
                    scDat.Pupil.L = dat.Select(s => s.Pupil.L).Average();

                    // アイコンタクト回数カウント
                    var datArr = dat.ToArray();
                    scDat.ECCount = 0;
                    var noContact = true;
                    for (var i = 0; i < datArr.Count(); i++) {
                        if (datArr[i].Target > 0 && noContact) {
                            scDat.ECCount++;
                            noContact = false;
                        }
                        if (datArr[i].Target == 0 && datArr[i].GazeV.x >= 0) noContact = true;
                    }

                    var items = new string[] { 
                        $"{scDat.SceneNo}", $"{scDat.FBCount} 回",
                        $"{scDat.ECTime:0.00} 秒", $"{scDat.ECCount} 回", $"{scDat.Latency:0.00} 秒",
                        $"{scDat.Beat:0.00} bpm", $"{scDat.Blink} 回",
                        $"{scDat.Pupil.R:0.00} mm", $"{scDat.Pupil.L:0.00} mm"};
                    listView1.Items.Add(new ListViewItem(items));
                    _SceneTable.Add(scDat);
                }

                _ScMean.FBCount = _SceneTable.Select(s => s.FBCount).Average();
                _ScMean.ECTime = _SceneTable.Select(s => s.ECTime).Average();
                _ScMean.ECCount = _SceneTable.Select(s => s.ECCount).Average();
                var validLatency = _SceneTable.Where(s => s.FBCount > 0);
                _ScMean.Latency = validLatency.Count() > 0 ? validLatency.Select(s => s.Latency).Average() : 0;
                var validBeat = _SceneTable.Where(s => s.Beat > 0);
                _ScMean.Beat = validBeat.Count() > 0 ? validBeat.Select(s => s.Beat).Average() : 0;
                _ScMean.Blink = _SceneTable.Select(s => s.Blink).Average();
                _ScMean.Pupil.R = _SceneTable.Select(s => s.Pupil.R).Average();
                _ScMean.Pupil.L = _SceneTable.Select(s => s.Pupil.L).Average();

                listView1.Items.Add(new ListViewItem(new string[] {
                    "平均",
                    $"{_ScMean.FBCount:0.00} 回",
                    $"{_ScMean.ECTime:0.00} 秒",
                    $"{_ScMean.ECCount:0.00} 回",
                    $"{_ScMean.Latency:0.00} 秒",
                    $"{_ScMean.Beat:0.00} bpm",
                    $"{_ScMean.Blink:0.00} 回",
                    $"{_ScMean.Pupil.R:0.00} ms",
                    $"{_ScMean.Pupil.L:0.00} ms"}));
            }

            // HRV算出(1/4ブロック単位)
            listView2.Items.Clear();
            for (var block = 0; block < BLOCKS; block++) {
                LF[block] = 0D;
                HF[block] = 0D;

                // RR間隔データ取得
                var datArr = _Data.Where(s =>
                    s.StimNo >= 4 + block * 21 && s.StimNo < 24 + block * 21).ToArray();
                var startTime = datArr[0].ElapTime;
                var lastRRtime = startTime;
                var lastRR = datArr[0].RR;
                var rrTime = new List<double>();
                var rrVal = new List<double>();

                for (var i = 0; i < datArr.Count(); i++) {
                    if (datArr[i].RR >= RR_MIN && datArr[i].RR <= RR_MAX) {
                        if (datArr[i].RR != lastRR) {
                            while (datArr[i].ElapTime - lastRRtime > datArr[i].RR * 0.001 + 0.1) {
                                lastRRtime += lastRR * 0.001;
                                rrTime.Add(lastRRtime);
                                rrVal.Add(lastRR);
                            }
                            rrTime.Add(datArr[i].ElapTime);
                            rrVal.Add(datArr[i].RR);
                            lastRRtime = datArr[i].ElapTime;
                            lastRR = datArr[i].RR;
                        }
                    }
                }
               if (rrTime.Count < 100) continue;    // データが不足する場合はスキップ

                // ノイズ除去
                const int M = 2;
                var rrcTime = new List<double>();
                var rrcVal = new List<double>();
                for (var i = M; i < rrTime.Count() - M; i++) {
                    var wrrVal = rrVal.GetRange(i - M, M * 2 + 1);
                    wrrVal.Sort();
                    var median = wrrVal[M];
                    if (Math.Abs(rrVal[i] - median) / median < 0.5) {
                        rrcTime.Add(rrTime[i]);
                        rrcVal.Add(rrVal[i]);
                    }
                }
                // 時間等間隔に補間
                var linear = LinearSpline.Interpolate(rrcTime, rrcVal); // 線形補間
                //var cspline = CubicSpline.InterpolateNatural(rrcTime, rrcVal); // 3次スプライン補間
                var rriVal = new List<double>();
                const int WINDOW_LEN = 512; // ウインドウサイズ
                const int WINDOW_NUM = 4;   // ウインドウ分割数
                int SAMPLES = (WINDOW_LEN / 2) * (WINDOW_NUM + 1);
                const double fc = 0.04; // 低域遮断周波数
                double T = rrcTime.Max() - rrcTime[0];
                double F = 0.443 / fc;
                int n = (int)Math.Floor(F * SAMPLES / (T * (1 - F / T))) + 1;    // 移動平均サイズ
                var interval = T / (SAMPLES + n);
                //                Debug.WriteLine($"dt={interval}, n={n}, fc={0.443 / (n * interval)}");
                for (var t = rrcTime[0]; t < rrcTime.Max(); t += interval) {
                    rriVal.Add(linear.Interpolate(t));
                }
                // 移動平均によるトレンド除去
                var trend = Enumerable.Range(0, SAMPLES).Select(i => rriVal.Skip(i).Take(n).Average()).ToList();
                var rrtr = new List<double>();
                var sum = 0D;
                for (var i = 0; i < SAMPLES; i++) {
                    var tr = rriVal[i + n / 2] - trend[i];
                    rrtr.Add(tr);
                    sum += tr;
                }
                // 平均偏差
                sum /= SAMPLES;
                for (var i = 0; i < SAMPLES; i++) {
                    rrtr[i] -= sum;
                    //Debug.WriteLine($"{i * interval}\t{rrtr[i]}");
                }
                // ウインドウ処理
                var taperWeight = new double[WINDOW_LEN];
                var taperSize = WINDOW_LEN / 10;
                for (var i = 0; i < WINDOW_LEN; i++) {
                    taperWeight[i] = i < taperSize ? 0.5 * (1.0 - Math.Cos(Math.PI * (double)i / taperSize)) : 
                        i > WINDOW_LEN- taperSize ? 0.5 * (1.0 + Math.Cos(Math.PI * (double)(i- WINDOW_LEN + taperSize) / taperSize)) : 1.0;
                }
                var fft = new Complex[WINDOW_NUM][];
                for (var w = 0; w < WINDOW_NUM; w++) {
                    var rrw = rrtr.GetRange(w * WINDOW_LEN / 2, WINDOW_LEN);
                    fft[w] = new Complex[WINDOW_LEN];
                    for (var i = 0; i < WINDOW_LEN; i++) {
                        fft[w][i] = new Complex(rrw[i] * taperWeight[i], 0);
                    }
                    Fourier.Forward(fft[w], FourierOptions.Default);
                }
                var df = 1.0 / (WINDOW_LEN * interval); // フーリエ解析の周波数分解能
                const double LFmin = 0.04;
                const double LFmax = 0.15;
                const double HFmax = 0.4;
                for (var i = 0; i < WINDOW_LEN / 2; i++) {
                    var fftm = 0D;
                    for (var w = 0; w < WINDOW_NUM; w++) {
                        fftm += fft[w][i].Magnitude * fft[w][i].Magnitude;
                    }
                    fftm /= WINDOW_NUM;
                    var power = 2.0 * fftm / 0.875 / WINDOW_LEN * interval;
                    LF[block] += i * df >= LFmin && i * df < LFmax ? power : 0;
                    HF[block] += i * df >= LFmax && i * df < HFmax ? power : 0;
                    //Debug.WriteLine($"{i * df}\t{power}");
                }
                listView2.Items.Add(new ListViewItem(new string[] {
                    $"{block*10+1}-{block*10+10}", $"{LF[block]:0.0}", $"{HF[block]:0.0}", $"{LF[block]/HF[block]:0.00}" }));
            }
        }

        /// <summary>
        /// 瞬目回数を算出
        /// </summary>
        /// <param name="dat">瞬目回数</param>
        /// <returns></returns>
        private int CountEyeBlink(IEnumerable<LogData> dat) {
            const int BLINK_LENGTH = 4;    // 瞬目と判断する視線データ未検出回数
            int close = 0;  // 閉じている回数
            int blink = 0;  // 瞬目回数

            foreach (var d in dat.OrderBy(s => s.SceneTime)) {
                if (d.GazeV.x < 0) {
                    ++close;
                } else {
                    if (close >= BLINK_LENGTH) ++blink;
                    close = 0;
                }
            }

            return blink;
        }

        /// <summary>
        /// CSVファイルの読み込み
        /// </summary>
        static private void ReadCSV(LogFileInfo lfi, List<LogData> data) {

            using (var sr = new StreamReader(lfi.Filepath,
                Encoding.GetEncoding("shift_jis"))) {

                // ヘッダー検証
                var readHeader = sr.ReadLine().Split(',');
                var headerCol = new Dictionary<string, int>();
                foreach (var hi in _HeaderItems) {
                    headerCol.Add(hi, readHeader.ToList().IndexOf(hi));
                }
                if (headerCol.Count < _HeaderItems.Length-1) {
                    MessageBox.Show("必要な列がありません", "Error", 
                        MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return;
                }

                // 読み込み
                data.Clear();
                var sceneNoExist = headerCol["sceneNo"] >= 0;
                while (sr.Peek() > -1) {
                    var line = sr.ReadLine().Split(',');
                    var ld = new LogData();
                    ld.ElapTime = double.Parse(line[headerCol["time"]]);
                    ld.StimNo = int.Parse(line[headerCol["stimNo"]]);
                    ld.SceneTime = double.Parse(line[headerCol["stimTime"]]);
                    ld.Target = int.Parse(line[headerCol["target"]]);
                    ld.ContactTime = double.Parse(line[headerCol["contact time"]]);
                    ld.Feedback = int.Parse(line[headerCol["Feedback"]]);
                    ld.RR = int.Parse(line[headerCol["RR"]]);
                    ld.SceneNo = sceneNoExist ? int.Parse(line[headerCol["sceneNo"]]) 
                        : (_SceneNoAssign.ContainsKey(ld.StimNo) ? _SceneNoAssign[ld.StimNo] : 0);
                    ld.GazeV.x = float.Parse(line[headerCol["gazeVx"]]);
                    ld.GazeV.y = float.Parse(line[headerCol["gazeVy"]]);
                    ld.Pupil.R = float.Parse(line[headerCol["pupilR"]]);
                    ld.Pupil.L = float.Parse(line[headerCol["pupilL"]]);
                    data.Add(ld);
                }
                //Debug.WriteLine(_Data.Count);
            }
        }

        /// <summary>
        /// CSV保存ボタン
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void BtnSaveSceneTable_Click(object sender, EventArgs e) {
            if (_SceneTable.Count < 1) return;

            saveFileDialogCsv.FileName = $"シーン解析_{TxtSubject.Text}_{TxtVisit.Text}.csv";
            if (saveFileDialogCsv.ShowDialog() == DialogResult.Cancel) return;

            var enc = Encoding.GetEncoding("Shift_JIS");
            var sr = new StreamWriter(saveFileDialogCsv.FileName, false, enc);
            sr.WriteLine("シーン,FB回数,EC時間(s),EC回数,潜時(s),心拍数,瞬目回数,瞳孔径:右(mm),瞳孔径:左(mm)");
            foreach (var sc in _SceneTable) {
                sr.WriteLine($"{sc.SceneNo},{sc.FBCount},{sc.ECTime:0.00},{sc.ECCount},{sc.Latency:0.00}," +
                    $"{sc.Beat:0.00},{sc.Blink},{sc.Pupil.R:0.00},{sc.Pupil.L:0.00}");
            }
            sr.WriteLine($"平均,{_ScMean.FBCount},{_ScMean.ECTime:0.00},{_ScMean.ECCount:0.00},{_ScMean.Latency:0.00},{_ScMean.Beat:0.00}," +
                $"{_ScMean.Blink},{_ScMean.Pupil.R:0.00},{_ScMean.Pupil.L:0.00}");
            sr.WriteLine("");
            sr.WriteLine("シーン,LF,HF,LF/HF");
            for (var b =0; b<BLOCKS; b++) {
                sr.WriteLine($"'{b * 10 + 1}-{b * 10 + 10},{LF[b]:0.0},{HF[b]:0.0},{LF[b] / HF[b]:0.00}");
            }
            sr.Close();
        }

        /// <summary>
        /// ドラッグが入ったとき
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Form1_DragEnter(object sender, DragEventArgs e) {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
                e.Effect = DragDropEffects.Copy;
            else 
                e.Effect = DragDropEffects.None;
        }

        /// <summary>
        /// ファイルがドロップされた時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Form1_DragDrop(object sender, DragEventArgs e) {
            var filename = (string[])e.Data.GetData(DataFormats.FileDrop, false);
            if (Path.GetExtension(filename[0]) == ".csv") {
                OpenLog(filename[0]);
            }
        }

        /// <summary>
        /// ログファイルを開くボタン
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void BtnOpenLog_Click(object sender, EventArgs e) {
            if (openFileDialog1.ShowDialog() == DialogResult.Cancel) return;
            OpenLog(openFileDialog1.FileName);
        }

        /// <summary>
        /// 過去の実施結果と比較ボタン
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void BtnCompareLogs_Click(object sender, EventArgs e) {

            if (_NewLog) {
                var preCursor = Cursor.Current;
                Cursor.Current = Cursors.WaitCursor;

                // 同じ被験者のLogファイルを取得
                var dir = Path.GetDirectoryName(_LogFileInfo.Filepath);
                var files = Directory.GetFiles(dir, "*.csv");
                var LogFiles = new List<LogFileInfo>() { _LogFileInfo };

                foreach (var f in files) {
                    // ファイル情報の確認
                    if (f.Equals(_LogFileInfo.Filepath)) continue;
                    var lfi = new LogFileInfo();
                    if (!lfi.Set(f)) continue;
                    if (!lfi.Subject.Equals(_LogFileInfo.Subject)) continue;
                    if (!File.Exists(f)) continue;

                    // フィードバック集計
                    var data = new List<LogData>();
                    ReadCSV(lfi, data);
                    var fb = new Feedback();
                    lfi.FB.Calc(data);

                    LogFiles.Add(lfi);
                }

                _FormCompareLogs.SetData(LogFiles);
                _NewLog = false;

                Cursor.Current = preCursor;
            }
            _FormCompareLogs.ShowDialog();
        }

        /// <summary>
        /// 閉じるボタン
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void BtnClose_Click(object sender, EventArgs e) {
            Close();
        }
    }
}