2.2.1節 刺激提示と反応計測のための最小スクリプト

1) フリップ回数とウィンドウ表示時間の関係

2.4.1節を読むとわかるように、リフレッシュレートの回数だけflip()を行うとほぼ1秒となる。従って、range()の引数をリフレッシュレートのn倍にすればn秒間ウィンドウが表示される。ただし、ウィンドウの作成や廃棄にも時間を要するため、厳密にn秒間にはならない。このことは次節以降でスクリーンに刺激を描画するようになると、ウィンドウが開いてから刺激が描画されるまでわずかな遅延があることから確認できるだろう。

2.2.2節 図形や文字の描画

1) 単位cmによる描画の確認

2) 不透明度のアニメーション

22行目のgetKeys()から28行目のbox.draw()までの間の何処かで以下のコードを挿入すると良い。24行目のbox.setOri()の直前か直後にしておけばboxに対する変更が一か所にまとまっていて分かり易いだろう。

box.setOpacity(clock.getTime()%1.0)

=演算子を用いてパラメータを更新する場合は以下のように書けばよい。

box.opacity = clock.getTime()%1.0

20秒間かけて不透明度が0.0から1.0になるようにするには、20秒経過したときに解が1.0になる式をたてればよいのだから、経過時間を20.0で割った値をopacityに設定すればよい。

box.setOpacity(clock.getTime()/20.0)

なお、今回の問題では20秒経過すると描画を終了するのでこれでよいが、20秒経過以降は不透明度1.0を維持したいのであれば、引数の中で最小の値を返す関数min()を用いて1.0を超えないようにすればよい。

box.setOpacity(min(clock.getTime()/20.0, 1.0))

3) 黄色のRGB表現

RGB表現では黄色は(1.0, 1.0, -1.0)である。

4) 塗りつぶしなし/輪郭なしの描画

5) 単位normとheightの利用

一般的なPC用モニタは横長なので、単位がnormの時に回転角度0.0のwidthとheightが同じRectオブジェクトを描画すると横長の長方形になる。このRectオブジェクトを回転させると、まず回転後の4つの頂点の座標が計算されて、それらの座標がモニターの縦横比に合わせて変換されるため、平行四辺形が描かれる。

6) 浮動小数点数の出力

2.2.3節 キーボードイベントの処理と反応の保存

1) キー押し判定のテクニック

以下に例を示す。コード2.3の27から38行目の部分に相当する。

while waiting_keypress:
    keys = psychopy.event.getKeys()
    if 'left' in keys:  # 左キーが押されている
        datafile.write('left\n')
        waiting_keypress = False
    elif 'right' in keys:  # 右キーが押されている
        datafile.write('right\n')
        waiting_keypress = False
    stim1.draw()
    stim2.draw()
    probe.draw()

    win.flip()

右と左のカーソルのどちらが押されても処理が同じ場合は if 'left' in keys or 'right' in keys: と書くことが出来る。しかし、今回の問題でこの式を用いると実験参加者が複数のキーを同時押しした場合に処理が複雑となるため、上記の例のようなやり方のほうが無難だろう。

同時押しされている場合は反応と見なさずキーが押し直されるのを待つのであれば、以下のように書くことが出来る。keysの要素数が0であればキーが押されておらず、1より大きければ複数のキーが押されているので、keysの要素数が1の時のみキー押し判定を行えばよい。

while waiting_keypress:
    keys = psychopy.event.getKeys()
    if len(keys) == 1:  # keysの要素数が1なら押されたキーは1つ
        if 'left' in keys or 'right' in keys:
            datafile.write('{}\n'.format(keys[0]))
            waiting_keypress = False
    stim1.draw()
    stim2.draw()
    probe.draw()

    win.flip()

同時押しはエラーとして記録する(ERRORと出力する)ならば、以下のように書くことも出来る。

while waiting_keypress:
    keys = psychopy.event.getKeys()
    if len(keys) > 1:  # 複数のキーが押されている
        datafile.write('ERROR\n')  # ERRORと出力
        waiting_keypress = False
    elif 'left' in keys or 'right' in keys:
        datafile.write('{}\n'.format(keys[0]))
        waiting_keypress = False
    stim1.draw()
    stim2.draw()
    probe.draw()

    win.flip()

2) ミリ秒単位での反応時間の出力

ミリ秒に換算するには1000をかければよい。コード2.3の31行目のwrite()の引数を以下のように書き換える。

datafile.write('{},{:.1f}\n'.format(key[0],1000*key[1]))

2.2.4節 パラメータを無作為に変更した試行の繰り返し

1) 実験結果の確認

2) 刺激パラメータの変更

コード2.4の10から14行目を以下のように書き換える。probe_rumの計算は他にもいろいろな書き方が出来る。

conditions = []
for stim1_lum in [-1.0, -0.3, 0.3]:
    for probe_lum_index in range(7):
        probe_lum = -0.24 + 0.08 * probe_lum_index
        conditions.append([stim1_lum, probe_lum])

列挙するのが面倒でなければ、probe_lumの値を列挙してもよい。この場合はprobe_lum_indexという変数は不要である。

conditions = []
for stim1_lum in [-1.0, -0.3, 0.3]:
    for probe_lum in [-0.24, -0.16, -0.08, 0.0, 0.08, 0.16, 0.24]:
        conditions.append([stim1_lum, probe_lum])

3) waitKeys()の利用

教示を描画するためのTextStimオブジェクトを用意する必要がある。23から29行目の視覚刺激オブジェクトの定義のところに以下のように付け加える。刺激は刺激、教示は教示でまとまっている方が分かり易いと思われるので、29行目の後に挿入するといいだろう。

msg_stim = psychopy.visual.TextStim(
    win, text='スペースキーを押してください', height=1.0)

続いて、コード2.4の63から64行目に相当する部分(上記のコードを挿入することで行が変わっていることに注意)を以下のように変更する。flip()する前にmsg_stimをdraw()する点がポイントである。

msg_stim.draw()
win.flip()
psychopy.core.waitKeys(keyList=['space'])

