Exemplo n.º 1
0
class EyeTribeTracker(BaseEyeTracker):
    """A class for EyeTribeTracker objects"""
    def __init__(self,
                 display,
                 logfile=settings.LOGFILE,
                 eventdetection=settings.EVENTDETECTION,
                 saccade_velocity_threshold=35,
                 saccade_acceleration_threshold=9500,
                 blink_threshold=settings.BLINKTHRESH,
                 **args):
        """Initializes the EyeTribeTracker object
        
        arguments
        display    -- a pygaze.display.Display instance
        
        keyword arguments
        logfile    -- logfile name (string value); note that this is the
                   name for the eye data log file (default = LOGFILE)
        """

        # try to copy docstrings (but ignore it if it fails, as we do
        # not need it for actual functioning of the code)
        try:
            copy_docstr(BaseEyeTracker, EyeTribeTracker)
        except:
            # we're not even going to show a warning, since the copied
            # docstring is useful for code editors; these load the docs
            # in a non-verbose manner, so warning messages would be lost
            pass

        # object properties
        self.disp = display
        self.screen = Screen()
        self.dispsize = settings.DISPSIZE  # display size in pixels
        self.screensize = settings.SCREENSIZE  # display size in cm
        self.kb = Keyboard(keylist=['space', 'escape', 'q'], timeout=1)
        self.errorbeep = Sound(osc='saw', freq=100, length=100)

        # output file properties
        self.outputfile = logfile

        # eye tracker properties
        self.connected = False
        self.recording = False
        self.errdist = 2  # degrees; maximal error for drift correction
        self.pxerrdist = 30  # initial error in pixels
        self.maxtries = 100  # number of samples obtained before giving up (for obtaining accuracy and tracker distance information, as well as starting or stopping recording)
        self.prevsample = (-1, -1)
        self.prevps = -1

        # event detection properties
        self.fixtresh = 1.5  # degrees; maximal distance from fixation start (if gaze wanders beyond this, fixation has stopped)
        self.fixtimetresh = 100  # milliseconds; amount of time gaze has to linger within self.fixtresh to be marked as a fixation
        self.spdtresh = saccade_velocity_threshold  # degrees per second; saccade velocity threshold
        self.accthresh = saccade_acceleration_threshold  # degrees per second**2; saccade acceleration threshold
        self.blinkthresh = blink_threshold  # milliseconds; blink detection threshold used in PyGaze method
        self.eventdetection = eventdetection
        self.set_detection_type(self.eventdetection)
        self.weightdist = 10  # weighted distance, used for determining whether a movement is due to measurement error (1 is ok, higher is more conservative and will result in only larger saccades to be detected)

        # connect to the tracker
        self.eyetribe = EyeTribe(logfilename=logfile)

        # get info on the sample rate
        self.samplerate = self.eyetribe._samplefreq
        self.sampletime = 1000.0 * self.eyetribe._intsampletime

        # initiation report
        self.log("pygaze initiation report start")
        self.log("display resolution: {}x{}".format(self.dispsize[0],
                                                    self.dispsize[1]))
        self.log("display size in cm: {}x{}".format(self.screensize[0],
                                                    self.screensize[1]))
        self.log("samplerate: {} Hz".format(self.samplerate))
        self.log("sampletime: {} ms".format(self.sampletime))
        self.log("fixation threshold: {} degrees".format(self.fixtresh))
        self.log("speed threshold: {} degrees/second".format(self.spdtresh))
        self.log("acceleration threshold: {} degrees/second**2".format(
            self.accthresh))
        self.log("pygaze initiation report end")

    def calibrate(self):
        """Calibrates the eye tracking system
        
        arguments
        None
        
        keyword arguments
        None

        returns
        success    -- returns True if calibration succeeded, or False if
                   not; in addition a calibration log is added to the
                   log file and some properties are updated (i.e. the
                   thresholds for detection algorithms)
        """

        # CALIBRATION
        # determine the calibration points
        calibpoints = []
        for x in [0.1, 0.5, 0.9]:
            for y in [0.1, 0.5, 0.9]:
                calibpoints.append(
                    (int(x * self.dispsize[0]), int(y * self.dispsize[1])))
        random.shuffle(calibpoints)

        # show a message
        self.screen.clear()
        self.screen.draw_text(
            text="Press Space to calibrate, S to skip, and Q to quit",
            fontsize=20)
        self.disp.fill(self.screen)
        self.disp.show()

        # wait for keyboard input
        key, keytime = self.kb.get_key(keylist=['q', 's', 'space'],
                                       timeout=None,
                                       flush=True)
        if key == 's':
            return True
        if key == 'q':
            quited = True
        else:
            quited = False

        # Pause the processing of samples during the calibration.
#        self.eyetribe._pause_sample_processing()
# run until the user is statisfied, or quits
        calibrated = False
        calibresult = None
        while not quited and not calibrated:

            # Clear the existing calibration.
            if self.eyetribe._tracker.get_iscalibrated():
                self.eyetribe._lock.acquire(True)
                self.eyetribe.calibration.clear()
                self.eyetribe._lock.release()

            # Wait for a bit.
            clock.pause(1500)

            # start a new calibration
            if not self.eyetribe._tracker.get_iscalibrating():
                self.eyetribe._lock.acquire(True)
                self.eyetribe.calibration.start(pointcount=len(calibpoints))
                self.eyetribe._lock.release()

            # loop through calibration points
            for cpos in calibpoints:
                # Check whether the calibration is already done.
                # (Not sure how or why, but for some reason some data
                # can persist between calbrations, and the tracker will
                # simply stop allowing further pointstart requests.)
                if self.eyetribe._tracker.get_iscalibrated():
                    break

                # Draw a calibration target.
                self.draw_calibration_target(cpos[0], cpos[1])
                # wait for a bit to allow participant to start looking at
                # the calibration point (#TODO: space press?)
                clock.pause(settings.EYETRIBEPRECALIBDUR)
                # start calibration of point
                self.eyetribe._lock.acquire(True)
                self.eyetribe.calibration.pointstart(cpos[0], cpos[1])
                self.eyetribe._lock.release()
                # wait for a second
                clock.pause(settings.EYETRIBECALIBDUR)
                # stop calibration of this point
                self.eyetribe._lock.acquire(True)
                self.eyetribe.calibration.pointend()
                self.eyetribe._lock.release()
                # check if the Q key has been pressed
                if self.kb.get_key(keylist=['q'], timeout=10,
                                   flush=False)[0] == 'q':
                    # abort calibration
                    self.eyetribe._lock.acquire(True)
                    self.eyetribe.calibration.abort()
                    self.eyetribe._lock.release()
                    # set quited variable and break this for loop
                    quited = True
                    break

            # retry option if the calibration was aborted
            if quited:
                # show retry message
                self.screen.clear()
                self.screen.draw_text(
                    "Calibration aborted. Press Space to restart or 'Q' to quit",
                    fontsize=20)
                self.disp.fill(self.screen)
                self.disp.show()
                # get input
                key, keytime = self.kb.get_key(keylist=['q', 'space'],
                                               timeout=None,
                                               flush=True)
                if key == 'space':
                    # unset quited Boolean
                    quited = False
                # skip further processing
                continue

            # empty display
            self.disp.fill()
            self.disp.show()
            # allow for a bit of calculation time
            # (this is waaaaaay too much)
            clock.pause(1000)
            # get the calibration result
            self.eyetribe._lock.acquire(True)
            calibresult = self.eyetribe._tracker.get_calibresult()
            self.eyetribe._lock.release()

            # results
            # clear the screen
            self.screen.clear()
            # draw results for each point
            if type(calibresult) == dict:
                for p in calibresult['calibpoints']:
                    # only draw the point if data was obtained
                    if p['state'] > 0:
                        # draw the mean error
                        # self.screen.draw_circle(colour=(252,233,79),
                        #     pos=(p['cpx'],p['cpy']), r=p['mepix'], pw=0,
                        #     fill=True)
                        self.screen.draw_line(spos=(p['cpx'], p['cpy']),
                                              epos=(p['mecpx'], p['mecpy']),
                                              pw=2)
                        # draw the point
                        self.screen.draw_fixation(fixtype='dot',
                                                  colour=(115, 210, 22),
                                                  pos=(p['cpx'], p['cpy']))
                        # draw the estimated point
                        self.screen.draw_fixation(fixtype='dot',
                                                  colour=(32, 74, 135),
                                                  pos=(p['mecpx'], p['mecpy']))
                        # annotate accuracy
                        self.screen.draw_text(text="{}".format(\
                            round(p['acd'], ndigits=2)),
                            pos=(p['cpx']+10,p['cpy']+10), fontsize=20)
                    # if no data was obtained, draw the point in red
                    else:
                        self.screen.draw_fixation(fixtype='dot',
                                                  colour=(204, 0, 0),
                                                  pos=(p['cpx'], p['cpy']))
                # draw box for averages
                # self.screen.draw_rect(colour=(238,238,236), x=int(self.dispsize[0]*0.15), y=int(self.dispsize[1]*0.2), w=400, h=200, pw=0, fill=True)
                # draw result
                if calibresult['result']:
                    self.screen.draw_text(text="Calibration successful",
                                          colour=(0, 255, 0),
                                          pos=(int(self.dispsize[0] * 0.5),
                                               int(self.dispsize[1] * 0.25)),
                                          fontsize=20)
                else:
                    self.screen.draw_text(text="Calibration failed",
                                          colour=(255, 0, 0),
                                          pos=(int(self.dispsize[0] * 0.5),
                                               int(self.dispsize[1] * 0.25)),
                                          fontsize=20)
                # draw average accuracy
                self.screen.draw_text(
                    text="Average error = {} degrees".format(round(\
                    calibresult['deg'], ndigits=2)), \
                    pos=(int(self.dispsize[0]*0.5),int(self.dispsize[1]*0.25+30)),
                    fontsize=20)
                # draw input options
                self.screen.draw_text(
                    text="Press Space to continue or 'R' to restart",
                    pos=(int(self.dispsize[0] * 0.5),
                         int(self.dispsize[1] * 0.25 + 60)),
                    fontsize=20)
            else:
                self.screen.draw_text(
                    text="Calibration failed. Press 'R' to try again.",
                    fontsize=20)
            # show the results
            self.disp.fill(self.screen)
            self.disp.show()
            # wait for input
            key, keytime = self.kb.get_key(keylist=['space', 'r'],
                                           timeout=None,
                                           flush=True)
            # process input
            if key == 'space':
                calibrated = True

        # Continue the processing of samples after the calibration.
#        self.eyetribe._unpause_sample_processing()

