Skip to main content

Lightweight drawing framework for PsychoPy

Project description

PsychoPy-Scene

本项目是基于 PsychoPy 的轻量级绘制框架,核心源码 <300 行

实验上下文

实验上下文 Context 表示实验的全局参数,包括环境参数、任务参数等。 编写实验的第一步,就是创建实验上下文。

from exp import Context
from psychopy.visual import Window
from psychopy.monitors import Monitor
from psychopy.data import StairHandler

# create monitor
monitor = Monitor(
    name="testMonitor",
    width=52.65,
    distance=57,
)
monitor.setSizePix((1920, 1080))

# create window
win = Window(
    monitor=monitor,
    units="deg",
    fullscr=False,
    size=(800, 600),
)

# create experiment context
ctx = Context(
    win,
    handler=StairHandler(),
)

基本用法

实验任务可以被当作一系列画面 Scene 的组合,每个画面实现了 2 个功能:

  • 绘制刺激:根据参数绘制若干个刺激。
  • 处理交互:监听按键、鼠标点击等事件,并对这些事件进行处理。

接下来的示例将省略实验上下文的创建,直接展示如何编写任务。

画面采用链式调用(method chaining)模式进行配置。 duraion 方法设置持续时间,close_on 方法设置关闭画面的按键:

from psychopy.visual import TextStim

# create stimulus
stim = TextStim(win, text="")
# create scene
scene = ctx.Scene().duration(1).close_on("f", 'j')
# show scene
scene.show()

对于静态刺激的绘制,我们可能只关心要绘制什么内容,而不需要逐帧更新刺激参数。 所以我们可以直接把需要绘制的刺激传入 Scene 方法中,这样刺激将被自动绘制:

from psychopy.visual import TextStim

stim = TextStim(win, text="")
scene = ctx.Scene(stim)
scene.show()

通常我们需要在多个画面之间共享同一种刺激(文本),但是每个画面的内容又不同。 所以我们希望在画面首次绘制前设置刺激参数,而不是为每个画面都新建一个刺激。 这时,我们可以使用 hook 装饰器在 setup 阶段添加 生命周期 钩子, 被装饰的函数将在画面首次绘制前执行:

from psychopy.visual import TextStim

stim = TextStim(win, text="")

# this is equivalent to:
# scene = ctx.Scene(stim).hook('setup')(lambda: stim.text = "Welcome to the experiment")
@(ctx.Scene(stim).hook('setup'))
def scene():
    stim.text = "Welcome to the experiment"

scene.show()

同样,如果我们需要逐帧改变刺激参数(绘制动态刺激),可以在 frame 阶段添加生命周期钩子:

from psychopy import core
from psychopy.visual import TextStim

stim = TextStim(win, text="")

@(ctx.Scene(stim).hook('frame'))
def scene():
    stim.text = f"Current time is {core.getTime()}"

scene.show()

状态管理

大部分情况下,我们并不能一开始就决定刺激参数,而是需要在运行时根据条件动态地设置。 这时,我们可以将这类和画面绘制相关的动态数据作为画面的 状态

from psychopy.visual import TextStim

stim = TextStim(win, text="")

@(ctx.Scene(stim).duration(0.1).hook('setup'))
def scene():
    stim.text = scene.get("text") # get `text` state

for instensity in ['hello', 'world', 'goodbye']:
    scene.show(text=instensity) # set `text` state and show

需要注意的是,show 方法在其初始化阶段 重置状态,详见 渲染时机。 所以,应该通过 show 设置初始状态,并在 show 执行后获取状态。

内置状态

有些状态是由画面配置方法自动设置的:

状态 描述 由哪个方法设置
show_time 开始显示的时间戳 show
close_time 结束显示的时间戳 show
duration 持续时间 duration
keys 被按下的按键 close_on
responses_time 按下按键的时间戳 close_on

处理交互

画面使用发布/订阅模式(Pub/Sub Pattern)处理交互事件,我们可以通过添加事件监听器来处理不同的交互事件。

# add listener for keys, listener will be executed when corresponding key is pressed
ctx.Scene().on(
    space=lambda e: print(f"space key was pressed, this event is: {e}"),
    mouse_left=lambda e: print(f"left mouse button was pressed, this event is: {e}"),
)

需要注意的是,一种事件只能有一个监听器。

# only the last listener will be emitted when multiple listeners are added for the same key
ctx.Scene().on(
    space=lambda e: print("this listener won't be executed")
).on(
    space=lambda e: print("this listener will be executed")
)

数据收集

psychopy 推荐的实验数据收集方式是使用 ExperimentHandler,本库对其进行了简单封装。 现在我们可以使用 ctx.addLine 进行数据收集,并通过 ctx.expHandler 访问 ExperimentHandler 对象。

# it will call `ctx.expHandler.addData` and `ctx.expHandler.nextEntry` automatically
ctx.addLine(correct=..., rt=...)

正如 状态管理 部分所说,交互数据是由 close_on 自动收集的。 如果我们使用了 close_on 方法,可以在 show 方法执行后访问这些状态。

scene = ctx.Scene().close_on("f", "j")
scene.show()

keys = scene.get("keys") # KeyPress or str
responses_time = scene.get("responses_time") # float

当然,我们也可以像 处理交互 部分那样,手动添加监听器。

scene = ctx.Scene().on(space=lambda e: scene.set(rt=core.getTime() - scene.get("show_time")))
scene.show()

rt = scene.get("rt") # float

生命周期

