diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..0b92519 Binary files /dev/null and b/.DS_Store differ diff --git a/Images/neckerInstruction.tif b/Images/neckerInstruction.tif new file mode 100644 index 0000000..aed203b Binary files /dev/null and b/Images/neckerInstruction.tif differ diff --git a/SaccadePursuit.py b/SaccadePursuit.py old mode 100755 new mode 100644 index 087a09f..3366648 --- a/SaccadePursuit.py +++ b/SaccadePursuit.py @@ -222,7 +222,7 @@ class SPtask(template.BaseExperiment): self.experiment_window, text='+', color=self.stim_color, height=self.fixation_size, units='deg') if tracker: - tracker.send_message('Start Fixation') + tracker.send_message('Start Stim') for frameN in range(int(round(wait_time*60))): stim.draw() @@ -355,9 +355,11 @@ class SPtask(template.BaseExperiment): self.experiment_window.color = self.necker_bg_color stim = psychopy.visual.ImageStim( self.experiment_window, - image=self.necker_file) + image=self.necker_file, pos=(0.15,0.15)) stim.size *= self.necker_scale stim.setColor(self.necker_color, 'rgb255') + stim2 = psychopy.visual.TextStim( + self.experiment_window, text='+', color=self.stim_color, height=self.fixation_size, units='deg') responses = [] if tracker: tracker.send_message('Start Stim') @@ -372,9 +374,10 @@ class SPtask(template.BaseExperiment): direction = 'Center' responses.append((response[0],direction,psychopy.core.getAbsTime())) if tracker: - #print(response[0]) + print('Direction: %s, Key: %s' % (direction, response[0])) tracker.send_message('Direction: %s, Key: %s' % (direction, response[0])) stim.draw() + #stim2.draw() self.experiment_window.flip() #print(responses) return responses diff --git a/SaccadePursuitEyeTracking.py b/SaccadePursuitEyeTracking.py old mode 100755 new mode 100644 index 46edf77..b21cd6e --- a/SaccadePursuitEyeTracking.py +++ b/SaccadePursuitEyeTracking.py @@ -77,14 +77,14 @@ pursuit_time = [40, 20, 13.33] # Necker Cube Parameters number_of_necker_trials = 1 -number_of_necker_blocks = 3 +number_of_necker_blocks = 1 necker_time = 60 necker_color = [190, 190, 190] necker_bg_color = [30, 30, 30] -necker_scale = 0.5 +necker_scale = 0.25 necker_file = os.path.join(image_directory, 'Necker1.tif') -necker_response_box_file = os.path.join(image_directory, 'ResponseBox5.tif') -necker_response_box_scale = 0.22 +necker_response_box_file = os.path.join(image_directory, 'neckerInstruction.tif') +necker_response_box_scale = 0.307 # Fixation Parameters number_of_fixation_trials = 1 @@ -121,11 +121,12 @@ data_fields = [ # Add additional questions here questionaire_dict = { + 'Run Target Fixation': True, 'Run Smooth Pursuit': True, 'Run Saccade': True, 'Run Anti-Saccade': True, - # 'Run Necker Cube': False, - 'Run Binocular Rivalry': True, + 'Run Necker Cube': True, + 'Run Binocular Rivalry': False, } questionaire_order = [ @@ -134,10 +135,11 @@ questionaire_order = [ 'Timepoint', 'Experimenter Initials', 'Gender', + 'Run Target Fixation', 'Run Smooth Pursuit', 'Run Saccade', 'Run Anti-Saccade', - # 'Run Necker Cube', + 'Run Necker Cube', 'Run Binocular Rivalry', ] @@ -153,8 +155,10 @@ instruct_text = [ # '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.\n\nMove only your eyes.\n'), + #'Do not move your head during the trials of this ' + #'experiment.\n\nMove only your eyes.\n' + 'Remember: Please keep your head still with your forehead\n' + 'against the bar and your chin rested on the pad.'), # 'You will be offered rests between sections.\n\n' # 'Press any key to continue.'), ] @@ -305,14 +309,16 @@ class EyeTrackingSaccadePursuit(SaccadePursuit.SPtask): sys.exit(1) conditions = [] + if self.experiment_info['Run Target Fixation']: + conditions.append('Fixation') if self.experiment_info['Run Smooth Pursuit']: conditions.append('Pursuit') if self.experiment_info['Run Saccade']: conditions.append('Saccade') if self.experiment_info['Run Anti-Saccade']: conditions.append('AntiSaccade') -# if self.experiment_info['Run Necker Cube']: -# conditions.append('Necker') + if self.experiment_info['Run Necker Cube']: + conditions.append('Necker') if self.experiment_info['Run Binocular Rivalry']: conditions.append('Rivalry') @@ -360,6 +366,7 @@ class EyeTrackingSaccadePursuit(SaccadePursuit.SPtask): numBlocks = self.number_of_saccade_blocks numTrials = self.number_of_saccade_trials elif condition == 'Fixation': + self.display_fixation_instructions() numBlocks = self.number_of_fixation_blocks numTrials = self.number_of_fixation_trials elif condition == 'Pursuit': @@ -367,7 +374,7 @@ class EyeTrackingSaccadePursuit(SaccadePursuit.SPtask): numBlocks = self.number_of_pursuit_blocks numTrials = self.number_of_pursuit_trials elif condition == 'Necker': - self.display_text_screen(text=necker_instruct_text, + self.display_text_screen(#text=necker_instruct_text, image_file=self.necker_response_box_file, image_scale=self.necker_response_box_scale) numBlocks = self.number_of_necker_blocks @@ -412,17 +419,17 @@ class EyeTrackingSaccadePursuit(SaccadePursuit.SPtask): self.tracker.send_status(status) # psychopy.sound.Sound('C').play() self.tracker.start_recording() - if condition == 'Pursuit' and trial_num == 0: - self.display_fixation( - self.fixation_trial_time, self.tracker) + # if condition == 'Pursuit' and trial_num == 0: + # self.display_fixation( + # self.fixation_trial_time, self.tracker) data = self.run_trial( trial, block_num, trial_num, self.tracker) self.tracker.stop_recording() # psychopy.sound.Sound('C').play() else: - if condition == 'Pursuit' and trial_num == 0: - self.display_fixation( - self.fixation_trial_time, self.tracker) + # if condition == 'Pursuit' and trial_num == 0: + # self.display_fixation( + # self.fixation_trial_time, self.tracker) data = self.run_trial(trial, block_num, trial_num) data.update({'Condition': condition}) self.send_data(data) @@ -437,6 +444,7 @@ class EyeTrackingSaccadePursuit(SaccadePursuit.SPtask): self.display_fixation(0.5, []) self.save_data_to_csv() + # Display text to prepare for next block if block_num + 1 != numBlocks: self.display_break() @@ -636,6 +644,42 @@ class EyeTrackingSaccadePursuit(SaccadePursuit.SPtask): return keys + def display_fixation_instructions( + self, anti=False, bg_color=[0, 0, 0], wait_for_input=True, **kwargs): + + bg_color = convert_color_value(bg_color) + fg_color = convert_color_value([255, 255, 255]) + + backgroundRect = psychopy.visual.Rect( + self.experiment_window, fillColor=bg_color, units='norm', width=2, + height=2) + backgroundRect.draw() + + borderFrame = psychopy.visual.Rect( + self.experiment_window, lineColor=fg_color, units='deg', + width=self.pursuit_distance*2.1, height=3, pos=(0, self.instHeight)) + + dispText = '** Target Fixation Example **' + + textObject = psychopy.visual.TextStim( + self.experiment_window, text=dispText, color=fg_color, units='deg', + height=1, pos=(0, self.instHeight+3), **kwargs) + + fixation = psychopy.visual.TextStim( + self.experiment_window, text='+', color=self.stim_color, + height=self.fixation_size, units='deg', pos=[0, self.instHeight]) + + borderFrame.draw() + textObject.draw() + fixation.draw() + self.experiment_window.flip() + + keys = None + while keys != ['space']: + keys = psychopy.event.waitKeys() + + return keys + def display_rivalry_instructions( self, anti=False, bg_color=[0, 0, 0], wait_for_input=True, image_file=[], image_scale=0.25, **kwargs): @@ -704,8 +748,8 @@ class EyeTrackingSaccadePursuit(SaccadePursuit.SPtask): lineColor=color, lineWidth=lineSize) textObject.draw() - stim1.draw() - stim2.draw() + #stim1.draw() + #stim2.draw() stimBorder1.draw() stimBorder2.draw() stimFix1_top.draw() @@ -727,10 +771,35 @@ class EyeTrackingSaccadePursuit(SaccadePursuit.SPtask): self.experiment_window.flip() keys = None + showImages = False if wait_for_input: psychopy.core.wait(.2) # Prevents accidental key presses keys = psychopy.event.waitKeys() while keys != ['space']: + showImages = not showImages + if showImages: + textObject.draw() + stim1.draw() + stim2.draw() + stimBorder1.draw() + stimBorder2.draw() + stimFix1_top.draw() + stimFix1_left.draw() + stimFix2_bottom.draw() + stimFix2_right.draw() + imageObject.draw() + else: + textObject.draw() + # stim1.draw() + # stim2.draw() + stimBorder1.draw() + stimBorder2.draw() + stimFix1_top.draw() + stimFix1_left.draw() + stimFix2_bottom.draw() + stimFix2_right.draw() + imageObject.draw() + self.experiment_window.flip() keys = psychopy.event.waitKeys() self.experiment_window.flip() diff --git a/Screening.py b/Screening.py new file mode 100644 index 0000000..6118170 --- /dev/null +++ b/Screening.py @@ -0,0 +1,849 @@ +"""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 +import math +import numpy + +# Necesssary to access psychopy paths +import psychopy # noqa:F401 + +import eyelinker + +import SaccadePursuit + +disableTracker = False # For Debugging +mikeComp = False # Also for debugging + +# Experimental Parameters +monitor_name = 'testMonitor' +distance_to_monitor = 74 + +if mikeComp: + window_screen = 0 + monitor_px = [2560,1440] + monitor_width = 59 +else: + window_screen = 1 + monitor_px = [1440, 900] + monitor_width = 41 # 25.8 height + +isi_time = 2 # Interstimulus Interval +data_directory = os.path.join( + os.path.expanduser('~'), 'Desktop', 'ExperimentalData', 'Screening') +if mikeComp: + image_directory = os.path.join(os.getcwd(),'Images') +else: + image_directory = os.path.join( + os.path.expanduser('~'), 'Desktop', 'SaccadePursuitExperiment', 'Images') + +new_trial_sound = 'A' +instHeight = 6 + +# Saccade / Antisaccade Parameters +number_of_saccade_trials = 1 +number_of_saccade_blocks = 1 +saccade_distance = 15 # Degrees per direction +number_of_saccade_lights = 2 # Number of active lights per direction +saccade_time = 3 # Maximum Time +stimulus_size = 0.3 +stim_color = [1, -1, -1] +saccade_fixation_color = [255, 255, 255] +saccade_dot_color = [50, 50, 50] +antisaccade_instruct_file = os.path.join( + image_directory, 'AntiSaccadeInstruct2.tif') +antisaccade_file_scale = 1 + +# Pursuit Parameters +number_of_pursuit_trials = 1 +number_of_pursuit_blocks = 1 +pursuit_distance = 15 +pursuit_frequencies = [0.2] +pursuit_time = [10] + +# Necker Cube Parameters +number_of_necker_trials = 1 +number_of_necker_blocks = 1 +necker_time = 10 +necker_color = [190, 190, 190] +necker_bg_color = [30, 30, 30] +necker_scale = 0.25 +necker_file = os.path.join(image_directory, 'Necker1.tif') +necker_response_box_file = os.path.join(image_directory, 'neckerInstruction.tif') +necker_response_box_scale = 0.307 + +# Fixation Parameters +number_of_fixation_trials = 1 +number_of_fixation_blocks = 1 +fixation_size = stimulus_size*2 +fixation_trial_time = 5 + +# Binocular Rivalry Parameters +number_of_rivalry_trials = 1 +number_of_rivalry_blocks = 1 +rivalry_time = 10 +#rivalry_scale = 2.5 +rivalry_height = 1.5 +rivalry_width = 1 +rivalry_distance = 3 +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 = [190, 190, 190] +rivalry_border_width = 5 +response_box_file = os.path.join(image_directory, 'ResponseBox3.tif') +response_box_scale = 0.22 + +data_fields = [ + 'Subject', + 'Condition', + 'Block', + 'Trial', + 'Timestamp', + 'TrialType', + 'Duration', + 'Frequency', + 'Locations', +] + +# Add additional questions here +questionaire_dict = { + 'Run Target Fixation': True, + 'Run Smooth Pursuit': True, + 'Run Saccade': True, + 'Run Anti-Saccade': True, + 'Run Necker Cube': True, + 'Run Binocular Rivalry': False, +} + +questionaire_order = [ + 'Subject ID', + 'Session', + 'Timepoint', + 'Experimenter Initials', + 'Gender', + 'Run Target Fixation', + 'Run Smooth Pursuit', + 'Run Saccade', + 'Run Anti-Saccade', + 'Run Necker Cube', + 'Run Binocular Rivalry', +] + +instruct_text = [ + ('Welcome to the experiment'), + ( # '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.\n\nMove only your eyes.\n' + 'Remember: Please keep your head still with your forehead\n' + 'against the bar and your chin rested on the pad.'), + # 'You will be offered rests between sections.\n\n' + # 'Press any key to continue.'), +] + +saccade_instruct_text = ( + 'For this experiment, we want to know how accurately your ' + 'eyes can move from one position to another. You will see ' + 'many gray dots and a cross on the screen. Please focus on ' + 'the fixation cross in the beginning. When the red dot ' + 'appears, move your eyes to the red dot as quickly as ' + 'possible and fixate on the red dot. When you hear a sound, ' + 'move your eyes back to the fixation cross.\n\n' + 'Try not to blink while a red dot is displayed.\n\n' + 'You can then blink or close your eyes to rest for a few seconds.\n\n' + # 'Press any key to continue.' +) + +antisaccade_instruct_text = ( + 'For this experiment, we also want to know how accurately your ' + 'eyes can move from one point to another. Please focus on the ' + 'fixation cross. When the red dot appears, move your eyes to ' + 'the OPPOSITE direction of the target at approximately the ' + 'same distance from the fixation cross as the target as shown ' + 'by the arrow in the image below this text. The arrow will ' + 'not be present during the experiment. Move your eyes back ' + 'to the fixation point after each target disappears.\n\n' + 'Try not to blink while a red dot is displayed.\n\n' + 'You can then blink or close your eyes to rest for a few seconds.\n\n' + # 'Press any key to continue.' +) + +pursuit_instruct_text = ( + 'For this experiment, we want to know how well your eyes can ' + 'follow a target at different speeds. Please fixate the cross. ' + 'When the dot appears, follow the target with your eyes. You ' + 'will hear a sound when the trial is done. You may blink ' + 'until the next trial begins. A new trial will begin after ' + 'two seconds.\n\n' + 'Try not to blink while the dot is moving.\n\n' + 'You can then blink or close your eyes to rest for a few seconds.\n\n' + # 'Press any key to continue.' +) + +necker_instruct_text = ( + 'For this experiment, focus on the square. When you see 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. Refer to the image below this ' + 'text. The center button is not used during this experiment.\n\n' + 'Respond at any time during the stimulus.\n\n' + 'Try not to blink after the cube appears.\n\n' + 'You can blink or close your eyes to rest after the cube ' + 'disappears.\n\n' + # 'Press any key to continue.' +) + +rivalry_instruct_text = ( + 'For this experiment, you will see different images in your ' + 'left and right eyes through the mirrors.\n\n' + 'If you see a face, press the right button. If you see a ' + 'house, press the left button. If you perceive a mixture of ' + 'the face and house, press the center button.\n\n' + 'You can blink or close your eyes to rest for a few seconds ' + 'after the pictures disappear.\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.disableTracker = 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_saccade_lights = number_of_saccade_lights + 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 + self.necker_response_box_file = necker_response_box_file + self.necker_response_box_scale = necker_response_box_scale + self.response_box_scale = response_box_scale + self.antisaccade_instruct_file = antisaccade_instruct_file + self.antisaccade_file_scale = antisaccade_file_scale + self.instHeight = instHeight + + 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: + self.tracker.set_offline_mode() + self.tracker.close_edf() + self.tracker.transfer_edf() + self.tracker.close_connection() + fName = os.path.join(self.data_directory, + self.experiment_info['Subject ID'] + + self.experiment_info['Timepoint'] + '.edf') + if os.path.exists(fName): + baseName = os.path.join(self.data_directory, + 'ETSP_' + + self.experiment_info['Subject ID'] + + self.experiment_info['Session'] + + self.experiment_info['Timepoint']) + fName2 = baseName + '.edf' + if os.path.exists(fName2): + ii = 1 + newName = baseName+'('+str(ii)+')'+'.edf' + while os.path.exists(newName): + ii+=1 + newName = baseName+'('+str(ii)+')'+'.edf' + fName2 = newName + os.rename(fName, fName2) + subprocess.call(['edf2asc', fName2]) + + super(EyeTrackingSaccadePursuit, self).quit_experiment() + + def run(self): + self.chdir() + + ok = self.get_experiment_info_from_dialog( + additional_fields_dict=questionaire_dict, + field_order=questionaire_order) + + if not ok: + print('Experiment has been terminated.') + sys.exit(1) + + conditions = [] + if self.experiment_info['Run Target Fixation']: + conditions.append('Fixation') + if self.experiment_info['Run Smooth Pursuit']: + conditions.append('Pursuit') + if self.experiment_info['Run Saccade']: + conditions.append('Saccade') + if self.experiment_info['Run Anti-Saccade']: + conditions.append('AntiSaccade') + if self.experiment_info['Run Necker Cube']: + conditions.append('Necker') + if self.experiment_info['Run Binocular Rivalry']: + conditions.append('Rivalry') + + 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, + self.experiment_info['Subject ID'] + + self.experiment_info['Timepoint'] + '.edf', + 'BOTH' + ) + self.tracker.initialize_graphics() + self.tracker.open_edf() + self.tracker.initialize_tracker() + self.tracker.send_calibration_settings( + settings={ + 'preamble_text': 'Saccade Pursuit Experiment Plus Fixation and Necker Cube', } + ) + + 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() + self.tracker.send_command("track_search_limits=NO") + + # random.shuffle(conditions) + + condition_counter = 0 + for condition in conditions: + condition_counter += 1 + numBlocks = 1 + numTrials = 1 + if condition == 'Saccade': + self.display_saccade_instructions() + numBlocks = self.number_of_saccade_blocks + numTrials = self.number_of_saccade_trials + elif condition == 'AntiSaccade': + self.display_saccade_instructions(anti=True) + numBlocks = self.number_of_saccade_blocks + numTrials = self.number_of_saccade_trials + elif condition == 'Fixation': + self.display_fixation_instructions() + numBlocks = self.number_of_fixation_blocks + numTrials = self.number_of_fixation_trials + elif condition == 'Pursuit': + self.display_pursuit_instructions() + 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.necker_response_box_file, + image_scale=self.necker_response_box_scale) + numBlocks = self.number_of_necker_blocks + numTrials = self.number_of_necker_trials + elif condition == 'Rivalry': + self.display_rivalry_instructions( + image_file=self.response_box_file, + image_scale=self.response_box_scale) + 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): + printString = ("Condition: %s (%d/%d) Block: %d/%d Trial %d/%d" + % (condition, condition_counter, len(conditions), + block_num+1, numBlocks, trial_num+1, len(block))) + if condition == 'Saccade' or condition == 'AntiSaccade': + if trial['locations'][0] > 0: + printString = printString + " (Right)" + else: + printString = printString + " (Left)" + print(printString) + 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) + self.tracker.send_message('Location: %d,%d' % ( + trial['locations'][0], trial['locations'][1])) + status = '%s: Block %d, Trial %d' % ( + condition, block_num, trial_num) + self.tracker.send_status(status) + # psychopy.sound.Sound('C').play() + self.tracker.start_recording() + # if condition == 'Pursuit' and trial_num == 0: + # self.display_fixation( + # self.fixation_trial_time, self.tracker) + data = self.run_trial( + trial, block_num, trial_num, self.tracker) + self.tracker.stop_recording() + # psychopy.sound.Sound('C').play() + else: + # if condition == 'Pursuit' and trial_num == 0: + # self.display_fixation( + # self.fixation_trial_time, self.tracker) + 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() + + # Display text to prepare for next block + if block_num + 1 != numBlocks: + self.display_break() + + if condition == 'Saccade': + self.display_saccade_instructions() + elif condition == 'AntiSaccade': + self.display_saccade_instructions(anti=True) + elif condition == 'Fixation': + self.display_text_screen( + text='Prepare for next trial.') + elif condition == 'Pursuit': + self.display_pursuit_instructions() + elif condition == 'Necker': + self.display_text_screen(text='Remember:\n\n' + necker_instruct_text, + image_file=self.necker_response_box_file, + image_scale=self.necker_response_box_scale) + elif condition == 'Rivalry': + self.display_rivalry_instructions( + image_file=self.response_box_file, + image_scale=self.response_box_scale) + + 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() + + def display_pursuit_instructions( + self, bg_color=[0, 0, 0], wait_for_input=True, **kwargs): + + bg_color = convert_color_value(bg_color) + fg_color = convert_color_value([255, 255, 255]) + + backgroundRect = psychopy.visual.Rect( + self.experiment_window, fillColor=bg_color, units='norm', width=2, + height=2) + backgroundRect.draw() + + borderFrame = psychopy.visual.Rect( + self.experiment_window, lineColor=fg_color, units='deg', + width=self.pursuit_distance*2.1, height=1.5, pos=(0, self.instHeight)) + + textObject = psychopy.visual.TextStim( + self.experiment_window, text='** Smooth Pursuit Example **', color=fg_color, units='deg', + height=1, pos=(0, self.instHeight+2), **kwargs) + + color = self.stim_color + + Xposition = [0] + num_frames_per_second = 60 + counter = 0 + stim_frequency = [0.1] + stim_time = [10] + for freq in stim_frequency: + stim_frames = int(round(stim_time[counter]*num_frames_per_second)) + for time in range(stim_frames): + Xposition.append(math.sin( + freq*math.radians((time+1)*360/num_frames_per_second))*self.pursuit_distance) + counter += 1 + + stim = psychopy.visual.Circle( + self.experiment_window, radius=self.stimulus_size/2, + pos=(0, 0), fillColor=color, + lineColor=color, units='deg') + + keys = None + while keys != ['space']: + for Xpos in Xposition: + textObject.draw() + borderFrame.draw() + stim.pos = (Xpos, self.instHeight) + stim.draw() + self.experiment_window.flip() + keys = psychopy.event.getKeys() + if keys == ['space']: + break + + return keys + + def display_saccade_instructions( + self, anti=False, bg_color=[0, 0, 0], wait_for_input=True, **kwargs): + + bg_color = convert_color_value(bg_color) + fg_color = convert_color_value([255, 255, 255]) + + backgroundRect = psychopy.visual.Rect( + self.experiment_window, fillColor=bg_color, units='norm', width=2, + height=2) + backgroundRect.draw() + + borderFrame = psychopy.visual.Rect( + self.experiment_window, lineColor=fg_color, units='deg', + width=self.pursuit_distance*2.1, height=3, pos=(0, self.instHeight)) + + if anti: + dispText = '** Anti-Saccade Example **' + else: + dispText = '** Saccade Example **' + + textObject = psychopy.visual.TextStim( + self.experiment_window, text=dispText, color=fg_color, units='deg', + height=1, pos=(0, self.instHeight+3), **kwargs) + + color = self.stim_color + + stim = psychopy.visual.Circle( + self.experiment_window, radius=self.stimulus_size/2, + pos=(1, 0), fillColor=color, + lineColor=color, units='deg') + + #arrowVert = [(-0.8,0.1),(-0.8,-0.1),(-0.4,-0.1),(-0.4,-0.2),(0,0),(-0.4,0.2),(-0.4,0.1)] + arrowVert = numpy.array([(-.25, -2), (.25, -2), (.25, -.75), + (.5, -.75), (0, 0), (-.5, -.75), (-.25, -.75), (-.25, -2)]) + arrowVert = arrowVert + (0, self.instHeight + 5.25) + arrowStim = psychopy.visual.ShapeStim( + self.experiment_window, vertices=arrowVert, fillColor=color, size=0.5, lineColor=color, units='deg') + + fixation = psychopy.visual.TextStim( + self.experiment_window, text='+', color=self.saccade_fixation_color, + height=self.fixation_size, units='deg', pos=[0, self.instHeight]) + + stimList = [] + temp = list(range(-self.saccade_distance, self.saccade_distance, 1)) + for circleN in temp: + if circleN < 0: + tempCoord = circleN + else: + tempCoord = circleN+1 + + tempStim = psychopy.visual.Circle( + self.experiment_window, radius=self.stimulus_size/2, + pos=[tempCoord, self.instHeight], fillColor=self.saccade_dot_color, + lineColor=self.saccade_dot_color, units='deg') + stimList.append(tempStim) + + temp1 = random.sample( + range(1, self.saccade_distance+1), self.number_of_saccade_lights) + temp2 = random.sample( + range(1, self.saccade_distance+1), self.number_of_saccade_lights) + saccade_locations = [x*-1 for x in temp1] + temp2 + + keys = None + while keys != ['space']: + for ii in range(4): + if anti: + stim_time = 5 + else: + stim_time = round(random.uniform(1, self.saccade_time), 3) + for frameN in range(90): + textObject.draw() + borderFrame.draw() + for s in stimList: + s.draw() + fixation.color = color + fixation.draw() + self.experiment_window.flip() + keys = psychopy.event.getKeys() + if keys == ['space']: + break + if keys == ['space']: + break + Xpos = random.randint(1, 15)*random.choice([-1, 1]) + for frameN in range(int(round(stim_time*60))): + textObject.draw() + borderFrame.draw() + for s in stimList: + s.draw() + stim.pos = (Xpos, self.instHeight) + stim.draw() + fixation.color = self.saccade_fixation_color + fixation.draw() + if anti and frameN > stim_time*30: + arrowVert2 = arrowVert + (-Xpos*2, 0) + arrowStim.vertices = arrowVert2 + arrowStim.draw() + self.experiment_window.flip() + keys = psychopy.event.getKeys() + if keys == ['space']: + break + if keys == ['space']: + break + + if keys == ['space']: + break + if anti: + textObject.draw() + borderFrame.draw() + for s in stimList: + s.draw() + fixation.color = color + fixation.draw() + self.experiment_window.flip() + keys = psychopy.event.waitKeys() + + return keys + + def display_fixation_instructions( + self, anti=False, bg_color=[0, 0, 0], wait_for_input=True, **kwargs): + + bg_color = convert_color_value(bg_color) + fg_color = convert_color_value([255, 255, 255]) + + backgroundRect = psychopy.visual.Rect( + self.experiment_window, fillColor=bg_color, units='norm', width=2, + height=2) + backgroundRect.draw() + + borderFrame = psychopy.visual.Rect( + self.experiment_window, lineColor=fg_color, units='deg', + width=self.pursuit_distance*2.1, height=3, pos=(0, self.instHeight)) + + dispText = '** Target Fixation Example **' + + textObject = psychopy.visual.TextStim( + self.experiment_window, text=dispText, color=fg_color, units='deg', + height=1, pos=(0, self.instHeight+3), **kwargs) + + fixation = psychopy.visual.TextStim( + self.experiment_window, text='+', color=self.stim_color, + height=self.fixation_size, units='deg', pos=[0, self.instHeight]) + + borderFrame.draw() + textObject.draw() + fixation.draw() + self.experiment_window.flip() + + keys = None + while keys != ['space']: + keys = psychopy.event.waitKeys() + + return keys + + def display_rivalry_instructions( + self, anti=False, bg_color=[0, 0, 0], wait_for_input=True, + image_file=[], image_scale=0.25, **kwargs): + + fg_color = convert_color_value([255, 255, 255]) + bg_color = convert_color_value(bg_color) + + backgroundRect = psychopy.visual.Rect( + self.experiment_window, fillColor=bg_color, units='norm', width=2, + height=2) + backgroundRect.draw() + + textObject = psychopy.visual.TextStim( + self.experiment_window, text='** Binocular Rivalry Example **', color=fg_color, units='deg', + height=1, pos=(0, self.instHeight+2), **kwargs) + + color = self.rivalry_border_color + lineSize = self.rivalry_border_width + + stimPos = self.experiment_window.size[0]/4 + stimPos = psychopy.tools.monitorunittools.pix2deg( + stimPos, self.experiment_monitor) + stim1 = psychopy.visual.ImageStim( + self.experiment_window, + image=self.rivalry_file1, pos=(-stimPos, 0)) + stim1.size = (self.rivalry_width, self.rivalry_height) + stim2 = psychopy.visual.ImageStim( + self.experiment_window, + image=self.rivalry_file2, pos=(stimPos, 0)) + stim2.size = (self.rivalry_width, self.rivalry_height) + + borderScale = stim1.size[0]*0.4 + borderWidth1 = stim1.size[0]+borderScale + borderHeight1 = stim1.size[1]+borderScale + borderWidth2 = stim2.size[0]+borderScale + borderHeight2 = stim2.size[1]+borderScale + stimBorder1 = psychopy.visual.Rect( + self.experiment_window, width=borderWidth1, + height=borderHeight1, + pos=stim1.pos, lineWidth=lineSize, + lineColor=color, units='deg') + stimBorder2 = psychopy.visual.Rect( + self.experiment_window, width=borderWidth2, + height=borderHeight2, + pos=stim2.pos, lineWidth=lineSize, + lineColor=color, units='deg') + stimFix1_top = psychopy.visual.Line( + self.experiment_window, start=( + stim1.pos[0], stim1.pos[1]+borderHeight1/2), + end=(stim1.pos[0], stim1.pos[1]+stim1.size[1]/1.9), units='deg', + lineColor=color, lineWidth=lineSize) + stimFix1_left = psychopy.visual.Line( + self.experiment_window, start=( + stim1.pos[0]-borderWidth1/2, stim1.pos[1]), + end=(stim1.pos[0]-stim1.size[0]/1.9, stim1.pos[1]), units='deg', + lineColor=color, lineWidth=lineSize) + stimFix2_bottom = psychopy.visual.Line( + self.experiment_window, start=( + stim2.pos[0], stim2.pos[1]-borderHeight2/2), + end=(stim2.pos[0], stim2.pos[1]-stim2.size[1]/1.9), units='deg', + lineColor=color, lineWidth=lineSize) + stimFix2_right = psychopy.visual.Line( + self.experiment_window, start=( + stim2.pos[0]+borderWidth2/2, stim2.pos[1]), + end=(stim2.pos[0]+stim2.size[0]/1.9, stim2.pos[1]), units='deg', + lineColor=color, lineWidth=lineSize) + + textObject.draw() + #stim1.draw() + #stim2.draw() + stimBorder1.draw() + stimBorder2.draw() + stimFix1_top.draw() + stimFix1_left.draw() + stimFix2_bottom.draw() + stimFix2_right.draw() + + imageObject = psychopy.visual.ImageStim( + self.experiment_window, units='pix', + image=image_file) + sizex = int(round(imageObject.size[0])*image_scale) + sizey = int(round(imageObject.size[1])*image_scale) + bottomOfScreen = int( + math.floor(-self.experiment_window.size[1]/2+sizey/2))+5 + imageObject.size = [sizex, sizey] + imageObject.pos = (0, bottomOfScreen) + imageObject.draw() + + self.experiment_window.flip() + + keys = None + showImages = False + if wait_for_input: + psychopy.core.wait(.2) # Prevents accidental key presses + keys = psychopy.event.waitKeys() + while keys != ['space']: + showImages = not showImages + if showImages: + textObject.draw() + stim1.draw() + stim2.draw() + stimBorder1.draw() + stimBorder2.draw() + stimFix1_top.draw() + stimFix1_left.draw() + stimFix2_bottom.draw() + stimFix2_right.draw() + imageObject.draw() + else: + textObject.draw() + # stim1.draw() + # stim2.draw() + stimBorder1.draw() + stimBorder2.draw() + stimFix1_top.draw() + stimFix1_left.draw() + stimFix2_bottom.draw() + stimFix2_right.draw() + imageObject.draw() + self.experiment_window.flip() + keys = psychopy.event.waitKeys() + self.experiment_window.flip() + + +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, + pursuit_time=pursuit_time, stim_color=stim_color, + pursuit_frequencies=pursuit_frequencies, + saccade_distance=saccade_distance, + saccade_time=saccade_time, + number_of_saccade_lights=number_of_saccade_lights, + saccade_fixation_color=convert_color_value(saccade_fixation_color), + saccade_dot_color=convert_color_value(saccade_dot_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_height=rivalry_height, + rivalry_width=rivalry_width, + 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, + new_trial_sound=new_trial_sound +) + +if __name__ == '__main__': + try: + experiment.run() + except Exception: + print(traceback.format_exc()) + if not experiment.quit: + experiment.quit_experiment() diff --git a/calibrate.py b/calibrate.py new file mode 100644 index 0000000..46b875d --- /dev/null +++ b/calibrate.py @@ -0,0 +1,63 @@ +from __future__ import division +from __future__ import print_function + +import os +import random +import sys +import traceback +import subprocess +import math +import numpy + +# Necesssary to access psychopy paths +import psychopy # noqa:F401 + +import eyelinker + +import SaccadePursuit + +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] + + +#myPR655 = PR655(5) +#print(myPR655) +#myPR655.getLum() +#nm, power = myPR655.getLastSpectrum() + +#print(nm) +#print(power) + +#phot = hardware.findPhotometer() +#print(phot) +#print(phot.measure()) +#print(phot.getLum()) + +#bg_color = convert_color_value(bg_color) +fg_color = convert_color_value([0,0,255]) + +monitor_px = [1440,900] +window_screen = 1 + +experiment_monitor = psychopy.monitors.Monitor( + 'testMonitor', width=41, + distance=74) +experiment_monitor.setSizePix(monitor_px) + +experiment_window = psychopy.visual.Window(monitor_px, + monitor=experiment_monitor, fullscr=True, color=fg_color, + colorSpace='rgb', units='deg', allowGUI=False, screen=1) + +keys = psychopy.event.waitKeys() + +#backgroundRect = psychopy.visual.Rect( +# experiment_window, fillColor=fg_color, units='norm', width=2, +# height=2) +#backgroundRect.draw() + diff --git a/eyelinker.py b/eyelinker.py old mode 100755 new mode 100644 index 02d4ba7..8608d81 --- a/eyelinker.py +++ b/eyelinker.py @@ -1,273 +1,273 @@ -from __future__ import print_function -from __future__ import division - -import os -import sys -import time - -import pylink as pl -#from PsychoPyCustomDisplay import PsychoPyCustomDisplay -from EyeLinkCoreGraphicsPsychoPy import EyeLinkCoreGraphicsPsychoPy - -import psychopy.event -import psychopy.visual - - -class EyeLinker(object): - def __init__(self, window, filename, eye): - if len(filename) > 12: - raise ValueError( - 'EDF filename must be at most 12 characters long including the extension.') - - if filename[-4:] != '.edf': - raise ValueError( - 'Please include the .edf extension in the filename.') - - if eye not in ('LEFT', 'RIGHT', 'BOTH'): - raise ValueError('eye must be set to LEFT, RIGHT, or BOTH.') - - self.window = window - self.edf_filename = filename - self.edf_open = False - self.eye = eye - self.resolution = tuple(window.size) - self.tracker = pl.EyeLink() - #self.genv = PsychoPyCustomDisplay(self.window, self.tracker) - self.genv = EyeLinkCoreGraphicsPsychoPy(self.tracker, self.window) - - if all(i >= 0.5 for i in self.window.color): - self.text_color = (-1, -1, -1) - else: - self.text_color = (1, 1, 1) - - def initialize_graphics(self): - self.set_offline_mode() - pl.openGraphicsEx(self.genv) - - def initialize_tracker(self): - if not self.edf_open: - raise RuntimeError('EDF file must be open before tracker can be initialized.') - - pl.flushGetkeyQueue() - self.set_offline_mode() - - self.send_command("screen_pixel_coords = 0 0 %d %d" % self.resolution) - self.send_message("DISPLAY_COORDS 0 0 %d %d" % self.resolution) - - #self.tracker.setFileEventFilter( - # "LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON,INPUT") - #self.tracker.setFileSampleFilter( - # "LEFT,RIGHT,GAZE,AREA,GAZERES,STATUS") - #self.tracker.setLinkEventFilter( - # "LEFT,RIGHT,FIXATION,FIXUPDATE,SACCADE,BLINK,BUTTON,INPUT") - #self.tracker.setLinkSampleFilter( - # "LEFT,RIGHT,GAZE,GAZERES,AREA,STATUS") - self.tracker.sendCommand("file_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON,INPUT") - self.tracker.sendCommand("link_event_filter = LEFT,RIGHT,FIXATION,FIXUPDATE,SACCADE,BLINK,BUTTON,INPUT") - self.tracker.sendCommand("file_sample_data = LEFT,RIGHT,GAZE,GAZERES,PUPIL,HREF,AREA,STATUS,HTARGET,INPUT") - self.tracker.sendCommand("link_sample_data = LEFT,RIGHT,GAZE,GAZERES,PUPIL,HREF,AREA,STATUS,HTARGET,INPUT") - - def send_calibration_settings(self, settings=None): - defaults = { - 'automatic_calibration_pacing': 1000, - 'background_color': (0, 0, 0), - 'calibration_area_proportion': (0.5, 0.5), - 'calibration_type': 'HV5', - 'elcl_configuration': 'BTABLER', - 'enable_automatic_calibration': 'YES', - 'error_sound': '', - 'foreground_color': (255, 255, 255), - 'good_sound': '', - 'preamble_text': None, - 'pupil_size_diameter': 'NO', - 'saccade_acceleration_threshold': 9500, - 'saccade_motion_threshold': 0.15, - 'saccade_pursuit_fixup': 60, - 'saccade_velocity_threshold': 30, - 'sample_rate': 1000, - 'target_sound': '', - 'validation_area_proportion': (0.5, 0.5), - } - - if settings is None: - settings = {} - - settings.update(defaults) - - self.send_command('elcl_select_configuration = %s' % settings['elcl_configuration']) - - pl.setCalibrationColors(settings['foreground_color'], settings['background_color']) - pl.setCalibrationSounds( - settings['target_sound'], settings['good_sound'], settings['error_sound']) - - if self.eye in ('LEFT', 'RIGHT'): - self.send_command('active_eye = %s' % self.eye) - - self.send_command( - 'automatic_calibration_pacing = %i' % settings['automatic_calibration_pacing']) - - if self.eye == 'BOTH': - self.send_command('binocular_enabled = YES') - else: - self.send_command('binocular_enabled = NO') - - #self.send_command( - # 'calibration_area_proportion %f %f' % settings['calibration_area_proportion']) - - - self.send_command('calibration_type = %s' % settings['calibration_type']) - self.send_command( - 'enable_automatic_calibration = %s' % settings['enable_automatic_calibration']) - if settings['preamble_text'] is not None: - self.send_command('add_file_preamble_text %s' % '"' + settings['preamble_text'] + '"') - self.send_command('pupil_size_diameter = %s' % settings['pupil_size_diameter']) - #self.send_command( - # 'saccade_acceleration_threshold = %i' % settings['saccade_acceleration_threshold']) - #self.send_command('saccade_motion_threshold = %i' % settings['saccade_motion_threshold']) - #self.send_command('saccade_pursuit_fixup = %i' % settings['saccade_pursuit_fixup']) - #self.send_command( - # 'saccade_velocity_threshold = %i' % settings['saccade_velocity_threshold']) - self.send_command('sample_rate = %i' % settings['sample_rate']) - #self.send_command( - # 'validation_area_proportion %f %f' % settings['validation_area_proportion']) - - def open_edf(self): - self.tracker.openDataFile(self.edf_filename) - self.edf_open = True - - def close_edf(self): - self.tracker.closeDataFile() - self.edf_open = False - - def transfer_edf(self, newFilename=None): - if not newFilename: - newFilename = self.edf_filename - - # Prevents timeouts due to excessive printing - sys.stdout = open(os.devnull, "w") - self.tracker.receiveDataFile(self.edf_filename, newFilename) - sys.stdout = sys.__stdout__ - print(newFilename + ' has been transferred successfully.') - - def setup_tracker(self): - self.window.flip() - self.tracker.doTrackerSetup() - - def display_eyetracking_instructions(self): - self.window.flip() - - psychopy.visual.Circle( - self.window, units='pix', radius=18, lineColor='black', fillColor='white' - ).draw() - psychopy.visual.Circle( - self.window, units='pix', radius=6, lineColor='black', fillColor='black' - ).draw() - - psychopy.visual.TextStim( - self.window, text='Sometimes a target that looks like this will appear.', - color=self.text_color, units='norm', pos=(0, 0.22), height=0.05 - ).draw() - - psychopy.visual.TextStim( - self.window, color=self.text_color, units='norm', pos=(0, -0.18), height=0.05, - text='We use it to calibrate the eye tracker. Stare at it whenever you see it.' - ).draw() - - psychopy.visual.TextStim( - self.window, color=self.text_color, units='norm', pos=(0, -0.28), height=0.05, - text='Press any key to continue.' - ).draw() - - self.window.flip() - psychopy.event.waitKeys() - self.window.flip() - - def calibrate(self, text=None): - self.window.flip() - - if text is None: - text = ( - 'Experimenter:\n' - 'If you would like to calibrate, press space.\n' - 'To skip calibration, press the escape key.' - ) - - psychopy.visual.TextStim( - self.window, text=text, pos=(0, 0), height=0.05, units='norm', color=self.text_color - ).draw() - - self.window.flip() - - keys = psychopy.event.waitKeys(keyList=['escape', 'space']) - - self.window.flip() - - if 'space' in keys: - self.tracker.doTrackerSetup() - - def drift_correct(self, position=None, setup=1): - if position is None: - position = tuple([int(round(i/2)) for i in self.resolution]) - - try: - self.tracker.doDriftCorrect(position[0], position[1], 1, setup) - self.tracker.applyDriftCorrect() - except RuntimeError as e: - print(e.message) - #self.tracker.doTrackerSetup() - - def record(self, trial_func): - def wrapped_func(): - self.start_recording() - trial_func() - self.stop_recording() - return wrapped_func - - def start_recording(self): - self.tracker.startRecording(1, 1, 1, 1) - time.sleep(.1) # required - - def stop_recording(self): - time.sleep(.1) # required - self.tracker.stopRecording() - - @property - def gaze_data(self): - sample = self.tracker.getNewestSample() - - if self.eye == 'LEFT': - return sample.getLeftEye().getGaze() - elif self.eye == 'RIGHT': - return sample.getRightEye().getGaze() - else: - return (sample.getLeftEye().getGaze(), sample.getRightEye().getGaze()) - - @property - def pupil_size(self): - sample = self.tracker.getNewestSample() - - if self.eye == 'LEFT': - return sample.getLeftEye().getPupilSize() - elif self.eye == 'RIGHT': - return sample.getRightEye().getPupilSize() - else: - return (sample.getLeftEye().getPupilSize(), sample.getRightEye().getPupilSize()) - - def set_offline_mode(self): - self.tracker.setOfflineMode() - - def send_command(self, cmd): - self.tracker.sendCommand(cmd) - - def send_message(self, msg): - self.tracker.sendMessage(msg) - - def send_status(self, status): - if len(status) >= 80: - print('Warning: Status should be less than 80 characters.') - - self.send_command("record_status_message '%s'" % status) - - def close_connection(self): - self.tracker.close() - pl.closeGraphics() +from __future__ import print_function +from __future__ import division + +import os +import sys +import time + +import pylink as pl +#from PsychoPyCustomDisplay import PsychoPyCustomDisplay +from EyeLinkCoreGraphicsPsychoPy import EyeLinkCoreGraphicsPsychoPy + +import psychopy.event +import psychopy.visual + + +class EyeLinker(object): + def __init__(self, window, filename, eye): + if len(filename) > 12: + raise ValueError( + 'EDF filename must be at most 12 characters long including the extension.') + + if filename[-4:] != '.edf': + raise ValueError( + 'Please include the .edf extension in the filename.') + + if eye not in ('LEFT', 'RIGHT', 'BOTH'): + raise ValueError('eye must be set to LEFT, RIGHT, or BOTH.') + + self.window = window + self.edf_filename = filename + self.edf_open = False + self.eye = eye + self.resolution = tuple(window.size) + self.tracker = pl.EyeLink() + #self.genv = PsychoPyCustomDisplay(self.window, self.tracker) + self.genv = EyeLinkCoreGraphicsPsychoPy(self.tracker, self.window) + + if all(i >= 0.5 for i in self.window.color): + self.text_color = (-1, -1, -1) + else: + self.text_color = (1, 1, 1) + + def initialize_graphics(self): + self.set_offline_mode() + pl.openGraphicsEx(self.genv) + + def initialize_tracker(self): + if not self.edf_open: + raise RuntimeError('EDF file must be open before tracker can be initialized.') + + pl.flushGetkeyQueue() + self.set_offline_mode() + + self.send_command("screen_pixel_coords = 0 0 %d %d" % self.resolution) + self.send_message("DISPLAY_COORDS 0 0 %d %d" % self.resolution) + + #self.tracker.setFileEventFilter( + # "LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON,INPUT") + #self.tracker.setFileSampleFilter( + # "LEFT,RIGHT,GAZE,AREA,GAZERES,STATUS") + #self.tracker.setLinkEventFilter( + # "LEFT,RIGHT,FIXATION,FIXUPDATE,SACCADE,BLINK,BUTTON,INPUT") + #self.tracker.setLinkSampleFilter( + # "LEFT,RIGHT,GAZE,GAZERES,AREA,STATUS") + self.tracker.sendCommand("file_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON,INPUT") + self.tracker.sendCommand("link_event_filter = LEFT,RIGHT,FIXATION,FIXUPDATE,SACCADE,BLINK,BUTTON,INPUT") + self.tracker.sendCommand("file_sample_data = LEFT,RIGHT,GAZE,GAZERES,PUPIL,HREF,AREA,STATUS,HTARGET,INPUT") + self.tracker.sendCommand("link_sample_data = LEFT,RIGHT,GAZE,GAZERES,PUPIL,HREF,AREA,STATUS,HTARGET,INPUT") + + def send_calibration_settings(self, settings=None): + defaults = { + 'automatic_calibration_pacing': 1000, + 'background_color': (0, 0, 0), + 'calibration_area_proportion': (0.5, 0.5), + 'calibration_type': 'HV5', + 'elcl_configuration': 'BTABLER', + 'enable_automatic_calibration': 'YES', + 'error_sound': '', + 'foreground_color': (255, 255, 255), + 'good_sound': '', + 'preamble_text': None, + 'pupil_size_diameter': 'NO', + 'saccade_acceleration_threshold': 9500, + 'saccade_motion_threshold': 0.15, + 'saccade_pursuit_fixup': 60, + 'saccade_velocity_threshold': 30, + 'sample_rate': 1000, + 'target_sound': '', + 'validation_area_proportion': (0.5, 0.5), + } + + if settings is None: + settings = {} + + settings.update(defaults) + + self.send_command('elcl_select_configuration = %s' % settings['elcl_configuration']) + + pl.setCalibrationColors(settings['foreground_color'], settings['background_color']) + pl.setCalibrationSounds( + settings['target_sound'], settings['good_sound'], settings['error_sound']) + + if self.eye in ('LEFT', 'RIGHT'): + self.send_command('active_eye = %s' % self.eye) + + self.send_command( + 'automatic_calibration_pacing = %i' % settings['automatic_calibration_pacing']) + + if self.eye == 'BOTH': + self.send_command('binocular_enabled = YES') + else: + self.send_command('binocular_enabled = NO') + + #self.send_command( + # 'calibration_area_proportion %f %f' % settings['calibration_area_proportion']) + + + self.send_command('calibration_type = %s' % settings['calibration_type']) + self.send_command( + 'enable_automatic_calibration = %s' % settings['enable_automatic_calibration']) + if settings['preamble_text'] is not None: + self.send_command('add_file_preamble_text %s' % '"' + settings['preamble_text'] + '"') + self.send_command('pupil_size_diameter = %s' % settings['pupil_size_diameter']) + #self.send_command( + # 'saccade_acceleration_threshold = %i' % settings['saccade_acceleration_threshold']) + #self.send_command('saccade_motion_threshold = %i' % settings['saccade_motion_threshold']) + #self.send_command('saccade_pursuit_fixup = %i' % settings['saccade_pursuit_fixup']) + #self.send_command( + # 'saccade_velocity_threshold = %i' % settings['saccade_velocity_threshold']) + self.send_command('sample_rate = %i' % settings['sample_rate']) + #self.send_command( + # 'validation_area_proportion %f %f' % settings['validation_area_proportion']) + + def open_edf(self): + self.tracker.openDataFile(self.edf_filename) + self.edf_open = True + + def close_edf(self): + self.tracker.closeDataFile() + self.edf_open = False + + def transfer_edf(self, newFilename=None): + if not newFilename: + newFilename = self.edf_filename + + # Prevents timeouts due to excessive printing + sys.stdout = open(os.devnull, "w") + self.tracker.receiveDataFile(self.edf_filename, newFilename) + sys.stdout = sys.__stdout__ + print(newFilename + ' has been transferred successfully.') + + def setup_tracker(self): + self.window.flip() + self.tracker.doTrackerSetup() + + def display_eyetracking_instructions(self): + self.window.flip() + + psychopy.visual.Circle( + self.window, units='pix', radius=18, lineColor='black', fillColor='white' + ).draw() + psychopy.visual.Circle( + self.window, units='pix', radius=6, lineColor='black', fillColor='black' + ).draw() + + # psychopy.visual.TextStim( + # self.window, text='Sometimes a target that looks like this will appear.', + # color=self.text_color, units='norm', pos=(0, 0.22), height=0.05 + # ).draw() + + # psychopy.visual.TextStim( + # self.window, color=self.text_color, units='norm', pos=(0, -0.18), height=0.05, + # text='We use it to calibrate the eye tracker. Stare at it whenever you see it.' + # ).draw() + + # psychopy.visual.TextStim( + # self.window, color=self.text_color, units='norm', pos=(0, -0.28), height=0.05, + # text='Press any key to continue.' + # ).draw() + + self.window.flip() + psychopy.event.waitKeys() + self.window.flip() + + def calibrate(self, text=None): + self.window.flip() + + if text is None: + text = ( + 'Experimenter:\n' + 'If you would like to calibrate, press space.\n' + 'To skip calibration, press the escape key.' + ) + + psychopy.visual.TextStim( + self.window, text=text, pos=(0, 0), height=0.05, units='norm', color=self.text_color + ).draw() + + self.window.flip() + + keys = psychopy.event.waitKeys(keyList=['escape', 'space']) + + self.window.flip() + + if 'space' in keys: + self.tracker.doTrackerSetup() + + def drift_correct(self, position=None, setup=1): + if position is None: + position = tuple([int(round(i/2)) for i in self.resolution]) + + try: + self.tracker.doDriftCorrect(position[0], position[1], 1, setup) + self.tracker.applyDriftCorrect() + except RuntimeError as e: + print(e.message) + #self.tracker.doTrackerSetup() + + def record(self, trial_func): + def wrapped_func(): + self.start_recording() + trial_func() + self.stop_recording() + return wrapped_func + + def start_recording(self): + self.tracker.startRecording(1, 1, 1, 1) + time.sleep(.1) # required + + def stop_recording(self): + time.sleep(.1) # required + self.tracker.stopRecording() + + @property + def gaze_data(self): + sample = self.tracker.getNewestSample() + + if self.eye == 'LEFT': + return sample.getLeftEye().getGaze() + elif self.eye == 'RIGHT': + return sample.getRightEye().getGaze() + else: + return (sample.getLeftEye().getGaze(), sample.getRightEye().getGaze()) + + @property + def pupil_size(self): + sample = self.tracker.getNewestSample() + + if self.eye == 'LEFT': + return sample.getLeftEye().getPupilSize() + elif self.eye == 'RIGHT': + return sample.getRightEye().getPupilSize() + else: + return (sample.getLeftEye().getPupilSize(), sample.getRightEye().getPupilSize()) + + def set_offline_mode(self): + self.tracker.setOfflineMode() + + def send_command(self, cmd): + self.tracker.sendCommand(cmd) + + def send_message(self, msg): + self.tracker.sendMessage(msg) + + def send_status(self, status): + if len(status) >= 80: + print('Warning: Status should be less than 80 characters.') + + self.send_command("record_status_message '%s'" % status) + + def close_connection(self): + self.tracker.close() + pl.closeGraphics() diff --git a/template.py b/template.py old mode 100755 new mode 100644 index df22a0d..e3e9af5 --- a/template.py +++ b/template.py @@ -121,9 +121,9 @@ class BaseExperiment(object): """ overwrite_dlg = psychopy.gui.Dlg( - 'New File?', labelButtonOK='New File', - labelButtonCancel='Cancel', screen=0) - overwrite_dlg.addText('File already exists.') + 'Overwrite?', labelButtonOK='Overwrite', + labelButtonCancel='New File', screen=0) + overwrite_dlg.addText('File already exists. Overwrite?') overwrite_dlg.show() return overwrite_dlg.OK @@ -181,7 +181,7 @@ class BaseExperiment(object): if os.path.isfile(filename + '.txt'): if self.overwrite_ok is None: self.overwrite_ok = self._confirm_overwrite() - if self.overwrite_ok: + if not self.overwrite_ok: # If the file exists make a new filename i = 1 new_filename = filename + '(' + str(i) + ')' @@ -189,8 +189,6 @@ class BaseExperiment(object): i += 1 new_filename = filename + '(' + str(i) + ')' filename = new_filename - else: - raise Exception('Filename Error') filename = filename + '.txt' @@ -217,7 +215,7 @@ class BaseExperiment(object): if os.path.isfile(data_filename + '.csv'): if self.overwrite_ok is None: self.overwrite_ok = self._confirm_overwrite() - if self.overwrite_ok: + if not self.overwrite_ok: # If the file exists and we can't overwrite make a new filename i = 1 new_filename = data_filename + '(' + str(i) + ')' @@ -225,8 +223,6 @@ class BaseExperiment(object): i += 1 new_filename = data_filename + '(' + str(i) + ')' data_filename = new_filename - else: - raise Exception('Filename Error') self.experiment_data_filename = data_filename + '.csv' @@ -378,7 +374,10 @@ class BaseExperiment(object): bottomOfScreen = int(math.floor(-self.experiment_window.size[1]/2+sizey/2))+5 imageObject.size = [sizex,sizey] #imageObject.pos = (0,belowText) - imageObject.pos = (0,bottomOfScreen) + if text: + imageObject.pos = (0,bottomOfScreen) + else: + imageObject.pos = (0,0) imageObject.draw() else: textObject.draw()