# calibration failed if the user quited
        if quited:
            return False

        # NOISE CALIBRATION
        # get all error estimates (pixels)
        var = []
        for p in calibresult['calibpoints']:
            # only draw the point if data was obtained
            if p['state'] > 0:
                var.append(p['mepix'])
        noise = sum(var) / float(len(var))
        self.pxdsttresh = (noise, noise)

        # AFTERMATH
        # store some variables
        pixpercm = (self.dispsize[0] / float(self.screensize[0]) +
                    self.dispsize[1] / float(self.screensize[1])) / 2
        screendist = settings.SCREENDIST
        # calculate thresholds based on tracker settings
        self.accuracy = ((calibresult['Ldeg'], calibresult['Ldeg']),
                         (calibresult['Rdeg'], calibresult['Rdeg']))
        self.pxerrdist = deg2pix(screendist, self.errdist, pixpercm)
        self.pxfixtresh = deg2pix(screendist, self.fixtresh, pixpercm)
        self.pxaccuracy = ((deg2pix(screendist, self.accuracy[0][0], pixpercm),
                            deg2pix(screendist, self.accuracy[0][1],
                                    pixpercm)),
                           (deg2pix(screendist, self.accuracy[1][0], pixpercm),
                            deg2pix(screendist, self.accuracy[1][1],
                                    pixpercm)))
        self.pxspdtresh = deg2pix(screendist, self.spdtresh / 1000.0,
                                  pixpercm)  # in pixels per millisecond
        self.pxacctresh = deg2pix(screendist, self.accthresh / 1000.0,
                                  pixpercm)  # in pixels per millisecond**2

        # calibration report
        self.log("pygaze calibration report start")
        self.log("accuracy (degrees): LX={}, LY={}, RX={}, RY={}".format(
            self.accuracy[0][0], self.accuracy[0][1], self.accuracy[1][0], \
            self.accuracy[1][1]))
        self.log("accuracy (in pixels): LX={}, LY={}, RX={}, RY={}".format( \
            self.pxaccuracy[0][0], self.pxaccuracy[0][1], \
            self.pxaccuracy[1][0], self.pxaccuracy[1][1]))
        self.log("precision (RMS noise in pixels): X={}, Y={}".format( \
            self.pxdsttresh[0], self.pxdsttresh[1]))
        self.log("distance between participant and display: {} cm".format( \
            screendist))
        self.log("fixation threshold: {} pixels".format(self.pxfixtresh))
        self.log("speed threshold: {} pixels/ms".format(self.pxspdtresh))
        self.log("acceleration threshold: {} pixels/ms**2".format( \
            self.pxacctresh))
        self.log("pygaze calibration report end")

        return True

    def close(self):
        """Neatly close connection to tracker
        
        arguments
        None
        
        returns
        Nothing    -- saves data and sets self.connected to False
        """

        # close connection
        self.eyetribe.close()
        self.connected = False

    def connected(self):
        """Checks if the tracker is connected
        
        arguments
        None
        
        returns
        connected    -- True if connection is established, False if not;
                   sets self.connected to the same value
        """

        res = self.eyetribe._tracker.get_trackerstate()

        if res == 0:
            self.connected = True
        else:
            self.connected = False

        return self.connected

    def drift_correction(self, pos=None, fix_triggered=False):
        """Performs a drift check
        
        arguments
        None
        
        keyword arguments
        pos            -- (x, y) position of the fixation dot or None for
                       a central fixation (default = None)
        fix_triggered    -- Boolean indicating if drift check should be
                       performed based on gaze position (fix_triggered
                       = True) or on spacepress (fix_triggered = 
                       False) (default = False)
        
        returns
        checked        -- Boolaan indicating if drift check is ok (True)
                       or not (False); or calls self.calibrate if 'q'
                       or 'escape' is pressed
        """

        if pos == None:
            pos = self.dispsize[0] / 2, self.dispsize[1] / 2
        if fix_triggered:
            return self.fix_triggered_drift_correction(pos)
        self.draw_drift_correction_target(pos[0], pos[1])
        pressed = False
        while not pressed:
            pressed, presstime = self.kb.get_key()
            if pressed:
                if pressed == 'escape' or pressed == 'q':
                    print(
                        "libeyetribe.EyeTribeTracker.drift_correction: 'q' or 'escape' pressed"
                    )
                    return self.calibrate()
                gazepos = self.sample()
                if ((gazepos[0] - pos[0])**2 +
                    (gazepos[1] - pos[1])**2)**0.5 < self.pxerrdist:
                    return True
                else:
                    self.errorbeep.play()
        return False

    def draw_drift_correction_target(self, x, y):
        """
        Draws the drift-correction target.
        
        arguments
        
        x        --    The X coordinate
        y        --    The Y coordinate
        """

        self.screen.clear()
        self.screen.draw_fixation(fixtype='dot',
                                  colour=settings.FGC,
                                  pos=(x, y),
                                  pw=0,
                                  diameter=12)
        self.disp.fill(self.screen)
        self.disp.show()

    def draw_calibration_target(self, x, y):

        self.draw_drift_correction_target(x, y)

    def fix_triggered_drift_correction(self,
                                       pos=None,
                                       min_samples=10,
                                       max_dev=60,
                                       reset_threshold=30):
        """Performs a fixation triggered drift correction by collecting
        a number of samples and calculating the average distance from the
        fixation position
        
        arguments
        None
        
        keyword arguments
        pos            -- (x, y) position of the fixation dot or None for
                       a central fixation (default = None)
        min_samples        -- minimal amount of samples after which an
                       average deviation is calculated (default = 10)
        max_dev        -- maximal deviation from fixation in pixels
                       (default = 60)
        reset_threshold    -- if the horizontal or vertical distance in
                       pixels between two consecutive samples is
                       larger than this threshold, the sample
                       collection is reset (default = 30)
        
        returns
        checked        -- Boolaan indicating if drift check is ok (True)
                       or not (False); or calls self.calibrate if 'q'
                       or 'escape' is pressed
        """

        self.draw_drift_correction_target(pos[0], pos[1])
        if pos == None:
            pos = self.dispsize[0] / 2, self.dispsize[1] / 2

        # loop until we have sufficient samples
        lx = []
        ly = []
        while len(lx) < min_samples:

            # pressing escape enters the calibration screen
            if self.kb.get_key()[0] in ['escape', 'q']:
                print(
                    "libeyetribe.EyeTribeTracker.fix_triggered_drift_correction: 'q' or 'escape' pressed"
                )
                return self.calibrate()

            # collect a sample
            x, y = self.sample()

            if len(lx) == 0 or x != lx[-1] or y != ly[-1]:

                # if present sample deviates too much from previous sample, reset counting
                if len(lx) > 0 and (abs(x - lx[-1]) > reset_threshold
                                    or abs(y - ly[-1]) > reset_threshold):
                    lx = []
                    ly = []

                # collect samples
                else:
                    lx.append(x)
                    ly.append(y)

            if len(lx) == min_samples:

                avg_x = sum(lx) / len(lx)
                avg_y = sum(ly) / len(ly)
                d = ((avg_x - pos[0])**2 + (avg_y - pos[1])**2)**0.5

                if d < max_dev:
                    return True
                else:
                    lx = []
                    ly = []

    def get_eyetracker_clock_async(self):
        """Not supported for EyeTribeTracker (yet)"""

        print("function not supported yet")

    def log(self, msg):
        """Writes a message to the log file
        
        arguments
        ms        -- a string to include in the log file
        
        returns
        Nothing    -- uses native log function of iViewX to include a line
                   in the log file
        """

        self.eyetribe.log_message(msg)

    def prepare_drift_correction(self, pos):
        """Not supported for EyeTribeTracker (yet)"""

        print("function not supported yet")

    def pupil_size(self):
        """Return pupil size
        
        arguments
        None
        
        returns
        pupil size    -- returns pupil diameter for the eye that is currently
                   being tracked (as specified by self.eye_used) or -1
                   when no data is obtainable
        """

        # get newest pupil size
        ps = self.eyetribe.pupil_size()

        # invalid data
        if ps == None:
            return -1

        # check if the new pupil size is the same as the previous
        if ps != self.prevps:
            # update the pupil size
            self.prevps = copy.copy(ps)

        return self.prevps

    def sample(self):
        """Returns newest available gaze position
        
        arguments
        None
        
        returns
        sample    -- an (x,y) tuple or a (-1,-1) on an error
        """

        # get newest sample
        s = self.eyetribe.sample()

        # invalid data
        if s == (None, None):
            return (-1, -1)

        # check if the new sample is the same as the previous
        if s != self.prevsample:
            # update the current sample
            self.prevsample = copy.copy(s)

        return self.prevsample

    def send_command(self, cmd):
        """Sends a command to the eye tracker
        
        arguments
        cmd        --    the command to be sent to the EyeTribe, which should
                    be a list with the following information:
                        [category, request, values]
        
        returns
        Nothing
        """

        self.eyetribe._connection.request(cmd)

    def start_recording(self):
        """Starts recording eye position
        
        arguments
        None
        
        returns
        Nothing    -- sets self.recording to True when recording is
                   successfully started
        """

        self.eyetribe.start_recording()
        self.recording = True

    def status_msg(self, msg):
        """Not supported for EyeTribeTracker (yet)"""

        print("function not supported yet")

    def stop_recording(self):
        """Stop recording eye position
        
        arguments
        None
        
        returns
        Nothing    -- sets self.recording to False when recording is
                   successfully started
        """

        self.eyetribe.stop_recording()
        self.recording = False

    def set_detection_type(self, eventdetection):
        """Set the event detection type to either PyGaze algorithms, or
        native algorithms as provided by the manufacturer (only if
        available: detection type will default to PyGaze if no native
        functions are available)
        
        arguments
        eventdetection    --    a string indicating which detection type
                        should be employed: either 'pygaze' for
                        PyGaze event detection algorithms or
                        'native' for manufacturers algorithms (only
                        if available; will default to 'pygaze' if no
                        native event detection is available)
        returns        --    detection type for saccades, fixations and
                        blinks in a tuple, e.g. 
                        ('pygaze','native','native') when 'native'
                        was passed, but native detection was not
                        available for saccade detection
        """

        if eventdetection in ['pygaze', 'native']:
            self.eventdetection = eventdetection

        return ('pygaze', 'pygaze', 'pygaze')

    def wait_for_event(self, event):
        """Waits for event
        
        arguments
        event        -- an integer event code, one of the following:
                    3 = STARTBLINK
                    4 = ENDBLINK
                    5 = STARTSACC
                    6 = ENDSACC
                    7 = STARTFIX
                    8 = ENDFIX
        
        returns
        outcome    -- a self.wait_for_* method is called, depending on the
                   specified event; the return values of corresponding
                   method are returned
        """

        if event == 5:
            outcome = self.wait_for_saccade_start()
        elif event == 6:
            outcome = self.wait_for_saccade_end()
        elif event == 7:
            outcome = self.wait_for_fixation_start()
        elif event == 8:
            outcome = self.wait_for_fixation_end()
        elif event == 3:
            outcome = self.wait_for_blink_start()
        elif event == 4:
            outcome = self.wait_for_blink_end()
        else:
            raise Exception(
                "Error in libeyetribe.EyeTribeTracker.wait_for_event: eventcode {} is not supported"
                .format(event))

        return outcome

    def wait_for_blink_end(self):
        """Waits for a blink end and returns the blink ending time
        
        arguments
        None
        
        returns
        timestamp        --    blink ending time in milliseconds, as
                        measured from experiment begin time
        """

        # # # # #
        # EyeTribe method

        if self.eventdetection == 'native':

            # print warning, since EyeTribe does not have a blink detection
            # built into their API

            print("WARNING! 'native' event detection has been selected, \
                but EyeTribe does not offer blink detection; PyGaze algorithm \
                will be used")

        # # # # #
        # PyGaze method

        blinking = True

        # loop while there is a blink
        while blinking:
            # get newest sample
            gazepos = self.sample()
            # check if it's valid
            if self.is_valid_sample(gazepos):
                # if it is a valid sample, blinking has stopped
                blinking = False

        # return timestamp of blink end
        return clock.get_time()

    def wait_for_blink_start(self):
        """Waits for a blink start and returns the blink starting time
        
        arguments
        None
        
        returns
        timestamp        --    blink starting time in milliseconds, as
                        measured from experiment begin time
        """

        # # # # #
        # EyeTribe method

        if self.eventdetection == 'native':

            # print warning, since EyeTribe does not have a blink detection
            # built into their API

            print("WARNING! 'native' event detection has been selected, \
                but EyeTribe does not offer blink detection; PyGaze algorithm \
                will be used")

        # # # # #
        # PyGaze method

        blinking = False

        # loop until there is a blink
        while not blinking:
            # get newest sample
            gazepos = self.sample()
            # check if it's a valid sample
            if not self.is_valid_sample(gazepos):
                # get timestamp for possible blink start
                t0 = clock.get_time()
                # loop until a blink is determined, or a valid sample occurs
                while not self.is_valid_sample(self.sample()):
                    # check if time has surpassed BLINKTHRESH
                    if clock.get_time() - t0 >= self.blinkthresh:
                        # return timestamp of blink start
                        return t0

    def wait_for_fixation_end(self):
        """Returns time and gaze position when a fixation has ended;
        function assumes that a 'fixation' has ended when a deviation of
        more than self.pxfixtresh from the initial fixation position has
        been detected (self.pxfixtresh is created in self.calibration,
        based on self.fixtresh, a property defined in self.__init__)
        
        arguments
        None
        
        returns
        time, gazepos    -- time is the starting time in milliseconds (from
                       expstart), gazepos is a (x,y) gaze position
                       tuple of the position from which the fixation
                       was initiated
        """

        # # # # #
        # EyeTribe method

        if self.eventdetection == 'native':

            # print warning, since EyeTribe does not have a blink detection
            # built into their API

            print("WARNING! 'native' event detection has been selected, \
                but EyeTribe does not offer fixation detection; \
                PyGaze algorithm will be used")

        # # # # #
        # PyGaze method

        # function assumes that a 'fixation' has ended when a deviation of more than fixtresh
        # from the initial 'fixation' position has been detected

        # get starting time and position
        stime, spos = self.wait_for_fixation_start()

        # loop until fixation has ended
        while True:
            # get new sample
            npos = self.sample()  # get newest sample
            # check if sample is valid
            if self.is_valid_sample(npos):
                # check if sample deviates to much from starting position
                if (npos[0] - spos[0])**2 + (
                        npos[1] -
                        spos[1])**2 > self.pxfixtresh**2:  # Pythagoras
                    # break loop if deviation is too high
                    break

        return clock.get_time(), spos

    def wait_for_fixation_start(self):
        """Returns starting time and position when a fixation is started;
        function assumes a 'fixation' has started when gaze position
        remains reasonably stable (i.e. when most deviant samples are
        within self.pxfixtresh) for five samples in a row (self.pxfixtresh
        is created in self.calibration, based on self.fixtresh, a property
        defined in self.__init__)
        
        arguments
        None
        
        returns
        time, gazepos    -- time is the starting time in milliseconds (from
                       expstart), gazepos is a (x,y) gaze position
                       tuple of the position from which the fixation
                       was initiated
        """

        # # # # #
        # EyeTribe method

        if self.eventdetection == 'native':

            # print warning, since EyeTribe does not have a fixation start
            # detection built into their API (only ending)

            print("WARNING! 'native' event detection has been selected, \
                but EyeTribe does not offer fixation detection; \
                PyGaze algorithm will be used")

        # # # # #
        # PyGaze method

        # function assumes a 'fixation' has started when gaze position
        # remains reasonably stable for self.fixtimetresh

        # get starting position
        spos = self.sample()
        while not self.is_valid_sample(spos):
            spos = self.sample()

        # get starting time
        t0 = clock.get_time()

        # wait for reasonably stable position
        moving = True
        while moving:
            # get new sample
            npos = self.sample()
            # check if sample is valid
            if self.is_valid_sample(npos):
                # check if new sample is too far from starting position
                if (npos[0] - spos[0])**2 + (
                        npos[1] -
                        spos[1])**2 > self.pxfixtresh**2:  # Pythagoras
                    # if not, reset starting position and time
                    spos = copy.copy(npos)
                    t0 = clock.get_time()
                # if new sample is close to starting sample
                else:
                    # get timestamp
                    t1 = clock.get_time()
                    # check if fixation time threshold has been surpassed
                    if t1 - t0 >= self.fixtimetresh:
                        # return time and starting position
                        return t1, spos

    def wait_for_saccade_end(self):
        """Returns ending time, starting and end position when a saccade is
        ended; based on Dalmaijer et al. (2013) online saccade detection
        algorithm
        
        arguments
        None
        
        returns
        endtime, startpos, endpos    -- endtime in milliseconds (from 
                               expbegintime); startpos and endpos
                               are (x,y) gaze position tuples
        """

        # # # # #
        # EyeTribe method

        if self.eventdetection == 'native':

            # print warning, since EyeTribe does not have a blink detection
            # built into their API

            print("WARNING! 'native' event detection has been selected, \
                but EyeTribe does not offer saccade detection; PyGaze \
                algorithm will be used")

        # # # # #
        # PyGaze method

        # get starting position (no blinks)
        t0, spos = self.wait_for_saccade_start()
        # get valid sample
        prevpos = self.sample()
        while not self.is_valid_sample(prevpos):
            prevpos = self.sample()
        # get starting time, intersample distance, and velocity
        t1 = clock.get_time()
        s = ((prevpos[0] - spos[0])**2 + (prevpos[1] - spos[1])**
             2)**0.5  # = intersample distance = speed in px/sample
        v0 = s / (t1 - t0)

        # run until velocity and acceleration go below threshold
        saccadic = True
        while saccadic:
            # get new sample
            newpos = self.sample()
            t1 = clock.get_time()
            if self.is_valid_sample(newpos) and newpos != prevpos:
                # calculate distance
                s = ((newpos[0] - prevpos[0])**2 + (newpos[1] - prevpos[1])**
                     2)**0.5  # = speed in pixels/sample
                # calculate velocity
                v1 = s / (t1 - t0)
                # calculate acceleration
                a = (v1 - v0) / (
                    t1 - t0
                )  # acceleration in pixels/sample**2 (actually is v1-v0 / t1-t0; but t1-t0 = 1 sample)
                # check if velocity and acceleration are below threshold
                if v1 < self.pxspdtresh and (a > -1 * self.pxacctresh
                                             and a < 0):
                    saccadic = False
                    epos = newpos[:]
                    etime = clock.get_time()
                # update previous values
                t0 = copy.copy(t1)
                v0 = copy.copy(v1)
            # udate previous sample
            prevpos = newpos[:]

        return etime, spos, epos

    def wait_for_saccade_start(self):
        """Returns starting time and starting position when a saccade is
        started; based on Dalmaijer et al. (2013) online saccade detection
        algorithm
        
        arguments
        None
        
        returns
        endtime, startpos    -- endtime in milliseconds (from expbegintime);
                       startpos is an (x,y) gaze position tuple
        """

        # # # # #
        # EyeTribe method

        if self.eventdetection == 'native':

            # print warning, since EyeTribe does not have a blink detection
            # built into their API

            print("WARNING! 'native' event detection has been selected, \
                but EyeTribe does not offer saccade detection; PyGaze \
                algorithm will be used")

        # # # # #
        # PyGaze method

        # get starting position (no blinks)
        newpos = self.sample()
        while not self.is_valid_sample(newpos):
            newpos = self.sample()
        # get starting time, position, intersampledistance, and velocity
        t0 = clock.get_time()
        prevpos = newpos[:]
        s = 0
        v0 = 0

        # get samples
        saccadic = False
        while not saccadic:
            # get new sample
            newpos = self.sample()
            t1 = clock.get_time()
            if self.is_valid_sample(newpos) and newpos != prevpos:
                # check if distance is larger than precision error
                sx = newpos[0] - prevpos[0]
                sy = newpos[1] - prevpos[1]
                if (sx / self.pxdsttresh[0])**2 + (
                        sy / self.pxdsttresh[1]
                )**2 > self.weightdist:  # weigthed distance: (sx/tx)**2 + (sy/ty)**2 > 1 means movement larger than RMS noise
                    # calculate distance
                    s = ((sx)**2 + (sy)**
                         2)**0.5  # intersampledistance = speed in pixels/ms
                    # calculate velocity
                    v1 = s / (t1 - t0)
                    # calculate acceleration
                    a = (v1 - v0) / (t1 - t0)  # acceleration in pixels/ms**2
                    # check if either velocity or acceleration are above threshold values
                    if v1 > self.pxspdtresh or a > self.pxacctresh:
                        saccadic = True
                        spos = prevpos[:]
                        stime = clock.get_time()
                    # update previous values
                    t0 = copy.copy(t1)
                    v0 = copy.copy(v1)

                # udate previous sample
                prevpos = newpos[:]

        return stime, spos

    def is_valid_sample(self, gazepos):
        """Checks if the sample provided is valid, based on EyeTribe specific
        criteria (for internal use)
        
        arguments
        gazepos        --    a (x,y) gaze position tuple, as returned by
                        self.sample()
        
        returns
        valid        --    a Boolean: True on a valid sample, False on
                        an invalid sample
        """

        # return False if a sample is invalid
        if gazepos == (None, None) or gazepos == (-1, -1):
            return False

        # in any other case, the sample is valid
        return True
Exemplo n.º 2
0
#scr.draw_rect()
scr.clear()
scr.draw_text("There should be two rectangles on the screen: \
\nred filled on the left, and green unfilled on the right", pos=(DISPSIZE[0]/2, DISPSIZE[1]/4))
scr.draw_rect(colour=(255,0,0), x=DISPSIZE[0]*0.25, y=DISPSIZE[1]/2, w=DISPSIZE[0]/10, h=DISPSIZE[0]/5, pw=5, fill=True)
scr.draw_rect(colour=(0,255,0), x=DISPSIZE[0]*0.75, y=DISPSIZE[1]/2, w=DISPSIZE[0]/10, h=DISPSIZE[0]/5, pw=5, fill=False)
disp.fill(scr)
disp.show()
kb.get_key()

#scr.draw_line()
scr.clear()
scr.draw_text("There should be three lines on the screen: \
\nred oblique on the left, green horizontal in the centre, and blue vertical on the right", pos=(DISPSIZE[0]/2, DISPSIZE[1]/4))
scr.draw_line(colour=(255,0,0), spos=(DISPSIZE[0]*0.20,DISPSIZE[1]*0.45), epos=(DISPSIZE[0]*0.30,DISPSIZE[1]*0.55), pw=5)
scr.draw_line(colour=(0,255,0), spos=(DISPSIZE[0]*0.45,DISPSIZE[1]/2), epos=(DISPSIZE[0]*0.55,DISPSIZE[1]/2), pw=5)
scr.draw_line(colour=(0,0,255), spos=(DISPSIZE[0]*0.75,DISPSIZE[1]*0.45), epos=(DISPSIZE[0]*0.75,DISPSIZE[1]*0.55), pw=5)
disp.fill(scr)
disp.show()
kb.get_key()

