separate camera init from experiment init; dot size options

This commit is contained in:
Josh de Leeuw 2021-03-01 10:16:42 -05:00
parent 7e8b83825d
commit d330c4ad7d
8 changed files with 147 additions and 88 deletions

View File

@ -19,6 +19,7 @@ jsPsych.init({
Parameter | Type | Default Value | Description Parameter | Type | Default Value | Description
----------|------|---------------|------------ ----------|------|---------------|------------
webgazer | object | `undefined` | You can explicitly pass a reference to a loaded instance of the webgazer.js library. If no explicit reference is passed then the extension will look for a global `webgazer` object. If you are loading webgazer.js via a `<script>` tag you do not need to set this parameter in most circumstances. webgazer | object | `undefined` | You can explicitly pass a reference to a loaded instance of the webgazer.js library. If no explicit reference is passed then the extension will look for a global `webgazer` object. If you are loading webgazer.js via a `<script>` tag you do not need to set this parameter in most circumstances.
auto_initialize | bool | false | Whether to automatically initialize webgazer when the experiment begins. If set to `true` then the experiment will attempt to access the user's webcam immediately upon page load. The default value is `false` because it is probably a good idea to explain to the user why camera permission will be needed before asking for it. The `webgazer-init-camera` plugin can be used to initialize the camera during the experiment.
round_predictions | bool | true | Whether to round the `x`,`y` coordinates predicted by WebGazer to the nearest whole number. This *greatly* reduces the size of the data, as WebGazer records data to 15 decimal places by default. Given the noise of the system, there's really no need to record data to this level of precision. round_predictions | bool | true | Whether to round the `x`,`y` coordinates predicted by WebGazer to the nearest whole number. This *greatly* reduces the size of the data, as WebGazer records data to 15 decimal places by default. Given the noise of the system, there's really no need to record data to this level of precision.
### Trial Parameters ### Trial Parameters
@ -49,6 +50,14 @@ webgazer_targets | array | An array of objects contain the pixel coordinates of
In addition to the jsPsych webgazer-* plugins, the jsPsych webgazer extension provides a set of functions that allow the researcher to interact more directly with WebGazer. These functions can be called at any point during an experiment, and are crucial for building trial plugins that interact with WebGazer. All of the functions below must be prefixed with `jsPsych.extensions.webgazer` (e.g. `jsPsych.extensions.webgazer.faceDetected()`). In addition to the jsPsych webgazer-* plugins, the jsPsych webgazer extension provides a set of functions that allow the researcher to interact more directly with WebGazer. These functions can be called at any point during an experiment, and are crucial for building trial plugins that interact with WebGazer. All of the functions below must be prefixed with `jsPsych.extensions.webgazer` (e.g. `jsPsych.extensions.webgazer.faceDetected()`).
### start()
Performs initialization of webgazer, including requesting permissions from the user to access the camera. Returns a `Promise` that resolves when the camera is initialized and fails if the camera cannot be accessed, e.g., because the user denies permission. This is handled automatically if using the `webgazer-init-camera` plugin or setting `auto_initialize` to `true` in the extension parameters.
### isInitialized()
Returns `true` if `start()` has been successfully called at some point, and `false` otherwise.
### faceDetected() ### faceDetected()
Returns `true` if WebGazer is ready to make predictions (`webgazer.getTracker().predictionReady` is `true`). Returns `true` if WebGazer is ready to make predictions (`webgazer.getTracker().predictionReady` is `true`).

View File

@ -11,6 +11,7 @@ Parameter | Type | Default Value | Description
calibration_points | array | `[[10,10], [10,50], [10,90], [50,10], [50,50], [50,90], [90,10], [90,50], [90,90]]` | Array of points in `[x,y]` coordinates. Specified as a percentage of the screen width and height, from the left and top edge. The default grid is 9 points. calibration_points | array | `[[10,10], [10,50], [10,90], [50,10], [50,50], [50,90], [90,10], [90,50], [90,90]]` | Array of points in `[x,y]` coordinates. Specified as a percentage of the screen width and height, from the left and top edge. The default grid is 9 points.
calibration_mode | string | `'click'` | Can specify `click` to have subjects click on calibration points or `view` to have subjects passively watch calibration points. calibration_mode | string | `'click'` | Can specify `click` to have subjects click on calibration points or `view` to have subjects passively watch calibration points.
repetitions_per_point | numeric | 1 | The number of times to repeat the sequence of calibration points. repetitions_per_point | numeric | 1 | The number of times to repeat the sequence of calibration points.
point_size | numeric | 20 | Diameter of the calibration points in pixels.
randomize_calibration_order | bool | `false` | Whether to randomize the order of the calibration points. randomize_calibration_order | bool | `false` | Whether to randomize the order of the calibration points.
time_to_saccade | numeric | 1000 | If `calibration_mode` is set to `view`, then this is the delay before calibrating after showing a point. Gives the participant time to fixate on the new target before assuming that the participant is looking at the target. time_to_saccade | numeric | 1000 | If `calibration_mode` is set to `view`, then this is the delay before calibrating after showing a point. Gives the participant time to fixate on the new target before assuming that the participant is looking at the target.
time_per_point | numeric | 1000 | If `calibration_mode` is set to `view`, then this is the length of time to show a point while calibrating. Note that if `click` calibration is used then the point will remain on the screen until clicked. time_per_point | numeric | 1000 | If `calibration_mode` is set to `view`, then this is the length of time to show a point while calibrating. Note that if `click` calibration is used then the point will remain on the screen until clicked.

View File

@ -15,7 +15,7 @@ repetitions_per_point | numeric | 1 | The number of times to repeat the sequence
randomize_validation_order | bool | `false` | Whether to randomize the order of the validation points. randomize_validation_order | bool | `false` | Whether to randomize the order of the validation points.
time_to_saccade | numeric | 1000 | The delay before validating after showing a point. Gives the participant time to fixate on the new target before assuming that the participant is looking at the target. time_to_saccade | numeric | 1000 | The delay before validating after showing a point. Gives the participant time to fixate on the new target before assuming that the participant is looking at the target.
validation_duration | numeric | 2000 | If `calibration_mode` is set to `view`, then this is the length of time to show a point while calibrating. Note that if `click` calibration is used then the point will remain on the screen until clicked. validation_duration | numeric | 2000 | If `calibration_mode` is set to `view`, then this is the length of time to show a point while calibrating. Note that if `click` calibration is used then the point will remain on the screen until clicked.
point_size | numeric | 10 | Diameter of the validation points in pixels. point_size | numeric | 20 | Diameter of the validation points in pixels.
show_validation_data | bool | false | If `true` then a visualization of the validation data will be shown on the screen after the validation is complete. This will show each measured gaze location color coded by whether it is within the `roi_radius` of the target point. This is mainly intended for testing and debugging. show_validation_data | bool | false | If `true` then a visualization of the validation data will be shown on the screen after the validation is complete. This will show each measured gaze location color coded by whether it is within the `roi_radius` of the target point. This is mainly intended for testing and debugging.
## Data Generated ## Data Generated

View File

@ -20,6 +20,18 @@
<script> <script>
var camera_instructions = {
type: 'html-button-response',
stimulus: `
<p>This experiment uses your camera for eye tracking.</p>
<p>In order to participate you must allow the experiment to use your camera.</p>
<p>You will be prompted to do this on the next screen.</p>
<p>If you do not want to permit the experiment to use your camera, please close the page.</p>
`,
choices: ['Click to begin'],
post_trial_gap: 1000
}
var init_camera = { var init_camera = {
type: 'webgazer-init-camera' type: 'webgazer-init-camera'
} }
@ -142,6 +154,7 @@
} }
var timeline = []; var timeline = [];
timeline.push(camera_instructions);
timeline.push(init_camera); timeline.push(init_camera);
timeline.push(calibration_instructions); timeline.push(calibration_instructions);
timeline.push(calibration); timeline.push(calibration);

