Psychopy

From Lupyan Lab
Jump to: navigation, search

Contents

Psychopy Reference

Please use the official psychopy reference page for basic info. Below is some extra functionality that you will find useful.

A simple function for popping up an error using a gui window

def popupError(text):
	errorDlg = gui.Dlg(title="Error", pos=(200,400))
	errorDlg.addText('Error: '+text, color='Red')
	errorDlg.show()

Popping up a box to get user input e.g., to specify a condition name

First, make sure gui is one of the things you're importing from psychopy (if it's already listed at the top of the file, you don't need to re-import it)

 from psychopy import gui

Here's the simplest code:

 userVar = {'Name':'Enter your name'}
 dlg = gui.DlgFromDict(userVar)

You can now access the value of the 'Name' variable using the following syntax:

userVar['Name']

For example, if I enter 'Gary' in the pop-up box. Then once I press enter and the box closes,

userVar['Name'] 

will return the string 'Gary'

Note that when the box pops up it will be pre-filled with "Enter your name". To overwrite it, just hit tab until it's highlights. Or position your cursor inside it, hit command-A (ctrl-A on PC) which will highlight all the text, and then start typing. For a more elegant text box, see below.

A different way of showing a text box

myDlg = gui.Dlg()
myDlg.addText('Enter your name')
myDlg.addField('Name:')
myDlg.show()
#when the box closes, the data you want will be in myDlg.data
#if you want to get a bit fancier, you can monitor the 'OK' or 'cancel' press:
if myDlg.OK:  # then the user pressed OK
    print myDlg.data #this will print the list of passed in values in the order in which you added the fields above.
else:
    print 'user cancelled'

Note that myDlg.data is a list, so you access the first element, you should index it: myDlg.data[0]


Easy way to get runtime variables (subject code, etc.)

def getRunTimeVars(varsToGet,order,expVersion):
	"""Get run time variables, see http://www.psychopy.org/api/gui.html for explanation"""
	infoDlg = gui.DlgFromDict(dictionary=varsToGet, title=expVersion, fixed=[expVersion],order=order)
	if infoDlg.OK:
		return varsToGet
	else: print 'User Cancelled'

Sample usage:

order =  ['subjCode','seed','gender']
runTimeVars = getRunTimeVars({'subjCode':'pGrouping_101', 'seed':10, 'gender':['Choose', 'male','female']},order,'ver1')


How do I keep doing something while waiting for a response?

This loop will execute until a user presses the 'q' or spacebar.

while not event.getKeys(keyList=['q','space']):
    #do stuff here

To terminate based on any keypress, just use the getKeys() function without any arguments:

while not event.getKeys():
    #do stuff here

A generic function for collecting responses

def getKeyboardResponse(validResponses,duration=0):
	"""Returns keypress and RT. Specify a duration (in secs) if you want response collection to last 
        that long. Unlike event.waitkeys(maxWait=..), this function will not exit until duration. 
        Use waitKeys with a maxWait parameter if you want to have a response deadline, but exit as soon 
        as a response is received."""
 
	event.clearEvents() 
	#not strictly necessary here, but good practice - 
	#will prevent buffer overruns if, for some reason there are too many responses in
	#between auto-clears (e.g., from mouse, eye tracking data)
 
	responded = False
	done = False
	rt = '*'
	responseTimer = core.Clock()
	while True: 
		if not responded:
			responded = event.getKeys(validResponses, responseTimer) 
		if duration>0:
			if responseTimer.getTime() > duration:
				break
		else: #end on response
			if responded:
				break
	if not responded:
		return ['*','*']
	else:
		return responded[0] #only get the first resp

A function for writing a string (trial info) to a file.

def writeToFile(fileHandle,trial,sync=True):
	"""Writes a trial (array of lists) to a file. File needs to be opened outside the function. Pass in the filehandle as an argument"""
	line = '\t'.join([str(i) for i in trial]) #TABify
	line += '\n' #add a newline
	fileHandle.write(line)
	if sync:
		fileHandle.flush()
		os.fsync(fileHandle)

Convert from polar to rectangular coordinates

You'll want this if you you're trying to display stimuli arranged in a circle. As it happens, psychopy provides a handy function for this as part of the misc package.
See the misc.pol2cart() function.
The function is simple trig; it's important to understand exactly what it's doing (not magic, just trig). Below is a function I've written in pre-psychopy days. The function is a more general version of the built-in one, allowing you to pass in a list of angles:

from math import *
def polarToRect(angleList,radius):
	"""Accepts a list of angles and a radius.  Outputs the x,y positions for the angles"""
	coords=[]
	for curAngle in angleList:
		radAngle = (float(curAngle)*2.0*pi)/360.0
		xCoord = round(float(radius)*cos(radAngle),0)
		yCoord = round(float(radius)*sin(radAngle),0)
		coords.append([xCoord,yCoord])
	return coords
 >>> polarToRect([0,30,60,90],50) #generate the x,y coordinates for supplied angles with a radius of 50
 [[50.0, 0.0], [43.0, 25.0], [25.0, 43.0], [0.0, 50.0]]



Hint: you can use the map-lambda construction to make psychopy's built-in function work like the one above:

map(lambda x: misc.pol2cart(x,50), [0,30,60,90])

You can plug a round() in there to take care of the rounding. (Best not to try to set the position of your stimulus to something to the negative 15th power.)

map(lambda x: [round(misc.pol2cart(x,50)[0],0),round(misc.pol2cart(x,50)[1],0)], [0,30,60,90])

...or you can just use my function above and save yourself the headache of having all these map-lambda-rounds.

A flexible routine for loading image and audio files

def loadFiles(directory,extension,fileType,win='',whichFiles='*',stimList=[]):
	""" Load all the pics and sounds. Uses pyo or pygame for the sound library (see prefs.general['audioLib'])"""
	path = os.getcwd() #set path to current directory
	if isinstance(extension,list):
		fileList = []
		for curExtension in extension:
			fileList.extend(glob.glob(os.path.join(path,directory,whichFiles+curExtension)))
	else:
		fileList = glob.glob(os.path.join(path,directory,whichFiles+extension))
	fileMatrix = {} #initialize fileMatrix  as a dict because it'll be accessed by file names (picture names, sound names)
	for num,curFile in enumerate(fileList):
		fullPath = curFile
		fullFileName = os.path.basename(fullPath)
		stimFile = os.path.splitext(fullFileName)[0]
		if fileType=="image":
			try:
				surface = pygame.image.load(fullPath) #gets height/width of the image
				stim = visual.ImageStim(win, image=fullPath,mask=None,interpolate=True)
				(width,height) = (surface.get_width(),surface.get_height())
			except: #no pygame, so don't store the image dimensions
				(width,height) = ('','')
			stim = visual.ImageStim(win, image=fullPath,mask=None,interpolate=True)
			fileMatrix[stimFile] = {'stim':stim,'fullPath':fullFileName,'filename':stimFile,'num':num,'width':width, 'height':height}
		elif fileType=="sound":
			fileMatrix[stimFile] = {'stim':sound.Sound(fullPath), 'duration':sound.Sound(fullPath).getDuration()}
 
	#optionally check a list of desired stimuli against those that've been loaded
	if stimList and set(fileMatrix.keys()).intersection(stimList) != set(stimList):
		popupError(str(set(stimList).difference(fileMatrix.keys())) + " does not exist in " + path+'\\'+directory) 
	return fileMatrix

Sample usage, assuming your stimuli are in stimuli subfolders:

picStims =  loadFiles('stimuli/pics','.jpg','image', win=win)
soundStims = loadFiles('stimuli/sounds','.wav','sound', win=win)