#scr.draw_polygon()
scr.clear()
scr.draw_text("There should be two polygons on the screen: \
\nred filled triangle on the left, and green unfilled hexagon on the right", pos=(DISPSIZE[0]/2, DISPSIZE[1]/4))
pl = [(DISPSIZE[0]*0.25, DISPSIZE[1]*0.45), (DISPSIZE[0]*0.2, DISPSIZE[1]*0.55), (DISPSIZE[0]*0.3, DISPSIZE[1]*0.55)]
scr.draw_polygon(pl, colour=(255,0,0), pw=5, fill=True)
# topleft, topright, centreright, bottomright, bottomleft, centreleft
pl = [(DISPSIZE[0]*0.70, DISPSIZE[1]*0.40), (DISPSIZE[0]*0.80, DISPSIZE[1]*0.40), (DISPSIZE[0]*0.85, DISPSIZE[1]*0.5), (DISPSIZE[0]*0.80, DISPSIZE[1]*0.60), (DISPSIZE[0]*0.70, DISPSIZE[1]*0.60), (DISPSIZE[0]*0.65, DISPSIZE[1]*0.5)]
scr.draw_polygon(pl, colour=(0,255,0), pw=5, fill=False)
Exemplo n.º 3
0
class TobiiProTracker(BaseEyeTracker):
    """A class for Tobii Pro EyeTracker objects"""

    def __init__(self, display, logfile=settings.LOGFILE,
                 eventdetection=settings.EVENTDETECTION,
                 saccade_velocity_threshold=35,
                 saccade_acceleration_threshold=9500,
                 blink_threshold=settings.BLINKTHRESH, **args):
        """Initializes a TobiiProTracker instance

        arguments
        display	--	a pygaze.display.Display instance

        keyword arguments
        None
        """
        self.gaze = []

        self.disp = display

        # initialize a screen
        self.screen = Screen()

        # initialize keyboard
        self.kb = Keyboard(keylist=['space', 'escape', 'q','enter'], timeout=1)

        self.recording = False

        self.screendist = settings.SCREENDIST

        if hasattr(settings, 'TRACKERSERIALNUMBER'):
            # Search for a specific eye tracker
            self.eyetrackers = [t for t in tr.find_all_eyetrackers() if t.serial_number == settings.TRACKERSERIALNUMBER]
        else:
            # Search for all eye trackers (The first one found will be selected)
            self.eyetrackers = tr.find_all_eyetrackers()

        if self.eyetrackers:
            self.eyetracker = self.eyetrackers[0]
        else:
            print("WARNING! libtobii.TobiiProTracker.__init__: no eye trackers found!")


        self.LEFT_EYE = 0
        self.RIGHT_EYE = 1
        self.BINOCULAR = 2
        self.eye_used = 0  # 0=left, 1=right, 2=binocular

        # calibration and validation points
        lb = 0.1  # left bound
        xc = 0.5  # horizontal center
        rb = 0.9  # right bound
        ub = 0.1  # upper bound
        yc = 0.5  # vertical center
        bb = 0.9  # bottom bound

        self.points_to_calibrate = [self._norm_2_px(p) for p in [(lb, ub), (xc,ub), (rb, ub), (lb,yc), (xc, yc),(rb,yc), (lb, bb),(xc,bb),(rb, bb)]]

        # event detection properties
        self.fixtresh = 1.5  # degrees; maximal distance from fixation start (if gaze wanders beyond this, fixation has stopped)
        self.fixtimetresh = 100  # milliseconds; amount of time gaze has to linger within self.fixtresh to be marked as a fixation
        self.spdtresh = saccade_velocity_threshold  # degrees per second; saccade velocity threshold
        self.accthresh = saccade_acceleration_threshold  # degrees per second**2; saccade acceleration threshold
        self.blinkthresh = blink_threshold  # milliseconds; blink detection threshold used in PyGaze method
        self.eventdetection = eventdetection
        self.weightdist = 10  # weighted distance, used for determining whether a movement is due to measurement error (1 is ok, higher is more conservative and will result in only larger saccades to be detected)

        self.screensize = settings.SCREENSIZE  # display size in cm
        self.pixpercm = (self.disp.dispsize[0] / float(self.screensize[0]) + self.disp.dispsize[1] / float(self.screensize[1])) / 2.0
        self.errdist = 2  # degrees; maximal error for drift correction
        self.pxerrdist = self._deg2pix(self.screendist, self.errdist, self.pixpercm)

        self.event_data = []

        self.t0 = None
        self._write_enabled = True

        self.datafile = open("{0}_TOBII_output.tsv".format(logfile), 'w')

        # initiation report
        self.datafile.write("pygaze initiation report start\n")
        self.datafile.write("display resolution: %sx%s\n" % (self.disp.dispsize[0], self.disp.dispsize[1]))
        self.datafile.write("display size in cm: %sx%s\n" % (self.screensize[0], self.screensize[1]))
        self.datafile.write("fixation threshold: %s degrees\n" % self.fixtresh)
        self.datafile.write("speed threshold: %s degrees/second\n" % self.spdtresh)
        self.datafile.write("acceleration threshold: %s degrees/second**2\n" % self.accthresh)
        self.datafile.write("pygaze initiation report end\n")

    def _norm_2_px(self, normalized_point):
        return (round(normalized_point[0] * self.disp.dispsize[0], 0), round(normalized_point[1] * self.disp.dispsize[1], 0))

    def _px_2_norm(self, pixelized_point):
        return (pixelized_point[0] / self.disp.dispsize[0], pixelized_point[1] / self.disp.dispsize[1])

    def _mean(self, array):
        if array:
            a = [s for s in array if s is not None]
            return sum(a) / float(len(a))

    def _deg2pix(self, cmdist, angle, pixpercm):
        return pixpercm * math.tan(math.radians(angle)) * float(cmdist)

    def log_var(self, var, val):
        """Writes a variable to the log file

        arguments
        var		-- variable name
        val		-- variable value

        returns
        Nothing	-- uses native log function to include a line
                    in the log file in a "var NAME VALUE" layout
        """
        self.log("var %s %s" % (var, val))

    def set_eye_used(self):
        """Logs the eye_used variable, based on which eye was specified.

        arguments
        None

        returns
        Nothing	-- logs which eye is used by calling self.log_var, e.g.
                   self.log_var("eye_used", "right")
        """
        if self.eye_used == self.BINOCULAR:
            self.log_var("eye_used", "binocular")
        elif self.eye_used == self.RIGHT_EYE:
            self.log_var("eye_used", "right")
        else:
            self.log_var("eye_used", "left")

    def is_valid_sample(self, sample):
        """Checks if the sample provided is valid, based on Tobii specific
        criteria (for internal use)

        arguments
        sample		--	a (x,y) gaze position tuple, as returned by
                        self.sample()

        returns
        valid			--	a Boolean: True on a valid sample, False on
                        an invalid sample
        """
        return sample != (-1, -1)

    def _on_gaze_data(self, gaze_data):
        self.gaze.append(gaze_data)
        if self._write_enabled:
            self._write_sample(gaze_data)

    def start_recording(self):
        """Starts recording eye position

        arguments
        None

        returns
        None		-- sets self.recording to True when recording is
                   successfully started
        """
        if not self.t0 and self._write_enabled:
            self.t0 = tr.get_system_time_stamp()
            self._write_header()

        if self.recording:
            print("WARNING! libtobii.TobiiProTracker.start_recording: Recording already started!")
            self.gaze = []
        else:
            self.gaze = []
            self.eyetracker.subscribe_to(tr.EYETRACKER_GAZE_DATA, self._on_gaze_data, as_dictionary=True)
            time.sleep(1)
            self.recording = True

    def stop_recording(self):
        """Stop recording eye position

        arguments
        None

        returns
        Nothing	-- sets self.recording to False when recording is
                   successfully started
        """
        if self.recording:
            self.eyetracker.unsubscribe_from(tr.EYETRACKER_GAZE_DATA)
            self.recording = False
            self.event_data = []
        else:
            print("WARNING! libtobii.TobiiProTracker.stop_recording: A recording has not been started!")

    def sample(self):
        """Returns newest available gaze position

        The gaze position is relative to the self.eye_used currently selected.
        If both eyes are selected, the gaze position is averaged from the data of both eyes.

        arguments
        None

        returns
        sample	-- an (x,y) tuple or a (-1,-1) on an error
        """

        gaze_sample = copy.copy(self.gaze[-1])


        if self.eye_used == self.LEFT_EYE and gaze_sample["left_gaze_point_validity"]:
            return self._norm_2_px(gaze_sample["left_gaze_point_on_display_area"])
        if self.eye_used == self.RIGHT_EYE and gaze_sample["right_gaze_point_validity"]:
            return self._norm_2_px(gaze_sample["right_gaze_point_on_display_area"])

        if self.eye_used == self.BINOCULAR:
            if gaze_sample["left_gaze_point_validity"] and gaze_sample["right_gaze_point_validity"]:
                left_sample = self._norm_2_px(gaze_sample["left_gaze_point_on_display_area"])
                right_sample = self._norm_2_px(gaze_sample["right_gaze_point_on_display_area"])
                return (self._mean([left_sample[0], right_sample[0]]), self._mean([left_sample[1], right_sample[1]]))

            if gaze_sample["left_gaze_point_validity"]:
                return self._norm_2_px(gaze_sample["left_gaze_point_on_display_area"])

            if gaze_sample["right_gaze_point_validity"]:
                return self._norm_2_px(gaze_sample["right_gaze_point_on_display_area"])

        return (-1, -1)

    def pupil_size(self):
        """Returns newest available pupil size

        arguments
        None

        returns
        pupilsize	-- a float if only eye is selected or only one eye has valid data.
                    -- a tuple with two floats if both eyes are selected.
                    -- -1 if there is no valid pupil data available.
        """
        if self.gaze:
            gaze_sample = copy.copy(self.gaze[-1])
            if self.eye_used == self.BINOCULAR:
                pupil_data = [-1, -1]
                if gaze_sample["left_pupil_validity"]:
                    pupil_data[0] = gaze_sample["left_pupil_diameter"]
                if gaze_sample["right_pupil_validity"]:
                    pupil_data[1] = gaze_sample["right_pupil_diameter"]
                return tuple(pupil_data)
            if self.eye_used == self.LEFT_EYE and gaze_sample["left_pupil_validity"]:
                return gaze_sample["left_pupil_diameter"]
            if self.eye_used == self.RIGHT_EYE and gaze_sample["right_pupil_validity"]:
                return gaze_sample["right_pupil_diameter"]
        return -1


    def ReduceBall (self,point,facteur,colour):
        self.showPoint(point,colour,facteur)
        for i in range (0,200,3):
            self.screen.clear()
            self.screen.draw_circle(colour=colour, pos=point, r=int(self.disp.dispsize[0] / (facteur+i)), pw=5, fill=True)
            self.disp.fill(self.screen)
            self.disp.show()
			



    def showPoint( self,point,colour,facteur):
        self.screen.clear()
        self.screen.draw_circle(colour=colour, pos=point, r=int(self.disp.dispsize[0] / facteur), pw=5, fill=True)
        self.disp.fill(self.screen)
        self.disp.show()


    def calibrate(self, calibrate=True, validate=True):
        """Calibrates the eye tracker.

        arguments
        None

        keyword arguments
        calibrate	--	Boolean indicating if calibration should be
                    performed (default = True).
        validate	--	Boolean indicating if validation should be performed
                    (default = True).

        returns
        success	--	returns True if calibration succeeded, or False if
                    not; in addition a calibration log is added to the
                    log file and some properties are updated (i.e. the
                    thresholds for detection algorithms)
        """
        self._write_enabled = False
        self.start_recording()
        self.screen.set_background_colour(colour=(0, 0, 0))

        if calibrate:
            origin = (int(self.disp.dispsize[0] / 4), int(self.disp.dispsize[1] / 4))
            size = (int(2 * self.disp.dispsize[0] / 4), int(2 * self.disp.dispsize[1] / 4))

            while not self.kb.get_key(keylist=['space'], flush=False)[0]:
                gaze_sample = copy.copy(self.gaze[-1])

                self.screen.clear()

                validity_colour = (255, 0, 0)

                if gaze_sample['right_gaze_origin_validity'] and gaze_sample['left_gaze_origin_validity']:
                    left_validity = 0.15 < gaze_sample['left_gaze_origin_in_trackbox_coordinate_system'][2] < 0.85
                    right_validity = 0.15 < gaze_sample['right_gaze_origin_in_trackbox_coordinate_system'][2] < 0.85
                    if left_validity and right_validity:
                        validity_colour = (0, 255, 0)

                self.screen.draw_text(text="When correctly positioned press \'space\' to start the calibration.", pos=(int(self.disp.dispsize[0] / 2), int(self.disp.dispsize[1] * 0.1)), colour=(255, 255, 255), fontsize=20)
                self.screen.draw_line(colour=validity_colour, spos=origin, epos=(origin[0] + size[0], origin[1]), pw=1)
                self.screen.draw_line(colour=validity_colour, spos=origin, epos=(origin[0], origin[1] + size[1]), pw=1)
                self.screen.draw_line(colour=validity_colour, spos=(origin[0], origin[1] + size[1]), epos=(origin[0] + size[0], origin[1] + size[1]), pw=1)
                self.screen.draw_line(colour=validity_colour, spos=(origin[0] + size[0], origin[1] + size[1]), epos=(origin[0] + size[0], origin[1]), pw=1)

                right_eye, left_eye, distance = None, None, []
                if gaze_sample['right_gaze_origin_validity']:
                    distance.append(round(gaze_sample['right_gaze_origin_in_user_coordinate_system'][2] / 10, 1))
                    right_eye = ((1 - gaze_sample['right_gaze_origin_in_trackbox_coordinate_system'][0]) * size[0] + origin[0],
                                gaze_sample['right_gaze_origin_in_trackbox_coordinate_system'][1] * size[1] + origin[1])
                    self.screen.draw_circle(colour=validity_colour, pos=right_eye, r=int(self.disp.dispsize[0] / 100), pw=5, fill=True)

                if gaze_sample['left_gaze_origin_validity']:
                    distance.append(round(gaze_sample['left_gaze_origin_in_user_coordinate_system'][2] / 10, 1))
                    left_eye = ((1 - gaze_sample['left_gaze_origin_in_trackbox_coordinate_system'][0]) * size[0] + origin[0],
                                gaze_sample['left_gaze_origin_in_trackbox_coordinate_system'][1] * size[1] + origin[1])
                    self.screen.draw_circle(colour=validity_colour, pos=left_eye, r=int(self.disp.dispsize[0] / 100), pw=5, fill=True)

                self.screen.draw_text(text="Current distance to the eye tracker: {0} cm.".format(self._mean(distance)), pos=(int(self.disp.dispsize[0] / 2), int(self.disp.dispsize[1] * 0.9)), colour=(255, 255, 255), fontsize=20)

                self.disp.fill(self.screen)
                self.disp.show()

            # # # # # #
            # # calibration

            if not self.eyetracker:
                print("WARNING! libtobii.TobiiProTracker.calibrate: no eye trackers found for the calibration!")
                self.stop_recording()
                return False

            calibration = tr.ScreenBasedCalibration(self.eyetracker)

            calibrating = True


            while calibrating:
                calibration.enter_calibration_mode()
                for point in self.points_to_calibrate:
                    self.screen.clear()
		    # CDP : Changement couleur
                    #self.screen.draw_circle(colour='yellow', pos=point, r=int(self.disp.dispsize[0] / 100.0), pw=5, fill=True)
                    #self.screen.draw_circle(colour=(255, 0, 0), pos=point, r=int(self.disp.dispsize[0] / 400.0), pw=5, fill=True)
                    #self.disp.fill(self.screen)
                    #self.disp.show()

                    # Wait a little for user to focus.
		    # CDP : Ajout sasie clavier
                    #clock.pause(1000)

                    self.ReduceBall(point,30,'yellow')

                    pressed_key = self.kb.get_key(keylist=['space', 'r'], flush=True, timeout=None)


                    normalized_point = self._px_2_norm(point)

                    if calibration.collect_data(normalized_point[0], normalized_point[1]) != tr.CALIBRATION_STATUS_SUCCESS:
                        # Try again if it didn't go well the first time.
                        # Not all eye tracker models will fail at this point, but instead fail on ComputeAndApply.
                        calibration.collect_data(normalized_point[0], normalized_point[1])

                self.screen.clear()
                self.screen.draw_text("Calculating calibration result....", colour=(255, 255, 255), fontsize=20)
                self.disp.fill(self.screen)
                self.disp.show()

                calibration_result = calibration.compute_and_apply()


                calibration.leave_calibration_mode()

                print "Compute and apply returned {0} and collected at {1} points.".\
                    format(calibration_result.status, len(calibration_result.calibration_points))

                if calibration_result.status != tr.CALIBRATION_STATUS_SUCCESS:
                    self.stop_recording()
                    print("WARNING! libtobii.TobiiProTracker.calibrate: Calibration was unsuccessful!")
                    return False

                self.screen.clear()
                for point in calibration_result.calibration_points:
                    self.screen.draw_circle(colour=(255, 255, 255), pos=self._norm_2_px(point.position_on_display_area), r=self.disp.dispsize[0] / 200, pw=1, fill=False)
                    for sample in point.calibration_samples:
                        if sample.left_eye.validity == tr.VALIDITY_VALID_AND_USED:
                            self.screen.draw_circle(colour=(255, 0, 0), pos=self._norm_2_px(sample.left_eye.position_on_display_area), r=self.disp.dispsize[0] / 450, pw=self.disp.dispsize[0] / 450, fill=False)
                            self.screen.draw_line(colour=(255, 0, 0), spos=self._norm_2_px(point.position_on_display_area), epos=self._norm_2_px(sample.left_eye.position_on_display_area), pw=1)
                        if sample.right_eye.validity == tr.VALIDITY_VALID_AND_USED:
                            self.screen.draw_circle(colour=(0, 0, 255), pos=self._norm_2_px(sample.right_eye.position_on_display_area), r=self.disp.dispsize[0] / 450, pw=self.disp.dispsize[0] / 450, fill=False)
                            self.screen.draw_line(colour=(0, 0, 255), spos=self._norm_2_px(point.position_on_display_area), epos=self._norm_2_px(sample.right_eye.position_on_display_area), pw=1)

                self.screen.draw_text("Press the \'R\' key to recalibrate or \'Space\' to continue....", pos=(0.5 * self.disp.dispsize[0], 0.95 * self.disp.dispsize[1]), colour=(255, 255, 255), fontsize=20)

                self.screen.draw_text("Left Eye", pos=(0.5 * self.disp.dispsize[0], 0.01 * self.disp.dispsize[1]), colour=(255, 0, 0), fontsize=20)
                self.screen.draw_text("Right Eye", pos=(0.5 * self.disp.dispsize[0], 0.03 * self.disp.dispsize[1]), colour=(0, 0, 255), fontsize=20)

                self.disp.fill(self.screen)
                self.disp.show()

                pressed_key = self.kb.get_key(keylist=['space', 'r'], flush=True, timeout=None)

                if pressed_key[0] == 'space':
                    calibrating = False

        if validate:
            # # # show menu
            self.screen.clear()
            self.screen.draw_text(text="Press space to start validation", colour=(255, 255, 255), fontsize=20)
            self.disp.fill(self.screen)
            self.disp.show()

            # # # wait for spacepress
            self.kb.get_key(keylist=['space'], flush=True, timeout=None)

            # # # # # #
            # # validation

            # # # arrays for data storage
            lxacc, lyacc, rxacc, ryacc = [], [], [], []

            # # loop through all calibration positions
            for pos in self.points_to_calibrate:
                # show validation point
                self.screen.clear()
                self.screen.draw_fixation(fixtype='dot', pos=pos, colour=(255, 255, 255))
                self.disp.fill(self.screen)
                self.disp.show()

                # allow user some time to gaze at dot
                clock.pause(1000)

                lxsamples, lysamples, rxsamples, rysamples = [], [], [], []
                for sample in self.gaze:
                    if sample["left_gaze_point_validity"]:
                        gaze_point = self._norm_2_px(sample["left_gaze_point_on_display_area"])
                        lxsamples.append(abs(gaze_point[0] - pos[0]))
                        lysamples.append(abs(gaze_point[1] - pos[1]))
                    if sample["right_gaze_point_validity"]:
                        gaze_point = self._norm_2_px(sample["right_gaze_point_on_display_area"])
                        rxsamples.append(abs(gaze_point[0] - pos[0]))
                        rysamples.append(abs(gaze_point[1] - pos[1]))

                # calculate mean deviation
                lxacc.append(self._mean(lxsamples))
                lyacc.append(self._mean(lysamples))
                rxacc.append(self._mean(rxsamples))
                ryacc.append(self._mean(rysamples))

                # wait for a bit to slow down validation process a bit
                clock.pause(1000)

            # calculate mean accuracy
            self.pxaccuracy = [(self._mean(lxacc), self._mean(lyacc)), (self._mean(rxacc), self._mean(ryacc))]

            # sample rate
            # calculate intersample times
            timestamps = []
            gaze_samples = copy.copy(self.gaze)
            for i in xrange(0, len(gaze_samples) - 1):
                timestamps.append((gaze_samples[i + 1]['system_time_stamp'] - gaze_samples[i]['system_time_stamp']) / 1000.0)

            # mean intersample time
            self.sampletime = self._mean(timestamps)
            self.samplerate = int(1000.0 / self.sampletime)

            # # # # # #
            # # RMS noise

            # # present instructions
            self.screen.clear()
            self.screen.draw_text(text="Noise calibration: please look at the dot\n\n(press space to start)", pos=(self.disp.dispsize[0] / 2, int(self.disp.dispsize[1] * 0.2)), colour=(255, 255, 255), fontsize=20)
            self.screen.draw_fixation(fixtype='dot', colour=(255, 255, 255))
            self.disp.fill(self.screen)
            self.disp.show()

            # # wait for spacepress
            self.kb.get_key(keylist=['space'], flush=True, timeout=None)

            # # show fixation
            self.screen.draw_fixation(fixtype='dot', colour=(255, 255, 255))
            self.disp.fill(self.screen)
            self.disp.show()
            self.screen.clear()

            # # wait for a bit, to allow participant to fixate
            clock.pause(500)

            # # get samples
            sl = [self.sample()]  # samplelist, prefilled with 1 sample to prevent sl[-1] from producing an error; first sample will be ignored for RMS calculation
            t0 = clock.get_time()  # starting time
            while clock.get_time() - t0 < 1000:
                s = self.sample()  # sample
                if s != sl[-1] and self.is_valid_sample(s) and s != (0, 0):
                    sl.append(s)

            # # calculate RMS noise
            Xvar, Yvar = [], []
            for i in xrange(2, len(sl)):
                Xvar.append((sl[i][0] - sl[i - 1][0])**2)
                Yvar.append((sl[i][1] - sl[i - 1][1])**2)
            XRMS = (self._mean(Xvar))**0.5
            YRMS = (self._mean(Yvar))**0.5
            self.pxdsttresh = (XRMS, YRMS)

            # # # # # # #
            # # # calibration report

            # # # # recalculate thresholds (degrees to pixels)
            self.pxfixtresh = self._deg2pix(self.screendist, self.fixtresh, self.pixpercm)
            self.pxspdtresh = self._deg2pix(self.screendist, self.spdtresh / 1000.0, self.pixpercm)  # in pixels per millisecons
            self.pxacctresh = self._deg2pix(self.screendist, self.accthresh / 1000.0, self.pixpercm)  # in pixels per millisecond**2

            data_to_write = ''
            data_to_write += "pygaze calibration report start\n"
            data_to_write += "samplerate: %s Hz\n" % self.samplerate
            data_to_write += "sampletime: %s ms\n" % self.sampletime
            data_to_write += "accuracy (in pixels): LX=%s, LY=%s, RX=%s, RY=%s\n" % (self.pxaccuracy[0][0], self.pxaccuracy[0][1], self.pxaccuracy[1][0], self.pxaccuracy[1][1])
            data_to_write += "precision (RMS noise in pixels): X=%s, Y=%s\n" % (self.pxdsttresh[0], self.pxdsttresh[1])
            data_to_write += "distance between participant and display: %s cm\n" % self.screendist
            data_to_write += "fixation threshold: %s pixels\n" % self.pxfixtresh
            data_to_write += "speed threshold: %s pixels/ms\n" % self.pxspdtresh
            data_to_write += "accuracy threshold: %s pixels/ms**2\n" % self.pxacctresh
            data_to_write += "pygaze calibration report end\n"

            # # # # write report to log
            #self.datafile.write(data_to_write)

            self.screen.clear()
            self.screen.draw_text(text=data_to_write, pos=(self.disp.dispsize[0] / 2, int(self.disp.dispsize[1] / 2)), colour=(255, 255, 255), fontsize=20)
            self.disp.fill(self.screen)
            self.disp.show()

            self.kb.get_key(keylist=['space'], flush=True, timeout=None)

        self.stop_recording()
        self._write_enabled = True

        return True

    def fix_triggered_drift_correction(self, pos=None, min_samples=10, max_dev=60, reset_threshold=30):
        """Performs a fixation triggered drift correction by collecting
        a number of samples and calculating the average distance from the
        fixation position

        arguments
        None

        keyword arguments
        pos			-- (x, y) position of the fixation dot or None for
                       a central fixation (default = None)
        min_samples		-- minimal amount of samples after which an
                       average deviation is calculated (default = 10)
        max_dev		-- maximal deviation from fixation in pixels
                       (default = 60)
        reset_threshold	-- if the horizontal or vertical distance in
                       pixels between two consecutive samples is
                       larger than this threshold, the sample
                       collection is reset (default = 30)

        returns
        checked		-- Boolean indicating if drift check is ok (True)
                       or not (False); or calls self.calibrate if 'q'
                       or 'escape' is pressed
        """
        if pos is None:
            pos = self.disp.dispsize[0] / 2, self.disp.dispsize[1] / 2

        # start recording if recording has not yet started
        if not self.recording:
            self.start_recording()
            stoprec = True
        else:
            stoprec = False

        # loop until we have sufficient samples
        lx = []
        ly = []
        while len(lx) < min_samples:

            # pressing escape enters the calibration screen
            if self.kb.get_key()[0] in ['escape', 'q']:
                print("libtobii.TobiiTracker.fix_triggered_drift_correction: 'q' or 'escape' pressed")
                return self.calibrate(calibrate=True, validate=True)

            # collect a sample
            x, y = self.sample()

            if len(lx) == 0 or (x, y) != (lx[-1], ly[-1]):

                # if present sample deviates too much from previous sample, reset counting
                if len(lx) > 0 and (abs(x - lx[-1]) > reset_threshold or abs(y - ly[-1]) > reset_threshold):
                    lx = []
                    ly = []

                # collect samples
                else:
                    lx.append(x)
                    ly.append(y)

            if len(lx) == min_samples:

                avg_x = self._mean(lx)
                avg_y = self._mean(ly)
                d = ((avg_x - pos[0]) ** 2 + (avg_y - pos[1]) ** 2)**0.5

                if d < max_dev:
                    if stoprec:
                        self.stop_recording()
                    return True
                else:
                    lx = []
                    ly = []
        if stoprec:
            self.stop_recording()

    def drift_correction(self, pos=None, fix_triggered=False):
        """Performs a drift check

        arguments
        None

        keyword arguments
        pos			-- (x, y) position of the fixation dot or None for
                       a central fixation (default = None)
        fix_triggered	-- Boolean indicating if drift check should be
                       performed based on gaze position (fix_triggered
                       = True) or on spacepress (fix_triggered =
                       False) (default = False)

        returns
        checked		-- Boolean indicating if drift check is ok (True)
                       or not (False); or calls self.calibrate if 'q'
                       or 'escape' is pressed
        """
        if fix_triggered:
            return self.fix_triggered_drift_correction(pos)

        if pos is None:
            pos = self.disp.dispsize[0] / 2, self.disp.dispsize[1] / 2

        # start recording if recording has not yet started
        if not self.recording:
            self.start_recording()
            stoprec = True
        else:
            stoprec = False

        result = False
        pressed = False
        while not pressed:
            pressed, presstime = self.kb.get_key()
            if pressed:
                if pressed == 'escape' or pressed == 'q':
                    print("libtobii.TobiiProTracker.drift_correction: 'q' or 'escape' pressed")
                    return self.calibrate(calibrate=True, validate=True)
                gazepos = self.sample()
                if ((gazepos[0] - pos[0])**2 + (gazepos[1] - pos[1])**2)**0.5 < self.pxerrdist:
                    result = True

        if stoprec:
            self.stop_recording()

        return result

    def wait_for_fixation_start(self):
        """Returns starting time and position when a fixation is started;
        function assumes a 'fixation' has started when gaze position
        remains reasonably stable (i.e. when most deviant samples are
        within self.pxfixtresh) for five samples in a row (self.pxfixtresh
        is created in self.calibration, based on self.fixtresh, a property
        defined in self.__init__)

        arguments
        None

        returns
        time, gazepos	-- time is the starting time in milliseconds (from
                       expstart), gazepos is a (x,y) gaze position
                       tuple of the position from which the fixation
                       was initiated
        """
        # # # # #
        # Tobii method

        if self.eventdetection == 'native':
            # print warning, since Tobii does not have a fixation start
            # detection built into their API (only ending)
            print("WARNING! 'native' event detection has been selected, \
                but Tobii does not offer fixation detection; PyGaze \
                algorithm will be used")

        # # # # #
        # PyGaze method

        # function assumes a 'fixation' has started when gaze position
        # remains reasonably stable for self.fixtimetresh

        # start recording if recording has not yet started
        if not self.recording:
            self.start_recording()
            stoprec = True
        else:
            stoprec = False

        # get starting position
        spos = self.sample()
        while not self.is_valid_sample(spos):
            spos = self.sample()

        # get starting time
        t0 = clock.get_time()

        # wait for reasonably stable position
        moving = True
        while moving:
            # get new sample
            npos = self.sample()
            # check if sample is valid
            if self.is_valid_sample(npos):
                # check if new sample is too far from starting position
                if (npos[0] - spos[0])**2 + (npos[1] - spos[1])**2 > self.pxfixtresh**2:  # Pythagoras
                    # if not, reset starting position and time
                    spos = copy.copy(npos)
                    t0 = clock.get_time()
                # if new sample is close to starting sample
                else:
                    # get timestamp
                    t1 = clock.get_time()
                    # check if fixation time threshold has been surpassed
                    if t1 - t0 >= self.fixtimetresh:
                        if stoprec:
                            self.stop_recording()
                        # return time and starting position
                        return t0, spos

    def wait_for_fixation_end(self):
        """Returns time and gaze position when a fixation has ended;
        function assumes that a 'fixation' has ended when a deviation of
        more than self.pxfixtresh from the initial fixation position has
        been detected (self.pxfixtresh is created in self.calibration,
        based on self.fixtresh, a property defined in self.__init__)

        arguments
        None

        returns
        time, gazepos	-- time is the starting time in milliseconds (from
                       expstart), gazepos is a (x,y) gaze position
                       tuple of the position from which the fixation
                       was initiated
        """
        # # # # #
        # Tobii method

        if self.eventdetection == 'native':
            # print warning, since Tobii does not have a fixation detection
            # built into their API
            print("WARNING! 'native' event detection has been selected, \
                but Tobii does not offer fixation detection; PyGaze algorithm \
                will be used")

        # # # # #
        # PyGaze method

        # function assumes that a 'fixation' has ended when a deviation of more than fixtresh
        # from the initial 'fixation' position has been detected

        # get starting time and position
        stime, spos = self.wait_for_fixation_start()

        # start recording if recording has not yet started
        if not self.recording:
            self.start_recording()
            stoprec = True
        else:
            stoprec = False

        # loop until fixation has ended
        while True:
            # get new sample
            npos = self.sample()  # get newest sample
            # check if sample is valid
            if self.is_valid_sample(npos):
                # check if sample deviates to much from starting position
                if (npos[0] - spos[0])**2 + (npos[1] - spos[1])**2 > self.pxfixtresh**2:
                    # break loop if deviation is too high
                    break

        if stoprec:
            self.stop_recording()

        return clock.get_time(), spos

    def wait_for_saccade_start(self):
        """Returns starting time and starting position when a saccade is
        started; based on Dalmaijer et al. (2013) online saccade detection
        algorithm

        arguments
        None

        returns
        endtime, startpos	-- endtime in milliseconds (from expbegintime);
                       startpos is an (x,y) gaze position tuple
        """
        # # # # #
        # Tobii method

        if self.eventdetection == 'native':
            # print warning, since Tobii does not have a blink detection
            # built into their API
            print("WARNING! 'native' event detection has been selected, \
                but Tobii does not offer saccade detection; PyGaze \
                algorithm will be used")

        # # # # #
        # PyGaze method

        # start recording if recording has not yet started
        if not self.recording:
            self.start_recording()
            stoprec = True
        else:
            stoprec = False

        # get starting position (no blinks)
        newpos = self.sample()
        while not self.is_valid_sample(newpos):
            newpos = self.sample()
        # get starting time, position, intersampledistance, and velocity
        t0 = clock.get_time()
        prevpos = newpos[:]
        s = 0
        v0 = 0

        # get samples
        saccadic = False
        while not saccadic:
            # get new sample
            newpos = self.sample()
            t1 = clock.get_time()
            if self.is_valid_sample(newpos) and newpos != prevpos:
                # check if distance is larger than precision error
                sx = newpos[0] - prevpos[0]
                sy = newpos[1] - prevpos[1]
                if (sx / self.pxdsttresh[0])**2 + (sy / self.pxdsttresh[1])**2 > self.weightdist:  # weigthed distance: (sx/tx)**2 + (sy/ty)**2 > 1 means movement larger than RMS noise
                    # calculate distance
                    s = ((sx)**2 + (sy)**2)**0.5  # intersampledistance = speed in pixels/ms
                    # calculate velocity
                    v1 = s / (t1 - t0)
                    # calculate acceleration
                    a = (v1 - v0) / (t1 - t0)  # acceleration in pixels/ms**2
                    # check if either velocity or acceleration are above threshold values
                    if v1 > self.pxspdtresh or a > self.pxacctresh:
                        saccadic = True
                        spos = prevpos[:]
                        stime = clock.get_time()
                    # update previous values
                    t0 = copy.copy(t1)
                    v0 = copy.copy(v1)

                # udate previous sample
                prevpos = newpos[:]

        if stoprec:
            self.stop_recording()

        return stime, spos

    def wait_for_saccade_end(self):
        """Returns ending time, starting and end position when a saccade is
        ended; based on Dalmaijer et al. (2013) online saccade detection
        algorithm

        arguments
        None

        returns
        endtime, startpos, endpos	-- endtime in milliseconds (from
                               expbegintime); startpos and endpos
                               are (x,y) gaze position tuples
        """
        # # # # #
        # Tobii method

        if self.eventdetection == 'native':
            # print warning, since Tobii does not have a blink detection
            # built into their API
            print("WARNING! 'native' event detection has been selected, \
                but Tobii does not offer saccade detection; PyGaze \
                algorithm will be used")

        # # # # #
        # PyGaze method

        # get starting position (no blinks)
        t0, spos = self.wait_for_saccade_start()

        # start recording if recording has not yet started
        if not self.recording:
            self.start_recording()
            stoprec = True
        else:
            stoprec = False

        # get valid sample
        prevpos = self.sample()
        while not self.is_valid_sample(prevpos):
            prevpos = self.sample()
        # get starting time, intersample distance, and velocity
        t1 = clock.get_time()
        s = ((prevpos[0] - spos[0])**2 + (prevpos[1] - spos[1])**2)**0.5  # = intersample distance = speed in px/sample
        v0 = s / (t1 - t0)

        # run until velocity and acceleration go below threshold
        saccadic = True
        while saccadic:
            # get new sample
            newpos = self.sample()
            t1 = clock.get_time()
            if self.is_valid_sample(newpos) and newpos != prevpos:
                # calculate distance
                s = ((newpos[0] - prevpos[0])**2 + (newpos[1] - prevpos[1])**2)**0.5  # = speed in pixels/sample
                # calculate velocity
                v1 = s / (t1 - t0)
                # calculate acceleration
                a = (v1 - v0) / (t1 - t0)  # acceleration in pixels/sample**2 (actually is v1-v0 / t1-t0; but t1-t0 = 1 sample)
                # check if velocity and acceleration are below threshold
                if v1 < self.pxspdtresh and (a > -1 * self.pxacctresh and a < 0):
                    saccadic = False
                    epos = newpos[:]
                    etime = clock.get_time()
                # update previous values
                t0 = copy.copy(t1)
                v0 = copy.copy(v1)
            # udate previous sample
            prevpos = newpos[:]

        if stoprec:
            self.stop_recording()

        return etime, spos, epos

    def wait_for_blink_start(self):
        """Waits for a blink start and returns the blink starting time

        arguments
        None

        returns
        timestamp		--	blink starting time in milliseconds, as
                        measured from experiment begin time
        """
        # # # # #
        # Tobii method

        if self.eventdetection == 'native':
            # print warning, since Tobii does not have a blink detection
            # built into their API
            print("WARNING! 'native' event detection has been selected, \
                but Tobii does not offer blink detection; PyGaze algorithm \
                will be used")

        # # # # #
        # PyGaze method

        # start recording if recording has not yet started
        if not self.recording:
            self.start_recording()
            stoprec = True
        else:
            stoprec = False

        blinking = False

        # loop until there is a blink
        while not blinking:
            # get newest sample
            gazepos = self.sample()
            # check if it's a valid sample
            if self.is_valid_sample(gazepos):
                # get timestamp for possible blink start
                t0 = clock.get_time()
                # loop until a blink is determined, or a valid sample occurs
                while not self.is_valid_sample(self.sample()):
                    # check if time has surpassed BLINKTHRESH
                    if clock.get_time() - t0 >= self.blinkthresh:
                        if stoprec:
                            self.stop_recording()
                        # return timestamp of blink start
                        return t0

    def wait_for_blink_end(self):
        """Waits for a blink end and returns the blink ending time

        arguments
        None

        returns
        timestamp		--	blink ending time in milliseconds, as
                        measured from experiment begin time
        """
        # # # # #
        # Tobii method
        if self.eventdetection == 'native':
            # print warning, since Tobii does not have a blink detection
            # built into their API
            print("WARNING! 'native' event detection has been selected, \
                but Tobii does not offer blink detection; PyGaze algorithm \
                will be used")

        # # # # #
        # PyGaze method

        # start recording if recording has not yet started
        if not self.recording:
            self.start_recording()
            stoprec = True
        else:
            stoprec = False

        blinking = True
        # loop while there is a blink
        while blinking:
            # get newest sample
            gazepos = self.sample()
            # check if it's valid
            if self.is_valid_sample(gazepos):
                # if it is a valid sample, blinking has stopped
                blinking = False

        if stoprec:
            self.stop_recording()

        # return timestamp of blink end
        return clock.get_time()

    def log(self, msg):
        """Writes a message to the log file

        arguments
        msg		-- a string to include in the log file

        returns
        Nothing	-- uses native log function to include a line
                   in the log file
        """
        t = tr.get_system_time_stamp()
        if not self.t0:
            self.t0 = t
            self._write_header()

        self.datafile.write('%.4f\t%s\n' % ((t - self.t0) / 1000.0, msg))
        self._flush_to_file()

    def _flush_to_file(self):
        # write data to disk
        self.datafile.flush()  # internal buffer to RAM
        os.fsync(self.datafile.fileno())  # RAM file cache to disk

    def _write_header(self):
        # write header
        self.datafile.write('\t'.join(['TimeStamp',
                                       'Event',
                                       'GazePointXLeft',
                                       'GazePointYLeft',
                                       'ValidityLeft',
                                       'GazePointXRight',
                                       'GazePointYRight',
                                       'ValidityRight',
                                       'GazePointX',
                                       'GazePointY',
                                       'PupilSizeLeft',
                                       'PupilValidityLeft',
                                       'PupilSizeRight',
                                       'PupilValidityRight']) + '\n')
        self._flush_to_file()

    def _write_sample(self, sample):
        # write timestamp and gaze position for both eyes to the datafile
        left_gaze_point = self._norm_2_px(sample['left_gaze_point_on_display_area']) if sample['left_gaze_point_validity'] else (-1, -1)
        right_gaze_point = self._norm_2_px(sample['right_gaze_point_on_display_area']) if sample['right_gaze_point_validity'] else (-1, -1)
        self.datafile.write('%.4f\t\t%d\t%d\t%d\t%d\t%d\t%d' % (
                            (sample['system_time_stamp'] - self.t0) / 1000.0,
                            left_gaze_point[0],
                            left_gaze_point[1],
                            sample['left_gaze_point_validity'],
                            right_gaze_point[0],
                            right_gaze_point[1],
                            sample['right_gaze_point_validity']))

        # if no correct sample is available, data is missing
        if not (sample['left_gaze_point_validity'] or sample['right_gaze_point_validity']):  # not detected
            ave = (-1.0, -1.0)
        # if the right sample is unavailable, use left sample
        elif not sample['right_gaze_point_validity']:
            ave = left_gaze_point
        # if the left sample is unavailable, use right sample
        elif not sample['left_gaze_point_validity']:
            ave = right_gaze_point
        # if we have both samples, use both samples
        else:
            ave = (int(round((left_gaze_point[0] + right_gaze_point[0]) / 2.0, 0)),
                   (int(round(left_gaze_point[1] + right_gaze_point[1]) / 2.0)))

        # write gaze position to the datafile, based on the selected sample(s)
        self.datafile.write('\t%d\t%d' % ave)

        left_pupil = sample['left_pupil_diameter'] if sample['left_pupil_validity'] else -1
        right_pupil = sample['right_pupil_diameter'] if sample['right_pupil_validity'] else -1

        self.datafile.write('\t%.4f\t%d\t%.4f\t%d' % (left_pupil,
                            sample['left_pupil_validity'],
                            right_pupil,
                            sample['right_pupil_validity']))

        self.datafile.write('\n')

        self._flush_to_file()

    def close(self):
        """Closes the currently used log file.

        arguments
        None

        returns
        None		--	closes the log file.
        """
        self.datafile.close()
