eyetracking with eyelink in psychtoolbox, now with oop

I’ve started trying out MATLAB’s OOP after mounting suspicion that the way I’d been coding experiments basically involved making something that looked and behaved like an object–but did so in a convoluted and inefficient way. See this post on eyetracking with PTB as proof.

This post is brief, and is about as well thought out as a github gist/gitlab snippet.

The two classes I’ll work with here is a Window class and a Tracker class. The window class has 3 methods. The first constructor method exists just to create the object. The constructed object will have a few default properties of the class. The second method is open, which (can you guess?) calls the PTB functions to open an onscreen window. The open method is fancier than it needs to be for this post (note the PsychImaging configuration, and the optional debugLevel flag). The final window method is the desctructor. The destructor method is one of the advantages of leaning on MATLAB’s OOP syntax. That method will get called whenever the Window object’s lifecycle has ended (which might happen from explicit deletion of the object, closing MATLAB, the object is no longer referenced in the call stack, etc).

The second class is the Tracker class, which interfaces with Eyelink. The Window class is only present here because Eyelink needs an open window to run calibration. There are five Tracker methods, but they are either analogous to the Window objects methods (constructor, destructor) or were largely presented in the previous post.

Window Object


classdef Window < handle
    % Window handles opening and closing of screen
    
    properties (Constant)
        screenNumber = 0
        
        % background color of screen
        background = GrayIndex(Window.screenNumber)
    end
    
    properties
        pointer
        winRect
    end
    
    methods
        function obj = Window()
        end
        
        function open(obj, skipsynctests, debuglevel)
            
            PsychImaging('PrepareConfiguration');
            PsychImaging('AddTask', 'General', 'FloatingPoint16Bit');
            
            Screen('Preference', 'SkipSyncTests', skipsynctests);
            switch debuglevel
                % no debug. run as usual, without listening to keyboard input
                % and also hiding the cursor
                case 0
                    ListenChar(-1);
                    HideCursor;
                    [obj.pointer, obj.winRect] = ...
                        PsychImaging('OpenWindow', obj.screenNumber, obj.background);
                % light debug: still open fullscreen window, but keep keyboard input
                case 1
                    [obj.pointer, obj.winRect] = ...
                        PsychImaging('OpenWindow', obj.screenNumber, obj.background);
                % full debug: only open transparent window
                case 10
                    PsychDebugWindowConfiguration(0, .5)
                    [obj.pointer, obj.winRect] = ...
                        PsychImaging('OpenWindow', obj.screenNumber, obj.background);
            end
                        
            % Turn on blendfunction for antialiasing of drawing dots
            Screen('BlendFunction', obj.pointer, 'GL_SRC_ALPHA', 'GL_ONE_MINUS_SRC_ALPHA');
            
            topPriorityLevel = MaxPriority(obj.pointer);
            Priority(topPriorityLevel);
        end
        
        % will auto-close open windows and return keyboard control when
        % this object is deleted
        function delete(obj) %#ok<INUSD>
            ListenChar(0);
            Priority(0);
            sca;
        end
    end
    
end

Tracker Object

The tracker object will mostly do what it did in the previous post. Same functionality, but the syntax is much cleaner than the heavy use of switch/case conditionals.

