PsychoPy is a powerful Python library for creating the type of stimuli that are frequently used in psychological and neuroscientific experiments. I use it all the time, mostly from within OpenSesame, but I remember that I initially found working with PsychoPy quite daunting. This is because PsychoPy takes a very different approach to stimulus generation than most people are used to. You have to think in terms of patches, textures, and, masks, rather than in conventional drawing primitives, such as rectangles and lines (although newer versions of PsychoPy also support these drawing primitives). Therefore, I decided to write a short tutorial that explains the basics of working with PsychoPy.
In this tutorial, I will explain how to use textures and masks from the ground up. I will assume very little prior knowledge, except a basic understanding of Python. I will assume that you are running OpenSesame, which you can download from here, and comes bundled with all necessary Python libraries. The code snippets below can be pasted directly into an OpenSesame inline_script
item. You will probably want to insert a keyboard_response
after the inline_script
item that contains the code, so you that have the chance to see your stimuli before the experiment finishes!
For your convenience, you can download an OpenSesame template for this tutorial from here:
- Download OpenSesame template (Follow the link, click on download, and save as
.opensesame
file)
Overview
- Example 1: The basics
- Example 2: A simple texture
- A note on colors
- Example 3: A colorful texture
- Example 4: A mask
- Built-in textures and masks
Example 1: The basics
Let’s start with a simple example. If you have downloaded the template, simply paste this code into the inline_script
item called psychopy and quick-run the experiment (Control+Shift+W).
from psychopy.visual import GratingStim myStim = GratingStim(win, tex=None, mask=None, size=256) myStim.draw() win.flip()
If you run this example, you will simply see a white square on a black background:
To get the basics out of the way, let’s do a line-by-line dissection of this piece of code:
from psychopy.visual import GratingStim
This line imports the class GratingStim
, which is part of the module visual
, which is in turn part of the package psychopy
. We need to do this first, so that we can use GratingStim
later on.
myStim = GratingStim(win, tex=None, mask=None, size=256)
Now we create an object called myStim
, which is an instance of a GratingStim
class.
A small digression: The line above may appear a bit obscure, certainly if you are not familiar with object oriented programming. But for the purpose of this tutorial, a thorough understanding of classes and objects is not necessary (but see here). In a nutshell, the relationship between an object (here myStim
) and a class (here GratingStim
) is like the relationship between 3 and numbers: 3 is a specific instance of a number.
myStim.draw()
This line calls the draw()
method on the myStim
object. Perhaps somewhat surprisingly, this will not cause myStim
to appear on the display. What this does is draw myStim
to an internal buffer, which can be shown by calling the flip()
method on the PsychoPy window, which is called win
in OpenSesame …
win.flip()
… so, like this. This line will cause the white rectangle to actually appear on the display.
Example 2: A simple texture
A slightly more complex example shows how you can create a 2x2 grid with various shades of gray.
from psychopy.visual import GratingStim import numpy as np myTex = np.array([ [ 1, 0], [ 0,-1] ]) myStim = GratingStim(win, tex=myTex, mask=None, size=256) myStim.draw() win.flip()
The resulting stimulus looks like this:
There are a few new lines here, which warrant some explanation. The first is …
import numpy as np
… which imports the NumPy package and ‘renames’ it to np
. The reason for using as np
is simply one of convenience: np
is shorter than numpy
, so your code will require less typing. I do not generally recommend this way of importing, but since it’s quite common in the case of NumPy, I will follow this convention here as well.
So what is NumPy? It’s a Python package that offers powerful routines for numerical computations. The most important NumPy class is the array
, which is a 1-D (list), 2-D (matrix), or more-dimensional array of things, usually numbers. When working with PsychoPy, you will frequently need NumPy arrays.
More specifically, a texture must be:
- a NumPy
array
- 2 dimensional
- either 1 x N
- or N x N
- where N is a power of 2 (so 1, 2, 4, 8, etc.)
- 3 dimensional, which we will get to in Example 2
- 2 dimensional
- a text
string
, which we will get to in Example 5
Here we create the texture, which is a 2 x 2 NumPy array
, as follows:
myTex = np.array([ [ 1, 0], [ 0,-1] ])
The first line [ 1, 0]
is the bottom row, the second line [ 0, -1]
is the top row. This is slightly odd (you would expect the other way around), but has to do with the fact that PsychoPy uses mathematical coordinates, where high Y values are up, and low Y values are down. The number defines the color: 1
corresponds to white, 0
to gray, and -1
to black.
To use our newly created texture, we simply pass it with the keyword argument tex
to GratingStim
:
myStim = GratingStim(win, tex=myTex, mask=None, size=256)
A note on colors
Colors are a bit tricky in PsychoPy! Saying that 1
corresponds to white, and -1
to black is actually a (potentially misleading) oversimplification. In fact, 1
means the color of the patch, and -1
means the color that is opposite from that color. Because we did not specify a color in the example above, GratingStim
falls back to white, which is opposite from black. But let’s see what happens when we specify the color red:
myStim = GratingStim(win, tex=myTex, mask=None, size=256, color='red')
The resulting stimulus looks like this:
Were did the aqua come from!? This might be surprising, but can be explained if we consider the RGB (red, green, blue) structure of the color red:
red: yes (1) green: no (-1) blue: no (-1)
The opposite color of red is therefore:
red: no (-1) green: yes (1) blue: yes (1)
So the opposite of red is aqua (a mixture of blue and green), which explains the aqua color that you see in the image above.
Example 3: A colorful texture
Instead of using a single number to specify the color, you can also use 4 values to specify a color: These 4 values correspond to the red, green, blue, and alpha channel (=transparency) of the color. If we specify colors this way, a third color dimension is added to the texture. So in addition to the criteria described under Example 2, a texture can also be:
- 3 dimensional
- either 1 x N x 4
- or N x N x 4
- where N is a power of 2 (so 4, 8, 16, 32, etc.)
- where N is at least 4 (I don’t know why, but smaller values crash with my version of PsychoPy)
Let’s put this new knowledge to work:
from psychopy.visual import GratingStim import numpy as np myTex = np.random.random( (8,8,4) ) myStim = GratingStim(win, tex=myTex, mask=None, size=256) myStim.draw() win.flip()
The resulting stimulus looks (approximately) like this:
There’s a new line here …
myTex = np.random.random( (8,8,4) )
… which creates a random array
(i.e. an array filled with random values) that has three dimensions, the first (X coordinate) of length 8, the second (Y coordinate) also of length 8, and the fourth (color) of length 4. I could have written out an entire array
value-by-value, like I did before, but that would have taken too long!
Example 4: A mask
A mask is similar to a texture, but instead of color, it specifies transparency. You may recall that you can also specify transparency using a texture, using the RGBA notation explained in Example 3. Therefore, masks do not really add any new functionality. Still, it is often faster and more convenient to use a mask, rather than to rely purely on textures.
So let’s see a mask in action:
from psychopy.visual import GratingStim import numpy as np myMask = np.array([ [ 1, 0,-1, 0], [ 0,-1, 0, 1], [-1, 0, 1, 0], [ 0, 1, 0,-1] ]) myStim = GratingStim(win, tex=None, mask=myMask, size=256) myStim.draw() win.flip()
The resulting stimulus looks like this (I specified a blue background in OpenSesame, to illustrate that the mask really handles transparency):
This example contains a new line to define a mask myMask
…
myMask = np.array([ [ 1, 0,-1, 0], [ 0,-1, 0, 1], [-1, 0, 1, 0], [ 0, 1, 0,-1] ])
… which you may recognize as being very similar to the way that you define a texture. And indeed it is, the only difference is the interpretation: 1
is fully opaque, and -1
is fully transparent. A mask also has to meet the same criteria as a texture, with the exception that 3 dimensional masks are not allowed (see Example 2).
To use the mask, simply pass it to the GratingStim
using the mask
keyword:
myStim = GratingStim(win, tex=None, mask=myMask, size=256)
Built-in textures and masks
PsychoPy comes with a number of built-in textures and masks, which can be specified by passing a string as tex
or mask
keyword argument to GratingStim
. For example, this line will draw a sinusoid patch (note the sf
keyword argument, which specifies the spatial frequency):
myStim = GratingStim(win, tex='sin', mask=None, sf=.01, size=256)
There are four of these built-in textures: sin
for a sinusoid, sqr
for a square wave, saw
for a sawtooth wave, and tri
for triangular wave:
(Source: http://pastebin.com/Phe2C7np)
Similarly, there are three built in masks: gauss
for a Gaussian envelope, raisedCos
for a raised cosine envelope (essentially a fuzzy circular patch), and circle
for a circular envelope:
(Source: http://pastebin.com/VzbnqmXM)
The point to note here is that these built-in textures and masks don’t do anything magical, although they are very convenient. You could get the same effects with a little clever NumPy wizardry!
That’s it for this tutorial. You now have the basic knowledge to get started creating your own fancy, sciency-looking stimuli. Here are three reference sites that are good to have among your bookmarks: