【MATLAB】從零開始運(yùn)用Psychtoolbox編寫一個簡單的心理學(xué)實(shí)驗(yàn)程序

1 引言

分享一下如何使用MATLAB中的心理學(xué)工具箱(Psychtoolbox,PTB)編寫一個完整的心理學(xué)實(shí)驗(yàn)。一般而言,任何行為實(shí)驗(yàn)都可以按照本文的框架來編寫。

今天要編寫的例子是一個很簡單的按鍵判斷的實(shí)驗(yàn),只有一個block,block里有五個trial。每個trial的流程圖如下。首先呈現(xiàn)一個500~1200ms的注視點(diǎn),然后隨機(jī)出現(xiàn)左箭頭或右箭頭,被試需要根據(jù)出現(xiàn)的刺激按方向鍵反應(yīng),如果500ms之內(nèi)按鍵則記錄反應(yīng)時、正確率等信息,并且箭頭消失,否則箭頭會呈現(xiàn)500ms的時間后自動消失,最后是300ms的空屏。

2 實(shí)驗(yàn)程序的編寫

2.1 創(chuàng)建文件夾、構(gòu)建程序的基本框架

首先新建一個文件夾,將來的實(shí)驗(yàn)程序文件都放在這個文件夾中,這樣子管理起來比較方便。我的叫“My_Psych_exp”,注意MATLAB讀取文件時有一些命名規(guī)范,一般采用英文命名即可,單詞與單詞之間用首字母大寫或下劃線來區(qū)分。

在MATLAB左邊的面板點(diǎn)擊進(jìn)入剛剛創(chuàng)建的文件夾,新建一個叫“exp_example”的文件夾,在該文件夾中通過快捷鍵Ctrl+N新建一個腳本。

開頭輸入如下三行代碼。clear是清除所有變量,clc是清除命令行窗口的代碼,sca相當(dāng)于Screen('CloseAll'),即關(guān)閉PTB里所有打開的窗口。

clear;
clc;
sca;

接下來輸入如下代碼。

try

    HideCursor;
    
catch
    ShowCursor;
    sca;
    psychrethrow(psychlasterror);
end

編寫實(shí)驗(yàn)程序時推薦采用這種“try……catch……end”的框架,實(shí)驗(yàn)的主要代碼就放在try和catch中間,這樣萬一出現(xiàn)錯誤,就會執(zhí)行catch之后的語句。

我們一般做行為實(shí)驗(yàn)時都是通過按鍵反應(yīng),鼠標(biāo)指針是隱藏起來的,因此可以在一開始使用HideCursor隱藏鼠標(biāo)指針,同時在catch部分寫上ShowCursor,這樣程序出錯時鼠標(biāo)指針就會顯示出來。而sca會關(guān)閉窗口,psychrethrow(psychlasterror)則會在命令行窗口告訴我們出錯的代碼的位置。

2.2 創(chuàng)建數(shù)據(jù)表格

現(xiàn)在,讓我們來看看整個實(shí)驗(yàn)程序最重要的部分:數(shù)據(jù)。

在MATLAB中,我們的數(shù)據(jù)都儲存在變量中,雖然可以直接將這些變量保存為“mat”格式的文件,并在MATLAB中處理這些數(shù)據(jù),但最好還是將實(shí)驗(yàn)數(shù)據(jù)統(tǒng)一整理并導(dǎo)出為外部文件,以便將來進(jìn)行查閱和統(tǒng)計(jì)分析。

在這個例子中,我們想要收集的信息有:被試的基本信息(包括被試編號、性別、年齡和利手)、注視點(diǎn)呈現(xiàn)時間、箭頭刺激的朝向、反應(yīng)按鍵、反應(yīng)時、正確率。并且這個實(shí)驗(yàn)總共有5個trial。因此預(yù)期的數(shù)據(jù)矩陣的格式如下(為了方便理解,添加了表頭)。

