Skip to content

Commit

Permalink
Move from wm_utilities
Browse files Browse the repository at this point in the history
wanjam committed Sep 21, 2020

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent ab0100a commit 7cdf50e
Showing 12 changed files with 2,030 additions and 1 deletion.
328 changes: 328 additions & 0 deletions EyeLinkCoreGraphicsPsychoPy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
# Copyright (C) 2016 Zhiguo Wang
# Copyright (C) 2017 SR Research
# Distributed under the terms of the GNU General Public License (GPL).

from psychopy import visual, monitors, event, core, sound
from numpy import linspace
from math import sin, cos, pi
from PIL import Image
import array, string, pylink, psychopy

class EyeLinkCoreGraphicsPsychoPy(pylink.EyeLinkCustomDisplay):
def __init__(self, tracker, win):#
'''Initialize a Custom EyeLinkCoreGraphics
tracker: an eye-tracker instance
win: the Psychopy display we plan to use for stimulus presentation '''

pylink.EyeLinkCustomDisplay.__init__(self)
self.display = win
# Let's disable the beeps as the Psychopy "sound" module will bite our ass
#self.__target_beep__ = sound.Sound('type.wav')
#self.__target_beep__done__ = sound.Sound('qbeep.wav')
#self.__target_beep__error__ = sound.Sound('error.wav')
self.imgBuffInitType = 'I'
self.imagebuffer = array.array(self.imgBuffInitType)
self.pal = None
self.size = (384,320)
self.bg_color = win.color
self.sizeX = win.size[0]
self.sizeY = win.size[1]

# check the screen units of Psychopy and make all necessary conversions for the drawing functions
self.units = win.units
self.monWidthCm = win.monitor.getWidth()
self.monViewDist = win.monitor.getDistance()
self.monSizePix = win.monitor.getSizePix()

# a scaling factor to make the screen units right for Psychopy
self.cfX = 1.0
self.cfY = 1.0
if self.units == 'pix':
pass
elif self.units == 'height':
self.cfX = 1.0/self.monSizePix[1]
self.cfY = 1.0/self.monSizePix[1]
elif self.units == 'norm':
self.cfX = 2.0/self.monSizePix[0]
self.cfY = 2.0/self.monSizePix[1]
elif self.units == 'cm':
self.cfX = self.monWidthCm*1.0/self.monSizePix[0]
self.cfY = self.cfX
else: # here comes the 'deg*' units
self.cfX = self.monWidthCm/self.monViewDist/pi*180.0/self.monSizePix[0]
self.cfY = self.cfX

# initial setup for the mouse
self.display.mouseVisible = False
self.mouse = event.Mouse(visible=False)
self.mouse.setPos([0,0]) # make the mouse appear at the center of the camera image
self.last_mouse_state = -1

# image title
self.msgHeight = self.size[1]/20.0*self.cfY
self.title = visual.TextStim(self.display,'', height=self.msgHeight, color=[1,1,1])

# lines
self.line = visual.Line(self.display, start=(0, 0), end=(0,0),
lineWidth=2.0*self.cfX, lineColor=[0,0,0])

def setTracker(self, tracker):
''' set proper tracker parameters '''

self.tracker = tracker
self.tracker_version = tracker.getTrackerVersion()
if self.tracker_version >=3:
self.tracker.sendCommand("enable_search_limits=YES")
self.tracker.sendCommand("track_search_limits=YES")
self.tracker.sendCommand("autothreshold_click=YES")
self.tracker.sendCommand("autothreshold_repeat=YES")
self.tracker.sendCommand("enable_camera_position_detect=YES")

def setup_cal_display(self):
'''Set up the calibration display before entering the calibration/validation routine'''

self.display.color = self.bg_color
self.title.autoDraw = False
self.display.flip()

def clear_cal_display(self):
'''Clear the calibration display'''

self.display.color = self.bg_color

def exit_cal_display(self):
'''Exit the calibration/validation routine'''

self.clear_cal_display()

def record_abort_hide(self):
'''This function is called if aborted'''

