Source code for psychopy.iohub.devices.eyetracker.hw.mouse.eyetracker

# -*- coding: utf-8 -*-
# Part of the PsychoPy library
# Copyright (C) 2012-2020 iSolver Software Solutions (C) 2021 Open Science Tools Ltd.
# Distributed under the terms of the GNU General Public License (GPL).
from psychopy.iohub.errors import print2err, printExceptionDetailsToStdErr
from psychopy.iohub.constants import EyeTrackerConstants, EventConstants
from psychopy.iohub.devices import Computer, Device
from psychopy.iohub.devices.eyetracker import EyeTrackerDevice
from psychopy.iohub.devices.eyetracker.hw.mouse.calibration import MouseGazeCalibrationProcedure
import math
ET_UNDEFINED = EyeTrackerConstants.UNDEFINED
getTime = Computer.getTime


[docs]class EyeTracker(EyeTrackerDevice): """ To start iohub with a Mouse Simulated eye tracker, add the full iohub device name as a kwarg passed to launchHubServer:: eyetracker.hw.mouse.EyeTracker Examples: A. Start ioHub with the Mouse Simulated eye tracker:: from psychopy.iohub import launchHubServer from psychopy.core import getTime, wait iohub_config = {'eyetracker.hw.mouse.EyeTracker': {}} io = launchHubServer(**iohub_config) # Get the eye tracker device. tracker = io.devices.tracker B. Print all eye tracker events received for 2 seconds:: # Check for and print any eye tracker events received... tracker.setRecordingState(True) stime = getTime() while getTime()-stime < 2.0: for e in tracker.getEvents(): print(e) C. Print current eye position for 5 seconds:: # Check for and print current eye position every 100 msec. stime = getTime() while getTime()-stime < 5.0: print(tracker.getPosition()) wait(0.1) tracker.setRecordingState(False) # Stop the ioHub Server io.quit() """ DEVICE_TIMEBASE_TO_SEC = 1.0 EVENT_CLASS_NAMES = [ 'MonocularEyeSampleEvent', 'FixationStartEvent', 'FixationEndEvent', 'SaccadeStartEvent', 'SaccadeEndEvent', 'BlinkStartEvent', 'BlinkEndEvent'] __slots__ = [] _ioMouse = None _recording = False _eye_state = "NONE" _last_mouse_event_time = 0 _ISI = 0.01 # Later set by runtime_settings.sampling_rate _saccade_threshold = 0.5 # Later set by runtime_settings.sampling_rate _move_eye_buttons = [False, False, False] _blink_eye_buttons = [False, False, False] _last_event_start = 0.0 _last_start_event_pos = None _sacc_end_time = 0.0 _sacc_amplitude = 0.0, 0.0 _button_ix = dict(LEFT_BUTTON=0, MIDDLE_BUTTON=1, RIGHT_BUTTON=2) def __init__(self, *args, **kwargs): EyeTrackerDevice.__init__(self, *args, **kwargs) config = self.getConfiguration() # Used to hold the last sample processed by iohub. self._latest_sample = None # Calculate the desired ISI for the mouse sample stream. EyeTracker._ISI = 1.0/config.get('runtime_settings').get('sampling_rate') EyeTracker._saccade_threshold = config.get('controls').get('saccade_threshold') mb_list = config.get('controls').get('move') if isinstance(mb_list, str): mb_list = (mb_list,) if "CONTINUOUS" in mb_list: # CONTINUOUS == no buttons required to move == [] mb_list = [] bb_list = config.get('controls').get('blink') if isinstance(bb_list, str): bb_list = (bb_list,) for mb in mb_list: self._move_eye_buttons[self._button_ix.get(mb)] = True for mb in bb_list: self._blink_eye_buttons[self._button_ix.get(mb)] = True EyeTracker._move_eye_buttons = tuple(self._move_eye_buttons) EyeTracker._blink_eye_buttons = tuple(self._blink_eye_buttons) # Used to hold the last valid gaze position processed by ioHub. # If the last mouse tracker in a blink state, then this is set to None # self._latest_gaze_position = None def _connectMouse(self): if self._iohub_server: for dev in self._iohub_server.devices: if dev.__class__.__name__ == 'Mouse': EyeTracker._ioMouse = dev def _poll(self): if self.isConnected() and self.isRecordingEnabled(): if EyeTracker._last_mouse_event_time == 0: EyeTracker._last_mouse_event_time = getTime() - self._ISI # Start off mousegaze pos with current mouse position self._latest_gaze_position = self._ioMouse.getPosition() while getTime() - EyeTracker._last_mouse_event_time >= self._ISI: # Generate an eye sample every ISI seconds button_states = self._ioMouse.getCurrentButtonStates() last_gpos = self._latest_gaze_position if EyeTracker._eye_state == 'SACC' and getTime() >= EyeTracker._sacc_end_time: # Create end saccade event. end_pos = self._latest_gaze_position start_pos = self._last_start_event_pos self._addSaccadeEvent(False, start_pos, end_pos) self._addFixationEvent(True) create_blink_start = False if button_states == self._blink_eye_buttons: # In blink state.... # None means eyes are missing. if self._latest_gaze_position: # Blink just started, create event.... create_blink_start = True else: if self._eye_state == "BLINK": # Not in blink state anymore, create BlinkEndEvent self._latest_gaze_position = self._ioMouse.getPosition() self._addBlinkEvent(False) if button_states == self._move_eye_buttons: if self._eye_state == "FIX": display = self._display_device sacc_end_pos = self._ioMouse.getPosition() sacc_start_pos = self._latest_gaze_position spix = display._displayCoord2Pixel(sacc_start_pos[0], sacc_start_pos[1]) epix = display._displayCoord2Pixel(sacc_end_pos[0], sacc_end_pos[1]) sx = (epix[0]-spix[0])/display.getPixelsPerDegree()[0] sy = (epix[1]-spix[1])/display.getPixelsPerDegree()[1] sacc_amp_xy = math.fabs(math.hypot(sx, sy)) if sacc_amp_xy > EyeTracker._saccade_threshold: EyeTracker._sacc_amplitude = sx, sy self._addFixationEvent(False, sacc_start_pos) self._addSaccadeEvent(True, sacc_start_pos, sacc_end_pos) else: self._latest_gaze_position = self._ioMouse.getPosition() else: self._latest_gaze_position = self._ioMouse.getPosition() if self._eye_state not in ["FIX","SACC"] and self._latest_gaze_position: # Fixation start self._addFixationEvent(True) elif self._eye_state == "FIX" and create_blink_start: # Fixation End self._addFixationEvent(False, last_gpos) if create_blink_start: self._latest_gaze_position = None self._addBlinkEvent(True) EyeTracker._last_mouse_event_time += self._ISI next_sample_time = EyeTracker._last_mouse_event_time self._addSample(next_sample_time) def _addSaccadeEvent(self, startEvent, sacc_start_pos, sacc_end_pos): stime = EyeTracker._last_mouse_event_time if startEvent: eye_evt = [0, 0, 0, Device._getNextEventID(), EventConstants.SACCADE_START, stime, stime, stime, 0, 0, 0, EyeTrackerConstants.RIGHT_EYE, sacc_start_pos[0], sacc_start_pos[1], ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, 5, EyeTrackerConstants.PUPIL_DIAMETER_MM, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, 0] EyeTracker._eye_state = 'SACC' EyeTracker._last_event_start = stime sacc_amp_xy = math.fabs(math.hypot(*EyeTracker._sacc_amplitude)) saccade_duration = 2.2 * sacc_amp_xy + 21.0 saccade_duration = saccade_duration/1000.0 # convert to seconds EyeTracker._sacc_end_time = stime + saccade_duration EyeTracker._last_start_event_pos = sacc_start_pos self._latest_gaze_position = sacc_end_pos else: start_event_time = EyeTracker._last_event_start end_event_time = EyeTracker._sacc_end_time event_duration = end_event_time - start_event_time s_gaze = sacc_start_pos s_pupilsize = 4 e_gaze = sacc_end_pos e_pupilsize = 5 eye_evt = [0, 0, 0, Device._getNextEventID(), EventConstants.SACCADE_END, end_event_time, end_event_time, end_event_time, 0, 0, 0, EyeTrackerConstants.RIGHT_EYE, event_duration, EyeTracker._sacc_amplitude[0], # e_amp[0], EyeTracker._sacc_amplitude[1], # e_amp[1], 0, # e_angle, s_gaze[0], s_gaze[1], ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, s_pupilsize, EyeTrackerConstants.PUPIL_DIAMETER_MM, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, e_gaze[0], e_gaze[1], ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, e_pupilsize, EyeTrackerConstants.PUPIL_DIAMETER_MM, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, 0] EyeTracker._eye_state = 'FIX' EyeTracker._sacc_end_time = 0 self._addNativeEventToBuffer(eye_evt) def _addFixationEvent(self, startEvent, end_pos=None): ftime = EyeTracker._last_mouse_event_time gaze = self._latest_gaze_position if startEvent: eye_evt = [0, 0, 0, Device._getNextEventID(), EventConstants.FIXATION_START, ftime, ftime, ftime, 0, 0, 0, EyeTrackerConstants.RIGHT_EYE, gaze[0], gaze[1], ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, 5, EyeTrackerConstants.PUPIL_DIAMETER_MM, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, 0] EyeTracker._last_event_start = ftime EyeTracker._last_start_event_pos = gaze EyeTracker._eye_state = "FIX" else: start_event_time = EyeTracker._last_event_start end_event_time = ftime event_duration = end_event_time - start_event_time s_gaze = self._last_start_event_pos e_gaze = end_pos a_gaze = (s_gaze[0]+e_gaze[0])/2, (s_gaze[1]+e_gaze[1])/2 EyeTracker._last_event_start = 0.0 EyeTracker._last_start_event_pos = None eye_evt = [0, 0, 0, Device._getNextEventID(), EventConstants.FIXATION_END, ftime, ftime, ftime, 0, 0, 0, EyeTrackerConstants.RIGHT_EYE, event_duration, s_gaze[0], s_gaze[1], ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, 4, EyeTrackerConstants.PUPIL_DIAMETER_MM, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, e_gaze[0], e_gaze[1], ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, 5, EyeTrackerConstants.PUPIL_DIAMETER_MM, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, a_gaze[0], a_gaze[1], ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, 4.5, EyeTrackerConstants.PUPIL_DIAMETER_MM, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, 0] self._addNativeEventToBuffer(eye_evt) def _addBlinkEvent(self, startEvent): btime = EyeTracker._last_mouse_event_time if startEvent: # Create a blink start EyeTracker._last_event_start = btime EyeTracker._eye_state = "BLINK" eye_evt = [0, 0, 0, # device id (not currently used) Device._getNextEventID(), EventConstants.BLINK_START, btime, btime, btime, 0, 0, 0, EyeTrackerConstants.RIGHT_EYE, 0] else: # Create a blink end eye_evt = [ 0, 0, 0, Device._getNextEventID(), EventConstants.BLINK_END, btime, btime, btime, 0, 0, 0, EyeTrackerConstants.RIGHT_EYE, btime - EyeTracker._last_event_start, 0 ] EyeTracker._last_event_start = 0.0 self._addNativeEventToBuffer(eye_evt) def _addSample(self, sample_time): if self._latest_gaze_position: gx, gy = self._latest_gaze_position status = 0 else: gx, gy = EyeTrackerConstants.UNDEFINED, EyeTrackerConstants.UNDEFINED status = 2 pupilSize = 5 monoSample = [0, 0, 0, Device._getNextEventID(), EventConstants.MONOCULAR_EYE_SAMPLE, sample_time, sample_time, sample_time, 0, 0, 0, EyeTrackerConstants.RIGHT_EYE, gx, gy, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, pupilSize, EyeTrackerConstants.PUPIL_DIAMETER_MM, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, ET_UNDEFINED, status ] self._latest_sample = monoSample self._addNativeEventToBuffer(monoSample)
[docs] def trackerTime(self): """ Current eye tracker time. Returns: float: current eye tracker time in seconds. """ return getTime()
[docs] def trackerSec(self): """ Same as trackerTime(). """ return getTime()
def setConnectionState(self, enable): """ When 'connected', the Mouse Simulated Eye Tracker taps into the ioHub Mouse event stream. """ if enable and self._ioMouse is None: self._connectMouse() elif enable is False and self._ioMouse: EyeTracker._ioMouse = None return self.isConnected() def isConnected(self): """ """ return self._ioMouse is not None
[docs] def enableEventReporting(self, enabled=True): """enableEventReporting is functionally identical to the eye tracker device specific setRecordingState method.""" try: self.setRecordingState(enabled) enabled = EyeTrackerDevice.enableEventReporting(self, enabled) return enabled except Exception as e: print2err('Exception in EyeTracker.enableEventReporting: ', str(e)) printExceptionDetailsToStdErr()
[docs] def setRecordingState(self, recording): """ setRecordingState is used to start or stop the recording of data from the eye tracking device. """ current_state = self.isRecordingEnabled() if recording is True and current_state is False: EyeTracker._recording = True if self._ioMouse is None: self._connectMouse() elif recording is False and current_state is True: EyeTracker._recording = False self._latest_sample = None EyeTracker._last_mouse_event_time = 0 return EyeTrackerDevice.enableEventReporting(self, recording)
[docs] def isRecordingEnabled(self): """ isRecordingEnabled returns the recording state from the eye tracking device. Return: bool: True == the device is recording data; False == Recording is not occurring """ return self._recording
[docs] def runSetupProcedure(self, calibration_args={}): """ runSetupProcedure displays a mock calibration procedure. No calibration is actually done. """ calibration = MouseGazeCalibrationProcedure(self, calibration_args) cal_run = calibration.runCalibration() calibration.window.close() # NOTE - The following line has been commented out to prevent ioHub from # losing keyboard input after the window is closed. The new keyboard # input system does not require this call anymore. # calibration._unregisterEventMonitors() calibration.clearAllEventBuffers() if cal_run: return {"RESULT": "CALIBRATION_OK"} else: return {"RESULT": "CALIBRATION_ABORTED"}
def _getIOHubEventObject(self, native_event_data): """The _getIOHubEventObject method is called by the ioHub Process to convert new native device event objects that have been received to the appropriate ioHub Event type representation.""" self._latest_sample = native_event_data return self._latest_sample def _eyeTrackerToDisplayCoords(self, eyetracker_point=()): """Converts MouseGaze positions to the Display device coordinate space. """ return eyetracker_point[0], eyetracker_point[1] def _displayToEyeTrackerCoords(self, display_x, display_y): """Converts a Display device point to MouseGaze position coordinate space. """ return display_x, display_y def _close(self): self.setRecordingState(False) self.setConnectionState(False) EyeTrackerDevice._close(self)

Back to top