在實(shí)驗(yàn)過程中,我們需要時不時地從這么一個矩陣中讀寫信息,例如將被試的反應(yīng)按鍵記錄到矩陣中,然后在計(jì)算正確率的時候再將這一信息讀取出來使用。為此,我們先創(chuàng)建一個名為“data”的cell矩陣,矩陣的大小就按照我們預(yù)期的要求。之所以使用cell,是因?yàn)槲覀兊臄?shù)據(jù)信息同時包含了數(shù)值和字符串。現(xiàn)在,讓我們將下面這段代碼添加至“try……”的后面。

    % Create a cell array to save data
    data = cell(5,9);

2.3 收集被試的基本信息

在開始實(shí)驗(yàn)前,我們還需要收集被試的基本信息。

使用過E-Prime的同學(xué)應(yīng)該知道,E-Prime運(yùn)行實(shí)驗(yàn)程序時,會出現(xiàn)一些收集被試的基本信息的對話框。在MATLAB中,我們同樣可以采用對話框的方式來收集被試的信息。

    % Obtain some information of experiment
    prompt = {'Subject Number','Gender[1 = m, 2 = f]','Age','Handeness[1 = left, 2 = right]'};
    title = 'Exp infor'; % The title of the dialog box
    definput = {'','','',''}; % Default input value(s)
    % Using inputdlg() to obtain information and save it to cell array
    data(1:5, 1:4) = repmat(inputdlg(prompt,title,[1, 50],definput)', 5, 1);

這里用到了MATLAB自帶的inputdlg函數(shù),其作用是打開一個收集用戶輸入的對話框,并將輸入的信息賦給一個變量(在這里就是賦給我們剛剛創(chuàng)建的cell矩陣?yán)玻?。至?code>inputdlg中的參數(shù),prompt是對需要輸入的信息的提示,title就是對話框標(biāo)題,“[1, 50]”是輸入框的維度,表示對話框的高度為一個字符,長度為50個字符。如果需要增加、減少想要收集的信息,則在prompt這個變量里添加/刪除一下就可以了。

接著,我們將收集到的信息,通過'(英文的單引號,效果是將行變列、將列變行)進(jìn)行轉(zhuǎn)置,并通過repmat函數(shù)復(fù)制5次后,寫入我們剛剛創(chuàng)建的數(shù)據(jù)矩陣中。先讓我們看一下最終的效果。

其實(shí)這些信息只需要一行就夠了,我這么做只是為了美觀一點(diǎn)……

2.4 打開窗口

繼續(xù)在try部分添加下面這些代碼。代碼的作用是打開一個窗口(這個窗口便是我們呈現(xiàn)實(shí)驗(yàn)刺激的地方),同時我們還設(shè)置了一些所需的參數(shù)。

    HideCursor;
    
    % Open a black backgound window
    [w, wrect] = Screen('OpenWindow', 0, [0, 0, 0]);
    
    % Define the center coordinates
    [x_center, y_center] = RectCenter(wrect);
    
    % Measure the vertical refresh rate of the monitor
    ifi = Screen('GetFlipInterval', w);
    
    % Text font and text color
    Screen('TextFont', w, 'Simhei');
    Screen('TextSize', w, 65);

接下來解釋一下各行代碼。

Screen('OpenWindow')的作用是打開一個窗口,我們將這個窗口命名為“w”,“wrect”返回的值是窗口的分辨率,0是顯示器的編號,第一個顯示器的編號是0,后面依次是1、2、3……,當(dāng)存在多個顯示器時,可以通過代碼自動獲取各個顯示器的編號,這里不再贅述。[0, 0, 0] 是窗口的顏色,這里以RGB的方式編寫,三個0就是黑色,三個255就是白色,很好記。

RectCenter(wrect)是獲取顯示器屏幕的中心點(diǎn)的位置,之后我們可以用x_center和y_center這兩個值作為坐標(biāo)來呈現(xiàn)注視點(diǎn)。

Screen('GetFlipInterval')的目的是獲取當(dāng)前顯示器每刷新一幀所需的秒數(shù),即每次“Flip”所需的時間,后面講到timing的時候會介紹這一數(shù)值的作用。

最后兩行是設(shè)置窗口中呈現(xiàn)的文本的字體、字號,“w”就是我們定義的窗口的名字啦,要想在PTB中正常顯示中文,就需要選擇一個支持中文的字體,這里選擇的是黑體“SimHei”,字號是65。

2.5 設(shè)置按鍵

接下來設(shè)置一下我們需要用到的按鍵,也就是鍵盤上的左、右方向鍵,然后我們再設(shè)置一個退出鍵——“Esc”,中途需要退出時便可以使用這個按鍵。

    % Define some keyboard keys
    KbName('UnifyKeyNames');
    left_key = KbName('LeftArrow');
    right_key = KbName('RightArrow');
    esc_key = KbName('escape');

2.6 準(zhǔn)備實(shí)驗(yàn)材料

接下來,我們要準(zhǔn)備兩個內(nèi)容:其一是所有trial的注視點(diǎn)的呈現(xiàn)時間(在500ms至1200ms之內(nèi)隨機(jī)決定 ),其二是所有trial的箭頭刺激的朝向(朝左或朝右)。

    % Prepare materials
    for i = 1:5
        data{i, 5} = unifrnd(0.5,1.2); % Fixation time
        if unidrnd(2) == 1 % Arrow orientation
            data{i, 6} = '←';
        else
            data{i, 6} = '→';
        end
    end

這里用了一個循環(huán)語句,從而依次生成5個trial的注視點(diǎn)呈現(xiàn)時間和箭頭朝向,并將這些信息寫入我們的數(shù)據(jù)矩陣中。首先通過unifrnd(0.5,1.2)隨機(jī)生成 [0.5,1.2] 之中的值,作為注視點(diǎn)的呈現(xiàn)時間。其次,通過unifrnd(2)生成一個隨機(jī)數(shù),這個隨機(jī)數(shù)為1或2,我們通過邏輯語句對這個數(shù)值進(jìn)行判斷,當(dāng)生成的隨機(jī)數(shù)是1時,這次trial的箭頭刺激朝左,否則朝右。

不過,做正式實(shí)驗(yàn)的時候,正確的做法應(yīng)該是將刺激序列保存為一個外部的表格文件,每次運(yùn)行腳本時,就調(diào)用一次表格文件,同時按需要將其順序打亂從而以隨機(jī)序列的方式呈現(xiàn)刺激。本文之所以將所有代碼都放置在一個腳本中,只是為了方便大家理解。

2.7 呈現(xiàn)指導(dǎo)語

然后便是準(zhǔn)備指導(dǎo)語了,雖然我們可以用DrawTextDrawFormattedText直接在屏幕上“畫”出指導(dǎo)語,但是為了美觀,還是推薦通過讀取事先制作好的圖片來呈現(xiàn)指導(dǎo)語。

我們可以在PowerPoint中編寫指導(dǎo)語界面,保存為圖片并放入文件夾中,最后用MakeTexture函數(shù),將指導(dǎo)語和結(jié)束語圖片賦給exp_instructionexp_end兩個變量,這樣就準(zhǔn)備完成了。需要的時候,用DrawTexture“畫”出來即可。

現(xiàn)在我們先呈現(xiàn)指導(dǎo)語,然后通過KbStrokeWait等待按鍵,以便被試在閱讀完指導(dǎo)語后按任意鍵繼續(xù)。

    % Load pictures
    exp_instruction = Screen('MakeTexture', w, imread('pic\exp_instruction.tif'));
    exp_end = Screen('MakeTexture', w, imread('pic\exp_end.tif'));
    
    % Display instruction
    Screen('DrawTexture', w, exp_instruction, []);
    Screen('Flip', w);
    KbStrokeWait;

需要注意的是,這里我們將指導(dǎo)語和結(jié)束語的圖片放在了實(shí)驗(yàn)程序文件夾中的一個叫“pic”的子文件夾,之后輸出的實(shí)驗(yàn)數(shù)據(jù)文件我們會放到另一個叫“data”的子文件夾,這樣做是為了方便整理,以免各種實(shí)驗(yàn)材料、數(shù)據(jù)、程序都放在同一個文件夾中而導(dǎo)致過于混亂。

指導(dǎo)語和結(jié)束語的圖片如下,你可以將其另存為tif格式的圖片,然后放置在“pic”文件夾中。

2.8 構(gòu)建trial的框架

接下來便是編寫一個trial之中的內(nèi)容,即呈現(xiàn)注視點(diǎn)呀、收集反應(yīng)信息呀什么的,我們采用“for……end”的格式。即該實(shí)驗(yàn)有五個trial,循環(huán)運(yùn)行五個trial之后就結(jié)束這一部分。trial = 1:5中的trial是計(jì)數(shù)用的變量,寫成i = 1:5或任何其它名稱都可以(當(dāng)然不能是MATLAB中的保留字),為了方便理解,這里寫為trial。

    % Loop for the total number of trials
    for trial = 1:5
        
    
    end

2.9 呈現(xiàn)注視點(diǎn)

我們可以采用DrawDots繪制注視點(diǎn),其中的參數(shù)是指,在“w”窗口的中心畫出一個點(diǎn),大小是10,顏色是白色。

        % Fixation: 500~1200ms
        for i = 1:round(cell2mat(data(trial, 5)) / ifi)
            Screen('DrawDots', w, [x_center; y_center], 10, [255,255,255], [], 1);
            Screen('Flip', w);
        end

至于注視點(diǎn)的時間,需要先從我們的數(shù)據(jù)矩陣中調(diào)用剛剛隨機(jī)生成的值(即cell2mat(data(trial, 5))),這個值是秒數(shù),我們將這個值除以ifi,并通過round取整,便得到該時間段在這臺顯示器上對應(yīng)的幀數(shù)。例如,我的顯示器的ifi是0.0167,一秒對應(yīng)的幀數(shù)就是1/0.0167≈60,也就是說我的顯示器的FPS(frames per second)是60幀/秒,也就是60Hz的刷新率。

所以,我們只需根據(jù)秒數(shù)算出幀數(shù)x,然后反復(fù)繪制x次刺激并通過Screen('Flip')刷新屏幕(繪制的刺激是在緩沖區(qū),需要刷新屏幕使其呈現(xiàn)出來),便可以在指定的時間內(nèi)精確地呈現(xiàn)刺激。

2.10 呈現(xiàn)刺激

注視點(diǎn)消失后便是箭頭刺激的呈現(xiàn),很簡單,和呈現(xiàn)注視點(diǎn)是類似的套路。DrawFormattedText的第二個參數(shù)是我們想要呈現(xiàn)的文本,這里同樣從數(shù)據(jù)矩陣中調(diào)用已經(jīng)隨機(jī)生成好的信息。

        % Stimulus: 500ms
        DrawFormattedText(w, data{trial, 6}, 'center', 'center', [255,255,255]);
        Screen('Flip', w);

2.11 收集反應(yīng)信息

現(xiàn)在我們需要收集被試的反應(yīng)啦。首先試著分析一下如何達(dá)到這個目的。我們希望被試在箭頭刺激呈現(xiàn)的500ms之內(nèi)反應(yīng),所以可以通過一個while循環(huán)來實(shí)現(xiàn),只要刺激呈現(xiàn)的時間距離當(dāng)前時間的差值在500ms以內(nèi),則執(zhí)行一些語句,以便記錄被試的按鍵、反應(yīng)時、正確率等信息,并提前結(jié)束刺激的呈現(xiàn),如果超出500ms,則直接結(jié)束刺激的呈現(xiàn),然后將按鍵、反應(yīng)時和正確率記錄為空值。

現(xiàn)在,我們按照上述的構(gòu)思,一步步寫出代碼。

首先,我們通過GetSecs獲取一個時間戳,賦給t0,作為刺激呈現(xiàn)的起始時間點(diǎn),之后我們可以隨時獲取新的時間戳,并減去t0,得到的便是自刺激呈現(xiàn)之后經(jīng)過的時間。

讓我們來定義一個While循環(huán),使腳本在呈現(xiàn)刺激后的500ms內(nèi),反復(fù)運(yùn)行while循環(huán)內(nèi)的語句。

        % Record responses data
        t0 = GetSecs;
        while GetSecs - t0 < 0.5

        end

接著在循環(huán)內(nèi)添加以下代碼,其作用是檢查被試按下的按鍵。

[keyisdown, secs, keycode] = KbCheck;

然后通過if語句判斷,如果按的是Esc鍵,則直接結(jié)束腳本。

            if keycode(esc_key)
                sca;
                return

如果是按了其它的按鍵,則將按鍵的名稱和反應(yīng)時間(GetSecs - t0)記錄到數(shù)據(jù)矩陣中。

            elseif keyisdown
                data{trial, 7} = KbName(keycode); % resp
                data{trial, 8} = GetSecs - t0; % rt

接著便是判斷此次反應(yīng)的正確率了,如果按鍵為左方向鍵且刺激為左箭頭,或者按鍵為右方向鍵且刺激為右箭頭,則該試次的正確率記為1,反之記為0,最后通過break來結(jié)束循環(huán)。

                % acc
                if keycode(left_key) && data{trial, 6} == '←'
                    data{trial, 9} = 1; % acc=1
                elseif keycode(right_key) && data{trial, 6} == '→'
                    data{trial, 9} = 1; % acc=1
                else
                    data{trial, 9} = 0; % acc=0
                end
                break % break the loop

還有一種情況是沒有按鍵反應(yīng),此時我們將反應(yīng)按鍵、反應(yīng)時和正確率都記為NA(當(dāng)然,不同的實(shí)驗(yàn)設(shè)計(jì),記錄的方式是不同的,這里只是舉個例子)。

            else
                data{trial, 7} = 'NA'; % resp
                data{trial, 8} = 'NA'; % rt
                data{trial, 9} = 'NA'; % acc
            end
        end

該部分完整的代碼如下:

        % Record responses data
        t0 = GetSecs;
        while GetSecs - t0 < 0.5
            [keyisdown, secs, keycode] = KbCheck;
            if keycode(esc_key)
                sca;
                return
            elseif keyisdown
                data{trial, 7} = KbName(keycode); % resp
                data{trial, 8} = GetSecs - t0; % rt
                % acc
                if keycode(left_key) && data{trial, 6} == '←'
                    data{trial, 9} = 1; % acc=1
                elseif keycode(right_key) && data{trial, 6} == '→'
                    data{trial, 9} = 1; % acc=1
                else
                    data{trial, 9} = 0; % acc=0
                end
                break % break the loop
            else
                data{trial, 7} = 'NA'; % resp
                data{trial, 8} = 'NA'; % rt
                data{trial, 9} = 'NA'; % acc
            end
        end

2.12 呈現(xiàn)空屏

之后便是300ms的空屏,刷新一下屏幕然后等待0.3秒即可。

        % Interval: 300ms
        Screen('Flip', w);
        WaitSecs(0.3);

2.13 呈現(xiàn)結(jié)束語

至此,實(shí)驗(yàn)主體部分的內(nèi)容都完成了,接下來便是呈現(xiàn)結(jié)束語并退出實(shí)驗(yàn)程序。這里我們設(shè)置的是在呈現(xiàn)結(jié)束語的1秒后自動退出。

    % Display instruction2
    for i = 1:round(1 / ifi)
        Screen('DrawTexture', w, exp_end, []);
        Screen('Flip', w);
    end
    
    sca;

2.14 保存數(shù)據(jù)文件

但退出窗口之后,我們還有一個很重要的任務(wù),也就是將收集到的數(shù)據(jù)保存起來。之所以在呈現(xiàn)結(jié)束語、退出窗口后才保存數(shù)據(jù),是因?yàn)閷?shù)據(jù)寫入表格是需要一定的時間的,如果在呈現(xiàn)結(jié)束語之前寫入數(shù)據(jù),會導(dǎo)致trial和結(jié)束語之間有一個短暫的“卡頓”,就讓人感覺程序運(yùn)行起來不夠“絲滑”了。