4) 試行間にブランク画面を挿入

5) キーイベントのクリア

2.3.1節 実験のブロック化

1) 実験のブロック化

以下に例を示す。強調部分が変更点である(字下げだけの変更を除く)。

 1#coding:utf-8
 2from __future__ import division
 3from __future__ import unicode_literals
 4import psychopy.visual
 5import psychopy.event
 6import psychopy.core
 7import codecs
 8import random # 疑似乱数の発生や無作為な並び替えなどを行う
 9
10conditions = []
11for stim1_lum in [-0.3, 0.3]:
12    tmp_conditions = []
13    for probe_lum_index in range(9):
14        probe_lum = 0.05*(probe_lum_index-4)
15        tmp_conditions.append(probe_lum)
16    tmp_conditions *= 5
17    random.shuffle(tmp_conditions)
18    conditions.append([stim1_lum, tmp_conditions])
19random.shuffle(conditions) # ブロックの順序も無作為化したい場合
20
21win = psychopy.visual.Window(fullscr=True,
22    monitor='defaultMonitor', units='cm', color='black')
23clock = psychopy.core.Clock()
24
25stim1 = psychopy.visual.Rect( # fillColorは各試行開始時に指定する
26    win, width=6.0, height=6.0, pos=[-4,0], lineColor=None)
27stim2 = psychopy.visual.Rect( # stim2のfillColorは固定
28    win, width=3.0, height=3.0, pos=[-4,0],
29    lineColor=None, fillColor=[0.0, 0.0, 0.0])
30probe = psychopy.visual.Rect(
31    win, width=3.0, height=3.0, pos=[4,0], lineColor=None)
32msg = psychopy.visual.TextStim(win, height=0.8)
33
34datafile = codecs.open('data.csv','w','utf-8')
35datafile.write('Stim1,Probe,Response,RT\n') # ヘッダを出力しておく
36
37for condition in conditions: # conditionsから各試行の条件を取り出す
38    msg.setText('スペースキーを押すと実験が始まります')
39    msg.draw()
40    win.flip()
41    psychopy.event.waitKeys(keyList=['space'])
42
43    # 刺激色を更新する
44    stim1_lum = condition[0]
45    stim1.setFillColor([stim1_lum, stim1_lum, stim1_lum])
46
47    for probe_lum in condition[1]:
48        probe.setFillColor([probe_lum, probe_lum, probe_lum])
49
50        waiting_keypress = True
51        psychopy.event.getKeys()
52        clock.reset()
53        while waiting_keypress:
54            keys = psychopy.event.getKeys(timeStamped=clock)
55            for key in keys:
56                if key[0]=='left' or key[0]=='right':
57                    datafile.write( # 刺激のパラメータと反応を出力
58                        '{:.1f},{:.1f},{},{:.3f}\n'.format(
59                            stim1_lum, probe_lum, key[0], key[1]))
60                    datafile.flush() # 直ちにファイルに書き出す
61                    waiting_keypress = False
62                    break
63                elif key[0]=='escape': # ESCキーが押された場合は
64                    datafile.close()   # 直ちに終了する
65                    psychopy.core.exit()
66            stim1.draw()
67            stim2.draw()
68            probe.draw()
69
70            win.flip()
71
72        win.flip() #刺激を消去する
73        psychopy.core.wait(1.0) #1.0秒待つ
74
75msg.setText('実験は終了しました\nご協力ありがとうございました')
76msg.draw()
77win.flip()
78psychopy.event.waitKeys()
79
80datafile.close() #データファイルを閉じる

2) ブロック開始時の教示とキー押し待ち

以下に例を示す。強調部分が変更点である。

 1#coding:utf-8
 2from __future__ import division
 3from __future__ import unicode_literals
 4import psychopy.visual
 5import psychopy.event
 6import psychopy.core
 7import codecs
 8import random # 疑似乱数の発生や無作為な並び替えなどを行う
 9
10conditions = [] # 空リストを用意しappend()で条件を加えていく
11for stim1_lum in [-0.3, 0.3]:
12    for probe_lum_index in range(9):
13        probe_lum = 0.05*(probe_lum_index-4)
14        conditions.append([stim1_lum, probe_lum])
15
16conditions *= 5 # リストの内容を5回繰り返す
17random.shuffle(conditions) # 無作為な順序に並び替える
18
19win = psychopy.visual.Window(fullscr=True,
20    monitor='defaultMonitor', units='cm', color='black')
21clock = psychopy.core.Clock()
22
23stim1 = psychopy.visual.Rect( # fillColorは各試行開始時に指定する
24    win, width=6.0, height=6.0, pos=[-4,0], lineColor=None)
25stim2 = psychopy.visual.Rect( # stim2のfillColorは固定
26    win, width=3.0, height=3.0, pos=[-4,0],
27    lineColor=None, fillColor=[0.0, 0.0, 0.0])
28probe = psychopy.visual.Rect(
29    win, width=3.0, height=3.0, pos=[4,0], lineColor=None)
30msg = psychopy.visual.TextStim(win, height=0.8)
31
32datafile = codecs.open('data.csv','w','utf-8')
33datafile.write('Stim1,Probe,Response,RT\n') # ヘッダを出力しておく
34
35for condition_index in range(len(conditions)):
36    if condition_index != 0 and condition_index % 30 == 0:
37        msg.setText('''第{}試行\n準備が出来たらカーソルキーの左右いずれかを押して
38実験を再開してください'''.format(condition_index+1))
39        msg.draw()
40        win.flip()
41        psychopy.event.waitKeys(keyList=['left','right'])
42
43    # 刺激色を更新する
44    stim1_lum = conditions[condition_index][0]
45    probe_lum = conditions[condition_index][1]
46    stim1.setFillColor([stim1_lum, stim1_lum, stim1_lum])
47    probe.setFillColor([probe_lum, probe_lum, probe_lum])
48
49    waiting_keypress = True
50    psychopy.event.getKeys()
51    clock.reset()
52    while waiting_keypress:
53        keys = psychopy.event.getKeys(timeStamped=clock)
54        for key in keys:
55            if key[0]=='left' or key[0]=='right':
56                datafile.write( # 刺激のパラメータと反応を出力
57                    '{:.1f},{:.1f},{},{:.3f}\n'.format(
58                        stim1_lum, probe_lum, key[0], key[1]))
59                datafile.flush() # 直ちにファイルに書き出す
60                waiting_keypress = False
61                break
62            elif key[0]=='escape': # ESCキーが押された場合は
63                datafile.close()   # 直ちに終了する
64                psychopy.core.exit()
65        stim1.draw()
66        stim2.draw()
67        probe.draw()
68
69        win.flip()
70
71    win.flip() #刺激を消去する
72    psychopy.core.wait(1.0) #1.0秒待つ
73
74datafile.close() #データファイルを閉じる

