Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Supported by

[open] "Animated" distractor

edited November 2012 in OpenSesame

Greetings.
I am coding an experiment which requires a distractor stimuli in some of the trial conditions. The distractor is supposed to be an X, that scrolls diagonally a bit off to the side of the main stimuli in the center. I have written the following inline script, in which I have attempted to create a for loop, where each subsequent "frame" of the canvas would offset the x and y coordinates of the distractor by 30 pixels, creating a short 5-frame "animation". The first appearance of the stimuli in each trial is drawn from a range of random offsets, but all subsequent iterations should follow in the above 30-pixel pattern. But alas, they all appear at random. Does anyone see what I am missing?

Here's the code:

from openexp.canvas import canvas
import random

c1 = self.copy_sketchpad("stimuli")    # copy stimuli sketchpad

start=[-250, -150, -100, 100, 250, 500]    # starting offset range for x and y

x = random.choice(start) + c1.xcenter()    # random choice from offset range added to canvas center
y = random.choice(start) + c1.ycenter()    # random choice from offset range added to canvas center

p_step = 30    # pixel steps
t_step = 500    # time steps

for time in range(0, 1500, t_step):     # for loop to display animated distractor "X"
    c1.clear()  
    c1.text('X', x = x + p_step, y = y + p_step)
    c1.show() 
    self.sleep(100) 

Best regards,
Jákup