使用 show 方法将画面绘制到屏幕上时,需要经过一系列初始化步骤: 重置并初始化状态、清理事件缓冲、绘制刺激、记录开始显示时间等。 在这个过程中,同时会执行生命周期钩子,让我们在画面渲染的特定阶段进行一些自定义的操作。

如果我们想在首次绘制前更改刺激参数,可以使用 hook 装饰器在 setup 阶段添加生命周期钩子:

from psychopy.visual import TextStim

stim = TextStim(win, text="")

@(ctx.Scene().hook('setup'))
def scene():
    # change stimulus parameters here
    stim.color = "red"

scene.show()

生命周期阶段:

阶段 执行时机 通常用法
setup 首次绘制前 设置刺激参数
drawn 首次绘制后 执行耗时任务
frame 逐帧 更新刺激参数

渲染时机

show 方法逻辑图示:

graph TD
初始化 --> setup --> 首次绘制 --> drawn --> c{是否显示}
c -->|否| 关闭
c -->|是| frame --> 再次绘制 --> 监听交互事件 --> c

最佳实践

上下文和任务分离

建议以函数形式编写任务,并将实验上下文作为第一参数传入,其余参数作为任务的特有参数,并返回实验数据。

from exp import Context

def task(ctx: Context, duration: float):
    from psychopy.visual import TextStim

    stim = TextStim(ctx.win, text="")
    scene = ctx.Scene(stim).duration(duration)
    scene.show()
    return ctx.expHandler.getAllEntries()

只关注特定于任务的逻辑

任务函数不应包含任何与任务本身无关的逻辑,例如:

  • 指导语和结束语
  • block 数量
  • 数据处理、分析、结果展示

一个好的任务函数应该只呈现一个 block,除非 block 间存在数据依赖。 对于需要呈现多个 block 的实验,考虑以下示例。

from exp import Context
from psychopy.visual import Window

def task(ctx: Context):
    from psychopy.visual import TextStim

    stim = TextStim(ctx.win, text="")
    scene = ctx.Scene(stim).duration(0.2)
    scene.show()
    return ctx.expHandler.getAllEntries()

ctx = Context(Window())
data = []
for block_index in range(10):
    block_data = task(ctx)
    data.extends(block_data)

trial 迭代器和任务分离

得益于 psychopy 对 trial 的封装,我们可以很方便地控制下一个 trial。

from psychopy.data import TrialHandler

handler = TrialHandler(trialsList=['A', 'B', 'C'], nReps=1, nTrials=10)
for trial in handler:
    trial # except: 'A" or 'B' or 'C'

为了 trial 迭代器和任务函数的分离,本库提供了 ctx.handler 属性, 它可以用来控制下一个 trial,并将 trial 相关的数据收集到 ctx.expHandler 中。 我们只需要在创建上下文时设置 handler 参数即可。

from exp import Context
from psychopy.visual import Window
from psychopy.data import TrialHandler

def task(ctx: Context):
    from psychopy.visual import TextStim

    stim = TextStim(ctx.win, text="")
    @(ctx.Scene(stim).duration(0.2).hook('setup'))
    def scene():
        stim.text = scene.get("text")
    for instensity in ctx.handler:
        scene.show(text=instensity)
    return ctx.expHandler.getAllEntries()

ctx = Context(
    Window(),
    handler=TrialHandler(trialsList=['A', 'B', 'C'], nReps=1, nTrials=10),
)
data = task(ctx)

但是,当我们打算使用 StairHandler 并访问 ctx.handler.addResponse 时, pylance 类型检查器会报错,即使 ctx.handlerStairHandler 对象。 这是因为 ctx.handler 的类型并没有 addResponse 方法, 为了解决这个问题,我们可以使用 ctx.responseHandler 替代 ctx.handler

需要注意的是,如果在运行时 ctx.handler 没有 addResponse 方法, 访问 ctx.responseHandler 将会抛出异常。 所以在使用 ctx.responseHandler 时,请确保传入的 handler 参数有 addResponse 方法。

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

psychopy_scene-0.1.0rc1.tar.gz (20.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

psychopy_scene-0.1.0rc1-py3-none-any.whl (20.2 kB view details)

Uploaded Python 3

File details

Details for the file psychopy_scene-0.1.0rc1.tar.gz.

File metadata

  • Download URL: psychopy_scene-0.1.0rc1.tar.gz
  • Upload date:
  • Size: 20.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for psychopy_scene-0.1.0rc1.tar.gz
Algorithm Hash digest
SHA256 f6486033fc0cecdbfaa4fe0e85df380522553aa934a52d574515e3819ec46e3c
MD5 fb342aba8057280ce672a09c52acbe00
BLAKE2b-256 ea91369d8e01fe3f931d084e9fa46dc4677d5295464d0cce0811f073cf8a0acd

See more details on using hashes here.

Provenance

The following attestation bundles were made for psychopy_scene-0.1.0rc1.tar.gz:

Publisher: pypi.yaml on bluebones-team/psychopy-scene

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file psychopy_scene-0.1.0rc1-py3-none-any.whl.

File metadata

File hashes

Hashes for psychopy_scene-0.1.0rc1-py3-none-any.whl
Algorithm Hash digest
SHA256 036ee58d64ba56e4f0f43404df854b857d340cada1d14ea3a0246a4878ce167a
MD5 96bfc242c7dc2f0f5976d40930c2dafc
BLAKE2b-256 cf7fafb46c203e78b2e7dfe69e0d1e09d2b6d1d5e649735ad19c6bf0f9a98aef

See more details on using hashes here.

Provenance

The following attestation bundles were made for psychopy_scene-0.1.0rc1-py3-none-any.whl:

Publisher: pypi.yaml on bluebones-team/psychopy-scene

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page