2.3.2節 テキストファイルへのデータの書き出しと読み込み

1) タブ区切りデータの書き出しと読み込み

ファイルへの出力では、write()の引数の,を\tに変更すればよい。あと、出力ファイル名の拡張子を変更するように指示されているのでopen()の引数も変更する必要がある。

 1#coding:utf-8
 2from __future__ import division
 3from __future__ import unicode_literals
 4import codecs
 5import random
 6
 7conditions = []
 8for stim1_lum in [-0.3, 0.3]:
 9    for probe_lum_index in range(9):
10        probe_lum = 0.05*(probe_lum_index-4)
11        conditions.append([stim1_lum, probe_lum])
12conditions *= 5
13random.shuffle(conditions)
14
15with codecs.open('conditions.txt', 'w', 'utf-8') as fp
16    for condition in conditions:
17        fp.write('{:.1f}\t{:.2f}\n'.format(*condition))

読み込みでも同様に、ファイル名を変更してsplit()の引数の,を\tに変更すればよい。

 1#coding:utf-8
 2from __future__ import division
 3from __future__ import unicode_literals
 4import codecs
 5
 6conditions = []
 7with codecs.open('conditions.txt', 'r', 'utf-8') as fp:
 8    for line in fp:
 9        data = line.rstrip().split('\t')
10        conditions.append(float(data[0]), float(data[1]))
11
12print(conditions)

2.3.3節 反応に基づいた処理の分岐

1) 極限法への書き換え

2) プローブの初期値の無作為化

以下に例を示す。

候補となる値が(符号を無視して)4つしかないのでrandom.choice()の引数に値を列挙する方法を用いた。 符号は下降系列では正、上昇系列では負でなければならないので、if文を用いてseriesの値に応じて処理を振り分ける際に上昇系列ならば-1をかけて負の値にすればよい。

以上の処理により、conditionsにprobe_lumの値を含む必要がなくなったので、conditionsの生成処理も変更されている。

 1#coding:utf-8
 2from __future__ import division
 3from __future__ import unicode_literals
 4import psychopy.visual
 5import psychopy.event
 6import psychopy.core
 7import codecs
 8import random
 9
10conditions = []
11for stim1_lum in [-0.3, 0.3]:
12    for series in ['up', 'down']:
13        conditions.append([stim1_lum, series])
14
15conditions *= 5 # リストの内容を5回繰り返す
16random.shuffle(conditions) # 無作為な順序に並び替える
17
18win = psychopy.visual.Window(
19    monitor='defaultMonitor', units='cm', color='black')
20clock = psychopy.core.Clock()
21
22stim1 = psychopy.visual.Rect( # fillColorは各試行開始時に指定する
23    win, width=6.0, height=6.0, pos=[-4,0], lineColor=None)
24stim2 = psychopy.visual.Rect( # stim2のfillColorは固定
25    win, width=3.0, height=3.0, pos=[-4,0],
26    lineColor=None, fillColor=[0.0, 0.0, 0.0])
27probe = psychopy.visual.Rect(
28    win, width=3.0, height=3.0, pos=[4,0], lineColor=None)
29
30datafile = codecs.open('data.csv','w','utf-8')
31datafile.write('Stim1,Probe_Start,Step,Probe\n') # ヘッダを出力しておく
32
33for condition in conditions: # conditionsから各試行の条件を取り出す
34    # 刺激色を更新する
35    stim1_lum = condition[0]
36    probe_lum = random.choice([0.85, 0.8, 0.75, 0.7]) # 無作為に選ぶ
37    series = condition[1]  # この行以降を追加
38    if series == 'down':   # 下降系列
39        step = -0.05       # 変化量はマイナス
40    else:                  # 上昇系列
41        step = 0.05        # 変化量はプラス
42        probe_lum *= -1    # probe_lumの符号を反転し低輝度から始める
43    stim1.setFillColor([stim1_lum, stim1_lum, stim1_lum])
44    probe.setFillColor([probe_lum, probe_lum, probe_lum])
45
46    start_probe_lum = probe_lum # 開始前にstart_probe_lumに値を複製
47    in_series = True
48    psychopy.event.getKeys()
49    while in_series:
50        stim1.draw() # ここで描画等を済ませておく
51        stim2.draw()
52        probe.draw()
53        win.flip()
54
55        keys = psychopy.event.waitKeys(keyList=['up','down'])
56        if series in keys: # 刺激を変化させる場合
57            probe_lum += step
58            probe.setFillColor([probe_lum, probe_lum, probe_lum])
59        elif keys != []:   # 現在の系列を終了させる場合
60            datafile.write('{:.2f},{:.2f},{:.2f},{:.2f}\n'.format(
61                stim1_lum, start_probe_lum, step, probe_lum))
62            in_series = False
63
64    win.flip() #刺激を消去する
65    psychopy.core.wait(1.0) #1.0秒待つ
66
67datafile.close() #データファイルを閉じる

この方法では±0.85,±0.80,±0.75,±0.70が出現する頻度は均等にならない。総試行数が8の倍数ではないので当然である。 総試行数が変わってしまうが以下のように条件リストを作成すると、各誘導刺激と系列の組み合わせにつき±0.85,±0.80,±0.75,±0.70が均等に1回ずつ出現するようにすることが出来る。

