12 – PsychoPy (2)

Einführung in Python und PsychoPy

Autor

Clemens Brunner

Veröffentlicht

19. Januar 2023

PsychoPy Coder

In dieser Einheit werden wir uns mit dem Python-Code beschäftigen, welcher jedem PsychoPy-Experiment zugrunde liegt. Selbst wenn ein Experiment mit der grafischen Oberfläche Builder erstellt wurde, wird dieses vor dem Ausführen zuerst in ein Python-Script konvertiert. Erst dieses Script wird dann tatsächlich von Python ausgeführt. Selbstverständlich kann man aber auch direkt ein Python-Script erstellen um PsychoPy zu verwenden – dies führt in den allermeisten Fällen zu wesentlich kompakterem Code.

PsychoPy Coder ist ein einfacher Code-Editor, mit dem man Python-Scripts (also insbesondere auch PsychoPy-Experimente) erstellen kann. Wenn man aber lieber einen anderen Editorvverwenden möchte (wie z.B. Visual Studio Code oder PyCharm), kann man das ohne Einschränkungen auch tun. Falls man aber die Standalone-Pakete von PsychoPy unter Windows oder macOS verwendet, sollte man PsychoPy Coder verwenden, da dies keine zusätzlichen Konfigurationsschritte erfordert.

Hinweis

Unter macOS kann man die Python-Umgebung von PsychoPy, welche mit dem Standalone-Installer installiert wurde, gar nicht mit anderen Editoren verwenden. Hier ist man also auf PsychoPy Coder angewiesen. Unter Windows kann man in Visual Studio Code den Python-Interpreter der PsychoPy-Installation auswählen und verwenden.

Der Code des Stroop-Experiments

Wenn man in PsychoPy Builder ein Experiment grafisch erstellt hat, kann man sich den zugehörigen Code mit ToolsCompile (bzw. durch Klicken auf das Icon “Compile to Python script”) erzeugen lassen. PsychoPy Coder wird dann automatisch mit dem generierten Script geöffnet.

Probieren wir das mit unserem Stroop-Experiment aus der letzten Einheit aus. Wir vereinfachen dieses Experiment aber, indem wir die Routine “Feedback” entfernen (Rechtsklick auf die Routine und “remove” auswählen). Auch das entsprechende Tab können wir schließen und aus dem Experiment entfernen.

Mit einem Klick auf das Icon “Compile to Python script” wird das Experiment dann in ein Python-Script konvertiert. Dieses wird auch gleich mit PsychoPy Coder geöffnet.

Das erstellte Script ist für ein so einfaches Experiment mit 487 Zeilen relativ lang. Es enthält viel Code, der für das Experiment eigentlich gar nicht notwendig ist, wie z.B. viele import-Statements die im weiteren Script nicht verwendet werden. Wir werden uns dieses Script nicht genauer ansehen, sondern versuchen herauszufinden, wie einfachere Komponenten direkt mit Code geschrieben werden können.

Minimales Experiment

Das kürzeste PsychoPy-Experiment ist fünf Zeilen lang (bzw. sechs Zeilen mit einer Leerzeile). Es besteht aus einem leeren Fenster, welches für 2 Sekunden sichtbar ist.

from psychopy import core, visual

win = visual.Window(size=[800, 400])
core.wait(2)
win.close()
core.quit()

Obwohl dieses Experiment nichts tut, kann man die grundlegende Struktur eines Experimentes erkennen.

Zu Beginn importiert man alle nötigen Module aus dem psychopy-Paket. Die PsychoPy-Dokumentation enthält eine detaillierte Beschreibung aller vorhandenen Module, wir benötigen hier aber nur die Module core (Basisfunktionen) und visual (visuelle Stimuli).

Danach erstellen wir das Programmfenster, in dem das Experiment läuft. Dazu rufen wir visual.Window auf, welches uns ein Programmfenster der angegebenen Größe erzeugt und zurückgibt (wir nennen dieses Fenster win).