You can optionally specify stimList=[the list of stims you'll be using in the experiment]. This is not necessary, but lets you check to make sure that all the stimuli you need are available.

Now you can display the image dog.jpg like so:

picStims['dog']['stim'].draw()
win.flip()

Get the full path of the image like this:

picStims['dog']['fullPath']

You can play a sound like this

soundStims['beep']['stim'].play()

And get its duration like this:

soundStims['beep']['duration']

A sample factorial design trial list generation file (generateTrials.py)

import random
 
stims = ["cat", "car", "dog", "frog", "gun","motorcycle","rooster", "train", "cow", "whistle"] 
primeTypes = ["label","sound","noPrime"]
side = {'left':'right','right':'left'}
isValid = ["0"]*1+["1"]*4
separator = ","
seed=10
 
headerCols = ['curStim', 'curPrimeType', 'curIsValid', 'curSoundFile', 'curTargetSide', 'curDistractorSide']
header = separator.join(headerCols)
 
 
def generateTrials(subjCode,seed):
	trialList=[]
	for curStim in stims:
		for curPrimeType in primeTypes:
			for curIsValid in isValid:
				for curTargetSide in side.keys():
					if curPrimeType=='noPrime':
						soundFile = 'noise'
						curIsValid = '*'
					else:
						if curIsValid=="1":
							curSoundFile = '_'.join([curStim,curPrimeType])
						elif curIsValid=="0":
							curSoundFile = '_'.join([randomButNot(stims,curStim),curPrimeType])
					curDistractorSide = side[curTargetSide]
					try:
						trial = separator.join(map(str,map(eval,headerCols)))
					except NameError:
						print 'column not defined.  check code'
					trialList.append(trial)
 
	random.seed(seed)
	random.shuffle(trialList)
	try:
		trialFile = open('trialList_'+subjCode+'.csv','w')
		trialFile.write(header+'\n')
		[trialFile.write(curTrial+'\n') for curTrial in trialList]
		return trialList
	except:
		return False
 
if __name__ == "__main__":
	#for testing; this part is only executed if you run generateTrials.py from the terminal.
	trialList = generateTrials('test',10)
	print header
	for curTrial in trialList:
		print curTrial

To use it inside your main experiment file, import it like so:

from generateTrials import generateTrials

(this assumes that the code above is placed in a file called generateTrials.py) Now, you can create a trialList_subjCode.csv file by having a line like this in your main experiment file (making sure that subjCode and seed are contain the values entered at runtime:

generateTrials(subjCode,seed)

A flexible routine for importing trial lists (of the kind generated by the generateTrials function) into a list of dictionaries, keyed by column name

def importTrials(trialsFilename, colNames=None, separator='\t'):
	trialsFile = open(trialsFilename, 'rb')
 
	if colNames is None:
		# Assume the first row contains the column names
		colNames = trialsFile.next().rstrip().split(separator)
	trialsList = []
	for trialStr in trialsFile:
		trialList = trialStr.rstrip().split(separator)
		assert len(trialList) == len(colNames)
		trialDict = dict(zip(colNames, trialList))
		trialsList.append(trialDict)
	return trialsList

You can use it like so:

trialList = importTrials('trialList.txt')
for curTrial in trialList:
    curTrial[colName] #to access a particular value (where colName is something like 'pictureType')


Note: this functionality is actually built into PsychoPy using the importConditions function (see here)

A function for grabbing screenshots

When executed, the function will write a png file containing the stuff currently shown on the screen. It will default to using 'win' as the variable for your psychopy window. If that is not what you've called it, you must pass it to the function explicitly. You also need to pass in the name of the file to which you want to write.

grabScreenshot(fileName,win=win)
    """Outputs the current contents of the screen to fileName.png"""
    win.getMovieFrame()
    fileName=str(fileName)+'.png'
    win.saveMovieFrames(fileName)
    win.movieFrames=[]

Popping up a web-based survey from within your python script

The simplest way to pop-up a web-page from your script is to use the webbrowser package:

import webbrowser as web
surveyURL = 'http://' #full URL goes here
web.open(surveyURL) #put this line at whatever point you want to open up the webpage


It is often useful to pre-fill the survey with some information such as the subject code. Doing so will eliminate mismatches between the subject's code between the data and the survey. The easiest way to do this is to pass in the relevant info as part of the URL.

Google form

First, get the question codes to the questions you want to prefill by following the instructions here.

Then, add the question codes to the survey URL, like so:

surveyURL = 'https://docs.google.com/forms/d/1rg_sf55XnPOtv7ad4ESuxUpso1X5c_SvkbCL2B6hRzo/viewform'
surveyURL += '?entry.900342216=%s&entry.142747125=%s' % (runTimeVars['subjCode'], runTimeVars['room'])
web.open(surveyURL)


prefilled google form


Qualtrics forms

The same logic applies to qualtrics forms except instead of pre-filling a question, you would include the subject-code, etc, as part of an embedded field. See here for how to set embedded fields in qualtrics.

Using a webcam in psychopy

This is tested to work with Mac's iSight camera and should work on integrated PC cameras. Not sure if it will work with external USB cameras. Requires the CV package which can be installed through the Canopy package manager.

from psychopy import visual, event, core
import Image, time, pylab, cv, numpy
 
win = visual.Window([800,600], monitor='testMonitor', color="black")
 
capture = cv.CaptureFromCAM(0)
myStim = visual.ImageStim(win=win,image=None,flipHoriz=True)
ori=0
screenCapNum=0
flipHoriz=0
while True:
	img = cv.QueryFrame(capture)
	pi = Image.fromstring("RGB", cv.GetSize(img), img.tostring(), "raw", "BGR", 0, 1)
	myStim.setImage(pi)
	if event.getKeys('r'):
		ori = (ori+180) % 360
	if event.getKeys('f'):
		flipHoriz = (flipHoriz+1) % 2
	if event.getKeys('g'):
		cv.SaveImage("grab_"+str(screenCapNum)+".jpg", img)
		screenCapNum+=1
	myStim.setOri(ori)
	myStim.flipHoriz = bool(flipHoriz)
	myStim.draw()
	win.flip()
	if event.getKeys('q'):
		break
Personal tools
Namespaces
Variants
Actions
Navigation
Download and Install
Notes for each class - will be updated ~week before each class.
Programming Exercises
Projects
Quick reference
Toolbox