Skip to main content
Skip table of contents

A simple TRACKPixx3 calibration

This demo shows how to perform a simple 13-point calibration of the TRACKPixx3 using Python.

First, the camera view is shown so that the position of the participant & camera may be adjusted as needed.

Hitting ‘Enter’ on the keyboard will then trigger the 13 point calibration. Points are shown in a sequence and gaze data is collected.

Finally, a simple gaze follower is used to verify the participant’s gaze is being tracked well.

This simple calibration is ideal for running in between test blocks, or where a simple re-calibration of the eyes is needed. It may be modified to set threshold criterion for a successful calibration, according to the experiment protocol.

PY
import pypixxlib._libdpx as dp
from psychopy import visual, core
from psychopy.hardware import keyboard
import numpy as np
import PIL

def TPxSimpleCalibration():

    calibrationSuccess = False # Return whether the camera is calibrated
    screenNumber = 1 # Screen number of monitor to be used for experiment. Minus one from the number assigned by the OS (e.g., 1 is actually screen 2).
    ledIntensity = 8 # Intensity of infrared illuminator
    approximateIrisSize = 140 # There is no rule of thumb for this value. Set this in PyPixx to be sure.

    # Set up the hardware
    dp.DPxOpen()
    dp.TPxHideOverlay()
    dp.TPxClearDeviceCalibration()
    dp.DPxSetTPxAwake()
    dp.TPxSetLEDIntensity(ledIntensity)
    dp.TPxSetIrisExpectedSize(approximateIrisSize)
    dp.DPxWriteRegCache()

    # Set up PsychoPy
    windowPtr = visual.Window(fullscr=True,color=-1,screen=screenNumber) # Open a black PsychoPy window
    windowRect = windowPtr.size # Get window dimensions
    kb = keyboard.Keyboard() # Record key strokes with PsychoPy

    t = dp.DPxGetTime() # Time stamps
    t2 = dp.DPxGetTime()

    # Create text stimulus
    textStim = visual.TextStim(windowPtr,text='Instructions:\n\n 1- Focus the eyes.'+
                            '\n\n 2- Press Enter when ready to calibrate '+
                            'or Escape to exit.',anchorHoriz='center',anchorVert='bottom',units='pix')
    textStim.size = 24
    textStim.pos = (0,-windowRect[1]/2)

    ######################### SCREEN 1: Start screen ##########################
    while True:
        if ((t2 - t) > 1/60): # Just refresh at 60 Hz
            # Get static image of eye from TPx. Draw to screen.
            dp.DPxUpdateRegCache()
            imageStim = visual.SimpleImageStim(windowPtr,image=PIL.Image.fromarray(dp.TPxGetEyeImage()),units='pix',pos=(0,0))
            drawCollection( [imageStim, textStim] ) # See below for definition
            windowPtr.flip()
            t = t2
        else:
            dp.DPxUpdateRegCache()
            t2 = dp.DPxGetTime() # Get most recent time stamp

        # Check for key presses
        keys = kb.getKeys(['escape', 'return'], waitRelease=True)
        if 'escape' in keys:
            escape(windowPtr)
            return False
        elif 'return' in keys:
            break

    ###################### SCREEN 2: Calibration routine ######################
    cx = windowRect[0]/2 # Screen center x coordinate
    cy = windowRect[1]/2 # Screen center y coordinate
    windowRect[1]/windowRect[0] # Aspect ratio
    dx = 600 # How big of a range to cover in X (center +/- 600 pixels)
    dy = windowRect[1]/windowRect[0]*dx # How big of a range to cover in Y  (same as x, scaled by AR)

    # Define (x,y) target positions for a 13-point calibration grid
    xy = np.array(
         [  [cx, cy],
            [cx, cy+dy],
            [cx+dx, cy],
            [cx, cy-dy],
            [cx-dx, cy],
            [cx+dx, cy+dy],
            [cx-dx, cy+dy],
            [cx+dx, cy-dy],
            [cx-dx, cy-dy],
            [cx+dx/2, cy+dy/2],
            [cx-dx/2, cy+dy/2],
            [cx-dx/2, cy-dy/2],
            [cx+dx/2, cy-dy/2] ])
    xyCartesian = np.array( dp.TPxConvertCoordSysToCartesian(xy, offsetX=-cx, offsetY=-cy) )
    npts = xy.size//2

    # Define calibration targets
    outerCircle = visual.Circle(windowPtr,units='pix',color=(0,1,0),colorSpace='rgb',radius=30)
    innerCircle = visual.Circle(windowPtr,units='pix',color=(1,0,0),colorSpace='rgb',radius=8)
    outerCircle.color = (0, 1, 0)
    innerCircle.color = (1, 0, 0)

    i = 0 # Calibration target iterator
    raw_vector = np.zeros( (npts,4) ) # Get the pupil-center-to-corneal-reflection-vector data
    showing_dot = 0 # Flag
    t = 0
    t2 = 0
    while i < npts:
        # Present current dot. Calibrate .95 seconds after dot appears. Display dot for an additional 1.05 seconds. Repeat with next dot.
        if (t2 - t) > 2: # Calibration targets each presented for 2 sec total
            Sx = xyCartesian[i,0] # Get screen coordinates of current calibration target
            Sy = xyCartesian[i,1]
            outerCircle.pos = innerCircle.pos = (Sx,Sy) # Update the position of the calibration targets (dots)
            drawCollection( [outerCircle,innerCircle] ) # Draw dots
            windowPtr.flip()
            t = t2
            showing_dot = 1
        else:
            dp.DPxUpdateRegCache() # Get most recent time stamp
            t2 = dp.DPxGetTime()

        if showing_dot and (t2 - t) > 0.95:
            print('calibrating point %d...' % (i+1) )
            raw_vector[i,:] = dp.TPxGetEyePositionDuringCalib_returnsRaw(Sx, Sy, 3) # Get raw values from TPx
            print('done!\n')
            i += 1 # Next point
            showing_dot = 0

    ##### Check for calibration parameters in hardware
    dp.TPxBestPolyFinishCalibration() # Compute affine tranformations
    dp.DPxUpdateRegCache()
    if dp.TPxIsDeviceCalibrated(): # Flag any issues
        calibrationSuccess = True
    else:
        escape(windowPtr)
        raise("Could not successfully finish calibration process... exiting now")
    core.wait(2)
    #####


    ###### SCREEN 3: Free viewing of calibration grid with gaze follower ######
    textStim = visual.TextStim(windowPtr,text='Following your gaze now!' + 
                               '\n\nPress Enter to accept calibration '+
                               'or Escape to clear it.', anchorHoriz='center',anchorVert='bottom',units='pix')
    textStim.size = 24
    textStim.pos = (0,-windowRect[1]/2)

    targDots = []
    for i in range(xy.shape[0]):
        targDots.append( visual.Circle(windowPtr,units='pix',color=(1,1,1),colorSpace='rgb',radius=20) )
        targDots[i].color = (1,1,1) # these seem to get ignored in the constructor... i know not why psychopy does the things it does... or doesn't
        targDots[i].pos = (xyCartesian[i,0],xyCartesian[i,1])

    rightEye = visual.Circle(windowPtr,units='pix',color=(1,1,1),colorSpace='rgb',radius=15)
    leftEye = visual.Circle(windowPtr,units='pix',color=(1,1,1),colorSpace='rgb',radius=15)
    rightEye.color = (1,0,0)
    leftEye.color = (0,0,1)

    while True:
        dp.DPxUpdateRegCache()
        eyeData = dp.TPxGetEyePosition()

        leftEye.pos = tuple(eyeData[0:2])
        rightEye.pos = tuple(eyeData[2:4])
        
        drawCollection( [textStim,targDots,rightEye,leftEye] )
        windowPtr.flip()

        # Check for key presses
        keys = kb.getKeys(['escape', 'return'], waitRelease=True)
        if 'escape' in keys:
            dp.TPxClearDeviceCalibration()
            break
        elif 'return' in keys:            
            break

    escape(windowPtr)
    return calibrationSuccess

###############################################################################
############################### HELPER FUNCTIONS ##############################
###############################################################################


def escape(wp):
    wp.close()
    dp.TPxUninitialize()
    dp.DPxClose()
    core.quit()

def drawCollection(collection):
    """
    Draws all items contained within 'collection' onto the back buffer. Assumes every item in
    'collection' is an object of type psychopy.visual.*. If an element in 'collection' is itself
    a collection, then this function will just recursively draw that sub-collection also.

    Args:
        collection (list and/or tuple): some iteratable collection of psychopy.visual.* objects
    """
    for item in collection:
        if type(item) is list or type(item) is tuple:
            drawCollection(item)
        else:
            item.draw()

###############################################################################
if __name__ == '__main__':
    TPxSimpleCalibration()
JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.