classdef Tracker < handle
    
    properties
        % flag to be called in scripts which enable turning on or off the tracker
        % in an experiment (e.g., when debug mode is on)
        using_tracker logical = false
        
        % name of the write. must follow eyelink conventions. alphanumeric only, no 
        % more than 8 characters
        filename char = ''
        
        % eyelink object structure. stores many relevant parameters
        el
    end
    
    methods
        function obj = Tracker(using_tracker, filename, window)            
            obj.using_tracker = using_tracker;
            obj.filename = filename;
            
            % run calibration for tracker (see method below)
            calibrate(obj, window);
        end
        
        function calibrate(obj, window)
            if obj.using_tracker
                % Provide Eyelink with details about the graphics environment
                % and perform some initializations. The information is returned
                % in a structure that also contains useful defaults
                % and control codes (e.g. tracker state bit and Eyelink key values).
                obj.el = EyelinkInitDefaults(window.pointer);
                
                if ~EyelinkInit(0, 1)
                    error('\n Eyelink Init aborted \n');
                end
                
                %Reduce FOV for calibration and validation. Helpful when the
                % the stimulus is only in the center of the screen, or at places
                % like the fMRI scanner at UMass where the eyes have a lot in front
                % of them
                Eyelink('Command','calibration_area_proportion = 0.5 0.5');
                Eyelink('Command','validation_area_proportion = 0.5 0.5');
                
                % open file to record data to
                status = Eyelink('Openfile', obj.filename);
                if status ~= 0
                    error('\n Eyelink Init aborted \n');
                end
                
                % Setting the proper recording resolution, proper calibration type,
                % as well as the data file content;
                Eyelink('Command','screen_pixel_coords = %ld %ld %ld %ld', 0, 0, window.winRect(3)-1, window.winRect(4)-1);
                Eyelink('message', 'DISPLAY_COORDS %ld %ld %ld %ld', 0, 0, window.winRect(3)-1, window.winRect(4)-1);
                % set calibration type to 5 point.
                Eyelink('Command', 'calibration_type = HV5');
                
                % set EDF file contents using the file_sample_data and
                % file-event_filter commands
                % set link data thtough link_sample_data and link_event_filter
                Eyelink('Command', 'file_event_filter = RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON,INPUT');
                Eyelink('Command', 'link_event_filter = RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON,INPUT');
                
                % check the software version
                % add "HTARGET" to record possible target data for EyeLink Remote
                Eyelink('command', 'file_sample_data  = RIGHT,GAZE,HREF,GAZERES,AREA,HTARGET,STATUS,INPUT');
                Eyelink('command', 'link_sample_data  = RIGHT,GAZE,HREF,GAZERES,AREA,HTARGET,STATUS,INPUT');
                
                % make sure we're still connected.
                if Eyelink('IsConnected')~=1
                    error('\n Eyelink Init aborted \n');
                end
                
                % set sample rate in camera setup screen
                Eyelink('Command', 'sample_rate = %d', 1000);
                
                % opens up main calibration scheme
                EyelinkDoTrackerSetup(obj.el);
            end
        end
        
        function status = eyelink(obj, varargin)
            % calls main Eyelink routines only when
            % this tracker object property using_tracker==true.
            status = [];
            if obj.using_tracker
                if nargin==2
                    % construct calls to eyelink that don't output any
                    % status
                    if strcmp(varargin{1}, 'StopRecording') || ...
                            strcmp(varargin{1}, 'Shutdown') ||...
                            strcmp(varargin{1}, 'SetOfflineMode')
                            
                        % magic happens here, where the variable argument input
                        % is expanded an repassed through to Eyelink()
                        Eyelink(varargin{:});
                    else
                        status = Eyelink(varargin{:});
                    end
                % all calls to Eyelink that have more than two inputs (e.g., the
                % name of a function with some parameters to that function) return
                % some status
                else
                    status = Eyelink(varargin{:});
                end
            end
        end
        
        % starts up the eyelink machine. call this once the start of each
        % experiment. could modify function to also draw something special
        % to the screen (e.g., a background image). this might be the kind
        % of function to modify if you wanted to draw trial-by-trial material
        % to the eyetracking computer
        function startup(obj)
            
            % Must be offline to draw to EyeLink screen
            obj.eyelink('SetOfflineMode');
            
            % clear tracker display and draw background img to host pc
            obj.eyelink('Command', 'clear_screen 0');
            
            % draw simple fixation cross as later reference
            obj.eyelink('command', 'draw_cross %d %d', 1920/2, 1080/2);
            
            % give image transfer time to finish
            WaitSecs(0.1);
        end
        
        % destructor function will get called whenever tracker object is deleted (e.g.,
        % this function is automatically called when MATLAB closes, meaning you can't
        % forget to close the file connection with the tracker computer).
        function delete(obj)
          % waitsecs occur because the filetransfer often takes a moment, and moving
          % on too quickly will result in an error
            
            % End of Experiment; close the file first
            % close graphics window, close data file and shut down tracker
            obj.eyelink('StopRecording');
            WaitSecs(0.1); % Slack to let stop definitely happen
            obj.eyelink('SetOfflineMode'); 
            
            obj.eyelink('CloseFile');
            WaitSecs(0.1);
            
            obj.eyelink('ReceiveFile', obj.filename, fullfile(pwd,'events'), 1);
            WaitSecs(0.2);
            obj.eyelink('Shutdown');
        end
        
    end
end

Run the calibration (and use the tracker)

Putting this together, the following script starts calibration, and outlines how this tracker could be used in an experiment.


%% input 
% ------------------

skipsynctests = 2;
debuglevel = 0;
using_tracker = true;

%% setup
% ------------------
  
% boilerplate setup
PsychDefaultSetup(2);

% initialize window
window = Window();

% open that window
open(window, skipsynctests, debuglevel)

% Initialize tracker object
tracker = Tracker(using_tracker, 'OOPDEMO.edf', window);

% run calibration
tracker.startup();

% Let Eyelink know that the experiment starts now
tracker.eyelink('message', 'SYNCTIME');

%% Experiment/trial code
% ------------------

% note that we should not need to wait to start recording,
% given that the stimulus will always be drawn a bit later
% (determined by how often phase changes occur)
tracker.eyelink('StartRecording');

% trial/experiment happens here ...

tracker.eyelink('StopRecording');
% Wait moment to ensure that tracker is definitely finished with the last few samples
WaitSecs(0.001);

%% Cleanup
% ------------------

% closes connection to Eyelink system, saves file
delete(tracker);

% closes window, restores keyboard input
delete(window);

What’s nice about this syntax (as before) is that only very minimal changes are required you don’t want to call the Eyelink functions (e.g., if you’re testing on a computer that doesn’t have the Eyelink system connected, or you’re debugging other parts of the experiment). By changing just the input, the Eyelink functions won’t be called.


using_tracker = false;

% all the rest as above
% ...

Summary

There’s not much to summarize because I haven’t explained much! Again, this post is largely just an attempt to revise what I now think is a poor implementation, presented in an earlier post.

View Page Source

Patrick Sadil
Patrick Sadil
Research Associate; Biostatistics Faculty