0

I'm looking for a more efficient way to draw continuous lines in PsychoPy. That's what I've come up with, for now...

edit: the only improvement I could think of is to add a new line only if the mouse has really moved by adding if (mspos1-mspos2).any():

ms = event.Mouse(myWin)

lines = []

mspos1 = ms.getPos()
while True:
    mspos2 = ms.getPos()
    if (mspos1-mspos2).any():
        lines.append(visual.Line(myWin, start=mspos1, end=mspos2))
    for j in lines:
        j.draw()
    myWin.flip()
    mspos1 = mspos2

edit: I tried it with Shape.Stim (code below), hoping that it would work better, but it get's edgy even more quickly..

vertices = [ms.getPos()]
con_line = visual.ShapeStim(myWin,
        lineColor='red',
        closeShape=False)
myclock.reset()
i = 0
while myclock.getTime() < 15:
    new_pos = ms.getPos()
    if (vertices[i]-new_pos).any():
        vertices.append(new_pos)
        i += 1
    con_line.vertices=vertices
    con_line.draw()
    myWin.flip()
piot
  • 3
  • 4
  • 1
    Can you describe more what you mean by "more efficient way"? What's wrong with your current approach, that you would like to make better/fix? – Castaglia Mar 05 '16 at 16:31
  • after around 14 seconds the lines become edgy, because it is too much of a workload to draw all the lines with each loop; so I would like to find another way to draw smooth lines – piot Mar 05 '16 at 18:48
  • Could you be clear about what "edgy" means? Preferably, post a screenshot of non-edgy and edgy lines. Also, a sentence or two about your purpose would make it easier to propose solutions and make it easier for others to find this on Google. – Jonas Lindeløv Mar 05 '16 at 21:21
  • I would like to let participants draw a curve about how they were feeling during the experiment. While participants draw the line, values on a scale are recorded and saved in a file later on. Because I'm using a minimal debian distribution (fluxbox) I tried to test the code, while drawing for more than 10 sec. After some time, there is a delay and the line starts dropping frames. However I think that it should be good enough for the experiment, because the rating will take only a few seconds. – piot Mar 05 '16 at 22:11
  • In neurobs presentation picture parts are added to one picture. I thought about a similar solution. I'm new to psychopy and thought that there might be a better solution... – piot Mar 05 '16 at 22:15
  • Actually, using your code above could you simply indent the line `con_line.vertices=vertices` by one? At the moment you update the list only when you detect a change, but you update stimulus every frame (often with an unchanged list). Make the vertex update part of your if...statement – Jon Mar 07 '16 at 11:02

1 Answers1

2

The problem is that it becomes too ressource demanding to draw those many visual.Lines or manipulate those many vertices in the visual.ShapeStim on each iteration of the loop. So it will hang on the draw (for Lines) or vertex assignment (for ShapeStim) so long that the mouse has moved enough for the line to show discontinuities ("edgy").

So it's a performance issue. Here are two ideas:

  1. Have a lower threshold for the minimum distance travelled by the mouse before you want to add a new coordinate to the line. In the example below I impose a the criterion that the mouse position should be at least 10 pixels away from the previous vertex to be recorded. In my testing, this compressed the number of vertices recorded per second to about a third. This strategy alone will postpone the performance issue but not prevent it, so on to...
  2. Use the ShapeStim solution but regularly use new ShapeStims, each with fewer vertices so that the stimulus to be updated isn't too complex. In the example below I set the complexity at 500 pixels before shifting to a new stimulus. There might be a small glitch while generating the new stimulus, but nothing I've noticed.

So combining these two strategies, starting and ending mouse drawing with a press on the keyboard:

# Setting things up
from psychopy import visual, event, core
import numpy as np

# The crucial controls for performance. Adjust to your system/liking.
distance_to_record = 10  # number of pixels between coordinate recordings
screenshot_interval = 500  # number of coordinate recordings before shifting to a new ShapeStim

# Stimuli
myWin = visual.Window(units='pix')
ms = event.Mouse()
myclock = core.Clock()

# The initial ShapeStim in the "stimuli" list. We can refer to the latest
# as stimuli[-1] and will do that throughout the script. The others are 
# "finished" and will only be used for draw.
stimuli = [visual.ShapeStim(myWin,
        lineColor='white',
        closeShape=False,
        vertices=np.empty((0, 2)))]

# Wait for a key, then start with this mouse position
event.waitKeys()
stimuli[-1].vertices = np.array([ms.getPos()])
myclock.reset()
while not event.getKeys():
    # Get mouse position
    new_pos = ms.getPos()

    # Calculating distance moved since last. Pure pythagoras.
    # Index -1 is the last row.index
    distance_moved = np.sqrt((stimuli[-1].vertices[-1][0]-new_pos[0])**2+(stimuli[-1].vertices[-1][1]-new_pos[1])**2)

    # If mouse has moved the minimum required distance, add the new vertex to the ShapeStim.
    if distance_moved > distance_to_record:
        stimuli[-1].vertices = np.append(stimuli[-1].vertices, np.array([new_pos]), axis=0)

    # ... and show it (along with any "full" ShapeStims
    for stim in stimuli:
        stim.draw()
    myWin.flip()

    # Add a new ShapeStim once the old one is too full
    if len(stimuli[-1].vertices) > screenshot_interval:
        print "new shapestim now!"
        stimuli.append(visual.ShapeStim(myWin, 
                                        lineColor='white', 
                                        closeShape=False, 
                                        vertices=[stimuli[-1].vertices[-1]]))  # start from the last vertex
Jonas Lindeløv
  • 5,442
  • 6
  • 31
  • 54