Slow drawing of multiple shapes (different colours)

Fairly new to PsychoPy/Python, so bear with me. I’m trying to create a task where people “dig” for gold. For this I’ve created an array which holds the coordinates, the status (dug/not-dug) and whether it contains gold. For a set number of clicks (while loop) I want to draw the grid (eventually based on the status column, with say different shades), which refreshes the display each click. So far I’ve got the grid programmed and I can display all elements as either black (no gold) or yellow (gold). I’ve also got a blue circle showing in click positions for now, just so i know clicks have been registered.

So my question is, does the below code look at all optimal or efficient? The reason I ask is that the timing is showing that it takes around .35 seconds to draw the grid. About half of this seems to be taken up by the draw command - if I comment out patch.draw() it is about .16 seconds.

In PsychToolBox I would take advantage of the buffer clear parameter, to hold contents on the screen and only redraw the clicked square. But as I understand it, that’s not how the buffers/draw work in PsychoPy.

I can’t find a lot of help for it, but I’m assuming that “ElementArrayStim” might be the solution here. Could I go about setting up squares of different colours with that?

from psychopy import visual, event, core
from psychopy.event import Mouse
import numpy as np
import time, random

win = visual.Window([1200,1200])

mouse = Mouse(visible=True)

stimBlue = visual.Circle(win, radius=.01, edges=64, fillColorSpace='rgb255', fillColor=[0, 0, 255])
gridSize = 40
gridWidth = .9 # how much border around grid (1 = none)
patchSize = (gridWidth*2)/gridSize
goldQuantity = 15

gridX = np.linspace(0-gridWidth,0+gridWidth,gridSize)

numEl = gridSize ** 2
gridDetails = np.zeros((numEl, 4))  # X, Y, status, gold
gridDetails[:, 0] = np.resize(gridX, numEl)
gridDetails[:, 1] = np.resize(np.transpose(np.tile(gridX, (gridSize, 1))), numEl)
gold = np.random.choice(numEl, goldQuantity)
for g in gold: gridDetails[g, 3] = 1

mouse_clicked = False

patch = visual.Rect(win=win, width=patchSize, height=patchSize, fillColorSpace='rgb255')

stimBlue.opacity = 0

for i in range(0,5):

    t0 = time.clock()
    for g in gridDetails:
        patch.pos = g[0:2]
        if g[3] == 1:
            patch.fillColor = [255, 255, 0]
        else:
            patch.fillColor = [0, 0, 0]
        patch.draw()
    t1 = time.clock()
    stimBlue.draw()
    win.flip()
    print(t1-t0)

    while True:
        if mouse.getPressed()[0] == 1:  # button press detected
            mouse_clicked = True
        if mouse.getPressed()[0] == 0 and mouse_clicked:  # button must have been released
            mouse_clicked = False
            stimBlue.pos = mouse.getPos()
            stimBlue.opacity = 1

            break

Hi @Tom_Beesley, you can use element array stim. You feed the elementArrayStim an array of locations and colors in order to customise each element color. The lists for locations and colors of the elements in this example are xys and rgbs, respectively.

import random
import psychopy
from psychopy import visual, event
import numpy as np

def random_color():
    # Generate random color - gold or black
    if random.random() <= .95:
        return (-1,-1,-1) # Black
    else:
        return (212/127-1, 175/127-1,55/127-1) # Gold
        
win = psychopy.visual.Window(
    size=[800, 800],
    units="pix",
    fullscr=False)

nLocs = 15
xLocs = np.linspace(-200, 200, nLocs)
yLocs = np.linspace(200,-200, nLocs)
xys = []   # Location array
rgbs = []  # Color array

for y in range(nLocs):
    for x in range(nLocs):
        xys.append([xLocs[x], yLocs[y]])
        rgbs.append(random_color())

grid = visual.ElementArrayStim(
        win=win,
        units="pix",
        nElements=nLocs**2,
        elementTex=None,
        elementMask=None,
        xys=xys,
        rgbs=rgbs,
        sizes=20)

for i in range(300):
    grid.draw()
    win.flip()

win.close()

The issue will be, how to detect whether elements of the element array stim have been clicked and then which element should have its color changed. For that, you may want to see Clicking on element in ElementArrayStim? "contains" function for elementArrayStim implementation

1 Like

Thanks @dvbridges. That worked perfectly. I’ll try the mouse code too.

This should be an easy thing to add. I’ve noticed many people rely on this behavior and kind of expect it. It’s really just a matter of copying the front buffer to the back buffer after swapping buffers. The operation can be used to force CPU/GPU synchronization at v-sync in place of what’s currently being used.

Yeah, in the situation where you’ve got multiple heterogeneous objects to draw (i.e., that can’t be accommodated in ElementStimArray), you may well have to suffer very long draw times. Something like visual search, in particular where stimuli are changing within a trial, might be tricky.

I’m adding support to make buffers persistent across flip calls. However, I think you can get the effect you want by setting useFBO=True when creating a window and calling flip(clearBuffer=False). Using an FBO would be faster since copying the front buffer to the back is a more costly operation, especially at higher resolutions.

2 Likes

In case anyone comes across this thread in the future and wants to know a solution to the mouse click issue I faced, here’s how I solved it. I expanded my gridDetails list that contained the X and Y coordinates for drawing the grid, such that it reflected Left, Top, Right, Bottom coordinates:

gridDetails[:,[0, 1]] -= patchSize/2 # drawing occurs from centre, so shift for Left and Top
gridDetails[:, 2] = gridDetails[:, 0] + patchSize  # Right
gridDetails[:, 3] = gridDetails[:, 1] + patchSize  # Bottom

I then found a pretty straightfoward way to check mouse clicks within these regions across the list of locations:

    while True:
        if mouse.getPressed()[0] == 1:  # button press detected
            mouse_clicked = True
        if mouse.getPressed()[0] == 0 and mouse_clicked:  # button must have been released
            mouse_clicked = False
            clickPos = mouse.getPos()
            break

    elementClickedOn = np.where((clickPos[0] > gridDetails[:, 0]) & (clickPos[0] <= gridDetails[:, 2]) & \
                                (clickPos[1] > gridDetails[:, 1]) & (clickPos[1] <= gridDetails[:, 3]))
    e = int(elementClickedOn[0])

I’m not sure how versatile this is, but it works well for my non-overlapping simple stimuli. I can then use that index of the element that has been clicked to change its properties:

    if gridDetails[e, 5] == 1:
        rgbs[e] = goldColour
        goldCount += 1
    elif gridDetails[e,5] == 0:
        rgbs[e] = dirtColour

    grid.setColors(rgbs)  # update colours
    grid.draw()
1 Like