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