commit
e87e4f79b8
5 changed files with 1837 additions and 0 deletions
@ -0,0 +1,366 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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