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, you will gain more experience with using PsychoPy for various stimulus drawing tasks.
Author: Jonas Kubilius
Year: 2014
Copyright: Public Domain as in CC0
(you'll have to rerun this cell every time the kernel dies)
import numpy as np
from psychopy import visual, core, event, monitors
class psychopy.visual.TextStim(win, text='Hello World', font='', ...)
. If you imported the relevant PsychoPy modules with the command above, then Python knows what visual
is. So you initialize the TextStim
as: visual.TextStim(...)
.Hit spacebar
? Then visual.TextStim(win, text='Hit spacebar')
will suffice. Notice that there are two types of parameters: normal arguments (win
) and keyword arguments (text=...
, pos=...
). Normal arguments are always required in order to call an objects. Keyword arguments are optional because they have a default value which is used unless you pass a different value.Window
written in the PsychoPy documentation, that means you have to call it exactly like that and not window
or WiNdOW
. The convention is that classes start with a capital letter and may have some more capital letters mixed in (e.g., TextStim()
), while the rest is in lowercase (e.g., flip()
). In PsychoPy, one unconventional thing is that functions usually have some capital letters, like waitKeys()
. For your own scripts, try to stick to lowercase, like show_stimuli()
.Draw a red rectangle on white background and show it until a key is pressed.
No idea what to do, right? Basically, you have to fill in the blanks and ellipses:
# open a window
win = visual.Window(color='white')
# create a red rectangle stimulus
rect = visual.Rect(win, size=(.5,.3), fillColor='red')
# draw the stimulus
rect.draw()
# flip the window
win.flip()
# wait for a key response
event.waitKeys()
# close window
win.close()
OK, but you can't remember any PsychoPy commands? Me neither. I use the online documentation to help me out.
Tip: Can't close the window? Restart the kernel (Kernel > Restart). Remember to reimport all packages that are listed at the top of this notebook after restart.
win = visual.Window(color='white')
rect = visual.Rect(win, width=.5, height=.3, fillColor='red')
rect.draw()
win.flip() # don't forget to flip when done with drawing all stimuli so that the stimuli become visible
event.waitKeys()
win.close()
Tip. Notice the fillColor
keyword. Make sure you understand why we use this and not just color
. Check out the explanation of colors and color spaces.
Draw a red circle on white background and show it until a keys is pressed.
Oh, that sounds trivial now? Just change Rectangle to Circle? Well, try it:
win = visual.Window(color='white')
circle = visual.Circle(win, radius=.4, fillColor='red')
circle.draw()
win.flip() # don't forget to flip when done with drawing all stimuli so that the stimuli become visible
event.waitKeys()
win.close()
And oops, you get an ellipse (at least I do). Why?
By default, PsychoPy is using normalized ('norm') units that are proportional to the window. Since the window is rectangular, everything gets distorted horizontally. In order to keep aspect ratio sane, use 'height' units. Read more here
win = visual.Window(color='white', units='height')
circle = visual.Circle(win, radius=.4, fillColor='red')
circle.draw()
win.flip() # don't forget to flip when done with drawing all stimuli so that the stimuli become visible
event.waitKeys()
win.close()
Usually, however, people use 'deg' units that allow defining stimuli in terms of their size in visual angle. However, to be able to use visual angle, you first have to define you monitor parameters: resolution, width, and viewing distance. (You can also apply gamma correction etc.)
mon = monitors.Monitor('My screen', width=37.5, distance=57)
mon.setSizePix((1280,1024))
win = visual.Window(color='white', units='deg', monitor=mon)
circle = visual.Circle(win, radius=.4, fillColor='red')
circle.draw()
win.flip() # don't forget to flip when done with drawing all stimuli so that the stimuli become visible
event.waitKeys()
win.close()
Pro Tip. Hate specifying monitor resolution manually? (Note that wx is messed up and next time you run this snippet it's not gonna work because 'app' is somehow already there... just rename app to app2 then.)
import wx
app = wx.App(False) # create an app if there isn't one and don't show it
nmons = wx.Display.GetCount() # how many monitors we have
mon_sizes = [wx.Display(i).GetGeometry().GetSize() for i in range(nmons)]
print mon_sizes
Draw a fixation cross, a radial stimulus on the left (like used in fMRI for retinotopic mapping) and a gabor patch on the left all on the default ugly gray background.
Oh no, how do you make a gabor patch? And a radial stimulus? Something like that was in Part 3 so are we going to do the same? Well, think a bit. Chances are that other people needed these kind of stimulus in the past. Maybe PsychoPy has them built-in?
mon = monitors.Monitor('My screen', width=37.5, distance=57)
mon.setSizePix((1280,1024))
win = visual.Window(units='deg', monitor=mon)
# make stimuli
fix_hor = visual.Line(win, start=(-.3, 0), end=(.3, 0), lineWidth=3)
fix_ver = visual.Line(win, start=(0, -.3), end=(0, .3), lineWidth=3)
radial = visual.RadialStim(win, mask='gauss',size=(3,3), pos=(-4, 0))
gabor = visual.GratingStim(win, mask='gauss', size=(3,3), pos=(4, 0))
# draw stimuli
fix_hor.draw()
fix_ver.draw()
radial.draw()
gabor.draw()
win.flip()
event.waitKeys()
win.close()
PsychoPy has been around for long enough to be a stable package. However, it is still evolving and bugs may occur. Some of them are quite complex but others are something you can easily fix as long as you're not afraid of getting your hand dirty. You shouldn't be, and I'll illustrate that with the following example:
Draw the same shapes as before but this time make the fixation cross black.
So that should be a piece of cake, right? According to the documentation, simply adding color='black'
to the LineStim should do the trick. Go ahead ant try it:
mon = monitors.Monitor('My screen', width=37.5, distance=57)
mon.setSizePix((1280,1024))
win = visual.Window(units='deg', monitor=mon)
# make stimulic
fix_hor = visual.Line(win, start=(-.3, 0), end=(.3, 0), lineColor='black')
fix_ver = visual.Line(win, start=(0, -.3), end=(0, .3), lineColor='black')
radial = visual.RadialStim(win, size=(3,3), pos=(-4, 0))
gabor = visual.GratingStim(win, mask='gauss', size=(3,3), pos=(4, 0))
# draw stimuli
fix_hor.draw()
fix_ver.draw()
radial.draw()
gabor.draw()
win.flip()
event.waitKeys()
win.close()
You should get an error along the lines of
TypeError: __init__() got an unexpected keyword argument 'color'
*(Because of this error, the window remains open -- simply restart the kernel to kill it, and reimport all modules at Quick setup.)
So now what? You need that black fixation cross real bad. Notice the error message tells you the whole hierarchy of how the problem came about:
fix_hor = visual.Line(win, start=(-.3, 0), end=(.3, 0), color='black')
-- clearly due to the color
keyword cause it used to work beforeShapeStim.__init__(self, win, **kwargs)
and that raised an error.If you were to check out ShapeStim's documentation, you'd see that ShapeStim only accepts fillColor
and lineColor
but not color
keywords (even though later in the documentation it seems as if there were a color
keyword too -- yet another bug).
OK, so if you don't care, just use lineColor='black'
and it will do the job.
However, consider that Jon Peirce and other people has put lots of love in creating PsychoPy. If you find something not working, why not let them know? You can easily report bugs on Psychopy's GitHub repo or, if you're not confident there is a bug, just post it on the Psychopy's help forum.
But the best of all is trying to fix it yourself, and reporting the bug together with a fix. That way you help not only yourself, but also many other users. Let's see if we can fix this one. First, notice that the problem is that ShapeStim does not recognize the color
keyword. We are not going to mess with ShapeStim because it has fillColor
and lineColor
for a reason. So instead we can modify Line to accept this keyword. So -- open up the file where Line is defined and change it. In my case, this is C:\Miniconda32\envs\psychopy\lib\site-packages\psychopy\visual\line.py
.
Simply insert color=None
in def __init__()
(line 21 in my case), and kwargs['lineColor'] = color
just below kwargs['fillColor'] = None
(line 50) and self.color = self.lineColor
right after calling ShapeStim.__init__()
(line 51). That's it! Just restart the kernel in this notebook, reimport all packages at the top (to update them with this change), and run the code above again. That should run now.
Note that this is not the full fix yet because we still need to include colorSpace
keyword and also functions such as setColor
and setColorSpace
, and there may be yet other compactibility issues to verify. But for our modest purposes, it's fixed!
Let it be a lesson for you as well about the whole idea behind open source -- if something is not working, just open the source file and fix it. You're in control here. Now go ahead and fix a bug in your Windows or OS X.
Advanced. The proper way to submit you fixes is by forking the repo, making a patch, and submitting a pull request, as explained on GitHub's help.
In this exercise, we will create the famous Hinton's "Lilac Chaser". The display consists of 12 equally-spaced blurry pink dots on a larger circle (on a light gray background). Dots are disappearing one after another to create an illusion of a green dot moving.
If your math is a bit rusty at the moment, here is how to find the coordinates for placing the pink dots on a circle:
r = 5 # radius of the big circle
ncircles = 12
angle = 2 * np.pi / ncircles # angle between two pink dots in radians
for i in range(ncircles):
pos = (r*np.cos(angle*i), r*np.sin(angle*i))
Draw 12 equally-spaced dots on a larger circle. (Don't worry about making them blurry for now.) Also make a fixation spot.
mon = monitors.Monitor('My screen', width=37.5, distance=57)
mon.setSizePix((1280,1024))
win = visual.Window(color='lightgray', units='deg', monitor=mon)
# draw a fixation
fix = visual.Circle(win, radius=.1, fillColor='black')
fix.draw()
r = 5 # radius of the larger circle
ncircles = 12
angle = 2 * np.pi / ncircles
# make and draw stimuli
for i in range(ncircles):
pos = (r*np.cos(angle*i), r*np.sin(angle*i))
circle = visual.Circle(win, radius=.5, fillColor='purple', lineColor='purple', pos=pos)
circle.draw()
win.flip()
event.waitKeys()
win.close()
Make dots disappear one at a time.
mon = monitors.Monitor('My screen', width=37.5, distance=57)
mon.setSizePix((1280,1024))
win = visual.Window(color='lightgray', units='deg', monitor=mon)
# make a fixation
fix = visual.Circle(win, radius=.1, fillColor='black')
r = 5 # radius of the larger circle
ncircles = 12
angle = 2 * np.pi / ncircles
# make and draw stimuli
dis = 0 # which one will disappear
while len(event.getKeys()) == 0:
for i in range(ncircles):
if i != dis:
pos = (r*np.cos(angle*i), r*np.sin(angle*i))
circle = visual.Circle(win, radius=.5, fillColor='purple', lineColor='purple', pos=pos)
circle.draw()
dis = (dis + 1) % ncircles
fix.draw()
win.flip()
core.wait(.1)
win.close()
Optimize your code; make dots blurry.
mon = monitors.Monitor('My screen', width=37.5, distance=57)
mon.setSizePix((1280,1024))
win = visual.Window(color='lightgray', units='deg', monitor=mon)
r = 5 # radius of the larger circle
ncircles = 12
angle = 2 * np.pi / ncircles
# make a stimuli
fix = visual.Circle(win, radius=.1, fillColor='black')
circle = visual.GratingStim(win, size=(2,2), tex=None, mask='gauss', color='purple')
# make and draw stimuli
dis = 0 # which one will disappear
while len(event.getKeys()) == 0:
for i in range(ncircles):
if i != dis:
pos = (r*np.cos(angle*i), r*np.sin(angle*i))
circle.setPos(pos)
circle.draw()
dis = (dis + 1) % ncircles
fix.draw()
win.flip()
core.wait(.1)
win.close()
import site; print site.getsitepackages()
Cite PsychoPy in your papers (at least one of the folllowing):