Ab jetzt könnte man alle Stimuli erzeugen, die im Verlauf des Experimentes benötigt werden. Wir werden dies in einem weiteren Beispiel sehen, dieses Experiment enthält aber keine Stimuli. Es wird mit core.wait(2) lediglich zwei Sekunden gewartet. Danach ist das Experiment zu Ende, und die beiden letzten Zeilen schließen das Programmfenster und beenden das Experiment.

Hinweis

Das Script kann durch Klicken auf das Icon “Run experiment” ausgeführt werden.

Darstellen von Stimuli am Bildschirm

In einem richtigen Experiment verwendet man natürlich diverse Stimuli. Im Folgenden werden wir einige visuelle Stimuli näher betrachten. PsychoPy kann mit den im Modul visual enthaltenen Funktionen eine große Anzahl an unterschiedlichsten visuellen Stimuli darstellen, wie beispielsweise Text, Formen (Kreise, Rechtecke, Linien), Bilddateien, Muster, usw.

Die Darstellung von visuellen Stimuli am Bildschirm funktioniert wie folgt:

  1. Zunächst wird das Fenster win erzeugt. Darin können später alle Stimuli gezeichnet werden.
  2. Anschließend kann ein Stimulus-Objekt stim erzeugt werden, welches dem Fenster zugeordnet wird. Beim Erzeugen des Stimulus kann man das Aussehen wie z.B. die Farbe, die Position, die Orientierung, etc. festlegen. Alle Eigenschaften eines Stimulus können aber auch nachträglich im Verlauf des Experimentes geändert werden.
  3. Schließlich wird der Stimulus stim mit der Methode stim.draw() gezeichnet. Das Zeichnen erfolgt aber zunächst unsichtbar im sogenannten Backbuffer – man kann sich diesen Buffer wie die Rückseite des Bildschirms vorstellen. Alles was gezeichnet wird, landet zuerst einmal im Backbuffer und ist somit noch nicht am Bildschirm sichtbar.
  4. Wenn alle gewünschten Stimuli im Backbuffer gezeichnet sind, kann der gesamte Inhalt des Backbuffers mit win.flip() sichtbar gemacht werden. Dies bedeutet, dass der Inhalt des unsichtbaren Backbuffers in den sichtbaren Frontbuffer übertragen wird – der Backbuffer wird dadurch wieder geleert. Diese Operation ist mit der Bildwiederholfrequenz synchronisiert, d.h. bei einem Bildschirm mit 60 Hz wird das Bild 60 Mal pro Sekunde neu gezeichnet. Ein Aufruf von win.flip() wird dann bei der nächsten Bildschirm-Aktualisierung durchgeführt.

Der Vorteil dieser Aufteilung in Backbuffer und Frontbuffer ist, dass alle Stimuli im Backbuffer exakt gleichzeitig sichtbar werden, und zwar genau dann wenn man win.flip() aufruft. Würden die Stimuli direkt nach dem Zeichnen sichtbar werden, könnte es vorkommen, dass die verschiedenen Komponenten in einem Bild zu unterschiedlichen Zeiten gezeichnet werden.

Unser minimales Experiment können wir nun also mit einem visuellen Text-Stimulus wie folgt erweitern:

from psychopy import core, visual

win = visual.Window(size=[800, 400])
text = visual.TextStim(win, "Text\n\n2 Sekunden sichtbar.")
text.draw()
win.flip()
core.wait(2)
win.close()
core.quit()

Warten auf einen Tastendruck

Wir können nun schon fast einen Instruktionsbildschirm anzeigen – es fehlt nur mehr die Möglichkeit, die Instruktionen so lange anzuzeigen bis die Versuchsperson eine bestimmte Taste (z.B. die Leertaste) drückt. Das PsychoPy-Modul event beinhaltet diverse Funktionen um Benutzereingaben (wie z.B. von der Tastatur) abzufragen. Mit der Funktion event.waitKeys() kann man so lange warten, bis eine Taste gedrückt wird. Insbesondere kann man mit dem keyList-Argument die erlaubten Tasten als Liste angeben. Unser Beispiel kann damit wie folgt erweitert werden:

from psychopy import core, visual, event