conditions = []
for stim1_lum in [-0.3, 0.3]:
    for series in ['up', 'down']:
        for probe_lum in [0.85, 0.8, 0.75, 0.7]:
            if series == 'up':
                conditions.append([stim1_lum, -probe_lum, series])
            else:
                conditions.append([stim1_lum, probe_lum, series])

2.3.4節 マウスの利用

1) ウィンドウの単位とマウス

2) マウスカーソル初期位置の指定と表示のトグル

以下に例を示す。

スクリーンの左上の座標の計算がやや難しいポイントである。単位はheightなのだからY座標は0.5である。X座標はスクリーン高さを1.0とするのだから、スクリーン解像度の幅÷高さに-0.5を乗じた値である。この例では読者がどのような解像度のスクリーンで実行するかわからないので、Windowオブジェクトのデータ属性sizeから解像度を得るという方法をとった(16行目)。

マウスカーソルのON/OFFは、マウスカーソルの状態を維持する変数を用意するとよいだろう。スペースキーが押されたら変数の値をTrueからFalseへ、あるいはFalseからTrueへ変更しなければならないが、これはnot演算子を使うと場合分けせずに処理できる。34行目のようにgetKeys()で検出するキーをスペースキーだけに制限してしまえば、getKeys()の戻り値が[]ではないことを判定するだけでスペースキー押しを検出できる。

 1#coding:utf-8
 2from __future__ import division
 3from __future__ import unicode_literals
 4import psychopy.visual
 5import psychopy.event
 6
 7win = psychopy.visual.Window(
 8    monitor='defaultMonitor', units='height', fullscr=True)
 9
10button = psychopy.visual.Rect(win, width=0.1, height=0.1)
11state = psychopy.visual.TextStim(
12    win, pos=[0,-0.2], height=0.05)
13
14mouse = psychopy.event.Mouse()
15
16wh, ww = win.size
17mouse.setPos((0.5*(ww/wh), 0.5))
18
19mouse_visible = True
20mouse.setVisible(mouse_visible)
21
22mouse.clickReset()
23while True:
24    if mouse.isPressedIn(button,buttons=[0]): # 中でボタン0が押された
25        break
26    elif button.contains(mouse): # 中にマウスカーソルがある
27        button.setFillColor([-1,1,-1])
28    else: # 中にマウスカーソルが無い
29        button.setFillColor([-1,-1,-1])
30
31    state.setText('Pos: {}\nButtons: {}'.format(
32        mouse.getPos(), mouse.getPressed(getTime=True)))
33
34    if not psychopy.event.getKeys(keyList=['space']) == []:
35        mouse_visible = not mouse_visible
36        mouse.setVisible(mouse_visible)
37
38    button.draw()
39    state.draw()
40    win.flip()

3) マウスカーソルに合わせて刺激を移動

以下に例を示す。27行目でgetPos()を実行してしまっているので30から31行目のsetText()ではgetPos()を呼ばずに先ほどのgetPos()の結果を利用している。

 1#coding:utf-8
 2from __future__ import division
 3from __future__ import unicode_literals
 4import psychopy.visual
 5import psychopy.event
 6
 7win = psychopy.visual.Window(
 8    monitor='defaultMonitor', units='height', fullscr=True)
 9
10button = psychopy.visual.Rect(win, width=0.1, height=0.1)
11moving_rect = psychopy.visual.Rect(
12    win, width=0.2, height=0.2, fillColor=None, lineColor='red')
13state = psychopy.visual.TextStim(
14    win, pos=[0,-0.2], height=0.05)
15
16mouse = psychopy.event.Mouse()
17
18mouse.clickReset()
19while True:
20    if mouse.isPressedIn(button,buttons=[0]): # 中でボタン0が押された
21        break
22    elif button.contains(mouse): # 中にマウスカーソルがある
23        button.setFillColor([-1,1,-1])
24    else: # 中にマウスカーソルが無い
25        button.setFillColor([-1,-1,-1])
26
27    pos = mouse.getPos()
28    moving_rect.setPos(pos)
29
30    state.setText('Pos: {}\nButtons: {}'.format(
31        pos, mouse.getPressed(getTime=True)))
32
33    button.draw()
34    moving_rect.draw()
35    state.draw()
36    win.flip()

4) マウスのボタン押し回数のカウント

以下に例を示す。prev_stateの更新はボタン押し反応の判定の終了後に行うことと、if文の結果に関わらず常に更新を行うことがポイントである。

 1#coding:utf-8
 2from __future__ import division
 3from __future__ import unicode_literals
 4import psychopy.visual
 5import psychopy.event
 6
 7key_count = 0
 8press_count = 0
 9
10win = psychopy.visual.Window(monitor='defaultMonitor', fullscr=False)
11state = psychopy.visual.TextStim(win)
12mouse = psychopy.event.Mouse()
13prev_state = mouse.getPressed()[0]
14
15while True:
16    keys = psychopy.event.getKeys(keyList=['space'])
17    key_count += len(keys)
18
19    buttons = mouse.getPressed()
20
21    if buttons[0] == 1 and prev_state == 0:
22        press_count += 1
23    prev_state = buttons[0]
24
25    if buttons[2] == 1: break
26
27    state.setText(
28        'Key: {}, Button:{}, {},{}'.format(key_count,press_count,buttons[0],prev_state))
29    state.draw()
30    win.flip()

2.3.5節 様々な外部機器の利用

1) ジョイスティックによる刺激位置の変更

以下に例を示す。ジョイスティックは物によりレバーを操作した時の戻り値が異なるので、どのようなジョイスティックでも同じ結果が得られるコードを書くのは難しい。この例では、レバーを操作するとgetAllAxes()の戻り値の対応する要素が±1.0になるデジタル式のレバーを持つものを想定している。このようなデジタル式のレバーでも、レバーを戻した後に値が完全に0に戻らない(0.01などの値になる)ものがあるので、変数thresholdに「レバーが操作されている」と判定する閾値を設定して、閾値を超えた時のみ変数stepだけ位置を変化させている。

 1#coding:utf-8
 2from __future__ import division
 3from __future__ import unicode_literals
 4import psychopy.visual
 5import psychopy.core
 6import psychopy.hardware.joystick
 7
 8win = psychopy.visual.Window(units='height')
 9rect = psychopy.visual.Rect(win, width=0.1, height=0.1)
