Back to the main index

Introduction to PsychoPy for creating experiments

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)

Contents

Introduction

PsychoPy is a package which implements for you a bunch of useful functions for running psychological experiments, divided into several modules:

  • core: various functions
  • data: handles condition parameters, response registration, and order of trials
  • visual: handles the drawing of stimuli to the screen
  • event: handles keyboard input
  • gui: handles the creation of dialog boxes
  • ...and more, but we will not cover them here

These modules will allow us to implement the missing components:

  • Show the session info input dialog
  • Create a window
  • Define and manipulate the stimuli
  • Define a trial loop (TrialHandler)
  • Draw stimuli onto the screen
  • Control the timing
  • Register the keyboard input
  • Quit the experiment from anywhere

What you need

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):

In [1]:
%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.

Input dialog

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:

In [ ]:
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.

In [ ]:
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.:

In [ ]:
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:

In [ ]:
from psychopy import data

exp_info['date'] = data.getDateStr()

Window

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:

In [ ]:
# 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:

In [ ]:
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.

In [ ]:
win = visual.Window(size=(600,400), color='white')
core.wait(3)
win.close()
In [ ]:
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.

Stimuli

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.

Image

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:

In [ ]:
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.

In [ ]:
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.)

In [ ]:
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()

Geometric shapes

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:

In [ ]:
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

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:

In [ ]:
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()

Updating stimulus properties

Before each trial we need to be able to update images and their orientations. This is generally done using `set` commands, such as `setImage` and `setOri` in our example:
In [ ]:
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.

Keyboard input

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:

In [ ]:
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()

Timing

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.

Clock-based timing

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:

In [ ]:
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.

Frame-based timing

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:

In [ ]:
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()

Which one to use?

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.

Running experiment

During the trial

During the trial, we want to have the images flip back and forth continuously, so we need a loop:

In [ ]:
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:

In [ ]:
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()

Get participant input and response time

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:

In [ ]:
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:

In [ ]:
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.

Draw bubbles

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):

In [ ]:
# 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()

End of trial

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.

In [ ]:
# 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

Trial loop

Finally, we have to wrap this into a trial loop:

In [ ]:
# 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
    # ...

Code advancement: Final

Below is the full code for the experiment with the discussed PsychoPy functions added to the Code advancement 3

In [3]:
%load script_final.py
In [ ]:
#===============
# 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()
In [2]:
 
An exception has occurred, use %tb to see the full traceback.

SystemExit: 0
To exit: use 'exit', 'quit', or Ctrl-D.
In [ ]:
#===============
# 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()

Peaking at data

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:

In [ ]:
import glob
import pandas

datafile = glob.glob('data/*.csv')[0]
print datafile
df = pandas.read_csv(datafile)
df