win = visual.Window(fullscr=True)
s = """Text Rotation

Press 'left' to rotate the text to the left.
Press 'right' to rotate the text to the right.

Press Space to start!
"""
text = visual.TextStim(win=win, text=s, color="white", height=0.05)
text.draw()
win.flip()
event.waitKeys(keyList=["space"])
win.close()
core.quit()

Durch die Angabe von keyList=["space"] wird also so lange gewartet bis die Leertaste gedrückt wird. Erst dann wird das Script in der nächsten Zeile fortgesetzt.

Verwenden einer Schleife

Wir versuchen jetzt, das im vorigen Beispiel beschriebene Experiment umzusetzen. Nach dem Instruktionsbildschirm soll ein Text (nämlich PsychoPy) am Bildschirm angezeigt werden. Wenn man die linke Pfeiltaste drückt, soll der Text nach links rotieren. Drückt man die linke Taste wiederholt, wird die Rotation schneller. Drückt man die rechte Pfeiltaste, verlangsamt die Rotation nach links und man kann den Text dann nach rechts rotieren lassen. Wenn die Leertaste gedrückt wird soll das Experiment beendet werden.

from psychopy import core, visual, event

win = visual.Window(fullscr=False)
s = """Text Rotation

Press 'left' to rotate the text to the left.
Press 'right' to rotate the text to the right.

Press Space to start!
"""
text = visual.TextStim(win=win, text=s, color="white", height=0.05)
text.draw()
win.flip()
event.waitKeys(keyList=["space"])

text.setText("PsychoPy")
text.setHeight(0.15)

rotation = 0
speed = 0

while True:
    keys = event.getKeys(keyList=["space", "left", "right"])
    if keys:  # a key was pressed
        if keys[0] == "space":  # exit
            break
        elif keys[0] == "left":
            speed -= 1
        else:
            speed += 1
    rotation += speed
    text.setOri(rotation)
    text.draw()
    win.flip()

win.close()
core.quit()

Ein neuer Bestandteil in diesem Experiment ist die Tatsache, dass wir unseren existierenden Text-Stimulus text wiederverwenden. Beim Initialisieren zeigen wir zunächst die Instruktionen damit an, danach setzen wir aber den Text mit text.setText("PsychoPy") sowie die Größe mit text.setHeight(0.15) auf andere Werte. Diese Operation ist oft schneller als das Erzeugen eines neuen Text-Stimulus und sollte deswegen in den meisten Fällen bevorzugt werden.

Danach folgt eine Endlosschleife while True. Man erkennt aber schon, dass diese Schleife mit break auch verlassen werden kann, und zwar wenn die Leertaste gedrückt wird.

Die erste Anweisung innerhalb der Schleife gibt eine Liste der gedrückten Tasten zurück. Allerdings werden durch das keyList-Argument nur die angegebenen Tasten berücksichtigt (alle anderen Tasten werden ignoriert). Wenn keine Taste gedrückt wurde, ist keys eine leere Liste. Mit if keys: überprüft man daher, ob die Liste keys Einträge enthält oder nicht (wenn die Liste Einträge enthält wurde eine Taste gedrückt, dann ist der Ausdruck True; wenn die Liste leer ist wurde keine Taste gedrückt und der Ausdruck ist False).

Wenn die gedrückte Taste die linke Pfeiltaste ist, wird die Rotationsgeschwindigkeit speed (anfänglich auf 0 gesetzt) um den Wert 1 verringert. Wenn die gedrückte Taste die rechte Pfeiltaste ist, wird speed um den Wert 1 erhöht. Die Rotation rotation des Textes wird nun durch Addition des aktuellen Wertes von rotation und der Rotationsgeschwindigkeit speed bestimmt. Danach wird der Text mit text.setOri(rotation) auf den Winkel rotation gedreht. Jetzt kann der Text-Stimulus gezeichnet werden und anschließend wird der Inhalt des Buffers mit win.flip() angezeigt. Sobald die Leertaste gedrückt wird, wird die Schleife verlassen und die letzten beiden Zeilen des Scripts ausgeführt.

Tipp

Die Schreibweise x += 1 erhöht den Wert von x um 1. Man könnte auch x = x + 1 schreiben.