10text = psychopy.visual.TextStim(win, height=0.03)
11
12num_joys = psychopy.hardware.joystick.getNumJoysticks()
13if num_joys==0: # ジョイスティックが0個(存在しない)なら終了
14    text.setText('No joystick was found.')
15    text.draw()
16    win.flip()
17    psychopy.core.wait(1.0)
18    psychopy.core.exit()
19
20joystick = psychopy.hardware.joystick.Joystick(0)
21
22pos = [0, 0]
23threshold = 0.1
24step = 0.02
25
26while True:
27    buttons = joystick.getAllButtons()
28    if not False in buttons[:2]:
29        break
30
31    axes = joystick.getAllAxes()
32    if axes[0] > 0 and axes[0] > threshold:
33        pos[0] += step
34    elif axes[0] < 0 and axes[0] < -threshold:
35        pos[0] -= step
36    if axes[1] > 0 and axes[1] > threshold:
37        pos[1] += step
38    elif axes[1] < 0 and axes[1] < -threshold:
39        pos[1] -= step
40
41    rect.setPos(pos)
42    rect.draw()
43
44    text.setText('Axes:{}\n\nButtons:{}\n\nHats:{}'.format(
45        axes, buttons, joystick.getAllHats()))
46    text.draw()
47    win.flip()
48
49win.close()

32から39行目のif文はお世辞にも見やすいコードと言えないので、本文で解説されていない関数を使って整理してみよう。まず、絶対値を返すnumpy.abs()、符号を返すnumpy.sign()を用いると、elifを使用せずに記述することが出来る。

if np.abs(axes[0]) > threshold:
    pos[0] += step * np.sign(axes[0])
if np.abs(axes[1]) > threshold:
    pos[1] += step * np.sign(axes[1])

rect.setPos(pos)

さらに、X軸とY軸の処理はaxesのインデックスが異なるだけなので、関数を使ってまとめることが出来る。

def getStep(axis):
    if np.abs(axis) > threshold:
        return step * np.sign(axis)

この関数を以下のように使用する。

pos[0] += getStep(axis[0])
pos[1] += getStep(axis[1])

rect.setPos(pos)

なお、=演算子を用いてパラメータを更新する方法を用いるならば、以下のように書くことも可能である。

rect.pos += [getStep(axis[0]), getStep(axis[1])]

アナログジョイスティックを使用しているのであれば、thresholdを設定せずにgetAllAxes()の戻り値に適当な定数を乗じて使用すればいいだろう。

rect.pos += [axes[0] * v, axes[1] * v] # vは移動速度を調整するための定数

rect.setPos(pos)

2) callOnFlip()からclock.getTime()を実行する(フリップ時に時刻を得る)

これは難問である。まず、clock.getTime()ではなくgetKeys()で得られた時刻を出力する方法について述べる。これは単に47から49行目のdatafile.write()をcallOnFlip()に置き換えればよい。ただし、50行目のflush()は意味がなくなるので60行目のflip()の後へ回した方がよいだろう。ここまでわかればとりあえずcallOnFlip()の基本的な使い方が分かっていると思ってよい。

win.callOnFlip(
    datafile.write,
    '{:.1f},{:.1f},{},{:.3f}\n'.format(stim1_lum, probe_lum, key[0], key[1]))

さて、練習問題はclock.getTime()を実行してその結果を出力するというものであった。上と同じような方法では前もってgetTime()を実行しておいた結果しか出力できないので、関数を利用する。練習問題では時刻以外のものを出力するように求められていないが、元のコード2.4と同様に刺激のパラメータやキー名も出力するとしよう。以下のように、時刻以外のパラメータを引数として受け取って、内部でgetTime()を実行してwrite()、flush()を行う関数を用意する。

def write_on_flip(stim1_lum, probe_lum, key):
   datafile.write('{:.1f},{:.1f},{},{:.3f},#write_on_flip\n'.format(
                        stim1_lum, probe_lum, key, clock.getTime()))
   datafile.flush()

この関数の内部ではdatfile、clockが定義されていないが、これらの値は関数の外部で定義されているものが利用される。この関数をcallOnFlip()で実行するようにした例を以下に示す。元のコード2.4でのwrite()もそのまま残して、callOnFlip()による出力と比較できるようにしている。

 1#coding:utf-8
 2from __future__ import division
 3from __future__ import unicode_literals
 4import psychopy.visual
 5import psychopy.event
 6import psychopy.core
 7import codecs
 8import random # 疑似乱数の発生や無作為な並び替えなどを行う
 9
