Psychopy useful functions

The following document contains a collection of functions created to run experiments in Psychopy.

Creating coloured gratings (pseudo-Gabor).

from psychopy import visual, event, monitors, tools
from psychopy.visual import filters
from psychopy.tools import monitorunittools as mut
import numpy as np
import math
import os

###############################################################################
#                                VARIABLES
###############################################################################

SAVE_IMAGE = True             #Set to true if you want to output an image
DIFFERENT_BRIGHTNESS = True   #Should the stimuli have different luminance?
SWITCH_SIDE = True            #Invert the left and right grating
WHAT_TO_DRAW = "right"        #Either "both", "left" or "rigth"
# Parameters for the gratings
param_stim = {
    "resolution": 5,           #Size of the stimulus grating in deg (this will be coverted in pix later)
    "mask_resolution": 2**11,  #Resolution of the mask used to render the gratings as circles (must be a power of 2)
    "ori_left": 45,            #Orientation of the first grating
    "ori_right": -45,          #Orientation of the second grating 
    "pos_left": (0, 0),        #Position of the first grating (0,0 is the center of the screen)
    "pos_right": (0, 0),       #Position of the second grating (0,0 is the center of the screen)
    "cycles": 4*5,             #Spatial frequency of the gratings. This should be resolution X cycles per deg
    "vergence_cycles": 5,      #Spatial frequency of the gratings used to create the vergence patterns
    "vergence_sf": 0.03,       #This value controls the number of gratings used in the vergence patterns (use values < 0.5)
    "alpha_left": 1,
    "max_value_first": -0.3 #Red: Psychopy [-0.0030, -1,-1], RGB [89,0,0], HSV[0,100,35]
}

# Screen and window parameters - for Psychopy
param_pc = {
    "resolution": (1920, 1080),
    "width": 34.2}

# Directories and file names for the image output
this_dir = os.path.dirname(os.path.abspath(__file__))
image_name = "red_single_03-1-1.png"  #Name of the file to output at the end

###############################################################################
#                                   WINDOW
###############################################################################

# Create monitor and windows
mon = monitors.Monitor(
    name="desk_monitor",
    width=param_pc["width"],
    distance=57
)
mon.setSizePix = param_pc["resolution"]

win = visual.Window(
    size=param_pc["resolution"],
    monitor=mon,
    units="pix",
    allowGUI=False,
    screen=1,
    fullscr=False,
    color=(-1, -1, -1),
    colorSpace='rgb',
    blendMode='avg',
    winType='pyglet',
    useFBO=True)


###############################################################################
#                                  FROM DEG TO PIX
###############################################################################

# Convert to pix
param_stim["resolution"] = int(mut.deg2pix(param_stim["resolution"], mon))
# Round pix to the closest power of 2. NOTE this works for "low" values but 
# cannot be generalized to high values (eg. 100000). However, here we work with 
# values in the 100 range (eg. 256 pix).
param_stim["resolution"] = 2**round(math.log2(param_stim["resolution"]))

###############################################################################
#                                    STIMULI
# To create the gratings we start by creating a black texture defined as a 
# matrix of dimension [dim1, dim2, 3], where the three layers represent the RGB
# colours. Then, we will replace the layer of the colour we are interested in 
# with a grating, which is a [dim1, dim2] array, conatining values from -1 to 1 
# representing the intensity of the colour. Doing this will create a grating 
# stimulus of the desired colour.
# For the red stimulusg, we are interested in manipulating its brightness. To do 
# so, we define a colour in HSV space and convert it into RGB (use tool online)
# Then we modify the grating range, so that it goes from -1 (black) to N, where
# N is the values obtained online
###############################################################################

# Switch side if requested
if SWITCH_SIDE:
    pos_left  = param_stim["pos_left"]
    pos_right = param_stim["pos_right"]
    param_stim["pos_left"] = pos_right
    param_stim["pos_right"] = pos_left
    
# ---Coloured Gabors---#

# Create a black texture for both stimuli...
grating_left = np.ones((param_stim["resolution"], param_stim["resolution"], 3)) * -1
grating_right = np.ones((param_stim["resolution"], param_stim["resolution"], 3)) * -1

# GREEN --> For the green stimulus we simply overaly the grating to the G channel
grating_right[:, :, 1] = filters.makeGrating(res=param_stim["resolution"],
                                              ori=param_stim["ori_right"],
                                              cycles=param_stim["cycles"],
                                              gratType="sin")



# RED --> For the red stimulus we need to do some more work...

# Create a grating
sin_mask = filters.makeGrating(res=param_stim["resolution"],
                                            ori=param_stim["ori_left"],
                                            cycles=param_stim["cycles"],
                                            gratType="sin")

