Back to the main index
Part of the introductory series to using Python for Vision Research brought to you by the GestaltReVision group (KU Leuven, Belgium).
In this part we will capitalize on the basics you learned in Part 1 to build a real working experiment.
Authors: Maarten Demeyer, Jonas Kubilius
Year: 2014
Copyright: Public Domain as in CC0 (images used in this part are thanks to Unsplash but are in Public Domain too)
PsychoPy is a package which implements for you a bunch of useful functions for running psychological experiments, divided into several modules:
These modules will allow us to implement the missing components:
PsychoPy is a powerful stimulus generation and experimental control software, meaning that you need equally powerful hardware for it to function properly. Issues often appear when using an old machine, old (integrated) graphics card or when display drivers are not up-to-date. We will now run a quick system check (takes about half a minute) to see where you are (open a web report at the end of it; issues will be indicated in red):
%run ../check_config.py
4.4992 WARNING User requested fullscreen with size [800 600], but screen is actually [1920, 1200]. Using actual size 6.9827 WARNING Could not raise thread priority with sched_setscheduler. To enable rush(), if you are using a debian-based linux, try this in a terminal window: 'sudo setcap cap_sys_nice=eip /usr/bin/python' [NB: You may need to install 'setcap' first.] If you are using the system's python (eg /usr/bin/python2.x), its highly recommended to change cap_sys_nice back to normal afterwards: 'sudo setcap cap_sys_nice= /usr/bin/python' 8.2164 WARNING Could not raise thread priority with sched_setscheduler. To enable rush(), if you are using a debian-based linux, try this in a terminal window: 'sudo setcap cap_sys_nice=eip /usr/bin/python' [NB: You may need to install 'setcap' first.] If you are using the system's python (eg /usr/bin/python2.x), its highly recommended to change cap_sys_nice back to normal afterwards: 'sudo setcap cap_sys_nice= /usr/bin/python' 13.1987 WARNING t of last frame was 166.69ms (=1/5) 15.2987 WARNING t of last frame was 33.29ms (=1/30) 16.3487 WARNING t of last frame was 33.29ms (=1/30) 17.3988 WARNING t of last frame was 33.32ms (=1/30) 18.4487 WARNING Multiple dropped frames have occurred - I'll stop bothering you about them! benchmark version: 0.1 full-screen: True dots_circle: 700 dots_square: 1400 available memory: 1098M psychopy: 1.80.06 locale: en_US.UTF-8 python version: 2.7.6 (64bit) wx: 2.8.12.1 (gtk2-unicode) pyglet: 1.1.4 rush: True openGL version: 3.0 Mesa 10.1.3 openGL vendor: X.Org screen size: 1920 x 1200 have shaders: True visual sync (refresh): 16.67 ms/frame refresh stability (SD): 2.16 ms no dropped frames: 0 / 180 pyglet avbin: 7 openGL max vertices: 3000 GL_ARB_multitexture: True GL_EXT_framebuffer_object: True GL_ARB_fragment_program: True GL_ARB_shader_objects: True GL_ARB_vertex_shader: True GL_ARB_texture_non_power_of_two: True GL_ARB_texture_float: True GL_STEREO: False pyo: 0.6.8 microphone latency: 0.0058 s speakers latency: 0.0239 s flac: (missing) numpy: 1.8.1 scipy: 0.13.3 matplotlib: 1.3.1 platform: linux 3.13.0-24-generic internet access: True auto proxy: True background processes: Dropbox ... OpenSSL: 1.0.1f 6 Jan 2014 CPU speed test: 0.008 s PIL: 1.1.7 openpyxl: 1.7.0 lxml: import ok setuptools: 3.3 pytest: -- sphinx: 1.2.2 psignifit: -- pyserial: 2.6 pp: 1.6.4 pynetstation: import ok ioLabs: -- labjack: --
Some problems are not important for this course (such as microphone latency) but others (e.g., outdated drivers) will be very visible throughout the course. You may be unable to draw certain stimuli or have them show up incorrectly.
When you run an experiment, the first thing you probably want to do is to collect some information about the participant, such as participant ID or session number. PsychoPy provides a very simple interface for that via its gui module:
from psychopy import gui
exp_name = 'Change Detection'
exp_info = {
'participant': '',
'gender': ('male', 'female'),
'age':'',
'left-handed':False
}
dlg = gui.DlgFromDict(dictionary=exp_info, title=exp_name)
print exp_info
Notice how you can make drop-down lists and tickboxes. As you can see, the exp_info
variable gets updated after you enter information in the Dialog Box.
Now try rerunning that code snippet again, provide the participant ID again, but now click Cancel. What happens to exp_info
? You'll see that exp_info
is still updated. This is not a desired outcome so we need to improve our code to quit the experiment if Cancel is clicked.
from psychopy import gui, core
exp_name = 'Change Detection'
exp_info = {
'participant': '',
'gender': ('male', 'female'),
'age':'',
'left-handed':False
}
dlg = gui.DlgFromDict(dictionary=exp_info, title=exp_name)
if dlg.OK == False:
core.quit() # user pressed cancel, so we quit
Since exp_info
is a simple dict, you can store any information you like in it, e.g.:
exp_info['exp_name'] = exp_name
You may also want to record date and time when the experiment took place. PsychoPy provides a simple date stamp in its data module:
from psychopy import data
exp_info['date'] = data.getDateStr()
All stimuli in your experiment will be displayed on a single window. You could have more windows if you like (for a stereo setup, for example) but usually we only need one window which is defined by a Window class in PsychoPy's visual module. Below is how you open a window but don't run this cell:
# DON'T RUN THIS
from psychopy import visual
visual.Window() # don't run this line
Had you gone ahead and opened a window right now, you would have seen it wouldn't close nicely. In fact, given its purpose to show stimuli, the window is expected to be created, displayed for a certain period of time, and then close. To define its duration, we can use the wait function from PsychoPy's core module. After this time is up, we simply close the window:
from psychopy import visual, core
win = visual.Window()
core.wait(3) # seconds
win.close()
Windows have several parameters that we can manipulate. Try running the following examples and observed what is changing.
win = visual.Window(size=(600,400), color='white')
core.wait(3)
win.close()
win = visual.Window(fullscr=True)
core.wait(3)
win.close()
One final useful parameter is units
which controls the units you use to define stimuli size. In this experiment, we simply pass units='pix'
which means that (by default) you define stimulus size in pixels. If you fail to define units, there may be problems later in drawing stimuli. However, often we want to define stimuli in terms of their size in degrees visual angle. In that case, we would give units='deg'
to the Window class. This not sufficient though because PsychoPy needs to know the size of your screen and participant's distance from it. These parameters are stored in the Monitor Center and can be accessed by creating a monitor. We will not cover this procedure here but you can read more about units here.
PsychoPy allows to draw many kinds of stimuli: geometric shapes, images, gratings, element arrays, text, and even movies. You can see all possibilities listed in the visual module; in this tutorial, we will draw images, text and circles.
In general, you first create an object for each kind of stimulus, and as you go into experimental loop, their properties are manipulated to show different stimuli on each trial. Note how this is a different strategy from creating one object per stimulus we want to show. In our experiment, for example, we have 12 images to show. We could create 12 image objects, each storing a different image, but that's inefficient and inelegant. In fact, we only need one bitmap object defined upfront, and we will be updating which image is shown by updating the image file path of this bitmap object.
Let's draw an image on our window. Images are displayed using ImageStim At the very least, you need to supply two arguments to create an image instance: the window where the image has to be drawn and the path to the image file:
from psychopy import visual, core
win = visual.Window(size=(600,400), color='white', units='pix')
bitmap = visual.ImageStim(win, 'images/1a.jpg', size=(600,400))
core.wait(3)
win.close()
But where is the image? The thing is that the bitmap
object was created but it has yet to be drawn on the window for us to see.
win = visual.Window(size=(600,400), color='white', units='pix')
bitmap = visual.ImageStim(win, 'images/1a.jpg', size=(600,400))
bitmap.draw()
core.wait(3)
win.close()
Still nothing... Here is one more thing to learn: stimuli are drawn on the back buffer and this buffer screen needs to be brought to the front to be seen. Think about it as always having two screens: front (the one you a currently seeing) and back (the one drawn in the background and not yet seen). What you want to do is to flip the back screen up front, which is conveniently done using win.flip()
command.
(Wondering why we need these two buffers? The idea is to increase the performance. Drawing stimuli might take a long time but flipping between drawn screens is fast. Here PsychoPy cleverly allows you to draw next stimuli while the current ones are still on the screen.)
win = visual.Window(size=(600,400), color='white', units='pix')
bitmap = visual.ImageStim(win, 'images/1a.jpg', size=(600,400))
bitmap.draw() # draw bitmap on the window
win.flip() # make bitmap visible
core.wait(3)
win.close()
You can draw various geometric shapes using PsychoPy's ShapeStim class. However, several common shapes are available immediately; here is an example of drawing a black circle which we need to create the bubbles in our experiment:
win = visual.Window(size=(600,400), color='white', units='pix')
bubble = visual.Circle(win, fillColor='black', lineColor='black', radius=30)
bubble.draw()
win.flip()
core.wait(3) # seconds
win.close()
Text stimulus is created using the TextStim class. It behaves just like other stimuli but, of course, font size and so on can be manipulated:
win = visual.Window(size=(600,400), color='white', units='pix')
text = visual.TextStim(win, text='Press spacebar to start the trial', color='red', height=20)
text.draw()
win.flip()
core.wait(3) # seconds
win.close()
from psychopy import visual, core
win = visual.Window(size=(600,400), color='white', units='pix')
bitmap = visual.ImageStim(win, 'images/1a.jpg', size=(600,400))
bitmap.draw() # draw bitmap on the window
win.flip() # make bitmap visible
core.wait(3)
bitmap.setImage('images/2a.jpg')
bitmap.setOri(180)
bitmap.draw()
win.flip()
core.wait(3)
win.close()
You could, of course, just define a new stimulus (i.e., bitmap = visual.SimpleImageStim(win, 'images/2a.jpg'
) but this is slower and therefore not recommended in actual experiments.
Now that we have a text stimulus on the screen that says "Press space to start the trial", let's learn how to register user input. PsychoPy provides a simple interface to wait for a key press in the event module:
from psychopy import visual, core, event
win = visual.Window(size=(600,400), color='white', units='pix')
text = visual.TextStim(win, text='Press spacebar to start the trial', color='red', height=20)
text.draw()
win.flip()
keys = event.waitKeys(keyList=['space', 'escape'])
print keys
if 'escape' in keys:
win.close()
else:
print 'Start of the trial'
win.close()
PsychoPy has been built with precise timing control in mind (see also a recent report in PLOS ONE claiming otherwise and PsychoPy's community showing that this is not the case).
There are two ways to control timing in PsychoPy: clock-based and frame-based.
PsychoPy provides a core.Clock
class for a very easy control of timing. You simply define a clock and use the reset()
command to reset it to zero and, effectively, start counting. You can check how much time passed by calling the getTime()
command. So we can rewrite the code above using core.wait(3)
in the following manner:
from psychopy import visual, core
win = visual.Window(size=(600,400), color='white', units='pix')
bitmap = visual.ImageStim(win, 'images/1a.jpg', size=(600,400))
# define and start a clock
rt_clock = core.Clock()
rt_clock.reset()
# this is equivallent to core.wait(3)
while rt_clock.getTime() < 3:
bitmap.draw() # draw bitmap on the window
win.flip() # make bitmap visible
win.close()
At this point there is little reason to do so because we are not changing anything about the stimulus while it is on the screen. Usually, however, either we are updating something (stimulus position, color etc) or at least recording user input, and this routine becomes important.
Another way to control timing is to present stimuli for a specified number of frames. Why frames? You should understand that the computer monitor does not update information immediately. Rather, images on the screen are updated at each refresh period. If you have a typical monitor of 60 Hz, that means it is updating every 1000/60 = 16.667 ms. That's just how often you can change something on a screen. The implication is that you cannot present a stimulus for 20 or 25 ms, for example. Not good your taste? Get a better monitor, like 100 Hz.
Because of this, many experiments that need short stimulus durations rely on frame-based timing. The basic implementation is like this:
from psychopy import visual, core
nframes = 12 # will see stimulus for 12 frames, i.e., 200 ms on most monitors
win = visual.Window(size=(600,400), color='white', units='pix')
bitmap = visual.ImageStim(win, 'images/1a.jpg', size=(600,400))
core.wait(2) # blank screen initially
for frame in range(nframes):
bitmap.draw() # draw bitmap on the window
win.flip() # make bitmap visible
win.flip()
core.wait(2) # blank screen afterwards
win.close()
It depends on your specific experiment. For long stimulus presentation times (hundreds of miliseconds), it's okay to use core.Clock
because you probably don't need milisecond precision and it's more user-friendly. But for short durations (tens of miliseconds) or when you care about precise timing you should go with frame-based timing.
Another point to consider is a slightly different behavior of the two. Clock-based timing will always end exactly the specified amount of miliseconds after it started. Frame-based timing, on the other hand, will present the exact number of frames that you asked for. The more frames you ask for, the more likely it is that some will be dropped, meaning that several frames will be drawn not a for a single duration of a frame (e.g., 16.667 ms) but for twice as long. Since the amount of frames is fixed, the total duration of a trial will consequently be longer by that amount. This is unlikely to happen when you have only several frames presented but if you try to present stimulus for 8 sec using frame-based timing, you may get slightly longer stimulus durations. In many cases, that doesn't matter. But in fMRI experiments where stimulus onset must be time-locked to the onset of a scanner sequence, this may lead to an unacceptable desynchronization.
During the trial, we want to have the images flip back and forth continuously, so we need a loop:
from psychopy import visual, core
scrsize = (600,400)
win = visual.Window(size=scrsize, color='white', units='pix')
bitmap1 = visual.ImageStim(win, 'images/1a.jpg', size=scrsize)
bitmap2 = visual.ImageStim(win, 'images/1b.jpg', size=scrsize)
for i in range(5):
bitmap1.draw() # draw bitmap on the window
win.flip() # make bitmap visible
core.wait(.5)
bitmap2.draw()
win.flip()
core.wait(.5)
win.close()
However, notice that we are not very efficient here: many commands are implemented twice. We can do better, and should always strive for more elegance in Python:
from psychopy import visual, core
scrsize = (600,400)
win = visual.Window(size=scrsize, color='white', units='pix')
bitmap1 = visual.ImageStim(win, 'images/1a.jpg', size=scrsize)
bitmap2 = visual.ImageStim(win, 'images/1b.jpg', size=scrsize)
bitmap = bitmap1
for i in range(10): # note that we now need 10, not 5 iterations
# change the bitmap
if bitmap == bitmap1:
bitmap = bitmap2
else:
bitmap = bitmap1
bitmap.draw()
win.flip()
core.wait(.5)
win.close()
So far we had the loop continue for a fixed number of iterations. However, the actual experiment requires participants to hit a space bar to indicate that they saw a change, meaning that the stimuli have to be flipping until the response and not for a fixed number of times. So that's a while loop. Also, the trial should stop after 30 seconds if no response was made. Thus, we need to implement two things: input registration and clocks to count time.
For input registration and timing, remember that we used event.waitKeys
before. We can try to use it here as well.
Putting these ideas together we arrive to the following code:
from psychopy import visual, core, event
scrsize = (600,400)
win = visual.Window(size=scrsize, color='white', units='pix')
bitmap1 = visual.ImageStim(win, 'images/1a.jpg', size=scrsize)
bitmap2 = visual.ImageStim(win, 'images/1b.jpg', size=scrsize)
bitmap = bitmap1
# Initialize clock to register response time
rt_clock = core.Clock()
rt_clock.reset() # set rt clock to 0
done = False
# Empty the keypresses list
keys = None
# Start the trial
# Stop trial if spacebar or escape has been pressed, or if 30s have passed
while keys is None and rt_clock.getTime() < 30:
# Switch the image
if bitmap == bitmap1:
bitmap = bitmap2
else:
bitmap = bitmap1
bitmap.draw()
# Show the new screen we've drawn
win.flip()
# For 0.5s, listen for a spacebar or escape press
keys = event.waitKeys(maxWait=.5, keyList=['space', 'escape'], timeStamped=rt_clock)
print keys
win.close()
Note how we added timeStamped=rt_clock
in event.waitKeys
. The effect is that the returned keys also contain time when they were pressed according to the particular clock we supplied.
There is also an alternative for recording user inputs using a while loop:
from psychopy import visual, core, event
scrsize = (600,400)
win = visual.Window(size=scrsize, color='white', units='pix')
bitmap1 = visual.ImageStim(win, image='images/1a.jpg', size=scrsize)
bitmap2 = visual.ImageStim(win, image='images/1b.jpg', size=scrsize)
bitmap = bitmap1
# Initialize clock to register response time
rt_clock = core.Clock()
rt_clock.reset() # set rt clock to 0
# Initialize clock to control stimulus presentation time
change_clock = core.Clock()
# Empty the keypresses list
keys = []
# Start the trial
# Stop trial if spacebar or escape has been pressed, or if 30s have passed
while len(keys) == 0 and rt_clock.getTime() < 30:
# Switch the image
if bitmap == bitmap1:
bitmap = bitmap2
else:
bitmap = bitmap1
bitmap.draw()
# Show the new screen we've drawn
win.flip()
# For 0.5s, listen for a spacebar or escape press
change_clock.reset()
while change_clock.getTime() <=.5:
keys = event.getKeys(keyList=['space', 'escape'])
print keys
if len(keys) > 0:
rt = rt_clock.getTime()
break
print keys, rt
win.close()
This approach is useful in scenarios where trials occur non-stop, i.e. there is no waiting for user response. For example, in fMRI experiments, trial onsets must be time-locked such that their onsets are in sync with scanner. In such paradigms we want to make sure that a trial ends just in time. Now you may think that it does so with event.waitKeys too (since we set maxWait=.5
) but the harsh truth is that this waiting starts only after stimuli are drawn. So if it took the computer 20 ms to draw the stimuli, the total trial duration will be 520 ms instead of 500 ms. These tiny offsets accumulate quickly so one idea is to constantly check trial and global timing using these while-loops and end as soon as necessary as counted from the beginning of a trial. So I find the while-loop approach to be more robust so in our experiment we use this method.
Now we also need to put some bubbles on top of the image. Also, these bubbles should be changing their size and positions, so here is the bit that controls these parameters (you'll have to insert that in the while loop in the snippet above if you want to test it):
# Draw bubbles of increasing radius at random positions
for radius in range(n_bubbles):
bubble.setRadius(radius/2.)
bubble.setPos(((rnd.random()-.5) * scrsize[0],
(rnd.random()-.5) * scrsize[1] ))
bubble.draw()
At the end of a trial, we check if the participant actually responded (the keys
list shouldn't be empty). If not, that means the time limit was reached.
# Analyze the keypress
if keys:
if 'escape' in keys:
# Escape press = quit the experiment
break
else:
# Spacebar press = correct change detection; register response time
acc = 1
rt = rt_clock.getTime()
else:
# No press = failed change detection; maximal response time
acc = 0
rt = timelimit
Finally, we have to wrap this into a trial loop:
# Display the start message
start_message.draw()
win.flip()
# Start the main loop that goes through all trials
for trial in trials:
# Wait for spacebar press to start (or escape to quit)
keys = event.waitKeys(keyList=['space', 'escape'])
# Set the images, set the orientation
im_fname = os.path.join(impath, trial['im'])
bitmap1.setImage(im_fname + asfx)
bitmap1.setFlipHoriz(trial['ori'])
bitmap2.setImage(im_fname + bsfx)
bitmap2.setFlipHoriz(trial['ori'])
# Show stimuli, collect responses
# ...
Below is the full code for the experiment with the discussed PsychoPy functions added to the Code advancement 3
%load script_final.py
#===============
# Import modules
#===============
import os # for file/folder operations
import numpy.random as rnd # for random number generators
from psychopy import visual, event, core, gui, data
#==============================================
# Settings that we might want to tweak later on
#==============================================
datapath = 'data' # directory to save data in
impath = 'images' # directory where images can be found
imlist = ['1','2','3','4','5','6'] # image names without the suffixes
asfx = 'a.jpg' # suffix for the first image
bsfx = 'b.jpg' # suffix for the second image
scrsize = (600,400) # screen size in pixels
timelimit = 30 # image freezing time in seconds
changetime = .5 # image changing time in seconds
n_bubbles = 40 # number of bubbles overlayed on the image
#========================================
# Store info about the experiment session
#========================================
# Get subject name, gender, age, handedness through a dialog box
exp_name = 'Change Detection'
exp_info = {
'participant': '',
'gender': ('male', 'female'),
'age':'',
'left-handed':False
}
dlg = gui.DlgFromDict(dictionary=exp_info, title=exp_name)
# If 'Cancel' is pressed, quit
if dlg.OK == False:
core.quit()
# Get date and time
exp_info['date'] = data.getDateStr()
exp_info['exp_name'] = exp_name
# Create a unique filename for the experiment data
if not os.path.isdir(datapath):
os.makedirs(datapath)
data_fname = exp_info['participant'] + '_' + exp_info['date']
data_fname = os.path.join(datapath, data_fname)
#========================
# Prepare condition lists
#========================
# Check if all images exist
for im in imlist:
if (not os.path.exists(os.path.join(impath, im+asfx)) or
not os.path.exists(os.path.join(impath, im+bsfx))):
raise Exception('Image files not found in image folder: ' + str(im))
# Randomize the image order
rnd.shuffle(imlist)
# Create the orientations list: half upright, half inverted (rotated by 180 deg)
orilist = [0,180]*(len(imlist)/2)
# Randomize the orientation order
rnd.shuffle(orilist)
#===============================
# Creation of window and stimuli
#===============================
# Open a window
win = visual.Window(size=scrsize, color='white', units='pix', fullscr=False)
# Define trial start text
start_message = visual.TextStim(win,
text="Press spacebar to start the trial. Hit spacebar again when you detect a change.",
color='red', height=20)
# Define bitmap stimulus (contents can still change)
bitmap1 = visual.ImageStim(win, size=scrsize)
bitmap2 = visual.ImageStim(win, size=scrsize)
# Define a bubble (position and size can still change)
bubble = visual.Circle(win, fillColor='black', lineColor='black')
#==========================
# Define the trial sequence
#==========================
# Define a list of trials with their properties:
# - Which image (without the suffix)
# - Which orientation
stim_order = []
for im, ori in zip(imlist, orilist):
stim_order.append({'im': im, 'ori': ori})
trials = data.TrialHandler(stim_order, nReps=1, extraInfo=exp_info,
method='sequential', originPath=datapath)
#=====================
# Start the experiment
#=====================
# Initialize two clocks:
# - for image change time
# - for response time
change_clock = core.Clock()
rt_clock = core.Clock()
# Run through the trials
for trial in trials:
# Display trial start text
start_message.draw()
win.flip()
# Wait for a spacebar press to start the trial, or escape to quit
keys = event.waitKeys(keyList=['space', 'escape'])
# Set the images, set the orientation
im_fname = os.path.join(impath, trial['im'])
bitmap1.setImage(im_fname + asfx)
bitmap1.setOri(trial['ori'])
bitmap2.setImage(im_fname + bsfx)
bitmap2.setOri(trial['ori'])
bitmap = bitmap1
# Set the clocks to 0
change_clock.reset()
rt_clock.reset()
# Empty the keypresses list
# Leave an 'escape' press in for immediate exit
if 'space' in keys:
keys = []
# Start the trial
# Stop trial if spacebar or escape has been pressed, or if 30s have passed
while not keys and rt_clock.getTime() < timelimit:
# Switch the image
if bitmap == bitmap1:
bitmap = bitmap2
else:
bitmap = bitmap1
bitmap.draw()
# Draw bubbles of increasing radius at random positions
for radius in range(n_bubbles):
bubble.setRadius(radius/2.)
bubble.setPos(((rnd.random()-.5) * scrsize[0],
(rnd.random()-.5) * scrsize[1] ))
bubble.draw()
# Show the new screen we've drawn
win.flip()
# For the duration of 'changetime',
# Listen for a spacebar or escape press
change_clock.reset()
while change_clock.getTime() <= changetime:
keys = event.getKeys(keyList=['space','escape'])
if keys:
break
# Analyze the keypress
if keys:
if 'escape' in keys:
# Escape press = quit the experiment
break
else:
# Spacebar press = correct change detection; register response time
acc = 1
rt = rt_clock.getTime()
else:
# No press = failed change detection; maximal response time
acc = 0
rt = timelimit
# Add the current trial's data to the TrialHandler
trials.addData('rt', rt)
trials.addData('acc', acc)
# Advance to the next trial
#======================
# End of the experiment
#======================
# Save all data to a file
trials.saveAsWideText(data_fname + '.csv', delim=',')
# Quit the experiment
win.close()
An exception has occurred, use %tb to see the full traceback. SystemExit: 0
To exit: use 'exit', 'quit', or Ctrl-D.
#===============
# Import modules
#===============
import os # for file/folder operations
import numpy.random as rnd # for random number generators
from psychopy import visual, event, core, gui, data
#==============================================
# Settings that we might want to tweak later on
#==============================================
datapath = 'data' # directory to save data in
impath = 'images' # directory where images can be found
imlist = ['1','2','3','4','5','6'] # image names without the suffixes
asfx = 'a.jpg' # suffix for the first image
bsfx = 'b.jpg' # suffix for the second image
scrsize = (600,400) # screen size in pixels
timelimit = 30 # image freezing time in seconds
changetime = .5 # image changing time in seconds
n_bubbles = 40 # number of bubbles overlayed on the image
#========================================
# Store info about the experiment session
#========================================
# Get subject name, gender, age, handedness through a dialog box
exp_name = 'Change Detection'
exp_info = {
'participant': '',
'gender': ('male', 'female'),
'age':'',
'left-handed':False
}
dlg = gui.DlgFromDict(dictionary=exp_info, title=exp_name)
# If 'Cancel' is pressed, quit
if dlg.OK == False:
core.quit()
# Get date and time
exp_info['date'] = data.getDateStr()
exp_info['exp_name'] = exp_name
# Create a unique filename for the experiment data
if not os.path.isdir(datapath):
os.makedirs(datapath)
data_fname = exp_info['participant'] + '_' + exp_info['date']
data_fname = os.path.join(datapath, data_fname)
#========================
# Prepare condition lists
#========================
# Check if all images exist
for im in imlist:
if (not os.path.exists(os.path.join(impath, im+asfx)) or
not os.path.exists(os.path.join(impath, im+bsfx))):
raise Exception('Image files not found in image folder: ' + str(im))
# Randomize the image order
rnd.shuffle(imlist)
# Create the orientations list: half upright, half inverted (rotated by 180 deg)
orilist = [0,180]*(len(imlist)/2)
# Randomize the orientation order
rnd.shuffle(orilist)
#===============================
# Creation of window and stimuli
#===============================
# Open a window
win = visual.Window(size=scrsize, color='white', units='pix', fullscr=False)
# Define trial start text
start_message = visual.TextStim(win,
text="Press spacebar to start the trial. Hit spacebar again when you detect a change.",
color='red', height=20)
# Define bitmap stimulus (contents can still change)
bitmap1 = visual.ImageStim(win, size=scrsize)
bitmap2 = visual.ImageStim(win, size=scrsize)
# Define a bubble (position and size can still change)
bubble = visual.Circle(win, fillColor='black', lineColor='black')
#==========================
# Define the trial sequence
#==========================
# Define a list of trials with their properties:
# - Which image (without the suffix)
# - Which orientation
stim_order = []
for im, ori in zip(imlist, orilist):
stim_order.append({'im': im, 'ori': ori})
trials = data.TrialHandler(stim_order, nReps=1, extraInfo=exp_info,
method='sequential', originPath=datapath)
#=====================
# Start the experiment
#=====================
# Initialize two clocks:
# - for image change time
# - for response time
change_clock = core.Clock()
rt_clock = core.Clock()
# Run through the trials
for trial in trials:
# Display trial start text
start_message.draw()
win.flip()
# Wait for a spacebar press to start the trial, or escape to quit
keys = event.waitKeys(keyList=['space', 'escape'])
# Set the images, set the orientation
im_fname = os.path.join(impath, trial['im'])
bitmap1.setImage(im_fname + asfx)
bitmap1.setOri(trial['ori'])
bitmap2.setImage(im_fname + bsfx)
bitmap2.setOri(trial['ori'])
bitmap = bitmap1
# Set the clocks to 0
change_clock.reset()
rt_clock.reset()
# Empty the keypresses list
# Leave an 'escape' press in for immediate exit
if 'space' in keys:
keys = []
# Start the trial
# Stop trial if spacebar or escape has been pressed, or if 30s have passed
while len(keys) == 0 and rt_clock.getTime() < timelimit:
# Switch the image
if bitmap == bitmap1:
bitmap = bitmap2
else:
bitmap = bitmap1
bitmap.draw()
# Draw bubbles of increasing radius at random positions
for radius in range(n_bubbles):
bubble.setRadius(radius/2.)
bubble.setPos(((rnd.random()-.5) * scrsize[0],
(rnd.random()-.5) * scrsize[1] ))
bubble.draw()
# Show the new screen we've drawn
win.flip()
# For the duration of 'changetime',
# Listen for a spacebar or escape press
change_clock.reset()
while change_clock.getTime() <= changetime:
keys = event.getKeys(keyList=['space','escape'])
if len(keys) > 0:
break
# Analyze the keypress
if keys:
if 'escape' in keys:
# Escape press = quit the experiment
break
else:
# Spacebar press = correct change detection; register response time
acc = 1
rt = rt_clock.getTime()
else:
# No press = failed change detection; maximal response time
acc = 0
rt = timelimit
# Add the current trial's data to the TrialHandler
trials.addData('rt', rt)
trials.addData('acc', acc)
# Advance to the next trial
#======================
# End of the experiment
#======================
# Save all data to a file
trials.saveAsWideText(data_fname + '.csv', delim=',')
# Quit the experiment
win.close()
If you ran the entire experiment, you can find the data in the data folder. It is in a 'csv' (comma-separated value) file and you may not know what to do with it.
An easy thing to do is to open it with Excel and use Data > Split Text into Columns.
But if you want to analyze data in Python, you can too! We'll talk more about that in Part 5 (or read more on our wiki) but here's a preview how to read in your data using pandas
:
import glob
import pandas
datafile = glob.glob('data/*.csv')[0]
print datafile
df = pandas.read_csv(datafile)
df