You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
472 lines
18 KiB
472 lines
18 KiB
"""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 = 41 |
|
distance_to_monitor = 74 |
|
monitor_px = [1440,900] |
|
window_screen = 1 |
|
|
|
disableTracker = False # For Debugging |
|
isi_time = 2 # Interstimulus Interval |
|
data_directory = os.path.join( |
|
os.path.expanduser('~'), 'Desktop', 'ExperimentalData', 'SaccadePursuitEyeTracking') |
|
image_directory = os.path.join(os.getcwd(),'Images') |
|
new_trial_sound = 'A' |
|
|
|
# Saccade / Antisaccade Parameters |
|
number_of_saccade_trials = 1 |
|
number_of_saccade_blocks = 1 |
|
saccade_distance = 15 # Degrees 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.1,0.2,0.4] |
|
pursuit_time = [40,20,15] |
|
|
|
# Necker Cube Parameters |
|
number_of_necker_trials = 1 |
|
number_of_necker_blocks = 4 |
|
necker_time = 60 |
|
necker_color = [190,190,190] |
|
necker_bg_color = [30,30,30] |
|
necker_scale = 0.5 |
|
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 |
|
|
|
# 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 = [190,190,190] |
|
rivalry_border_width = 5 |
|
rivalry_distance = 4 |
|
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 Fixation': True, |
|
'Run Smooth Pursuit': True, |
|
'Run Saccade': True, |
|
'Run Anti-Saccade': True, |
|
'Run Necker Cube': True, |
|
'Run Binocular Rivalry': False, |
|
} |
|
|
|
questionaire_order=[ |
|
'Subject Identifier', |
|
'Age', |
|
'Experimenter Initials', |
|
'Gender', |
|
'Run Fixation', |
|
'Run Smooth Pursuit', |
|
'Run Saccade', |
|
'Run Anti-Saccade', |
|
'Run Necker Cube', |
|
'Run Binocular Rivalry', |
|
] |
|
|
|
instruct_text = [ |
|
('Welcome to the experiment. Press any key 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 be offered breaks in 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.' |
|
) |
|
|
|
fixation_instruct_text = ( |
|
'For this experiment, we want to know how well you can keep ' |
|
'your eyes fixed on a target without moving. When the cross ' |
|
'appears, please fixate on it.\n\n' |
|
'Try not to move your eyes from the cross or blink until it ' |
|
'disappears.\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.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 |
|
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 |
|
|
|
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( |
|
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 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, |
|
'ETSP' + self.experiment_info['Subject Identifier'] + '.edf', |
|
'BOTH' |
|
) |
|
self.tracker.initialize_graphics() |
|
self.tracker.open_edf() |
|
self.tracker.send_command("add_file_preamble_text 'Saccade Pursuit Experiment Plus Fixation and Necker Cube'") |
|
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(conditions) |
|
|
|
condition_counter = 0 |
|
for condition in 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, |
|
image_file = antisaccade_instruct_file, |
|
image_scale = self.antisaccade_file_scale) |
|
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.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_text_screen(text=rivalry_instruct_text, |
|
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) |
|
if not self.disableTracker: |
|
self.tracker.drift_correct() |
|
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(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) |
|
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() |
|
data = self.run_trial(trial, block_num, trial_num, self.tracker) |
|
self.tracker.stop_recording() |
|
#psychopy.sound.Sound('C').play() |
|
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, |
|
image_file = antisaccade_instruct_file, |
|
image_scale = self.antisaccade_file_scale) |
|
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.necker_response_box_file, |
|
image_scale=self.necker_response_box_scale) |
|
elif condition=='Rivalry': |
|
self.display_text_screen(text='Remember:\n\n' + rivalry_instruct_text, |
|
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() |
|
|
|
|
|
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, |
|
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_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, |
|
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()
|
|
|