Comments

  • edited November 2012

    Hi Jákub!

    from openexp.canvas import canvas
    import random
    
    c1 = self.copy_sketchpad("welcome")    # copy stimuli sketchpad
    
    start=[-250, -150, -100, 100, 250, 500]    # starting offset range for x and y
    
    x = random.choice(start) + c1.xcenter()    # random choice from offset range added to canvas center
    y = random.choice(start) + c1.ycenter()    # random choice from offset range added to canvas center
    
    p_step = 30    # pixel steps
    t_step = 500    # time steps
    
    print((x,y))
    
    for time in range(0, 1500, t_step):     # for loop to display animated distractor "X"
        c1.clear()  
        c1.text('X', x=x, y=y)
        x += p_step
        y += p_step
        c1.show() 
        self.sleep(100)
    

    x = x+ p_step when used in the text() function of a Canvas object does not mean the same as x = x+ p_step when used as a single line of Python code. So, basically, your code works, but it keeps drawing the X on the same location. With the small adjustment you see above, the x- and y-coordinates now do change with every step.

    EDIT: I now notice that you speak of a five frame animation, but what you have now is a script for three frames (since 1500/t_step = 3). I don't know if this is your whole script, so it might be that you've solved this already, but I thought a small heads-up was appropriate :)

    Good luck!

    Edwin

  • edited 5:09PM

    Hello Edwin.
    Thank you soooo much!
    I love it when the solution is so elegant as this :)

    And thanks for the heads up! With the solutions I was trying out, a three-step animation was easier to keep track of, but the final version will indeed be with 300 ms steps :)

    Thanks again man!

    • Jákup
  • edited 5:09PM

    Hello again.
    Now I have another problem I did not foresee. This distractor animation is supposed to overlay the experimental stimuli, which I thought I could implement by displaying another copy of the sketchpad in the for loop:

    from openexp.canvas import canvas
    import random
    
    c1 = self.copy_sketchpad("stimuli") #copy stimuli sketchpad
    c0 = self.copy_sketchpad("stimuli") #copy stimuli sketchpad
    
    start=[-250, -150, -100, 100, 250, 500] #starting offset range for x and y
    
    x = random.choice(start) + c1.xcenter() #random offset range + canvas center
    y = random.choice(start) + c1.ycenter() #random offset range + canvas center
    
    p_step = 30 #pixel steps
    t_step = 300 #time steps
    
    for time in range(0, 1500, t_step): #for loop to display animated distractor "X"    
        c1.clear()
        c1.text('X', x = x, y = y) 
        x += stepx
        y += stepy
        c1.show()               # show the distractor
        self.sleep(t_step)
        c0.show()               # show the background sketchpad, sans distractor
        self.sleep(t_step)
    

    However this gives a blinking display, each time the distractor sketchpad (c1) is cleared in the for loop. I can get past this by discounting the clear() function, but then makes the distractor Xs appear as a continuous line progressing across the screen instead of one X for each frame.

    How can I implement this distractor animation while keeping a background sketchpad constant?

    Best regards,
    Jákup

  • edited November 2012

    What you're doing now is updating the screen every t_step with either the blank or the distractor display. This will cause a blinking between those to displays at a rate of t_step (causing an annoying flickering). Why don't you just draw the X on the canvas with experimental stimuli?

    Also, without the clear function, you draw the distractor on different locations, but on the same canvas. What you want to do, is copy the basic canvas (with everything on it but the distractor) and draw the distractor onto that canvas for each different location.

    from openexp.canvas import canvas
    import random
    
    c1 = self.copy_sketchpad("stimuli") #copy stimuli sketchpad
    c0 = self.copy_sketchpad("stimuli") #copy stimuli sketchpad
    
    start=[-250, -150, -100, 100, 250, 500] #starting offset range for x and y
    
    x = random.choice(start) + c1.xcenter() #random offset range + canvas center
    y = random.choice(start) + c1.ycenter() #random offset range + canvas center
    
    p_step = 30 #pixel steps
    t_step = 300 #time steps
    
    for time in range(0, 1500, t_step): #for loop to display animated distractor "X"    
        c1.copy(c0) # copy stimulus canvas without distractor
        c1.text('X', x = x, y = y) # draw distractor onto stmulus canvas
        x += stepx # change x-coordinate
        y += stepy # change y-coordinate
        c1.show() # update display
        self.sleep(t_step) # wait for t_step
    

    Mind you, in this way of presenting stimuli, you are drawing inside of the loop where you are updating the screen. This means you're adding extra delay. For this small drawing and calculating two new coordinates, it won't be that much extra delay (probably neglectable considering the size of t_step), but when you are doing more elaborate stuff (and/or really precise timing), this would be something to take into consideration.

    NOTE: I haven't tested the above script myself, so if it doesn't work, please inform me ;)

    Good luck!

    Edwin

  • edited 5:09PM

    Aaaah ok. Thanks again! Especially for explaining exactly what is going with(out) the clear function in contrast with copy(). That did exactly what I wanted. So thanks again! :)

    The reason I'm doing this in script form, is that I am transferring an existing matlab experiment to OpenSesame, and this was how the distractor was coded. When you ask, why I don't draw the X on the canvas with experiment stimuli, are you thinking about drawing an X in a sketchpad, and then having its coordinated defined by loop variables, or something similar? Would that actually be faster then in the above case? And if I stick with this, perhaps it would be preferable to move some of that code to the prepare faze.

    But thank you again, so much, for your help :)

    ATB

    • Jákup
  • edited 5:09PM

    No, I meant to draw the X on the same canvas as the stimuli were on (in this case: c1), since you had two different canvasses on which you either drew the X or only the stimuli. I wouldn't recommend drawing the X in a sketchpad, since it would produce a messy experiment (with either a lot of different sketchpads or an extra loop, both constructions having their downside).

    I've created an experiment with moving stimuli a while back: a Stoplight Game. The screen drawing for that one took quite a while (that is: on slow computers, the frame rate would drop). What I did there was creating all my canvasses up front and then just playing them in the trials. You could do something similar, by creating the canvasses in the prepare phase (or even before the experimental loop).

    Preparatory script:

    from openexp.canvas import canvas
    import random
    
    global c1, t_step
    
    # parameters
    start=[-250, -150, -100, 100, 250, 500] #starting offset range for x and y
    p_step = 30 #pixel steps
    t_step = 300 #time steps
    
    # empty list for the canvasses
    c1 = []
    
    # create canvasses
    c0 = self.copy_sketchpad("stimuli") #copy stimuli sketchpad
    x = random.choice(start) + c0.xcenter() #random offset range + canvas center
    y = random.choice(start) + c0.ycenter() #random offset range + canvas center
    for time in range(0, 1500, t_step): #for loop to display animated distractor "X"    
        newcanvas = self.copy_sketchpad("stimuli") #copy stimuli sketchpad
        newcanvas.copy(c0) # copy stimulus canvas without distractor
        newcanvas.text('X', x = x, y = y) # draw distractor onto stmulus canvas
        x += p_step # change x-coordinate
        y += p_step # change y-coordinate
        c1.append(newcanvas) # add new canvas to the list of canvasses
    

    Run script:

    from openexp.canvas import canvas
    
    global c1, t_step
    
    for canvas in c1:
        canvas.show()
        self.sleep(t_step)
    

    But, as I said before: you do a really small amount of drawing, so I don't think this would be necessary.

  • edited November 2012

    Ah I see. Well, I need to have two canvases because, in addition to the animation we talked about above, I need it to repeat itself 5 times within a 1800 ms window. And each iteration should be as random as above. If it would be possible with one canvas, I would love to do that, as I loath redundancy :)

    Again, thanks for the example. I really like your solution. It sort of keeps the messy stuff out-of-mind, and hidden behind the neat run tab :) However, I have not been able to figure out, how to use this if movement should be randomized at each iteration. I think I shall need some more practice, in order to fully grasp and utilize the preparatory phase of canvas scripting.

    As to the timing issue, while it is not a problem with my short animation, it is when I try to implement the repeated animation in the 1800 ms window in nested loops. I've been trying various range() solutions and tested the timing using print time.time(), and I cannot get it to consistently perform as need. With one for loop, the timing was truly excellent on both Psycho and Legacy backends.

    This is the best solution, I have come up with, after working on the problem for the better part of a full 24 hours [yes, I am a neophyte coder :p ]. The timing is a bit off, but the animation works as intended:

    from openexp.canvas import canvas
    from random import choice, randint
    import time 
    global time
    
    c1 = self.copy_sketchpad("stimuli") #copy stimuli sketchpad
    c0 = self.copy_sketchpad("stimuli") #copy stimuli sketchpad
    
    dur = 9 # 90 steps
    step = 20 # of 20 ms (which should equal 1800ms) 
    
    for i in range(dur): # for loop, controls duration  
    
        ###### randomize distractor movement algorithm
        
        # parameters:
        stepx = 0 # pixel steps for x
        stepy = 0 # pixel steps for y
        p_step = 30 # no. of pixels to steps
        start=[-200, -150, -100, 100, 150, 200] #starting offset range for x and y
        x = choice(start) + c1.xcenter() # random start offset (range + canvas center)
        y = choice(start) + c1.ycenter() # random start offset (range + canvas center)
        
        # randomize directionality of pixel steps
        p1=randint(1,2) #choose 1 or 2
        p2=randint(1,3) #choose 1, 2 or 3
        if p1 == 1 and p2 == 1: # random p_step directions based on p1 and p2:
            stepx = p_step
            stepy = -p_step
        if p1==2 and p2==1:
            stepx = -p_step
            stepy = p_step
        if p1==1 and p2==2:
            stepx = p_step
            stepy = p_step
        if p1==2 and p2==2:
            stepx = -p_step
            stepy = -p_step
        if p1==1 and p2==3:
            stepx = p_step
            stepy = 0
        if p1==2 and p2==3:
            stepx = -p_step
            stepy = 0   
        
        for i in range(5): # 5 fame distractor animation
            c1.copy(c0) # copy fresh canvas
            c1.set_font('mono', 40)  
            c1.text('+', x = x, y = y) # display a large +
            x += stepx # increment x
            y += stepy # increment y 
            ########## but keep distractor in central areas of the screen:
            if x > 812: 
                stepx = -p_step  
            if x < 212: 
                stepx = p_step
            if y > 560: 
                stepy = -p_step
            if y < 204: 
                stepy = p_step
            c1.show() # show canvas
            print time.time() # print time stamp to debug window for testing
            self.sleep(step) # wait 20 ms
    

    Is it possible to shift most of this to the preparatory phase? And does anyone have a clue, why the timing on this ranges from 1100 to 1500 ms? I suspect, that I'm missing something crucial, but for the life of me, I cannot seem to be able to find it.

    Best wishes for the weekend!

    • Jákup
  • edited December 2012

    Booyah! Using the system time seems to do the trick:

    from openexp.canvas import canvas
    import random
    import time 
    global time
    
    # copy stimuli sketchpad:
    c1 = self.copy_sketchpad("stimuli") 
    c0 = self.copy_sketchpad("stimuli")  
    
    # get starting-time
    t0 = time.time()*1000  # in ms
    
    ### and run for 1800 ms:
    while time.time()*1000 < t0 + 1800:  
    
        ### randomize distractor movement algorithm:
        p_step = [-30, 0, 30] # step directionality
        stepx = random.choice(p_step) # pixel steps for x
        stepy = random.choice(p_step) # pixel steps for y
        
        start=[-200, -150, -100, 100, 150, 200] # starting offset range for x and y
        x = random.choice(start) + c1.xcenter() # random start offset + canvas center
        y = random.choice(start) + c1.ycenter() # random start offset + canvas center
        
        ### animate distractor:
        for i in range(5): # 5 frames 
            c1.copy(c0) # copy fresh canvas
            c1.set_font('mono', 40)  
            c1.text('+', x = x, y = y) # display a large "+"
            x += stepx # increment x
            y += stepy # increment y 
    
            ### keep distractor in central areas of the screen:
            if x > 812: 
                stepx = -stepx
            if x < 212: 
                stepx = stepx
            if y > 560: 
                stepy = -stepy
            if y < 204: 
                stepy = stepy
    
            c1.show() # show canvas
            print time.time() # print time stamp to debug window for testing
            self.sleep(20) # wait 20 ms
    

    Now it runs for exactly 1800 ms :)

    And you gotta love Python's randomizing functions - compare the p_step list and the choice() function with the unsightly if calls in the prior example :)

    All the best!

    • Jákup
  • edited December 2012

    Hi Jakup!

    Great that you got it to work! And the random module is indeed a really nice one. On your question: it is possible to move most of this to the prepare phase. If this is the prepare phase of an item placed within the trial sequence (thus run once every trial) this will not effect the randomization, since at the start of every trial the prepare phase is run (with your random.choice solution).

    BUT considering the large amount of canvasses that would need to be stored, it would make more sense to prepare all the individual canvasses up front (even before the trial sequence) and then in the prepare phase, randomly determine which of the screens to run. In the run phase you would simply show the specified screens. This, however, is a far to elaborate process to implement when the drawing time of your own solution isn't that large. So basically, I'd keep it as it is :)

    What might be something to check, is whether you show your canvasses with appropriate timing with regard to your display's refresh rate. You'll probably show 'em two frames each (about 33 ms, if you use a 60 Hz monitor), but I'm not quite sure how fast the drawing is done.

    Best!

    Edwin

  • edited 5:09PM

    So when preparing canvases up front, would that simply mean inserting inline scripts at the top of OS overview tree, in which all the canvases were coded, declaring them global and then preparing and running them as needed further down the experiment? And would these then be created when OpenSesame launches the experiment, so you could have faster presentation times in exchange for one longer loading time?

    I think you're right, btw., that my simple calculations do not necessarily need this amount of optimization at this point, but it's an interesting distinction to learn early on, before I might move on to more elaborate experiments.

    In regards to the refresh rate, yes I noticed that sometimes, the time printed to the debug window would indicate that the timing was off by about a frame. I don't think it is critical for our paradigm, but thanks for pointing it out. I will run sum tests on the actual computers where we will conduct the experiment, just to be sure.

    And thanks again for all your help! I wouldn't have come this far without it :)

    All the best!

    Jákup

Sign In or Register to comment.