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;
private string[] _HeaderItems = {
"time", "stimNo", "sceneNo", "stimTime", "target", "contact time",
"Feedback", "RR", "gazeVx", "gazeVy", "pupilR", "pupilL" };
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];
/// <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) {
TxtLogFilename.Text = "not opened";
var args = Environment.GetCommandLineArgs();
if (args.Length > 1) {
ClearData();
ReadCSV(args[1]);
Analyze();
}
}
/// <summary>
/// データ初期化
/// </summary>
private void ClearData() {
_Data.Clear();
_SceneTable.Clear();
listView1.Items.Clear();
foreach (var ctl in this.Controls) {
if (((Control)ctl).GetType().Equals(typeof(TextBox))){
((Control)ctl).Text = "";
}
}
}
/// <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間隔の変動範囲の最大値
// FB回数
var fbCounts = new int[5];
var fbPts = new int[5];
for (int i=0; i<fbCounts.Length; i++) {
fbCounts[i] = _Data.Where(s => s.Feedback == i + 1).Count();
fbPts[i] = fbCounts[i] * (i + 1);
}
// シーン別解析
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}" }));
}
// 表示
TxtFB1.Text = $"{fbCounts[0],2}回 {fbPts[0],4}ポイント";
TxtFB2.Text = $"{fbCounts[1],2}回 {fbPts[1],4}ポイント";
TxtFB3.Text = $"{fbCounts[2],2}回 {fbPts[2],4}ポイント";
TxtFB4.Text = $"{fbCounts[3],2}回 {fbPts[3],4}ポイント";
TxtFB5.Text = $"{fbCounts[4],2}回 {fbPts[4],4}ポイント";
TxtFBTotal.Text = $"{fbCounts.Sum(),2}回 {fbPts.Sum(),4}ポイント";
}
/// <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>
/// <param name="csvfile"></param>
private void ReadCSV(string csvfile) {
char[] sep = { ',' };
if (!File.Exists(csvfile)) {
MessageBox.Show("ファイルがありません", "Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
using (var sr = new StreamReader(csvfile,
Encoding.GetEncoding("shift_jis"))) {
// ヘッダー検証
var readHeader = sr.ReadLine().Split(sep);
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(sep);
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);
// 表示
TxtLogFilename.Text = csvfile;
var filename = Path.GetFileNameWithoutExtension(csvfile);
TxtDateTime.Text = string.Format("{0}年{1}月{2}日 {3}時{4}分",
filename.Substring(0,4), filename.Substring(4, 2),
filename.Substring(6, 2), filename.Substring(9, 2),
filename.Substring(11, 2));
var strs = filename.Split('_');
TxtSubject.Text = $"{strs[2]}";
TxtVisit.Text = $"{strs[3]}";
}
}
/// <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") {
ClearData();
ReadCSV(filename[0]);
Analyze();
}
}
/// <summary>
/// ログファイルを開くボタン
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void BtnOpenLog_Click(object sender, EventArgs e) {
if (openFileDialog1.ShowDialog() == DialogResult.Cancel) return;
ClearData();
ReadCSV(openFileDialog1.FileName);
Analyze();
}
}
}