View File

@ -11,7 +11,11 @@ jsPsych.extensions['webgazer'] = (function () {
// required, will be called at jsPsych.init // required, will be called at jsPsych.init
// should return a Promise // should return a Promise
extension.initialize = function (params) { extension.initialize = function (params) {
return new Promise(function(resolve, reject){ // setting default values for params if not defined
params.round_predictions = typeof params.round_predictions === 'undefined' ? true : params.round_predictions;
params.auto_initialize = typeof params.auto_initialize === 'undefined' ? false : params.auto_initialize;
return new Promise(function (resolve, reject) {
if (typeof params.webgazer === 'undefined') { if (typeof params.webgazer === 'undefined') {
if (window.webgazer) { if (window.webgazer) {
state.webgazer = window.webgazer; state.webgazer = window.webgazer;
@ -22,28 +26,34 @@ jsPsych.extensions['webgazer'] = (function () {
state.webgazer = params.webgazer; state.webgazer = params.webgazer;
} }
if (typeof params.round_predictions === 'undefined'){
state.round_predictions = true;
} else {
state.round_predictions = params.round_predictions;
}
// sets up event handler for webgazer data // sets up event handler for webgazer data
state.webgazer.setGazeListener(handleGazeDataUpdate); state.webgazer.setGazeListener(handleGazeDataUpdate);
// starts webgazer, and once it initializes we stop mouseCalibration and // sets state for initialization
// pause webgazer data. state.initialized = false;
state.webgazer.begin().then(function () { state.activeTrial = false;
extension.stopMouseCalibration();
extension.pause();
resolve();
})
// hide video by default // hide video by default
extension.hideVideo(); extension.hideVideo();
// hide predictions by default // hide predictions by default
extension.hidePredictions(); extension.hidePredictions();
if (params.auto_initialize) {
// starts webgazer, and once it initializes we stop mouseCalibration and
// pause webgazer data.
state.webgazer.begin().then(function () {
state.initialized = true;
extension.stopMouseCalibration();
extension.pause();
resolve();
}).catch(function (error) {
console.error(error);
reject(error);
});
} else {
resolve();
}
}) })
} }
@ -66,11 +76,11 @@ jsPsych.extensions['webgazer'] = (function () {
state.activeTrial = true; state.activeTrial = true;
// record bounding box of any elements in params.targets // record bounding box of any elements in params.targets
if(typeof params !== 'undefined'){ if (typeof params !== 'undefined') {
if(typeof params.targets !== 'undefined'){ if (typeof params.targets !== 'undefined') {
for(var i=0; i<params.targets.length; i++){ for (var i = 0; i < params.targets.length; i++) {
var target = document.querySelector(params.targets[i]); var target = document.querySelector(params.targets[i]);
if(target !== null){ if (target !== null) {
var bounding_rect = target.getBoundingClientRect(); var bounding_rect = target.getBoundingClientRect();
state.currentTrialTargets.push({ state.currentTrialTargets.push({
selector: params.targets[i], selector: params.targets[i],
@ -101,6 +111,24 @@ jsPsych.extensions['webgazer'] = (function () {
} }
} }
extension.start = function () {
return new Promise(function (resolve, reject) {
state.webgazer.begin().then(function () {
state.initialized = true;
extension.stopMouseCalibration();
extension.pause();
resolve();
}).catch(function (error) {
console.error(error);
reject(error);
});
});
}
extension.isInitialized = function(){
return state.initialized;
}
extension.faceDetected = function () { extension.faceDetected = function () {
return state.webgazer.getTracker().predictionReady; return state.webgazer.getTracker().predictionReady;
} }
@ -156,7 +184,7 @@ jsPsych.extensions['webgazer'] = (function () {
extension.getCurrentPrediction = async function () { extension.getCurrentPrediction = async function () {
var prediction = await state.webgazer.getCurrentPrediction(); var prediction = await state.webgazer.getCurrentPrediction();
if(state.round_predictions){ if (state.round_predictions) {
prediction.x = Math.round(prediction.x); prediction.x = Math.round(prediction.x);
prediction.y = Math.round(prediction.y); prediction.y = Math.round(prediction.y);
} }

View File

@ -19,6 +19,10 @@ jsPsych.plugins["webgazer-calibrate"] = (function() {
type: jsPsych.plugins.parameterType.STRING, type: jsPsych.plugins.parameterType.STRING,
default: 'click', // options: 'click', 'view' default: 'click', // options: 'click', 'view'
}, },
point_size:{
type: jsPsych.plugins.parameterType.INT,
default: 20
},
repetitions_per_point: { repetitions_per_point: {
type: jsPsych.plugins.parameterType.INT, type: jsPsych.plugins.parameterType.INT,
default: 1 default: 1
@ -38,12 +42,6 @@ jsPsych.plugins["webgazer-calibrate"] = (function() {
} }
} }
// 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) { plugin.trial = function(display_element, trial) {
var html = ` var html = `
@ -62,7 +60,6 @@ jsPsych.plugins["webgazer-calibrate"] = (function() {
calibrate(); calibrate();
function calibrate(){ function calibrate(){
jsPsych.extensions['webgazer'].resume(); jsPsych.extensions['webgazer'].resume();
if(trial.calibration_mode == 'click'){ if(trial.calibration_mode == 'click'){
@ -97,7 +94,7 @@ jsPsych.plugins["webgazer-calibrate"] = (function() {
} }
function calibration_display_gaze_only(pt){ function calibration_display_gaze_only(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>' var pt_html = `<div id="calibration-point" style="width:${trial.point_size}px; height:${trial.point_size}px; border-radius:${trial.point_size}px; border: 1px solid #000; background-color: #333; position: absolute; left:${pt[0]}%; top:${pt[1]}%;"></div>`
wg_container.innerHTML = pt_html; wg_container.innerHTML = pt_html;
var pt_dom = wg_container.querySelector('#calibration-point'); var pt_dom = wg_container.querySelector('#calibration-point');

View File

@ -3,43 +3,53 @@
* Josh de Leeuw * Josh de Leeuw
**/ **/
jsPsych.plugins["webgazer-init-camera"] = (function() { jsPsych.plugins["webgazer-init-camera"] = (function () {
var plugin = {}; var plugin = {};
plugin.info = { plugin.info = {
name: 'webgazer-init-camera', name: 'webgazer-init-camera',
description: '', description: '',
parameters: { parameters: {
instructions: { instructions: {
type: jsPsych.plugins.parameterType.HTML_STRING, type: jsPsych.plugins.parameterType.HTML_STRING,
default: ` default: `
<p>Position your head so that the webcam has a good view of your eyes.</p> <p>Position your head so that the webcam has a good view of your eyes.</p>
<p>Use the video in the upper-left corner as a guide. Center your face in the box and look directly towards the camera.</p> <p>Use the video in the upper-left corner as a guide. Center your face in the box and look directly towards the camera.</p>
<p>It is important that you try and keep your head reasonably still throughout the experiment, so please take a moment to adjust your setup as needed.</p> <p>It is important that you try and keep your head reasonably still throughout the experiment, so please take a moment to adjust your setup as needed.</p>
<p>When your face is centered in the box and the box turns green, you can click to continue.</p>` <p>When your face is centered in the box and the box turns green, you can click to continue.</p>`
}, },
button_text: { button_text: {
type: jsPsych.plugins.parameterType.STRING, type: jsPsych.plugins.parameterType.STRING,
default: 'Continue' default: 'Continue'
}
} }
} }
}
plugin.trial = function(display_element, trial) {
plugin.trial = function (display_element, trial) {
if (!jsPsych.extensions.webgazer.isInitialized()) {
jsPsych.extensions.webgazer.start().then(function () {
showTrial();
}).catch(function () {
display_element.innerHTML = `<p>The experiment cannot continue because the eye tracker failed to start.</p>
<p>This may be because of a technical problem or because you did not grant permission for the page to use your camera.</p>`
});
}
function showTrial() {
var html = ` var html = `
<div id='webgazer-init-container' style='position: relative; width:100vw; height:100vh'> <div id='webgazer-init-container' style='position: relative; width:100vw; height:100vh'>
</div>` </div>`
display_element.innerHTML = html; display_element.innerHTML = html;
jsPsych.extensions['webgazer'].showVideo(); jsPsych.extensions['webgazer'].showVideo();
jsPsych.extensions['webgazer'].resume(); jsPsych.extensions['webgazer'].resume();
var wg_container = display_element.querySelector('#webgazer-init-container'); var wg_container = display_element.querySelector('#webgazer-init-container');
wg_container.innerHTML = ` wg_container.innerHTML = `
<div style='position: absolute; top: 50%; left: calc(50% - 350px); transform: translateY(-50%); width:700px;'> <div style='position: absolute; top: 50%; left: calc(50% - 350px); transform: translateY(-50%); width:700px;'>
${trial.instructions} ${trial.instructions}
@ -52,44 +62,45 @@ jsPsych.plugins["webgazer-init-camera"] = (function() {
attributeFilter: ['style'], attributeFilter: ['style'],
subtree: true subtree: true
}); });
document.querySelector('#jspsych-wg-cont').addEventListener('click', function(){ document.querySelector('#jspsych-wg-cont').addEventListener('click', function () {
observer.disconnect(); observer.disconnect();
end_trial(); end_trial();
}); });
}
function face_detect_event_observer(mutationsList, observer){ function face_detect_event_observer(mutationsList, observer) {
if(mutationsList[0].target == document.querySelector('#webgazerFaceFeedbackBox')){ if (mutationsList[0].target == document.querySelector('#webgazerFaceFeedbackBox')) {
if(mutationsList[0].type == 'attributes' && mutationsList[0].target.style.borderColor == "green"){ if (mutationsList[0].type == 'attributes' && mutationsList[0].target.style.borderColor == "green") {
document.querySelector('#jspsych-wg-cont').disabled = false; document.querySelector('#jspsych-wg-cont').disabled = false;
} }
if(mutationsList[0].type == 'attributes' && mutationsList[0].target.style.borderColor == "red"){ if (mutationsList[0].type == 'attributes' && mutationsList[0].target.style.borderColor == "red") {
document.querySelector('#jspsych-wg-cont').disabled = true; document.querySelector('#jspsych-wg-cont').disabled = true;
}
} }
} }
}
// function to end trial when it is time
function end_trial() { // function to end trial when it is time
jsPsych.extensions['webgazer'].pause(); function end_trial() {
jsPsych.extensions['webgazer'].hideVideo(); jsPsych.extensions['webgazer'].pause();
jsPsych.extensions['webgazer'].hideVideo();
// kill any remaining setTimeout handlers
jsPsych.pluginAPI.clearAllTimeouts(); // kill any remaining setTimeout handlers
jsPsych.pluginAPI.clearAllTimeouts();
// gather the data to store for the trial
var trial_data = { // 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);
}; };
// clear the display
display_element.innerHTML = '';
// move on to the next trial
jsPsych.finishTrial(trial_data);
}; };
return plugin; };
})();
return plugin;
})();

View File

@ -37,7 +37,7 @@ jsPsych.plugins["webgazer-validate"] = (function() {
}, },
point_size:{ point_size:{
type: jsPsych.plugins.parameterType.INT, type: jsPsych.plugins.parameterType.INT,
default: 10 default: 20
}, },
show_validation_data: { show_validation_data: {
type: jsPsych.plugins.parameterType.BOOL, type: jsPsych.plugins.parameterType.BOOL,