Newer
Older
PrismSoftware / ECTrainer2 / Worker.cpp
#include "BaseProcess.h"
#include "Worker.h"
#include "ECTrainer.h"
#include "EyeTrack.h"
#include "Stimulus.h"
#include "BitalMonitor.h"
#include "Marker.h"
#include "MovieObject.h"
#include "TobiiREST.h"
#include "ECTrainerGUI.h"
#include "nkcWinUtils.h"
#include "HPTimer.h"

// コンストラクタ
Worker::Worker(ECTrainer* pEct) : BaseProcess(pEct)
	, _AppStatus(APP_STATUS::BOOT)
	, _pExpTimer(NULL)
	, _pContactTimer(NULL)
	, _TargetImage()
	, _FullScreenImage()
	, _FBLevel(1)
	, _TrainingLevel(1)
	, _Shift(0, 0)
	, _StartStage(0)
	, _EnableTrainLevel(false)
	, _Error(-1.F)
{
	_pExpTimer = new nkc::HPTimer();
	_pContactTimer = new nkc::HPTimer();
	_Trigger = ::CreateEvent(NULL, FALSE, FALSE, NULL);
}

// デストラクタ
Worker::~Worker() {
	nkc::wut::SafeDelete((void**)&_pExpTimer);
	nkc::wut::SafeDelete((void**)&_pContactTimer);
	::CloseHandle(_Trigger);
}

// 初期化
bool Worker::Init() {

	_AppStatus = APP_STATUS::IDLE;

	return true;
}

// 基本処理
bool Worker::Routine() {
	// 視線情報更新を待つ
	if (::WaitForSingleObject(_Trigger, 1) == WAIT_TIMEOUT) return false;

	cv::Point2f gazeV = (Ect()->PEyeTrack()->GetGazeV());
	int stimNo = Ect()->PStimulus()->GetStimNo();
	int calibNo = Ect()->PStimulus()->GetSoftCalibNo();

	// 注視点を画像座標に変換
	auto gazeI = gazeV.x < 0 ? cv::Point2f(-1, -1) : Ect()->PMarker()->ConvV2I(gazeV) - _Shift;

	// 表示画像取得
	if (_StimImage.empty() || Ect()->PStimulus()->IsNewDisplay()) {
		_StimImage = Ect()->PStimulus()->GetDisplay();
		if (_StimImage.cols != Stimulus::SMALL_MOVIE_WIDTH)	_FullScreenImage.Put(_StimImage);
	}

	// ターゲット画像生成
	int hit = -1;
	double dtMin = 0;
	int fb = 0;
	double frameInterval = _pContactTimer->Interval();
	std::vector<Element> elems = Ect()->PStimulus()->GetMovieObject();
	if (stimNo >= 0 || calibNo >= 0) {
		cv::Mat stimImg = _StimImage.clone();
		float scale = (float)stimImg.cols / Ect()->PMarker()->IMGSIZE.width;
		if (elems.size() > 0) {
			for (int e = 0; e < elems.size(); e++) {
				// ターゲット描画
				auto target = cv::Point2f(elems[e].x, elems[e].y);
				if (elems[e].d > 0) {
					float dMod = elems[e].d * (1.0F - TRAINING_LEVEL_EFFECT * (_TrainingLevel - 1));
					cv::circle(stimImg, target * scale, (int)(dMod * scale), CV_RGB(255, 0, 0), 2);

					if (gazeI.x >= 0) {
						// ターゲット判定
						auto dt = cv::norm(gazeI - target);
						if (dt < dMod && (hit < 0 || dt < dtMin)) {
							hit = e;
							dtMin = dt;
						}
					}
				}
			}
		}
		if (gazeI.x >= 0) {
			// 画像上の注視点描画
			cv::circle(stimImg, gazeI * scale, (int)(20 * scale), CV_RGB(0, 0, 255), 3);

			if (Ect()->PStimulus()->IsSoftCalib()) {
				// ソフトウェアキャリブレーション
				if (Ect()->PStimulus()->GetStimTime() >= SHIFT_LOG_STARTTIME && elems.size() > 0) {
					auto shift = gazeI - cv::Point2f(elems[0].x, elems[0].y);
					_ShiftLog.push_back(shift );
					//nkc::wut::DebugPrintf(_T("Shift %.1f,%.1f\n"), shift.x, shift.y);
				}
			} else if (Ect()->PStimulus()->IsCalibCheck()) {
				// キャリブレーションチェック
				if (Ect()->PStimulus()->GetStimTime() >= SHIFT_LOG_STARTTIME && elems.size() > 0) {
					auto shift = gazeI - cv::Point2f(elems[0].x, elems[0].y);
					_ErrorLog.push_back(shift);
				}
			} else {
				if (hit >= 0) {
					_ContactTime += frameInterval;

					if (_ContactTime >= (double)(FEEDBACK_TIME * _FBLevel)) {
						this->PostMsg((int)ECTMSG::FB_OK + _FBLevel - 1);
						fb = _FBLevel++;
						if (_FBLevel > 5) {
							this->ResetParams(); // 最終FB後にリセット
							if (*Ect()->PWorker()->EnableTrainLevelPtr()) _TrainingLevel++;
						}
					}
				}
			}
		}

		_TargetImage.Put(stimImg);

		// ずれ解析
		if (!Ect()->PStimulus()->IsSoftCalib() && _ShiftLog.size() > 0) {
			cv::Point2f sum(0,0);
			for (auto& x : _ShiftLog) sum += x;
			_Shift.x = sum.x / _ShiftLog.size();
			_Shift.y = sum.y / _ShiftLog.size();
			nkc::wut::DebugPrintf(_T("Mean Shift %.1f,%.1f\n"), _Shift.x, _Shift.y);
			_ShiftLog.clear();
		}
	} else {
		_TargetImage.Put(_StimImage);
	}

	// ログ出力
	if (stimNo >= 0 || calibNo >= 0) {
		Record r;
		r.ElapTime = _pExpTimer->Elapse() / 1000.;	// 経過時間
		r.StimNo = stimNo >= 0 ? stimNo : calibNo;	// 刺激データ番号
		r.SceneNo = stimNo < 1 ? 0 : Ect()->PStimulus()->GetScene(stimNo - 1);	// シーン番号
		r.SceneTime = Ect()->PStimulus()->GetStimTime() / 1000.;	// 刺激提示の経過時間
		r.GazeV = gazeV;							// 注視点(視野カメラ座標)
		r.Shift = _ShiftLog.size() > 0 && Ect()->PStimulus()->GetStimTime() >= SHIFT_LOG_STARTTIME
			&& gazeI.x >= 0 ? _ShiftLog.back() : _Shift;	// ずれ
		r.GazeI = gazeI;							// 注視点(画像座標)
		r.Target = hit + 1;							// ターゲット判定
		r.ContactTime = _ContactTime / 1000.;		// 目標コンタクト時間
		r.Feedback = fb;							// フィードバック
		r.TrainingLevel = _TrainingLevel;			// トレーニングレベル
		r.RR = Ect()->PBitalMonitor()->GetRR();		// バイタル出力(RR間隔)
		auto pd = Ect()->PEyeTrack()->GetPupilDiam();	// 瞳孔径
		r.PupilL = pd.l;
		r.PupilR = pd.r;
		r.H = Ect()->PMarker()->GetHomography();	// ホモグラフィ行列
		_DataLog.WriteRecord(r);
	}

	return true;
}

