Hello,
最近在家實在無聊,所以只好安安心心學習啦。你最近在家幹什麼呢?
從今天開始,我將使用2期左右帶大家完整的完成一個心理學行為實驗程式。並且在這個過程中能夠讓大家對 psychopy 有一個比較好的瞭解。
本期我們先來看有關刺激呈現的相關知識。
(flanker正規化任務最終呈現)
對於 Python 來說,其功能的實現是由一個一個的模組(Module)來進行的。所謂模組,是前人為了實現某些功能而編寫的一段程式碼,其中包括了我們實現功能所需要的東西。透過引用,相應的功能得以在我們的程式中實現。
當我們需要在程式中使用某一個模組時,我們一般使用 import <模組名> 來進行匯入,而對於psychopy,我們使用from psychopy import <模組名> 來進行匯入。與刺激呈現有關的是 psychopy 中的 visual 模組,那我們需要在開頭編寫:
from psychopy import visual
所需要的“工具”準備好以後我們先來回顧一下常用的flanker正規化(Eriksen & Eriksen, 1974)的呈現過程,如圖:
這是一個最簡單常用的 flanker 正規化的過程(未使用原文獻中的字母刺激),我們就以此為例來看一下如何利用psychopy 實現 flanker 正規化的呈現。
Part2 各個部分的詳細講解我們要想進行刺激的呈現,首先要建立一個視窗。
根據流程圖,我們建立一個以中灰為背景色的,1024 * 768 畫素大小的視窗,程式碼如下:
from psychopy import visual Win = visual.Window((1024,768), color=(128, 128, 128), fullscr=False, units='pix',colorSpace='rgb255')
(看完整程式碼,可點選最下方橫條左右拖拉喲)
我們使用 visual 模組中的 Window 方法進行視窗的定義以及相關引數的設定。該方法的引數如下:
其中,第二個引數是視窗的背景色,我們使用在 psychopy 中被定義為 'rgb255' 的方式進行編寫,這種方式將光學三原色(紅綠藍)以0--255表達出來,其中當紅綠藍三成分均為128時可以得到中灰,而 (255,255,255) 為白色,(0,0,0) 為黑色。使用這種方法時,引數colorSpace需要設定為 'rgb255'。
另外,第三個引數 fullscr 控制是否全屏顯示,在日常的實驗編寫過程中建議保持非全屏False,這樣如果編寫過程中出現錯誤可以方便退出;而在正式實驗的時候可以將其改為 True。
到此,我們設定出的視窗是一個 1024*768 的,單位為畫素的,且中心座標為(0,0)的視窗,如圖:
設定完視窗以後,我們繼續設定所需要的刺激。
首先,對於注視點,我們使用 visual 模組中的TextStim 方法,這種方法主要對文字刺激進行編寫,而注視點可以使用文字“+”來替代;而對於我們需要的箭頭,由於不同的試次有所不同,因此在後面我們進行與試次有關的設定時我們再進行編寫。除了注視點,這裡我們可以首先將結束語編寫出來,程式碼如下:
# -*- coding: utf-8 -*- from psychopy import visual Win = visual.Window((1024,768), color=(128,128,128), fullscr=False, units='pix',colorSpace='rgb255') fix = visual.TextStim(Win, text='+', color='black', height=50,bold=True) endPrompt = visual.TextStim(Win, text='實驗結束,謝謝!', color='black', height=60)
(看完整程式碼,可點選最下方橫條左右拖拉喲)
各個引數的解析如下(以注視點為例):
結束語與其相類似。
其中,這裡展示了另一種顏色的編寫方式,即直接使用顏色的對應單詞來進行編寫。這種方式雖然比較簡單,但是如果需要編寫的實驗對顏色的精確性要求很高,則還是建議使用'rgb255'的方式進行編寫。
同時,由於結束語是中文文字,因此需要進行檔案編碼型別的轉化,因此程式碼開頭加了
# -*- coding: utf-8 -*-
這一小段特殊註釋建議在編寫python程式的過程中都在開頭處加上。
目前,我們把除了反應屏以外的其他刺激都編寫完成,下面需要對本正規化中最複雜的部分進行編寫。
首先,一般的 flanker 正規化有兩個自變數,即2(兩側:左,右)×2(中央:一致,不一致)實驗設計,我們首先把這四種情況定義出來,並將其順序打亂:
import random var = [] #建立自變數空列表 for flanker in ['left', 'right']: for center in ['same','diff']: var.append([flanker, center]) #在列表中加入相應list元素 random.shuffle(var) #列表隨機
(看完整程式碼,可點選最下方橫條左右拖拉喲)
這裡打亂的方法我們使用 random 模組中的 shuffle() 方法,這種方法可以將列表中的所有元素以隨機方式排列,此時由於我們使用了一個新的模組,需要在開頭對應地將該模組進行 import。
自此,我們得到了一個具有四個元素的列表,其中每個元素又是一個兩個元素的列表:
之後,我們對該屏的箭頭位置進行定義。
同樣以列表的方式進行:
sites = [] #建立箭頭位置空列表 for site in range(5): sites.append((-100+50*site,0)) #新增5個位置
這裡我們用到了 range() 方法,該方法所實現的是一個整數的迭代,當其中只有一個引數時,與for 迴圈相配合可以將從0開始的整數不停賦值給變數,賦值次數即為該整數,每次賦值都會在前一次的基礎上加1。
如,例子中,該 for 迴圈會連續給變數 site 賦值 0,1,2,3,4,雖然看起來與列表有些類似,但是這種方法比列表所需要的記憶體更小。
透過這種方式我們獲取了具有5個元組的一個列表,即:
到此,與試次無關的變數基本設定完成,我們來進入每個試次的迴圈。在試次迴圈中,我們首先定義完整的反應屏5個箭頭的刺激,程式碼如下:
stims = [] #定義刺激空列表 for stim in range(5): if (trial[0] == 'left' and trial[1] == 'diff' and stim == 2) or\ (trial[0] == 'right' and trial[1] == 'diff' and stim != 2) or\ (trial[0] == 'right' and trial[1] == 'same'): Horiz = True #需要進行翻轉的情況 else: Horiz = False #不需要進行翻轉的情況 arr = visual.TextStim(Win, text='←', color='black', height=50, pos=(-200+100*stim,0),flipHoriz=Horiz,bold=True) stims.append(arr) #在列表中加入定義好的箭頭
(看完整程式碼,可點選最下方橫條左右拖拉喲)
我們給每個試次賦值列表var中的元素,即每個試次對應的水平型別,如 ['right', 'same'],此時對於每個 trial 來說,其型別均為 list,並且該 list 中 0 號位置可代表兩側箭頭的朝向,而 1 號位置可以規定出中間箭頭的朝向。
如果我們假定箭頭的初始位置為向左,那麼需要進行水平翻轉的有以下三種情況:
Ø 兩側箭頭向左,中央箭頭與兩側不同時,中央箭頭需要翻轉
Ø 兩側箭頭向右,中央箭頭與兩側不同時,兩側的四個箭頭需要翻轉
Ø 兩側箭頭向右,中央箭頭與兩側相同時,所有箭頭翻轉
轉化為程式語言即為:
(trial[0] == 'left' and trial[1] == 'diff' and stim == 2) or\ (trial[0] == 'right' and trial[1] == 'diff' and stim != 2) or\ (trial[0] == 'right' and trial[1] == 'same')
我們透過設定變數 Horiz 來限定箭頭是否翻轉:
stims = [] #定義刺激空列表 for stim in range(5): if (trial[0] == 'left' and trial[1] == 'diff' and stim == 2) or\ (trial[0] == 'right' and trial[1] == 'diff' and stim != 2) or\ (trial[0] == 'right' and trial[1] == 'same'): Horiz = True #需要進行翻轉的情況 else: Horiz = False #不需要進行翻轉的情況
(看完整程式碼,可點選最下方橫條左右拖拉喲)
之後,我們同樣透過文字刺激 TextStim 來定義箭頭刺激,並重復5次以定義5個箭頭並放入列表 stims,引數解析如下:
其中,運用引數flipHoriz 時需要注意,這一翻轉是在原始狀態進行翻轉,而不是當前狀態(The flip is relative to the original, not relative to the current state)。也就是說,如果對一個刺激進行兩次翻轉,其與進行一次翻轉的效果相同。
到此我們所有的刺激都已定義完成,下一步我們需要把定義好的刺激呈現在螢幕上。
如果我們要呈現刺激,首先要知道這個刺激我們想要呈現的時間以及如何規定呈現時間。雖然 python 中有進行時間相關功能的模組 time,但是我們這裡使用一個更為簡便的方法來控制時間。
對於我們的計算機螢幕來說,其呈現畫面是透過不停地重新整理來進行的,重新整理的頻率我們稱為重新整理率(單位:赫茲,Hz)。
一般來說,我們普通的膝上型電腦的重新整理率為60Hz,實驗室等比較專業的螢幕重新整理率可以達到100Hz。我們可以透過控制電腦螢幕的重新整理次數來控制某個刺激的重新整理時間。
在 psychopy 中,我們可以將電腦理解為有兩個“螢幕”。一個是我們平時看到的螢幕,即“前屏”,它主要是對我們設定的刺激進行呈現;還有一個是虛擬的“後屏”,即電腦對刺激進行繪製的螢幕。
一個刺激的呈現有兩個步驟:電腦先在“後屏”上繪製(draw())出我們想要的刺激,之後再透過重新整理(flip())將其呈現到螢幕上。我們不斷重複這個步驟,刺激就會不停地呈現出來,當我們規定重新整理的次數,那麼也就規定出了重新整理的時間。
例如,如果我們想要設定的注視點呈現 300ms,那麼我們只需要計算出 300ms需要重新整理多少次即可。空屏 500ms的呈現時間同理。計算方法如下:
for frame in range(fix_times): fix.draw() #繪製 Win.flip() #翻轉(重新整理)
我們將 Rate 設定為 60 來指代螢幕的重新整理率,即Hz,而 Hz 的實質為 次/秒,1/Rate 即為 秒/次 ,Dura為 1000*(1/Rate) 即為 毫秒/次,我們設定的呈現時間為 300ms,將其除以 毫秒/次,便可得出 300ms 對應的重新整理次數。
之後透過 round() 進行四捨五入取整,再使用 int() 的確保其為整型,我們可以得到最終的重新整理次數 fix_times,這裡的重新整理次數回到迴圈外進行定義即可。
當我們有了重新整理次數,我們可以透過 for 迴圈來把一次次地重新整理實現出來,從而把單個刺激呈現在螢幕上:
for frame in range(fix_times): fix.draw() #繪製 Win.flip() #翻轉(重新整理)
對於反應屏,我們需要刺激一直呈現直到被試做出判斷(左 或 右)後消失,因此可以透過 while 迴圈並加上 psychopy 中的 event.getKeys() 來實現,同時需要在開頭加上 from psychopy import event,程式碼如下:
from psychopy import event while True: [s.draw() for s in stims] Win.flip() if len(event.getKeys(['left','right'])) > 0: break
我們在這裡透過列表生成式對多個刺激進行同時的呈現,所謂列表生成式,是指一種比一般 for 迴圈更加簡便的表達形式,例如:
[s.draw() for s in stims]
等價於:
for s in stims: s.draw()
這裡 stims 是我們規定好的有5個箭頭的列表,因此可以同時將其呈現出來。
我們最後一個屏是需要空屏,則不需要draw(),直接flip()即可:
a = 0 for trial in var: if a==len(var): #判斷本試次是否為最終試次 while True: endPrompt.draw() Win.flip() if len(event.getKeys()) > 0: break Win.close() #關閉對話方塊
這也是最為常見的三種呈現型別,即單一刺激的呈現、多刺激的同時呈現、呈現空屏。
當所有試次進行完成,我們需要呈現結束語,並且被試按任意鍵退出,那麼我們來定義一個變數 a 對試次數進行監控,當 a 與我們水平個數相等時,呈現結束語,並且被試按任意鍵後,程式結束,關閉視窗:
a = 0 for trial in var: if a==len(var): #判斷本試次是否為最終試次 while True: endPrompt.draw() Win.flip() if len(event.getKeys()) > 0: break Win.close() #關閉對話方塊
Part3 完整程式程式碼及結果展示整個程式完整的程式碼如下:
# -*- coding: utf-8 -*- from psychopy import visual, event import random Win = visual.Window((1024,768), color=(128,128,128), fullscr=False, units='pix',colorSpace='rgb255') fix = visual.TextStim(Win, text='+', color='black', height=50,bold=True) endPrompt = visual.TextStim(Win, text='實驗結束,謝謝!', color='black', height=60) Rate = 60 Dura = 1000/Rate fix_Dura = 300 blank_Dura = 500 fix_times = int(round(fix_Dura/Dura)) blank_times = int(round(blank_Dura/Dura)) var = [] for flanker in ['left', 'right']: for center in ['same','diff']: var.append([flanker, center]) random.shuffle(var) sites = [] for site in range(5): sites.append((-100+50*site,0)) a = 0 for trial in var: a+=1 stims = [] for stim in range(5): if (trial[0] == 'left' and trial[1] == 'diff' and stim == 2) or\ (trial[0] == 'right' and trial[1] == 'diff' and stim != 2) or\ (trial[0] == 'right' and trial[1] == 'same'): Horiz = True else: Horiz = False arr = visual.TextStim(Win, text='←', color='black', height=50, pos=(-200+100*stim,0),flipHoriz=Horiz, bold=True) stims.append(arr) for frame in range(fix_times): fix.draw() Win.flip() while True: [s.draw() for s in stims] Win.flip() if len(event.getKeys(['left','right'])) > 0: break for frame in range(blank_times): Win.flip() if a==len(var): while True: endPrompt.draw() Win.flip() if len(event.getKeys()) > 0: break Win.close()
(看完整程式碼,可點選最下方橫條左右拖拉喲)
執行後效果如下:
本期,主要給大家介紹了有關 psychopy.visual 的一些內容以及一小部分 psychopy.event 和 random 模組的內容。
Part4 系列課程的總結至此,我們已經學習了Psychopy入門、資料型別與運算子、條件與迴圈、flanker正規化的完整程式設計。
基本學完了 Python 在 Psychopy 中需要用到的大多數知識,雖然難度不是很大,但是比較繁雜,建議透過練習以熟悉這些基本的語句和方法。
PS:後臺回覆關鍵詞“psychopy第3期”即可獲得所述的資料及程式碼啦!
排版:青柚
參考文獻:
Eriksen, B. A., & Eriksen, C. W. (1974). Effects of noise letters upon the identification of a target letter in a nonsearch task. Perception & Psychophysics, 16(1), 143-149.