diff --git a/README.md b/README.md index d8ce45c..fe79e73 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This is a collection of scripts that allows acquiring training data for DeepMRey ## How to use 1) Install [MATLAB](https://matlab.mathworks.com) and [Psychtoolbox](http://psychtoolbox.org) 2) Clone this repository -``` +```bash git clone https://github.com/DeepMReye/Calibration.git cd Calibration ``` diff --git a/Start_deepMReye_calib.m b/Start_deepMReye_calib.m index fc512d3..87870ad 100644 --- a/Start_deepMReye_calib.m +++ b/Start_deepMReye_calib.m @@ -1,101 +1,106 @@ -function Start_deepMReye_calib(subjID, run) -% ------ Calibration script to acquire training data for DeepMReye ------ % - -% This script requires Psychtoolbox: http://psychtoolbox.org -% To run it, paste "Start_deepMReye_calib('test', 1)" into the command -% window and click enter. Screen and task settings can be changed below. -% Last update: MN, September 2021 - -% ------------------------- This is a template -------------------------- % - -% The type of training data you need depends on your study objectives. -% Please adjust this script accordingly. See the user documentation for -% details: https://deepmreye.slite.com/p/channel/MUgmvViEbaATSrqt3susLZ - -% ---------------------------- 3 conditions ----------------------------- % - -% By default, the script loops through following conditions -% 1) Fixations: The fixation cross will jump to various screen locations. -% 2) Smooth pursuit: The fixation cross will move smoothly at various speeds -% on a random-walk trajectory with defined directional sampling. -% 3) Free viewing of images (only if camera-based eye tracking is available). - -% ----------------------------- References ------------------------------ % - -% If you use parts of this script please cite the following articles. - -% 1) Frey* M., Nau* M., Doeller C.F. (2021). Magnetic resonance-based eye -% tracking using deep neural networks. Nature Neuroscience. -% 2) Kleiner M., Brainard D., Pelli D. (2007). What's new in Psychtoolbox-3. -% Perception, 36, 1. - -% ----------------------------------------------------------------------- % - -% clean up -close all; clearvars -except subjID run; clc -Screen('Preference', 'SkipSyncTests', 1); % for debugging only - -% set state of random number generator -rng('shuffle'); - -% participant info for logging -settings.subjID = subjID; -settings.run = run; -settings.root = '/Users/naum2/Desktop/Calibration-main'; % path to this folder - -% screen settings -settings.scr.scrID = max(Screen('Screens')); % select screen -settings.scr.width = 3440; % screen width in pixel -settings.scr.width_cm = 88; % screen width in cm -settings.scr.height = 1440; % screen height in pixel -settings.scr.height_cm = 40; % screen height in cm -settings.scr.hz = 60; % screen refresh rate in hertz -settings.scr.eye2scr = 65.5; % eye to screen distance in cm -settings.scr.txtsz = 44; % font size for task instructions (whatever looks good on your setup) - -% compute units (e.g. degree visual angle) -settings.units = getUnits(settings); - -% eye tracking (Eyelink) -settings.eyeTracking = 0; % Eyelink running? yes = 1, no = 0 - -% fixation cross settings -settings.crossSz = ceil(settings.scr.width/85); % size of the fixation cross in pixel -settings.crossVert = [0 0 0 settings.crossSz]; % vertical line -settings.crossHorz = [0 0 settings.crossSz 0]; % horizontal line - -% condition 1: fixation task -settings.fixtask.win_sz = [settings.scr.height, settings.scr.height]./1.25; % sample fixations within this window -settings.fixtask.n_locs = [10 10]; % n fixation locations [horizontal, vertical] -settings.fixtask.dur = 1.5; % fixation duration in s -settings = getFixLocations(settings); - -% condition 2: smooth pursuit task (pseudo-random walk) -settings.pursuit.win_sz = settings.fixtask.win_sz; -settings.pursuit.angles = deg2rad(0:15:359); % tested directions -settings.pursuit.mov_amp = [4 6 8]; % movement amplitudes in visual angle -settings.pursuit.dur = 1.5; % movement duration before changing direction -valid = 0; while ~valid - [settings, valid] = getFixLocations_pursuit(settings, valid); end % smooth pursuit task - -% condition 3: free picture viewing task (requires camera eye tracker) -settings.picTask.path2pics = fullfile(settings.root, 'images'); % path to your own images -settings.picTask.n_pics = 10; % how many of the pictures in the folder should be shown (random selection)? -settings.picTask.dur = 3; % duration of each image in seconds - -% other settings -settings.colors = getColors; % define colors -settings.waitSecsAfterRun = 5; % wait a few seconds before closing (>15s in case HRF should be captured) - -% start stimulus -[logs, settings] = deepMReye_calib(settings); - -% save logs -logs{1}.settings = settings; -logs{1}.run = run; -logs{1}.subjID = subjID; -saveLogs(logs) - -% stop key press monitoring -KbQueueFlush; KbQueueStop; -end \ No newline at end of file +function Start_deepMReye_calib(subjID, run) + % ------ Calibration script to acquire training data for DeepMReye ------ % + + % This script requires Psychtoolbox: http://psychtoolbox.org + % To run it, paste "Start_deepMReye_calib('test', 1)" into the command + % window and click enter. Screen and task settings can be changed below. + % Last update: MN, September 2021 + + % ------------------------- This is a template -------------------------- % + + % The type of training data you need depends on your study objectives. + % Please adjust this script accordingly. See the user documentation for + % details: https://deepmreye.slite.com/p/channel/MUgmvViEbaATSrqt3susLZ + + % ---------------------------- 3 conditions ----------------------------- % + + % By default, the script loops through following conditions + % 1) Fixations: The fixation cross will jump to various screen locations. + % 2) Smooth pursuit: The fixation cross will move smoothly at various speeds + % on a random-walk trajectory with defined directional sampling. + % 3) Free viewing of images (only if camera-based eye tracking is available). + + % ----------------------------- References ------------------------------ % + + % If you use parts of this script please cite the following articles. + + % 1) Frey* M., Nau* M., Doeller C.F. (2021). Magnetic resonance-based eye + % tracking using deep neural networks. Nature Neuroscience. + % 2) Kleiner M., Brainard D., Pelli D. (2007). What's new in Psychtoolbox-3. + % Perception, 36, 1. + + % ----------------------------------------------------------------------- % + + % clean up + close all; + clearvars -except subjID run; + clc; + Screen('Preference', 'SkipSyncTests', 1); % for debugging only + + % set state of random number generator + rng('shuffle'); + + % participant info for logging + settings.subjID = subjID; + settings.run = run; + settings.root = '/Users/naum2/Desktop/Calibration-main'; % path to this folder + + % screen settings + settings.scr.scrID = max(Screen('Screens')); % select screen + settings.scr.width = 3440; % screen width in pixel + settings.scr.width_cm = 88; % screen width in cm + settings.scr.height = 1440; % screen height in pixel + settings.scr.height_cm = 40; % screen height in cm + settings.scr.hz = 60; % screen refresh rate in hertz + settings.scr.eye2scr = 65.5; % eye to screen distance in cm + settings.scr.txtsz = 44; % font size for task instructions (whatever looks good on your setup) + + % compute units (e.g. degree visual angle) + settings.units = getUnits(settings); + + % eye tracking (Eyelink) + settings.eyeTracking = 0; % Eyelink running? yes = 1, no = 0 + + % fixation cross settings + settings.crossSz = ceil(settings.scr.width / 85); % size of the fixation cross in pixel + settings.crossVert = [0 0 0 settings.crossSz]; % vertical line + settings.crossHorz = [0 0 settings.crossSz 0]; % horizontal line + + % condition 1: fixation task + settings.fixtask.win_sz = [settings.scr.height, settings.scr.height] ./ 1.25; % sample fixations within this window + settings.fixtask.n_locs = [10 10]; % n fixation locations [horizontal, vertical] + settings.fixtask.dur = 1.5; % fixation duration in s + settings = getFixLocations(settings); + + % condition 2: smooth pursuit task (pseudo-random walk) + settings.pursuit.win_sz = settings.fixtask.win_sz; + settings.pursuit.angles = deg2rad(0:15:359); % tested directions + settings.pursuit.mov_amp = [4 6 8]; % movement amplitudes in visual angle + settings.pursuit.dur = 1.5; % movement duration before changing direction + valid = 0; + while ~valid + [settings, valid] = getFixLocations_pursuit(settings, valid); + end % smooth pursuit task + + % condition 3: free picture viewing task (requires camera eye tracker) + settings.picTask.path2pics = fullfile(settings.root, 'images'); % path to your own images + settings.picTask.n_pics = 10; % how many of the pictures in the folder should be shown (random selection)? + settings.picTask.dur = 3; % duration of each image in seconds + + % other settings + settings.colors = getColors; % define colors + settings.waitSecsAfterRun = 5; % wait a few seconds before closing (>15s in case HRF should be captured) + + % start stimulus + [logs, settings] = deepMReye_calib(settings); + + % save logs + logs{1}.settings = settings; + logs{1}.run = run; + logs{1}.subjID = subjID; + saveLogs(logs); + + % stop key press monitoring + KbQueueFlush; + KbQueueStop; +end diff --git a/deepMReye_calib.m b/deepMReye_calib.m index 50eb9a3..81c7b6a 100644 --- a/deepMReye_calib.m +++ b/deepMReye_calib.m @@ -1,144 +1,155 @@ -function [logs, settings] = deepMReye_calib(settings) -% ======================================================================= % -% Main stimulus script % -% ======================================================================= % -% Presents following tasks: fixation, smooth pursuit, picture viewing -% MN, September 2021 - -% ------------------ Open ptb-window and initiate task ------------------ % -% PsychDebugWindowConfiguration % make ptb screen transparent for debugging -[logs, oldRes] = openPTBwindow(settings); - -% Initialize eye tracker -if settings.eyeTracking == 1 - [logs, settings] = initEyeLink(logs, settings); -end - -% Load images for picture viewing -settings = importPics(settings, logs); - -% Wait for trigger & show instructions -theStr{1} = 'DeepMReye exemplary calibration script \n\n'; -theStr{2} = 'Waiting for triggers... (Dummy run: click "s" to start.)'; -logs = wait4Trigger(logs, settings, theStr); - -% ----------------------------------------------------------------------- % - - -% ============================ Start tasks ============================= % - -% ------------- Condition 1: Fixation (pseudorandom walk) ------------- % - -% draw task instructions -theStr = 'Fixation task - always fixate at the black cross'; -logs{1}.passedTrials = 1; -[logs, settings] = draw_text(settings, logs, 1, 4, theStr); - -% go! -logs{1}.passedTrials = logs{1}.passedTrials + 1; currTrial = 0; -while currTrial<=numel(settings.fixtask.xy_trials)-1 && logs{1}.userQuit == 0 - currTrial = currTrial + 1; cFrame = 0; - - % send trial info to eye tracker - if settings.eyeTracking == 1; Eyelink('Message',sprintf('Trial%d', currTrial)); end - - % show fixation sequence - frames = cFrame+1:cFrame+settings.fixtask.dur*settings.scr.hz; - [logs, cFrame] = playGuidedViewing(logs, settings, currTrial, frames, 'fixation'); -end -logs{1}.passedTrials = logs{1}.passedTrials + currTrial; - - -% ----------- Condition 2: Smooth Pursuit (pseudorandom walk) ---------- % - -% draw task instructions -theStr = 'Smooth-pursuit task - always fixate at the moving black cross'; -[logs, settings] = draw_text(settings, logs, 1, 4, theStr); - -% go! -logs{1}.passedTrials = logs{1}.passedTrials + 1; currTrial = 0; -while currTrial<=numel(settings.pursuit.xy_trials_pursuit)-1 && logs{1}.userQuit == 0 - currTrial = currTrial + 1; cFrame = 0; - - % send trial info to eye tracker - if settings.eyeTracking == 1; Eyelink('Message',sprintf('Tr%d', currTrial)); end - - % show pursuit sequence - frames = cFrame+1:cFrame+settings.pursuit.dur*settings.scr.hz; - [logs, cFrame] = playGuidedViewing(logs, settings, currTrial, frames, 'pursuit'); -end -logs{1}.passedTrials = logs{1}.passedTrials + currTrial; - - -% ----------------- Condition 3: Free image viewing ------------------- % - -% draw task instructions -theStr = 'Free viewing task - explore the following images however you like'; -[logs, settings] = draw_text(settings, logs, 1, 4, theStr); % draw task instructions - -% go! -logs{1}.passedTrials = logs{1}.passedTrials + 1; currTrial = 0; -while currTrial 50 - return; - end - - % select random angle x amplitude combo - trial_id = randi(numel(mov_angles)); - trial_angle = mov_angles(trial_id); - trial_amp = mov_amp(trial_id); - - settings.pursuit.xy(cTrial,1) = (settings.pursuit.xy(cTrial-1,1)) + round(trial_amp * cos(trial_angle)); - settings.pursuit.xy(cTrial,2) = (settings.pursuit.xy(cTrial-1,2)) + round(trial_amp * sin(trial_angle)); - - % if fixation cross leaves calibration window, try selecting another angle & amplitude - if settings.pursuit.xy(cTrial,1) < calib_win(1,1) || settings.pursuit.xy(cTrial,1)>calib_win(1,2) || ... - settings.pursuit.xy(cTrial,2)calib_win(2,2) - stuck = stuck+1; - continue; - else - % if fixation cross is within calibration window, remove chosen angle from list - mov_angles(trial_id) = []; - mov_amp(trial_id) = []; - cTrial = cTrial + 1; - end -end - -% interpolate position inbetween screen locations -if isempty(mov_angles) && isempty(mov_amp) - valid = 1; - for cTrial = 2:size(settings.pursuit.xy,1) - settings.pursuit.xy_trials_pursuit{cTrial-1}(1,:) = linspace(settings.pursuit.xy(cTrial-1,1), settings.pursuit.xy(cTrial,1), settings.pursuit.dur*settings.scr.hz+1); - settings.pursuit.xy_trials_pursuit{cTrial-1}(2,:) = linspace(settings.pursuit.xy(cTrial-1,2), settings.pursuit.xy(cTrial,2), settings.pursuit.dur*settings.scr.hz+1); - end -end - -% add starting position (0,0) as first trial -settings.pursuit.xy_trials_pursuit = arrayfun(@(x) settings.pursuit.xy_trials_pursuit{x}(:,2:end)', 1:numel(settings.pursuit.xy_trials_pursuit), 'UniformOutput', false); -settings.pursuit.xy_trials_pursuit = [{zeros(settings.pursuit.dur*settings.scr.hz,2)}, settings.pursuit.xy_trials_pursuit]; -settings.pursuit.xy_trials_pursuit = cellfun(@round, settings.pursuit.xy_trials_pursuit, 'uni', 0); -% ----------------------------------------------------------------------- % -end \ No newline at end of file +function [settings, valid] = getFixLocations_pursuit(settings, valid) + % Pseudorandom walk for smooth pusuit task balancing eye-movement directions + % and speeds. MN, September 2021 + + % get movement angles and amplitudes (in pixel coordinates) + mov_angles = repmat(settings.pursuit.angles, 1, numel(settings.pursuit.mov_amp)); + mov_amp = settings.pursuit.mov_amp * settings.units.pxPdeg; + mov_amp = ones(numel(settings.pursuit.angles), numel(settings.pursuit.mov_amp)) .* mov_amp; + mov_amp = mov_amp(:)'; + + % window for pursuit trajectory (fixation cross will not leave this window) + calib_win = [-settings.pursuit.win_sz / 2; settings.pursuit.win_sz / 2]'; + + % create pursuit trajectory with defined movement angles & amplitudes within predefined window + settings.pursuit.xy = [0, 0]; % start at screen center + cTrial = 2; + stuck = 0; + while cTrial <= (numel(settings.pursuit.mov_amp) * numel(settings.pursuit.angles) + 1) + + % if no solution can be found, step out and try again + if stuck > 50 + return + end + + % select random angle x amplitude combo + trial_id = randi(numel(mov_angles)); + trial_angle = mov_angles(trial_id); + trial_amp = mov_amp(trial_id); + + settings.pursuit.xy(cTrial, 1) = (settings.pursuit.xy(cTrial - 1, 1)) + round(trial_amp * cos(trial_angle)); + settings.pursuit.xy(cTrial, 2) = (settings.pursuit.xy(cTrial - 1, 2)) + round(trial_amp * sin(trial_angle)); + + % if fixation cross leaves calibration window, try selecting another angle & amplitude + if settings.pursuit.xy(cTrial, 1) < calib_win(1, 1) || settings.pursuit.xy(cTrial, 1) > calib_win(1, 2) || ... + settings.pursuit.xy(cTrial, 2) < calib_win(2, 1) || settings.pursuit.xy(cTrial, 2) > calib_win(2, 2) + stuck = stuck + 1; + continue + else + % if fixation cross is within calibration window, remove chosen angle from list + mov_angles(trial_id) = []; + mov_amp(trial_id) = []; + cTrial = cTrial + 1; + end + end + + % interpolate position inbetween screen locations + if isempty(mov_angles) && isempty(mov_amp) + valid = 1; + for cTrial = 2:size(settings.pursuit.xy, 1) + settings.pursuit.xy_trials_pursuit{cTrial - 1}(1, :) = linspace(settings.pursuit.xy(cTrial - 1, 1), settings.pursuit.xy(cTrial, 1), settings.pursuit.dur * settings.scr.hz + 1); + settings.pursuit.xy_trials_pursuit{cTrial - 1}(2, :) = linspace(settings.pursuit.xy(cTrial - 1, 2), settings.pursuit.xy(cTrial, 2), settings.pursuit.dur * settings.scr.hz + 1); + end + end + + % add starting position (0,0) as first trial + settings.pursuit.xy_trials_pursuit = arrayfun(@(x) settings.pursuit.xy_trials_pursuit{x}(:, 2:end)', 1:numel(settings.pursuit.xy_trials_pursuit), 'UniformOutput', false); + settings.pursuit.xy_trials_pursuit = [{zeros(settings.pursuit.dur * settings.scr.hz, 2)}, settings.pursuit.xy_trials_pursuit]; + settings.pursuit.xy_trials_pursuit = cellfun(@round, settings.pursuit.xy_trials_pursuit, 'uni', 0); + % ----------------------------------------------------------------------- % +end diff --git a/getKeyboard.m b/getKeyboard.m index 2f39872..31d96a4 100644 --- a/getKeyboard.m +++ b/getKeyboard.m @@ -1,18 +1,18 @@ -function logs = getKeyboard(logs) -% set up key board and start monitoring button presses -% MN, September 2021 - -KbName('UnifyKeyNames') -logs{1}.keys.keyBoardInds = GetKeyboardIndices; -logs{1}.keys.escapeKey = KbName('ESCAPE'); % quit -logs{1}.keys.upKey = KbName('UpArrow'); -logs{1}.keys.downKey = KbName('DownArrow'); -logs{1}.keys.leftKey = KbName('LeftArrow'); -logs{1}.keys.rightKey = KbName('RightArrow'); -logs{1}.keys.scannerTriggerKey = KbName('s'); % scanner trigger -logs{1}.keys.spaceKey = KbName('Space'); % enter -logs{1}.keys.response_right = KbName('c'); -logs{1}.keys.response_left = KbName('b'); -KbQueueCreate(max(logs{1}.keys.keyBoardInds)); -KbQueueStart(); -end \ No newline at end of file +function logs = getKeyboard(logs) + % set up key board and start monitoring button presses + % MN, September 2021 + + KbName('UnifyKeyNames'); + logs{1}.keys.keyBoardInds = GetKeyboardIndices; + logs{1}.keys.escapeKey = KbName('ESCAPE'); % quit + logs{1}.keys.upKey = KbName('UpArrow'); + logs{1}.keys.downKey = KbName('DownArrow'); + logs{1}.keys.leftKey = KbName('LeftArrow'); + logs{1}.keys.rightKey = KbName('RightArrow'); + logs{1}.keys.scannerTriggerKey = KbName('s'); % scanner trigger + logs{1}.keys.spaceKey = KbName('Space'); % enter + logs{1}.keys.response_right = KbName('c'); + logs{1}.keys.response_left = KbName('b'); + KbQueueCreate(max(logs{1}.keys.keyBoardInds)); + KbQueueStart(); +end diff --git a/getUnits.m b/getUnits.m index a23f7ed..8b564ec 100644 --- a/getUnits.m +++ b/getUnits.m @@ -1,11 +1,11 @@ -function [units] = getUnits(settings) -% Compute a few useful units based on your screen settings -% MN, September 2021 - -units.ScrDiag_px = sqrt(settings.scr.width^2 + settings.scr.height^2); % screen diagonal in pixel -units.ScrDiag_cm = sqrt(settings.scr.width_cm^2 + settings.scr.height_cm^2); % screen diagonal in cm -units.pxPcm = units.ScrDiag_px/units.ScrDiag_cm; % pixel per cm -units.pxPdeg = (settings.scr.eye2scr*tand(1))*units.pxPcm; % pixel per degree - -% add your favorite units here -end \ No newline at end of file +function [units] = getUnits(settings) + % Compute a few useful units based on your screen settings + % MN, September 2021 + + units.ScrDiag_px = sqrt(settings.scr.width^2 + settings.scr.height^2); % screen diagonal in pixel + units.ScrDiag_cm = sqrt(settings.scr.width_cm^2 + settings.scr.height_cm^2); % screen diagonal in cm + units.pxPcm = units.ScrDiag_px / units.ScrDiag_cm; % pixel per cm + units.pxPdeg = (settings.scr.eye2scr * tand(1)) * units.pxPcm; % pixel per degree + + % add your favorite units here +end diff --git a/importPics.m b/importPics.m index e6c7c68..46a29fb 100644 --- a/importPics.m +++ b/importPics.m @@ -1,14 +1,15 @@ -function settings = importPics(settings, logs) -% import random images from specified folder. -% MN, September 2021 - -% select random images -path2pics = dir(fullfile(settings.picTask.path2pics, 'image*')); -path2pics = fullfile(path2pics(1).folder, {path2pics(:).name}'); -tmp = unique(randi(numel(path2pics), 1, 100)); tmp = tmp(randperm(numel(tmp))); -path2pics = path2pics(tmp(1:settings.picTask.n_pics)); -settings.pics.paths = path2pics; - -% import images -settings.pics.id = cell2mat(arrayfun(@(x) Screen('MakeTexture', logs{1}.w, imread(path2pics{x})), 1:settings.picTask.n_pics, 'uni', 0)); -end +function settings = importPics(settings, logs) + % import random images from specified folder. + % MN, September 2021 + + % select random images + path2pics = dir(fullfile(settings.picTask.path2pics, 'image*')); + path2pics = fullfile(path2pics(1).folder, {path2pics(:).name}'); + tmp = unique(randi(numel(path2pics), 1, 100)); + tmp = tmp(randperm(numel(tmp))); + path2pics = path2pics(tmp(1:settings.picTask.n_pics)); + settings.pics.paths = path2pics; + + % import images + settings.pics.id = cell2mat(arrayfun(@(x) Screen('MakeTexture', logs{1}.w, imread(path2pics{x})), 1:settings.picTask.n_pics, 'uni', 0)); +end diff --git a/initEyeLink.m b/initEyeLink.m index a43ffe7..55e27c0 100644 --- a/initEyeLink.m +++ b/initEyeLink.m @@ -1,47 +1,46 @@ -function [logs, settings] = initEyelink(logs, settings) -% initialize EYELINK eye tracker -% MN, September 2021 - - % Provide Eyelink with details about the graphics environment & - % initialize - el=EyelinkInitDefaults(logs{1}.w); - dummymode = 0; - if ~EyelinkInit(dummymode, 1)% Check - fprintf('Eyelink Init aborted.\n'); - cleanup; return; - end - [~, vs]=Eyelink('GetTrackerVersion'); - fprintf('Running experiment on a ''%s'' tracker.\n', vs );% Check - - % configuration settings - Eyelink('command', 'file_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE'); - Eyelink('command', 'file_sample_data = LEFT,RIGHT,GAZE,AREA'); % Check - % set link data (used for gaze cursor) - Eyelink('command', 'link_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON,INPUT'); - Eyelink('command', 'link_sample_data = LEFT,RIGHT,GAZE,AREA'); - - % make sure that we get gaze data from the Eyelink - Eyelink('Command', 'link_sample_data = LEFT,RIGHT,GAZE,AREA'); - - % open file to record data to - thedatestr=datestr(now,'yyyy-mm-dd_HH.MM'); - settings.eyelink.edfFile=[sprintf('%s_run%d', settings.subjID, settings.run)]; - status = Eyelink('Openfile', [settings.eyelink.edfFile '.edf']); - if status ~= 0 - fprintf('Could not open Eyelink file') - end - - % Calibrate the eye tracker - EyelinkDoTrackerSetup(el); - - % check calibration and do drift correction if necessary - % EyelinkDoDriftCorrection(el); - - % start recording - Eyelink('StartRecording'); - - % add go signal to log file - Eyelink('Message', 'SYNCTIME'); -end - - +function [logs, settings] = initEyelink(logs, settings) + % initialize EYELINK eye tracker + % MN, September 2021 + + % Provide Eyelink with details about the graphics environment & + % initialize + el = EyelinkInitDefaults(logs{1}.w); + dummymode = 0; + if ~EyelinkInit(dummymode, 1) % Check + fprintf('Eyelink Init aborted.\n'); + cleanup; + return + end + [~, vs] = Eyelink('GetTrackerVersion'); + fprintf('Running experiment on a ''%s'' tracker.\n', vs); % Check + + % configuration settings + Eyelink('command', 'file_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE'); + Eyelink('command', 'file_sample_data = LEFT,RIGHT,GAZE,AREA'); % Check + % set link data (used for gaze cursor) + Eyelink('command', 'link_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON,INPUT'); + Eyelink('command', 'link_sample_data = LEFT,RIGHT,GAZE,AREA'); + + % make sure that we get gaze data from the Eyelink + Eyelink('Command', 'link_sample_data = LEFT,RIGHT,GAZE,AREA'); + + % open file to record data to + thedatestr = datestr(now, 'yyyy-mm-dd_HH.MM'); + settings.eyelink.edfFile = [sprintf('%s_run%d', settings.subjID, settings.run)]; + status = Eyelink('Openfile', [settings.eyelink.edfFile '.edf']); + if status ~= 0 + fprintf('Could not open Eyelink file'); + end + + % Calibrate the eye tracker + EyelinkDoTrackerSetup(el); + + % check calibration and do drift correction if necessary + % EyelinkDoDriftCorrection(el); + + % start recording + Eyelink('StartRecording'); + + % add go signal to log file + Eyelink('Message', 'SYNCTIME'); +end diff --git a/miss_hit.cfg b/miss_hit.cfg new file mode 100644 index 0000000..e1d960d --- /dev/null +++ b/miss_hit.cfg @@ -0,0 +1,18 @@ +project_root + +octave: true + +# style guide (https://florianschanda.github.io/miss_hit/style_checker.html) +line_length: 200 + +suppress_rule: "copyright_notice" +suppress_rule: "naming_parameters" +suppress_rule: "naming_functions" + +tab_width: 4 + +# metrics limit for the code quality (https://florianschanda.github.io/miss_hit/metrics.html) +metric "cnest": limit 4 +metric "file_length": limit 350 +metric "cyc": limit 19 +metric "parameters": limit 8 diff --git a/openPTBwindow.m b/openPTBwindow.m index 0db24c4..60d041a 100644 --- a/openPTBwindow.m +++ b/openPTBwindow.m @@ -1,30 +1,29 @@ -function [logs, oldRes] = openPTBwindow(settings) -% Initialize psychtoolbox and open window -% MN, September 2021 - -% HideCursor; -dbstop if error -Screen('Preference', 'ConserveVRAM', 4096); - -% set up keyboard and start queue for monitoring button presses -logs = getKeyboard; - -% adjust screen resolution -try - oldRes = Screen('Resolution', settings.scr.scrID, settings.scr.width, ... - settings.scr.height,settings.scr.hz,[],[]); -catch - warning('Your screen resolution was not changed - do it manually!') - oldRes = []; -end - - -% open window -[logs{1}.w, logs{1}.winRect] = Screen('OpenWindow',settings.scr.scrID); -Screen('TextFont',logs{1}.w, 'Arial'); -Screen('TextSize', logs{1}.w, settings.scr.txtsz); -% Priority(MaxPriority(logs{1}.w)); - -% initial flip to sync us to VBL, get start timestamp and get rid of some bugs -logs{1}.vbl = Screen('Flip', logs{1}.w); -end +function [logs, oldRes] = openPTBwindow(settings) + % Initialize psychtoolbox and open window + % MN, September 2021 + + % HideCursor; + dbstop if error; + Screen('Preference', 'ConserveVRAM', 4096); + + % set up keyboard and start queue for monitoring button presses + logs = getKeyboard; + + % adjust screen resolution + try + oldRes = Screen('Resolution', settings.scr.scrID, settings.scr.width, ... + settings.scr.height, settings.scr.hz, [], []); + catch + warning('Your screen resolution was not changed - do it manually!'); + oldRes = []; + end + + % open window + [logs{1}.w, logs{1}.winRect] = Screen('OpenWindow', settings.scr.scrID); + Screen('TextFont', logs{1}.w, 'Arial'); + Screen('TextSize', logs{1}.w, settings.scr.txtsz); + % Priority(MaxPriority(logs{1}.w)); + + % initial flip to sync us to VBL, get start timestamp and get rid of some bugs + logs{1}.vbl = Screen('Flip', logs{1}.w); +end diff --git a/playGuidedViewing.m b/playGuidedViewing.m index efe5226..3b5c08a 100644 --- a/playGuidedViewing.m +++ b/playGuidedViewing.m @@ -1,37 +1,42 @@ -function [logs, cFrame] = playGuidedViewing(logs, settings, currTrial, frames, flag) -% play smooth pursuit task -% MN, September 2021 - -% select fixation/pursuit trajectory -if strcmp(flag, 'fixation'); xy = settings.fixtask.xy_trials; -elseif strcmp(flag, 'pursuit'); xy = settings.pursuit.xy_trials_pursuit; end - -% play all frames -for cFrame = frames - - % quit if user aborted experiment - [~, firstPress] = KbQueueCheck(); - if firstPress(logs{1}.keys.escapeKey) || logs{1}.userQuit == 1 - logs{1}.userQuit = 1; return; end - - % draw fix cross - total_n_Trial = currTrial+logs{1}.passedTrials; - logs{total_n_Trial}.xy(cFrame, :) = [xy{currTrial}(cFrame,1), xy{currTrial}(cFrame,2)]; - logs{total_n_Trial}.crossVert = OffsetRect(CenterRect(settings.crossVert, logs{1}.winRect), xy{currTrial}(cFrame,1), xy{currTrial}(cFrame,2)); - logs{total_n_Trial}.crossHorz = OffsetRect(CenterRect(settings.crossHorz, logs{1}.winRect), xy{currTrial}(cFrame,1), xy{currTrial}(cFrame,2)); - Screen('DrawLines', logs{1}.w, [logs{total_n_Trial}.crossVert(1:2)', logs{total_n_Trial}.crossVert(3:4)', ... - logs{total_n_Trial}.crossHorz(1:2)', logs{total_n_Trial}.crossHorz(3:4)'], 3, settings.colors.black, [], []); - - % finish drawing and flip to screen - Screen('DrawingFinished', logs{1}.w); - logs{total_n_Trial}.flips(cFrame,1) = Screen('Flip', logs{1}.w); - - % log scanner trigger - if firstPress(logs{1}.keys.scannerTriggerKey) - logs{1}.tTriggers = [logs{1}.tTriggers, logs{total_n_Trial}.flips(cFrame,1)]; - end -end - -% add trial info to log file -logs{total_n_Trial}.trialType = flag; -end +function [logs, cFrame] = playGuidedViewing(logs, settings, currTrial, frames, flag) + % play smooth pursuit task + % MN, September 2021 + + % select fixation/pursuit trajectory + if strcmp(flag, 'fixation') + xy = settings.fixtask.xy_trials; + elseif strcmp(flag, 'pursuit') + xy = settings.pursuit.xy_trials_pursuit; + end + + % play all frames + for cFrame = frames + + % quit if user aborted experiment + [~, firstPress] = KbQueueCheck(); + if firstPress(logs{1}.keys.escapeKey) || logs{1}.userQuit == 1 + logs{1}.userQuit = 1; + return + end + + % draw fix cross + total_n_Trial = currTrial + logs{1}.passedTrials; + logs{total_n_Trial}.xy(cFrame, :) = [xy{currTrial}(cFrame, 1), xy{currTrial}(cFrame, 2)]; + logs{total_n_Trial}.crossVert = OffsetRect(CenterRect(settings.crossVert, logs{1}.winRect), xy{currTrial}(cFrame, 1), xy{currTrial}(cFrame, 2)); + logs{total_n_Trial}.crossHorz = OffsetRect(CenterRect(settings.crossHorz, logs{1}.winRect), xy{currTrial}(cFrame, 1), xy{currTrial}(cFrame, 2)); + Screen('DrawLines', logs{1}.w, [logs{total_n_Trial}.crossVert(1:2)', logs{total_n_Trial}.crossVert(3:4)', ... + logs{total_n_Trial}.crossHorz(1:2)', logs{total_n_Trial}.crossHorz(3:4)'], 3, settings.colors.black, [], []); + + % finish drawing and flip to screen + Screen('DrawingFinished', logs{1}.w); + logs{total_n_Trial}.flips(cFrame, 1) = Screen('Flip', logs{1}.w); + + % log scanner trigger + if firstPress(logs{1}.keys.scannerTriggerKey) + logs{1}.tTriggers = [logs{1}.tTriggers, logs{total_n_Trial}.flips(cFrame, 1)]; + end + end + + % add trial info to log file + logs{total_n_Trial}.trialType = flag; +end diff --git a/playPictureViewing.m b/playPictureViewing.m index 43668c3..2460d63 100644 --- a/playPictureViewing.m +++ b/playPictureViewing.m @@ -1,39 +1,38 @@ -function [settings, logs] = playPictureViewing(settings, logs, currTrial, frames) -% play picture-viewing task -% MN, September 2021 - -total_n_Trial = currTrial+logs{1}.passedTrials; -logs{total_n_Trial}.pic = settings.pics.paths{currTrial}; -for cFrame = frames - - % quit if user aborted experiment - [~, firstPress] = KbQueueCheck(); - if firstPress(logs{1}.keys.escapeKey) || logs{1}.userQuit == 1 - logs{1}.userQuit = 1; - return; - end - - % draw image - total_n_Trial = currTrial+logs{1}.passedTrials; - picSz = round(settings.scr.height/3); - [center(1), center(2)] = RectCenter(logs{1}.winRect); - picCoords = [center(1)-picSz center(2)-picSz, center(1)+picSz center(2)+picSz]; - Screen('DrawTexture', logs{1}.w, settings.pics.id(currTrial), [],picCoords); - - % finish drawing and flip to screen - Screen('DrawingFinished', logs{1}.w); - logs{total_n_Trial}.flips(cFrame,1) = Screen('Flip', logs{1}.w); - - % log scanner trigger - if firstPress(logs{1}.keys.scannerTriggerKey) - logs{1}.tTriggers = [logs{1}.tTriggers, logs{total_n_Trial}.flips(cFrame,1)]; - end - - % log NaN's for xy's - logs{total_n_Trial}.xy(cFrame, :) = [nan, nan]; -end - -% add trial info to log file -logs{total_n_Trial}.trialType = 'free_viewing'; -end - +function [settings, logs] = playPictureViewing(settings, logs, currTrial, frames) + % play picture-viewing task + % MN, September 2021 + + total_n_Trial = currTrial + logs{1}.passedTrials; + logs{total_n_Trial}.pic = settings.pics.paths{currTrial}; + for cFrame = frames + + % quit if user aborted experiment + [~, firstPress] = KbQueueCheck(); + if firstPress(logs{1}.keys.escapeKey) || logs{1}.userQuit == 1 + logs{1}.userQuit = 1; + return + end + + % draw image + total_n_Trial = currTrial + logs{1}.passedTrials; + picSz = round(settings.scr.height / 3); + [center(1), center(2)] = RectCenter(logs{1}.winRect); + picCoords = [center(1) - picSz center(2) - picSz, center(1) + picSz center(2) + picSz]; + Screen('DrawTexture', logs{1}.w, settings.pics.id(currTrial), [], picCoords); + + % finish drawing and flip to screen + Screen('DrawingFinished', logs{1}.w); + logs{total_n_Trial}.flips(cFrame, 1) = Screen('Flip', logs{1}.w); + + % log scanner trigger + if firstPress(logs{1}.keys.scannerTriggerKey) + logs{1}.tTriggers = [logs{1}.tTriggers, logs{total_n_Trial}.flips(cFrame, 1)]; + end + + % log NaN's for xy's + logs{total_n_Trial}.xy(cFrame, :) = [nan, nan]; + end + + % add trial info to log file + logs{total_n_Trial}.trialType = 'free_viewing'; +end diff --git a/saveLogs.m b/saveLogs.m index f324a60..fcf1f8f 100644 --- a/saveLogs.m +++ b/saveLogs.m @@ -1,22 +1,24 @@ -function saveLogs(logs) -% log and save everything -% MN, September 2021 - -% output path -outPath = which('Start_deepMReye_calib.m'); % where are the stimulus script? -outPath = fullfile(fileparts(outPath), 'logs'); % - -% create output directory if it does not exist -if exist(outPath, 'dir') ~= 7; mkdir(outPath); end - -% set file name -if ~logs{1}.userQuit % if experiment ended normally - file2save = sprintf('%d_%s_logfile_%s.mat', logs{1}.run, logs{1}.subjID, datestr(now, 'dd.mm.yy_HH-MM')); -else % if user quit experiment manually - file2save = sprintf('quit_%d_%s_logfile_%s.mat', logs{1}.run, logs{1}.subjID, datestr(now, 'dd.mm.yy_HH-MM')); -end - -% save -save(fullfile(outPath, file2save), 'logs'); - -end \ No newline at end of file +function saveLogs(logs) + % log and save everything + % MN, September 2021 + + % output path + outPath = which('Start_deepMReye_calib.m'); % where are the stimulus script? + outPath = fullfile(fileparts(outPath), 'logs'); % + + % create output directory if it does not exist + if exist(outPath, 'dir') ~= 7 + mkdir(outPath); + end + + % set file name + if ~logs{1}.userQuit % if experiment ended normally + file2save = sprintf('%d_%s_logfile_%s.mat', logs{1}.run, logs{1}.subjID, datestr(now, 'dd.mm.yy_HH-MM')); + else % if user quit experiment manually + file2save = sprintf('quit_%d_%s_logfile_%s.mat', logs{1}.run, logs{1}.subjID, datestr(now, 'dd.mm.yy_HH-MM')); + end + + % save + save(fullfile(outPath, file2save), 'logs'); + +end diff --git a/vline.m b/vline.m index d4d448c..a24b622 100644 --- a/vline.m +++ b/vline.m @@ -1,107 +1,104 @@ -function hhh=vline(x,in1,in2) -% function h=vline(x, linetype, label) -% -% Draws a vertical line on the current axes at the location specified by 'x'. Optional arguments are -% 'linetype' (default is 'r:') and 'label', which applies a text label to the graph near the line. The -% label appears in the same color as the line. -% -% The line is held on the current axes, and after plotting the line, the function returns the axes to -% its prior hold state. -% -% The HandleVisibility property of the line object is set to "off", so not only does it not appear on -% legends, but it is not findable by using findobj. Specifying an output argument causes the function to -% return a handle to the line, so it can be manipulated or deleted. Also, the HandleVisibility can be -% overridden by setting the root's ShowHiddenHandles property to on. -% -% h = vline(42,'g','The Answer') -% -% returns a handle to a green vertical line on the current axes at x=42, and creates a text object on -% the current axes, close to the line, which reads "The Answer". -% -% vline also supports vector inputs to draw multiple lines at once. For example, -% -% vline([4 8 12],{'g','r','b'},{'l1','lab2','LABELC'}) -% -% draws three lines with the appropriate labels and colors. -% -% By Brandon Kuczenski for Kensington Labs. -% brandon_kuczenski@kensingtonlabs.com -% 8 November 2001 - -if length(x)>1 % vector input - for I=1:length(x) - switch nargin - case 1 - linetype='r:'; - label=''; - case 2 - if ~iscell(in1) - in1={in1}; - end - if I>length(in1) - linetype=in1{end}; - else - linetype=in1{I}; - end - label=''; - case 3 - if ~iscell(in1) - in1={in1}; - end - if ~iscell(in2) - in2={in2}; - end - if I>length(in1) - linetype=in1{end}; - else - linetype=in1{I}; - end - if I>length(in2) - label=in2{end}; - else - label=in2{I}; - end - end - h(I)=vline(x(I),linetype,label); - end -else - switch nargin - case 1 - linetype='r:'; - label=''; - case 2 - linetype=in1; - label=''; - case 3 - linetype=in1; - label=in2; - end - - - - - g=ishold(gca); - hold on - - y=get(gca,'ylim'); - h=plot([x x],y,linetype); - if length(label) - xx=get(gca,'xlim'); - xrange=xx(2)-xx(1); - xunit=(x-xx(1))/xrange; - if xunit<0.8 - text(x+0.01*xrange,y(1)+0.1*(y(2)-y(1)),label,'color',get(h,'color')) - else - text(x-.05*xrange,y(1)+0.1*(y(2)-y(1)),label,'color',get(h,'color')) - end - end - - if g==0 - hold off - end - set(h,'tag','vline','handlevisibility','off') -end % else - -if nargout - hhh=h; -end +function hhh = vline(x, in1, in2) + % function h=vline(x, linetype, label) + % + % Draws a vertical line on the current axes at the location specified by 'x'. Optional arguments are + % 'linetype' (default is 'r:') and 'label', which applies a text label to the graph near the line. The + % label appears in the same color as the line. + % + % The line is held on the current axes, and after plotting the line, the function returns the axes to + % its prior hold state. + % + % The HandleVisibility property of the line object is set to "off", so not only does it not appear on + % legends, but it is not findable by using findobj. Specifying an output argument causes the function to + % return a handle to the line, so it can be manipulated or deleted. Also, the HandleVisibility can be + % overridden by setting the root's ShowHiddenHandles property to on. + % + % h = vline(42,'g','The Answer') + % + % returns a handle to a green vertical line on the current axes at x=42, and creates a text object on + % the current axes, close to the line, which reads "The Answer". + % + % vline also supports vector inputs to draw multiple lines at once. For example, + % + % vline([4 8 12],{'g','r','b'},{'l1','lab2','LABELC'}) + % + % draws three lines with the appropriate labels and colors. + % + % By Brandon Kuczenski for Kensington Labs. + % brandon_kuczenski@kensingtonlabs.com + % 8 November 2001 + + if length(x) > 1 % vector input + for I = 1:length(x) + switch nargin + case 1 + linetype = 'r:'; + label = ''; + case 2 + if ~iscell(in1) + in1 = {in1}; + end + if I > length(in1) + linetype = in1{end}; + else + linetype = in1{I}; + end + label = ''; + case 3 + if ~iscell(in1) + in1 = {in1}; + end + if ~iscell(in2) + in2 = {in2}; + end + if I > length(in1) + linetype = in1{end}; + else + linetype = in1{I}; + end + if I > length(in2) + label = in2{end}; + else + label = in2{I}; + end + end + h(I) = vline(x(I), linetype, label); + end + else + switch nargin + case 1 + linetype = 'r:'; + label = ''; + case 2 + linetype = in1; + label = ''; + case 3 + linetype = in1; + label = in2; + end + + g = ishold(gca); + hold on; + + y = get(gca, 'ylim'); + h = plot([x x], y, linetype); + if length(label) + xx = get(gca, 'xlim'); + xrange = xx(2) - xx(1); + xunit = (x - xx(1)) / xrange; + if xunit < 0.8 + text(x + 0.01 * xrange, y(1) + 0.1 * (y(2) - y(1)), label, 'color', get(h, 'color')); + else + text(x - .05 * xrange, y(1) + 0.1 * (y(2) - y(1)), label, 'color', get(h, 'color')); + end + end + + if g == 0 + hold off; + end + set(h, 'tag', 'vline', 'handlevisibility', 'off'); + end % else + + if nargout + hhh = h; + end diff --git a/wait4Trigger.m b/wait4Trigger.m index 2e2cb7c..11d60e8 100644 --- a/wait4Trigger.m +++ b/wait4Trigger.m @@ -1,39 +1,40 @@ -function logs = wait4Trigger(logs, settings, theStr) -% Wait for scanner trigger. -% MN, September 2021 - -logs{1}.userQuit = 0; -waiting4Trigger = 1; -fprintf('\n Waiting for trigger...') -while waiting4Trigger - gotTrigger = 0; - - % draw waiting screen - Screen('FillRect', logs{1}.w, settings.colors.gray); - DrawFormattedText(logs{1}.w, cell2mat(theStr(1:numel(theStr))), 'center','center', settings.colors.black); - Screen('Flip', logs{1}.w); - - % Check if trigger arrived - [~, firstPress] = KbQueueCheck(); - if firstPress(logs{1}.keys.scannerTriggerKey) % trigger arrived - gotTrigger = 1; - triggertstamp = firstPress(logs{1}.keys.scannerTriggerKey); - elseif firstPress(logs{1}.keys.escapeKey) % user quit - logs{1}.userQuit = 1; sca - return; - end - - % if trigger arrived, break while loop and start stimulus - if gotTrigger - % print to user - fprintf(' Got it!'); - fprintf('\nStarting stimulus ...'); - - % log everything - waiting4Trigger = 0; - scannerTrCtr = 1; - logs{1}.FirstVolOfExp = triggertstamp; - logs{1}.tTriggers(scannerTrCtr) = triggertstamp; - end -end -end \ No newline at end of file +function logs = wait4Trigger(logs, settings, theStr) + % Wait for scanner trigger. + % MN, September 2021 + + logs{1}.userQuit = 0; + waiting4Trigger = 1; + fprintf('\n Waiting for trigger...'); + while waiting4Trigger + gotTrigger = 0; + + % draw waiting screen + Screen('FillRect', logs{1}.w, settings.colors.gray); + DrawFormattedText(logs{1}.w, cell2mat(theStr(1:numel(theStr))), 'center', 'center', settings.colors.black); + Screen('Flip', logs{1}.w); + + % Check if trigger arrived + [~, firstPress] = KbQueueCheck(); + if firstPress(logs{1}.keys.scannerTriggerKey) % trigger arrived + gotTrigger = 1; + triggertstamp = firstPress(logs{1}.keys.scannerTriggerKey); + elseif firstPress(logs{1}.keys.escapeKey) % user quit + logs{1}.userQuit = 1; + sca; + return + end + + % if trigger arrived, break while loop and start stimulus + if gotTrigger + % print to user + fprintf(' Got it!'); + fprintf('\nStarting stimulus ...'); + + % log everything + waiting4Trigger = 0; + scannerTrCtr = 1; + logs{1}.FirstVolOfExp = triggertstamp; + logs{1}.tTriggers(scannerTrCtr) = triggertstamp; + end + end +end