Search code examples
psychopy

efficient way to draw continuous line in psychopy


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()

Solution

  • 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