Exemplo n.º 4
0
class OpenGazeTracker(BaseEyeTracker):

	"""A class for OpenGazeTracker objects"""

	def __init__(self, display, logfile=settings.LOGFILE, \
		eventdetection=settings.EVENTDETECTION, \
		saccade_velocity_threshold=35, \
		saccade_acceleration_threshold=9500, \
		blink_threshold=settings.BLINKTHRESH, \
		**args):

		"""Initializes the OpenGazeTracker object
		
		arguments
		display	-- a pygaze.display.Display instance
		
		keyword arguments
		logfile	-- logfile name (string value); note that this is the
				   name for the eye data log file (default = LOGFILE)
		"""

		# try to copy docstrings (but ignore it if it fails, as we do
		# not need it for actual functioning of the code)
		try:
			copy_docstr(BaseEyeTracker, OpenGazeTracker)
		except:
			# we're not even going to show a warning, since the copied
			# docstring is useful for code editors; these load the docs
			# in a non-verbose manner, so warning messages would be lost
			pass

		# object properties
		self.disp = display
		self.screen = Screen()
		self.dispsize = settings.DISPSIZE # display size in pixels
		self.screensize = settings.SCREENSIZE # display size in cm
		self.kb = Keyboard(keylist=['space', 'escape', 'q'], timeout=1)
		self.errorbeep = Sound(osc='saw', freq=100, length=100)
		
		# show a message
		self.screen.clear()
		self.screen.draw_text(
			text="Initialising the eye tracker, please wait...",
			fontsize=20)
		self.disp.fill(self.screen)
		self.disp.show()
		
		# output file properties
		self.outputfile = logfile + '.tsv'
		self.extralogname = logfile + '_log.txt'
		self.extralogfile = open(self.extralogname, 'w')
		
		# eye tracker properties
		self.has_been_calibrated_before = False
		self.connected = False
		self.recording = False
		self.errdist = 2 # degrees; maximal error for drift correction
		self.pxerrdist = 30 # initial error in pixels
		self.maxtries = 100 # number of samples obtained before giving up (for obtaining accuracy and tracker distance information, as well as starting or stopping recording)
		self.prevsample = (-1,-1)
		self.prevps = -1
		
		# event detection properties
		self.fixtresh = 1.5 # degrees; maximal distance from fixation start (if gaze wanders beyond this, fixation has stopped)
		self.fixtimetresh = 100 # milliseconds; amount of time gaze has to linger within self.fixtresh to be marked as a fixation
		self.spdtresh = saccade_velocity_threshold # degrees per second; saccade velocity threshold
		self.accthresh = saccade_acceleration_threshold # degrees per second**2; saccade acceleration threshold
		self.blinkthresh = blink_threshold # milliseconds; blink detection threshold used in PyGaze method
		self.eventdetection = eventdetection
		self.set_detection_type(self.eventdetection)
		self.weightdist = 10 # weighted distance, used for determining whether a movement is due to measurement error (1 is ok, higher is more conservative and will result in only larger saccades to be detected)

		# connect to the tracker
		self.opengaze = OpenGaze(ip='127.0.0.1', port=4242, \
			logfile=self.outputfile, debug=False)

		# get info on the sample rate
		# TODO: Compute after streaming some samples?
		self.samplerate = 60.0
		self.sampletime = 1000.0 / self.samplerate

		# initiation report
		self._elog("pygaze initiation report start")
		self._elog("display resolution: %sx%s" % (self.dispsize[0], self.dispsize[1]))
		self._elog("display size in cm: %sx%s" % (self.screensize[0], self.screensize[1]))
		self._elog("samplerate: %.2f Hz" % self.samplerate)
		self._elog("sampletime: %.2f ms" % self.sampletime)
		self._elog("fixation threshold: %s degrees" % self.fixtresh)
		self._elog("speed threshold: %s degrees/second" % self.spdtresh)
		self._elog("acceleration threshold: %s degrees/second**2" % self.accthresh)
		self._elog("pygaze initiation report end")


	def _elog(self, msg):
		
		"""Logs a message to the additional log.
		"""
		
		self.extralogfile.write(msg + '\n')


	def calibrate(self):

		"""Calibrates the eye tracking system
		
		arguments
		None
		
		keyword arguments
		None

		returns
		success	-- returns True if calibration succeeded, or False if
				   not; in addition a calibration log is added to the
				   log file and some properties are updated (i.e. the
				   thresholds for detection algorithms)
		"""
		
		# show a message
		self.screen.clear()
		self.screen.draw_text(
			text="Preparing the calibration, please wait...",
			fontsize=20)
		self.disp.fill(self.screen)
		self.disp.show()
		
		# CALIBRATION
		# Set the duration of the calibration animation, and of the
		# calibration point.
		caldur = {'animation':1.5, 'point':1.0, 'timeout':10.0}
		self.opengaze.calibrate_delay(caldur['animation'])
		self.opengaze.calibrate_timeout(caldur['point'])
		# Determine the calibration points.
		#calibpoints = [(0.1,0.5), (0.5, 0.1), (0.5, 0.5), (0.5, 0.9), (0.9, 0.5)]
		calibpoints = []
		for x in [0.1, 0.5, 0.9]:
			for y in [0.1, 0.5, 0.9]:
				calibpoints.append((x,y))
		random.shuffle(calibpoints)
		
		# Clear the OpenGaze calibration.
		self.opengaze.calibrate_clear()
		# Add all new points (as proportions of the display resolution).
		for x, y in calibpoints:
			self.opengaze.calibrate_addpoint(x, y)
		
		# show a message
		self.screen.clear()
		self.screen.draw_text(
			text="Press Space to calibrate, S to skip, and Q to quit",
			fontsize=20)
		self.disp.fill(self.screen)
		self.disp.show()
		
		# wait for keyboard input
		key, keytime = self.kb.get_key(keylist=['q', 's', 'space'],
			timeout=None, flush=True)
		if key == 's':
			return True
		if key == 'q':
			quited = True
		else:
			quited = False
		
		# Run until the user is statisfied, or quits.
		calibrated = False
		while not quited and not calibrated:

			# CALIBRATE
			# Run a new calibration. The result is the latest available
			# calibration results as a list of dicts, each with the
			#  following keys:
			# CALX: Calibration point's horizontal coordinate.
			# CALY: Calibration point's vertical coordinate
			# LX:   Left eye's recorded horizontal point of gaze.
			# LY:   Left eye's recorded vertical point of gaze.
			# LV:   Left eye's validity status (1=valid, 0=invalid)
			# RX:   Right eye's recorded horizontal point of gaze.
			# RY:   Right eye's recorded vertical point of gaze.
			# RV:   Right eye's validity status (1=valid, 0=invalid)

			# Clear the existing calibration results.
			self.opengaze.clear_calibration_result()
			
			# Make sure we have the right calibration points.
			# NOTE: Somehow polling this results in no weird OpenGaze errors
			# on calibrations that occur after the first one.
			# (WTF, Gazepoint?!)
			calibpoints = self.opengaze.get_calibration_points()

			# Show the calibration screen.
			# NOTE: THIS DOESN'T WORK IN FULL SCREEN MODE :(
			#self.opengaze.calibrate_show(True)

			# Start the calibration.
			self.opengaze.calibrate_start(True)

			# Show the calibration dots. The strategy is to wait for the
			# next calibration point start, then to show that dot, and
			# then to show the animation (hoping to Godzilla that the
			# timing roughly matches that of the OpenGaze server), and
			# then to keep the target on-screen until the start of the
			# next calibration point.
			pointnr = 0
			n_points = len(calibpoints)
			# On a restart, the calibration starts with the last point,
			# before looping through all the other points. (DAMN YOU,
			# GAZEPOINT, THAT DOES NOT MAKE SENSE!)
			if self.has_been_calibrated_before:
				n_points += 1
			# Loop through all the points.
			for i in range(n_points):
				# Wait for the next calibration point.
				pointnr, pos = self.opengaze.wait_for_calibration_point_start( \
					timeout=caldur['timeout'])
				# The wait_for_calibration_point_start function returns
				# None if no point was started before a timeout. We
				# should panic if no calibration point was started.
				if pointnr is None:
					# Break the calibration loop, and quit the current
					# calibration.
					quited = True
					break
				# Compute the point in display coordinates.
				x = int(pos[0] * self.dispsize[0])
				y = int(pos[1] * self.dispsize[1])
				# Get a timestamp for the start of the animation.
				t1 = clock.get_time()
				t = clock.get_time()
				# Show the animation.
				while t - t1 < caldur['animation']*1000:
					# Check if the Q key has been pressed, and break
					# if it has.
					if self.kb.get_key(keylist=['q'], timeout=10, \
						flush=False)[0] == 'q':
						quited = True
						break
					# Clear the screen.
					self.screen.clear(colour=(0,0,0))
					# Caculate at which point in the animation we are.
					p = 1.0 - float(t-t1) / (caldur['animation']*1000)
					# Draw the animated disk.
					self.screen.draw_circle(colour=(255,255,255), \
						pos=(x, y), r=max(1, int(30*p)), fill=True)
					# Draw the calibration target.
					self.screen.draw_circle(colour=(255,0,0), \
						pos=(x, y), r=3, fill=True)
					# Show the screen.
					self.disp.fill(self.screen)
					t = self.disp.show()
				# Check if the Q key has been pressed, and break
				# if it has.
				if self.kb.get_key(keylist=['q'], timeout=1, \
					flush=False)[0] == 'q':
					quited = True
				# Don't show the other points if Q was pressed.
				if quited:
					break

			# Wait for the calibration result.
			calibresult = None
			while (calibresult is None) and (not quited):
				# Check if there is a result yet (returns None if there
				# isn't).
				calibresult = self.opengaze.get_calibration_result()
				# Check if the Q key has been pressed, and break if it
				# is.
				if self.kb.get_key(keylist=['q'], timeout=100, \
					flush=False)[0] == 'q':
					quited = True
					break
			# Hide the calibration window.
			# NOTE: No need for this in full-screen mode.
			#self.opengaze.calibrate_show(False)

			# Retry option if the calibration was aborted			
			if quited:
				# show retry message
				self.screen.clear()
				self.screen.draw_text( \
					text="Calibration aborted. Press Space to restart or 'Q' to quit", \
					fontsize=20)
				self.disp.fill(self.screen)
				self.disp.show()
				# get input
				key, keytime = self.kb.get_key(keylist=['q','space'], \
					timeout=None, flush=True)
				if key == 'space':
					# unset quited Boolean
					quited = False
				# skip further processing
				continue

			# Empty display.
			self.disp.fill()
			self.disp.show()

			# RESULTS
			# Clear the screen.
			self.screen.clear()
			# draw results for each point
			if calibresult is not None:

				# Loop through all points.
				for p in calibresult:
					
					# Convert the points (relative coordinates) to
					# display coordinates.
					for param in ['CALX', 'LX', 'RX']:
						p[param] *= self.dispsize[0]
					for param in ['CALY', 'LY', 'RY']:
						p[param] *= self.dispsize[1]
					
					# Draw the target.
					self.screen.draw_fixation(fixtype='dot',
						colour=(115,210,22), \
						pos=(p['CALX'], p['CALY']))
					
					# If the calibration for this target is valid,
					# draw the estimated point. We have two points:
					# one for left and one for right.
					col = {'L':(32,74,135), 'R':(92,53,102)}
					for eye in ['L', 'R']:
						# Check if the eye is valid, and choose the
						# position and colour accordingly.
						if p['%sV' % (eye)]:
							x = p['%sX' % (eye)]
							y = p['%sY' % (eye)]
							c = col[eye]
						else:
							x = p['CALX']
							y = p['CALY']
							c = (204,0,0)
						# Draw a line between the estimated and the
						# actual point.
						if p['%sV' % (eye)]:
							self.screen.draw_line(colour=c, \
								spos=(p['CALX'], p['CALY']), \
								epos=(x,y), \
								pw=3)
						# Draw the estimated gaze point.
						self.screen.draw_fixation( \
							fixtype='dot', pos=(x, y), colour=c)
						# Annotate which eye this is.
						self.screen.draw_text(text=eye, \
							pos=(x+10, y+10), colour=c, \
							fontsize=20)

				# Draw input options.
				self.screen.draw_text(
					text="Press Space to continue or 'R' to restart",
					pos=(int(self.dispsize[0]*0.5), \
						int(self.dispsize[1]*0.25+60)), \
					fontsize=20)
			else:
				self.screen.draw_text(
					text="Calibration failed. Press 'R' to try again.",
					fontsize=20)

			# Show the results.
			self.disp.fill(self.screen)
			self.disp.show()
			# Wait for input.
			key, keytime = self.kb.get_key(keylist=['space','r'], \
				timeout=None, flush=True)
			# Process input.
			if key == 'space':
				calibrated = True

			# Set the 'restart' flag to True, because everything that
			# happens after this will be a repeated calibration or
			# will have noting to do with the calibration.
			self.has_been_calibrated_before = True

		# Calibration failed if the user quited.
		if quited:
			return False

		# NOISE CALIBRATION
		# Get all error estimates (distance between the real and the
		# estimated points in pixels).
		err = {'LX':[], 'LY':[], 'RX':[], 'RY':[]}
		var = {'LX':[], 'LY':[], 'RX':[], 'RY':[]}
		for p in calibresult:
			# Only use the point if it was valid.
			for eye in ['L', 'R']:
				for dim in ['X', 'Y']:
					if p['%sV' % (eye)]:
						# Compute the distance between the points.
						d = p['%s%s' % (eye, dim)] - \
							p['CAL%s' % (dim)]
						# Store the distance.
						err['%s%s' % (eye, dim)].append(abs(d))
						# Store the squared distance.
						var['%s%s' % (eye, dim)].append(d**2)
		# Compute the RMS noise for the calibration points.
		xnoise = (math.sqrt(sum(var['LX']) / float(len(var['LX']))) + \
			math.sqrt(sum(var['RX']) / float(len(var['RX'])))) / 2.0
		ynoise = (math.sqrt(sum(var['LY']) / float(len(var['LY']))) + \
			math.sqrt(sum(var['RY']) / float(len(var['RY'])))) / 2.0
		self.pxdsttresh = (xnoise, ynoise)
				
		# AFTERMATH
		# store some variables
		pixpercm = (self.dispsize[0] / float(self.screensize[0]) + \
			self.dispsize[1]/float(self.screensize[1])) / 2
		screendist = settings.SCREENDIST
		# calculate thresholds based on tracker settings
		self.accuracy = ( \
			(pix2deg(screendist, sum(err['LX']) / float(len(err['LX'])), pixpercm), \
			pix2deg(screendist, sum(err['LY']) / float(len(err['LY'])), pixpercm)), \
			(pix2deg(screendist, sum(err['RX']) / float(len(err['RX'])), pixpercm), \
			pix2deg(screendist, sum(err['RY']) / float(len(err['RY'])), pixpercm)))
		self.pxerrdist = deg2pix(screendist, self.errdist, pixpercm)
		self.pxfixtresh = deg2pix(screendist, self.fixtresh, pixpercm)
		self.pxaccuracy = ( \
			(sum(err['LX']) / float(len(err['LX'])), \
			sum(err['LY']) / float(len(err['LY']))), \
			(sum(err['RX']) / float(len(err['RX'])), \
			sum(err['RY']) / float(len(err['RY']))))
		self.pxspdtresh = deg2pix(screendist, self.spdtresh/1000.0, pixpercm) # in pixels per millisecond
		self.pxacctresh = deg2pix(screendist, self.accthresh/1000.0, pixpercm) # in pixels per millisecond**2

		# calibration report
		self._elog("pygaze calibration report start")
		self._elog("accuracy (degrees): LX=%s, LY=%s, RX=%s, RY=%s" % (self.accuracy[0][0],self.accuracy[0][1],self.accuracy[1][0],self.accuracy[1][1]))
		self._elog("accuracy (in pixels): LX=%s, LY=%s, RX=%s, RY=%s" % (self.pxaccuracy[0][0],self.pxaccuracy[0][1],self.pxaccuracy[1][0],self.pxaccuracy[1][1]))
		self._elog("precision (RMS noise in pixels): X=%s, Y=%s" % (self.pxdsttresh[0],self.pxdsttresh[1]))
		self._elog("distance between participant and display: %s cm" % screendist)
		self._elog("fixation threshold: %s pixels" % self.pxfixtresh)
		self._elog("speed threshold: %s pixels/ms" % self.pxspdtresh)
		self._elog("acceleration threshold: %s pixels/ms**2" % self.pxacctresh)
		self._elog("pygaze calibration report end")

		return True


	def close(self):

		"""Neatly close connection to tracker
		
		arguments
		None
		
		returns
		Nothing	-- saves data and sets self.connected to False
		"""

		# Close additional log file.
		self.extralogfile.close()

		# close connection
		self.opengaze.close()
		self.connected = False		


	def connected(self):

		"""Checks if the tracker is connected
		
		arguments
		None
		
		returns
		connected	-- True if connection is established, False if not;
				   sets self.connected to the same value
		"""

		self.connected = self.opengaze._connected.is_set()

		return self.connected


	def drift_correction(self, pos=None, fix_triggered=False):

		"""Performs a drift check
		
		arguments
		None
		
		keyword arguments
		pos			-- (x, y) position of the fixation dot or None for
					   a central fixation (default = None)
		fix_triggered	-- Boolean indicating if drift check should be
					   performed based on gaze position (fix_triggered
					   = True) or on spacepress (fix_triggered = 
					   False) (default = False)
		
		returns
		checked		-- Boolaan indicating if drift check is ok (True)
					   or not (False); or calls self.calibrate if 'q'
					   or 'escape' is pressed
		"""
		
		if pos == None:
			pos = self.dispsize[0] / 2, self.dispsize[1] / 2
		if fix_triggered:
			return self.fix_triggered_drift_correction(pos)
		
		# DEBUG #
		print(("Running drift correction, pos=(%d, %d)" % (pos[0], pos[1])))
		# # # # #

		self.draw_drift_correction_target(pos[0], pos[1])

		pressed = False
		while not pressed:
			pressed, presstime = self.kb.get_key()
			if pressed:
				if pressed == 'escape' or pressed == 'q':
					print("libopengaze.OpenGazeTracker.drift_correction: 'q' or 'escape' pressed")
					return self.calibrate()
				gazepos = self.sample()
				if ((gazepos[0]-pos[0])**2  + (gazepos[1]-pos[1])**2)**0.5 < self.pxerrdist:
					return True
				else:
					self.errorbeep.play()
		return False
		
	def draw_drift_correction_target(self, x, y):
		
		"""
		Draws the drift-correction target.
		
		arguments
		
		x		--	The X coordinate
		y		--	The Y coordinate
		"""
		
		self.screen.clear()
		self.screen.draw_fixation(fixtype='dot', colour=settings.FGC, pos=(x,y),
			pw=0, diameter=12)
		self.disp.fill(self.screen)
		self.disp.show()			
		
	def draw_calibration_target(self, x, y):
		
		self.draw_drift_correction_target(x, y)

	def fix_triggered_drift_correction(self, pos=None, min_samples=4, max_dev=120, timeout=10000):

		"""Performs a fixation triggered drift correction by collecting
		a number of samples and calculating the average distance from the
		fixation position
		
		arguments
		None
		
		keyword arguments
		pos			-- (x, y) position of the fixation dot or None for
					   a central fixation (default = None)
		min_samples	-- minimal amount of samples after which a
					   fixation is accepted (default = 4)
		max_dev		-- maximal deviation from fixation in pixels
					   (default = 120)
		timeout		-- Time in milliseconds until fixation-triggering is
					   given up on, and calibration is started 
					   (default = 10000)
		
		returns
		checked		-- Boolean indicating if drift check is ok (True)
					   or not (False); or calls self.calibrate if 'q'
					   or 'escape' is pressed
		"""

		if pos == None:
			pos = self.dispsize[0] / 2, self.dispsize[1] / 2

		self.draw_drift_correction_target(pos[0], pos[1])
		
		t0 = clock.get_time()
		consecutive_count = 0
		while consecutive_count < min_samples:

			# Get new sample.
			x, y = self.sample()

			# Ignore empty samples.
			if (x is None) or (y is None):
				continue

			# Measure the distance to the target position.
			d = ((x-pos[0])**2 + (y-pos[1])**2)**0.5
			# Check whether the distance is below the allowed distance.
			if d <= max_dev:
				# Increment count.
				consecutive_count += 1
			else:
				# Reset count.
				consecutive_count = 0
			
			# Check for a timeout.
			if clock.get_time() - t0 > timeout:
				print("libopengaze.OpenGazeTracker.fix_triggered_drift_correction: timeout during fixation-triggered drift check")
				return self.calibrate()
				

			# Pressing escape enters the calibration screen.
			if self.kb.get_key()[0] in ['escape','q']:
				print("libopengaze.OpenGazeTracker.fix_triggered_drift_correction: 'q' or 'escape' pressed")
				return self.calibrate()
		
		return True


	def get_eyetracker_clock_async(self):

		"""Not supported for OpenGazeTracker (yet)"""

		print("function not supported yet")


	def log(self, msg):

		"""Writes a message to the log file
		
		arguments
		ms		-- a string to include in the log file
		
		returns
		Nothing	-- uses native log function of iViewX to include a line
				   in the log file
		"""
		
		self._elog(msg)
		if self.recording:
			self.opengaze.log(msg)

	def prepare_drift_correction(self, pos):

		"""Not supported for OpenGazeTracker (yet)"""

		print("function not supported yet")


	def pupil_size(self):

		"""Return pupil size
		
		arguments
		None
		
		returns
		pupil size	-- returns pupil diameter for the eye that is currently
				   being tracked (as specified by self.eye_used) or -1
				   when no data is obtainable
		"""
		
		# get newest pupil size
		ps = self.opengaze.pupil_size()
		
		# invalid data
		if ps == None:
			return -1
		
		# check if the new pupil size is the same as the previous
		if ps != self.prevps:
			# update the pupil size
			self.prevps = copy.copy(ps)
		
		return self.prevps


	def sample(self):

		"""Returns newest available gaze position
		
		arguments
		None
		
		returns
		sample	-- an (x,y) tuple or a (-1,-1) on an error
		"""

		# Get newest sample.
		rs = self.opengaze.sample()
		
		# Invalid data.
		if rs == (None, None):
			return (-1,-1)
		
		# Convert relative coordinates to display coordinates.
		s = (rs[0]*self.dispsize[0], rs[1]*self.dispsize[1])
		# Check if the new sample is the same as the previous.
		if s != self.prevsample:
			# Update the current sample.
			self.prevsample = copy.copy(s)
		
		return self.prevsample


	def send_command(self, cmd):

		"""Function not supported. Use self.opengaze instead; it supports
		all possible API calls.
		"""

		print("send_command function not supported; use self.opengaze instead")


	def start_recording(self):

		"""Starts recording eye position
		
		arguments
		None
		
		returns
		Nothing	-- sets self.recording to True when recording is
				   successfully started
		"""

		self.opengaze.start_recording()
		self.recording = True


	def status_msg(self, msg):

		"""Not supported for OpenGazeTracker (yet)"""

		print("function not supported yet")


	def stop_recording(self):

		"""Stop recording eye position
		
		arguments
		None
		
		returns
		Nothing	-- sets self.recording to False when recording is
				   successfully started
		"""

		self.opengaze.stop_recording()
		self.recording = False
	
	
	def set_detection_type(self, eventdetection):
		
		"""Set the event detection type to either PyGaze algorithms, or
		native algorithms as provided by the manufacturer (only if
		available: detection type will default to PyGaze if no native
		functions are available)
		
		arguments
		eventdetection	--	a string indicating which detection type
						should be employed: either 'pygaze' for
						PyGaze event detection algorithms or
						'native' for manufacturers algorithms (only
						if available; will default to 'pygaze' if no
						native event detection is available)
		returns		--	detection type for saccades, fixations and
						blinks in a tuple, e.g. 
						('pygaze','native','native') when 'native'
						was passed, but native detection was not
						available for saccade detection
		"""
		
		if eventdetection in ['pygaze','native']:
			self.eventdetection = eventdetection
		
		return ('pygaze','pygaze','pygaze')


	def wait_for_event(self, event):

		"""Waits for event
		
		arguments
		event		-- an integer event code, one of the following:
					3 = STARTBLINK
					4 = ENDBLINK
					5 = STARTSACC
					6 = ENDSACC
					7 = STARTFIX
					8 = ENDFIX
		
		returns
		outcome	-- a self.wait_for_* method is called, depending on the
				   specified event; the return values of corresponding
				   method are returned
		"""

		if event == 5:
			outcome = self.wait_for_saccade_start()
		elif event == 6:
			outcome = self.wait_for_saccade_end()
		elif event == 7:
			outcome = self.wait_for_fixation_start()
		elif event == 8:
			outcome = self.wait_for_fixation_end()
		elif event == 3:
			outcome = self.wait_for_blink_start()
		elif event == 4:
			outcome = self.wait_for_blink_end()
		else:
			raise Exception("Error in libopengaze.OpenGazeTracker.wait_for_event: eventcode %s is not supported" % event)

		return outcome


	def wait_for_blink_end(self):

		"""Waits for a blink end and returns the blink ending time
		
		arguments
		None
		
		returns
		timestamp		--	blink ending time in milliseconds, as
						measured from experiment begin time
		"""

		
		# # # # #
		# OpenGaze method

		if self.eventdetection == 'native':
			
			# print warning, since OpenGaze does not have a blink detection
			# built into their API
			
			print("WARNING! 'native' event detection has been selected, \
				but OpenGaze does not offer blink detection; PyGaze algorithm \
				will be used")

		# # # # #
		# PyGaze method
		
		blinking = True
		
		# loop while there is a blink
		while blinking:
			# get newest sample
			gazepos = self.sample()
			# check if it's valid
			if self.is_valid_sample(gazepos):
				# if it is a valid sample, blinking has stopped
				blinking = False
		
		# return timestamp of blink end
		return clock.get_time()		
		

	def wait_for_blink_start(self):

		"""Waits for a blink start and returns the blink starting time
		
		arguments
		None
		
		returns
		timestamp		--	blink starting time in milliseconds, as
						measured from experiment begin time
		"""
		
		# # # # #
		# OpenGaze method

		if self.eventdetection == 'native':
			
			# print warning, since OpenGaze does not have a blink detection
			# built into their API
			
			print("WARNING! 'native' event detection has been selected, \
				but OpenGaze does not offer blink detection; PyGaze algorithm \
				will be used")

		# # # # #
		# PyGaze method
		
		blinking = False
		
		# loop until there is a blink
		while not blinking:
			# get newest sample
			gazepos = self.sample()
			# check if it's a valid sample
			if not self.is_valid_sample(gazepos):
				# get timestamp for possible blink start
				t0 = clock.get_time()
				# loop until a blink is determined, or a valid sample occurs
				while not self.is_valid_sample(self.sample()):
					# check if time has surpassed BLINKTHRESH
					if clock.get_time()-t0 >= self.blinkthresh:
						# return timestamp of blink start
						return t0
		

	def wait_for_fixation_end(self):

		"""Returns time and gaze position when a fixation has ended;
		function assumes that a 'fixation' has ended when a deviation of
		more than self.pxfixtresh from the initial fixation position has
		been detected (self.pxfixtresh is created in self.calibration,
		based on self.fixtresh, a property defined in self.__init__)
		
		arguments
		None
		
		returns
		time, gazepos	-- time is the starting time in milliseconds (from
					   expstart), gazepos is a (x,y) gaze position
					   tuple of the position from which the fixation
					   was initiated
		"""

		# # # # #
		# OpenGaze method

		if self.eventdetection == 'native':
			
			# print warning, since OpenGaze does not have a blink detection
			# built into their API
			
			print("WARNING! 'native' event detection has been selected, \
				but OpenGaze does not offer fixation detection; \
				PyGaze algorithm will be used")

		# # # # #
		# PyGaze method
			
		# function assumes that a 'fixation' has ended when a deviation of more than fixtresh
		# from the initial 'fixation' position has been detected
		
		# get starting time and position
		stime, spos = self.wait_for_fixation_start()
		
		# loop until fixation has ended
		while True:
			# get new sample
			npos = self.sample() # get newest sample
			# check if sample is valid
			if self.is_valid_sample(npos):
				# check if sample deviates to much from starting position
				if (npos[0]-spos[0])**2 + (npos[1]-spos[1])**2 > self.pxfixtresh**2: # Pythagoras
					# break loop if deviation is too high
					break

		return clock.get_time(), spos


	def wait_for_fixation_start(self):

		"""Returns starting time and position when a fixation is started;
		function assumes a 'fixation' has started when gaze position
		remains reasonably stable (i.e. when most deviant samples are
		within self.pxfixtresh) for five samples in a row (self.pxfixtresh
		is created in self.calibration, based on self.fixtresh, a property
		defined in self.__init__)
		
		arguments
		None
		
		returns
		time, gazepos	-- time is the starting time in milliseconds (from
					   expstart), gazepos is a (x,y) gaze position
					   tuple of the position from which the fixation
					   was initiated
		"""
		
		# # # # #
		# OpenGaze method

		if self.eventdetection == 'native':
			
			# print warning, since OpenGaze does not have a fixation start
			# detection built into their API (only ending)
			
			print("WARNING! 'native' event detection has been selected, \
				but OpenGaze does not offer fixation detection; \
				PyGaze algorithm will be used")
			
			
		# # # # #
		# PyGaze method
		
		# function assumes a 'fixation' has started when gaze position
		# remains reasonably stable for self.fixtimetresh
		
		# get starting position
		spos = self.sample()
		while not self.is_valid_sample(spos):
			spos = self.sample()
		
		# get starting time
		t0 = clock.get_time()

		# wait for reasonably stable position
		moving = True
		while moving:
			# get new sample
			npos = self.sample()
			# check if sample is valid
			if self.is_valid_sample(npos):
				# check if new sample is too far from starting position
				if (npos[0]-spos[0])**2 + (npos[1]-spos[1])**2 > self.pxfixtresh**2: # Pythagoras
					# if not, reset starting position and time
					spos = copy.copy(npos)
					t0 = clock.get_time()
				# if new sample is close to starting sample
				else:
					# get timestamp
					t1 = clock.get_time()
					# check if fixation time threshold has been surpassed
					if t1 - t0 >= self.fixtimetresh:
						# return time and starting position
						return t1, spos


	def wait_for_saccade_end(self):

		"""Returns ending time, starting and end position when a saccade is
		ended; based on Dalmaijer et al. (2013) online saccade detection
		algorithm
		
		arguments
		None
		
		returns
		endtime, startpos, endpos	-- endtime in milliseconds (from 
							   expbegintime); startpos and endpos
							   are (x,y) gaze position tuples
		"""

		# # # # #
		# OpenGaze method

		if self.eventdetection == 'native':
			
			# print warning, since OpenGaze does not have a blink detection
			# built into their API
			
			print("WARNING! 'native' event detection has been selected, \
				but OpenGaze does not offer saccade detection; PyGaze \
				algorithm will be used")

		# # # # #
		# PyGaze method
		
		# get starting position (no blinks)
		t0, spos = self.wait_for_saccade_start()
		# get valid sample
		prevpos = self.sample()
		while not self.is_valid_sample(prevpos):
			prevpos = self.sample()
		# get starting time, intersample distance, and velocity
		t1 = clock.get_time()
		s = ((prevpos[0]-spos[0])**2 + (prevpos[1]-spos[1])**2)**0.5 # = intersample distance = speed in px/sample
		v0 = s / (t1-t0)

		# run until velocity and acceleration go below threshold
		saccadic = True
		while saccadic:
			# get new sample
			newpos = self.sample()
			t1 = clock.get_time()
			if self.is_valid_sample(newpos) and newpos != prevpos:
				# calculate distance
				s = ((newpos[0]-prevpos[0])**2 + (newpos[1]-prevpos[1])**2)**0.5 # = speed in pixels/sample
				# calculate velocity
				v1 = s / (t1-t0)
				# calculate acceleration
				a = (v1-v0) / (t1-t0) # acceleration in pixels/sample**2 (actually is v1-v0 / t1-t0; but t1-t0 = 1 sample)
				# check if velocity and acceleration are below threshold
				if v1 < self.pxspdtresh and (a > -1*self.pxacctresh and a < 0):
					saccadic = False
					epos = newpos[:]
					etime = clock.get_time()
				# update previous values
				t0 = copy.copy(t1)
				v0 = copy.copy(v1)
			# udate previous sample
			prevpos = newpos[:]

		return etime, spos, epos


	def wait_for_saccade_start(self):

		"""Returns starting time and starting position when a saccade is
		started; based on Dalmaijer et al. (2013) online saccade detection
		algorithm
		
		arguments
		None
		
		returns
		endtime, startpos	-- endtime in milliseconds (from expbegintime);
					   startpos is an (x,y) gaze position tuple
		"""

		# # # # #
		# OpenGaze method

		if self.eventdetection == 'native':
			
			# print warning, since OpenGaze does not have a blink detection
			# built into their API
			
			print("WARNING! 'native' event detection has been selected, \
				but OpenGaze does not offer saccade detection; PyGaze \
				algorithm will be used")

		# # # # #
		# PyGaze method
		
		# get starting position (no blinks)
		newpos = self.sample()
		while not self.is_valid_sample(newpos):
			newpos = self.sample()
		# get starting time, position, intersampledistance, and velocity
		t0 = clock.get_time()
		prevpos = newpos[:]
		s = 0
		v0 = 0

		# get samples
		saccadic = False
		while not saccadic:
			# get new sample
			newpos = self.sample()
			t1 = clock.get_time()
			if self.is_valid_sample(newpos) and newpos != prevpos:
				# check if distance is larger than precision error
				sx = newpos[0]-prevpos[0]; sy = newpos[1]-prevpos[1]
				if (sx/self.pxdsttresh[0])**2 + (sy/self.pxdsttresh[1])**2 > self.weightdist: # weigthed distance: (sx/tx)**2 + (sy/ty)**2 > 1 means movement larger than RMS noise
					# calculate distance
					s = ((sx)**2 + (sy)**2)**0.5 # intersampledistance = speed in pixels/ms
					# calculate velocity
					v1 = s / (t1-t0)
					# calculate acceleration
					a = (v1-v0) / (t1-t0) # acceleration in pixels/ms**2
					# check if either velocity or acceleration are above threshold values
					if v1 > self.pxspdtresh or a > self.pxacctresh:
						saccadic = True
						spos = prevpos[:]
						stime = clock.get_time()
					# update previous values
					t0 = copy.copy(t1)
					v0 = copy.copy(v1)

				# udate previous sample
				prevpos = newpos[:]

		return stime, spos
	
	
	def is_valid_sample(self, gazepos):
		
		"""Checks if the sample provided is valid, based on OpenGaze specific
		criteria (for internal use)
		
		arguments
		gazepos		--	a (x,y) gaze position tuple, as returned by
						self.sample()
		
		returns
		valid		--	a Boolean: True on a valid sample, False on
						an invalid sample
		"""
		
		# return False if a sample is invalid
		if gazepos == (None,None) or gazepos == (-1,-1):
			return False
		
		# in any other case, the sample is valid
		return True