10conditions = [] # 空リストを用意しappend()で条件を加えていく
11for stim1_lum in [-0.3, 0.3]:
12    for probe_lum_index in range(9):
13        probe_lum = 0.05*(probe_lum_index-4)
14        conditions.append([stim1_lum, probe_lum])
15
16conditions *= 5 # リストの内容を5回繰り返す
17random.shuffle(conditions) # 無作為な順序に並び替える
18
19win = psychopy.visual.Window(fullscr=True,
20    monitor='defaultMonitor', units='cm', color='black')
21clock = psychopy.core.Clock()
22
23stim1 = psychopy.visual.Rect( # fillColorは各試行開始時に指定する
24    win, width=6.0, height=6.0, pos=[-4,0], lineColor=None)
25stim2 = psychopy.visual.Rect( # stim2のfillColorは固定
26    win, width=3.0, height=3.0, pos=[-4,0],
27    lineColor=None, fillColor=[0.0, 0.0, 0.0])
28probe = psychopy.visual.Rect(
29    win, width=3.0, height=3.0, pos=[4,0], lineColor=None)
30
31datafile = codecs.open('data.csv','w','utf-8')
32datafile.write('Stim1,Probe,Response,RT\n') # ヘッダを出力しておく
33
34def write_on_flip(stim1_lum, probe_lum, key):
35   datafile.write('{:.1f},{:.1f},{},{:.3f},#write_on_flip\n'.format(
36                        stim1_lum, probe_lum, key, clock.getTime()))
37
38for condition in conditions: # conditionsから各試行の条件を取り出す
39    # 刺激色を更新する
40    stim1_lum = condition[0]
41    probe_lum = condition[1]
42    stim1.setFillColor([stim1_lum, stim1_lum, stim1_lum])
43    probe.setFillColor([probe_lum, probe_lum, probe_lum])
44
45    waiting_keypress = True
46    psychopy.event.getKeys()
47    clock.reset()
48    while waiting_keypress:
49        keys = psychopy.event.getKeys(timeStamped=clock)
50        for key in keys:
51            if key[0]=='left' or key[0]=='right':
52                datafile.write( # 刺激のパラメータと反応を出力
53                    '{:.1f},{:.1f},{},{:.3f}\n'.format(
54                        stim1_lum, probe_lum, key[0], key[1]))
55                win.callOnFlip(write_on_flip, stim1_lum, probe_lum, key[0])
56                waiting_keypress = False
57                break
58            elif key[0]=='escape': # ESCキーが押された場合は
59                datafile.close()   # 直ちに終了する
60                psychopy.core.exit()
61        stim1.draw()
62        stim2.draw()
63        probe.draw()
64
65        win.flip()
66
67    win.flip() #刺激を消去する
68    psychopy.core.wait(1.0) #1.0秒待つ
69
70datafile.close() #データファイルを閉じる

60fpsのモニターで実行した際の出力例を以下に示す。#write_on_flipと後ろについているのがcallOnFlip()によって出力された行である。

Stim1,Probe,Response,RT
0.3,0.2,right,2.504
0.3,0.2,right,2.520,#write_on_flip
-0.3,0.2,right,1.349
-0.3,0.2,right,1.365,#write_on_flip
0.3,-0.2,left,1.416
0.3,-0.2,left,1.432,#write_on_flip
-0.3,-0.2,left,1.200
-0.3,-0.2,left,1.216,#write_on_flip
-0.3,-0.2,left,1.016
-0.3,-0.2,left,1.033,#write_on_flip
0.3,0.0,right,1.400
0.3,0.0,right,1.416,#write_on_flip

分かり易いように、RTの列だけを抜き出したものを以下に示す。#write_on_flipと出力されている行の値から、その一つ上の行の値を引くと、0.016から0.017であることがわかる。言うまでもなく60fps = 0.01666...ms/frameなので、ほぼフレーム間時間ほどずれていることが分かる。

RT
2.504
2.52        #write_on_flip
1.349
1.365       #write_on_flip
1.416
1.432       #write_on_flip
1.2
1.216       #write_on_flip
1.016
1.033       #write_on_flip
1.4
1.416       #write_on_flip

この練習問題ではcallOnFlip()に引数を渡したり、callOnFlip()の時点で何かの処理をおこなったりする練習としてgetTime()の結果をwrite()で出力したが、反応時間の計測としてはキー押しが検出できた後1フレーム分遅れるので望ましくない。flip()は刺激描画の直後に処理を行うためのものなので、本文のコード2.18のような使い方が効果的である。

2.3.6節 ダイアログを用いた実行時のパラメータ変更

1) 実験パラメータをダイアログで指定できるようにする

以下に例を示す。参考のため、問題文では求められていないが、ダイアログに入力された値のチェックを行っている。

 1#coding:utf-8
 2from __future__ import division
 3from __future__ import unicode_literals
 4import psychopy.visual
 5import psychopy.event
 6import psychopy.core
 7import psychopy.gui
 8import codecs
 9import random # 疑似乱数の発生や無作為な並び替えなどを行う
10
11param_dict = {
12    'データファイル名':'data.csv',
13    '繰り返し回数':10,
14    }
15try:
16    dlg = psychopy.gui.DlgFromDict(param_dict, title='パラメータ入力')
17except:
18    print 'dialog has terminated abnormally.'
19    psychopy.core.quit()
20
21if not dlg.OK:
22    psychopy.core.quit()
23
24try:
25    num_repetition = int(param_dict['繰り返し回数'])
26except:
27    print 'number of repetition must be an integer.'
28    psychopy.core.quit()
29
30if num_repetition <= 0:
31    print 'number of repetition must be greater than 0.'
32    psychopy.core.quit()
33
34try:
35    datafile = codecs.open(param_dict['データファイル名'],'w','utf-8')
36except:
37    print 'could not open {},'.format(param_dict['データファイル名'])
38    psychopy.core.quit()
39
40conditions = [] # 空リストを用意しappend()で条件を加えていく
41for stim1_lum in [-0.3, 0.3]:
42    for probe_lum_index in range(9):
43        probe_lum = 0.05*(probe_lum_index-4)
44        conditions.append([stim1_lum, probe_lum])
45
46conditions *=  num_repetition
47random.shuffle(conditions) # 無作為な順序に並び替える
48
49# 以下コード2.4と同一なので省略

この例では、繰り返し回数に小数が入力されていた場合にもそのまま実行されてしまうので(int()が例外を起こさないため)、本文中では紹介していない関数isinstance()を用いて以下のようにする方が正確である。

num_repetition = param_dict['繰り返し回数']
if not isinstance(num_repetition, int):
    print 'number of repetition must be an integer.'
    psychopy.core.quit()

あるいは、1での剰余が0ではないことで判定することも出来る(raiseは例外を送出する文)。他にもnum_repetition == int(num_repetition)がFalseになることで判定することも出来るだろう。

try:
    num_repetition = param_dict['繰り返し回数']
    if num_repetition % 1 != 0:
        raise
    num_repetition = int(num_repetition)
