Vision Psychophysics
Getting Started
- Install the BonVision package from the Bonsai Community feed in the package manager.
- Go through the basic stimuli tutorial at the BonVision website.
Warning
Make sure the latest version of the BonVision package is installed for this worksheet.
Orientation Discrimination
In this worksheet you will build the skeleton of an orientation discrimination vision psychophysics task. In this variant of the task we will present two test gratings in quick succession at different random orientations, and ask the participant to report which of the gratings had the more clockwise orientation. Orientations for each grating will be drawn from a random uniform distribution, and feedback of whether the response was correct or incorrect will be provided visually.
The following set of exercises are to be developed in a single workflow, so do not remove the elements from the previous exercise from subsequent exercises, unless it is specifically mentioned.
Exercise 1: Random Orientation Grating
To allow sharing screen calibration for all displayed task elements, we start by defining a common BonVision render pipeline.
- Insert a
CreateWindow
source and set theClearColor
property toGray
. - Insert the
BonVisionResources
andLoadResources
operators to preload all built-in BonVision shaders. - Insert the
WorkflowOutput
operator afterLoadResources
to ensure the workflow terminates when the shader window is closed. - Insert a
RenderFrame
source. This source will emit a notification when it is time for a new frame to be drawn on the screen. - Insert a
NormalizedView
operator. This will specify that our stimulus dimensions are resolution independent, aspect ratio corrected, and normalized to the range [-1,1]. - Insert a
PublishSubject
operator and set itsName
property toDraw
. We will use these events whenever we need to draw any element on the screen.
The first step in developing our task will be to display a grating in the center of the screen at a random orientation for a specified period of time, and store the value of the orientation, so we can use it later to test the participant.
- Insert a
CreateRandom
source. - Insert a
CreateContinuousUniform
and set itsLower
andUpper
properties to -1 and 1, respectively. - Insert an
AsyncSubject
and set its name property toAngleDistribution
.
For now, we start by displaying a repeating sequence of random orientation gratings.
- Insert a
Timer (Shaders)
source and set itsDueTime
property to 2 seconds. - Insert a
SelectMany
operator and set its name toReferenceGrating
. - Insert a
Repeat
operator.
Note
The Timer (Shaders)
source works exactly like the default Timer (Reactive)
source, but it counts the time by using the screen refresh time, rather than the operating system time. This can be important for precise timing of screen stimuli, as it avoid clock drift and jitter when synchronizing multiple visual elements, and should be in general preferred when specifying the various intervals used to control elements in the BonVision or Shaders packages.
To implement the ReferenceGrating
state, we will need to sample a random angle from the angle distribution, use it to initialize the angle property of the gratings, and present the gratings for a specified period of time. At the end, we need to send out as a result the value of the random orientation which was generated.
ReferenceGrating
:
- Use the
Sample (Numerics)
operator to sample a random orientation value from theAngleDistribution
subject and store it in a newAsyncSubject
namedAngle
. This will allow us to reuse the sampled value when drawing the gratings later. - Subscribe to the
Draw
subject we defined previously and insert aDrawGratings
operator. - Externalize the
Angle
property from theDrawGratings
node and connect theAngle
subject we created to it. - Insert a
Timer
operator and set itsDueTime
property to 1 second. - Insert a
TakeUntil
operator and connect theDrawGratings
node as the source, and theTimer
as the trigger. - Insert a
Last
operator. This will ensure we will get a notification whenever theTimer
stops the presentation of the stimulus. - Insert a
Sample
operator following theAngle
declaration, and connect theLast
operator as a trigger. This will store the sampled angle value until it is time to return. - Insert a
WorkflowOutput
operator to specify the final output of the state.
Run the workflow and verify whether the behaviour of the system is correct. Are different orientation values being used for each subsequent presentation of the gratings?
Exercise 2: Reusing stimulus definitions
The second step in defining the contrast discrimination task is to display a second randomly oriented grating in each trial, with a small blank (or masking) period in between. To do this, we want to avoid repeating the entire workflow we designed for our reference grating, so we will make use of the IncludeWorkflow
operator to reuse our stimulus presentation logic.
ReferenceGrating
:
- Inside the
ReferenceGrating
state, select all nodes beforeWorkflowOutput
, right-click the selection, and choose theSave as Workflow
option. ChooseRandomOrientationGrating
as the name for the extension.
After we have our new reusable operator, we can extend the workflow to include the blank period and the second grating stimulus.
- Insert a
SelectMany
operator after theReferenceGrating
state and set itsName
property toBlank
. - Insert another
SelectMany
operator afterBlank
with the nameTestGrating
. - Insert a
Repeat
operator.
For the Blank
state we will use a simple gap interval where nothing is drawn on the screen. We can do this easily by delaying the transmission of the result of the previous state, before we move on to the next state.
Blank
:
- Insert a
Delay (Shaders)
operator between the input and the output of the state workflow.
Note
Similar to Timer (Shaders)
, the Delay (Shaders)
operator works exactly like the Delay (Reactive)
operator, but using the screen refresh clock instead of the operating system clock. This also ensures that any delayed notifications are resynchronized with the render loop, in case they were emitted from other external devices.
To implement the TestGrating
state, we want to reuse our previous RandomOrientationGrating
extension workflow and simply combine the random generated angle with the angle from the reference grating.
TestGrating
:
- Insert a new
RandomOrientationGrating
operator from the toolbox and combine it with the input by using theZip
combinator. This will generate a pair where the first value is the random angle from the first reference grating, and the second value is the random angle for this test grating.
Run the workflow and validate the random angle pairs are distinct and valid from trial to trial.
Exercise 3: Collect test response and compute trial outcome
Now that we have our two randomly generated gratings, we need to gather the response from the participant and compare it with the actual situation to determine whether the trial was successful.
- Insert a new
Response
state after theTestGrating
state using theSelectMany
operator.
To implement the response gathering state we will use key presses from the participant. We will use the left and right arrow keys to indicate which stimulus had the most clockwise orientation and compare the response with whether or not the first stimulus was more clockwise than the second stimulus.
Response
:
- Connect the
Draw
subject from the toolbox to a newDrawText
operator and set itsText
property to a suggestive question (e.g.A or B?
). Also edit theFont
property and make sure the size is at least 72pt for readability. - Insert a
DelaySubscription (Shaders)
operator and set itsDueTime
property to 1 second.
Note
As before, the difference with DelaySubscription (Reactive)
is that DelaySubscription (Shaders)
will use the screen refresh time and make sure that all effects of subscription are synchronized with the render loop.
- Insert a
LessThan
operator after the input source node. This will compare the value of the randomly sampled angles for the first and second gratings, respectively, and will return true if the first grating is more clockwise than the second grating (i.e. its angle in radians is smaller than the second grating). - Insert a
KeyDown (Shaders)
source and set itsKey
property toLeft
. - Insert a
KeyDown (Shaders)
source and set itsKey
property toRight
. - Insert a
Boolean
operator after each of the key press sources and set theValue
property toTrue
for the operator following the left key press. - Combine the results of both key presses with a
Merge
operator. - Insert a
First
operator since we are only interested in the first response from the participant. - Combine the comparison from
LessThan
with the response from the participant using theZip
combinator. - Insert an
Equal
operator to check whether or not the response matches the true angle comparison. This will be the result of theResponse
state and after it is reported, all other effects of the state will be determined (i.e. the question display).
Exercise 4: Present trial outcome feedback to participants
The only step left for finishing our experimental prototype is to report the feedback of each trial back to the participants. We will do this by drawing a colored square, indicating green for a correct response, and red for an incorrect response.
- Insert a new
Feedback
state after theResponse
state using theSelectMany
operator.
This final state will simply display a quad for a certain period of time, where the color will be modulated by the trial outcome value. We want to store this value until the end of the trial so we can report it for subsequent processing.
Feedback
:
- Insert an
AsyncSubject
operator and set itsName
property toResult
. This will store the trial outcome result so it can be used to compute the color value of the quad. - Subscribe to the
Draw
subject and insert aDrawQuad
operator. - Externalize the
ColorR
,ColorG
, andColorB
properties from theDrawQuad
node. - Subscribe to the
Result
subject and create a newExpressionTransform
operator.
In the Expression
property of the ExpressionTransform
operator, create a structure holding the RGB color value using the following script:
it ? new(0 as R, 1 as G, 0 as B) : new(1 as R, 0 as G, 0 as B)
- Connect the
ExpressionTransform
to the externalized properties. - Insert a
Timer
operator and set itsDueTime
property to 1 second. - Insert a
TakeUntil
operator and connect theDrawQuad
node as the source, and theTimer
as the trigger. - Insert a
Last
operator. This will ensure we will get a notification whenever theTimer
stops the feedback presentation. - Insert a
Sample
operator following theResult
declaration, and connect theLast
operator as a trigger. This will store the trial outcome value until it is time to return. - Connect it to the
WorkflowOutput
operator to specify the final output of the state and trial.
Run the workflow and verify the visual feedback indeed matches the perceived results from each trial.
Exercise 5 (Optional): Measure psychometric data
What is the minimal discrimination threshold for humans in this task? How would you extend the previous workflow in order to assess this?