Exemplo n.º 5
0
tracker.calibrate()

# starting screen
scr.clear()
scr.draw_text(text="Press Space to start")

disp.fill(scr)
disp.show()
kb.get_key(keylist=['space'], timeout=None, flush=True)


# # # # #
# VALIDATION horizontal

scr.clear()
scr.draw_line((0,0,0), (int(0.05*DISPSIZE[0]),int(0.15*DISPSIZE[1])), (int(0.95*DISPSIZE[0]),int(0.15*DISPSIZE[1])), 1)
scr.draw_line((0,0,0), (int(0.95*DISPSIZE[0]),int(0.15*DISPSIZE[1])), (int(0.95*DISPSIZE[0]),int(0.5*DISPSIZE[1])), 1)
scr.draw_line((0,0,0), (int(0.95*DISPSIZE[0]),int(0.5*DISPSIZE[1])), (int(0.05*DISPSIZE[0]),int(0.5*DISPSIZE[1])), 1)
scr.draw_line((0,0,0), (int(0.05*DISPSIZE[0]),int(0.5*DISPSIZE[1])), (int(0.05*DISPSIZE[0]),int(0.85*DISPSIZE[1])), 1)
scr.draw_line((0,0,0), (int(0.05*DISPSIZE[0]),int(0.85*DISPSIZE[1])), (int(0.95*DISPSIZE[0]),int(0.85*DISPSIZE[1])), 1)

