You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

473 lines
17 KiB

"""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.sound
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',
]
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,
new_trial_sound,
questionaire_dict=None,
questionaire_order=None,
**kwargs):
self.new_trial_sound = new_trial_sound
# 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':
counter1 = 0
for freq in self.pursuit_frequencies:
trial = self.make_trial(self.pursuit_time[counter1], freq, condition)
trial_list.append(trial)
counter1 += 1
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 any key 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_dot_color,
lineColor=self.saccade_dot_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_dot_color,
lineColor=self.saccade_dot_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
if not isinstance(stim_frequency, list):
stim_frequency = [stim_frequency]
if not isinstance(stim_time, list):
stim_time = [stim_time]
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, Key: %s' % (direction, response[0])])
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'])
psychopy.sound.Sound(self.new_trial_sound).play()
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'])
psychopy.sound.Sound(self.new_trial_sound).play()
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