// キャリブレーションチェックの判定
bool Worker::IsCalibCheckOK() {
	if (_ErrorLog.size() == 0) return false;
	cv::Point2f sum(0, 0), error;
	for (auto& x : _ErrorLog) sum += x;
	error.x = sum.x / _ErrorLog.size();
	error.y = sum.y / _ErrorLog.size();
	_Error = sqrtf(error.x * error.x + error.y * error.y);
	nkc::wut::DebugPrintf(_T("Error x=%.1f, y=%.1f, total=%.1f (Tole=%.1f)\n"), 
		error.x, error.y, _Error, CALIB_ERROR_TOLERANCE);
	_ErrorLog.clear();
	return _Error <= CALIB_ERROR_TOLERANCE;
}

// 判定パラメータをリセット
void Worker::ResetParams() {
	_pContactTimer->Reset(); 
	_FBLevel = 1;
	_ContactTime = 0;
}

// イベント処理
bool Worker::EventProc(MSG& msg) {
	switch (msg.message) {

	case (int)ECTMSG::SOFTWARE_START: // ソフトウェア起動
		break;

	case (int)ECTMSG::SOFTWARE_END: // ソフトウェア終了
		this->EventLog(_T("Software End"));
		Ect()->StopApp();
		break;

	case (int)ECTMSG::CALIB_START: // キャリブレーション開始
		if (_AppStatus == APP_STATUS::IDLE) {
			_AppStatus = APP_STATUS::CALIB;
			Ect()->PStimulus()->PostMsg((int)ECTMSG::CALIB_START);
			Ect()->PTobiiREST()->PostMsg((int)ECTMSG::CALIB_START);
			this->EventLog(_T("Calibration Start"));
		}
		break;

	case (int)ECTMSG::CALIB_OK:	// キャリブレーション成功
		_DataLog.StartRecord(Ect()->Subject(), Ect()->Visit(), true);	// キャリブレーションログ開始
		this->ResetShift();	// キャリブレーションデータの初期化
		Ect()->PStimulus()->PostMsg((int)ECTMSG::SOFTCALIB_START);
		this->EventLog(_T("Soft Calibration Start"));
		break;

	case (int)ECTMSG::CALIB_FAILED:	// キャリブレーション失敗
	case (int)ECTMSG::CALIB_ERR:	// キャリブレーションエラー
		_AppStatus = APP_STATUS::IDLE;
		Ect()->PStimulus()->PostMsg((int)ECTMSG::CALIB_FAILED);
		{
			TCHAR buf[256];
			_stprintf_s(buf, 256, _T("Calibration Failed  Err=%.1f"), _Error);
			this->EventLog(buf);
		}
		break;

	case (int)ECTMSG::SOFTCALIB_OK:	// キャリブレーション成功
		_AppStatus = APP_STATUS::IDLE;
		Ect()->PStimulus()->PostMsg((int)ECTMSG::SOFTCALIB_OK);
		{
			TCHAR buf[256];
			_stprintf_s(buf, 256, _T("Calibration OK  Err=%.1f"), _Error);
			this->EventLog(buf);
		}
		break;

	case (int)ECTMSG::EXP_START: // 実験開始
		if (_AppStatus == APP_STATUS::IDLE) {
			_DataLog.StartRecord(Ect()->Subject(), Ect()->Visit());
			this->EventLog(_T("Experiment Start"));
			_pExpTimer->Reset();
			this->ResetParams();
			Ect()->PStimulus()->PostMsg((int)ECTMSG::EXP_START);
			_AppStatus = APP_STATUS::STIM;
		}
		break;

	case (int)ECTMSG::EXP_STOP: // 実験停止
		if (_AppStatus == APP_STATUS::STIM) {
			_AppStatus = APP_STATUS::IDLE;
			Ect()->PStimulus()->PostMsg((int)ECTMSG::EXP_STOP);
			this->EventLog(_T("Experiment Stopped"));
			this->LaunchResult();
		}
		break;

	case (int)ECTMSG::EXP_END: // 実験終了
		if (_AppStatus == APP_STATUS::STIM) {
			_AppStatus = APP_STATUS::IDLE;
			Ect()->PStimulus()->PostMsg((int)ECTMSG::EXP_END);
			this->EventLog(_T("Experiment Finished"));
			this->LaunchResult();
		}
		break;

	case (int)ECTMSG::EXP_NEXT: // 次の刺激に移動
		//this->EventLog(_T("Next stimulation"));
		this->ResetParams();
		break;

	case (int)ECTMSG::SYSTEM_ERROR: // システムエラー
		this->EventLog(_T("System Error"));
		_AppStatus = APP_STATUS::IDLE;
		Ect()->PStimulus()->PostMsg((int)ECTMSG::SYSTEM_ERROR);
		Ect()->PECTrainerGUI()->PostMsg((int)ECTMSG::MOVIE_STOP);
		break;

	case (int)ECTMSG::FB_OK: // フィードバック:OK
		PlaySound(SOUND_OK, NULL, SND_FILENAME | SND_ASYNC);
		//this->EventLog(_T("Feedback OK"));
		break;

	case (int)ECTMSG::FB_GOOD: // フィードバック:Good
		PlaySound(SOUND_GOOD, NULL, SND_FILENAME | SND_ASYNC);
		//this->EventLog(_T("Feedback Good"));
		break;

	case (int)ECTMSG::FB_NICE: // フィードバック:Nice
		PlaySound(SOUND_NICE, NULL, SND_FILENAME | SND_ASYNC);
		//this->EventLog(_T("Feedback Nice"));
		break;

	case (int)ECTMSG::FB_GREAT: // フィードバック:Great
		PlaySound(SOUND_GREAT, NULL, SND_FILENAME | SND_ASYNC);
		//this->EventLog(_T("Feedback Great"));
		break;

	case (int)ECTMSG::FB_EXCELLENT: // フィードバック:Excellent
		PlaySound(SOUND_EXCELLENT, NULL, SND_FILENAME | SND_ASYNC);
		//this->EventLog(_T("Feedback Excellent"));
		break;

	case (int)ECTMSG::FB_GOOUT: // フィードバック:視線外れ
		PlaySound(SOUND_GOOUT, NULL, SND_FILENAME | SND_ASYNC);
		break;
	}
	return true;
}

// イベントログ出力
void Worker::EventLog(const TCHAR* msg) {
	Logger::WriteEvent(msg);
}

// コンタクト累積時間を取得
double Worker::GetContactTime() {
	return _AppStatus == APP_STATUS::STIM ? _ContactTime : 0; 
}

// 実験経過時間を取得(msec)
double Worker::GetExpTime() {
	return _AppStatus == APP_STATUS::STIM ? _pExpTimer->Elapse() : 0; 
}

// FPS表示
void Worker::FPS(double fps) {
	nkc::wut::DebugPrintf(_T("[Worker] %.1f fps\n"), fps);
}

// 結果表示ソフト起動
void Worker::LaunchResult() {
	PROCESS_INFORMATION pi = { 0 };
	STARTUPINFO si = { sizeof(STARTUPINFO) };
	TCHAR cmd[1024];
	_stprintf_s(cmd, 1024, _T("EcomAnalysis.exe \"%s\""), _DataLog.Filename().c_str());
	if (CreateProcess(NULL, cmd,
		NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi)) {
		this->EventLog(_T("Launch analysis software OK\n"));
	} else {
		this->EventLog(_T("Launch analysis software failed\n"));
	}
	CloseHandle(pi.hProcess);
	CloseHandle(pi.hThread);
}