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 # 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':
for freq in self.pursuit_frequencies:
trial = self.make_trial(self.pursuit_time, freq, 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 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_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
if not isinstance(stim_frequency, list):
stim_frequency = [stim_frequency]
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'])
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