calibration + validation sort of working...

This commit is contained in:
Josh de Leeuw 2021-01-12 17:52:41 -05:00
parent cf9fd335bb
commit b146037af3
4 changed files with 302 additions and 30 deletions

View File

@ -5,9 +5,13 @@
<script src="../jspsych.js"></script>
<script src="../plugins/jspsych-html-keyboard-response.js"></script>
<script src="../plugins/jspsych-webgazer-calibrate.js"></script>
<script src="../plugins/jspsych-webgazer-validate.js"></script>
<script src="js/webgazer.js"></script>
<script src="../extensions/jspsych-ext-webgazer.js"></script>
<link rel="stylesheet" href="../css/jspsych.css">
<style>
.jspsych-content { max-width: 100%;}
</style>
</head>
<body></body>
@ -15,7 +19,16 @@
<script>
var calibration = {
type: 'webgazer-calibrate'
type: 'webgazer-calibrate',
//calibration_points: [[10,10], [10,90], [90,10], [90,90]],
randomize_calibration_order: true,
time_per_point: 1000,
time_to_saccade: 1000
}
var validation = {
type: 'webgazer-validate',
validation_points: [[25,25], [25,75], [75,25], [75,75]]
}
var fixation = {
@ -60,6 +73,7 @@
var timeline = [];
timeline.push(calibration);
timeline.push(validation);
timeline.push(trial_proc);
jsPsych.init({

View File

@ -23,8 +23,10 @@ jsPsych.extensions['webgazer'] = (function () {
// sets up event handler for webgazer data
state.webgazer.setGazeListener(handleGazeDataUpdate);
// starts webgazer
state.webgazer.begin();
// starts webgazer, and once it initializes we stop mouseEvents by default
state.webgazer.begin().then(function(){
extension.stopMouseCalibration();
})
// hide video by default
extension.hideVideo();
@ -104,6 +106,18 @@ jsPsych.extensions['webgazer'] = (function () {
state.webgazer.pause();
}
extension.stopMouseCalibration = function(){
state.webgazer.removeMouseEventListeners()
}
extension.startMouseCalibration = function(){
state.webgazer.addMouseEventListeners()
}
extension.calibratePoint = function(x,y){
state.webgazer.recordScreenPosition(x,y,'click');
}
extension.setRegressionType = function(regression_type){
var valid_regression_models = ['ridge', 'weigthedRidge', 'threadedRidge'];
if(valid_regression_models.includes(regression_type)){
@ -113,6 +127,14 @@ jsPsych.extensions['webgazer'] = (function () {
}
}
extension.getCurrentPrediction = function(){
return state.webgazer.getCurrentPrediction();
}
// extension.addGazeDataUpdateListener(listener){
// state.webgazer.setGazeListener(listener);
// }
function handleGazeDataUpdate(gazeData, elapsedTime){
if(gazeData !== null && state.activeTrial){
var d = {

View File

@ -23,9 +23,13 @@ jsPsych.plugins["webgazer-calibrate"] = (function() {
type: jsPsych.plugins.parameterType.BOOL,
default: false
},
color_click_mode: {
type: jsPsych.plugins.parameterType.BOOL,
default: false
time_to_saccade: {
type: jsPsych.plugins.parameterType.INT,
default: 1000
},
time_per_point: {
type: jsPsych.plugins.parameterType.STRING,
default: 2000
}
}
}
@ -104,8 +108,8 @@ jsPsych.plugins["webgazer-calibrate"] = (function() {
jsPsych.extensions['webgazer'].hideVideo();
wg_container.innerHTML = "<div style='position: absolute; top: 50%; left: calc(50% - 350px); transform: translateY(-50%); width:700px;'>"+
"<p>Great! Now the eye tracker will be calibrated to translate the image of your eyes from the webcam to a location on your screen.</p>"+
"<p>To do this, you need to look at a series of dots and click on them with your mouse. Make sure to look where you are clicking. Each click teaches the eye tracker how to map the image of your eyes onto a location on the page.</p>"+
"<p>Please click each point 5 times.</p>"+
"<p>To do this, you need to look at a series of dots.</p>"+
"<p>Keep your head still, and focus on each dot as quickly as possible. Keep your gaze fixed on the dot for as long as it is on the screen.</p>"+
"<button id='begin-calibrate-btn' class='jspsych-btn'>Click to begin.</button>"+
"</div>"
document.querySelector('#begin-calibrate-btn').addEventListener('click', function(){
@ -113,9 +117,8 @@ jsPsych.plugins["webgazer-calibrate"] = (function() {
});
}
var points_completed = 0;
var points_completed = -1;
var cal_points = null;
var clicks = 0;
function calibrate(){
if(trial.randomize_calibration_order){
@ -123,33 +126,52 @@ jsPsych.plugins["webgazer-calibrate"] = (function() {
} else {
cal_points = trial.calibration_points;
}
points_completed = 0;
points_completed = -1;
jsPsych.extensions['webgazer'].resume();
next_calibration_point();
}
function next_calibration_point(){
clicks = 0;
var pt = cal_points[points_completed];
var pt_html = '<div id="calibration-point" style="width:20px; height:20px; border-radius:10px; border: 2px solid #f00; background-color: #333; position: absolute; left:'+pt[0]+'%; top:'+pt[1]+'%;"></div>'
points_completed++;
if(points_completed == cal_points.length){
calibration_done();
} else {
var pt = cal_points[points_completed];
calibration_display_color_change(pt);
}
}
function calibration_display_color_change(pt){
var pt_html = '<div id="calibration-point" style="width:10px; height:10px; border-radius:10px; border: 1px solid #000; background-color: #333; position: absolute; left:'+pt[0]+'%; top:'+pt[1]+'%;"></div>'
wg_container.innerHTML = pt_html;
jsPsych.pluginAPI.setTimeout(function(){
//wg_container.querySelector('#calibration-point').style.
var pt_dom = wg_container.querySelector('#calibration-point');
var br = pt_dom.getBoundingClientRect();
var x = br.left + br.width / 2;
var y = br.top + br.height / 2;
var pt_start_cal = performance.now() + trial.time_to_saccade;
var pt_finish = performance.now() + trial.time_to_saccade + trial.time_per_point;
requestAnimationFrame(function watch_dot(){
if(performance.now() > pt_start_cal){
jsPsych.extensions['webgazer'].calibratePoint(x,y,'click');
}
if(performance.now() < pt_finish){
requestAnimationFrame(watch_dot);
} else {
next_calibration_point();
}
})
wg_container.querySelector('#calibration-point').addEventListener('click', function(){
clicks++;
wg_container.querySelector('#calibration-point').style.opacity = `${100 - clicks*(80/trial.clicks_per_point)}%`
if(clicks >= trial.clicks_per_point){
points_completed++;
if(points_completed < trial.calibration_points.length){
next_calibration_point();
} else {
calibration_done();
}
}
});
// jsPsych.pluginAPI.setTimeout(function(){
// pt_dom.style.backgroundColor = "#fff";
// pt_dom.addEventListener('click', function(){
// next_calibration_point();
// });
// }, Math.random()*(trial.maximum_dot_change_delay-trial.minimum_dot_change_delay)+trial.minimum_dot_change_delay);
}
@ -158,8 +180,7 @@ jsPsych.plugins["webgazer-calibrate"] = (function() {
jsPsych.extensions['webgazer'].showPredictions();
setTimeout(end_trial, 4000);
}
// function to end trial when it is time
function end_trial() {
jsPsych.extensions['webgazer'].pause();

View File

@ -0,0 +1,215 @@
/**
* jspsych-webgazer-validate
* Josh de Leeuw
**/
jsPsych.plugins["webgazer-validate"] = (function() {
var plugin = {};
plugin.info = {
name: 'webgazer-validate',
description: '',
parameters: {
validation_points: {
type: jsPsych.plugins.parameterType.INT,
default: [[10,10], [10,50], [10,90], [50,10], [50,50], [50,90], [90,10], [90,50], [90,90]]
},
randomize_validation_order: {
type: jsPsych.plugins.parameterType.BOOL,
default: false
},
validation_duration: {
type: jsPsych.plugins.parameterType.INT,
default: 2000
}
}
}
// provide options for calibration routines?
// dot clicks?
// track a dot with mouse?
// then a validation phase of staring at the dot in different locations?
plugin.trial = function(display_element, trial) {
var trial_data = {}
trial_data.rawGaze = [];
var html = `
<div id='webgazer-validate-container' style='position: relative; width:100vw; height:100vh; overflow: hidden;'>
</div>`
display_element.innerHTML = html;
var wg_container = display_element.querySelector('#webgazer-validate-container');
show_begin_validate_message();
function show_begin_validate_message(){
wg_container.innerHTML = `<div style='position: absolute; top: 50%; left: calc(50% - 350px); transform: translateY(-50%); width:700px;'>
<p>Let's see how accurate the eye tracking is. </p>
<p>Keep your head still, and move your eyes to focus on each dot as it appears.</p>
<button id='begin-validate-btn' class='jspsych-btn'>Click to begin.</button>
</div>`
document.querySelector('#begin-validate-btn').addEventListener('click', function(){
validate();
});
}
var points_completed = -1;
var val_points = null;
function validate(){
if(trial.randomize_validation_order){
val_points = jsPsych.randomization.shuffle(trial.validation_points);
} else {
val_points = trial.validation_points;
}
points_completed = -1;
jsPsych.extensions['webgazer'].resume();
next_validation_point();
}
function next_validation_point(){
points_completed++;
if(points_completed == val_points.length){
validation_done();
} else {
var pt = val_points[points_completed];
validation_display(pt);
}
}
function validation_display(pt){
var pt_html = '<div id="validation-point" style="width:10px; height:10px; border-radius:10px; border: 1px solid #000; background-color: #333; position: absolute; left:'+pt[0]+'%; top:'+pt[1]+'%;"></div>'
wg_container.innerHTML = pt_html;
var pt_dom = wg_container.querySelector('#validation-point');
var br = pt_dom.getBoundingClientRect();
var x = br.left + br.width / 2;
var y = br.top + br.height / 2;
var pt_start_val = performance.now() + 1000;
var pt_finish = performance.now() + 3000;
var pt_data = [];
requestAnimationFrame(function watch_dot(){
if(performance.now() > pt_start_val){
jsPsych.extensions['webgazer'].getCurrentPrediction().then(function(prediction){
pt_data.push({dx: prediction.x - x, dy: prediction.y - y});
});
}
if(performance.now() < pt_finish){
requestAnimationFrame(watch_dot);
} else {
trial_data.rawGaze.push(pt_data);
next_validation_point();
}
})
// jsPsych.pluginAPI.setTimeout(function(){
// pt_dom.style.backgroundColor = "#fff";
// pt_dom.addEventListener('click', function(){
// next_calibration_point();
// });
// }, Math.random()*(trial.maximum_dot_change_delay-trial.minimum_dot_change_delay)+trial.minimum_dot_change_delay);
}
function drawValidationPoint(x,y){
return '<div class="validation-point" style="width:10px; height:10px; border-radius:10px; border: 1px solid #000; background-color: #333; position: absolute; left:'+x+'%; top:'+y+'%;"></div>'
}
function drawCircle(target_x, target_y, dx, dy,r){
var html = `
<div class="validation-centroid" style="width:${r*2}px; height:${r*2}px; border: 2px solid red; border-radius: ${r}px; background-color: transparent; position: absolute; left:calc(${target_x}% + ${dx}px); top:calc(${target_y}% + ${dy}px);"></div>
`
return html;
}
function drawRawDataPoint(target_x, target_y, dx, dy){
return `<div class="raw-data-point" style="width:5px; height:5px; border-radius:5px; border: 1px solid #f00; background-color: #faa; opacity:0.2; position: absolute; left:calc(${target_x}% + ${dx}px); top:calc(${target_y}% + ${dy}px);"></div>`
}
function median(arr){
var mid = Math.floor(arr.length/2);
var sorted_arr = arr.sort((a,b) => a-b);
if(arr.length % 2 == 0){
return sorted_arr[mid-1] + sorted_arr[mid] / 2;
} else {
return sorted_arr[mid];
}
}
function calculateGazeCentroid(gazeData){
var x_diff_m = gazeData.reduce(function(accumulator, currentValue, index){
accumulator += currentValue.dx;
if(index == gazeData.length-1){
return accumulator / gazeData.length;
} else {
return accumulator;
}
}, 0);
var y_diff_m = gazeData.reduce(function(accumulator, currentValue, index){
accumulator += currentValue.dy;
if(index == gazeData.length-1){
return accumulator / gazeData.length;
} else {
return accumulator;
}
}, 0);
var median_distance = median(gazeData.map(function(x){ return(Math.sqrt(Math.pow(x.dx-x_diff_m,2) + Math.pow(x.dy-y_diff_m,2)))}));
return {
x: x_diff_m,
y: y_diff_m,
r: median_distance
}
}
function validation_done(){
var html = '';
for(var i=0; i<trial.validation_points.length; i++){
html += drawValidationPoint(trial.validation_points[i][0], trial.validation_points[i][1]);
var gc = calculateGazeCentroid(trial_data.rawGaze[i]);
html += drawCircle(trial.validation_points[i][0], trial.validation_points[i][1], gc.x, gc.y, gc.r);
for(var j=0; j<trial_data.rawGaze[i].length; j++){
html += drawRawDataPoint(trial.validation_points[i][0], trial.validation_points[i][1], trial_data.rawGaze[i][j].dx, trial_data.rawGaze[i][j].dy)
}
}
wg_container.innerHTML = html;
}
// function to end trial when it is time
function end_trial() {
jsPsych.extensions['webgazer'].pause();
jsPsych.extensions['webgazer'].hidePredictions();
jsPsych.extensions['webgazer'].hideVideo();
// kill any remaining setTimeout handlers
jsPsych.pluginAPI.clearAllTimeouts();
// gather the data to store for the trial
var trial_data = {
};
// clear the display
display_element.innerHTML = '';
// move on to the next trial
jsPsych.finishTrial(trial_data);
};
};
return plugin;
})();