This tutorial will guide you step by step through programming a visual-search experiment in PsychoPy Coder. You will create a task in which participants search for a target object in a real-world scene, respond with the mouse, and save the behavioural data.
You can work through the tutorial with your group, and you are encouraged to help each other if you get stuck. The tutorial should take about two hours. Make sure to save your progress regularly.
By the end of the tutorial, you should have a better understanding of how experiments can be programmed in PsychoPy Coder and how Python code can be used to control stimuli, responses, timing, and data saving.
If you run into problems, first check your code carefully and compare it with the tutorial instructions. You can also ask members of your group, search the PsychoPy forum, or look up the issue online.
This tutorial is based on a PsychoPy Builder tutorial by Aylin Kallmayer, a former PhD student in the Scene Grammar Lab.
You can find more useful PsychoPy and Python resources here:
Like the Builder interface, the Coder interface has several sub-windows (panes). The panel on the left represents the Source assistant, which lists all files in the current working directory (in the File Browser tab) and information about the Python modules in the current working directory specifically (in the Structure tab).
At the bottom of the Coder interface in the Shelf pane, you’ll find a so-called “Python shell”. You can think of it as a type of command line, but specifically for Python code. You can only run a single line at once, but it’ll show the result immediately.
This Python shell is very useful to debug or try out short code
snippets. For example, if you forgot what the function len
returns, you can for example run the command len([1, 2]) in
the Python shell to find out.
Finally, the last pane in the middle is PsychoPy’s code editor. Here, you can open any plain-text file (not just Python files!) which you can modify and save. In practice, of course, you’ll probably mostly work with Python files in this editor. Like most code editors, the Psychopy code editor also does some code formatting and syntax highlighting.
Create a new Python file (File → New) and save it as
VisualSearch.py.
Add some code to your VisualSearch.py file, e.g.,
print("Hello world"), and run the file.
After clicking the Run experiment button, the Experiment runner window should pop up, displaying something like the following:
#### Running:/Path/to/your/file/VisualSearch.py ####
Hello world
##### Experiment ended. #####
You do not have to use PsychoPy’s built-in Coder interface to write PsychoPy experiments. You can also use other code editors, such as Visual Studio Code, provided that the editor is configured to use the correct Python environment with PsychoPy installed. PsychoPy provides guidance for setting up Visual Studio Code here: https://psychopy.org/developers/environment/vscode.html
In this tutorial, you will create a visual-search experiment using real-world scenes. On each trial, participants will search for a specified target object in a scene. The target will either appear in a logical, or consistent, location within the scene, or in an inconsistent location. For example, if you were looking for an alarm clock in a bedroom, a consistent location would be next to the bed on the nightstand, whereas an inconsistent location would be on the pillow.
In a typical version of this experiment, we would record eye movements to examine how participants search the scene. Here, however, you will perform the search using the mouse.
The trial sequence is as follows. First, a small fixation cross appears at the top of the screen. Participants should “look” at this cross and press the spacebar to start the trial. The fixation cross is then replaced by the name of the target object. After a short pause, the scene appears. Participants should find the target object as quickly and accurately as possible and click on it with the mouse. After each trial, feedback shows the actual location of the target before the next trial begins.
Before we start creating the experiment itself, we need to set up the environment in which the experiment will run. This includes importing the required Python packages and collecting basic participant information at the beginning of the experiment.
The first lines of code import the packages and modules we need for the experiment. Some of these come from PsychoPy, while others are standard Python packages or commonly used scientific packages.
The PsychoPy modules provide the main tools for running the experiment:
gui is used to create input boxes, for example to
collect participant information.visual is used to create and present visual
stimuli.core provides timing functions and allows us to quit
the experiment.event and keyboard are used to collect
responses.monitors is used to define the properties of the
display, such as its physical size and viewing distance.The remaining packages are used for general experiment management and data handling:
os and sys help with file paths and
system-level commands.datetime allows us to automatically get the current
date.numpy is used for numerical operations.pandas is used for storing and organising data
tables.ast is used to safely convert strings into Python
objects, for example when reading values from a file that should be
interpreted as lists or dictionaries.# ============================================================
# LIBRARIES
# ============================================================
from psychopy import gui, visual, core, event, monitors
from psychopy.hardware import keyboard
import os
import sys
from datetime import datetime
import numpy as np
import pandas as pd
import ast
At the beginning of many experiments, we need to provide the program with basic information about the current session. This usually includes information such as the participant ID, age, date of testing, gender, and handedness.
In Builder experiments, this kind of input box can be created in the
Experiment Settings, under the Basic tab. In Coder experiments, the same
functionality can be implemented using PsychoPy’s gui
module.
The psychopy.gui module contains several classes for
creating dialog boxes. One simple option is gui.Dlg(),
which allows us to build an input box field by field.
For example, we can create a dialog box that asks for basic participant information before the experiment starts.
# ============================================================
# DIALOG BOX
# ============================================================
# get today's date as a string in ddmmyyyy format
today = datetime.now().strftime("%d%m%Y")
# create participant information dialog
participant = gui.Dlg(title="Visual Search Task", pos=(200,400));
# add input fields to the dialog
participant.addField(key = 0, label = 'Participant ID (s##):', initial=99, tip="starts with 0 for one-digit IDs");
participant.addField(key = 1, label = 'Age:', initial = 99, tip = "Years");
YOUR CODE HERE
YOUR CODE HERE
YOUR CODE HERE
Add fields for date, gender, and handedness to the input box.
The date field should already contain the current date when the input box appears.
The gender field should be a drop-down menu with the options
Other, F, and M.
The handedness field should be a drop-down menu with the options
R, L, and Ambidextrous.
Next, we want to show the input box. After the input box is shown, we check whether the experimenter clicked OK or Cancel. If Cancel is clicked, the experiment stops immediately.
# show the dialog box
# if the participant/experimenter presses Cancel, end the experiment.
participant.show();
if not participant.OK:
core.quit()
sys.exit()
Run to start the experiment and check whether
the input box appears.
After the input box has been completed, the information entered by
the experimenter is stored in participant.data. We then
transfer these values into separate variables.
Each item in participant.data is accessed by its position in the
input box. Python starts counting at 0, so
participant.data[0] contains the first value entered,
participant.data[1] contains the second value, and so
on.
Some values are converted into integers using int(),
such as the participant ID and age. Other values are converted into
strings using str(), such as the date, gender, and
handedness.
At this point, the participant information has been collected and stored in variables. These variables can later be saved together with the experimental data, ensuring that each data file is linked to the correct participant.
# get info
ID = int(participant.data[0])
Age = int(participant.data[1])
Date = str(participant.data[2])
Gender = str(participant.data[3])
Handedness = str(participant.data[4])
Before the experiment starts, we need to make sure that there is a clear place for saving the data. The code below first sets the working directory to the location of the experiment script. This ensures that relative paths are interpreted relative to the script rather than another folder on the computer.
The code then creates a folder called data. If this
folder already exists, nothing changes. If it does not exist yet, it is
created automatically using os.makedirs().
# ============================================================
# CREATE OUTPUT FOLDER
# ============================================================
# set the working directory to the folder containing this
script_path = os.path.dirname(sys.argv[0])
if len(script_path) != 0:
os.chdir(script_path)
# create folder if it doesn't already exist
datafolder = 'data'
os.makedirs(datafolder, exist_ok=True)
Before we can present stimuli, we need to define the display on which the experiment will run.
First, scn_width_pix and scn_height_pix
define the screen resolution in pixels. The variable retina
specifies whether the experiment is running on a Retina display.
Next, we create a monitor profile called ExpWindow. This
profile stores the physical properties of the display, including the
screen width, viewing distance, and resolution. The screen width should
refer to the visible display area in centimetres, not the diagonal
screen size.
# ============================================================
# MONITOR AND WINDOW SETTINGS
# ============================================================
# screen resolution in pixels (change if using a different screen)
scn_width_pix, scn_height_pix = 1512, 982
# set to True for Retina displays, such as many MacBook screens
retina = True
# create a monitor profile
mon = monitors.Monitor('ExpWindow')
mon.setWidth(31.26) # screen width in cm
mon.setDistance(60) # viewing distance in cm
mon.setSizePix((scn_width_pix, scn_height_pix)) # resolution in pixels
mon.saveMon() # save monitor
Before you run the experiment, check whether the monitor settings match your own setup.
Change the following values if needed:
scn_width_pix and scn_height_pix: set
these to your screen resolution in pixels.retina: set this to True if you are using
a Retina display, and False otherwise.mon.setWidth(31.26): replace 31.26 with
the physical width of your visible screen area in centimetres.mon.setDistance(60): replace 60 with the
participant’s viewing distance in centimetres.The next part creates the PsychoPy experiment window.
fullscr=True opens the experiment in fullscreen
mode.monitor=mon tells PsychoPy to use the monitor profile
defined above.winType='pyglet' specifies the window backend.useRetina=retina tells PsychoPy whether to use Retina
scaling.checkTiming=True asks PsychoPy to check the screen
timing, which is useful for experiments where accurate presentation
times matter.color=(-1, -1, -1) sets the background colour to
blackThe argument units='height' determines how positions and
sizes are specified in the experiment window. With height units, the
height of the window is treated as 1. This means that the centre of the
screen is at (0, 0), the top of the screen is approximately
0.5, and the bottom of the screen is approximately -0.5. The horizontal
range depends on the aspect ratio of the screen. For example, on a wider
screen, the left and right edges will be further away from zero than
-0.5 and 0.5.
We use height units in this tutorial because they scale with the height of the window. This means that the experiment should still look reasonable even if your window has slightly different dimensions from mine.
During testing and debugging, it is often better to set
fullscr=False. When an experiment runs in fullscreen mode,
there is no easy way to close the window unless you have already
implemented a quit option in your experiment. For example, if you
accidentally create an infinite loop, you may get stuck inside the
PsychoPy window. For this reason, I’d recommend using fullscreen mode
only once the experiment is finished and ready to run properly. At that
point, set fullscr=True, because fullscreen mode can
improve timing precision.
# create the PsychoPy experiment window
win = visual.Window(
size=[scn_width_pix, scn_height_pix],
fullscr=True,
monitor=mon,
winType='pyglet',
units='height',
useRetina=retina,
checkTiming=True,
color=(-1, -1, -1))
Try changing the background colour of the experiment window to grey. Then run the experiment and check whether the background colour has changed.
Hint: PsychoPy’s RGB values range from -1 to
1, not from 0 to 255.
Now that the general experiment setup is complete, we can define the parameters that are specific to this task. These include the timing of the experiment, the PsychoPy components we need, the condition file, and the images that will be shown during the trial loop.
Computer screens do not update continuously. Instead, they update, or ‘refresh’ a certain number of times per second. This is called the refresh rate. For example, a 60 Hz monitor updates the screen 60 times per second, so one frame lasts approximately 1/60 seconds, or about 16.7 ms.
In PsychoPy, the duration of one frame is stored in
win.monitorFramePeriod. This value is already a duration in
seconds. For example, if win.monitorFramePeriod is
approximately 0.0167, this already corresponds to one frame on a 60 Hz
display.
On some systems, especially MacBooks with variable refresh rate, PsychoPy may fail to measure a stable refresh rate. If this happens, first try setting your display to a fixed refresh rate in your system settings. On a Mac, go to System Settings → Displays → Refresh Rate and change it from ProMotion, Adaptive, or Variable to a fixed value, for example 60 Hz or 120 Hz.
# ============================================================
# TIMINGS
# ============================================================
# get duration of a frame (1/refresh rate)
frame_dur = win.monitorFramePeriod
Because the screen refreshes in discrete frames, stimulus durations also have to be implemented as whole numbers of frames. For example, a 60 Hz monitor refreshes the screen 60 times per second. This means that one frame lasts approximately 1/60 seconds, or about 16.67 ms. A stimulus can therefore only be shown for whole numbers of frames: 1 frame, 2 frames, 3 frames, and so on.
This means that some durations can be shown exactly, while others cannot. For example, on a 60 Hz monitor, 100 ms corresponds to 6 frames, because 6 × 16.67 ms = 100 ms. In contrast, 110 ms does not correspond exactly to a whole number of frames, so it would have to be rounded to the nearest possible frame duration.
For experiments where precise visual timing is important, it is useful to define stimulus durations in frames rather than only in seconds. To show a stimulus for 60 frames, for example, we draw the stimulus and flip the window 60 times. On a 60 Hz monitor, this corresponds to approximately 1 second.
In this experiment, we define the durations in seconds first and then convert them into frames. This makes the code easier to read, while still allowing PsychoPy to present stimuli for a whole number of screen refreshes.
# raw durations in seconds
SearchTargetDur = 1
FeedbackDur = 1
# converted into frames
SearchTargetFrames = int(round(SearchTargetDur / frame_dur))
FeedbackFrames = int(round(FeedbackDur / frame_dur))
After setting up the experiment window and timing, we can create the components that will be shown or used during the experiment.
In PsychoPy, components are the building blocks of an experiment. They are the individual objects that make up what the participant sees and how the participant can respond. In Builder, these include text components, image components, keyboard components, and mouse components. In Coder, we create these components directly using PsychoPy classes.
Here, we create four visual components:
fixation: a text stimulus showing a fixation cross
(+).target_label: a text stimulus that will later be used
to show the target label.search_image: an image stimulus showing the visual
search display.target_box: a rectangle stimulus placed around the
target object. It will be used as the area of interest for mouse-click
responses and can later be shown as feedback to highlight the correct
target location.All visual components need to know which window they belong to. This
is why each visual stimulus is created with win as the
first argument. This tells PsychoPy where the stimulus should be
drawn.
For example, visual.TextStim(win, ...) creates a text
stimulus in the experiment window. The other arguments define properties
of the stimulus, such as the text, size, position, and colour.
The position and size of each stimulus are defined using the unit
system selected when creating the window. In this tutorial, we use
units='height', so positions and sizes are relative to the
height of the window.
Note that the search_image component is created using
visual.ImageStim. Unlike a text stimulus, an image stimulus
needs a path to an image file. Here, we provide an example image path
when creating the component, even though the image itself will be
updated later during the experiment.
# ============================================================
# PSYCHOPY COMPONENTS
# ============================================================
fixation = visual.TextStim(win, text='+', height=0.1, pos=[0,0.4],color='white')
target_label = YOUR CODE HERE
search_image = visual.ImageStim(win,image='images/C_20CON.png',size=(1.2, 0.9))
target_box = visual.Rect(win, width=0.2, height=0.2, pos=(0, 0), lineColor=None, fillColor=None,lineWidth=6)
The target_label component has not been created yet.
Create this component now.
It should start with an empty text field, because the target label will be added later during the experiment.
We also create two response components:
kb: a keyboard object for collecting key presses.mouse: a mouse object for collecting mouse
responses.The keyboard object is created using
keyboard.Keyboard(). We will use this later to check
whether the participant has pressed a key.
The mouse object is created using event.Mouse(win=win).
Here, we provide the experiment window using win=win, so
that PsychoPy can track the mouse position and clicks relative to the
correct window.
kb = keyboard.Keyboard()
mouse = event.Mouse(win=win)
Next, we import the condition file. This file tells PsychoPy which stimuli should be shown on each trial and where the target is located.
In this experiment, we have one independent variable, condition, with
two levels: consistent and
inconsistent. When you check the
images folder, you will see there are two possible image
versions: one in which the target appears in a consistent location and
one in which the target appears in an inconsistent location. For
example, the same scene may have a consistent version called
C_4CON.png and an inconsistent version called
C_4INCON.png.
Each participant completes both conditions overall, but they should not see both versions of the same scene. For example, a participant should not search through the same bathroom twice, once in the consistent condition and once in the inconsistent condition. To avoid this, the stimuli need to be counterbalanced across participants. We will take care of this later.
This is how the condition file looks like:
The condition file contains the information needed to do this. It has one row per scene and several columns:
stimulus: the base name of the scene stimulus, for
example C_20.condition_1: the condition assigned to one
counterbalancing version.condition_2: the alternative condition assigned to the
other counterbalancing version.stim_id: a numeric stimulus ID.target: the name of the object the participant needs to
search for.position_con: the target position in the consistent
version of the scene.size_con: the size of the target box in the consistent
version.position_incon: the target position in the inconsistent
version of the scene.size_incon: the size of the target box in the
inconsistent version.The plan is to load this file, shuffle the trial order, and then loop through the rows one by one. On each trial, PsychoPy will use the information from the current row to decide which image to show, which target label to display, and where the target box should be placed.
# ============================================================
# CONDITIONS
# ============================================================
# load the trial file, each row corresponds to one trial
YOUR CODE HERE
# shuffle trial order and reset the row index
# reset_index(drop=True) makes sure the trial numbers run from 0 to ntrials-1
df = df.sample(frac=1).reset_index(drop=True)
# number of trials is determined by the number of rows in the trial file
ntrials = len(df)
Add the missing line of code to import the condition file
condition_file.csv.
Store the imported file in a variable called df.
Before the experiment starts, we preload the images that may be shown during the trials.
Normally, PsychoPy would load an image file when it is needed.
However, loading images during a trial can take time and may lead to
timing problems. To avoid this, we load all possible images in advance
and store them in a dictionary called image_cache.
A dictionary stores information as key-value pairs. Here, the key is
the image path, for example images/C_20CON.png, and the
value is the corresponding PsychoPy ImageStim object.
We loop through all rows of the condition file. Each row contains one
scene, but there are two possible counterbalancing conditions:
condition_1 and condition_2. We preload both
versions because the version shown later depends on the participant
ID.
For each row, the code builds the full image path by combining:
images/C_20CON or
INCON.pngThis creates paths such as images/C_20CON.png or
images/C_20INCON.png.
The line if path not in image_cache: checks whether the
image has already been loaded. If it has not been loaded yet, PsychoPy
creates an ImageStim and stores it in image_cache.
# ============================================================
# IMAGE PRELOADING
# ============================================================
image_cache = {}
# Loop over all rows in the trial file
for _, row in df.iterrows():
# each trial has two possible counterbalancing conditions.
# preload both, because which one is used depends on the subject ID.
for cond_col in ['condition_1', 'condition_2']:
# get the stimulus name, for example "C_20"
stimulus = row['stimulus']
# get the condition suffix, for example "CON" or "INCON"
condition = row[cond_col]
# build the full image path, for example "images/C_20CON.png"
path = f"images/{stimulus}{condition}.png"
# only load the image if it has not already been loaded before
if path not in image_cache:
image_cache[path] = visual.ImageStim(
win,
image=path,
size=(1.2, 0.9))
So far, we have created the experiment window, loaded the condition file, preloaded the images, and created the PsychoPy components. The next step is to put these pieces together in a trial loop.
A trial loop repeats the same basic trial structure multiple times. In this experiment, each row of the condition file corresponds to one trial. Therefore, we loop over the rows of the shuffled DataFrame.
The variable ntrials tells Python how many trials there
are in total. Because we set ntrials = len(df) earlier, the
loop will run once for every row in the condition file.
Importantly, everything that belongs to the trial loop must now be indented. In Python, indentation defines which lines belong inside the loop. Any line that should be repeated on every trial needs to be indented under:
# ============================================================
# TRIAL LOOP
# ============================================================
for trial in range(ntrials):
Inside the loop, we first get the information for the current trial.
The expression df.iloc[trial] selects one row from the
DataFrame. On the first loop iteration, it selects the first row; on the
second loop iteration, it selects the second row; and so on. Because we
shuffled the DataFrame earlier, the trials are presented in a random
order.
# --------------------------------------------------------
# get current trial information from the shuffled data frame
# --------------------------------------------------------
this_trial = df.iloc[trial]
Next, we get the target label for the current trial and assign it to
the text component. Here, this_trial['target'] reads the
target name from the condition file. We then update the text of
target_label, so that the correct target is shown on this trial.
# get and set the target label/text for this trial
this_target = this_trial['target']
target_label.text = str(this_target)
We also get the stimulus name from the condition file.
# get the stimulus/image name from the trial file
stimulus = this_trial['stimulus']
The experiment has two possible versions of each scene: a consistent version and an inconsistent version. However, a participant should only see one version of each scene. To achieve this, we counterbalance the condition assignment based on the participant ID.
# counterbalance condition assignment based on subject ID
# even subject IDs use condition_1, odd subject IDs use condition_2
if YOUR CODE HERE:
condition = YOUR CODE HERE
else:
condition = YOUR CODE HERE
Complete the code so that even participant IDs use
condition_1 and odd participant IDs use
condition_2.
Hint: Use the modulo operator %. For example,
ID % 2 returns the remainder after dividing ID
by 2. This can be used to check whether a number is even or
odd.
Next, we build the image path for the current trial.
For example, if stimulus is C_20 and condition is
CON, the resulting path is:
images/C_20CON.png
Instead of loading the image from the file during the trial, we retrieve the already preloaded image from image_cache. This helps avoid timing problems during the experiment.
# build the image path and retrieve the preloaded image stimulus
path = f"images/{stimulus}{condition}.png"
search_image = image_cache[path]
Finally, we select the correct target-box position and size for the current condition. We will use this box to check whether the participant clicked on the target, and we will later show it as feedback after the participant responds.
The condition file stores positions and sizes as text. For example, a
position may be stored as the string “(0.18, -0.15)”. PsychoPy cannot
directly use this as a position, because it is still text. Therefore, we
use ast.literal_eval() to convert the text into a Python
tuple.
If the current trial is consistent, we use position_con
and size_con. If the current trial is inconsistent, we use
position_incon and size_incon.
# --------------------------------------------------------
# get target-box position and size for the current condition
# --------------------------------------------------------
if YOUR CODE HERE:
position = ast.literal_eval(this_trial['position_con'])
size = ast.literal_eval(this_trial['size_con'])
elif YOUR CODE HERE:
position = ast.literal_eval(this_trial['position_incon'])
size = ast.literal_eval(this_trial['size_incon'])
Complete the code so that the correct target-box position and size are selected for the current condition.
The last three lines update the target_box component for
the current trial. We set its position, set its size, and make its
outline invisible during the search display by setting
target_box.lineColor = None. We will come back to this
later.
# make the target box invisible during the search display
target_box.setPos(position)
target_box.setSize(size)
target_box.lineColor = None
Before the search display appears, we show a fixation cross. This gives the participant a clear starting point for each trial.
First, we move the mouse cursor to the fixation position using
mouse.setPos(fixation.pos). This ensures that the mouse
starts from the same location on every trial.
Next, we draw the fixation cross using
fixation.draw().
# --------------------------------------------------------
# fixation screen: wait until participant presses space
# --------------------------------------------------------
# put the mouse cursor at fixation before the trial starts
mouse.setPos(fixation.pos)
# draw fixation cross
fixation.draw()
Run the experiment and check whether the fixation cross appears.
You may notice that the fixation cross does not appear, even though
we call fixation.draw().
This is because PsychoPy separates drawing from showing. When we call
fixation.draw(), the fixation cross is drawn to a hidden
back buffer. This means that it is prepared, but not yet visible on the
screen.
To make the drawn stimulus visible, we need to update the screen
using win.flip(). Calling win.flip() moves the
back buffer to the front, so that the participant can actually see what
has been drawn.
This is also where the timing information from the previous section
becomes important. The screen can only update when the monitor
refreshes. Each call to win.flip() waits for the next
screen refresh and then updates the visible display.
Add win.flip() directly after
fixation.draw().
Then run the experiment again and check whether the fixation cross appears.
You may have noticed that the fixation cross was only shown very briefly. However, we want it to remain on the screen until the participant presses the space bar.
For this, we first clear any previous key presses using
kb.clearEvents(). This prevents key presses from earlier
parts of the experiment from accidentally starting the next trial.
Then, kb.waitKeys() pauses the experiment until the
participant presses the space bar. Because nothing else happens while
Python is waiting, the fixation cross stays visible on the screen.
# clear previous key presses
kb.clearEvents()
# present fixation cross until key press
kb.waitKeys(keyList=['space'], waitRelease=False)
After the participant starts the trial via the space-bar press, we show the target label. This tells the participant which object they should search for in the scene.
The target label was already set earlier in the trial loop using:
target_label.text = str(this_target)
Now we draw this text stimulus to the screen. We want the target label to be presented for 1 second.
The for loop controls how long the target label is shown. Earlier, we
converted the target-label duration from seconds into frames and stored
it in SearchTargetFrames.
On each iteration of the loop, target_label.draw() draws
the target label to the back buffer, and win.flip() updates
the screen at the next refresh.
Because the back buffer is cleared after each flip, the target label
needs to be drawn again on every frame. This is why
target_label.draw() is inside the for loop.
The line mouse.setPos(fixation.pos) moves the mouse
cursor back to the fixation position in case the participant has already
started moving the mouse before the search display appears. This ensures
that the search always starts from the same mouse position.
Once the loop has finished, the experiment continues to the next part of the trial.
# --------------------------------------------------------
# search label presentation
# --------------------------------------------------------
# present target for fixed number of frames
for frameN in range(SearchTargetFrames):
target_label.draw()
mouse.setPos(fixation.pos)
win.flip()
Run the experiment and check whether the fixation cross appears, the space bar starts the trial, and the target label is presented correctly.
Next, we present the search display and wait for the participant to click on the target object.
At this point in the trial, the participant has already seen the target label, so they know which object they need to find. We now show the scene image and collect a mouse response.
We first draw the search_image and the
target_box. The target_box is still invisible
because we set its line colour to None earlier. Even though
it is invisible, we keep drawing it because we will use its position and
size later to check whether the participant clicked inside the correct
target area.
# --------------------------------------------------------
# search display: present image and wait for mouse click
# --------------------------------------------------------
# prepare first search frame
search_image.draw()
target_box.draw()
Next, we reset the mouse timing exactly when the search image appears on the screen.
To do this, we use win.callOnFlip(mouse.clickReset).
This tells PsychoPy to call mouse.clickReset() at the
moment of the next screen flip. In other words, the mouse timing is
reset at the same time as the search display becomes visible. This is
important because response times should be measured from the onset of
the search display, not from an earlier line in the script.
The following win.flip() then presents the search image
and resets the mouse timing at the same moment.
# clear previous mouse clicks and reset mouse timing
win.callOnFlip(mouse.clickReset)
win.flip()
After that, the while True: loop keeps checking for a
mouse click while the search display is on the screen.
In this version, the search image has already been drawn and shown
before the loop starts. The loop itself only checks whether the
participant has clicked the mouse. This is enough because the display
stays visible until the next win.flip() call.
The line mouse.getPressed(getTime=True) returns two
pieces of information:
buttons: which mouse buttons are currently
pressed.times: the response times for those button
presses. # check for mouse click while search image is presented
while True:
buttons, times = mouse.getPressed(getTime=True)
if any(buttons):
click_pos = mouse.getPos()
button_idx = buttons.index(1)
click_rt = times[button_idx]
break
Within the loop, any(buttons) checks whether any mouse
button has been pressed. If a button has been pressed, we save two
things:
click_pos: the position of the mouse at the time of the
click as an x/y coordinate pair.click_rt: the response time, measured from the onset of
the search display.Because we allow any mouse button, we do not know in advance whether the participant clicked the left, middle, or right button. The variable buttons contains one value for each mouse button. A value of 1 means that the button is currently pressed, and a value of 0 means that it is not pressed.
For example: buttons = [0, 0, 1]
This means that the right mouse button was pressed. Python starts counting at 0, so the three positions in the list correspond to:
buttons[0] # left mouse button
buttons[1] # middle mouse button
buttons[2] # right mouse button
Because we allow any mouse button, we first need to find out which
button was pressed. The line buttons.index(1) finds the
first 1 in the buttons list and returns its index.
In this example: buttons = [0, 0, 1],
buttons.index(1) returns 2, because the pressed button is
at index 2. We save this index as button_idx:
button_idx = buttons.index(1)
We then use the same index to get the matching response time from the times list:
times = [None, None, 0.732]
click_rt = times[button_idx]
Here, button_idx is 2, so
click_rt = times[2]. This means that click_rt
would be 0.732.
Finally, break exits the loop, so the experiment can continue to the next part of the trial.
Run the experiment and check whether the correct search images are presented after each target label.
After the participant has clicked on the search image, we need to check whether the click was correct.
The invisible target_box defines the area around the
target object. We can use the .contains() method to test
whether the participant’s click position falls inside this box.
If the click was inside the target box, the response is counted as
correct. We save this by setting accuracy = 1, and we set
the feedback box colour to green.
If the click was outside the target box, the response is counted as
incorrect. In this case, we set accuracy = 0, and the
feedback box colour is set to red.
# --------------------------------------------------------
# accuracy check & feedback display
# --------------------------------------------------------
# check whether the click position was inside the invisible target box
if target_box.contains(click_pos):
accuracy = 1
box_color = "green"
else:
accuracy = 0
box_color = "red"
Next, we use the accuracy result to show feedback. The target box was invisible during the search display, but now we make it visible and colour it according to whether the click was correct.
If the participant clicked inside the target box, the box will be green. If they clicked outside the target box, it will be red.
We then present the search image together with the feedback box for a
fixed number of frames. On each frame, we draw both stimuli and then
call win.flip() to update the screen.
# make target box visible and colour it according to accuracy
YOUR CODE HERE
# present search image plus feedback box for fixed number of frames
for YOUR CODE HERE:
YOUR CODE HERE
YOUR CODE HERE
YOUR CODE HERE
Complete the missing lines of code.
The feedback display should show the search image again, together with the target box. The target box should be visible and should be green for correct clicks and red for incorrect clicks.
Then run the experiment and check whether the feedback display works correctly.
At the end of each trial, we save the relevant information into the
data frame df.
Each row of the data frame corresponds to one trial. The variable
trial tells Python which row we are currently writing to. For example,
df.at[trial, "condition"] = condition saves the condition
of the current trial in the column “condition”.
Some information, such as ID, age, date, gender, and handedness, is the same across trials. Other information, such as the click position, response time, and accuracy, can differ from trial to trial.
# --------------------------------------------------------
# Save trial data into data frame
# --------------------------------------------------------
df.at[trial, "ID"] = ID
df.at[trial, "Age"] = Age
df.at[trial, "Date"] = Date
df.at[trial, 'Gender'] = Gender
df.at[trial, 'Handedness'] = Handedness
df.at[trial, "condition"] = condition
YOUR CODE HERE
YOUR CODE HERE
YOUR CODE HERE
YOUR CODE HERE
Complete the missing lines of code.
Save the horizontal click position, vertical click position, response time, and accuracy in the data frame.
You should save:
click_xclick_yclick_rtaccuracy
After the trial loop has finished, the experiment is over. The following code should therefore no longer be indented. Code inside the loop runs once per trial; code outside the loop runs once after all trials are finished.
We save the data frame as a .csv file. The data frame df
now contains one row per trial, including the condition, click position,
response time, and accuracy.
The output filename includes the participant ID:
f"s{ID:02d}.csv"
The :02d part formats the ID with two digits. For
example, participant 1 is saved as s01.csv, and participant
12 is saved as s12.csv.
# ============================================================
# SAVE DATA FILE AFTER EXPERIMENT
# ============================================================
outfile = os.path.join(datafolder, f"s{ID:02d}.csv")
df.to_csv(outfile, index=False)
When Python reaches the end of the script, the PsychoPy window may close automatically and the Python process may finish. However, in PsychoPy experiments it is better to quit explicitly.
First, we close the PsychoPy window using win.close().
Then we stop the experiment using core.quit(). This makes
sure that PsychoPy exits cleanly and performs any necessary cleanup.
You should therefore include these lines at the very end of your script:
# ============================================================
# QUITTING THE EXPERIMENT
# ============================================================
win.close()
core.quit()
Run the experiment and check whether the data file is saved correctly.
After the experiment finishes, open the .csv file and
make sure that each trial has been stored with the correct condition,
click position, response time, and accuracy.
If you finished early, you can work on the bonus tasks below to extend or improve the experiment.
Add an escape key that allows the participant to quit the experiment early during the fixation screen.
Add a short break halfway through the experiment. During the break, show a short message and let the participant continue by pressing the space bar.
Add summary feedback at the end of the experiment. For example, show the participant their overall accuracy before closing the experiment.