# loop through points
for i in range(len(CALIBPOINTShor)):
    # get coordinate
    x, y = CALIBPOINTShor[i]
        # draw calibration point
        scr.draw_fixation(fixtype='cross', pos=(x,y))

disp.fill(scr)
disp.show()
Exemplo n.º 6
0
class EyeTribeTracker(BaseEyeTracker):

	"""A class for EyeTribeTracker objects"""

	def __init__(self, display, logfile=settings.LOGFILE,
		eventdetection=settings.EVENTDETECTION, saccade_velocity_threshold=35,
		saccade_acceleration_threshold=9500, blink_threshold=settings.BLINKTHRESH,
		**args):

		"""Initializes the EyeTribeTracker object
		
		arguments
		display	-- a pygaze.display.Display instance
		
		keyword arguments
		logfile	-- logfile name (string value); note that this is the
				   name for the eye data log file (default = LOGFILE)
		"""

		# try to copy docstrings (but ignore it if it fails, as we do
		# not need it for actual functioning of the code)
		try:
			copy_docstr(BaseEyeTracker, EyeTribeTracker)
		except:
			# we're not even going to show a warning, since the copied
			# docstring is useful for code editors; these load the docs
			# in a non-verbose manner, so warning messages would be lost
			pass

		# object properties
		self.disp = display
		self.screen = Screen()
		self.dispsize = settings.DISPSIZE # display size in pixels
		self.screensize = settings.SCREENSIZE # display size in cm
		self.kb = Keyboard(keylist=['space', 'escape', 'q'], timeout=1)
		self.errorbeep = Sound(osc='saw',freq=100, length=100)
		
		# output file properties
		self.outputfile = logfile
		
		# eye tracker properties
		self.connected = False
		self.recording = False
		self.errdist = 2 # degrees; maximal error for drift correction
		self.pxerrdist = 30 # initial error in pixels
		self.maxtries = 100 # number of samples obtained before giving up (for obtaining accuracy and tracker distance information, as well as starting or stopping recording)
		self.prevsample = (-1,-1)
		self.prevps = -1
		
		# event detection properties
		self.fixtresh = 1.5 # degrees; maximal distance from fixation start (if gaze wanders beyond this, fixation has stopped)
		self.fixtimetresh = 100 # milliseconds; amount of time gaze has to linger within self.fixtresh to be marked as a fixation
		self.spdtresh = saccade_velocity_threshold # degrees per second; saccade velocity threshold
		self.accthresh = saccade_acceleration_threshold # degrees per second**2; saccade acceleration threshold
		self.blinkthresh = blink_threshold # milliseconds; blink detection threshold used in PyGaze method
		self.eventdetection = eventdetection
		self.set_detection_type(self.eventdetection)
		self.weightdist = 10 # weighted distance, used for determining whether a movement is due to measurement error (1 is ok, higher is more conservative and will result in only larger saccades to be detected)

		# connect to the tracker
		self.eyetribe = EyeTribe(logfilename=logfile)

		# get info on the sample rate
		self.samplerate = self.eyetribe._samplefreq
		self.sampletime = 1000.0 * self.eyetribe._intsampletime

		# initiation report
		self.log("pygaze initiation report start")
		self.log("display resolution: %sx%s" % (self.dispsize[0],self.dispsize[1]))
		self.log("display size in cm: %sx%s" % (self.screensize[0],self.screensize[1]))
		self.log("samplerate: %.2f Hz" % self.samplerate)
		self.log("sampletime: %.2f ms" % self.sampletime)
		self.log("fixation threshold: %s degrees" % self.fixtresh)
		self.log("speed threshold: %s degrees/second" % self.spdtresh)
		self.log("acceleration threshold: %s degrees/second**2" % self.accthresh)
		self.log("pygaze initiation report end")


	def calibrate(self):

		"""Calibrates the eye tracking system
		
		arguments
		None
		
		keyword arguments
		None

		returns
		success	-- returns True if calibration succeeded, or False if
				   not; in addition a calibration log is added to the
				   log file and some properties are updated (i.e. the
				   thresholds for detection algorithms)
		"""
		
		# CALIBRATION
		# determine the calibration points
		calibpoints = []
		for x in [0.1,0.5,0.9]:
			for y in [0.1,0.5,0.9]:
				calibpoints.append((int(x*self.dispsize[0]),int(y*self.dispsize[1])))
		random.shuffle(calibpoints)
		
		# show a message
		self.screen.clear()
		self.screen.draw_text(
			text="Press Space to calibrate, S to skip, and Q to quit",
			fontsize=20)
		self.disp.fill(self.screen)
		self.disp.show()
		
		# wait for keyboard input
		key, keytime = self.kb.get_key(keylist=['q', 's', 'space'],
			timeout=None, flush=True)
		if key == 's':
			return True
		if key == 'q':
			quited = True
		else:
			quited = False
		
		# Pause the processing of samples during the calibration.