pass

def erase_cal_target(self):
'''Erase the calibration/validation & drift-check target'''

self.display.color = self.bg_color
self.display.flip()

def draw_cal_target(self, x, y):#
'''Draw the calibration/validation & drift-check target'''

xVis = (x - self.sizeX/2)*self.cfX
yVis = (self.sizeY/2 - y)*self.cfY
cal_target_out = visual.GratingStim(self.display, tex='none', mask='circle', size=2.0/100*self.sizeX*self.cfX, color=[1.0,1.0,1.0])
cal_target_in = visual.GratingStim(self.display, tex='none', mask='circle', size=2.0/300*self.sizeX*self.cfX, color=[-1.0,-1.0,-1.0])
cal_target_out.setPos((xVis, yVis))
cal_target_in.setPos((xVis, yVis))
cal_target_out.draw()
cal_target_in.draw()
self.display.flip()


def play_beep(self, beepid):
''' Play a sound during calibration/drift correct.'''

pass
# we need to disable the beeps to make this library work on all platforms
#if beepid == pylink.CAL_TARG_BEEP or beepid == pylink.DC_TARG_BEEP:
# self.__target_beep__.play()
#if beepid == pylink.CAL_ERR_BEEP or beepid == pylink.DC_ERR_BEEP:
# self.__target_beep__error__.play()
#if beepid in [pylink.CAL_GOOD_BEEP, pylink.DC_GOOD_BEEP]:
# self.__target_beep__done__.play()

def getColorFromIndex(self, colorindex):
'''Return psychopy colors for elements in the camera image'''

if colorindex == pylink.CR_HAIR_COLOR: return (1, 1, 1)
elif colorindex == pylink.PUPIL_HAIR_COLOR: return (1, 1, 1)
elif colorindex == pylink.PUPIL_BOX_COLOR: return (-1, 1, -1)
elif colorindex == pylink.SEARCH_LIMIT_BOX_COLOR: return (1, -1, -1)
elif colorindex == pylink.MOUSE_CURSOR_COLOR: return (1, -1, -1)
else: return (0,0,0)

def draw_line(self, x1, y1, x2, y2, colorindex):
'''Draw a line. This is used for drawing crosshairs/squares'''

y1 = (y1 * -1 + self.size[1]/2)*self.cfY
x1 = (x1 * 1 - self.size[0]/2)*self.cfX
y2 = (y2 * -1 + self.size[1]/2)*self.cfY
x2 = (x2 * 1 - self.size[0]/2)*self.cfX

color = self.getColorFromIndex(colorindex)
self.line.start = (x1, y1)
self.line.end = (x2, y2)
self.line.lineColor = color
self.line.draw()

def draw_lozenge(self, x, y, width, height, colorindex):
''' draw a lozenge to show the defined search limits'''

y = (y * -1 + self.size[1] - self.size[1]/2)*self.cfY
x = (x * 1 - self.size[0]/2)*self.cfX
width = width*self.cfX; height = height*self.cfY
color = self.getColorFromIndex(colorindex)

if width > height:
rad = height / 2
if rad == 0:
return #cannot draw the circle with 0 radius
#draw the lines
line1 = visual.Line(self.display, lineColor=color, lineWidth=2.0*self.cfX, start=(x + rad, y), end=(x + width - rad, y))
line2 = visual.Line(self.display, lineColor=color, lineWidth=2.0*self.cfX, start=(x + rad, y - height), end=(x + width - rad, y - height))

#draw semicircles
Xs1 = [rad*cos(t) + x + rad for t in linspace(pi/2, pi/2+pi, 72)]
Ys1 = [rad*sin(t) + y - rad for t in linspace(pi/2, pi/2+pi, 72)]