except:
    print 'number of repetition must be an integer.'
    psychopy.core.quit()

2.3.7節 RatingScaleの利用

1) 複数のRatingScaleを使う

以下に例を示す。すべてのRatingScaleのnoResponseがFalseになっている(Trueがひとつもない)ことをループの終了条件とすればよい。

 1#coding:utf-8
 2from __future__ import division
 3from __future__ import unicode_literals
 4import psychopy.visual
 5import codecs
 6
 7win = psychopy.visual.Window(
 8    monitor='defaultMonitor', units='height', fullscr=True)
 9
10scale1 = psychopy.visual.RatingScale(
11    win, low=1, high=5, markerStart=3, pos=[0,0.6], size=0.5)
12scale2 = psychopy.visual.RatingScale(win, low=1, high=7,
13    scale='1=好ましくない / 7=好ましい', textColor='black', size=0.5,
14    lineColor='black', markerColor='red', pos=[0,0.2])
15scale3 = psychopy.visual.RatingScale(win, choices=['赤','青','緑'],
16    size=0.5, pos=[0,-0.2])
17scale4 = psychopy.visual.RatingScale(
18    win, precision=100, low=1, high=5, pos=[0,-0.6], size=0.5)
19
20while True:
21    if not True in [scale1.noResponse, scale2.noResponse,
22                    scale3.noResponse, scale4.noResponse]:
23        break
24
25    scale1.draw()
26    scale2.draw()
27    scale3.draw()
28    scale4.draw()
29    win.flip()

2) 中間地点にマーカーを置けるようにする

2.3.8節 ファイルからの画像の提示

1) 画像を1/2のサイズで描画する

以下に例を示す。ImageStimオブジェクトのサイズを一度明示的に指定すると、その後にsetImage()で画像を変更しても指定したサイズが継承される。そこでこの例では、setImage()の後で本文中で述べた隠し属性_origSizeを用いてpix単位の画像の大きさを求め、heightに換算している。本来ならば、このような手法に頼るのではなく刺激画像作成の時点で大きさを統一しておくことが望ましい。

 1#coding:utf-8
 2from __future__ import division
 3from __future__ import unicode_literals
 4import psychopy.visual
 5import psychopy.event
 6
 7win = psychopy.visual.Window(
 8    monitor='defaultMonitor', units='height', fullscr=True)
 9stim = psychopy.visual.ImageStim(win, '01.jpg')
10stim.setAutoDraw(True)
11w, h = stim.size
12stim.setSize((w/2, -h/2))
13
14win.flip() # autoDraw=Trueを指定すると自動的にdraw()される
15psychopy.event.waitKeys()
16
17stim.setImage('images/02.jpg') # 画像を変更
18w = stim._origSize[0]/win.size[1]
19h = stim._origSize[1]/win.size[1]
20stim.setSize((w/2, -h/2))
21win.flip()
22psychopy.event.waitKeys()
23
24win.close()

2) 画像の周辺をぼかす

引数mask='gauss'を追加するだけである。問題文では要求されていないが、2枚目の画像で周辺をぼかさないようにする例も示してる。

 1#coding:utf-8
 2from __future__ import division
 3from __future__ import unicode_literals
 4import psychopy.visual
 5import psychopy.event
 6
 7win = psychopy.visual.Window(
 8    monitor='defaultMonitor', units='height', fullscr=True)
 9stim = psychopy.visual.ImageStim(win, '01.jpg', mask='gauss')
10stim.setAutoDraw(True)
11
12win.flip() # autoDraw=Trueを指定すると自動的にdraw()される
13psychopy.event.waitKeys()
14
15stim.setImage('images/02.jpg') # 画像を変更
16stim.setMask(None)             # マスクを消去
17win.flip()
18psychopy.event.waitKeys()
19
20win.close()

2.3.9節 音声と動画の提示及び音声の録音

1) 動画を10秒送り/10秒戻しする

以下に例を示す。筆者の動作環境(Win10 X64, PsychoPy 1.83.04)ではMovieStim3のシーク動作が不安定だったので、MovieStim2を使用している。

 1#coding:utf-8
 2from __future__ import division
 3from __future__ import unicode_literals
 4import psychopy.visual
 5
 6win = psychopy.visual.Window(units='height')
 7movie = psychopy.visual.MovieStim2(win, 'movie.mp4')
 8text = psychopy.visual.TextStim(win, height=0.03)
 9
10info_str = 'Size:{}, Duration:{}, FPS:{:.3f}\n'.format(
11    movie.size, movie.duration, movie.getFPS())
12
13while movie.status != psychopy.visual.FINISHED:
14    text.setText(info_str+'{}'.format(
15        movie.getCurrentFrameTime()))
16
17    keys = psychopy.event.getKeys()
18    if 'space' in keys:
19        if movie.status == psychopy.visual.PAUSED:
20            movie.play()
21        else:
22            movie.pause()
23    elif 'left' in keys:
24        t = movie.getCurrentFrameTime()
25        movie.seek(max(0,t-10))
26    elif 'right' in keys:
27        t = movie.getCurrentFrameTime()
28        movie.seek(min(movie.duration,t+10))
29
30    movie.draw()
31    text.draw()
32    win.flip()

2) データ属性statusを使わずに再生を終了する

以下に例を示す。

 1#coding:utf-8
 2from __future__ import division
 3from __future__ import unicode_literals
 4import psychopy.sound
 5import psychopy.core
 6
 7stim = psychopy.sound.Sound(440, secs=1.0)
 8duration = stim.getDuration()
 9clock = psychopy.core.Clock()
10stim.play()
11while clock.getTime() <= duration:
12    psychopy.core.wait(0.01)

2.4.1節 PCによる刺激提示および反応時間計測の仕組み

1) flip()の所要時間の確認

2) ログの出力

3) 刺激が変化したフレームのみログを出力(logOnFlip())

以下に例を示す。

 1#coding:utf-8
 2from __future__ import division
 3from __future__ import unicode_literals
 4import psychopy.visual
 5import psychopy.event
 6import psychopy.core
 7import psychopy.logging
 8import codecs
 9import random # 疑似乱数の発生や無作為な並び替えなどを行う