代碼如下。

    % Put all data to a table object
    header = {'SubjectNumber', 'Gender', 'Age', 'Handedness',...
        'DotsTime', 'Arrow', 'Resp', 'RT', 'ACC'};
    data_table = cell2table(data, 'VariableNames', header);
    
    % Create a csv file to save data
    exp_data = strcat('data\', 'exp_example_', char(data{1,1}), '_', date, '.csv');
    writetable(data_table, exp_data);

現(xiàn)在說明一下這幾行代碼的含義。

首先我們將cell格式的數(shù)據(jù)矩陣轉(zhuǎn)換為table。之所以轉(zhuǎn)換為table的形式,是因?yàn)槲覀兿M玫降臄?shù)據(jù)文件是帶有表頭的,header這個變量的內(nèi)容就是我們的表頭。然后,我們通過writetable將這個table寫入csv格式的表格文件。csv是一個開放性很好的數(shù)據(jù)文件格式,包括Excel、SPSS、R和Mplus在內(nèi)的各種軟件都支持對csv的讀寫。然后,我們通過strcat函數(shù)將各種字符和字符變量串起來,作為數(shù)據(jù)文件的文件名,在這里,exp_data這個變量包含了需要保存的數(shù)據(jù)文件的目錄(保存在“data”這個子文件夾里)、文件名和文件格式。其中文件名由實(shí)驗(yàn)的名稱(“exp_example”)、被試的編號和實(shí)驗(yàn)日期組成(date函數(shù)的功能是獲取當(dāng)前日期,當(dāng)然我們還可以通過datestr函數(shù)獲取更多樣化的日期和時間格式,作為文件名中的實(shí)驗(yàn)日期)。