# If different luminance is requested
if DIFFERENT_BRIGHTNESS:
    # Scale only positive values to change the colour (RED) but not the black through the Rohan's transform
    # NOTE: it's not a real transform...it was a tip from a friend
    scale_factor = 0.5*(param_stim["max_value_first"]+1)
    sin_mask_scaled = scale_factor * (sin_mask + 1) - 1
   
    grating_left[:,:,0] = sin_mask_scaled
# If no difference in brightness is required, apply the grating as above
else:
    grating_left[:, :, 0] = sin_mask


#---Vergence Gratings---#

# Create a gray texture (Psychopy [0,0,0] is gray)...
grating_vergence = np.zeros((param_stim["resolution"], param_stim["resolution"], 3))

#...then overimpose a grid on all the three RGB channels
grating_vergence[:, :, 0] = filters.makeGrating(res=param_stim["resolution"],
                                                cycles=param_stim["vergence_cycles"],
                                                ori=45,
                                                gratType='sin')
grating_vergence[:, :, 1] = filters.makeGrating(res=param_stim["resolution"],
                                                cycles=param_stim["vergence_cycles"],
                                                ori=45,
                                                gratType='sin')
grating_vergence[:, :, 2] = filters.makeGrating(res=param_stim["resolution"],
                                                cycles=param_stim["vergence_cycles"],
                                                ori=45,
                                                gratType='sin')

#---Circle Mask---#

# Generate a nice smooth (at least almost) circle mask
mask = filters.makeMask(matrixSize=param_stim["mask_resolution"],
                        shape="circle")

#---Generate Stimuli with Psychopy---#

# left grating stimulus
stim_left = visual.GratingStim(
    name="stimL",
    win=win,
    size=(param_stim["resolution"], param_stim["resolution"]),
    pos=param_stim["pos_left"],
    tex=grating_left,
    mask=mask,
    units="pix")
# Right grating stimulus
stim_right = visual.GratingStim(
    name="stimR",
    win=win,
    size=(param_stim["resolution"], param_stim["resolution"]),
    pos=param_stim["pos_right"],
    tex=grating_right,
    mask=mask,
    units="pix")
# Left vergence pattern
vergence_left = visual.GratingStim(
    name="vergL",
    win=win,
    size=(param_stim["resolution"]+50, param_stim["resolution"]+50),
    pos=param_stim["pos_left"],
    tex=grating_vergence,
    mask=mask,
    units="pix",
    sf=param_stim["vergence_sf"])
# Right vergence pattern
vergence_right = visual.GratingStim(
    name="vergR",
    win=win,
    size=(param_stim["resolution"]+50, param_stim["resolution"]+50),
    pos=param_stim["pos_right"],
    tex=grating_vergence,
    mask=mask,
    units="pix",
    sf=param_stim["vergence_sf"])

# Left fixation dot
fixation_left = visual.ShapeStim(
    win=win,
    name='polygon',
    size=(param_stim["resolution"]/50, param_stim["resolution"]/50),
    vertices='circle',
    ori=0.0,
    pos=param_stim["pos_left"],
    anchor='center',
    lineWidth=1.0,
    colorSpace='rgb',
    lineColor='white',
    fillColor='white',
    opacity=None,
    depth=0.0,
    interpolate=True)

fixation_right = visual.ShapeStim(
    win=win,
    name='polygon',
    size=(param_stim["resolution"]/50, param_stim["resolution"]/50),
    vertices='circle',
    ori=0.0,
    pos=param_stim["pos_right"],
    anchor='center',
    lineWidth=1.0,
    colorSpace='rgb',
    lineColor='white',
    fillColor='white',
    opacity=None,
    depth=0.0,
    interpolate=True)


###############################################################################
#                                    DRAW
###############################################################################

if WHAT_TO_DRAW == "both":
# Draw stimuli on buffer
    vergence_left.draw()
    vergence_right.draw()
    stim_left.draw()
    stim_right.draw()
    fixation_left.draw()
    fixation_right.draw()
elif WHAT_TO_DRAW == "left":
    vergence_left.draw()
    stim_left.draw()
    fixation_left.draw()
else:
    vergence_right.draw()
    stim_right.draw()
    fixation_right.draw()

# Present stimuli on the window
win.flip()
# Save stimuli if requested
if SAVE_IMAGE:
    frame = win.getMovieFrame()
    frame.save(os.path.join(this_dir, image_name))
# Terminate when a key is pressed
event.waitKeys()
# Close the Psychopy window
win.close()