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.
273 lines
9.9 KiB
273 lines
9.9 KiB
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()
|
|
|