{ "metadata": { "name": "", "signature": "sha256:433dde522e009da353d2ead527b2cb86418a2269f85867c5d61650df2d519e68" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "*Back to the main [index](../index.ipynb)*" ] }, { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "Practice with PsychoPy" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Part of the introductory series to using [Python for Vision Research](http://gestaltrevision.be/wiki/python/python) brought to you by the [GestaltReVision](http://gestaltrevision.be) group (KU Leuven, Belgium).*\n", "\n", "In this part, you will gain more experience with using PsychoPy for various stimulus drawing tasks.\n", "\n", "**Author:** [Jonas Kubilius](http://klab.lt) \n", "**Year:** 2014 \n", "**Copyright:** Public Domain as in [CC0](https://creativecommons.org/publicdomain/zero/1.0/)" ] }, { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "Contents" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- [Quick setup](#Quick-setup)\n", "- [Exercise 1:](#Exercise-1) Drawing a rectangle\n", "- [Exercise 2:](#Exercise-2:-Japanese-flag) A Japanese flag\n", "- [Exercise 3:](#Exercise-3) Radial and gabor stimuli\n", "- [Exercise 4:](#Exercise-4:-Hinton's-\"Lilac-Chaser\") Hinton's \"Lilac Chaser\"\n", "- [Resources](#Resources)" ] }, { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "Quick setup" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "(you'll have to rerun this cell every time the kernel dies)" ] }, { "cell_type": "code", "collapsed": false, "input": [ "import numpy as np\n", "from psychopy import visual, core, event, monitors" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "General tips" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "1. Break down the task into concrete steps of what you need to do.\n", "2. For each step, look up the classes and functions you need in the PsychoPy [Documentation](http://www.psychopy.org/api/api.html).\n", "3. Once you find the relevant object, you need to call it properly. Suppose you need the [TextStim](http://www.psychopy.org/api/visual/textstim.html#psychopy.visual.TextStim). You will see in documentation that is it defined like this: `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(...)`.\n", "4. Use the absolute minimum parameters necessary to initialize an object. Want a text `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.\n", "5. Also notice that object names are case-sensitive. If you see `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()`." ] }, { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "Exercise 1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Draw a red rectangle on white background and show it until a key is pressed.*" ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Information" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "No idea what to do, right? Basically, you have to fill in the blanks and ellipses:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "# open a window\n", "win = visual.Window(color='white')\n", "# create a red rectangle stimulus\n", "rect = visual.Rect(win, size=(.5,.3), fillColor='red')\n", "# draw the stimulus\n", "rect.draw()\n", "# flip the window\n", "win.flip()\n", "# wait for a key response\n", "event.waitKeys()\n", "# close window\n", "win.close()" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "OK, but you can't remember any PsychoPy commands? Me neither. I use [the online documentation](http://www.psychopy.org/api/visual.html) to help me out.\n", "\n", "*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." ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Solution" ] }, { "cell_type": "code", "collapsed": false, "input": [ "win = visual.Window(color='white')\n", "rect = visual.Rect(win, width=.5, height=.3, fillColor='red')\n", "rect.draw() \n", "win.flip() # don't forget to flip when done with drawing all stimuli so that the stimuli become visible\n", "event.waitKeys()\n", "win.close()" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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](http://www.psychopy.org/general/colours.html)." ] }, { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "Exercise 2: Japanese flag" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Draw a red circle on white background and show it until a keys is pressed.*" ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Information" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Oh, that sounds trivial now? Just change Rectangle to Circle? Well, try it:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "win = visual.Window(color='white')\n", "circle = visual.Circle(win, radius=.4, fillColor='red')\n", "circle.draw()\n", "win.flip() # don't forget to flip when done with drawing all stimuli so that the stimuli become visible\n", "event.waitKeys()\n", "win.close()" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And oops, you get an ellipse (at least I do). Why?" ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Solution" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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](http://www.psychopy.org/general/units.html)" ] }, { "cell_type": "code", "collapsed": false, "input": [ "win = visual.Window(color='white', units='height')\n", "circle = visual.Circle(win, radius=.4, fillColor='red')\n", "circle.draw()\n", "win.flip() # don't forget to flip when done with drawing all stimuli so that the stimuli become visible\n", "event.waitKeys()\n", "win.close()" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.)" ] }, { "cell_type": "code", "collapsed": false, "input": [ "mon = monitors.Monitor('My screen', width=37.5, distance=57)\n", "mon.setSizePix((1280,1024))\n", "win = visual.Window(color='white', units='deg', monitor=mon)\n", "circle = visual.Circle(win, radius=.4, fillColor='red')\n", "circle.draw()\n", "win.flip() # don't forget to flip when done with drawing all stimuli so that the stimuli become visible\n", "event.waitKeys()\n", "win.close()" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**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.)" ] }, { "cell_type": "code", "collapsed": false, "input": [ "import wx\n", "app = wx.App(False) # create an app if there isn't one and don't show it\n", "nmons = wx.Display.GetCount() # how many monitors we have\n", "mon_sizes = [wx.Display(i).GetGeometry().GetSize() for i in range(nmons)]\n", "print mon_sizes" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "Exercise 3" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*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.*" ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Information" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Oh no, how do you make a gabor patch? And a radial stimulus? Something like that was in [Part 3](#A-bit-harder:-The-Gabor) 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?" ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Solution" ] }, { "cell_type": "code", "collapsed": false, "input": [ "mon = monitors.Monitor('My screen', width=37.5, distance=57)\n", "mon.setSizePix((1280,1024))\n", "win = visual.Window(units='deg', monitor=mon)\n", "\n", "# make stimuli\n", "fix_hor = visual.Line(win, start=(-.3, 0), end=(.3, 0), lineWidth=3)\n", "fix_ver = visual.Line(win, start=(0, -.3), end=(0, .3), lineWidth=3)\n", "radial = visual.RadialStim(win, mask='gauss',size=(3,3), pos=(-4, 0))\n", "gabor = visual.GratingStim(win, mask='gauss', size=(3,3), pos=(4, 0))\n", "\n", "# draw stimuli\n", "fix_hor.draw()\n", "fix_ver.draw()\n", "radial.draw()\n", "gabor.draw()\n", "\n", "win.flip()\n", "event.waitKeys()\n", "win.close()" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Follow-up: PsychoPy is not perfect yet" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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:\n", "\n", "*Draw the same shapes as before but this time make the fixation cross black.*\n", "\n", "So that should be a piece of cake, right? [According to the documentation](http://www.psychopy.org/api/visual/line.html#psychopy.visual.Line.color), simply adding ``color='black'`` to the LineStim should do the trick. Go ahead ant try it:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "mon = monitors.Monitor('My screen', width=37.5, distance=57)\n", "mon.setSizePix((1280,1024))\n", "win = visual.Window(units='deg', monitor=mon)\n", "\n", "# make stimulic\n", "fix_hor = visual.Line(win, start=(-.3, 0), end=(.3, 0), lineColor='black')\n", "fix_ver = visual.Line(win, start=(0, -.3), end=(0, .3), lineColor='black')\n", "radial = visual.RadialStim(win, size=(3,3), pos=(-4, 0))\n", "gabor = visual.GratingStim(win, mask='gauss', size=(3,3), pos=(4, 0))\n", "\n", "# draw stimuli\n", "fix_hor.draw()\n", "fix_ver.draw()\n", "radial.draw()\n", "gabor.draw()\n", "\n", "win.flip()\n", "event.waitKeys()\n", "win.close()" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You should get an error along the lines of\n", "\n", "``TypeError: __init__() got an unexpected keyword argument 'color'``\n", "\n", "*(Because of this error, the window remains open -- simply restart the kernel to kill it, and reimport all modules at [Quick setup](#Quick-setup).)\n", "\n", "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:\n", "\n", "- it started with ``fix_hor = visual.Line(win, start=(-.3, 0), end=(.3, 0), color='black')`` -- clearly due to the ``color`` keyword cause it used to work before\n", "- which called ``ShapeStim.__init__(self, win, **kwargs)`` and that raised an error.\n", "\n", "If you were to check out [ShapeStim's documentation](http://www.psychopy.org/api/visual/shapestim.html), 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).\n", "\n", "OK, so if you don't care, just use ``lineColor='black'`` and it will do the job.\n", "\n", "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](https://github.com/psychopy/psychopy/issues) or, if you're not confident there is a bug, just post it on the [Psychopy's help forum](http://groups.google.com/group/psychopy-users).\n", "\n", "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``.\n", "\n", "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.\n", "\n", "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!\n", "\n", "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.\n", "\n", "**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](https://help.github.com/articles/using-pull-requests)." ] }, { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "Exercise 4: Hinton's \"Lilac Chaser\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*In this exercise, we will create the famous [Hinton's \"Lilac Chaser\"](http://michaelbach.de/ot/col-lilacChaser/index.html). 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.*" ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Hints" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "r = 5 # radius of the big circle\n", "ncircles = 12\n", "angle = 2 * np.pi / ncircles # angle between two pink dots in radians\n", "\n", "for i in range(ncircles): \n", " pos = (r*np.cos(angle*i), r*np.sin(angle*i))" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Part 1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Draw 12 equally-spaced dots on a larger circle. (Don't worry about making them blurry for now.) Also make a fixation spot.*" ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Solution" ] }, { "cell_type": "code", "collapsed": false, "input": [ "mon = monitors.Monitor('My screen', width=37.5, distance=57)\n", "mon.setSizePix((1280,1024))\n", "win = visual.Window(color='lightgray', units='deg', monitor=mon)\n", "\n", "# draw a fixation\n", "fix = visual.Circle(win, radius=.1, fillColor='black')\n", "fix.draw()\n", "\n", "r = 5 # radius of the larger circle\n", "ncircles = 12\n", "angle = 2 * np.pi / ncircles\n", "\n", "# make and draw stimuli\n", "for i in range(ncircles):\n", " pos = (r*np.cos(angle*i), r*np.sin(angle*i))\n", " circle = visual.Circle(win, radius=.5, fillColor='purple', lineColor='purple', pos=pos)\n", " circle.draw()\n", "\n", "win.flip()\n", "event.waitKeys()\n", "win.close()" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Part 2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Make dots disappear one at a time.*" ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Solution" ] }, { "cell_type": "code", "collapsed": true, "input": [ "mon = monitors.Monitor('My screen', width=37.5, distance=57)\n", "mon.setSizePix((1280,1024))\n", "win = visual.Window(color='lightgray', units='deg', monitor=mon)\n", "\n", "# make a fixation\n", "fix = visual.Circle(win, radius=.1, fillColor='black')\n", "\n", "r = 5 # radius of the larger circle\n", "ncircles = 12\n", "angle = 2 * np.pi / ncircles\n", "\n", "# make and draw stimuli\n", "dis = 0 # which one will disappear\n", "\n", "while len(event.getKeys()) == 0: \n", " for i in range(ncircles):\n", " if i != dis:\n", " pos = (r*np.cos(angle*i), r*np.sin(angle*i))\n", " circle = visual.Circle(win, radius=.5, fillColor='purple', lineColor='purple', pos=pos)\n", " circle.draw()\n", " dis = (dis + 1) % ncircles\n", " fix.draw()\n", " win.flip()\n", " core.wait(.1)\n", " \n", "win.close()" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Part 3 (advanced)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Optimize your code; make dots blurry.*" ] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Solution" ] }, { "cell_type": "code", "collapsed": false, "input": [ "mon = monitors.Monitor('My screen', width=37.5, distance=57)\n", "mon.setSizePix((1280,1024))\n", "win = visual.Window(color='lightgray', units='deg', monitor=mon)\n", "\n", "r = 5 # radius of the larger circle\n", "ncircles = 12\n", "angle = 2 * np.pi / ncircles\n", "\n", "# make a stimuli\n", "fix = visual.Circle(win, radius=.1, fillColor='black')\n", "circle = visual.GratingStim(win, size=(2,2), tex=None, mask='gauss', color='purple')\n", "\n", "# make and draw stimuli\n", "dis = 0 # which one will disappear\n", "\n", "while len(event.getKeys()) == 0: \n", " for i in range(ncircles):\n", " if i != dis:\n", " pos = (r*np.cos(angle*i), r*np.sin(angle*i))\n", " circle.setPos(pos)\n", " circle.draw()\n", " dis = (dis + 1) % ncircles\n", " fix.draw()\n", " win.flip()\n", " core.wait(.1)\n", " \n", "win.close()" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "Resources" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- [PsychoPy](http://www.psychopy.org/)\n", "- [Documentation](http://www.psychopy.org/api/api.html)\n", "- [Help forum](http://groups.google.com/group/psychopy-users)\n", "- Where are all my packages? ``import site; print site.getsitepackages()``\n", "- [GitHub repository](https://github.com/psychopy/psychopy) with the latest (but unstable) version of Psychopy where bugs might have been fixed\n", "- [Report bugs](https://github.com/psychopy/psychopy/issues)\n", "- Cite PsychoPy in your papers (at least one of the folllowing):\n", "\n", " * Peirce, JW (2007) PsychoPy - Psychophysics software in Python. J Neurosci Methods, 162(1-2):8-13\n", " * Peirce JW (2009) Generating stimuli for neuroscience using PsychoPy. Front. Neuroinform. 2:10. doi:10.3389/neuro.11.010.2008\n", "\n" ] } ], "metadata": {} } ] }