Durch den Befehl win.flip() werden die aktuellen Stimuli mit der Bildwiederholfrequenz gezeichnet. In den meisten Fällen ist dies 60 Hz, d.h. die while-Schleife wird 60 mal pro Sekunde durchlaufen.

Animierte Stimuli

Im vorigen Beispiel haben wir gesehen, wie wir die Orientierung eines Text-Stimulus mit text.setOri(rotation) in einer Schleife kontinuierlich verändern können. Dasselbe Prinzip können wir auf beliebige visuelle Stimuli übertragen, um diese zu animieren.

Das folgende Beispiel zeigt, wie man einen Kreis bewegt und gleichzeitig dessen Farbe ändert (siehe dazu die PsychoPy-Hilfe über Farben).

from psychopy import core, visual, event

win = visual.Window(size=[800, 800])

pos = [0, 0]  # initial x, y position
speed = [0.03, -0.02]  # initial x, y speed
colors = ["red", "yellow", "green"]
color = 0
circle = visual.Circle(win, radius=0.1, fillColor=colors[0], lineColor=None)

while True:
    keys = event.getKeys(keyList=["escape"])
    if keys:
        break
    pos[0] += speed[0]
    pos[1] += speed[1]
    if pos[0] < -1 or pos[0] > 1:  # is the x position on the screen border?
        speed[0] = -speed[0]  # flip x speed direction
        color = (color + 1) % 3  # set next color
    if pos[1] < -1 or pos[1] > 1:  # is the y position on the screen border?
        speed[1] = -speed[1]  # flip y speed direction
        color = (color + 1) % 3  # set next color
    circle.setPos(pos)
    circle.setFillColor(colors[color])
    circle.draw()
    win.flip()

win.close()
core.quit()

Timer

In den meisten Experimenten ist es wichtig, Stimuli zu definierten Zeitpunkten anzuzeigen. Wir haben bereits eine Möglichkeit kennengelernt, wie man in PsychoPy eine bestimmte Zeit warten kann: core.wait(2) wartet z.B. exakt zwei Sekunden bevor die nächste Codezeile ausgeführt wird. Dies hat allerdings den großen Nachteil, dass während der Wartezeit nichts passieren kann, da das gesamte Experiment für diese Zeitdauer blockiert ist.

Im Gegensatz dazu blockieren sogenannte Timer den Programmablauf nicht. Generell gibt es in PsychoPy zwei verschiedene Arten an Timern:

  1. core.Clock beinhaltet die seit der Erstellung des Timers vergangene Zeit (in Sekunden), zählt also hinauf.
  2. core.CountdownTimer beinhaltet die seit der Erstellung des Timers noch übrige Zeit (in Sekunden), zählt also hinunter.

Ersteren Timer erstellt man mit clock = core.Clock(). Mit dieser Zuweisung beginnt der Timer bei Sekunde 0 zu ticken. Mit clock.getTime() kann man dann die seit der Erstellung vergangenen Sekunden bekommen.

Bei der Erstellung eines Countdown-Timers kann man bei der Erstellung die Startzeit (in Sekunden) angeben, von der man herunterzählen möchte, also z.B. timer = core.CountdownTimer(10) zählt von Sekunde 10 herunter. Zu beachten ist, dass die Zeit hier auch negativ werden kann (im Beispiel also wenn mehr als 10 Sekunden vergangen sind). Die aktuell noch übrige Zeit kann auch hier mit timer.getTime() abgefragt werden.

Man kann in einem Script beliebig viele Timer erstellen. Man kann Timer außerdem jederzeit auf beliebige Werte zurücksetzen. Die beiden vorher erstellten Timer clock und timer kann man mit clock.reset() bzw. timer.reset() auf ihre ursprünglichen Zeiten zurücksetzen. Dies bewirkt, dass clock auf 0 und timer auf 10 gesetzt werden.