Xs2 = [rad*cos(t) + x - rad + width for t in linspace(pi/2+pi, pi/2+2*pi, 72)]
Ys2 = [rad*sin(t) + y - rad for t in linspace(pi/2+pi, pi/2+2*pi, 72)]
lozenge1 = visual.ShapeStim(self.display, vertices = zip(Xs1, Ys1), lineWidth=2.0*self.cfX, lineColor=color, closeShape=False)
lozenge2 = visual.ShapeStim(self.display, vertices = zip(Xs2, Ys2), lineWidth=2.0*self.cfX, lineColor=color, closeShape=False)
else:
rad = width / 2

#draw the lines
line1 = visual.Line(self.display, lineColor=color, lineWidth=2.0*self.cfX, start=(x, y - rad), end=(x, y - height + rad))
line2 = visual.Line(self.display, lineColor=color, lineWidth=2.0*self.cfX, start=(x + width, y - rad), end=(x + width, y - height + rad))

#draw semicircles
if rad == 0:
return #cannot draw sthe circle with 0 radius

Xs1 = [rad*cos(t) + x + rad for t in linspace(0, pi, 72)]
Ys1 = [rad*sin(t) + y - rad for t in linspace(0, pi, 72)]

Xs2 = [rad*cos(t) + x + rad for t in linspace(pi, 2*pi, 72)]
Ys2 = [rad*sin(t) + y + rad - height for t in linspace(pi, 2*pi, 72)]

lozenge1 = visual.ShapeStim(self.display, vertices = zip(Xs1, Ys1),lineWidth=2.0*self.cfX, lineColor=color, closeShape=False)
lozenge2 = visual.ShapeStim(self.display, vertices = zip(Xs2, Ys2),lineWidth=2.0*self.cfX, lineColor=color, closeShape=False)
lozenge1.draw()
lozenge2.draw()
line1.draw()
line2.draw()

def get_mouse_state(self):#
'''Get the current mouse position and status'''

X, Y = self.mouse.getPos()
mX = self.size[0]/2 + X*1.0/self.cfX
mY = self.size[1]/2 - Y*1.0/self.cfY
if mX <=0: mX = 0
if mX > self.size[0]: mX = self.size[0]
if mY < 0: mY = 0
if mY > self.size[1]: mY = self.size[1]

state = self.mouse.getPressed()[0]
return ((mX, mY), state)


def get_input_key(self):
''' this function will be constantly pools, update the stimuli here is you need
dynamic calibration target '''

ky=[]
for keycode, modifier in event.getKeys(modifiers=True):
k= pylink.JUNK_KEY
if keycode == 'f1': k = pylink.F1_KEY
elif keycode == 'f2': k = pylink.F2_KEY
elif keycode == 'f3': k = pylink.F3_KEY
elif keycode == 'f4': k = pylink.F4_KEY
elif keycode == 'f5': k = pylink.F5_KEY
elif keycode == 'f6': k = pylink.F6_KEY
elif keycode == 'f7': k = pylink.F7_KEY
elif keycode == 'f8': k = pylink.F8_KEY
elif keycode == 'f9': k = pylink.F9_KEY
elif keycode == 'f10': k = pylink.F10_KEY
elif keycode == 'pageup': k = pylink.PAGE_UP
elif keycode == 'pagedown': k = pylink.PAGE_DOWN
elif keycode == 'up': k = pylink.CURS_UP
elif keycode == 'down': k = pylink.CURS_DOWN
elif keycode == 'left': k = pylink.CURS_LEFT
elif keycode == 'right': k = pylink.CURS_RIGHT
elif keycode == 'backspace': k = ord('\b')
elif keycode == 'return': k = pylink.ENTER_KEY
elif keycode == 'space': k = ord(' ')
elif keycode == 'escape': k = pylink.ESC_KEY
elif keycode == 'tab': k = ord('\t')
elif keycode in string.ascii_letters: k = ord(keycode)
elif k== pylink.JUNK_KEY: key = 0

if modifier['alt']==True: mod = 256
else: mod = 0

ky.append(pylink.KeyInput(k, mod))
#event.clearEvents()
return ky

def exit_image_display(self):
'''Clcear the camera image'''

self.clear_cal_display()
self.display.flip()

def alert_printf(self,msg):
'''Print error messages.'''

