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.6 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()