commit
e87e4f79b8
5 changed files with 1837 additions and 0 deletions
@ -0,0 +1,366 @@ |
|||||||
|
# Copyright (C) 2018 Zhiguo Wang |
||||||
|
|
||||||
|
# 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 2 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, write to the Free Software Foundation, Inc., 51 |
||||||
|
# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||||
|
|
||||||
|
# Rev. March, 2018 |
||||||
|
# 1. added the print_function for Python 3 support |
||||||
|
# 2. use the list(zip(x,y)) function to make sure zip returns a list |
||||||
|
# 3. Added an image scaling factor to accommodate all versions of the API |
||||||
|
# 4. Added the missing CR adjustment keys (-/+=) |
||||||
|
# 5. revised the draw_image_line function |
||||||
|
# 6. tracker parameters set at initialization |
||||||
|
|
||||||
|
# Rev. July, 2018 |
||||||
|
# 1. Force drawing in absolute pixel coordinates, switch back to the units |
||||||
|
# set by the user at the end of the calibration routine. |
||||||
|
# 2. Updated the draw camera image function |
||||||
|
# 3. Updated the draw lozenge function |
||||||
|
# 4. camera image and drawing are all properly scaled |
||||||
|
|
||||||
|
# Rev. August 22, 2018 |
||||||
|
# 1. Misplacement of calibration targets on Macs |
||||||
|
# 2. Misalignment of crosshairs/squares and camera image |
||||||
|
|
||||||
|
# Rev. February 5, 2019 |
||||||
|
# 1. Misalighnment of crosshairs/squares on Mac |
||||||
|
|
||||||
|
# Rev. September 10, 2019 |
||||||
|
# 1. clear screen when entering camera setup mode |
||||||
|
# 2. tested with PsychoPy version 3.2.2 |
||||||
|
# 3. Reformatted the calibration instructions to improve code clarity |
||||||
|
|
||||||
|
from __future__ import print_function |
||||||
|
from psychopy import visual, event, core, sound |
||||||
|
from numpy import linspace |
||||||
|
from math import sin, cos, pi |
||||||
|
from PIL import Image |
||||||
|
import array, string, pylink, os |
||||||
|
|
||||||
|
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.pylinkMinorVer = pylink.__version__.split('.')[1] # minor version 1-Mac, 11-Win/Linux |
||||||
|
self.display = win |
||||||
|
self.w, self.h = win.size |
||||||
|
|
||||||
|
# on Macs with HiDPI screens, force the drawing routine to use the size defined |
||||||
|
# in the monitor instance, as win.size will give the wrong size of the screen |
||||||
|
if os.name == 'posix': |
||||||
|
self.w,self.h = win.monitor.getSizePix() |
||||||
|
|
||||||
|
#self.display.autoLog = False |
||||||
|
# check the screen units of Psychopy, forcing the screen to use 'pix' |
||||||
|
self.units = win.units |
||||||
|
if self.units != 'pix': self.display.setUnits('pix') |
||||||
|
|
||||||
|
# Simple warning beeps |
||||||
|
self.__target_beep__ = sound.Sound('A', octave=4, secs=0.1) |
||||||
|
self.__target_beep__done__ = sound.Sound('E', octave=4, secs=0.1) |
||||||
|
self.__target_beep__error__ = sound.Sound('E', octave=6, secs=0.1) |
||||||
|
|
||||||
|
self.imgBuffInitType = 'I' |
||||||
|
self.imagebuffer = array.array(self.imgBuffInitType) |
||||||
|
self.resizeImagebuffer = array.array(self.imgBuffInitType) |
||||||
|
self.pal = None |
||||||
|
self.bg_color = win.color |
||||||
|
self.img_scaling_factor = 3 |
||||||
|
self.size = (192*self.img_scaling_factor, 160*self.img_scaling_factor) |
||||||
|
|
||||||
|
# initial setup for the mouse |
||||||
|
self.display.mouseVisible = False |
||||||
|
self.mouse = event.Mouse(visible=False) |
||||||
|
self.last_mouse_state = -1 |
||||||
|
|
||||||
|
# image title & calibration instructions |
||||||
|
self.msgHeight = self.size[1]/20.0 |
||||||
|
self.title = visual.TextStim(self.display,'', height=self.msgHeight, color=[1,1,1], |
||||||
|
pos = (0,-self.size[1]/2-self.msgHeight), wrapWidth=self.w, units='pix') |
||||||
|
self.calibInst = visual.TextStim(self.display, alignHoriz='left',alignVert ='top', height=self.msgHeight, color=[1,1,1], |
||||||
|
pos = (-self.w/2.0, self.h/2.0), units='pix', |
||||||
|
text = 'Enter: Show/Hide camera image\n' + |
||||||
|
'Left/Right: Switch camera view\n' + |
||||||
|
'C: Calibration\n' + |
||||||
|
'V: Validation\n' + |
||||||
|
'O: Start Recording\n' + |
||||||
|
'+=/-: CR threshold\n' + |
||||||
|
'Up/Down: Pupil threshold\n' + |
||||||
|
'Alt+arrows: Search limit') |
||||||
|
|
||||||
|
# lines for drawing cross hair etc. |
||||||
|
self.line = visual.Line(self.display, start=(0, 0), end=(0,0), |
||||||
|
lineWidth=2.0, lineColor=[0,0,0], units='pix') |
||||||
|
|
||||||
|
# set a few tracker parameters |
||||||
|
self.tracker=tracker |
||||||
|
self.tracker.setOfflineMode() |
||||||
|
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") |
||||||
|
# let the tracker know the correct screen resolution being used |
||||||
|
self.tracker.sendCommand("screen_pixel_coords = 0 0 %d %d" % (self.w-1, self.h-1)) |
||||||
|
|
||||||
|
def setup_cal_display(self): |
||||||
|
'''Set up the calibration display before entering the calibration/validation routine''' |
||||||
|
|
||||||
|
self.display.clearBuffer() |
||||||
|
self.calibInst.autoDraw = True |
||||||
|
self.clear_cal_display() |
||||||
|
|
||||||
|
def clear_cal_display(self): |
||||||
|
'''Clear the calibration display''' |
||||||
|
|
||||||
|
self.calibInst.autoDraw = False |
||||||
|
self.title.autoDraw = False |
||||||
|
self.display.clearBuffer() |
||||||
|
self.display.color = self.bg_color |
||||||
|
self.display.flip() |
||||||
|
|
||||||
|
def exit_cal_display(self): |
||||||
|
'''Exit the calibration/validation routine, set the screen units to |
||||||
|
the original one used by the user''' |
||||||
|
|
||||||
|
self.display.setUnits(self.units) |
||||||
|
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.clear_cal_display() |
||||||
|
self.display.flip() |
||||||
|
|
||||||
|
def draw_cal_target(self, x, y): |
||||||
|
'''Draw the calibration/validation & drift-check target''' |
||||||
|
|
||||||
|
self.clear_cal_display() |
||||||
|
xVis = (x - self.w/2) |
||||||
|
yVis = (self.h/2 - y) |
||||||
|
cal_target_out = visual.GratingStim(self.display, tex='none', mask='circle', |
||||||
|
size=2.0/100*self.w, color=[1.0,1.0,1.0], units='pix') |
||||||
|
cal_target_in = visual.GratingStim(self.display, tex='none', mask='circle', |
||||||
|
size=2.0/300*self.w, color=[-1.0,-1.0,-1.0], units='pix') |
||||||
|
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.''' |
||||||
|
|
||||||
|
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''' |
||||||
|
|
||||||
|
if self.pylinkMinorVer== '1': # the Mac version |
||||||
|
x1 = x1/2; y1=y1/2; x2=x2/2;y2=y2/2; |
||||||
|
|
||||||
|
y1 = (-y1 + self.size[1]/2)* self.img_scaling_factor |
||||||
|
x1 = (+x1 - self.size[0]/2)* self.img_scaling_factor |
||||||
|
y2 = (-y2 + self.size[1]/2)* self.img_scaling_factor |
||||||
|
x2 = (+x2 - self.size[0]/2)* self.img_scaling_factor |
||||||
|
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 |
||||||
|
(x,y) is top-left corner of the bounding box |
||||||
|
''' |
||||||
|
|
||||||
|
if self.pylinkMinorVer == '1': # Mac version |
||||||
|
x = x/2; y=y/2; width=width/2;height=height/2; |
||||||
|
|
||||||
|
width = width * self.img_scaling_factor |
||||||
|
height = height* self.img_scaling_factor |
||||||
|
y = (-y + self.size[1]/2)* self.img_scaling_factor |
||||||
|
x = (+x - self.size[0]/2)* self.img_scaling_factor |
||||||
|
color = self.getColorFromIndex(colorindex) |
||||||
|
|
||||||
|
if width > height: |
||||||
|
rad = height / 2 |
||||||
|
if rad == 0: return #cannot draw the circle with 0 radius |
||||||
|
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)] |
||||||
|
else: |
||||||
|
rad = width / 2 |
||||||
|
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)] |
||||||
|
|
||||||
|
lozenge = visual.ShapeStim(self.display, vertices = list(zip(Xs1+Xs2, Ys1+Ys2)), |
||||||
|
lineWidth=2.0, lineColor=color, closeShape=True, units='pix') |
||||||
|
lozenge.draw() |
||||||
|
|
||||||
|
def get_mouse_state(self): |
||||||
|
'''Get the current mouse position and status''' |
||||||
|
|
||||||
|
X, Y = self.mouse.getPos() |
||||||
|
mX = self.size[0]/2.0*self.img_scaling_factor + X |
||||||
|
mY = self.size[1]/2.0*self.img_scaling_factor - Y |
||||||
|
if mX <=0: mX = 0 |
||||||
|
if mX > self.size[0]*self.img_scaling_factor: |
||||||
|
mX = self.size[0]*self.img_scaling_factor |
||||||
|
if mY < 0: mY = 0 |
||||||
|
if mY > self.size[1]*self.img_scaling_factor: |
||||||
|
mY = self.size[1]*self.img_scaling_factor |
||||||
|
state = self.mouse.getPressed()[0] |
||||||
|
mX = mX/self.img_scaling_factor |
||||||
|
mY = mY/self.img_scaling_factor |
||||||
|
|
||||||
|
if self.pylinkMinorVer == '1': |
||||||
|
mX = mX *2; mY = mY*2 |
||||||
|
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: k = 0 |
||||||
|
|
||||||
|
# plus/equal & minux signs for CR adjustment |
||||||
|
if keycode in ['num_add', 'equal']: k = ord('+') |
||||||
|
if keycode in ['num_subtract', 'minus']: k = ord('-') |
||||||
|
|
||||||
|
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.calibInst.autoDraw=True |
||||||
|
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.last_mouse_state = -1 |
||||||
|
self.size = (width, height) |
||||||
|
self.title.autoDraw = True |
||||||
|
self.calibInst.autoDraw=True |
||||||
|
|
||||||
|
def image_title(self, text): |
||||||
|
'''Draw title text below the camera image''' |
||||||
|
|
||||||
|
self.title.text = text |
||||||
|
|
||||||
|
def draw_image_line(self, width, line, totlines, buff): |
||||||
|
'''Display image pixel by pixel, line by line''' |
||||||
|
|
||||||
|
self.size = (width, totlines) |
||||||
|
|
||||||
|
i =0 |
||||||
|
for i in range(width): |
||||||
|
try: self.imagebuffer.append(self.pal[buff[i]]) |
||||||
|
except: pass |
||||||
|
|
||||||
|
if line == totlines: |
||||||
|
bufferv = self.imagebuffer.tostring() |
||||||
|
img = Image.frombytes("RGBX", (width, totlines), bufferv) # Pillow |
||||||
|
imgResize = img.resize((width*self.img_scaling_factor, totlines*self.img_scaling_factor)) |
||||||
|
imgResizeVisual = visual.ImageStim(self.display, image=imgResize, units='pix') |
||||||
|
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.resizeImagebuffer = 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 |
@ -0,0 +1,470 @@ |
|||||||
|
"""An experiment to cause eye saccades (movements) or smooth pursuits |
||||||
|
|
||||||
|
Author - Michael Tan (mtan30@uic.edu) |
||||||
|
|
||||||
|
Adapted from: https://github.com/colinquirk/PsychopyChangeDetection |
||||||
|
|
||||||
|
Note: this code relies on the Colin Quirk templateexperiments module. You can get it from |
||||||
|
https://github.com/colinquirk/templateexperiments and either put it in the same folder as this |
||||||
|
code or give the path to psychopy in the preferences. |
||||||
|
""" |
||||||
|
|
||||||
|
from __future__ import division |
||||||
|
from __future__ import print_function |
||||||
|
|
||||||
|
import os |
||||||
|
import sys |
||||||
|
import errno |
||||||
|
import math |
||||||
|
import warnings |
||||||
|
|
||||||
|
import json |
||||||
|
import random |
||||||
|
|
||||||
|
import numpy as np |
||||||
|
|
||||||
|
import psychopy.core |
||||||
|
import psychopy.event |
||||||
|
import psychopy.visual |
||||||
|
import psychopy.hardware.joystick |
||||||
|
|
||||||
|
import psychopy # noqa:F401 |
||||||
|
import eyelinker |
||||||
|
import template |
||||||
|
|
||||||
|
distance_to_monitor = 90 |
||||||
|
|
||||||
|
# Things you probably don't need to change, but can if you want to |
||||||
|
exp_name = 'SaccadePursuit' |
||||||
|
|
||||||
|
data_fields = [ |
||||||
|
'Subject', |
||||||
|
'Condition', |
||||||
|
'Block', |
||||||
|
'Trial', |
||||||
|
'Timestamp', |
||||||
|
'TrialType', |
||||||
|
'Duration', |
||||||
|
'Frequency', |
||||||
|
'Locations', |
||||||
|
] |
||||||
|
|
||||||
|
gender_options = [ |
||||||
|
'Male', |
||||||
|
'Female', |
||||||
|
'Other/Choose Not To Respond', |
||||||
|
] |
||||||
|
|
||||||
|
# Add additional questions here |
||||||
|
questionaire_dict = { |
||||||
|
'Age': 0, |
||||||
|
'Gender': gender_options, |
||||||
|
} |
||||||
|
|
||||||
|
def warning_on_one_line(message, category, filename, lineno, file=None, line=None): |
||||||
|
return ' %s:%s: %s:%s' % (filename, lineno, category.__name__, message) |
||||||
|
|
||||||
|
# This is the logic that runs the experiment |
||||||
|
# Change anything below this comment at your own risk |
||||||
|
class SPtask(template.BaseExperiment): |
||||||
|
"""The class that runs the Saccade Pursuit experiment. |
||||||
|
|
||||||
|
Parameters: |
||||||
|
|
||||||
|
Additional keyword arguments are sent to template.BaseExperiment(). |
||||||
|
|
||||||
|
Methods: |
||||||
|
|
||||||
|
""" |
||||||
|
|
||||||
|
def __init__(self, pursuit_time, |
||||||
|
stim_color, pursuit_frequencies, |
||||||
|
saccade_distance, saccade_time, |
||||||
|
stimulus_size, saccade_fixation_color, |
||||||
|
fixation_size, pursuit_distance, |
||||||
|
necker_time, necker_color, |
||||||
|
necker_scale, necker_bg_color, |
||||||
|
necker_file, rivalry_file1, rivalry_file2, |
||||||
|
fixation_trial_time, rivalry_time, |
||||||
|
rivalry_scale,rivalry_border_color, |
||||||
|
rivalry_border_width, rivalry_distance, |
||||||
|
questionaire_dict=questionaire_dict, **kwargs): |
||||||
|
|
||||||
|
# Pursuit |
||||||
|
self.pursuit_time = pursuit_time |
||||||
|
self.pursuit_distance = pursuit_distance |
||||||
|
self.pursuit_frequencies = pursuit_frequencies |
||||||
|
|
||||||
|
# Saccade |
||||||
|
self.stim_color = stim_color |
||||||
|
self.saccade_time = saccade_time |
||||||
|
self.stimulus_size = stimulus_size |
||||||
|
self.saccade_distance = saccade_distance |
||||||
|
self.saccade_fixation_color = saccade_fixation_color |
||||||
|
|
||||||
|
# Fixation |
||||||
|
self.fixation_size = fixation_size |
||||||
|
self.fixation_trial_time = fixation_trial_time |
||||||
|
|
||||||
|
# Necker |
||||||
|
self.necker_time = necker_time |
||||||
|
self.necker_color = necker_color |
||||||
|
self.necker_scale = necker_scale |
||||||
|
self.necker_bg_color = necker_bg_color |
||||||
|
self.necker_file = necker_file |
||||||
|
|
||||||
|
# Rivalry |
||||||
|
self.rivalry_time = rivalry_time |
||||||
|
self.rivalry_scale = rivalry_scale |
||||||
|
self.rivalry_file1 = rivalry_file1 |
||||||
|
self.rivalry_file2 = rivalry_file2 |
||||||
|
self.rivalry_border_color = rivalry_border_color |
||||||
|
self.rivalry_border_width = rivalry_border_width |
||||||
|
self.rivalry_distance = rivalry_distance |
||||||
|
|
||||||
|
self.questionaire_dict = questionaire_dict |
||||||
|
|
||||||
|
super(SPtask, self).__init__(**kwargs) |
||||||
|
|
||||||
|
def chdir(self): |
||||||
|
"""Changes the directory to where the data will be saved. |
||||||
|
""" |
||||||
|
|
||||||
|
try: |
||||||
|
os.makedirs(self.data_directory) |
||||||
|
except OSError as e: |
||||||
|
if e.errno != errno.EEXIST: |
||||||
|
raise |
||||||
|
|
||||||
|
os.chdir(self.data_directory) |
||||||
|
|
||||||
|
def make_block(self, condition, numTrials): |
||||||
|
"""Makes a block of trials. |
||||||
|
|
||||||
|
Returns a shuffled list of trials created by self.make_trial. |
||||||
|
""" |
||||||
|
|
||||||
|
trial_list = [] |
||||||
|
|
||||||
|
saccade_locations = list(range(-self.saccade_distance,0,1)) |
||||||
|
for x in range(self.saccade_distance): |
||||||
|
saccade_locations.append(x+1) |
||||||
|
random.shuffle(saccade_locations) |
||||||
|
|
||||||
|
counter = 0 |
||||||
|
for num_trials in range(numTrials): |
||||||
|
if condition == 'Pursuit': |
||||||
|
trial = self.make_trial(self.pursuit_time, self.pursuit_frequencies, condition) |
||||||
|
trial_list.append(trial) |
||||||
|
elif condition == 'Necker': |
||||||
|
trial = self.make_trial(self.necker_time, 1, condition) |
||||||
|
trial_list.append(trial) |
||||||
|
elif condition == 'Fixation': |
||||||
|
trial = self.make_trial(self.fixation_trial_time,1,condition) |
||||||
|
trial_list.append(trial) |
||||||
|
elif condition == 'Rivalry': |
||||||
|
trial = self.make_trial(self.rivalry_time,1,condition) |
||||||
|
trial_list.append(trial) |
||||||
|
else: |
||||||
|
for loc in saccade_locations: |
||||||
|
trial = self.make_trial(round(random.uniform(1,self.saccade_time),3), loc, condition) |
||||||
|
trial_list.append(trial) |
||||||
|
counter += 1 |
||||||
|
|
||||||
|
random.shuffle(trial_list) |
||||||
|
|
||||||
|
return trial_list |
||||||
|
|
||||||
|
def make_trial(self, trial_time, pursuit_frequency, trial_type): |
||||||
|
"""Creates a single trial dict. A helper function for self.make_block. |
||||||
|
|
||||||
|
Returns the trial dict. |
||||||
|
|
||||||
|
Parameters: |
||||||
|
set_size -- The number of stimuli for this trial. |
||||||
|
trial_type -- Whether this trial is same or different. |
||||||
|
""" |
||||||
|
locs = [pursuit_frequency,0] |
||||||
|
|
||||||
|
trial = { |
||||||
|
'locations': locs, |
||||||
|
'trial_time': trial_time, |
||||||
|
'pursuit_frequency': pursuit_frequency, |
||||||
|
'trial_type': trial_type |
||||||
|
} |
||||||
|
|
||||||
|
return trial |
||||||
|
|
||||||
|
def display_break(self): |
||||||
|
"""Displays a break screen in between blocks. |
||||||
|
""" |
||||||
|
|
||||||
|
break_text = 'Please take a short break. Press space to continue.' |
||||||
|
self.display_text_screen(text=break_text, bg_color=[0, 0, 0]) |
||||||
|
|
||||||
|
def display_fixation(self, wait_time, necker_bg_color=[-1,-1,-1]): |
||||||
|
"""Displays a fixation cross. A helper function for self.run_trial. |
||||||
|
|
||||||
|
Parameters: |
||||||
|
wait_time -- The amount of time the fixation should be displayed for. |
||||||
|
""" |
||||||
|
|
||||||
|
self.experiment_window.color = necker_bg_color |
||||||
|
|
||||||
|
stim = psychopy.visual.TextStim( |
||||||
|
self.experiment_window, text='+', color=self.stim_color, height=self.fixation_size, units='deg') |
||||||
|
|
||||||
|
for frameN in range(int(round(wait_time*60))): |
||||||
|
stim.draw() |
||||||
|
self.experiment_window.flip() |
||||||
|
|
||||||
|
def display_saccade(self, coordinates, stim_time): |
||||||
|
"""Displays the stimuli. A helper function for self.run_saccade_trial. |
||||||
|
|
||||||
|
Parameters: |
||||||
|
coordinates -- A list of coordinates (list of x and y value) describing where the stimuli |
||||||
|
should be displayed. |
||||||
|
colors -- A list of colors describing what should be drawn at each coordinate. |
||||||
|
""" |
||||||
|
|
||||||
|
color = self.stim_color |
||||||
|
|
||||||
|
stim = psychopy.visual.Circle( |
||||||
|
self.experiment_window, radius=self.stimulus_size/2, |
||||||
|
pos=coordinates, fillColor=color, |
||||||
|
lineColor=color, units='deg') |
||||||
|
fixation = psychopy.visual.TextStim( |
||||||
|
self.experiment_window, text='+', color=self.saccade_fixation_color, |
||||||
|
height=self.fixation_size, units='deg') |
||||||
|
|
||||||
|
stimList = [] |
||||||
|
temp = list(range(-self.saccade_distance,self.saccade_distance,1)) |
||||||
|
for circleN in temp: |
||||||
|
if circleN < 0: |
||||||
|
tempCoord = circleN |
||||||
|
else: |
||||||
|
tempCoord = circleN+1 |
||||||
|
tempStim = psychopy.visual.Circle( |
||||||
|
self.experiment_window, radius=self.stimulus_size/2, |
||||||
|
pos=[tempCoord,0], fillColor=self.saccade_fixation_color, |
||||||
|
lineColor=self.saccade_fixation_color, units='deg') |
||||||
|
stimList.append(tempStim) |
||||||
|
|
||||||
|
for frameN in range(int(round(stim_time*60))): |
||||||
|
for s in stimList: |
||||||
|
s.draw() |
||||||
|
fixation.draw() |
||||||
|
stim.draw() |
||||||
|
self.experiment_window.flip() |
||||||
|
|
||||||
|
def display_saccade_fixation(self, stim_time): |
||||||
|
"""Displays the stimuli. A helper function for self.run_saccade_trial. |
||||||
|
|
||||||
|
Parameters: |
||||||
|
coordinates -- A list of coordinates (list of x and y value) describing where the stimuli |
||||||
|
should be displayed. |
||||||
|
colors -- A list of colors describing what should be drawn at each coordinate. |
||||||
|
""" |
||||||
|
|
||||||
|
fixation = psychopy.visual.TextStim( |
||||||
|
self.experiment_window, text='+', color=self.stim_color, |
||||||
|
height=self.fixation_size, units='deg') |
||||||
|
|
||||||
|
stimList = [] |
||||||
|
temp = list(range(-self.saccade_distance,self.saccade_distance,1)) |
||||||
|
for circleN in temp: |
||||||
|
if circleN < 0: |
||||||
|
tempCoord = circleN |
||||||
|
else: |
||||||
|
tempCoord = circleN+1 |
||||||
|
tempStim = psychopy.visual.Circle( |
||||||
|
self.experiment_window, radius=self.stimulus_size/2, |
||||||
|
pos=[tempCoord,0], fillColor=self.saccade_fixation_color, |
||||||
|
lineColor=self.saccade_fixation_color, units='deg') |
||||||
|
stimList.append(tempStim) |
||||||
|
|
||||||
|
for frameN in range(int(round(stim_time*60))): |
||||||
|
for s in stimList: |
||||||
|
s.draw() |
||||||
|
fixation.draw() |
||||||
|
self.experiment_window.flip() |
||||||
|
|
||||||
|
def display_pursuit(self, stim_frequency, stim_time): |
||||||
|
"""Displays a pursuit stimululus. A helper function for self.run_pursuit_trial. |
||||||
|
|
||||||
|
Parameters: |
||||||
|
coordinates -- A list of coordinates (list of x and y value) describing where the start |
||||||
|
point of the stimulus. |
||||||
|
color -- A color describing what should be drawn. |
||||||
|
""" |
||||||
|
|
||||||
|
color = self.stim_color |
||||||
|
|
||||||
|
Xposition = [0] |
||||||
|
num_frames_per_second = 60 |
||||||
|
counter = 0 |
||||||
|
for freq in stim_frequency: |
||||||
|
stim_frames = int(round(stim_time[counter]*num_frames_per_second)) |
||||||
|
for time in range(stim_frames): |
||||||
|
Xposition.append(math.sin(freq*math.radians((time+1)*360/num_frames_per_second))*self.pursuit_distance) |
||||||
|
counter += 1 |
||||||
|
|
||||||
|
stim = psychopy.visual.Circle( |
||||||
|
self.experiment_window, radius=self.stimulus_size/2, |
||||||
|
pos=(0,0), fillColor=color, |
||||||
|
lineColor=color, units='deg') |
||||||
|
# Draw circle for 0.5s prior to moving |
||||||
|
for frameN in range(30): |
||||||
|
stim.draw() |
||||||
|
self.experiment_window.flip() |
||||||
|
|
||||||
|
for Xpos in Xposition: |
||||||
|
stim.pos = (Xpos,0) |
||||||
|
stim.draw() |
||||||
|
self.experiment_window.flip() |
||||||
|
|
||||||
|
# Draw circle for 0.5s after moving |
||||||
|
for frameN in range(30): |
||||||
|
stim.draw() |
||||||
|
self.experiment_window.flip() |
||||||
|
|
||||||
|
def display_necker(self, stim_time, tracker): |
||||||
|
self.experiment_window.color = self.necker_bg_color |
||||||
|
stim = psychopy.visual.ImageStim( |
||||||
|
self.experiment_window, |
||||||
|
image=self.necker_file) |
||||||
|
stim.size *= self.necker_scale |
||||||
|
stim.setColor(self.necker_color, 'rgb255') |
||||||
|
responses = [] |
||||||
|
for frameN in range(stim_time*60): |
||||||
|
response = psychopy.event.getKeys() |
||||||
|
if response: |
||||||
|
if response[0]=='1' or response[0]=='3': |
||||||
|
direction = 'Left' |
||||||
|
elif response[0]=='2' or response[0]=='4': |
||||||
|
direction = 'Right' |
||||||
|
else: |
||||||
|
direction = 'Center' |
||||||
|
responses.append((response[0],direction,psychopy.core.getAbsTime())) |
||||||
|
if tracker: |
||||||
|
#print(response[0]) |
||||||
|
tracker.send_message(['Direction: %s' % direction]) |
||||||
|
stim.draw() |
||||||
|
self.experiment_window.flip() |
||||||
|
#print(responses) |
||||||
|
return responses |
||||||
|
|
||||||
|
def display_rivalry(self, stim_time, tracker): |
||||||
|
color = self.rivalry_border_color |
||||||
|
lineSize = self.rivalry_border_width |
||||||
|
imDist = self.rivalry_distance |
||||||
|
stim1 = psychopy.visual.ImageStim( |
||||||
|
self.experiment_window, |
||||||
|
image=self.rivalry_file1, pos=(-imDist,0)) |
||||||
|
stim1.size *= self.rivalry_scale |
||||||
|
stim2 = psychopy.visual.ImageStim( |
||||||
|
self.experiment_window, |
||||||
|
image=self.rivalry_file2, pos=(imDist,0)) |
||||||
|
stim2.size *= self.rivalry_scale |
||||||
|
|
||||||
|
borderScale = stim1.size[0]*0.4 |
||||||
|
borderWidth1 = stim1.size[0]+borderScale |
||||||
|
borderHeight1 = stim1.size[1]+borderScale |
||||||
|
borderWidth2 = stim2.size[0]+borderScale |
||||||
|
borderHeight2 = stim2.size[1]+borderScale |
||||||
|
stimBorder1 = psychopy.visual.Rect( |
||||||
|
self.experiment_window, width=borderWidth1, |
||||||
|
height=borderHeight1, |
||||||
|
pos=stim1.pos, lineWidth=lineSize, |
||||||
|
lineColor=color, units='deg') |
||||||
|
stimBorder2 = psychopy.visual.Rect( |
||||||
|
self.experiment_window, width=borderWidth2, |
||||||
|
height=borderHeight2, |
||||||
|
pos=stim2.pos, lineWidth=lineSize, |
||||||
|
lineColor=color, units='deg') |
||||||
|
stimFix1_top = psychopy.visual.Line( |
||||||
|
self.experiment_window, start=(stim1.pos[0],stim1.pos[1]+borderHeight1/2), |
||||||
|
end=(stim1.pos[0],stim1.pos[1]+stim1.size[1]/1.9), units='deg', |
||||||
|
lineColor=color, lineWidth=lineSize) |
||||||
|
stimFix1_left = psychopy.visual.Line( |
||||||
|
self.experiment_window, start=(stim1.pos[0]-borderWidth1/2,stim1.pos[1]), |
||||||
|
end=(stim1.pos[0]-stim1.size[0]/1.9,stim1.pos[1]), units='deg', |
||||||
|
lineColor=color, lineWidth=lineSize) |
||||||
|
stimFix2_bottom = psychopy.visual.Line( |
||||||
|
self.experiment_window, start=(stim2.pos[0],stim2.pos[1]-borderHeight2/2), |
||||||
|
end=(stim2.pos[0],stim2.pos[1]-stim2.size[1]/1.9), units='deg', |
||||||
|
lineColor=color, lineWidth=lineSize) |
||||||
|
stimFix2_right = psychopy.visual.Line( |
||||||
|
self.experiment_window, start=(stim2.pos[0]+borderWidth2/2,stim2.pos[1]), |
||||||
|
end=(stim2.pos[0]+stim2.size[0]/1.9,stim2.pos[1]), units='deg', |
||||||
|
lineColor=color, lineWidth=lineSize) |
||||||
|
|
||||||
|
responses = [] |
||||||
|
for frameN in range(stim_time*60): |
||||||
|
response = psychopy.event.getKeys() |
||||||
|
if response: |
||||||
|
if response[0]=='1' or response[0]=='3': |
||||||
|
direction = 'Left' |
||||||
|
elif response[0]=='2' or response[0]=='4': |
||||||
|
direction = 'Right' |
||||||
|
else: |
||||||
|
direction = 'Center' |
||||||
|
responses.append((response[0],direction,psychopy.core.getAbsTime())) |
||||||
|
if tracker: |
||||||
|
tracker.send_message(['Direction: %s' % direction]) |
||||||
|
stim1.draw() |
||||||
|
stim2.draw() |
||||||
|
stimBorder1.draw() |
||||||
|
stimBorder2.draw() |
||||||
|
stimFix1_top.draw() |
||||||
|
stimFix1_left.draw() |
||||||
|
stimFix2_bottom.draw() |
||||||
|
stimFix2_right.draw() |
||||||
|
self.experiment_window.flip() |
||||||
|
return responses |
||||||
|
|
||||||
|
def send_data(self, data): |
||||||
|
"""Updates the experiment data with the information from the last trial. |
||||||
|
|
||||||
|
This function is seperated from run_trial to allow additional information to be added |
||||||
|
afterwards. |
||||||
|
|
||||||
|
Parameters: |
||||||
|
data -- A dict where keys exist in data_fields and values are to be saved. |
||||||
|
""" |
||||||
|
self.update_experiment_data([data]) |
||||||
|
|
||||||
|
def run_trial(self, trial, block_num, trial_num, tracker=[]): |
||||||
|
freq = [0] |
||||||
|
if trial['trial_type']=='Necker': |
||||||
|
freq = self.display_necker(trial['trial_time'], tracker) |
||||||
|
elif trial['trial_type']=='Pursuit': |
||||||
|
self.display_pursuit(trial['pursuit_frequency'],trial['trial_time']) |
||||||
|
freq = trial['pursuit_frequency'] |
||||||
|
elif trial['trial_type']=='Fixation': |
||||||
|
self.display_fixation(trial['trial_time']) |
||||||
|
elif trial['trial_type']=='Rivalry': |
||||||
|
freq = self.display_rivalry(trial['trial_time'], tracker) |
||||||
|
else: |
||||||
|
self.display_saccade(trial['locations'], trial['trial_time']) |
||||||
|
|
||||||
|
data = { |
||||||
|
'Subject': self.experiment_info['Subject Identifier'], |
||||||
|
'Block': block_num, |
||||||
|
'Trial': trial_num, |
||||||
|
'Timestamp': psychopy.core.getAbsTime(), |
||||||
|
'TrialType': trial['trial_type'], |
||||||
|
'Duration': trial['trial_time'], |
||||||
|
'Frequency': freq, |
||||||
|
'Locations': trial['locations'] |
||||||
|
} |
||||||
|
|
||||||
|
return data |
||||||
|
|
||||||
|
|
||||||
|
# If you call this script directly, directs you to correct file |
||||||
|
if __name__ == '__main__': |
||||||
|
warnings.showwarning( |
||||||
|
'Please run SaccadePursuitEyeTracking.py', |
||||||
|
Warning,'SaccadePursuit.py',414) |
||||||
|
warnings.formatwarning = warning_on_one_line |
@ -0,0 +1,367 @@ |
|||||||
|
"""A wrapper for SaccadePursuit to track eye movements in addition |
||||||
|
|
||||||
|
Author - Michael Tan (mtan30@uic.edu) |
||||||
|
|
||||||
|
Adapted from: https://github.com/colinquirk/ChangeDetectionEyeTracking |
||||||
|
|
||||||
|
Note: this code relies on the Colin Quirk templateexperiments module. You can get it from |
||||||
|
https://github.com/colinquirk/templateexperiments and either put it in the same folder as this |
||||||
|
code or give the path to psychopy in the preferences. |
||||||
|
""" |
||||||
|
from __future__ import division |
||||||
|
from __future__ import print_function |
||||||
|
|
||||||
|
import os |
||||||
|
import random |
||||||
|
import sys |
||||||
|
import traceback |
||||||
|
import subprocess |
||||||
|
|
||||||
|
# Necesssary to access psychopy paths |
||||||
|
import psychopy # noqa:F401 |
||||||
|
|
||||||
|
import eyelinker |
||||||
|
|
||||||
|
import SaccadePursuit |
||||||
|
|
||||||
|
# Experimental Parameters |
||||||
|
monitor_name = 'testMonitor' |
||||||
|
monitor_width = 41 |
||||||
|
distance_to_monitor = 70 |
||||||
|
monitor_px = [1440,900] |
||||||
|
window_screen = 1 |
||||||
|
|
||||||
|
|
||||||
|
disableTracker = False # For Debugging |
||||||
|
conditions = ['Fixation', 'Pursuit', 'Saccade', 'AntiSaccade', 'Necker', 'Rivalry'] |
||||||
|
#conditions = ['Saccade'] |
||||||
|
isi_time = 1 # Interstimulus Interval |
||||||
|
data_directory = os.path.join( |
||||||
|
os.path.expanduser('~'), 'Desktop', 'ExperimentalData', 'SaccadePursuitEyeTracking') |
||||||
|
image_directory = os.path.join(os.getcwd(),'Images') |
||||||
|
|
||||||
|
# Saccade / Antisaccade Parameters |
||||||
|
number_of_saccade_trials = 1 |
||||||
|
number_of_saccade_blocks = 1 |
||||||
|
saccade_distance = 15 #15 |
||||||
|
saccade_time = 3 #3 |
||||||
|
stimulus_size = 0.3 |
||||||
|
stim_color = [1,-1,-1] |
||||||
|
saccade_fixation_color = [1,1,1] |
||||||
|
|
||||||
|
# Pursuit Parameters |
||||||
|
number_of_pursuit_trials = 1 |
||||||
|
number_of_pursuit_blocks = 1 |
||||||
|
pursuit_distance = 15 |
||||||
|
pursuit_frequencies = [0.1,0.2,0.4] |
||||||
|
pursuit_time = [40,20,15] |
||||||
|
|
||||||
|
# Necker Cube Parameters |
||||||
|
number_of_necker_trials = 1 |
||||||
|
number_of_necker_blocks = 1 |
||||||
|
necker_time = 90 |
||||||
|
necker_color = [255,255,255] |
||||||
|
necker_bg_color = [-0.5,-0.5,-0.5] |
||||||
|
necker_scale = 0.5 |
||||||
|
necker_file = os.path.join(image_directory,'Necker1.tif') |
||||||
|
|
||||||
|
# Fixation Parameters |
||||||
|
number_of_fixation_trials = 1 |
||||||
|
number_of_fixation_blocks = 1 |
||||||
|
fixation_size = stimulus_size*2 |
||||||
|
fixation_trial_time = 15 |
||||||
|
|
||||||
|
# Binocular Rivalry Parameters |
||||||
|
number_of_rivalry_trials = 1 |
||||||
|
number_of_rivalry_blocks = 1 |
||||||
|
rivalry_time = 90 |
||||||
|
rivalry_scale = 2.5 |
||||||
|
rivalry_file1 = os.path.join(image_directory,'house4n_11-160.tif') |
||||||
|
rivalry_file2 = os.path.join(image_directory,'face2nS_11-160.tif') |
||||||
|
rivalry_border_color = [1,1,1] |
||||||
|
rivalry_border_width = 5 |
||||||
|
rivalry_distance = 4 |
||||||
|
|
||||||
|
data_fields = [ |
||||||
|
'Subject', |
||||||
|
'Condition', |
||||||
|
'Block', |
||||||
|
'Trial', |
||||||
|
'Timestamp', |
||||||
|
'TrialType', |
||||||
|
'Duration', |
||||||
|
'Frequency', |
||||||
|
'Locations', |
||||||
|
] |
||||||
|
|
||||||
|
instruct_text = [ |
||||||
|
('Welcome to the experiment. Press space to continue.'), |
||||||
|
('In this experiment you will be following targets.\n\n' |
||||||
|
'Each trial will start with a fixation cross. ' |
||||||
|
'Focus on the fixation cross until a stimulus appears.\n\n' |
||||||
|
'In the first three phases, you will follow the stimuli. ' |
||||||
|
'In the fourth phase, you will focus on the ' |
||||||
|
'opposite side of center cross at an ' |
||||||
|
'equal distance as the stimulus. In the fifth ' |
||||||
|
'phase, you will see a cube, and are to respond as ' |
||||||
|
'indicated.' |
||||||
|
'\n\n' |
||||||
|
'Do not move your head during the trials of this ' |
||||||
|
'experiment. Move only your eyes to follow the targets.' |
||||||
|
'You will get breaks in between blocks.\n\n' |
||||||
|
'Press space to continue.'), |
||||||
|
] |
||||||
|
|
||||||
|
saccade_instruct_text = ( |
||||||
|
'For these trials, focus on the fixation cross. When the target ' |
||||||
|
'appears, move your gaze to the target. Move your gaze back to ' |
||||||
|
'the fixation point after the target disappears.\n\n' |
||||||
|
'Try not to blink after the target appears.\n\n' |
||||||
|
'Press space to continue.' |
||||||
|
) |
||||||
|
|
||||||
|
antisaccade_instruct_text = ( |
||||||
|
'For these trials, focus on the fixation cross. When the target ' |
||||||
|
'appears, move your gaze to the OPPOSITE direction of the target ' |
||||||
|
'at approximately the same distance from the fixation cross as ' |
||||||
|
'the target. Move your gaze back to the fixation point after ' |
||||||
|
'the target disappears.\n\n' |
||||||
|
'Try not to blink after the target appears.\n\n' |
||||||
|
'Press space to continue.' |
||||||
|
) |
||||||
|
|
||||||
|
pursuit_instruct_text = ( |
||||||
|
'For these trials, when the circle target appears, follow the ' |
||||||
|
'target with your eyes.\n\n' |
||||||
|
'Try not to blink while the circle is moving.\n\n' |
||||||
|
'Press space to continue.' |
||||||
|
) |
||||||
|
|
||||||
|
fixation_instruct_text = ( |
||||||
|
'For these trials, when the cross appears, fixate on it.\n\n' |
||||||
|
'Try not to move your eyes from the cross while it ' |
||||||
|
'remains visible.\n\n' |
||||||
|
'Press space to continue.' |
||||||
|
) |
||||||
|
|
||||||
|
necker_instruct_text = ( |
||||||
|
'For these trials, focus on the square. When your ' |
||||||
|
'perception is that the square is pointing down and ' |
||||||
|
'to the left, press the left button. As soon as it ' |
||||||
|
'switches to up and to the right, press the right ' |
||||||
|
'button.\n\n' |
||||||
|
'Respond at any time during the stimulus.\n\n' |
||||||
|
'Press space to continue.' |
||||||
|
) |
||||||
|
|
||||||
|
rivalry_instruct_text = ( |
||||||
|
'For these trials, the experimenter will move a ' |
||||||
|
'system in front of your field of view. Look through ' |
||||||
|
'the mirrors to see a different image in each eye.\n\n' |
||||||
|
'If you perceive a face, press the right button. If ' |
||||||
|
'you perceive a house, press the left button. If ' |
||||||
|
'you perceive a combination of the face and house, ' |
||||||
|
'press the middle button.\n\n' |
||||||
|
'Respond at any time during the stimuli.\n\n' |
||||||
|
'Press space to continue.' |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class EyeTrackingSaccadePursuit(SaccadePursuit.SPtask): |
||||||
|
def __init__(self, **kwargs): |
||||||
|
self.quit = False # Needed because eyetracker must shut down |
||||||
|
self.tracker = None |
||||||
|
self.disable_tracker = disableTracker |
||||||
|
self.window_screen = window_screen |
||||||
|
|
||||||
|
self.number_of_saccade_trials = number_of_saccade_trials |
||||||
|
self.number_of_saccade_blocks = number_of_saccade_blocks |
||||||
|
self.number_of_pursuit_trials = number_of_pursuit_trials |
||||||
|
self.number_of_pursuit_blocks = number_of_pursuit_blocks |
||||||
|
self.number_of_necker_trials = number_of_necker_trials |
||||||
|
self.number_of_necker_blocks = number_of_necker_blocks |
||||||
|
self.number_of_fixation_trials = number_of_fixation_trials |
||||||
|
self.number_of_fixation_blocks = number_of_fixation_blocks |
||||||
|
self.number_of_rivalry_trials = number_of_rivalry_trials |
||||||
|
self.number_of_rivalry_blocks = number_of_rivalry_blocks |
||||||
|
|
||||||
|
super(EyeTrackingSaccadePursuit, self).__init__(**kwargs) |
||||||
|
|
||||||
|
def quit_experiment(self): |
||||||
|
self.quit = True |
||||||
|
if self.experiment_window: |
||||||
|
self.display_text_screen('Quiting...', wait_for_input=False) |
||||||
|
if self.tracker: |
||||||
|
fName = os.path.join(self.data_directory, |
||||||
|
'ETSP' + self.experiment_info['Subject Identifier'] + '.edf') |
||||||
|
self.tracker.set_offline_mode() |
||||||
|
self.tracker.close_edf() |
||||||
|
self.tracker.transfer_edf() |
||||||
|
self.tracker.close_connection() |
||||||
|
subprocess.call(['edf2asc',fName]) |
||||||
|
super(EyeTrackingSaccadePursuit, self).quit_experiment() |
||||||
|
|
||||||
|
def run(self): |
||||||
|
self.chdir() |
||||||
|
|
||||||
|
print('Note: EDF file will be overwritten if identical subject identifiers are used!') |
||||||
|
ok = self.get_experiment_info_from_dialog(self.questionaire_dict) |
||||||
|
|
||||||
|
if not ok: |
||||||
|
print('Experiment has been terminated.') |
||||||
|
sys.exit(1) |
||||||
|
|
||||||
|
self.save_experiment_info() |
||||||
|
self.open_csv_data_file() |
||||||
|
self.open_window(screen=self.window_screen) |
||||||
|
self.display_text_screen('Loading...', wait_for_input=False) |
||||||
|
|
||||||
|
if not self.disableTracker: |
||||||
|
self.tracker = eyelinker.EyeLinker( |
||||||
|
self.experiment_window, |
||||||
|
'ETSP' + self.experiment_info['Subject Identifier'] + '.edf', |
||||||
|
'BOTH' |
||||||
|
) |
||||||
|
self.tracker.initialize_graphics() |
||||||
|
self.tracker.open_edf() |
||||||
|
self.tracker.initialize_tracker() |
||||||
|
self.tracker.send_calibration_settings() |
||||||
|
|
||||||
|
for instruction in self.instruct_text: |
||||||
|
self.display_text_screen(text=instruction) |
||||||
|
|
||||||
|
if not self.disableTracker: |
||||||
|
self.tracker.display_eyetracking_instructions() |
||||||
|
self.tracker.setup_tracker() |
||||||
|
|
||||||
|
#random.shuffle(self.conditions) |
||||||
|
|
||||||
|
condition_counter = 0 |
||||||
|
for condition in self.conditions: |
||||||
|
condition_counter += 1 |
||||||
|
numBlocks = 1 |
||||||
|
numTrials = 1 |
||||||
|
if condition == 'Saccade': |
||||||
|
self.display_text_screen(text=saccade_instruct_text) |
||||||
|
numBlocks = self.number_of_saccade_blocks |
||||||
|
numTrials = self.number_of_saccade_trials |
||||||
|
elif condition=='AntiSaccade': |
||||||
|
self.display_text_screen(text=antisaccade_instruct_text) |
||||||
|
numBlocks = self.number_of_saccade_blocks |
||||||
|
numTrials = self.number_of_saccade_trials |
||||||
|
elif condition=='Fixation': |
||||||
|
self.display_text_screen(text=fixation_instruct_text) |
||||||
|
numBlocks = self.number_of_fixation_blocks |
||||||
|
numTrials = self.number_of_fixation_trials |
||||||
|
elif condition=='Pursuit': |
||||||
|
self.display_text_screen(text=pursuit_instruct_text) |
||||||
|
numBlocks = self.number_of_pursuit_blocks |
||||||
|
numTrials = self.number_of_pursuit_trials |
||||||
|
elif condition=='Necker': |
||||||
|
self.display_text_screen(text=necker_instruct_text) |
||||||
|
numBlocks = self.number_of_necker_blocks |
||||||
|
numTrials = self.number_of_necker_trials |
||||||
|
elif condition=='Rivalry': |
||||||
|
self.display_text_screen(text=rivalry_instruct_text) |
||||||
|
numBlocks = self.number_of_rivalry_blocks |
||||||
|
numTrials = self.number_of_rivalry_trials |
||||||
|
else: |
||||||
|
continue |
||||||
|
|
||||||
|
for block_num in range(numBlocks): |
||||||
|
block = self.make_block(condition, numTrials) |
||||||
|
self.display_text_screen(text='Get ready...', wait_for_input=False) |
||||||
|
psychopy.core.wait(2) |
||||||
|
if condition == 'Saccade' or condition == 'AntiSaccade': |
||||||
|
self.display_saccade_fixation(1) |
||||||
|
else: |
||||||
|
self.display_fixation(0.5) |
||||||
|
|
||||||
|
for trial_num, trial in enumerate(block): |
||||||
|
print( |
||||||
|
"Condition: ",condition,"(",condition_counter,"/",len(self.conditions),")", |
||||||
|
"Block ",block_num+1,"/",numBlocks, |
||||||
|
" Trial ",trial_num+1,"/",len(block) |
||||||
|
) |
||||||
|
if not self.disableTracker: |
||||||
|
self.tracker.send_message('CONDITION %s' % condition) |
||||||
|
self.tracker.send_message('BLOCK %d' % block_num) |
||||||
|
self.tracker.send_message('TRIAL %d' % trial_num) |
||||||
|
status = '%s: Block %d, Trial %d' % (condition, block_num, trial_num) |
||||||
|
self.tracker.send_status(status) |
||||||
|
self.tracker.start_recording() |
||||||
|
data = self.run_trial(trial, block_num, trial_num, self.tracker) |
||||||
|
self.tracker.stop_recording() |
||||||
|
else: |
||||||
|
data = self.run_trial(trial, block_num, trial_num) |
||||||
|
data.update({'Condition': condition}) |
||||||
|
self.send_data(data) |
||||||
|
if not condition == 'Saccade' and not condition=='AntiSaccade': |
||||||
|
self.display_fixation(self.isi_time) |
||||||
|
|
||||||
|
if condition == 'Saccade' or condition == 'AntiSaccade': |
||||||
|
self.display_saccade_fixation(1) |
||||||
|
else: |
||||||
|
self.display_fixation(0.5) |
||||||
|
self.save_data_to_csv() |
||||||
|
|
||||||
|
if block_num + 1 != numBlocks: |
||||||
|
self.display_break() |
||||||
|
|
||||||
|
if condition == 'Saccade': |
||||||
|
self.display_text_screen(text='Remember:\n\n' + saccade_instruct_text) |
||||||
|
elif condition=='AntiSaccade': |
||||||
|
self.display_text_screen(text='Remember:\n\n' + antisaccade_instruct_text) |
||||||
|
else: |
||||||
|
self.display_text_screen(text='Remember:\n\n' + pursuit_instruct_text) |
||||||
|
|
||||||
|
self.display_text_screen( |
||||||
|
'The experiment is now over.', |
||||||
|
bg_color=[50, 150, 50], text_color=[255, 255, 255], |
||||||
|
wait_for_input=False) |
||||||
|
|
||||||
|
psychopy.core.wait(5) |
||||||
|
|
||||||
|
self.quit_experiment() |
||||||
|
|
||||||
|
|
||||||
|
experiment = EyeTrackingSaccadePursuit( |
||||||
|
experiment_name='ETSP', |
||||||
|
data_fields=data_fields, |
||||||
|
data_directory=data_directory, |
||||||
|
instruct_text=instruct_text, |
||||||
|
monitor_name=monitor_name, |
||||||
|
monitor_width=monitor_width, |
||||||
|
monitor_px=monitor_px, |
||||||
|
monitor_distance=distance_to_monitor, |
||||||
|
conditions=conditions, |
||||||
|
pursuit_time=pursuit_time, stim_color=stim_color, |
||||||
|
pursuit_frequencies=pursuit_frequencies, |
||||||
|
saccade_distance=saccade_distance, |
||||||
|
saccade_time=saccade_time, |
||||||
|
saccade_fixation_color=saccade_fixation_color, |
||||||
|
isi_time=isi_time, stimulus_size=stimulus_size, |
||||||
|
fixation_size=fixation_size, |
||||||
|
pursuit_distance=pursuit_distance, |
||||||
|
necker_time=necker_time, necker_color=necker_color, |
||||||
|
necker_scale=necker_scale, |
||||||
|
necker_bg_color=necker_bg_color, |
||||||
|
fixation_trial_time=fixation_trial_time, |
||||||
|
rivalry_time=rivalry_time, |
||||||
|
rivalry_scale=rivalry_scale, |
||||||
|
necker_file=necker_file, |
||||||
|
rivalry_file1=rivalry_file1, |
||||||
|
rivalry_file2=rivalry_file2, |
||||||
|
rivalry_border_color=rivalry_border_color, |
||||||
|
disableTracker=disableTracker, |
||||||
|
rivalry_border_width=rivalry_border_width, |
||||||
|
rivalry_distance=rivalry_distance, |
||||||
|
) |
||||||
|
|
||||||
|
if __name__ == '__main__': |
||||||
|
try: |
||||||
|
experiment.run() |
||||||
|
except Exception: |
||||||
|
print(traceback.format_exc()) |
||||||
|
if not experiment.quit: |
||||||
|
experiment.quit_experiment() |
@ -0,0 +1,272 @@ |
|||||||
|
from __future__ import print_function |
||||||
|
from __future__ import division |
||||||
|
|
||||||
|
import os |
||||||
|
import sys |
||||||
|
import time |
||||||
|
|
||||||
|
import pylink as pl |
||||||
|
#from PsychoPyCustomDisplay import PsychoPyCustomDisplay |
||||||
|
from EyeLinkCoreGraphicsPsychoPy import EyeLinkCoreGraphicsPsychoPy |
||||||
|
|
||||||
|
import psychopy.event |
||||||
|
import psychopy.visual |
||||||
|
|
||||||
|
|
||||||
|
class EyeLinker(object): |
||||||
|
def __init__(self, window, filename, eye): |
||||||
|
if len(filename) > 12: |
||||||
|
raise ValueError( |
||||||
|
'EDF filename must be at most 12 characters long including the extension.') |
||||||
|
|
||||||
|
if filename[-4:] != '.edf': |
||||||
|
raise ValueError( |
||||||
|
'Please include the .edf extension in the filename.') |
||||||
|
|
||||||
|
if eye not in ('LEFT', 'RIGHT', 'BOTH'): |
||||||
|
raise ValueError('eye must be set to LEFT, RIGHT, or BOTH.') |
||||||
|
|
||||||
|
self.window = window |
||||||
|
self.edf_filename = filename |
||||||
|
self.edf_open = False |
||||||
|
self.eye = eye |
||||||
|
self.resolution = tuple(window.size) |
||||||
|
self.tracker = pl.EyeLink() |
||||||
|
#self.genv = PsychoPyCustomDisplay(self.window, self.tracker) |
||||||
|
self.genv = EyeLinkCoreGraphicsPsychoPy(self.tracker, self.window) |
||||||
|
|
||||||
|
if all(i >= 0.5 for i in self.window.color): |
||||||
|
self.text_color = (-1, -1, -1) |
||||||
|
else: |
||||||
|
self.text_color = (1, 1, 1) |
||||||
|
|
||||||
|
def initialize_graphics(self): |
||||||
|
self.set_offline_mode() |
||||||
|
pl.openGraphicsEx(self.genv) |
||||||
|
|
||||||
|
def initialize_tracker(self): |
||||||
|
if not self.edf_open: |
||||||
|
raise RuntimeError('EDF file must be open before tracker can be initialized.') |
||||||
|
|
||||||
|
pl.flushGetkeyQueue() |
||||||
|
self.set_offline_mode() |
||||||
|
|
||||||
|
self.send_command("screen_pixel_coords = 0 0 %d %d" % self.resolution) |
||||||
|
self.send_message("DISPLAY_COORDS 0 0 %d %d" % self.resolution) |
||||||
|
|
||||||
|
#self.tracker.setFileEventFilter( |
||||||
|
# "LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON,INPUT") |
||||||
|
#self.tracker.setFileSampleFilter( |
||||||
|
# "LEFT,RIGHT,GAZE,AREA,GAZERES,STATUS") |
||||||
|
#self.tracker.setLinkEventFilter( |
||||||
|
# "LEFT,RIGHT,FIXATION,FIXUPDATE,SACCADE,BLINK,BUTTON,INPUT") |
||||||
|
#self.tracker.setLinkSampleFilter( |
||||||
|
# "LEFT,RIGHT,GAZE,GAZERES,AREA,STATUS") |
||||||
|
self.tracker.sendCommand("file_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON,INPUT") |
||||||
|
self.tracker.sendCommand("link_event_filter = LEFT,RIGHT,FIXATION,FIXUPDATE,SACCADE,BLINK,BUTTON,INPUT") |
||||||
|
self.tracker.sendCommand("file_sample_data = LEFT,RIGHT,GAZE,GAZERES,PUPIL,HREF,AREA,STATUS,HTARGET,INPUT") |
||||||
|
self.tracker.sendCommand("link_sample_data = LEFT,RIGHT,GAZE,GAZERES,PUPIL,HREF,AREA,STATUS,HTARGET,INPUT") |
||||||
|
|
||||||
|
def send_calibration_settings(self, settings=None): |
||||||
|
defaults = { |
||||||
|
'automatic_calibration_pacing': 1000, |
||||||
|
'background_color': (0, 0, 0), |
||||||
|
'calibration_area_proportion': (0.5, 0.5), |
||||||
|
'calibration_type': 'HV5', |
||||||
|
'elcl_configuration': 'BTABLER', |
||||||
|
'enable_automatic_calibration': 'YES', |
||||||
|
'error_sound': '', |
||||||
|
'foreground_color': (255, 255, 255), |
||||||
|
'good_sound': '', |
||||||
|
'preamble_text': None, |
||||||
|
'pupil_size_diameter': 'NO', |
||||||
|
'saccade_acceleration_threshold': 9500, |
||||||
|
'saccade_motion_threshold': 0.15, |
||||||
|
'saccade_pursuit_fixup': 60, |
||||||
|
'saccade_velocity_threshold': 30, |
||||||
|
'sample_rate': 1000, |
||||||
|
'target_sound': '', |
||||||
|
'validation_area_proportion': (0.5, 0.5), |
||||||
|
} |
||||||
|
|
||||||
|
if settings is None: |
||||||
|
settings = {} |
||||||
|
|
||||||
|
settings.update(defaults) |
||||||
|
|
||||||
|
self.send_command('elcl_select_configuration = %s' % settings['elcl_configuration']) |
||||||
|
|
||||||
|
pl.setCalibrationColors(settings['foreground_color'], settings['background_color']) |
||||||
|
pl.setCalibrationSounds( |
||||||
|
settings['target_sound'], settings['good_sound'], settings['error_sound']) |
||||||
|
|
||||||
|
if self.eye in ('LEFT', 'RIGHT'): |
||||||
|
self.send_command('active_eye = %s' % self.eye) |
||||||
|
|
||||||
|
self.send_command( |
||||||
|
'automatic_calibration_pacing = %i' % settings['automatic_calibration_pacing']) |
||||||
|
|
||||||
|
if self.eye == 'BOTH': |
||||||
|
self.send_command('binocular_enabled = YES') |
||||||
|
else: |
||||||
|
self.send_command('binocular_enabled = NO') |
||||||
|
|
||||||
|
#self.send_command( |
||||||
|
# 'calibration_area_proportion %f %f' % settings['calibration_area_proportion']) |
||||||
|
|
||||||
|
|
||||||
|
self.send_command('calibration_type = %s' % settings['calibration_type']) |
||||||
|
self.send_command( |
||||||
|
'enable_automatic_calibration = %s' % settings['enable_automatic_calibration']) |
||||||
|
if settings['preamble_text'] is not None: |
||||||
|
self.send_command('add_file_preamble_text %s' % '"' + settings['preamble_text'] + '"') |
||||||
|
self.send_command('pupil_size_diameter = %s' % settings['pupil_size_diameter']) |
||||||
|
#self.send_command( |
||||||
|
# 'saccade_acceleration_threshold = %i' % settings['saccade_acceleration_threshold']) |
||||||
|
#self.send_command('saccade_motion_threshold = %i' % settings['saccade_motion_threshold']) |
||||||
|
#self.send_command('saccade_pursuit_fixup = %i' % settings['saccade_pursuit_fixup']) |
||||||
|
#self.send_command( |
||||||
|
# 'saccade_velocity_threshold = %i' % settings['saccade_velocity_threshold']) |
||||||
|
self.send_command('sample_rate = %i' % settings['sample_rate']) |
||||||
|
#self.send_command( |
||||||
|
# 'validation_area_proportion %f %f' % settings['validation_area_proportion']) |
||||||
|
|
||||||
|
def open_edf(self): |
||||||
|
self.tracker.openDataFile(self.edf_filename) |
||||||
|
self.edf_open = True |
||||||
|
|
||||||
|
def close_edf(self): |
||||||
|
self.tracker.closeDataFile() |
||||||
|
self.edf_open = False |
||||||
|
|
||||||
|
def transfer_edf(self, newFilename=None): |
||||||
|
if not newFilename: |
||||||
|
newFilename = self.edf_filename |
||||||
|
|
||||||
|
# Prevents timeouts due to excessive printing |
||||||
|
sys.stdout = open(os.devnull, "w") |
||||||
|
self.tracker.receiveDataFile(self.edf_filename, newFilename) |
||||||
|
sys.stdout = sys.__stdout__ |
||||||
|
print(newFilename + ' has been transferred successfully.') |
||||||
|
|
||||||
|
def setup_tracker(self): |
||||||
|
self.window.flip() |
||||||
|
self.tracker.doTrackerSetup() |
||||||
|
|
||||||
|
def display_eyetracking_instructions(self): |
||||||
|
self.window.flip() |
||||||
|
|
||||||
|
psychopy.visual.Circle( |
||||||
|
self.window, units='pix', radius=18, lineColor='black', fillColor='white' |
||||||
|
).draw() |
||||||
|
psychopy.visual.Circle( |
||||||
|
self.window, units='pix', radius=6, lineColor='black', fillColor='black' |
||||||
|
).draw() |
||||||
|
|
||||||
|
psychopy.visual.TextStim( |
||||||
|
self.window, text='Sometimes a target that looks like this will appear.', |
||||||
|
color=self.text_color, units='norm', pos=(0, 0.22), height=0.05 |
||||||
|
).draw() |
||||||
|
|
||||||
|
psychopy.visual.TextStim( |
||||||
|
self.window, color=self.text_color, units='norm', pos=(0, -0.18), height=0.05, |
||||||
|
text='We use it to calibrate the eye tracker. Stare at it whenever you see it.' |
||||||
|
).draw() |
||||||
|
|
||||||
|
psychopy.visual.TextStim( |
||||||
|
self.window, color=self.text_color, units='norm', pos=(0, -0.28), height=0.05, |
||||||
|
text='Press any key to continue.' |
||||||
|
).draw() |
||||||
|
|
||||||
|
self.window.flip() |
||||||
|
psychopy.event.waitKeys() |
||||||
|
self.window.flip() |
||||||
|
|
||||||
|
def calibrate(self, text=None): |
||||||
|
self.window.flip() |
||||||
|
|
||||||
|
if text is None: |
||||||
|
text = ( |
||||||
|
'Experimenter:\n' |
||||||
|
'If you would like to calibrate, press space.\n' |
||||||
|
'To skip calibration, press the escape key.' |
||||||
|
) |
||||||
|
|
||||||
|
psychopy.visual.TextStim( |
||||||
|
self.window, text=text, pos=(0, 0), height=0.05, units='norm', color=self.text_color |
||||||
|
).draw() |
||||||
|
|
||||||
|
self.window.flip() |
||||||
|
|
||||||
|
keys = psychopy.event.waitKeys(keyList=['escape', 'space']) |
||||||
|
|
||||||
|
self.window.flip() |
||||||
|
|
||||||
|
if 'space' in keys: |
||||||
|
self.tracker.doTrackerSetup() |
||||||
|
|
||||||
|
def drift_correct(self, position=None, setup=1): |
||||||
|
if position is None: |
||||||
|
position = tuple([int(round(i/2)) for i in self.resolution]) |
||||||
|
|
||||||
|
try: |
||||||
|
self.tracker.doDriftCorrect(position[0], position[1], 1, setup) |
||||||
|
self.tracker.applyDriftCorrect() |
||||||
|
except RuntimeError as e: |
||||||
|
print(e.message) |
||||||
|
|
||||||
|
def record(self, trial_func): |
||||||
|
def wrapped_func(): |
||||||
|
self.start_recording() |
||||||
|
trial_func() |
||||||
|
self.stop_recording() |
||||||
|
return wrapped_func |
||||||
|
|
||||||
|
def start_recording(self): |
||||||
|
self.tracker.startRecording(1, 1, 1, 1) |
||||||
|
time.sleep(.1) # required |
||||||
|
|
||||||
|
def stop_recording(self): |
||||||
|
time.sleep(.1) # required |
||||||
|
self.tracker.stopRecording() |
||||||
|
|
||||||
|
@property |
||||||
|
def gaze_data(self): |
||||||
|
sample = self.tracker.getNewestSample() |
||||||
|
|
||||||
|
if self.eye == 'LEFT': |
||||||
|
return sample.getLeftEye().getGaze() |
||||||
|
elif self.eye == 'RIGHT': |
||||||
|
return sample.getRightEye().getGaze() |
||||||
|
else: |
||||||
|
return (sample.getLeftEye().getGaze(), sample.getRightEye().getGaze()) |
||||||
|
|
||||||
|
@property |
||||||
|
def pupil_size(self): |
||||||
|
sample = self.tracker.getNewestSample() |
||||||
|
|
||||||
|
if self.eye == 'LEFT': |
||||||
|
return sample.getLeftEye().getPupilSize() |
||||||
|
elif self.eye == 'RIGHT': |
||||||
|
return sample.getRightEye().getPupilSize() |
||||||
|
else: |
||||||
|
return (sample.getLeftEye().getPupilSize(), sample.getRightEye().getPupilSize()) |
||||||
|
|
||||||
|
def set_offline_mode(self): |
||||||
|
self.tracker.setOfflineMode() |
||||||
|
|
||||||
|
def send_command(self, cmd): |
||||||
|
self.tracker.sendCommand(cmd) |
||||||
|
|
||||||
|
def send_message(self, msg): |
||||||
|
self.tracker.sendMessage(msg) |
||||||
|
|
||||||
|
def send_status(self, status): |
||||||
|
if len(status) >= 80: |
||||||
|
print('Warning: Status should be less than 80 characters.') |
||||||
|
|
||||||
|
self.send_command("record_status_message '%s'" % status) |
||||||
|
|
||||||
|
def close_connection(self): |
||||||
|
self.tracker.close() |
||||||
|
pl.closeGraphics() |
@ -0,0 +1,362 @@ |
|||||||
|
"""Basic experiment class that is designed to be extended. |
||||||
|
|
||||||
|
Author - Michael Tan (mtan30@uic.edu) |
||||||
|
|
||||||
|
Adapted from: https://github.com/colinquirk/templateexperiments |
||||||
|
|
||||||
|
This class provides basic utility functions that are needed by all |
||||||
|
experiments. Specific experiment classes should inherit and extend/overwrite as needed. |
||||||
|
|
||||||
|
Functions: |
||||||
|
convert_color_value -- Converts a list of 3 values from 0 to 255 to -1 to 1. |
||||||
|
""" |
||||||
|
|
||||||
|
from __future__ import division |
||||||
|
from __future__ import print_function |
||||||
|
|
||||||
|
import os |
||||||
|
import pickle |
||||||
|
import sys |
||||||
|
|
||||||
|
import psychopy.monitors |
||||||
|
import psychopy.visual |
||||||
|
import psychopy.gui |
||||||
|
import psychopy.core |
||||||
|
import psychopy.event |
||||||
|
|
||||||
|
|
||||||
|
# Convenience |
||||||
|
def convert_color_value(color): |
||||||
|
"""Converts a list of 3 values from 0 to 255 to -1 to 1. |
||||||
|
|
||||||
|
Parameters: |
||||||
|
color -- A list of 3 ints between 0 and 255 to be converted. |
||||||
|
""" |
||||||
|
|
||||||
|
return [round(((n/127.5)-1), 2) for n in color] |
||||||
|
|
||||||
|
|
||||||
|
class BaseExperiment(object): |
||||||
|
"""Basic experiment class providing functionality in all experiments |
||||||
|
|
||||||
|
Parameters: |
||||||
|
bg_color -- list of 3 values (0-255) defining the background color |
||||||
|
data_fields -- list of strings defining data fields |
||||||
|
experiment_name -- string defining the experiment title |
||||||
|
monitor_distance -- int describing participant distance from monitor in cm |
||||||
|
monitor_name -- name of the monitor to be used |
||||||
|
monitor_px -- list containing monitor resolution (x,y) |
||||||
|
monitor_width -- int describing length of display monitor in cm |
||||||
|
|
||||||
|
Methods: |
||||||
|
display_text_screen -- draws a string centered on the screen. |
||||||
|
get_experiment_info_from_dialog -- gets subject info from a dialog box. |
||||||
|
open_csv_data_file -- opens a csv data file and writes the header. |
||||||
|
open_window -- open a psychopy window. |
||||||
|
quit_experiment -- ends the experiment. |
||||||
|
save_data_to_csv -- append new entries in experiment_data to csv data file. |
||||||
|
save_experiment_info -- write the info from the dialog box to a text file. |
||||||
|
save_experiment_pickle -- save a pickle so crashes can be recovered from. |
||||||
|
update_experiment_data -- extends any new data to the experiment_data list. |
||||||
|
""" |
||||||
|
|
||||||
|
def __init__(self, experiment_name, data_fields, bg_color=[0, 0, 0], |
||||||
|
monitor_name='testMonitor', monitor_width=59.5, |
||||||
|
monitor_distance=30, monitor_px=[2560,1440], **kwargs): |
||||||
|
"""Creates a new BaseExperiment object. |
||||||
|
|
||||||
|
Parameters: |
||||||
|
bg_color -- A list of 3 values between 0 and 255 defining the |
||||||
|
background color. |
||||||
|
data_fields -- list of strings containing the data fields to be stored |
||||||
|
experiment_name -- A string for the experiment title that also defines |
||||||
|
the filename the experiment info from the dialog box is saved to. |
||||||
|
monitor_distance -- An int describing the distance the participant sits |
||||||
|
from the monitor in cm (default 70). |
||||||
|
monitor_name -- The name of the monitor to be used |
||||||
|
Psychopy will search for the provided name to see if it was defined |
||||||
|
in monitor center. If it is not defined, a temporary monitor will |
||||||
|
be created. |
||||||
|
monitor_px -- A list containing the resolution of the monitor (x,y). |
||||||
|
monitor_width -- An int describing the length of the display monitor |
||||||
|
in cm (default 53). |
||||||
|
""" |
||||||
|
self.experiment_name = experiment_name |
||||||
|
self.data_fields = data_fields |
||||||
|
self.bg_color = convert_color_value(bg_color) |
||||||
|
self.monitor_name = monitor_name |
||||||
|
self.monitor_width = monitor_width |
||||||
|
self.monitor_distance = monitor_distance |
||||||
|
self.monitor_px = monitor_px |
||||||
|
|
||||||
|
self.experiment_data = [] |
||||||
|
self.experiment_data_filename = None |
||||||
|
self.data_lines_written = 0 |
||||||
|
self.experiment_info = {} |
||||||
|
self.experiment_window = None |
||||||
|
|
||||||
|
self.overwrite_ok = None |
||||||
|
|
||||||
|
self.experiment_monitor = psychopy.monitors.Monitor( |
||||||
|
self.monitor_name, width=self.monitor_width, |
||||||
|
distance=self.monitor_distance) |
||||||
|
self.experiment_monitor.setSizePix(monitor_px) |
||||||
|
|
||||||
|
vars(self).update(kwargs) # Add anything else you want |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def _confirm_overwrite(): |
||||||
|
"""Private, static method that shows a dialog asking if a file can be |
||||||
|
overwritten. |
||||||
|
|
||||||
|
Returns a bool describing if the file should be overwritten. |
||||||
|
""" |
||||||
|
|
||||||
|
overwrite_dlg = psychopy.gui.Dlg( |
||||||
|
'Overwrite?', labelButtonOK='Overwrite', |
||||||
|
labelButtonCancel='New File', screen=0) |
||||||
|
overwrite_dlg.addText('File already exists. Overwrite?') |
||||||
|
overwrite_dlg.show() |
||||||
|
|
||||||
|
return overwrite_dlg.OK |
||||||
|
|
||||||
|
def get_experiment_info_from_dialog(self, additional_fields_dict=None): |
||||||
|
"""Gets subject info from dialog box. |
||||||
|
|
||||||
|
Parameters: |
||||||
|
additional_fields_dict -- An optional dictionary containing more |
||||||
|
fields for the dialog box and output dictionary. |
||||||
|
""" |
||||||
|
|
||||||
|
self.experiment_info = {'Subject Identifier': 'XX00', |
||||||
|
'Age': '0', |
||||||
|
'Experimenter Initials': 'MT', |
||||||
|
} |
||||||
|
|
||||||
|
if additional_fields_dict is not None: |
||||||
|
self.experiment_info.update(additional_fields_dict) |
||||||
|
|
||||||
|
# Modifies experiment_info dict directly |
||||||
|
exp_info = psychopy.gui.DlgFromDict( |
||||||
|
self.experiment_info, title=self.experiment_name, |
||||||
|
order=['Subject Identifier', |
||||||
|
'Age', |
||||||
|
'Experimenter Initials', |
||||||
|
], |
||||||
|
screen=0 |
||||||
|
) |
||||||
|
|
||||||
|
return exp_info.OK |
||||||
|
|
||||||
|
def save_experiment_info(self, filename=None): |
||||||
|
"""Writes the info from the dialog box to a text file. |
||||||
|
|
||||||
|
Parameters: |
||||||
|
filename -- a string defining the filename with no extension |
||||||
|
""" |
||||||
|
|
||||||
|
if filename is None: |
||||||
|
filename = (self.experiment_name + '_' + |
||||||
|
self.experiment_info['Subject Identifier'].zfill(3) + |
||||||
|
'_info') |
||||||
|
elif filename[-4:] == '.txt': |
||||||
|
filename = filename[:-4] |
||||||
|
|
||||||
|
if os.path.isfile(filename + '.txt'): |
||||||
|
if self.overwrite_ok is None: |
||||||
|
self.overwrite_ok = self._confirm_overwrite() |
||||||
|
if not self.overwrite_ok: |
||||||
|
# If the file exists make a new filename |
||||||
|
i = 1 |
||||||
|
new_filename = filename + '(' + str(i) + ')' |
||||||
|
while os.path.isfile(new_filename + '.txt'): |
||||||
|
i += 1 |
||||||
|
new_filename = filename + '(' + str(i) + ')' |
||||||
|
filename = new_filename |
||||||
|
|
||||||
|
filename = filename + '.txt' |
||||||
|
|
||||||
|
with open(filename, 'w') as info_file: |
||||||
|
for key, value in self.experiment_info.items(): |
||||||
|
info_file.write(key + ':' + str(value) + '\n') |
||||||
|
info_file.write('\n') |
||||||
|
|
||||||
|
def open_csv_data_file(self, data_filename=None): |
||||||
|
"""Opens the csv file and writes the header. |
||||||
|
|
||||||
|
Parameters: |
||||||
|
data_filename -- name of the csv file with no extension |
||||||
|
(defaults to experimentname_subjectnumber). |
||||||
|
""" |
||||||
|
|
||||||
|
if data_filename is None: |
||||||
|
data_filename = (self.experiment_name + '_' + |
||||||
|
self.experiment_info['Subject Identifier'].zfill(3)) |
||||||
|
elif data_filename[-4:] == '.csv': |
||||||
|
data_filename = data_filename[:-4] |
||||||
|
|
||||||
|
if os.path.isfile(data_filename + '.csv'): |
||||||
|
if self.overwrite_ok is None: |
||||||
|
self.overwrite_ok = self._confirm_overwrite() |
||||||
|
if not self.overwrite_ok: |
||||||
|
# If the file exists and we can't overwrite make a new filename |
||||||
|
i = 1 |
||||||
|
new_filename = data_filename + '(' + str(i) + ')' |
||||||
|
while os.path.isfile(new_filename + '.csv'): |
||||||
|
i += 1 |
||||||
|
new_filename = data_filename + '(' + str(i) + ')' |
||||||
|
data_filename = new_filename |
||||||
|
|
||||||
|
self.experiment_data_filename = data_filename + '.csv' |
||||||
|
|
||||||
|
# Write the header |
||||||
|
with open(self.experiment_data_filename, 'w+') as data_file: |
||||||
|
for field in self.data_fields: |
||||||
|
data_file.write('"') |
||||||
|
data_file.write(field) |
||||||
|
data_file.write('"') |
||||||
|
if field != self.data_fields[-1]: |
||||||
|
data_file.write(',') |
||||||
|
data_file.write('\n') |
||||||
|
|
||||||
|
def update_experiment_data(self, new_data): |
||||||
|
"""Extends any new data to the experiment_data list. |
||||||
|
|
||||||
|
Parameters: |
||||||
|
new_data -- A list of dictionaries that are extended to |
||||||
|
experiment_data. Only keys that are included in data_fields should |
||||||
|
be included, as only those will be written in save_data_to_csv() |
||||||
|
""" |
||||||
|
|
||||||
|
self.experiment_data.extend(new_data) |
||||||
|
|
||||||
|
def save_data_to_csv(self): |
||||||
|
"""Opens the data file and appends new entries in experiment_data. |
||||||
|
|
||||||
|
Only appends lines (tracked by data_lines_written) that have not yet |
||||||
|
been written to the csv. |
||||||
|
|
||||||
|
Update the experiment data to be written with update_experiment_data. |
||||||
|
""" |
||||||
|
|
||||||
|
with open(self.experiment_data_filename, 'a') as data_file: |
||||||
|
for trial in range( |
||||||
|
self.data_lines_written, len(self.experiment_data)): |
||||||
|
for field in self.data_fields: |
||||||
|
data_file.write('"') |
||||||
|
try: |
||||||
|
data_file.write( |
||||||
|
str(self.experiment_data[trial][field])) |
||||||
|
except KeyError: |
||||||
|
data_file.write('NA') |
||||||
|
data_file.write('"') |
||||||
|
if field != self.data_fields[-1]: |
||||||
|
data_file.write(',') |
||||||
|
data_file.write('\n') |
||||||
|
|
||||||
|
self.data_lines_written = len(self.experiment_data) |
||||||
|
|
||||||
|
def save_experiment_pickle(self, additional_fields_dict=None): |
||||||
|
"""Saves the pickle containing the experiment data so that a crash can |
||||||
|
be recovered from. |
||||||
|
|
||||||
|
This method uses dict.update() so if any keys in the |
||||||
|
additional_fields_dict are in the default dictionary the new values |
||||||
|
will be used. |
||||||
|
|
||||||
|
Parameters: |
||||||
|
additional_fields_dict -- An optional dictionary that updates the |
||||||
|
dictionary that is saved in the pickle file. |
||||||
|
""" |
||||||
|
|
||||||
|
pickle_dict = { |
||||||
|
'experiment_name': self.experiment_name, |
||||||
|
'data_fields': self.data_fields, |
||||||
|
'bg_color': self.bg_color, |
||||||
|
'monitor_name': self.monitor_name, |
||||||
|
'monitor_width': self.monitor_width, |
||||||
|
'monitor_distance': self.monitor_distance, |
||||||
|
'experiment_data': self.experiment_data, |
||||||
|
'experiment_data_filename': self.experiment_data_filename, |
||||||
|
'data_lines_written': self.data_lines_written, |
||||||
|
'experiment_info': self.experiment_info, |
||||||
|
} |
||||||
|
|
||||||
|
if additional_fields_dict is not None: |
||||||
|
pickle_dict.update(additional_fields_dict) |
||||||
|
|
||||||
|
pickle.dump(pickle_dict, open( |
||||||
|
self.experiment_name + '_' + |
||||||
|
self.experiment_info['Subject Number'].zfill(3) + '.pickle', |
||||||
|
'wb+')) |
||||||
|
|
||||||
|
def open_window(self, **kwargs): |
||||||
|
"""Opens the psychopy window. |
||||||
|
|
||||||
|
Additional keyword arguments are sent to psychopy.visual.Window(). |
||||||
|
""" |
||||||
|
self.experiment_window = psychopy.visual.Window(self.monitor_px, |
||||||
|
monitor=self.experiment_monitor, fullscr=True, color=self.bg_color, |
||||||
|
colorSpace='rgb', units='deg', allowGUI=False, **kwargs) |
||||||
|
|
||||||
|
def display_text_screen( |
||||||
|
self, text='', text_color=[255, 255, 255], text_height=36, |
||||||
|
bg_color=None, wait_for_input=True, **kwargs): |
||||||
|
"""Takes a string as input and draws it centered on the screen. |
||||||
|
|
||||||
|
Allows for simple writing of text to a screen with a background color |
||||||
|
other than the normal one. Switches back to the default background |
||||||
|
color after any keyboard input. |
||||||
|
|
||||||
|
This works by drawing a rect on top of the background |
||||||
|
that fills the whole screen with the selected color. |
||||||
|
|
||||||
|
Parameters: |
||||||
|
text -- A string containing the text to be displayed. |
||||||
|
text_color -- A list of 3 values between 0 and 255 |
||||||
|
(default is [0, 0, 0]). |
||||||
|
text_height --- An int that defines the height of the text in pix. |
||||||
|
bg_color -- A list of 3 values between 0 and 255 (default is default |
||||||
|
background color). |
||||||
|
wait_for_input -- Bool that defines whether the screen will wait for |
||||||
|
keyboard input before continuing. If waiting for keys, a .5 second |
||||||
|
buffer is added to prevent accidental advancing. |
||||||
|
|
||||||
|
Additional keyword arguments are sent to psychopy.visual.TextStim(). |
||||||
|
""" |
||||||
|
|
||||||
|
if bg_color is None: |
||||||
|
bg_color = self.bg_color |
||||||
|
else: |
||||||
|
bg_color = convert_color_value(bg_color) |
||||||
|
|
||||||
|
backgroundRect = psychopy.visual.Rect( |
||||||
|
self.experiment_window, fillColor=bg_color, units='norm', width=2, |
||||||
|
height=2) |
||||||
|
|
||||||
|
text_color = convert_color_value(text_color) |
||||||
|
|
||||||
|
textObject = psychopy.visual.TextStim( |
||||||
|
self.experiment_window, text=text, color=text_color, units='pix', |
||||||
|
height=text_height, alignHoriz='center', alignVert='center', |
||||||
|
wrapWidth=round(.8*self.experiment_window.size[0]), **kwargs) |
||||||
|
|
||||||
|
backgroundRect.draw() |
||||||
|
textObject.draw() |
||||||
|
self.experiment_window.flip() |
||||||
|
|
||||||
|
keys = None |
||||||
|
|
||||||
|
if wait_for_input: |
||||||
|
psychopy.core.wait(.2) # Prevents accidental key presses |
||||||
|
keys = psychopy.event.waitKeys() |
||||||
|
self.experiment_window.flip() |
||||||
|
|
||||||
|
return keys |
||||||
|
|
||||||
|
def quit_experiment(self): |
||||||
|
"""Completes anything that must occur when the experiment ends.""" |
||||||
|
if self.experiment_window: |
||||||
|
self.experiment_window.close() |
||||||
|
print('The experiment has ended.') |
||||||
|
sys.exit(0) |
Loading…
Reference in new issue