0

I am trying to use Shady to present a sequence of image frames. I'm controlling the flow from another machine, so that I first instruct the machine running Shady to present the first frame, and later on to run the rest of the frames. I create a World instance, and attach to it an animation callback function. Within this callback I listen for communications from the other machine (using UDP). First I receive a command to load a given sequence (stored as a numpy array), and I do

def loadSequence(self, fname):
    yy = np.load(fname)
    pages = []
    sz = yy.shape[0]
    for j in range(yy.shape[1]/yy.shape[0]):
         pages.append(yy[:, j*sz:(j+1)*sz])
    deltax, deltay = (self.screen_px[0] - sz) / 2, (self.screen_px[1] - sz) / 2
    if (self.sequence is None):
        self.sequence = self.wind.Stimulus(pages, 'sequence', multipage=True, anchor=Shady.LOCATION.UPPER_LEFT, position=[deltax, deltay], visible=False)
    else:
        self.sequence.LoadPages(pages, visible=False)

When I receive the command to show the first frame, I then do:

def showFirstFrame(self, pars):
    self.sequence.page = 0 if (pars[0] == 0) else (len(self.sequence.pages) - 1)
    self.sequence.visible = True

But what do I do now to get the other frames to be be displayed? In the examples I see, s.page is set as a function of time, but I need to show all frames, regardless of time. So I was thinking of doing something along these lines:

def showOtherFrames(self, pars, ackClient):
    direction, ack = pars[0], pars[2]
    self.sequence.page = range(1, len(self.sequence.pages)) if (direction == 0) else range(len(self.sequence.pages)-2, -1, -1)

But this won't work. Alternatively I thought of defining a function that takes t as argument, but ignores it and uses instead a counter kept in a global variable, but I'd like to understand what is the proper way of doing this.

jez
  • 14,867
  • 5
  • 37
  • 64
cq70
  • 21
  • 6

2 Answers2

0

When you make s.page a dynamic property, the function assigned to it must take one argument (t), but you can still just use any variables in the space when defining that function, and not even use the time argument at all.

So, for example, you could do something as simple as:

w = Shady.World(...)
s = w.Stimulus(...)
s.page = lambda t: w.framesCompleted

which will set the page property to the current frame count. That sounds like it could be useful for your problem.

swjm
  • 46
  • 5
  • 1
    How does framesCompleted update if there are dropped frames (e.g., if an animation callback takes longer than a refresh interval)? Is it updated only when a frame is actually updated by Shady? I could not find anything in the docs. Ultimately I need something that updates the page count (either up or down) whenever the stimulus is about to be displayed, regardless of frames dropped (I verify if there are frames dropped on the other machine, and mark trials as invalid when that occurs). Thanks – cq70 May 30 '19 at 17:44
  • 2
    @cq70 All dynamics (animation callbacks and dynamic property values) are only *called* once per frame. So that in itself is a way of counting frame by frame. Their input argument is wall-time in seconds, sure, but you can ignore it if you want. `w.framesCompleted` is incremented once per successful frame, whether that frame is late or not. – jez May 30 '19 at 17:59
  • @jez Could you please clarify what you mean when you say that all animation callbacks are only called once per frame? Suppose that I create 10 stimuli, and associate all of them to the same animation callback (with s.page = ... ). Would the callback be called once per frame or ten times per frame? I'm hoping for the former, as I have a bunch of stimuli, only one of which I need to present on any given frame, and I'd like to use a single callback, instead of needing to define 10. – cq70 Feb 11 '20 at 16:47
  • I have a marvelous answer which this margin is too narrow to contain. Can you ask this as a separate question? – jez Feb 11 '20 at 16:49
  • Sure, I'm going to do it now – cq70 Feb 11 '20 at 16:56
0

Your global-variable idea is one perfectly valid way to do this. Or, since it looks like you're defining things as methods of an instance of your own custom class, you could use instance methods as your animation callbacks and/or dynamic property values—then, instead of truly global variables, it makes sense to use attributes of self:

import Shady

class Foo(object):

    def __init__(self, stimSources):
        self.wind = Shady.World()
        self.stim = self.wind.Stimulus(stimSources, multipage=True)
        self.stim.page = self.determinePage  # dynamic property assignment

    def determinePage(self, t):
        # Your logic here.
        # Ignore `t` if you think that's appropriate.
        # Use `self.wind.framesCompleted` if it's helpful.
        # And/or use custom attributes of `self` if that's
        # helpful (or, similarly, global variables if you must).
        # But since this is called once per frame (whenever the
        # frame happens to be) it could be as simple as:
        return self.stim.page + 1
        # ...which is indefinitely sustainable since page lookup
        # will wrap around to the number of available pages.

# Let's demo this idea:
foo = Foo(Shady.PackagePath('examples/media/alien1/*.png'))
Shady.AutoFinish(foo.wind)

Equivalent to that simple example, you could have the statement self.stim.page += 1 (and whatever other logic) inside a more-general animation callback.

Another useful tool for frame-by-frame animation is support for python's generator functions, i.e. functions that include a yield statement. Worked examples are included in python -m Shady demo precision and python -m Shady demo dithering.

It can also be done in a StateMachine which is always my preferred answer to such things:

import Shady

class Foo(object):
    def __init__(self, stimSources):
        self.wind = Shady.World()
        self.stim = self.wind.Stimulus(stimSources, multipage=True)


foo = Foo(Shady.PackagePath('examples/media/alien1/*.png'))         

sm = Shady.StateMachine()
@sm.AddState
class PresentTenFrames(sm.State):
    def ongoing(self): # called on every frame while the state is active
        foo.stim.page += 1
        if foo.stim.page > 9:
            self.ChangeState()

@sm.AddState
class SelfDestruct(sm.State):
    onset = foo.wind.Close

foo.wind.SetAnimationCallback(sm)

Shady.AutoFinish(foo.wind)
jez
  • 14,867
  • 5
  • 37
  • 64
  • There is one thing I am not clear about. In the above example, the method showPages would get called even if the "visible" property for the stimulus is False? – cq70 May 30 '19 at 19:58
  • 1
    Yes: https://shady.readthedocs.io/en/release/source/Shady.html#Shady.Stimulus.Leave – jez May 30 '19 at 20:42