Das folgende Beispiel zeigt, wie man Timer einsetzen kann. Man erkennt, dass die while-Schleifen so lange ausgeführt werden, bis die Werte der Timer Null unterschreiten. Das bedeutet, dass die Schleife genau so lange dauert wie bei der Erstellung des Timers (bzw. eigentlich dann beim Zurücksetzen) angegeben wird.

from psychopy import core, visual

win = visual.Window(size=[800, 800])

text1 = visual.TextStim(win, text="", color="blue")
text2 = visual.TextStim(win, text="", color="red")

timer1 = core.CountdownTimer(3)
timer2 = core.CountdownTimer(5)

timer1.reset()
while timer1.getTime() > 0:
    text1.setText("{:.2f}".format(timer1.getTime()))
    text1.draw()
    win.flip()
timer2.reset()
while timer2.getTime() > 0:
    text2.setText("{:.2f}".format(timer2.getTime()))
    text2.draw()
    win.flip()

win.close()
core.quit()

Stroop als Script

Dies führt uns zum Schluss wieder zum Stroop-Experiment, welches wir nun in nur 43 Zeilen selbst als Script schreiben können. Es fehlt zwar die Abspeicherung der Daten wie z.B. Reaktionszeiten, dies würde aber nur wenige zusätzliche Zeilen in Anspruch nehmen. Im Vergleich mit den über 450 Zeilen des automatisch generierten Codes ist die selbst erstellte Variante aber wesentlich übersichtlicher (und dadurch vermutlich auch einfacher zu verstehen).

from psychopy import core, visual, event, data


win = visual.Window(fullscr=True)  # Vollbildmodus

s = """Simple Stroop

Name the color of the word on the screen.
Do not read the word!

If the word is red, press 'r'.
If the word is blue, press 'b'.
If the word is green, press 'g'.

Press Space when you are ready.
"""
text = visual.TextStim(win=win, text=s, color="white", height=0.05)
text.draw()
win.flip()
event.waitKeys(keyList=["space"])

conditions = data.importConditions("conditions.xlsx")
trials = data.TrialHandler(trialList=conditions, nReps=5)
timer = core.CountdownTimer()
text.setHeight(0.1)

for trial in trials:
    timer.reset(2)
    text.setText(trial["word"])
    text.setColor(trial["color"])
    while timer.getTime() > 0:
        if timer.getTime() > 1:
            text.draw()
        keys = event.getKeys(keyList=["r", "g", "b", "escape"])
        win.flip()
        if keys:
            rt = 2 - timer.getTime()
            if keys[0] == "escape":
                core.quit()
            break

win.close()
core.quit()

Neu an diesem Script ist die Möglichkeit, mit data.TrialHandler automatisch Trials aus der Datei conditions.xlsx zu erzeugen. Über das Ergebnis dieses Befehls kann dann iteriert werden (was den einzelnen Trials entspricht).

Weitere Literatur

PsychoPy Coder beinhaltet im Menüpunkt Demos viele Beispiel-Scripts, welche als Vorlage für eigene Experimente bzw. Lernmaterialien verwendet werden können.

Die Website der im Moment nicht mehr angebotenen Lehrveranstaltung “Methoden der apparitiven Versuchssteuerung” ist nach wie vor verfügbar – neben einer allgemeinen Einführung in Python werden hier viele PsychoPy-Experimente beschrieben.

Die PsychoPy-Website bietet auch ein kurzes Tutorial über PsychoPy Coder, in welchem mittels Code ein einfaches Experiment erstellt wird.

Übungen

Übung 1

Erzeugen Sie mit PsychoPy folgendes Experiment direkt als Script (d.h. verwenden Sie nicht die grafische Oberfläche PsychoPy Builder dafür, sondern PsychoPy Coder):

Im Experiment sollen drei verschiedene visuelle Stimuli (z.B. Kreis, Polygon, Text, Bild) gleichzeitig animiert werden. Innerhalb einer Schleife, welche so lange laufen soll bis die Escape-Taste gedrückt wird, soll von jedem der drei Stimuli zumindest eine Eigenschaft (wie z.B. Position, Größe, Farbe, Orientierung) kontinuierlich verändert werden. Alle drei Stimuli sollen gleichzeitig innerhalb der Schleife verändert werden.