【问题标题】:Controlling frame-by-frame drawing in Shady在 Shady 中控制逐帧绘制
【发布时间】:2019-05-29 05:22:42
【问题描述】:

我很难理解如何控制 Shady 在屏幕上绘制的内容的流动。我已经习惯了 Psychtoolbox,您可以在其中通过在后台缓冲区上绘图来不断地添加帧,然后通过调用 Flip() 将其显式推送到屏幕上。使用 Shady 时,事情似乎会自动发生,因此当您将 Stimulus 对象添加到 World 时,它会尽快被绘制。但是如果我必须添加几个刺激,我怎么能保证屏幕上没有更新,直到它们都被绘制出来?

例如,假设我想绘制一个空白屏幕,并在其左上角的顶部绘制一个小方块。我可以这样做:

w = Shady.World()
s1 = w.Stimulus(None, 'blank', envelopeSize=[1920, 1200], backgroundColor=[0, 0, 0], z=0)
s2 = w.Stimulus(None, 'square', envelopeSize=[20, 20], x=-950, y=590, backgroundColor=[1, 1, 1], z=1)

但是我如何保证我不会从第 n 帧开始绘制 s1,从第 n+1 帧开始绘制 s2?也许它们可以组合成一个 Stimulus 对象,但我想将它们分开(在我的实际问题中,小方块实际上是用来触发光电管的,所以我需要能够将其闪烁一次帧,在任务期间的不同时间)。

【问题讨论】:

    标签: python shady


    【解决方案1】:

    首先,最好阅读 Concurrency 上的 Shady 文档。前两个示例(在“运行单线程”标题下)没有您担心的问题,因为在实际渲染单个帧之前创建了 Stimulus 实例。这是一个简单的例子:

    import Shady
    w = Shady.World(threaded=False)
    # s1 = ...    
    # s2 = ...
    # ...    
    w.Run()
    

    除此之外,你是对的,Shady 固有的范式转变之一是你自己永远不会调用任何 flip() 等价物。起初这可能会让人感到陌生,但请放心:在我们基于 Shady 构建的应用程序中,我们不会错过自己致电 flip() 的日子。刺激参数的更新在回调中完成,例如:

    • 您可以选择安装到每个WorldStimulus 实例中的所谓“动画回调”;

    • 您可以分配给任何 WorldStimulus 属性的动态(可调用)值(这些值会在任何动画回调之后立即进行评估);

    • 响应按键等的事件回调

    您在同一个回调中所做的更改保证会在同一帧上呈现——这就是我们如何在运行单线程时保持严格控制时间的答案。确实,如果您运行多线程并一个接一个地发出w.Stimulus() 调用,则不能保证它们出现在同一帧上(实际上,它们保证出现在不同帧上,因为非绘图线程中的.Stimulus() 调用实际上将刺激创建的实际工作推迟到绘图线程中,然后等待直到完成后再返回)。可能的解毒剂是(1)运行单线程; (2) 在专用的Prepare() 方法中执行所有w.Stimulus() 创建调用,如文档的第二个示例中所示;或 (3) 确保刺激物在创建时具有visible=False,并且仅在以后使它们可见。在所有这些情况下,我们小心地将刺激(可能很慢)的创建与属性的操纵分开。

    前两种回调类型与您所描述的内容最相关。它们在 Shady 的 "Making properties dynamic" 文档中有所介绍。在它们提供的框架内,有许多不同的方法来实现您描述的目标(与 Python 和 Shady 一样)。就个人而言,我喜欢使用StateMachine 实例作为我的动画回调。下面是我如何创建一个简单的重复呈现的刺激,它的开始是由一个传感器贴片闪烁一帧来预示的:

    import random
    import Shady
    
    w = Shady.World(canvas=True, gamma=2.2)
    
    
    # STIMULI
    
    Shady.Stimulus.SetDefault(visible=False)
    # let all stimuli be invisible by default when created
    
    gabor = w.Sine(pp=0)
    sensorPatch = w.Stimulus(
        size = 100,  # small,
        color = 1,   # bright,
        anchor = Shady.UPPER_LEFT, # with its top-left corner stuck...
        position = w.Place(Shady.UPPER_LEFT),  # to the top-left corner of the screen
    )
    
    
    # STATE MACHINE
    
    sm = Shady.StateMachine()
    
    @sm.AddState
    class InterTrialInterval(sm.State):
        # state names should be descriptive but can be anything you want
    
        def duration(self):
            return random.uniform(1.0, 3.0)
    
        next = 'PresentGabor'
    
    @sm.AddState
    class PresentGabor(sm.State):
    
        def onset(self):
            gabor.visible = True
            sensorPatch.visible = Shady.Impulse() # a dynamic object: returns 1.0 the first time it is evaluated, then 0.0 thereafter
    
        duration = 2.0
    
        def offset(self):
            gabor.visible = False
    
        next = 'InterTrialInterval'
    
    
    w.SetAnimationCallback( sm )
    # now sm(t) will be called at every new time `t`, i.e. on every frame,
    # and this will in turn call `onset()` and `offset()` whenever appropriate
    

    【讨论】:

    • 谢谢你。在状态的持续时间是什么?秒?如果是这样,如何将其四舍五入到帧(尤其是在“丢弃”帧的情况下)?
    • 秒,是的。动画(动态属性,以及动画回调)总是被认为是一个函数f(t),时间为t,以秒为单位。量化到帧被认为是尽可能晚地发生。因此,如果您有(比如说)时间的正弦函数,但一帧被延迟或跳过,那么肯定会在该帧上出现明显的抖动,但该跳过或延迟没有“敲击”效应,因此没有需要更正它:您的 next 回调将收到正确的挂墙时间t,就好像什么都没发生一样。
    • StateMachine 只是该原则的一个特例:它被设计为尽可能地表现得好像它在连续(未量化)时间轴上改变状态。 onset()offset() 方法在它们应该发生的理论时间点之后发生的第一帧被调用。熟悉这一点的最好方法是持怀疑态度的经验主义:首先在回调中打印出w.timeInSecondsw.framesCompleted(但在最终版本中不要这样做——即使打印到控制台也是有成本的);测量传感器闪烁的时间。
    • 但是,假设我想确保呈现固定数量的图像帧(如果丢帧,这可能需要更多的监视器帧)。实现这一目标的最佳方法是什么?我使用多页刺激,理想情况下,我想在动画回调函数中将 s.page 设置为 range(num) 以触发开始,或类似的东西(这样在每次调用时它都会显示下一页,直到结束,然后停止)。
    猜你喜欢
    • 2020-06-06
    • 1970-01-01
    • 2015-09-20
    • 2019-03-30
    • 1970-01-01
    • 2020-01-11
    • 2013-10-07
    • 2014-05-30
    • 1970-01-01
    相关资源
    最近更新 更多