#		self.eyetribe._pause_sample_processing()
		# run until the user is statisfied, or quits
		calibrated = False
		calibresult = None
		while not quited and not calibrated:

			# Clear the existing calibration.
			if self.eyetribe._tracker.get_iscalibrated():
				self.eyetribe._lock.acquire(True)
				self.eyetribe.calibration.clear()
				self.eyetribe._lock.release()
			
			# Wait for a bit.
			clock.pause(1500)

			# start a new calibration
			if not self.eyetribe._tracker.get_iscalibrating():
				self.eyetribe._lock.acquire(True)
				self.eyetribe.calibration.start(pointcount=len(calibpoints))
				self.eyetribe._lock.release()
			
			# loop through calibration points
			for cpos in calibpoints:
				# Check whether the calibration is already done.
				# (Not sure how or why, but for some reason some data
				# can persist between calbrations, and the tracker will
				# simply stop allowing further pointstart requests.)
				if self.eyetribe._tracker.get_iscalibrated():
					break
				
				# Draw a calibration target.
				self.draw_calibration_target(cpos[0], cpos[1])
				# wait for a bit to allow participant to start looking at
				# the calibration point (#TODO: space press?)
				clock.pause(settings.EYETRIBEPRECALIBDUR)
				# start calibration of point
				self.eyetribe._lock.acquire(True)
				self.eyetribe.calibration.pointstart(cpos[0],cpos[1])
				self.eyetribe._lock.release()
				# wait for a second
				clock.pause(settings.EYETRIBECALIBDUR)
				# stop calibration of this point
				self.eyetribe._lock.acquire(True)
				self.eyetribe.calibration.pointend()
				self.eyetribe._lock.release()
				# check if the Q key has been pressed
				if self.kb.get_key(keylist=['q'],timeout=10,flush=False)[0] == 'q':
					# abort calibration
					self.eyetribe._lock.acquire(True)
					self.eyetribe.calibration.abort()
					self.eyetribe._lock.release()
					# set quited variable and break this for loop
					quited = True
					break
			
			# retry option if the calibration was aborted			
			if quited:
				# show retry message
				self.screen.clear()
				self.screen.draw_text(
					"Calibration aborted. Press Space to restart or 'Q' to quit",
					fontsize=20)
				self.disp.fill(self.screen)
				self.disp.show()
				# get input
				key, keytime = self.kb.get_key(keylist=['q','space'], timeout=None, flush=True)
				if key == 'space':
					# unset quited Boolean
					quited = False
				# skip further processing
				continue

			# empty display
			self.disp.fill()
			self.disp.show()
			# allow for a bit of calculation time
			# (this is waaaaaay too much)
			clock.pause(1000)
			# get the calibration result
			self.eyetribe._lock.acquire(True)
			calibresult = self.eyetribe._tracker.get_calibresult()
			self.eyetribe._lock.release()

			# results
			# clear the screen
			self.screen.clear()
			# draw results for each point
			if type(calibresult) == dict:
				for p in calibresult['calibpoints']:
					# only draw the point if data was obtained
					if p['state'] > 0:
						# draw the mean error
						# self.screen.draw_circle(colour=(252,233,79),
						# 	pos=(p['cpx'],p['cpy']), r=p['mepix'], pw=0,
						# 	fill=True)
						self.screen.draw_line(spos=(p['cpx'],p['cpy']),
							epos=(p['mecpx'],p['mecpy']), pw=2)
						# draw the point
						self.screen.draw_fixation(fixtype='dot',
							colour=(115,210,22), pos=(p['cpx'],p['cpy']))
						# draw the estimated point
						self.screen.draw_fixation(fixtype='dot',
							colour=(32,74,135), pos=(p['mecpx'],p['mecpy']))
						# annotate accuracy
						self.screen.draw_text(text='%.2f' % p['acd'],
							pos=(p['cpx']+10,p['cpy']+10), fontsize=20)
					# if no data was obtained, draw the point in red
					else:
						self.screen.draw_fixation(fixtype='dot',
							colour=(204,0,0), pos=(p['cpx'],p['cpy']))
				# draw box for averages
				# self.screen.draw_rect(colour=(238,238,236), x=int(self.dispsize[0]*0.15), y=int(self.dispsize[1]*0.2), w=400, h=200, pw=0, fill=True)
				# draw result
				if calibresult['result']:
					self.screen.draw_text(text="Calibration successful",
						colour='green',
						pos=(int(self.dispsize[0]*0.5), int(self.dispsize[1]*0.25)), 
						fontsize=20)
				else:
					self.screen.draw_text(text="Calibration failed",
						colour='red',
						pos=(int(self.dispsize[0]*0.5),int(self.dispsize[1]*0.25)),
						fontsize=20)
				# draw average accuracy
				self.screen.draw_text(
					text="Average error = %.2f degrees" % (calibresult['deg']),
					pos=(int(self.dispsize[0]*0.5),int(self.dispsize[1]*0.25+30)),
					fontsize=20)
				# draw input options
				self.screen.draw_text(
					text="Press Space to continue or 'R' to restart",
					pos=(int(self.dispsize[0]*0.5),int(self.dispsize[1]*0.25+60)),
					fontsize=20)
			else:
				self.screen.draw_text(
					text="Calibration failed. Press 'R' to try again.",
					fontsize=20)
			# show the results
			self.disp.fill(self.screen)
			self.disp.show()
			# wait for input
			key, keytime = self.kb.get_key(keylist=['space','r'], timeout=None,
				flush=True)
			# process input
			if key == 'space':
				calibrated = True

		# Continue the processing of samples after the calibration.
