.. title:: 「心理学実験プログラミング: Python/PsychoPyによる実験作成・データ処理」第2章練習問題の解答 2.2.1節 刺激提示と反応計測のための最小スクリプト ================================================== 1) フリップ回数とウィンドウ表示時間の関係 ------------------------------------------------- 2.4.1節を読むとわかるように、リフレッシュレートの回数だけflip()を行うとほぼ1秒となる。従って、range()の引数をリフレッシュレートのn倍にすればn秒間ウィンドウが表示される。ただし、ウィンドウの作成や廃棄にも時間を要するため、厳密にn秒間にはならない。このことは次節以降でスクリーンに刺激を描画するようになると、ウィンドウが開いてから刺激が描画されるまでわずかな遅延があることから確認できるだろう。 2.2.2節 図形や文字の描画 ============================= .. p.45-46 1) 単位cmによる描画の確認 -------------------------------- 略 2) 不透明度のアニメーション -------------------------------- 22行目のgetKeys()から28行目のbox.draw()までの間の何処かで以下のコードを挿入すると良い。24行目のbox.setOri()の直前か直後にしておけばboxに対する変更が一か所にまとまっていて分かり易いだろう。 .. code-block:: python box.setOpacity(clock.getTime()%1.0) =演算子を用いてパラメータを更新する場合は以下のように書けばよい。 .. code-block:: python box.opacity = clock.getTime()%1.0 20秒間かけて不透明度が0.0から1.0になるようにするには、20秒経過したときに解が1.0になる式をたてればよいのだから、経過時間を20.0で割った値をopacityに設定すればよい。 .. code-block:: python box.setOpacity(clock.getTime()/20.0) なお、今回の問題では20秒経過すると描画を終了するのでこれでよいが、20秒経過以降は不透明度1.0を維持したいのであれば、引数の中で最小の値を返す関数min()を用いて1.0を超えないようにすればよい。 .. code-block:: python 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節 キーボードイベントの処理と反応の保存 ================================================ .. p.50 1) キー押し判定のテクニック ----------------------------------- 以下に例を示す。コード2.3の27から38行目の部分に相当する。 .. code-block:: python 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の時のみキー押し判定を行えばよい。 .. code-block:: python 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と出力する)ならば、以下のように書くことも出来る。 .. code-block:: python 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()の引数を以下のように書き換える。 .. code-block:: python datafile.write('{},{:.1f}\n'.format(key[0],1000*key[1])) 2.2.4節 パラメータを無作為に変更した試行の繰り返し ==================================================== .. p.56 1) 実験結果の確認 -------------------- 略 2) 刺激パラメータの変更 ------------------------------ コード2.4の10から14行目を以下のように書き換える。probe_rumの計算は他にもいろいろな書き方が出来る。 .. code-block:: python 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という変数は不要である。 .. code-block:: python 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行目の後に挿入するといいだろう。 .. code-block:: python msg_stim = psychopy.visual.TextStim( win, text='スペースキーを押してください', height=1.0) 続いて、コード2.4の63から64行目に相当する部分(上記のコードを挿入することで行が変わっていることに注意)を以下のように変更する。flip()する前にmsg_stimをdraw()する点がポイントである。 .. code-block:: python msg_stim.draw() win.flip() psychopy.core.waitKeys(keyList=['space']) 4) 試行間にブランク画面を挿入 ----------------------------------------- 略 5) キーイベントのクリア ----------------------------------------- 略 2.3.1節 実験のブロック化 ============================= .. p.61 1) 実験のブロック化 ------------------------------------- 以下に例を示す。強調部分が変更点である(字下げだけの変更を除く)。 .. code-block:: python :linenos: :emphasize-lines: 10-19,32,38-48,75-78 #coding:utf-8 from __future__ import division from __future__ import unicode_literals import psychopy.visual import psychopy.event import psychopy.core import codecs import random # 疑似乱数の発生や無作為な並び替えなどを行う conditions = [] for stim1_lum in [-0.3, 0.3]: tmp_conditions = [] for probe_lum_index in range(9): probe_lum = 0.05*(probe_lum_index-4) tmp_conditions.append(probe_lum) tmp_conditions *= 5 random.shuffle(tmp_conditions) conditions.append([stim1_lum, tmp_conditions]) random.shuffle(conditions) # ブロックの順序も無作為化したい場合 win = psychopy.visual.Window(fullscr=True, monitor='defaultMonitor', units='cm', color='black') clock = psychopy.core.Clock() stim1 = psychopy.visual.Rect( # fillColorは各試行開始時に指定する win, width=6.0, height=6.0, pos=[-4,0], lineColor=None) stim2 = psychopy.visual.Rect( # stim2のfillColorは固定 win, width=3.0, height=3.0, pos=[-4,0], lineColor=None, fillColor=[0.0, 0.0, 0.0]) probe = psychopy.visual.Rect( win, width=3.0, height=3.0, pos=[4,0], lineColor=None) msg = psychopy.visual.TextStim(win, height=0.8) datafile = codecs.open('data.csv','w','utf-8') datafile.write('Stim1,Probe,Response,RT\n') # ヘッダを出力しておく for condition in conditions: # conditionsから各試行の条件を取り出す msg.setText('スペースキーを押すと実験が始まります') msg.draw() win.flip() psychopy.event.waitKeys(keyList=['space']) # 刺激色を更新する stim1_lum = condition[0] stim1.setFillColor([stim1_lum, stim1_lum, stim1_lum]) for probe_lum in condition[1]: probe.setFillColor([probe_lum, probe_lum, probe_lum]) waiting_keypress = True psychopy.event.getKeys() clock.reset() while waiting_keypress: keys = psychopy.event.getKeys(timeStamped=clock) for key in keys: if key[0]=='left' or key[0]=='right': datafile.write( # 刺激のパラメータと反応を出力 '{:.1f},{:.1f},{},{:.3f}\n'.format( stim1_lum, probe_lum, key[0], key[1])) datafile.flush() # 直ちにファイルに書き出す waiting_keypress = False break elif key[0]=='escape': # ESCキーが押された場合は datafile.close() # 直ちに終了する psychopy.core.exit() stim1.draw() stim2.draw() probe.draw() win.flip() win.flip() #刺激を消去する psychopy.core.wait(1.0) #1.0秒待つ msg.setText('実験は終了しました\nご協力ありがとうございました') msg.draw() win.flip() psychopy.event.waitKeys() datafile.close() #データファイルを閉じる 2) ブロック開始時の教示とキー押し待ち ------------------------------------------------- 以下に例を示す。強調部分が変更点である。 .. code-block:: python :linenos: :emphasize-lines: 30,35-41 #coding:utf-8 from __future__ import division from __future__ import unicode_literals import psychopy.visual import psychopy.event import psychopy.core import codecs import random # 疑似乱数の発生や無作為な並び替えなどを行う conditions = [] # 空リストを用意しappend()で条件を加えていく for stim1_lum in [-0.3, 0.3]: for probe_lum_index in range(9): probe_lum = 0.05*(probe_lum_index-4) conditions.append([stim1_lum, probe_lum]) conditions *= 5 # リストの内容を5回繰り返す random.shuffle(conditions) # 無作為な順序に並び替える win = psychopy.visual.Window(fullscr=True, monitor='defaultMonitor', units='cm', color='black') clock = psychopy.core.Clock() stim1 = psychopy.visual.Rect( # fillColorは各試行開始時に指定する win, width=6.0, height=6.0, pos=[-4,0], lineColor=None) stim2 = psychopy.visual.Rect( # stim2のfillColorは固定 win, width=3.0, height=3.0, pos=[-4,0], lineColor=None, fillColor=[0.0, 0.0, 0.0]) probe = psychopy.visual.Rect( win, width=3.0, height=3.0, pos=[4,0], lineColor=None) msg = psychopy.visual.TextStim(win, height=0.8) datafile = codecs.open('data.csv','w','utf-8') datafile.write('Stim1,Probe,Response,RT\n') # ヘッダを出力しておく for condition_index in range(len(conditions)): if condition_index != 0 and condition_index % 30 == 0: msg.setText('''第{}試行\n準備が出来たらカーソルキーの左右いずれかを押して 実験を再開してください'''.format(condition_index+1)) msg.draw() win.flip() psychopy.event.waitKeys(keyList=['left','right']) # 刺激色を更新する stim1_lum = conditions[condition_index][0] probe_lum = conditions[condition_index][1] stim1.setFillColor([stim1_lum, stim1_lum, stim1_lum]) probe.setFillColor([probe_lum, probe_lum, probe_lum]) waiting_keypress = True psychopy.event.getKeys() clock.reset() while waiting_keypress: keys = psychopy.event.getKeys(timeStamped=clock) for key in keys: if key[0]=='left' or key[0]=='right': datafile.write( # 刺激のパラメータと反応を出力 '{:.1f},{:.1f},{},{:.3f}\n'.format( stim1_lum, probe_lum, key[0], key[1])) datafile.flush() # 直ちにファイルに書き出す waiting_keypress = False break elif key[0]=='escape': # ESCキーが押された場合は datafile.close() # 直ちに終了する psychopy.core.exit() stim1.draw() stim2.draw() probe.draw() win.flip() win.flip() #刺激を消去する psychopy.core.wait(1.0) #1.0秒待つ datafile.close() #データファイルを閉じる 2.3.2節 テキストファイルへのデータの書き出しと読み込み ============================================================== .. p.63 1) タブ区切りデータの書き出しと読み込み ------------------------------------------ ファイルへの出力では、write()の引数の,を\\tに変更すればよい。あと、出力ファイル名の拡張子を変更するように指示されているのでopen()の引数も変更する必要がある。 .. code-block:: python :linenos: :emphasize-lines: 15,17 #coding:utf-8 from __future__ import division from __future__ import unicode_literals import codecs import random conditions = [] for stim1_lum in [-0.3, 0.3]: for probe_lum_index in range(9): probe_lum = 0.05*(probe_lum_index-4) conditions.append([stim1_lum, probe_lum]) conditions *= 5 random.shuffle(conditions) with codecs.open('conditions.txt', 'w', 'utf-8') as fp for condition in conditions: fp.write('{:.1f}\t{:.2f}\n'.format(*condition)) 読み込みでも同様に、ファイル名を変更してsplit()の引数の,を\\tに変更すればよい。 .. code-block:: python :linenos: :emphasize-lines: 7,9 #coding:utf-8 from __future__ import division from __future__ import unicode_literals import codecs conditions = [] with codecs.open('conditions.txt', 'r', 'utf-8') as fp: for line in fp: data = line.rstrip().split('\t') conditions.append(float(data[0]), float(data[1])) print(conditions) 2.3.3節 反応に基づいた処理の分岐 ========================================= 1) 極限法への書き換え ----------------------------------- 略 2) プローブの初期値の無作為化 ----------------------------------- 以下に例を示す。 候補となる値が(符号を無視して)4つしかないのでrandom.choice()の引数に値を列挙する方法を用いた。 符号は下降系列では正、上昇系列では負でなければならないので、if文を用いてseriesの値に応じて処理を振り分ける際に上昇系列ならば-1をかけて負の値にすればよい。 以上の処理により、conditionsにprobe_lumの値を含む必要がなくなったので、conditionsの生成処理も変更されている。 .. code-block:: python :linenos: :emphasize-lines: 11-13,36-44 #coding:utf-8 from __future__ import division from __future__ import unicode_literals import psychopy.visual import psychopy.event import psychopy.core import codecs import random conditions = [] for stim1_lum in [-0.3, 0.3]: for series in ['up', 'down']: conditions.append([stim1_lum, series]) conditions *= 5 # リストの内容を5回繰り返す random.shuffle(conditions) # 無作為な順序に並び替える win = psychopy.visual.Window( monitor='defaultMonitor', units='cm', color='black') clock = psychopy.core.Clock() stim1 = psychopy.visual.Rect( # fillColorは各試行開始時に指定する win, width=6.0, height=6.0, pos=[-4,0], lineColor=None) stim2 = psychopy.visual.Rect( # stim2のfillColorは固定 win, width=3.0, height=3.0, pos=[-4,0], lineColor=None, fillColor=[0.0, 0.0, 0.0]) probe = psychopy.visual.Rect( win, width=3.0, height=3.0, pos=[4,0], lineColor=None) datafile = codecs.open('data.csv','w','utf-8') datafile.write('Stim1,Probe_Start,Step,Probe\n') # ヘッダを出力しておく for condition in conditions: # conditionsから各試行の条件を取り出す # 刺激色を更新する stim1_lum = condition[0] probe_lum = random.choice([0.85, 0.8, 0.75, 0.7]) # 無作為に選ぶ series = condition[1] # この行以降を追加 if series == 'down': # 下降系列 step = -0.05 # 変化量はマイナス else: # 上昇系列 step = 0.05 # 変化量はプラス probe_lum *= -1 # probe_lumの符号を反転し低輝度から始める stim1.setFillColor([stim1_lum, stim1_lum, stim1_lum]) probe.setFillColor([probe_lum, probe_lum, probe_lum]) start_probe_lum = probe_lum # 開始前にstart_probe_lumに値を複製 in_series = True psychopy.event.getKeys() while in_series: stim1.draw() # ここで描画等を済ませておく stim2.draw() probe.draw() win.flip() keys = psychopy.event.waitKeys(keyList=['up','down']) if series in keys: # 刺激を変化させる場合 probe_lum += step probe.setFillColor([probe_lum, probe_lum, probe_lum]) elif keys != []: # 現在の系列を終了させる場合 datafile.write('{:.2f},{:.2f},{:.2f},{:.2f}\n'.format( stim1_lum, start_probe_lum, step, probe_lum)) in_series = False win.flip() #刺激を消去する psychopy.core.wait(1.0) #1.0秒待つ datafile.close() #データファイルを閉じる この方法では±0.85,±0.80,±0.75,±0.70が出現する頻度は均等にならない。総試行数が8の倍数ではないので当然である。 総試行数が変わってしまうが以下のように条件リストを作成すると、各誘導刺激と系列の組み合わせにつき±0.85,±0.80,±0.75,±0.70が均等に1回ずつ出現するようにすることが出来る。 .. code-block:: python 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節 マウスの利用 ============================= .. p.77 1) ウィンドウの単位とマウス ---------------------------------- 略 2) マウスカーソル初期位置の指定と表示のトグル ------------------------------------------------ 以下に例を示す。 スクリーンの左上の座標の計算がやや難しいポイントである。単位はheightなのだからY座標は0.5である。X座標はスクリーン高さを1.0とするのだから、スクリーン解像度の幅÷高さに-0.5を乗じた値である。この例では読者がどのような解像度のスクリーンで実行するかわからないので、Windowオブジェクトのデータ属性sizeから解像度を得るという方法をとった(16行目)。 マウスカーソルのON/OFFは、マウスカーソルの状態を維持する変数を用意するとよいだろう。スペースキーが押されたら変数の値をTrueからFalseへ、あるいはFalseからTrueへ変更しなければならないが、これはnot演算子を使うと場合分けせずに処理できる。34行目のようにgetKeys()で検出するキーをスペースキーだけに制限してしまえば、getKeys()の戻り値が[]ではないことを判定するだけでスペースキー押しを検出できる。 .. code-block:: python :linenos: :emphasize-lines: 16-17, 19-20, 34-36 #coding:utf-8 from __future__ import division from __future__ import unicode_literals import psychopy.visual import psychopy.event win = psychopy.visual.Window( monitor='defaultMonitor', units='height', fullscr=True) button = psychopy.visual.Rect(win, width=0.1, height=0.1) state = psychopy.visual.TextStim( win, pos=[0,-0.2], height=0.05) mouse = psychopy.event.Mouse() wh, ww = win.size mouse.setPos((0.5*(ww/wh), 0.5)) mouse_visible = True mouse.setVisible(mouse_visible) mouse.clickReset() while True: if mouse.isPressedIn(button,buttons=[0]): # 中でボタン0が押された break elif button.contains(mouse): # 中にマウスカーソルがある button.setFillColor([-1,1,-1]) else: # 中にマウスカーソルが無い button.setFillColor([-1,-1,-1]) state.setText('Pos: {}\nButtons: {}'.format( mouse.getPos(), mouse.getPressed(getTime=True))) if not psychopy.event.getKeys(keyList=['space']) == []: mouse_visible = not mouse_visible mouse.setVisible(mouse_visible) button.draw() state.draw() win.flip() 3) マウスカーソルに合わせて刺激を移動 ------------------------------------------- 以下に例を示す。27行目でgetPos()を実行してしまっているので30から31行目のsetText()ではgetPos()を呼ばずに先ほどのgetPos()の結果を利用している。 .. code-block:: python :linenos: :emphasize-lines: 11-12, 27-28, 30-31 #coding:utf-8 from __future__ import division from __future__ import unicode_literals import psychopy.visual import psychopy.event win = psychopy.visual.Window( monitor='defaultMonitor', units='height', fullscr=True) button = psychopy.visual.Rect(win, width=0.1, height=0.1) moving_rect = psychopy.visual.Rect( win, width=0.2, height=0.2, fillColor=None, lineColor='red') state = psychopy.visual.TextStim( win, pos=[0,-0.2], height=0.05) mouse = psychopy.event.Mouse() mouse.clickReset() while True: if mouse.isPressedIn(button,buttons=[0]): # 中でボタン0が押された break elif button.contains(mouse): # 中にマウスカーソルがある button.setFillColor([-1,1,-1]) else: # 中にマウスカーソルが無い button.setFillColor([-1,-1,-1]) pos = mouse.getPos() moving_rect.setPos(pos) state.setText('Pos: {}\nButtons: {}'.format( pos, mouse.getPressed(getTime=True))) button.draw() moving_rect.draw() state.draw() win.flip() 4) マウスのボタン押し回数のカウント -------------------------------------------------------------------- 以下に例を示す。prev_stateの更新はボタン押し反応の判定の終了後に行うことと、if文の結果に関わらず常に更新を行うことがポイントである。 .. code-block:: python :linenos: :emphasize-lines: 13, 21-23 #coding:utf-8 from __future__ import division from __future__ import unicode_literals import psychopy.visual import psychopy.event key_count = 0 press_count = 0 win = psychopy.visual.Window(monitor='defaultMonitor', fullscr=False) state = psychopy.visual.TextStim(win) mouse = psychopy.event.Mouse() prev_state = mouse.getPressed()[0] while True: keys = psychopy.event.getKeys(keyList=['space']) key_count += len(keys) buttons = mouse.getPressed() if buttons[0] == 1 and prev_state == 0: press_count += 1 prev_state = buttons[0] if buttons[2] == 1: break state.setText( 'Key: {}, Button:{}, {},{}'.format(key_count,press_count,buttons[0],prev_state)) state.draw() win.flip() 2.3.5節 様々な外部機器の利用 ============================= 1) ジョイスティックによる刺激位置の変更 ------------------------------------------- 以下に例を示す。ジョイスティックは物によりレバーを操作した時の戻り値が異なるので、どのようなジョイスティックでも同じ結果が得られるコードを書くのは難しい。この例では、レバーを操作するとgetAllAxes()の戻り値の対応する要素が±1.0になるデジタル式のレバーを持つものを想定している。このようなデジタル式のレバーでも、レバーを戻した後に値が完全に0に戻らない(0.01などの値になる)ものがあるので、変数thresholdに「レバーが操作されている」と判定する閾値を設定して、閾値を超えた時のみ変数stepだけ位置を変化させている。 .. code-block:: python :linenos: :emphasize-lines: 9, 21-24,31-42,44-45 #coding:utf-8 from __future__ import division from __future__ import unicode_literals import psychopy.visual import psychopy.core import psychopy.hardware.joystick win = psychopy.visual.Window(units='height') rect = psychopy.visual.Rect(win, width=0.1, height=0.1) text = psychopy.visual.TextStim(win, height=0.03) num_joys = psychopy.hardware.joystick.getNumJoysticks() if num_joys==0: # ジョイスティックが0個(存在しない)なら終了 text.setText('No joystick was found.') text.draw() win.flip() psychopy.core.wait(1.0) psychopy.core.exit() joystick = psychopy.hardware.joystick.Joystick(0) pos = [0, 0] threshold = 0.1 step = 0.02 while True: buttons = joystick.getAllButtons() if not False in buttons[:2]: break axes = joystick.getAllAxes() if axes[0] > 0 and axes[0] > threshold: pos[0] += step elif axes[0] < 0 and axes[0] < -threshold: pos[0] -= step if axes[1] > 0 and axes[1] > threshold: pos[1] += step elif axes[1] < 0 and axes[1] < -threshold: pos[1] -= step rect.setPos(pos) rect.draw() text.setText('Axes:{}\n\nButtons:{}\n\nHats:{}'.format( axes, buttons, joystick.getAllHats())) text.draw() win.flip() win.close() 32から39行目のif文はお世辞にも見やすいコードと言えないので、本文で解説されていない関数を使って整理してみよう。まず、絶対値を返すnumpy.abs()、符号を返すnumpy.sign()を用いると、elifを使用せずに記述することが出来る。 .. code-block:: python 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のインデックスが異なるだけなので、関数を使ってまとめることが出来る。 .. code-block:: python def getStep(axis): if np.abs(axis) > threshold: return step * np.sign(axis) この関数を以下のように使用する。 .. code-block:: python pos[0] += getStep(axis[0]) pos[1] += getStep(axis[1]) rect.setPos(pos) なお、=演算子を用いてパラメータを更新する方法を用いるならば、以下のように書くことも可能である。 .. code-block:: python rect.pos += [getStep(axis[0]), getStep(axis[1])] アナログジョイスティックを使用しているのであれば、thresholdを設定せずにgetAllAxes()の戻り値に適当な定数を乗じて使用すればいいだろう。 .. code-block:: python 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()の基本的な使い方が分かっていると思ってよい。 .. code-block:: python 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()を行う関数を用意する。 .. code-block:: python 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()による出力と比較できるようにしている。 .. code-block:: python :linenos: :emphasize-lines: 34-36,55 #coding:utf-8 from __future__ import division from __future__ import unicode_literals import psychopy.visual import psychopy.event import psychopy.core import codecs import random # 疑似乱数の発生や無作為な並び替えなどを行う conditions = [] # 空リストを用意しappend()で条件を加えていく for stim1_lum in [-0.3, 0.3]: for probe_lum_index in range(9): probe_lum = 0.05*(probe_lum_index-4) conditions.append([stim1_lum, probe_lum]) conditions *= 5 # リストの内容を5回繰り返す random.shuffle(conditions) # 無作為な順序に並び替える win = psychopy.visual.Window(fullscr=True, monitor='defaultMonitor', units='cm', color='black') clock = psychopy.core.Clock() stim1 = psychopy.visual.Rect( # fillColorは各試行開始時に指定する win, width=6.0, height=6.0, pos=[-4,0], lineColor=None) stim2 = psychopy.visual.Rect( # stim2のfillColorは固定 win, width=3.0, height=3.0, pos=[-4,0], lineColor=None, fillColor=[0.0, 0.0, 0.0]) probe = psychopy.visual.Rect( win, width=3.0, height=3.0, pos=[4,0], lineColor=None) datafile = codecs.open('data.csv','w','utf-8') datafile.write('Stim1,Probe,Response,RT\n') # ヘッダを出力しておく 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())) for condition in conditions: # conditionsから各試行の条件を取り出す # 刺激色を更新する stim1_lum = condition[0] probe_lum = condition[1] stim1.setFillColor([stim1_lum, stim1_lum, stim1_lum]) probe.setFillColor([probe_lum, probe_lum, probe_lum]) waiting_keypress = True psychopy.event.getKeys() clock.reset() while waiting_keypress: keys = psychopy.event.getKeys(timeStamped=clock) for key in keys: if key[0]=='left' or key[0]=='right': datafile.write( # 刺激のパラメータと反応を出力 '{:.1f},{:.1f},{},{:.3f}\n'.format( stim1_lum, probe_lum, key[0], key[1])) win.callOnFlip(write_on_flip, stim1_lum, probe_lum, key[0]) waiting_keypress = False break elif key[0]=='escape': # ESCキーが押された場合は datafile.close() # 直ちに終了する psychopy.core.exit() stim1.draw() stim2.draw() probe.draw() win.flip() win.flip() #刺激を消去する psychopy.core.wait(1.0) #1.0秒待つ datafile.close() #データファイルを閉じる 60fpsのモニターで実行した際の出力例を以下に示す。#write_on_flipと後ろについているのがcallOnFlip()によって出力された行である。 .. code-block:: none 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なので、ほぼフレーム間時間ほどずれていることが分かる。 .. code-block:: none 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) 実験パラメータをダイアログで指定できるようにする ----------------------------------------------------------- 以下に例を示す。参考のため、問題文では求められていないが、ダイアログに入力された値のチェックを行っている。 .. code-block:: python :linenos: :emphasize-lines: 7,11-38,46 #coding:utf-8 from __future__ import division from __future__ import unicode_literals import psychopy.visual import psychopy.event import psychopy.core import psychopy.gui import codecs import random # 疑似乱数の発生や無作為な並び替えなどを行う param_dict = { 'データファイル名':'data.csv', '繰り返し回数':10, } try: dlg = psychopy.gui.DlgFromDict(param_dict, title='パラメータ入力') except: print 'dialog has terminated abnormally.' psychopy.core.quit() if not dlg.OK: psychopy.core.quit() try: num_repetition = int(param_dict['繰り返し回数']) except: print 'number of repetition must be an integer.' psychopy.core.quit() if num_repetition <= 0: print 'number of repetition must be greater than 0.' psychopy.core.quit() try: datafile = codecs.open(param_dict['データファイル名'],'w','utf-8') except: print 'could not open {},'.format(param_dict['データファイル名']) psychopy.core.quit() conditions = [] # 空リストを用意しappend()で条件を加えていく for stim1_lum in [-0.3, 0.3]: for probe_lum_index in range(9): probe_lum = 0.05*(probe_lum_index-4) conditions.append([stim1_lum, probe_lum]) conditions *= num_repetition random.shuffle(conditions) # 無作為な順序に並び替える # 以下コード2.4と同一なので省略 この例では、繰り返し回数に小数が入力されていた場合にもそのまま実行されてしまうので(int()が例外を起こさないため)、本文中では紹介していない関数isinstance()を用いて以下のようにする方が正確である。 .. code-block:: python 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になることで判定することも出来るだろう。 .. code-block:: python 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がひとつもない)ことをループの終了条件とすればよい。 .. code-block:: python :linenos: #coding:utf-8 from __future__ import division from __future__ import unicode_literals import psychopy.visual import codecs win = psychopy.visual.Window( monitor='defaultMonitor', units='height', fullscr=True) scale1 = psychopy.visual.RatingScale( win, low=1, high=5, markerStart=3, pos=[0,0.6], size=0.5) scale2 = psychopy.visual.RatingScale(win, low=1, high=7, scale='1=好ましくない / 7=好ましい', textColor='black', size=0.5, lineColor='black', markerColor='red', pos=[0,0.2]) scale3 = psychopy.visual.RatingScale(win, choices=['赤','青','緑'], size=0.5, pos=[0,-0.2]) scale4 = psychopy.visual.RatingScale( win, precision=100, low=1, high=5, pos=[0,-0.6], size=0.5) while True: if not True in [scale1.noResponse, scale2.noResponse, scale3.noResponse, scale4.noResponse]: break scale1.draw() scale2.draw() scale3.draw() scale4.draw() win.flip() 2) 中間地点にマーカーを置けるようにする -------------------------------------------- 略 2.3.8節 ファイルからの画像の提示 ========================================= 1) 画像を1/2のサイズで描画する ----------------------------------- 以下に例を示す。ImageStimオブジェクトのサイズを一度明示的に指定すると、その後にsetImage()で画像を変更しても指定したサイズが継承される。そこでこの例では、setImage()の後で本文中で述べた隠し属性_origSizeを用いてpix単位の画像の大きさを求め、heightに換算している。本来ならば、このような手法に頼るのではなく刺激画像作成の時点で大きさを統一しておくことが望ましい。 .. code-block:: python :linenos: :emphasize-lines: 11-12,19-21 #coding:utf-8 from __future__ import division from __future__ import unicode_literals import psychopy.visual import psychopy.event win = psychopy.visual.Window( monitor='defaultMonitor', units='height', fullscr=True) stim = psychopy.visual.ImageStim(win, '01.jpg') stim.setAutoDraw(True) w, h = stim.size stim.setSize((w/2, -h/2)) win.flip() # autoDraw=Trueを指定すると自動的にdraw()される psychopy.event.waitKeys() stim.setImage('images/02.jpg') # 画像を変更 w = stim._origSize[0]/win.size[1] h = stim._origSize[1]/win.size[1] stim.setSize((w/2, -h/2)) win.flip() psychopy.event.waitKeys() win.close() 2) 画像の周辺をぼかす --------------------------------------------- 引数mask='gauss'を追加するだけである。問題文では要求されていないが、2枚目の画像で周辺をぼかさないようにする例も示してる。 .. code-block:: python :linenos: :emphasize-lines: 9,16 #coding:utf-8 from __future__ import division from __future__ import unicode_literals import psychopy.visual import psychopy.event win = psychopy.visual.Window( monitor='defaultMonitor', units='height', fullscr=True) stim = psychopy.visual.ImageStim(win, '01.jpg', mask='gauss') stim.setAutoDraw(True) win.flip() # autoDraw=Trueを指定すると自動的にdraw()される psychopy.event.waitKeys() stim.setImage('images/02.jpg') # 画像を変更 stim.setMask(None) # マスクを消去 win.flip() psychopy.event.waitKeys() win.close() 2.3.9節 音声と動画の提示及び音声の録音 ================================================== 1) 動画を10秒送り/10秒戻しする ------------------------------------------------------ 以下に例を示す。筆者の動作環境(Win10 X64, PsychoPy 1.83.04)ではMovieStim3のシーク動作が不安定だったので、MovieStim2を使用している。 .. code-block:: python :linenos: :emphasize-lines: 7,23-28 #coding:utf-8 from __future__ import division from __future__ import unicode_literals import psychopy.visual win = psychopy.visual.Window(units='height') movie = psychopy.visual.MovieStim2(win, 'movie.mp4') text = psychopy.visual.TextStim(win, height=0.03) info_str = 'Size:{}, Duration:{}, FPS:{:.3f}\n'.format( movie.size, movie.duration, movie.getFPS()) while movie.status != psychopy.visual.FINISHED: text.setText(info_str+'{}'.format( movie.getCurrentFrameTime())) keys = psychopy.event.getKeys() if 'space' in keys: if movie.status == psychopy.visual.PAUSED: movie.play() else: movie.pause() elif 'left' in keys: t = movie.getCurrentFrameTime() movie.seek(max(0,t-10)) elif 'right' in keys: t = movie.getCurrentFrameTime() movie.seek(min(movie.duration,t+10)) movie.draw() text.draw() win.flip() 2) データ属性statusを使わずに再生を終了する --------------------------------------------------- 以下に例を示す。 .. code-block:: python :linenos: #coding:utf-8 from __future__ import division from __future__ import unicode_literals import psychopy.sound import psychopy.core stim = psychopy.sound.Sound(440, secs=1.0) duration = stim.getDuration() clock = psychopy.core.Clock() stim.play() while clock.getTime() <= duration: psychopy.core.wait(0.01) 2.4.1節 PCによる刺激提示および反応時間計測の仕組み ========================================================= 1) flip()の所要時間の確認 ------------------------------- 略 2) ログの出力 ------------------------------- 略 3) 刺激が変化したフレームのみログを出力(logOnFlip()) ---------------------------------------------------------- 以下に例を示す。 .. code-block:: python :linenos: :emphasize-lines: 7,11-12,44,72 #coding:utf-8 from __future__ import division from __future__ import unicode_literals import psychopy.visual import psychopy.event import psychopy.core import psychopy.logging import codecs import random # 疑似乱数の発生や無作為な並び替えなどを行う log_file = psychopy.logging.LogFile( 'test.log', level=psychopy.logging.DEBUG) conditions = [] # 空リストを用意しappend()で条件を加えていく for stim1_lum in [-0.3, 0.3]: for probe_lum_index in range(9): probe_lum = 0.05*(probe_lum_index-4) conditions.append([stim1_lum, probe_lum]) conditions *= 5 # リストの内容を5回繰り返す random.shuffle(conditions) # 無作為な順序に並び替える win = psychopy.visual.Window(fullscr=True, monitor='defaultMonitor', units='cm', color='black') clock = psychopy.core.Clock() stim1 = psychopy.visual.Rect( # fillColorは各試行開始時に指定する win, width=6.0, height=6.0, pos=[-4,0], lineColor=None) stim2 = psychopy.visual.Rect( # stim2のfillColorは固定 win, width=3.0, height=3.0, pos=[-4,0], lineColor=None, fillColor=[0.0, 0.0, 0.0]) probe = psychopy.visual.Rect( win, width=3.0, height=3.0, pos=[4,0], lineColor=None) datafile = codecs.open('data.csv','w','utf-8') datafile.write('Stim1,Probe,Response,RT\n') # ヘッダを出力しておく for condition in conditions: # conditionsから各試行の条件を取り出す # 刺激色を更新する stim1_lum = condition[0] probe_lum = condition[1] stim1.setFillColor([stim1_lum, stim1_lum, stim1_lum]) probe.setFillColor([probe_lum, probe_lum, probe_lum]) win.logOnFlip('Stimulus onset', level=psychopy.logging.EXP) waiting_keypress = True psychopy.event.getKeys() clock.reset() while waiting_keypress: keys = psychopy.event.getKeys(timeStamped=clock) for key in keys: if key[0]=='left' or key[0]=='right': datafile.write( # 刺激のパラメータと反応を出力 '{:.1f},{:.1f},{},{:.3f}\n'.format( stim1_lum, probe_lum, key[0], key[1])) datafile.flush() # 直ちにファイルに書き出す waiting_keypress = False break elif key[0]=='escape': # ESCキーが押された場合は datafile.close() # 直ちに終了する psychopy.core.quit() stim1.draw() stim2.draw() probe.draw() win.flip() win.flip() #刺激を消去する psychopy.core.wait(1.0) #1.0秒待つ datafile.close() #データファイルを閉じる psychopy.logging.flush() 2.4.2節 ioHubパッケージ ============================= 1) ioHubを用いた反応の記録 ----------------------------------- 以下に例を示す。 .. code-block:: python :linenos: :emphasize-lines: 5,10-11,47-59,71 #coding:utf-8 from __future__ import division from __future__ import unicode_literals import psychopy.visual import psychopy.iohub import psychopy.core import codecs import random # 疑似乱数の発生や無作為な並び替えなどを行う io = psychopy.iohub.launchHubServer() io.clearEvents('all') conditions = [] # 空リストを用意しappend()で条件を加えていく for stim1_lum in [-0.3, 0.3]: for probe_lum_index in range(9): probe_lum = 0.05*(probe_lum_index-4) conditions.append([stim1_lum, probe_lum]) conditions *= 5 # リストの内容を5回繰り返す random.shuffle(conditions) # 無作為な順序に並び替える win = psychopy.visual.Window(fullscr=True, monitor='defaultMonitor', units='cm', color='black') clock = psychopy.core.Clock() stim1 = psychopy.visual.Rect( # fillColorは各試行開始時に指定する win, width=6.0, height=6.0, pos=[-4,0], lineColor=None) stim2 = psychopy.visual.Rect( # stim2のfillColorは固定 win, width=3.0, height=3.0, pos=[-4,0], lineColor=None, fillColor=[0.0, 0.0, 0.0]) probe = psychopy.visual.Rect( win, width=3.0, height=3.0, pos=[4,0], lineColor=None) datafile = codecs.open('data.csv','w','utf-8') datafile.write('Stim1,Probe,Response,RT\n') # ヘッダを出力しておく for condition in conditions: # conditionsから各試行の条件を取り出す # 刺激色を更新する stim1_lum = condition[0] probe_lum = condition[1] stim1.setFillColor([stim1_lum, stim1_lum, stim1_lum]) probe.setFillColor([probe_lum, probe_lum, probe_lum]) waiting_keypress = True io.devices.keyboard.getKeys() clock.reset() while waiting_keypress: events = io.devices.keyboard.getKeys() for event in events: if event.type == 'KEYBOARD_PRESS': if event.key=='left' or event.key=='right': datafile.write( # 刺激のパラメータと反応を出力 '{:.1f},{:.1f},{},{:.3f}\n'.format( stim1_lum, probe_lum, event.key, clock.getTime())) datafile.flush() # 直ちにファイルに書き出す waiting_keypress = False break elif event.key=='escape': # ESCキーが押された場合は datafile.close() # 直ちに終了する psychopy.core.quit() stim1.draw() stim2.draw() probe.draw() win.flip() win.flip() #刺激を消去する psychopy.core.wait(1.0) #1.0秒待つ datafile.close() #データファイルを閉じる io.quit()