2.15 收尾

完事之后,在命令行窗口輸出一些信息,這樣我們就知道程序是完全正常地運(yùn)行結(jié)束了。

    disp('Succeed!');

對了,如果你無法運(yùn)行這個腳本并且報錯信息指向Screen('OpenWindow')這行代碼,此時可能的原因有很多,其中一個主要的原因是電腦性能可能不夠了……

解決的方法則是在腳本的開頭添加下面這行代碼。

Screen('Preference', 'SkipSyncTests', 2);

這行代碼還有一個作用,就是跳過每次運(yùn)行腳本時會出現(xiàn)的冗長的初始化界面。當(dāng)然,為了使程序的timing更精確,正式實(shí)驗(yàn)時最好還是去掉這一行代碼。

3 實(shí)驗(yàn)程序的運(yùn)行效果

好啦,現(xiàn)在試著運(yùn)行一下看看效果。點(diǎn)擊運(yùn)行,同時系統(tǒng)提示我們選擇文件的保存位置和文件名,我就命名為“exp_demo”啦。

運(yùn)行結(jié)束后,命令行窗口顯示了“Succeed!”,說明是順利運(yùn)行了!

現(xiàn)在打開數(shù)據(jù)文件,看看數(shù)據(jù)的保存情況。

可以發(fā)現(xiàn),注視點(diǎn)的呈現(xiàn)時間確實(shí)如我們期望的一樣,處于500~1200ms的范圍之內(nèi)(為了確保符合預(yù)期效果可以多跑幾次看看)。剛剛運(yùn)行時,第一、第二個trial我按了正確的按鍵,第三個按了未定義的按鍵(上方向鍵),第四個按了相反的按鍵,第五個trial沒有按鍵反應(yīng),這些信息都在表格中記錄下來了。在這個程序里,對于“按了錯誤的按鍵”與“未按任何按鍵”,記錄的信息是一樣的,如果需要區(qū)分的話,可以繼續(xù)在程序里進(jìn)行修改完善。