#		self.eyetribe._unpause_sample_processing()

		# calibration failed if the user quited
		if quited:
			return False

		# NOISE CALIBRATION
		# get all error estimates (pixels)
		var = []
		for p in calibresult['calibpoints']:
			# only draw the point if data was obtained
			if p['state'] > 0:
				var.append(p['mepix'])
		noise = sum(var) / float(len(var))
		self.pxdsttresh = (noise, noise)
				
		# AFTERMATH
		# store some variables
		pixpercm = (self.dispsize[0]/float(self.screensize[0]) + self.dispsize[1]/float(self.screensize[1])) / 2
		screendist = settings.SCREENDIST
		# calculate thresholds based on tracker settings
		self.accuracy = ((calibresult['Ldeg'],calibresult['Ldeg']), (calibresult['Rdeg'],calibresult['Rdeg'])) 
		self.pxerrdist = deg2pix(screendist, self.errdist, pixpercm)
		self.pxfixtresh = deg2pix(screendist, self.fixtresh, pixpercm)
		self.pxaccuracy = ((deg2pix(screendist, self.accuracy[0][0], pixpercm),deg2pix(screendist, self.accuracy[0][1], pixpercm)), (deg2pix(screendist, self.accuracy[1][0], pixpercm),deg2pix(screendist, self.accuracy[1][1], pixpercm)))
		self.pxspdtresh = deg2pix(screendist, self.spdtresh/1000.0, pixpercm) # in pixels per millisecond
		self.pxacctresh = deg2pix(screendist, self.accthresh/1000.0, pixpercm) # in pixels per millisecond**2

		# calibration report
		self.log("pygaze calibration report start")
		self.log("accuracy (degrees): LX=%s, LY=%s, RX=%s, RY=%s" % (self.accuracy[0][0],self.accuracy[0][1],self.accuracy[1][0],self.accuracy[1][1]))
		self.log("accuracy (in pixels): LX=%s, LY=%s, RX=%s, RY=%s" % (self.pxaccuracy[0][0],self.pxaccuracy[0][1],self.pxaccuracy[1][0],self.pxaccuracy[1][1]))
		self.log("precision (RMS noise in pixels): X=%s, Y=%s" % (self.pxdsttresh[0],self.pxdsttresh[1]))
		self.log("distance between participant and display: %s cm" % screendist)
		self.log("fixation threshold: %s pixels" % self.pxfixtresh)
		self.log("speed threshold: %s pixels/ms" % self.pxspdtresh)
		self.log("acceleration threshold: %s pixels/ms**2" % self.pxacctresh)
		self.log("pygaze calibration report end")

		return True


	def close(self):

		"""Neatly close connection to tracker
		
		arguments
		None
		
		returns
		Nothing	-- saves data and sets self.connected to False
		"""

		# close connection
		self.eyetribe.close()
		self.connected = False		


	def connected(self):

		"""Checks if the tracker is connected
		
		arguments
		None
		
		returns
		connected	-- True if connection is established, False if not;
				   sets self.connected to the same value
		"""

		res = self.eyetribe._tracker.get_trackerstate()

		if res == 0:
			self.connected = True
		else:
			self.connected = False

		return self.connected


	def drift_correction(self, pos=None, fix_triggered=False):

		"""Performs a drift check
		
		arguments
		None
		
		keyword arguments
		pos			-- (x, y) position of the fixation dot or None for
					   a central fixation (default = None)
		fix_triggered	-- Boolean indicating if drift check should be
					   performed based on gaze position (fix_triggered
					   = True) or on spacepress (fix_triggered = 
					   False) (default = False)
		
		returns
		checked		-- Boolaan indicating if drift check is ok (True)
					   or not (False); or calls self.calibrate if 'q'
					   or 'escape' is pressed
		"""
		
		if pos == None:
			pos = self.dispsize[0] / 2, self.dispsize[1] / 2
		if fix_triggered:
			return self.fix_triggered_drift_correction(pos)		
		self.draw_drift_correction_target(pos[0], pos[1])
		pressed = False
		while not pressed:
			pressed, presstime = self.kb.get_key()
			if pressed:
				if pressed == 'escape' or pressed == 'q':
					print("libeyetribe.EyeTribeTracker.drift_correction: 'q' or 'escape' pressed")
					return self.calibrate()
				gazepos = self.sample()
				if ((gazepos[0]-pos[0])**2  + (gazepos[1]-pos[1])**2)**0.5 < self.pxerrdist:
					return True
				else:
					self.errorbeep.play()
		return False
		
	def draw_drift_correction_target(self, x, y):
		
		"""
		Draws the drift-correction target.
		
		arguments
		
		x		--	The X coordinate
		y		--	The Y coordinate
		"""
		
		self.screen.clear()
		self.screen.draw_fixation(fixtype='dot', colour=settings.FGC, pos=(x,y),
			pw=0, diameter=12)
		self.disp.fill(self.screen)
		self.disp.show()			
		
	def draw_calibration_target(self, x, y):
		
		self.draw_drift_correction_target(x, y)

	def fix_triggered_drift_correction(self, pos=None, min_samples=10, max_dev=60, reset_threshold=30):

		"""Performs a fixation triggered drift correction by collecting
		a number of samples and calculating the average distance from the
		fixation position
		
		arguments
		None
		
		keyword arguments
		pos			-- (x, y) position of the fixation dot or None for
					   a central fixation (default = None)
		min_samples		-- minimal amount of samples after which an
					   average deviation is calculated (default = 10)
		max_dev		-- maximal deviation from fixation in pixels
					   (default = 60)
		reset_threshold	-- if the horizontal or vertical distance in
					   pixels between two consecutive samples is
					   larger than this threshold, the sample
					   collection is reset (default = 30)
		
		returns
		checked		-- Boolaan indicating if drift check is ok (True)
					   or not (False); or calls self.calibrate if 'q'
					   or 'escape' is pressed
		"""

		self.draw_drift_correction_target(pos[0], pos[1])
		if pos == None:
			pos = self.dispsize[0] / 2, self.dispsize[1] / 2

		# loop until we have sufficient samples
		lx = []
		ly = []
		while len(lx) < min_samples:

			# pressing escape enters the calibration screen
			if self.kb.get_key()[0] in ['escape','q']:
				print("libeyetribe.EyeTribeTracker.fix_triggered_drift_correction: 'q' or 'escape' pressed")
				return self.calibrate()

			# collect a sample
			x, y = self.sample()

			if len(lx) == 0 or x != lx[-1] or y != ly[-1]:

				# if present sample deviates too much from previous sample, reset counting
				if len(lx) > 0 and (abs(x - lx[-1]) > reset_threshold or abs(y - ly[-1]) > reset_threshold):
					lx = []
					ly = []

				# collect samples
				else:
					lx.append(x)
					ly.append(y)

			if len(lx) == min_samples:

				avg_x = sum(lx) / len(lx)
				avg_y = sum(ly) / len(ly)
				d = ((avg_x - pos[0]) ** 2 + (avg_y - pos[1]) ** 2)**0.5

				if d < max_dev:
					return True
				else:
					lx = []
					ly = []			

	def get_eyetracker_clock_async(self):

		"""Not supported for EyeTribeTracker (yet)"""

		print("function not supported yet")


	def log(self, msg):

		"""Writes a message to the log file
		
		arguments
		ms		-- a string to include in the log file
		
		returns
		Nothing	-- uses native log function of iViewX to include a line
				   in the log file
		"""

		self.eyetribe.log_message(msg)

	def prepare_drift_correction(self, pos):

		"""Not supported for EyeTribeTracker (yet)"""

		print("function not supported yet")


	def pupil_size(self):

		"""Return pupil size
		
		arguments
		None
		
		returns
		pupil size	-- returns pupil diameter for the eye that is currently
				   being tracked (as specified by self.eye_used) or -1
				   when no data is obtainable
		"""
		
		# get newest pupil size
		ps = self.eyetribe.pupil_size()
		
		# invalid data
		if ps == None:
			return -1
		
		# check if the new pupil size is the same as the previous
		if ps != self.prevps:
			# update the pupil size
			self.prevps = copy.copy(ps)
		
		return self.prevps


	def sample(self):

		"""Returns newest available gaze position
		
		arguments
		None
		
		returns
		sample	-- an (x,y) tuple or a (-1,-1) on an error
		"""

		# get newest sample
		s = self.eyetribe.sample()
		
		# invalid data
		if s == (None,None):
			return (-1,-1)
		
		# check if the new sample is the same as the previous
		if s != self.prevsample:
			# update the current sample
			self.prevsample = copy.copy(s)
		
		return self.prevsample


	def send_command(self, cmd):

		"""Sends a command to the eye tracker
		
		arguments
		cmd		--	the command to be sent to the EyeTribe, which should
					be a list with the following information:
						[category, request, values]
		
		returns
		Nothing
		"""

		self.eyetribe._connection.request(cmd)


	def start_recording(self):

		"""Starts recording eye position
		
		arguments
		None
		
		returns
		Nothing	-- sets self.recording to True when recording is
				   successfully started
		"""

		self.eyetribe.start_recording()
		self.recording = True


	def status_msg(self, msg):

		"""Not supported for EyeTribeTracker (yet)"""

		print("function not supported yet")


	def stop_recording(self):

		"""Stop recording eye position
		
		arguments
		None
		
		returns
		Nothing	-- sets self.recording to False when recording is
				   successfully started
		"""

		self.eyetribe.stop_recording()
		self.recording = False
	
	
	def set_detection_type(self, eventdetection):
		
		"""Set the event detection type to either PyGaze algorithms, or
		native algorithms as provided by the manufacturer (only if
		available: detection type will default to PyGaze if no native
		functions are available)
		
		arguments
		eventdetection	--	a string indicating which detection type
						should be employed: either 'pygaze' for
						PyGaze event detection algorithms or
						'native' for manufacturers algorithms (only
						if available; will default to 'pygaze' if no
						native event detection is available)
		returns		--	detection type for saccades, fixations and
						blinks in a tuple, e.g. 
						('pygaze','native','native') when 'native'
						was passed, but native detection was not
						available for saccade detection
		"""
		
		if eventdetection in ['pygaze','native']:
			self.eventdetection = eventdetection
		
		return ('pygaze','pygaze','pygaze')


	def wait_for_event(self, event):

		"""Waits for event
		
		arguments
		event		-- an integer event code, one of the following:
					3 = STARTBLINK
					4 = ENDBLINK
					5 = STARTSACC
					6 = ENDSACC
					7 = STARTFIX
					8 = ENDFIX
		
		returns
		outcome	-- a self.wait_for_* method is called, depending on the
				   specified event; the return values of corresponding
				   method are returned
		"""

		if event == 5:
			outcome = self.wait_for_saccade_start()
		elif event == 6:
			outcome = self.wait_for_saccade_end()
		elif event == 7:
			outcome = self.wait_for_fixation_start()
		elif event == 8:
			outcome = self.wait_for_fixation_end()
		elif event == 3:
			outcome = self.wait_for_blink_start()
		elif event == 4:
			outcome = self.wait_for_blink_end()
		else:
			raise Exception("Error in libsmi.SMItracker.wait_for_event: eventcode %s is not supported" % event)

		return outcome


	def wait_for_blink_end(self):

		"""Waits for a blink end and returns the blink ending time
		
		arguments
		None
		
		returns
		timestamp		--	blink ending time in milliseconds, as
						measured from experiment begin time
		"""

		
		# # # # #
		# EyeTribe method

		if self.eventdetection == 'native':
			
			# print warning, since EyeTribe does not have a blink detection
			# built into their API
			
			print("WARNING! 'native' event detection has been selected, \
				but EyeTribe does not offer blink detection; PyGaze algorithm \
				will be used")

		# # # # #
		# PyGaze method
		
		blinking = True
		
		# loop while there is a blink
		while blinking:
			# get newest sample
			gazepos = self.sample()
			# check if it's valid
			if self.is_valid_sample(gazepos):
				# if it is a valid sample, blinking has stopped
				blinking = False
		
		# return timestamp of blink end
		return clock.get_time()		
		

	def wait_for_blink_start(self):

		"""Waits for a blink start and returns the blink starting time
		
		arguments
		None
		
		returns
		timestamp		--	blink starting time in milliseconds, as
						measured from experiment begin time
		"""
		
		# # # # #
		# EyeTribe method

		if self.eventdetection == 'native':
			
			# print warning, since EyeTribe does not have a blink detection
			# built into their API
			
			print("WARNING! 'native' event detection has been selected, \
				but EyeTribe does not offer blink detection; PyGaze algorithm \
				will be used")

		# # # # #
		# PyGaze method
		
		blinking = False
		
		# loop until there is a blink
		while not blinking:
			# get newest sample
			gazepos = self.sample()
			# check if it's a valid sample
			if not self.is_valid_sample(gazepos):
				# get timestamp for possible blink start
				t0 = clock.get_time()
				# loop until a blink is determined, or a valid sample occurs
				while not self.is_valid_sample(self.sample()):
					# check if time has surpassed BLINKTHRESH
					if clock.get_time()-t0 >= self.blinkthresh:
						# return timestamp of blink start
						return t0
		

	def wait_for_fixation_end(self):

		"""Returns time and gaze position when a fixation has ended;
		function assumes that a 'fixation' has ended when a deviation of
		more than self.pxfixtresh from the initial fixation position has
		been detected (self.pxfixtresh is created in self.calibration,
		based on self.fixtresh, a property defined in self.__init__)
		
		arguments
		None
		
		returns
		time, gazepos	-- time is the starting time in milliseconds (from
					   expstart), gazepos is a (x,y) gaze position
					   tuple of the position from which the fixation
					   was initiated
		"""

		# # # # #
		# EyeTribe method

		if self.eventdetection == 'native':
			
			# print warning, since EyeTribe does not have a blink detection
			# built into their API
			
			print("WARNING! 'native' event detection has been selected, \
				but EyeTribe does not offer fixation detection; \
				PyGaze algorithm will be used")

		# # # # #
		# PyGaze method
			
		# function assumes that a 'fixation' has ended when a deviation of more than fixtresh
		# from the initial 'fixation' position has been detected
		
		# get starting time and position
		stime, spos = self.wait_for_fixation_start()
		
		# loop until fixation has ended
		while True:
			# get new sample
			npos = self.sample() # get newest sample
			# check if sample is valid
			if self.is_valid_sample(npos):
				# check if sample deviates to much from starting position
				if (npos[0]-spos[0])**2 + (npos[1]-spos[1])**2 > self.pxfixtresh**2: # Pythagoras
					# break loop if deviation is too high
					break

		return clock.get_time(), spos


	def wait_for_fixation_start(self):

		"""Returns starting time and position when a fixation is started;
		function assumes a 'fixation' has started when gaze position
		remains reasonably stable (i.e. when most deviant samples are
		within self.pxfixtresh) for five samples in a row (self.pxfixtresh
		is created in self.calibration, based on self.fixtresh, a property
		defined in self.__init__)
		
		arguments
		None
		
		returns
		time, gazepos	-- time is the starting time in milliseconds (from
					   expstart), gazepos is a (x,y) gaze position
					   tuple of the position from which the fixation
					   was initiated
		"""
		
		# # # # #
		# EyeTribe method

		if self.eventdetection == 'native':
			
			# print warning, since EyeTribe does not have a fixation start
			# detection built into their API (only ending)
			
			print("WARNING! 'native' event detection has been selected, \
				but EyeTribe does not offer fixation detection; \
				PyGaze algorithm will be used")
			
			
		# # # # #
		# PyGaze method
		
		# function assumes a 'fixation' has started when gaze position
		# remains reasonably stable for self.fixtimetresh
		
		# get starting position
		spos = self.sample()
		while not self.is_valid_sample(spos):
			spos = self.sample()
		
		# get starting time
		t0 = clock.get_time()

		# wait for reasonably stable position
		moving = True
		while moving:
			# get new sample
			npos = self.sample()
			# check if sample is valid
			if self.is_valid_sample(npos):
				# check if new sample is too far from starting position
				if (npos[0]-spos[0])**2 + (npos[1]-spos[1])**2 > self.pxfixtresh**2: # Pythagoras
					# if not, reset starting position and time
					spos = copy.copy(npos)
					t0 = clock.get_time()
				# if new sample is close to starting sample
				else:
					# get timestamp
					t1 = clock.get_time()
					# check if fixation time threshold has been surpassed
					if t1 - t0 >= self.fixtimetresh:
						# return time and starting position
						return t1, spos


	def wait_for_saccade_end(self):

		"""Returns ending time, starting and end position when a saccade is
		ended; based on Dalmaijer et al. (2013) online saccade detection
		algorithm
		
		arguments
		None
		
		returns
		endtime, startpos, endpos	-- endtime in milliseconds (from 
							   expbegintime); startpos and endpos
							   are (x,y) gaze position tuples
		"""

		# # # # #
		# EyeTribe method

		if self.eventdetection == 'native':
			
			# print warning, since EyeTribe does not have a blink detection
			# built into their API
			
			print("WARNING! 'native' event detection has been selected, \
				but EyeTribe does not offer saccade detection; PyGaze \
				algorithm will be used")

		# # # # #
		# PyGaze method
		
		# get starting position (no blinks)
		t0, spos = self.wait_for_saccade_start()
		# get valid sample
		prevpos = self.sample()
		while not self.is_valid_sample(prevpos):
			prevpos = self.sample()
		# get starting time, intersample distance, and velocity
		t1 = clock.get_time()
		s = ((prevpos[0]-spos[0])**2 + (prevpos[1]-spos[1])**2)**0.5 # = intersample distance = speed in px/sample
		v0 = s / (t1-t0)

		# run until velocity and acceleration go below threshold
		saccadic = True
		while saccadic:
			# get new sample
			newpos = self.sample()
			t1 = clock.get_time()
			if self.is_valid_sample(newpos) and newpos != prevpos:
				# calculate distance
				s = ((newpos[0]-prevpos[0])**2 + (newpos[1]-prevpos[1])**2)**0.5 # = speed in pixels/sample
				# calculate velocity
				v1 = s / (t1-t0)
				# calculate acceleration
				a = (v1-v0) / (t1-t0) # acceleration in pixels/sample**2 (actually is v1-v0 / t1-t0; but t1-t0 = 1 sample)
				# check if velocity and acceleration are below threshold
				if v1 < self.pxspdtresh and (a > -1*self.pxacctresh and a < 0):
					saccadic = False
					epos = newpos[:]
					etime = clock.get_time()
				# update previous values
				t0 = copy.copy(t1)
				v0 = copy.copy(v1)
			# udate previous sample
			prevpos = newpos[:]

		return etime, spos, epos


	def wait_for_saccade_start(self):

		"""Returns starting time and starting position when a saccade is
		started; based on Dalmaijer et al. (2013) online saccade detection
		algorithm
		
		arguments
		None
		
		returns
		endtime, startpos	-- endtime in milliseconds (from expbegintime);
					   startpos is an (x,y) gaze position tuple
		"""

		# # # # #
		# EyeTribe method

		if self.eventdetection == 'native':
			
			# print warning, since EyeTribe does not have a blink detection
			# built into their API
			
			print("WARNING! 'native' event detection has been selected, \
				but EyeTribe does not offer saccade detection; PyGaze \
				algorithm will be used")

		# # # # #
		# PyGaze method
		
		# get starting position (no blinks)
		newpos = self.sample()
		while not self.is_valid_sample(newpos):
			newpos = self.sample()
		# get starting time, position, intersampledistance, and velocity
		t0 = clock.get_time()
		prevpos = newpos[:]
		s = 0
		v0 = 0

		# get samples
		saccadic = False
		while not saccadic:
			# get new sample
			newpos = self.sample()
			t1 = clock.get_time()
			if self.is_valid_sample(newpos) and newpos != prevpos:
				# check if distance is larger than precision error
				sx = newpos[0]-prevpos[0]; sy = newpos[1]-prevpos[1]
				if (sx/self.pxdsttresh[0])**2 + (sy/self.pxdsttresh[1])**2 > self.weightdist: # weigthed distance: (sx/tx)**2 + (sy/ty)**2 > 1 means movement larger than RMS noise
					# calculate distance
					s = ((sx)**2 + (sy)**2)**0.5 # intersampledistance = speed in pixels/ms
					# calculate velocity
					v1 = s / (t1-t0)
					# calculate acceleration
					a = (v1-v0) / (t1-t0) # acceleration in pixels/ms**2
					# check if either velocity or acceleration are above threshold values
					if v1 > self.pxspdtresh or a > self.pxacctresh:
						saccadic = True
						spos = prevpos[:]
						stime = clock.get_time()
					# update previous values
					t0 = copy.copy(t1)
					v0 = copy.copy(v1)

				# udate previous sample
				prevpos = newpos[:]

		return stime, spos
	
	
	def is_valid_sample(self, gazepos):
		
		"""Checks if the sample provided is valid, based on EyeTribe specific
		criteria (for internal use)
		
		arguments
		gazepos		--	a (x,y) gaze position tuple, as returned by
						self.sample()
		
		returns
		valid		--	a Boolean: True on a valid sample, False on
						an invalid sample
		"""
		
		# return False if a sample is invalid
		if gazepos == (None,None) or gazepos == (-1,-1):
			return False
		
		# in any other case, the sample is valid
		return True