print "Error: " + msg

def setup_image_display(self, width, height):
''' set up the camera image, for newer APIs, the size is 384 x 320 pixels'''

self.title.autoDraw = True
self.last_mouse_state = -1
self.size = (width, height)

def image_title(self, text):
'''Draw title text below the camera image'''

self.title.text = text
title_pos = (0, 0-self.size[0]/2.0*self.cfY-self.msgHeight)
self.title.pos = title_pos


def draw_image_line(self, width, line, totlines, buff):#
'''Display image pixel by pixel, line by line'''
#self.size = (width, totlines)
i =0
while i <width:
self.imagebuffer.append(self.pal[buff[i]])
i= i+1

if line == totlines:
bufferv = self.imagebuffer.tostring()
try:
img = Image.frombytes("RGBX", (width, totlines), bufferv) # Pillow
except:
img = Image.fromstring("RGBX", (width, totlines), bufferv) # PIL

imgResize = img.resize((self.size[0], self.size[1]))
imgResizeVisual = visual.ImageStim(self.display, image=imgResize)

imgResizeVisual.draw()
self.draw_cross_hair()
self.display.flip()

self.imagebuffer = array.array(self.imgBuffInitType)

def set_image_palette(self, r,g,b):
'''Given a set of RGB colors, create a list of 24bit numbers representing the pallet.
I.e., RGB of (1,64,127) would be saved as 82047, or the number 00000001 01000000 011111111'''

self.imagebuffer = array.array(self.imgBuffInitType)
#self.clear_cal_display()
sz = len(r)
i =0
self.pal = []
while i < sz:
rf = int(b[i])
gf = int(g[i])
bf = int(r[i])
self.pal.append((rf<<16) | (gf<<8) | (bf))
i = i+1



43 changes: 43 additions & 0 deletions EyelinkAvoidWrongTriggers.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
function [] = EyelinkAvoidWrongTriggers
% EYELINKAVOIDWRONGTRIGGERS throws an error if Eyelink is connected but not
% configured.
%
% Since we use a Y-cable, that is supposed to send the TTL-triggers from
% the PTB-PC to the EEG and he Eyetracker host, there is a permanent
% connection between the three parallelports (1. EEG, 2.PTB, 3.Eyelink).
% Therefore, all parallelports need to be set low, except for the one
% putting the triggers (i.e. PTB). The Eyelink host-pc sends a random
% trigger at startup if not configured properly. So if an experiment that
% doesn't use the Eyetracker sends a trigger to the EEG while the
% Eyetracker host-pc is turned on, that causes a wrong trigger in the EEG
% signal. EYELINKAVOIDWRONGTRIGGERS can be placed at the beginning of an
% experiment that doesn't use the eyelink to throw an error if the eyelink
% is still connected.
%
% Wanja Moessing Oct 12, 2016

% Copyright (C) 2016 Wanja Mössing
%
% This program is free software: you can redistribute it and/or modify
% it under the terms of the GNU General Public License as published by
% the Free Software Foundation, either version 3 of the License, or
% (at your option) any later version.
%
% This program is distributed in the hope that it will be useful,
% but WITHOUT ANY WARRANTY; without even the implied warranty of
% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
% GNU General Public License for more details.
%
% You should have received a copy of the GNU General Public License
% along with this program. If not, see <http://www.gnu.org/licenses/>.

fprintf(2,'Pinging Eyelink to check if connected...\n');
[notConnected, Info] = dos(['ping 100.1.1.1 -n 1']);

if Eyelink('IsConnected') || ~notConnected
error(['Eyelink host PC is turned on. Without running the appropriate startup routines '...
'(e.g., `EyelinkStart.m` from Wanja`s Github repo), this will cause faulty triggervalues'...
' in your EEG signal, because the parallel-port of the Eyelink-PC is set to a random'...
' value at startup instead of reading the port. Either turn off the Eyelink-PC or configure it!'])
end
end
Loading

0 comments on commit 7cdf50e

Please sign in to comment.