10
11log_file = psychopy.logging.LogFile(
12    'test.log', level=psychopy.logging.DEBUG)
13
14conditions = [] # 空リストを用意しappend()で条件を加えていく
15for stim1_lum in [-0.3, 0.3]:
16    for probe_lum_index in range(9):
17        probe_lum = 0.05*(probe_lum_index-4)
18        conditions.append([stim1_lum, probe_lum])
19
20conditions *= 5 # リストの内容を5回繰り返す
21random.shuffle(conditions) # 無作為な順序に並び替える
22
23win = psychopy.visual.Window(fullscr=True,
24    monitor='defaultMonitor', units='cm', color='black')
25clock = psychopy.core.Clock()
26
27stim1 = psychopy.visual.Rect( # fillColorは各試行開始時に指定する
28    win, width=6.0, height=6.0, pos=[-4,0], lineColor=None)
29stim2 = psychopy.visual.Rect( # stim2のfillColorは固定
30    win, width=3.0, height=3.0, pos=[-4,0],
31    lineColor=None, fillColor=[0.0, 0.0, 0.0])
32probe = psychopy.visual.Rect(
33    win, width=3.0, height=3.0, pos=[4,0], lineColor=None)
34
35datafile = codecs.open('data.csv','w','utf-8')
36datafile.write('Stim1,Probe,Response,RT\n') # ヘッダを出力しておく
37
38for condition in conditions: # conditionsから各試行の条件を取り出す
39    # 刺激色を更新する
40    stim1_lum = condition[0]
41    probe_lum = condition[1]
42    stim1.setFillColor([stim1_lum, stim1_lum, stim1_lum])
43    probe.setFillColor([probe_lum, probe_lum, probe_lum])
44    win.logOnFlip('Stimulus onset', level=psychopy.logging.EXP)
45
46    waiting_keypress = True
47    psychopy.event.getKeys()
48    clock.reset()
49    while waiting_keypress:
50        keys = psychopy.event.getKeys(timeStamped=clock)
51        for key in keys:
52            if key[0]=='left' or key[0]=='right':
53                datafile.write( # 刺激のパラメータと反応を出力
54                    '{:.1f},{:.1f},{},{:.3f}\n'.format(
55                        stim1_lum, probe_lum, key[0], key[1]))
56                datafile.flush() # 直ちにファイルに書き出す
57                waiting_keypress = False
58                break
59            elif key[0]=='escape': # ESCキーが押された場合は
60                datafile.close()   # 直ちに終了する
61                psychopy.core.quit()
62        stim1.draw()
63        stim2.draw()
64        probe.draw()
65
66        win.flip()
67
68    win.flip() #刺激を消去する
69    psychopy.core.wait(1.0) #1.0秒待つ
70
71datafile.close() #データファイルを閉じる
72psychopy.logging.flush()

2.4.2節 ioHubパッケージ

1) ioHubを用いた反応の記録

以下に例を示す。

 1#coding:utf-8
 2from __future__ import division
 3from __future__ import unicode_literals
 4import psychopy.visual
 5import psychopy.iohub
 6import psychopy.core
 7import codecs
 8import random # 疑似乱数の発生や無作為な並び替えなどを行う
 9
10io = psychopy.iohub.launchHubServer()
11io.clearEvents('all')
12
13conditions = [] # 空リストを用意しappend()で条件を加えていく
14for stim1_lum in [-0.3, 0.3]:
15    for probe_lum_index in range(9):
16        probe_lum = 0.05*(probe_lum_index-4)
17        conditions.append([stim1_lum, probe_lum])
18
19conditions *= 5 # リストの内容を5回繰り返す
20random.shuffle(conditions) # 無作為な順序に並び替える
21
22win = psychopy.visual.Window(fullscr=True,
23    monitor='defaultMonitor', units='cm', color='black')
24clock = psychopy.core.Clock()
25
26stim1 = psychopy.visual.Rect( # fillColorは各試行開始時に指定する
27    win, width=6.0, height=6.0, pos=[-4,0], lineColor=None)
28stim2 = psychopy.visual.Rect( # stim2のfillColorは固定
29    win, width=3.0, height=3.0, pos=[-4,0],
30    lineColor=None, fillColor=[0.0, 0.0, 0.0])
31probe = psychopy.visual.Rect(
32    win, width=3.0, height=3.0, pos=[4,0], lineColor=None)
33
34datafile = codecs.open('data.csv','w','utf-8')
35datafile.write('Stim1,Probe,Response,RT\n') # ヘッダを出力しておく
36
37for condition in conditions: # conditionsから各試行の条件を取り出す
38    # 刺激色を更新する
39    stim1_lum = condition[0]
40    probe_lum = condition[1]
41    stim1.setFillColor([stim1_lum, stim1_lum, stim1_lum])
42    probe.setFillColor([probe_lum, probe_lum, probe_lum])
43
44    waiting_keypress = True
45    io.devices.keyboard.getKeys()
46    clock.reset()
47    while waiting_keypress:
48        events = io.devices.keyboard.getKeys()
49        for event in events:
50            if event.type == 'KEYBOARD_PRESS':
51                if event.key=='left' or event.key=='right':
52                    datafile.write( # 刺激のパラメータと反応を出力
53                        '{:.1f},{:.1f},{},{:.3f}\n'.format(
54                            stim1_lum, probe_lum, event.key, clock.getTime()))
55                    datafile.flush() # 直ちにファイルに書き出す
56                    waiting_keypress = False
57                    break
58                elif event.key=='escape': # ESCキーが押された場合は
59                    datafile.close()   # 直ちに終了する
60                    psychopy.core.quit()
61        stim1.draw()
62        stim2.draw()
63        probe.draw()
64
65        win.flip()
66
67    win.flip() #刺激を消去する
68    psychopy.core.wait(1.0) #1.0秒待つ
69
70datafile.close() #データファイルを閉じる
71io.quit()