"""A wrapper for SaccadePursuit to track eye movements in addition Author - Michael Tan (mtan30@uic.edu) Adapted from: https://github.com/colinquirk/ChangeDetectionEyeTracking Note: this code relies on the Colin Quirk templateexperiments module. You can get it from https://github.com/colinquirk/templateexperiments and either put it in the same folder as this code or give the path to psychopy in the preferences. """ from __future__ import division from __future__ import print_function import os import random import sys import traceback import subprocess # Necesssary to access psychopy paths import psychopy # noqa:F401 import eyelinker import SaccadePursuit # Experimental Parameters monitor_name = 'testMonitor' monitor_width = 59.5 #41 distance_to_monitor = 70 monitor_px = [2560,1440] #[1440,900] window_screen = 0 #1 disableTracker = True # For Debugging #conditions = ['Fixation', 'Pursuit', 'Saccade', 'AntiSaccade', 'Necker'] #, 'Rivalry'] conditions = ['Necker'] isi_time = 1 # Interstimulus Interval data_directory = os.path.join( os.path.expanduser('~'), 'Desktop', 'ExperimentalData', 'SaccadePursuitEyeTracking') image_directory = os.path.join(os.getcwd(),'Images') # Saccade / Antisaccade Parameters number_of_saccade_trials = 1 number_of_saccade_blocks = 1 saccade_distance = 15 #15 saccade_time = 3 #3 stimulus_size = 0.3 stim_color = [1,-1,-1] saccade_fixation_color = [191,191,191] # Pursuit Parameters number_of_pursuit_trials = 1 number_of_pursuit_blocks = 1 pursuit_distance = 15 pursuit_frequencies = [0.1,0.2,0.4] pursuit_time = [5,5,5] #[40,20,15] # Necker Cube Parameters number_of_necker_trials = 1 number_of_necker_blocks = 4 necker_time = 6 necker_color = [191,191,191] necker_bg_color = [64,64,64] necker_scale = 0.5 necker_file = os.path.join(image_directory,'Necker1.tif') response_box_file = os.path.join(image_directory,'ResponseBox3.tif') # Fixation Parameters number_of_fixation_trials = 1 number_of_fixation_blocks = 1 fixation_size = stimulus_size*2 fixation_trial_time = 15 # Binocular Rivalry Parameters number_of_rivalry_trials = 1 number_of_rivalry_blocks = 1 rivalry_time = 90 rivalry_scale = 2.5 rivalry_file1 = os.path.join(image_directory,'house4n_11-160.tif') rivalry_file2 = os.path.join(image_directory,'face2nS_11-160.tif') rivalry_border_color = [191,191,191] rivalry_border_width = 5 rivalry_distance = 4 data_fields = [ 'Subject', 'Condition', 'Block', 'Trial', 'Timestamp', 'TrialType', 'Duration', 'Frequency', 'Locations', ] instruct_text = [ ('Welcome to the experiment. Press space to continue.'), ('In this experiment you will be following targets.\n\n' 'Each trial will start with a fixation cross. ' 'Focus on the fixation cross until a stimulus appears.\n\n' 'In the first three phases, you will follow the stimuli. ' 'In the fourth phase, you will focus on the ' 'opposite side of center cross at an ' 'equal distance as the stimulus. In the fifth ' 'phase, you will see a cube, and are to respond as ' 'indicated.' '\n\n' 'Do not move your head during the trials of this ' 'experiment. Move only your eyes to follow the targets.' 'You will get breaks in between blocks.\n\n' 'Press any key to continue.'), ] saccade_instruct_text = ( 'For these trials, focus on the fixation cross. When the target ' 'appears, move your gaze to the target. Move your gaze back to ' 'the fixation point after the target disappears.\n\n' 'Try not to blink after the target appears.\n\n' 'Press any key to continue.' ) antisaccade_instruct_text = ( 'For these trials, focus on the fixation cross. When the target ' 'appears, move your gaze to the OPPOSITE direction of the target ' 'at approximately the same distance from the fixation cross as ' 'the target. Move your gaze back to the fixation point after ' 'the target disappears.\n\n' 'Try not to blink after the target appears.\n\n' 'Press any key to continue.' ) pursuit_instruct_text = ( 'For these trials, when the circle target appears, follow the ' 'target with your eyes.\n\n' 'Try not to blink while the circle is moving.\n\n' 'Press any key to continue.' ) fixation_instruct_text = ( 'For these trials, when the cross appears, fixate on it.\n\n' 'Try not to move your eyes from the cross while it ' 'remains visible.\n\n' 'Press any key to continue.' ) necker_instruct_text = ( 'For these trials, focus on the square. When your ' 'perception is that the square is pointing down and ' 'to the left, press the left button. As soon as it ' 'switches to up and to the right, press the right ' 'button.\n\n' 'Respond at any time during the stimulus.\n\n' 'Press any key to continue.' ) rivalry_instruct_text = ( 'For these trials, the experimenter will move a ' 'system in front of your field of view. Look through ' 'the mirrors to see a different image in each eye.\n\n' 'If you perceive a face, press the right button. If ' 'you perceive a house, press the left button. If ' 'you perceive a combination of the face and house, ' 'press the middle button.\n\n' 'Respond at any time during the stimuli.\n\n' 'Press any key to continue.' ) def convert_color_value(color): """Converts a list of 3 values from 0 to 255 to -1 to 1. Parameters: color -- A list of 3 ints between 0 and 255 to be converted. """ return [round(((n/127.5)-1), 2) for n in color] class EyeTrackingSaccadePursuit(SaccadePursuit.SPtask): def __init__(self, **kwargs): self.quit = False # Needed because eyetracker must shut down self.tracker = None self.disable_tracker = disableTracker self.window_screen = window_screen self.monitor_px = monitor_px self.number_of_saccade_trials = number_of_saccade_trials self.number_of_saccade_blocks = number_of_saccade_blocks self.number_of_pursuit_trials = number_of_pursuit_trials self.number_of_pursuit_blocks = number_of_pursuit_blocks self.number_of_necker_trials = number_of_necker_trials self.number_of_necker_blocks = number_of_necker_blocks self.number_of_fixation_trials = number_of_fixation_trials self.number_of_fixation_blocks = number_of_fixation_blocks self.number_of_rivalry_trials = number_of_rivalry_trials self.number_of_rivalry_blocks = number_of_rivalry_blocks self.response_box_file = response_box_file super(EyeTrackingSaccadePursuit, self).__init__(**kwargs) def quit_experiment(self): self.quit = True if self.experiment_window: self.display_text_screen('Quiting...', wait_for_input=False) if self.tracker: fName = os.path.join(self.data_directory, 'ETSP' + self.experiment_info['Subject Identifier'] + '.edf') self.tracker.set_offline_mode() self.tracker.close_edf() self.tracker.transfer_edf() self.tracker.close_connection() subprocess.call(['edf2asc',fName]) super(EyeTrackingSaccadePursuit, self).quit_experiment() def run(self): self.chdir() print('Note: EDF file will be overwritten if identical subject identifiers are used!') ok = self.get_experiment_info_from_dialog(self.questionaire_dict) if not ok: print('Experiment has been terminated.') sys.exit(1) self.save_experiment_info() self.open_csv_data_file() self.open_window(screen=self.window_screen) self.display_text_screen('Loading...', wait_for_input=False) if not self.disableTracker: self.tracker = eyelinker.EyeLinker( self.experiment_window, 'ETSP' + self.experiment_info['Subject Identifier'] + '.edf', 'BOTH' ) self.tracker.initialize_graphics() self.tracker.open_edf() self.tracker.initialize_tracker() self.tracker.send_calibration_settings() for instruction in self.instruct_text: self.display_text_screen(text=instruction) if not self.disableTracker: self.tracker.display_eyetracking_instructions() self.tracker.setup_tracker() #random.shuffle(self.conditions) condition_counter = 0 for condition in self.conditions: condition_counter += 1 numBlocks = 1 numTrials = 1 if condition == 'Saccade': self.display_text_screen(text=saccade_instruct_text) numBlocks = self.number_of_saccade_blocks numTrials = self.number_of_saccade_trials elif condition=='AntiSaccade': self.display_text_screen(text=antisaccade_instruct_text) numBlocks = self.number_of_saccade_blocks numTrials = self.number_of_saccade_trials elif condition=='Fixation': self.display_text_screen(text=fixation_instruct_text) numBlocks = self.number_of_fixation_blocks numTrials = self.number_of_fixation_trials elif condition=='Pursuit': self.display_text_screen(text=pursuit_instruct_text) numBlocks = self.number_of_pursuit_blocks numTrials = self.number_of_pursuit_trials elif condition=='Necker': self.display_text_screen(text=necker_instruct_text, image_file=self.response_box_file) numBlocks = self.number_of_necker_blocks numTrials = self.number_of_necker_trials elif condition=='Rivalry': self.display_text_screen(text=rivalry_instruct_text, image_file=self.response_box_file) numBlocks = self.number_of_rivalry_blocks numTrials = self.number_of_rivalry_trials else: continue for block_num in range(numBlocks): block = self.make_block(condition, numTrials) self.display_text_screen(text='Get ready...', wait_for_input=False) psychopy.core.wait(2) if condition == 'Saccade' or condition == 'AntiSaccade': self.display_saccade_fixation(1) else: self.display_fixation(0.5) for trial_num, trial in enumerate(block): print( "Condition: ",condition,"(",condition_counter,"/",len(self.conditions),")", "Block ",block_num+1,"/",numBlocks, " Trial ",trial_num+1,"/",len(block) ) if not self.disableTracker: self.tracker.send_message('CONDITION %s' % condition) self.tracker.send_message('BLOCK %d' % block_num) self.tracker.send_message('TRIAL %d' % trial_num) status = '%s: Block %d, Trial %d' % (condition, block_num, trial_num) self.tracker.send_status(status) self.tracker.start_recording() data = self.run_trial(trial, block_num, trial_num, self.tracker) self.tracker.stop_recording() else: data = self.run_trial(trial, block_num, trial_num) data.update({'Condition': condition}) self.send_data(data) if condition == 'Saccade' or condition=='AntiSaccade': self.display_saccade_fixation(self.isi_time) else: self.display_fixation(self.isi_time) if condition == 'Saccade' or condition == 'AntiSaccade': self.display_saccade_fixation(1) else: self.display_fixation(0.5) self.save_data_to_csv() if block_num + 1 != numBlocks: self.display_break() if condition == 'Saccade': self.display_text_screen(text='Remember:\n\n' + saccade_instruct_text) elif condition=='AntiSaccade': self.display_text_screen(text='Remember:\n\n' + antisaccade_instruct_text) elif condition=='Fixation': self.display_text_screen(text='Remember:\n\n' + fixation_instruct_text) elif condition=='Pursuit': self.display_text_screen(text='Remember:\n\n' + pursuit_instruct_text) elif condition=='Necker': self.display_text_screen(text='Remember:\n\n' + necker_instruct_text, image_file=self.response_box_file) elif condition=='Rivalry': self.display_text_screen(text='Remember:\n\n' + rivalry_instruct_text, image_file=self.response_box_file) self.display_text_screen( 'The experiment is now over.', bg_color=[50, 150, 50], text_color=[255, 255, 255], wait_for_input=False) psychopy.core.wait(5) self.quit_experiment() experiment = EyeTrackingSaccadePursuit( experiment_name='ETSP', data_fields=data_fields, data_directory=data_directory, instruct_text=instruct_text, monitor_name=monitor_name, monitor_width=monitor_width, monitor_px=monitor_px, monitor_distance=distance_to_monitor, conditions=conditions, pursuit_time=pursuit_time, stim_color=stim_color, pursuit_frequencies=pursuit_frequencies, saccade_distance=saccade_distance, saccade_time=saccade_time, saccade_fixation_color=convert_color_value(saccade_fixation_color), isi_time=isi_time, stimulus_size=stimulus_size, fixation_size=fixation_size, pursuit_distance=pursuit_distance, necker_time=necker_time, necker_color=necker_color, necker_scale=necker_scale, necker_bg_color=convert_color_value(necker_bg_color), fixation_trial_time=fixation_trial_time, rivalry_time=rivalry_time, rivalry_scale=rivalry_scale, necker_file=necker_file, rivalry_file1=rivalry_file1, rivalry_file2=rivalry_file2, rivalry_border_color=convert_color_value(rivalry_border_color), disableTracker=disableTracker, rivalry_border_width=rivalry_border_width, rivalry_distance=rivalry_distance, ) if __name__ == '__main__': try: experiment.run() except Exception: print(traceback.format_exc()) if not experiment.quit: experiment.quit_experiment()