cognitive science
and more
A bit about patches, textures, and masks in PsychoPy

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:

Overview

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