最后來看一眼該實(shí)驗(yàn)程序所有的文件,m文件就是我們的實(shí)驗(yàn)程序,“pic”文件夾里放的是指導(dǎo)語的圖片,“data”文件夾里的是剛剛運(yùn)行后收集到的數(shù)據(jù),這些文件都保存在“exp_example”這個文件夾之下。

實(shí)驗(yàn)程序的全部代碼如下。加上注釋和換行也只有136行~

% exp_example_procedure.m
%
% A very simple behavioral experiment demo of PTB-3, 
% shows you how to create visual stimulus and record responses data.
% 
% Wei Zi-Qian, 2020, August

clear;
clc;
sca;

Screen('Preference', 'SkipSyncTests', 2);

try
    
    % Create a cell array to save data
    data = cell(5,9);

    % Obtain some information of experiment
    prompt = {'Subject Number','Gender[1 = m, 2 = f]','Age','Handeness[1 = left, 2 = right]'};
    title = 'Exp infor'; % The title of the dialog box
    definput = {'','','',''}; % Default input value(s)
    % Using inputdlg() to obtain information and save it to cell array
    data(1:5, 1:4) = repmat(inputdlg(prompt,title,[1, 50],definput)', 5, 1);
    
    HideCursor;
    
    % Open a black backgound window
    [w, wrect] = Screen('OpenWindow', 0, [0, 0, 0]);
    
    % Define the center coordinates
    [x_center, y_center] = RectCenter(wrect);
    
    % Measure the vertical refresh rate of the monitor
    ifi = Screen('GetFlipInterval', w);
    
    % Text font and text color
    Screen('TextFont', w, 'Simhei');
    Screen('TextSize', w, 65);
    
    % Define some keyboard keys
    KbName('UnifyKeyNames');
    left_key = KbName('LeftArrow');
    right_key = KbName('RightArrow');
    esc_key = KbName('escape');
    
    % Prepare materials
    for i = 1:5
        data{i, 5} = unifrnd(0.5,1.2); % Fixation time
        if unidrnd(2) == 1 % Arrow orientation
            data{i, 6} = '←';
        else
            data{i, 6} = '→';
        end
    end
    
    % Load pictures
    exp_instruction = Screen('MakeTexture', w, imread('pic\exp_instruction.tif'));
    exp_end = Screen('MakeTexture', w, imread('pic\exp_end.tif'));
    
    % Display instruction
    Screen('DrawTexture', w, exp_instruction, []);
    Screen('Flip', w);
    KbStrokeWait;
    
    % Loop for the total number of trials
    for trial = 1:5

        % Fixation: 500~1200ms
        for i = 1:round(cell2mat(data(trial, 5)) / ifi)
            Screen('DrawDots', w, [x_center; y_center], 10, [255,255,255], [], 1);
            Screen('Flip', w);
        end
        
        % Stimulus: 500ms
        DrawFormattedText(w, data{trial, 6}, 'center', 'center', [255,255,255]);
        Screen('Flip', w);
        
        % Record responses data
        t0 = GetSecs;
        while GetSecs - t0 < 0.5
            [keyisdown, secs, keycode] = KbCheck;
            if keycode(esc_key)
                sca;
                return
            elseif keyisdown
                data{trial, 7} = KbName(keycode); % resp
                data{trial, 8} = GetSecs - t0; % rt
                % acc
                if keycode(left_key) && data{trial, 6} == '←'
                    data{trial, 9} = 1; % acc=1
                elseif keycode(right_key) && data{trial, 6} == '→'
                    data{trial, 9} = 1; % acc=1
                else
                    data{trial, 9} = 0; % acc=0
                end
                break % break the loop
            else
                data{trial, 7} = 'NA'; % resp
                data{trial, 8} = 'NA'; % rt
                data{trial, 9} = 'NA'; % acc
            end
        end
        
        % Interval: 300ms
        Screen('Flip', w);
        WaitSecs(0.3);

    end
    
    % Display instruction2
    for i = 1:round(1 / ifi)
        Screen('DrawTexture', w, exp_end, []);
        Screen('Flip', w);
    end
    
    sca;
    
    % Put all data to a table object
    header = {'SubjectNumber', 'Gender', 'Age', 'Handedness',...
        'DotsTime', 'Arrow', 'Resp', 'RT', 'ACC'};
    data_table = cell2table(data, 'VariableNames', header);
    
    % Create a csv file to save data
    exp_data = strcat('data\', 'exp_example_', char(data{1,1}), '_', date, '.csv');
    writetable(data_table, exp_data);
    
    disp('Succeed!');
    
catch
    ShowCursor;
    sca;
    psychrethrow(psychlasterror);
end

4 擴(kuò)展

4.1 操作多個圖片的方法

舉個例子,假如我們有20張刺激圖片,名叫1.png、2.png、3.png……20.png,這些圖片放在實(shí)驗(yàn)程序所在文件夾中的名為pic的子文件夾。那么,我們可以通過以下代碼,將這些圖片依次加載至MATLAB,并統(tǒng)一放置在名為img的數(shù)組中。

    % Prepare image
    img = zeros(20, 1);
    for i = 1:20
        pic = strcat('pic\',string(i),'.png');
        img(i) = Screen('MakeTexture', w, imread(pic));
    end

上述這段代碼位于打開窗口之后,循環(huán)呈現(xiàn)指導(dǎo)語之前。更多的圖片也是類似的操作,修改一下參數(shù)即可。

接著,如果我們是想隨機(jī)呈現(xiàn)圖片的話,可以用以下代碼打亂img的順序。

    img=img(randperm(length(img))); % random order

某些情況下,我們還可以反復(fù)使用這一行代碼來打亂img的順序。例如實(shí)驗(yàn)設(shè)計(jì)有n個block,每個block的刺激內(nèi)容都是相同的,只是順序不同,此時便可以在每個block的起始位置添加該代碼,便能輕松實(shí)現(xiàn)隨機(jī)呈現(xiàn)的效果。

之后便是在每個trial中呈現(xiàn)圖片刺激了,非常簡單,只需要將trial的數(shù)值作為索引來調(diào)用img中的圖片。注意我這里將圖片調(diào)整為255*255像素的大小,你可以根據(jù)自己的實(shí)驗(yàn)設(shè)計(jì)來修改這一參數(shù)。

            % Stimulus: 500ms
            Screen('DrawTexture', w, img(trial), [0 0 255 255]);
            Screen('Flip', w);

最后,也是最重要的一點(diǎn),在保存每個trial的反應(yīng)信息的時候,千萬別忘了記錄這個trial中呈現(xiàn)的img的類型。

在這個例子中,圖片有兩種類型,如下。

    condition1 = [11,12,13,14,15,16,17,18,19,20];
    condition2 = [21,22,23,24,25,26,27,28,29,30];

其中的11、12、13等等,是img中的值,代表每個圖片,至于為什么會是這些值,我就不太清楚了,將圖片載入MATLAB后就是這樣的,我只是根據(jù)載入后的結(jié)果寫了這些代碼……

于是每個trial的結(jié)尾便可以通過以下代碼判斷img的類型。

            % Save img type
            if ismember(img(trial), condition1)
                imgType = 'condition1';
            elseif ismember(img(trial), condition2)
                imgType = 'condition2';
            end

這里的imgType只是舉例,你也可以直接將判斷的結(jié)果放入到保存所有數(shù)據(jù)的matrix中。

4.2 略

等待施工……

5 結(jié)語

本文的目的是用盡量簡單的方法在PTB中編寫一個心理學(xué)實(shí)驗(yàn)程序。不過鑒于作者也只是剛剛開始自學(xué)PTB,水平有限,某些部分可能會有更便捷的方法。以及,文中難免會有一些錯漏,請大家多多指正。

最后再說一下,在學(xué)習(xí)PTB的過程中,對于任何不清楚的部分,請善用MATLAB中的幫助系統(tǒng)。對于MATLAB自帶的函數(shù),可以通過help函數(shù)查看其功能(例如help writetable),對于PTB中的函數(shù),例如DrawText,可以通過Screen DrawText?這樣的形式查看其功能。此外,“Psychtoolbox”文件夾中的有一個名為“PsychDemos”的文件夾,其包含著許多展示PTB功能的demo。官網(wǎng)里也有不少學(xué)習(xí)PTB的資料,大家可以自行學(xué)習(xí),例如,“Tutorial”這個鏈接里就有很多介紹PTB功能的范例(其中第一、第二部分是入門PTB必看的)。

?
----------2020.10.23更新----------
修改了一處冗余的代碼:將61行的char(data(trial, 6))改成了data{trial, 6}

----------2020.12.30更新----------
更改了timing的方式:將“以秒為單位呈現(xiàn)刺激”改為了“以幀為單位呈現(xiàn)刺激”
更改了文章中的幾處表述,對“收集反應(yīng)信息”一節(jié)的內(nèi)容做了更詳細(xì)的說明

----------2021.01.12更新----------
修改了一處錯誤

----------2021.01.31更新----------
添加了擴(kuò)展內(nèi)容4.1

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容