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.
480 lines
17 KiB
480 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.tools.monitorunittools |
|
|
|
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 |
|
|
|
if not condition=='Pursuit': |
|
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, tracker): |
|
"""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) |
|
|
|
tracker.send_message('Start Stim') |
|
for frameN in range(int(round(stim_time*60))): |
|
for s in stimList: |
|
s.draw() |
|
fixation.draw() |
|
stim.draw() |
|
self.experiment_window.flip() |
|
#print(psychopy.tools.monitorunittools.deg2pix(stim.pos, self.experiment_monitor)) |
|
tracker.send_message('End Stim') |
|
|
|
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() |
|
#print(psychopy.tools.monitorunittools.deg2pix(stim.pos, self.experiment_monitor)) |
|
|
|
# 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'], tracker) |
|
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
|
|
|