この記事はTouchDesigner Advent Calendar 2018 –14日目の記事です。
2020年版はこちらをご覧ください。
https://qiita.com/shks/items/4ff22921ccbf1062bc1b
私はいわゆるインタラクティブシステムなどを作りつつ、それを用いた心理物理実験などを行う事があるのですが、前者をTouchDesignerで組むようになって、これって心理物理実験のシステム構築にもかなり使えるのでは無いか。と思い、最近使うようになりました。
この記事では、ゴリガリの3Dグラフィックやアニメーション等は扱いません。TouchDesignerで心理物理実験システムを組むと結構良かった。という、果たして対象読者はいるのであろうかという記事です。TouchDesignerを学習する上で、多くのQiita記事や@satoruhiagさんの映像音響処理概論 2018 講義資料、に助けられてきたので、今後何かの拍子にTouchDesignerで実験システムを組む人が現れた時に参考になれば幸いです。
今回のサンプルはこちら
https://github.com/shks/PsychoTDsample
心理物理実験とは
心理物理実験が行われる、心理物理学(精神物理学)は、人間に対する外的な刺激と人間の内的な感覚や知覚の対応関係を測定し、定量的な計測をしようとする学問とされています。詳しい定義や分類は別の文献や記事を見てください。
多くは人間を対象とした実験で、反応速度や弁別実験など単純な実験を様々な条件で沢山行うことで、人間が持つ知覚や認知の特性、感覚と物理量との関係性などを検証します。もし、心理実験等に興味がある方は基礎心理学実験法ハンドブックなどをご参照ください。この本から、ハンドブックというのは片手で持てる本という意味では無いのだという事が身をもって理解できます。
ストループ効果を題材に
今回はストループ効果を題材にした実験のためのシステムを組んで見ます。ストループ効果とは、文字の意味と文字色のように同時に目にするふたつの情報が干渉しあう現象です。(厳密にはストループ効果は心理物理ではなく、認知心理実験となりますが、題材が分かりやすいためこの効果を取り上げました。)
例として、「赤・青・黄」の文字がそれぞれの文字の意味と一致した色で表示されている場合には問題なくすばやく(音読)読めます。一方で、「赤・青・黄」が文字の意味と、一致しない色で表示されている場合には脳内で情報が干渉して、素早く音読することができなくなります。こちらに、体験できるデモがあります。
このストループ効果を題材にして、文字と文字色が一致している場合と、不一致である場合の文字選択の反応速度はどのにように変わるか、ということを検証するために、心理物理実験のシステムをTouchDesignerを用いて構築してみたいと思います。
実験内容
実験では、スクリーンに表示された左右に表示した文字(RED /BLUE)から、指定された文字の方がどちら側にあるかを”できるだけ早く”回答する、というタスクを想定し、その反応時間を測定します。
そのタスクにおいて、文字と文字色が一致している場合 (congruent 条件)と、不一致である場合(incongruent 条件)において、その反応時間を測定します。被験者の回答は、キーボード入力で計測することにします。
実験タスクでは
- 文字と色は一致しているか? [congruent / incongruent ]条件
の他に
- RED、BLUEどちらの文字を選択させるか [RED / BLUE]
- 文字の配置 [RED は右にあるか左にあるか]
変化させて十分な回数テストを行い、反応時間を記録してその傾向を分析することになると思います。(本来は、正答率なども記録しますが今回は割愛します)
それでは、今回は下記のような実験シーケンス想定します。
実験システムをTouchDesignerで構築する。
タイマー使い実験シーケンスを作る
上記のような実験シーケンスを永遠と回し続けるシステムの構築を、TimerCHOPを用いて作ります。TimerCHOP
には、segment
という複数の小分けのタイマーのようなものを定義できるようなので、それを利用します。
まずは、実験シーケンスをTableDAT
にタイムテーブルとして記述します。ここでは、簡単に[ pre, instruction, fixation cross, visualStim, end ]の5つのシーンを想定します。それぞれのシーンの開始時間とシーンの長さをそれぞれ begin
、length
として上記のように指定します。なお、section
列にはわかりやすいようにそれぞれのシーンの名前を入れましたが、ここは"section"でなくても動作しました。
新しくtimer1(TimerCHOP)
を配置して、segment tab
のSegment DAT
から、上記で作成したテーブルtimer_table(TableDAT)
を指定します。
timer1
のOutput
タブで、Segment
とRunning Time Count
をSecond
に指定。これで、上記の「シーン」と呼んでしたものが segment
に対応し、いまどこのsegment
かが出力されるようになります。
また、running_seconds
として、シーケンス開始からの経過時間が取得できるようになります。
実験シーケンスをループさせる
しかし、このまま、TimerCHOP
をStartさせても1回シーケンスが再生されて終わってしまうので、ループするようにします。
timer1のOn Done
をRe-start
に指定したうえで、timer1のTimerタブで、Init
, Start
をクリックすることで、指定したシーケンスが永遠とループし続ける事が確認できると思います。
実験シーケンスに応じた挙動を作る
このtimer1
の後にselectCHOPを接続して、segment
を指定すると現在どこのsegment
(シーン)にいるのかが取り出せます。これを後ほど、画面切り替えに利用します。
TimerCHOP
の右下にある下矢印のボタンをクリックすることで、callbackのスクリプトを記述することができます。例えば、「segment 4に入ったら観測データを記録して、ファイルに保存する」「あるセグメントに入ったら、外部装置にシリアルを送る、OSCメッセージを送信する」等、シーケンスの進行に応じた挙動をスクリプトで記述できます。
def onSegmentEnter(timerOp, segment, interrupt):
print('TimerCHOP Entering segment', segment)
return
#実験条件進行の設計
心理物理実験では、上記で組んだシーケンスを実験条件を変えて繰り返し行う場合が多いです。そして、その実験条件は様々な方法で順序が決められますが今回はランダムと想定して、実験条件の進行を設計します。
実験条件の進行も同じくTableDATで管理することにします。
今回の実験では
- RED、BLUEどちらの文字を選択させるか [RED / BLUE]
- 文字と色は一致しているか? [congruent / incongruent ]
- 文字の配置 [RED は右にあるか左にあるか]
のそれぞれ2x2x2 = 8つ条件が考えれます。
例えばこれらをそれぞれ10回づつ行い、かつそれら条件の進行はランダムに出現するようにシーケンスを組みたいと思います。
実験条件進行のテーブルを作成する
ここで、TableDATの中身を生成するpythonスクリプトを作ります。
入れ物のTableDATは trialTable
、スクリプトはTextDATで generateConditionSequence
にします。
import random
def initSequenceList():
#ここで、TableDATを取得
trialTable = op('trialTable')
trialTable.clear()
trialTable.appendRow(['index', 'inst_color', 'congruency', 'location','reaction time'])
#ランダムシーケンスを作るための一時的配列
trialseq = []
#'inst_color', 'congruency', 'location'
for i in range(10):
trialseq.append([ 'RED', 'congruent', 'redleft'])
trialseq.append([ 'RED', 'congruent', 'redright'])
trialseq.append([ 'RED', 'incongruent', 'redleft'])
trialseq.append([ 'RED', 'incongruent', 'redright'])
trialseq.append([ 'BLUE', 'congruent', 'redleft'])
trialseq.append([ 'BLUE', 'congruent', 'redright'])
trialseq.append([ 'BLUE', 'incongruent', 'redleft'])
trialseq.append([ 'BLUE', 'incongruent', 'redright'])
#ランダム
random.shuffle(trialseq)
index = 1
for seq in trialseq:
newRow = [index, seq[0], seq[1], seq[2], -1]
trialTable.appendRow(newRow)
index += 1
initSequenceList()
この後にTextDATをRunScript
するとTableDATの中身は更新されます。
ちなみに、TableDAT
の列に記録するデータ列も作っておくと後々、進行状況を確認したり、解析の際に役に立つので、Reaction Time
の列も追加してあります。今のところ、データはないので、-1
にしておきます。
実験条件の進行を制御する
次に、この実験条件進行の何番目を実験しているのかを制御するために、TimerCHOP
とTableDAT
の関係をつなげていきます。
(よりこなれた方法もありそうですが、今のところ使っている方法です)
実験進行の順番を表す、ConstantCHOP
を用意します。名前はINDEX
にしています。
そして、timer1
のcallback
スクリプトで onDone
のタイミングで、INDEX
が保持する値を1
増やします。これによって、実験シーケンスが1つおわると、実験進行のインデックスが1つふえる、という挙動になります。
def onDone(timerOp, segment, interrupt):
op('INDEX').par.value0 += 1
return
実験条件進行に応じてパラメータを取り出す
次に、実験進行のインデックス (INDEX
) を指定することで、その実験進行で用いる実験条件を読み出す部分を先述のTableDAT
を使って作ります。
先述のtrialTable(TableDAT)
から、SelectDAT
を新しく作って接続して、trialTable
から現在のINDEX
に対応するデータ列を取り出します。SelectDAT
は図のように、Select Rows
をby Index
にして、Start Row Index
, End Row Index
ともにop(‘INDEX’)[0]
を指定します。
また、Include First Row
を指定して、後で可読性を上げるために残してあります。
直接このSelectDAT
を参照する方法もありますが、読みやすいように3つの条件
- 文字と色は一致しているか? [congruent / incongruent]を
congruent_dat
- 文字の配置 [RED は右にあるか左にあるか]を
redLeft_dat
- RED、BLUEどちらの文字を選択させるか [RED / BLUE]を
targetText_dat
にぞれぞれSelectDAT
で取り出します。
さらに、[congruent / incongruent]、[RED は右にあるか左にあるか]は視覚刺激生成に後ほど使うので、この2つの条件は [0 or 1]に変換しておきます。
congruent_dat(文字と色は一致しているか?)は、一致していれば1
に一致していなければ 0
とします。この条件式はpythonスクリプトとして、ConstCHOPのvalue0に記述できるので、
1 if(op('congruent_dat')[0,0] == 'congruent') else 0
とします。
同じく、文字の配置 [RED は右にあるか左にあるか]をredLeft_dat
を、ConstCHOP
のvalue0
から参照して、下記にようにします。
1 if(op('redLeft_dat')[0,0] == 'redleft') else 0
ここまでで、永遠とシーケンスのループが回り、実験進行のインデックスが1づつ増え、その際の実験条件に関するデータが取得できました。
画面シーンの生成と切り替え
今回の実験シーケンスは5つのシーン(Segment
)から構成されます。ここでは、それぞれの画面映像を作るのにいくつかのTOPと、それらを切り替えるのに、SwitchTOP
を用います。
SwitchTOP
には、実験シーケンスを構成する5つそれぞれのシーンの映像が入力されています。これらのどれを選択して、出力するかを現在のsegment
の値で指定します。
TimerCHOP
からSelectCHOP
で取り出したsegment
の数値を、timer_segment
と命名したNullCHOP
へ入力し、5つのTOPが入力されているSwitchTOP
のIndex
の値を、下記のように、timer_segment
の値で指定することで、画面遷移を簡単に作ることができます。
TableDATのセル文字列を使って画面表示を作る
実験条件に合わせた、各シーンの表示作成
ここでは単純にTextTOPを使用します。
'CHOOSE ' + op('target_text')[0,0] + ' TEXT'
これで、RED文字を選択するか、BLUE文字を選択するのかの実験条件を反映した映像が作れます。
たとえば、BLUEが選択対象の場合でも反映されています。
実験条件を入力として、画面を出力する COMPを作る
今回の実験シーケンスで調査対象となるシーンを作ります。ここは割愛しますが、例えば文字色と文字の一致isCongruent
と RED文字が左右のどちらになるのかisRedLeft
を指定して、その画面をTOPとして出力するようなパッチをまとめてCOMPにするとスッキリするかと思います。
まとめたCOMP
の中身は複雑そうに見えますが、単純にisCongruent
とisRedLeft
の全4パターンをSwitch
で切り替えているだけです。(この辺もスクリプトを駆使すればもう少しスッキリ書けるとは思いますが、作成速さを優先して。)
反応時間の表示シーン
おそらく、普通の心理物理実験では一般的ではないかもしれませんが、ここではゲーム性も兼ねて反応時間の表示を行います。
まだ、反応時間を測る部分は作っていないので、仮にConstantCHOP
とNullCHOP
でダミーで組んでおきます。NullCHOP
をreaction_time
を命名し、TextTOP
からその数値を参照して、テキストとして表示します。
ここまでで、実験シーケンスの基本的な画面遷移が作れました。この後に、まだ終わっていない昨日を作って行きます。
反応時間の測定
この実験では、指定された文字(BLUE / RED)を上記のような視覚刺激が表示されてどのくらいの速さで選択できるかと計測します。ここで、選択行動は、キーボードで取得することにします。
つまり、この視覚刺激が表示されてから、キーボードが押されるまでの時間(反応時間)を計測します。
反応時間を測るためには
- 視覚刺激が表示された時間
- キーボードが押された時間
の二つを計測し、差分を出すことで経過時間を測定できます。
時間計測における注意
ここでTouchDesignerでよく使われるabsTime.seconds
や、上記のTimer
から出力されるrunning_seconds
を当初使おうとしていたのですが、気になる点があったので、現在は pythonスクリプト上でtime.time()
を呼んで経過時間を測定しています。
absTime.seconds
は、 TouchDesignerが起動してからの経過時間を取得できるものです。TouchDesigner上ですでに用意されており、TouchDesignerのコンポートや pythonコード上から読み出して使えます。しかし、このデータの更新頻度が TouchDesigner上の フレームレートに依存しているようので、 フレームレートとは関係なく時間を計測したい場合には注意が必要です。
こちらが、テストした場合の様子です。
Keyboard In.CHOP
で’L’のキーを押してから離すまでの経過時間を計測するようにしています。
比較は、 pythonの timeモジュールのtime.time()
関数、time.perf_counter()
関数そして、absTime.seconds
の3つを比較しています。画面右に出力しているのが、経過時間の表示です。
print('KEY CHOP -> chopexec - OnToOff duration time.time ', 1000 * duration_time)
print('KEY CHOP -> chopexec - OnToOff duration time.perf_counter', 1000 * duration_perf_counter)
print('KEY CHOP -> chopexec - OnToOff duration absTime ', 1000 * duration_absTime_on)
ここで、 FPS = 60で計測している場合には、それぞれ 2-3msecレベルの誤差なのですが、 FPS=20にした場合、absTime.secondsを用いた計測結果のみ他と大きく異なることがわかります。また、計測値も 50msecの倍数で表示されていることから、 20Fps は 1000msec / 20 = 50msecの時間分解能でしか経過時間を測定できていないことがわかります。なお、これと同じ現象はTimerCHOP
のrunning_time
を用いた計測をテストしても同様の結果が発生します。
どうやら、KeyboardIn.CHOP
のイベントはTouchDesignerのフレームレートとは関係なく発生し、pythonスクリプトのそのタイミングに準じて実行されますが、absTime.seconds
やTimerCHOP
の更新は、画面更新速度に依存するようです。
なので、例えばフレームレートが著しく低下するような実験状況でこれら、absTime.seconds
やTimerCHOP
の経過時間を用いて 画面更新以外のタイミングで発生する時間を計測する場合には注意が必要になります。
対応としては下記の選択肢があります。
- pythonの
time.time()
関数を用いて時間計測を行う。 - TouchDesignerのフレームレートの低下が起きないように注意する
フレームレートに依存しない時間計測
ここでは、1.pythonのtime.time()
関数を用いて時間計測を行う。で作ります。まず TextDatにpythonスクリプト、 計測出力用にConstantCHOP
を用意します。
TextDat
には、下記のようなスクリプトを書いて、mypyTimer
という名前にします。本来は色々な例外処理等を書くべきですが、とりあえずシンプルに書いておきます。startTimer関数では、開始時間を保持しておいて、measureDuration()
が呼ばれた時に、現時刻との差分を経過時間として、timer_duration
の値として反映させています。
import time
t_startTime = 0
def startTimer():
global t_startTime
t_startTime = time.time()
def measureDuration():
global t_startTime
duration = time.time() - t_startTime
op('timer_duration').par.value0 = duration * 1000
他のTextDAT
などのPythonスクリプトから、
import mypyTimer
とすれば、mypyTimer
にある関数を呼ぶことができます。
視覚刺激が表示されたらタイマーを開始
まず時間計測の開始のタイミングを先に作ってある、実験シーケンスを制御する timer1
のコールバックで、シーン segment
が3になった時に、mypyTimer
の計測を開始します。
import mypyTimer
def onSegmentEnter(timerOp, segment, interrupt):
if(segment == 3):
mypyTimer.startTimer()
return
キーボード入力で経過時間を測定
次にキーボード入力された時、経過時間を取得します。
keyboard InCHOP
をNullCHOP(null3)
につなげて、keyboardのLが押された場合の値の変化をnull3
に伝えます(null3の3は特に意味はありません) 。値を変化を検出した場合にコールバックを実行するCHOP Execute (chooexec3)
で、null3
を指定してかつ、CHOP Execute
のOff to On
をアクティブにします 。これで、null3
が0から1になった時に、chooexec3
に書いた次のスクリプトで、先ほどのmypyTimer
のmeasureDuration
関数を実行します。
すると、経過時間が、Constant CHOP
のtimer_duration
に反映されます。
import mypyTimer
def onOffToOn(channel, sampleIndex, val, prev):
mypyTimer.measureDuration()
return
こうして得られた経過時間が反映された、Constant CHOP
をそのまますでに作成してあった、reaction_time(NullCHOP)
へ接続することで、画面内容への反映も完了します。
計測データの保存
ここまでで、指定した実験条件進行の Index
で指定された条件で行なった実験シーケンスでの反応時間を得ることができました。
最後に、この結果を実験条件進行のテーブルである trialTable(TableDAT)
にデータとして書き込んで、途中状態を保存します。
計測データの反映・保存のタイミングをどこにするかは、用途にもよりますが、ここでは、その実験シーケンスが終わったら、つまり、timer1
のonDone
のコールバックで行うことにします。
def onDone(timerOp, segment, interrupt):
#現在の実験インデックス
currentIndex = op('INDEX')[0]
if(currentIndex > 0):
#timer_durationの値を、trialTableテーブルのcurrentIndex行、 4列目に入れる。
op('trialTable')[int(currentIndex) , 4] = op('timer_duration')[0]
#タブ区切り形式でデータを保存する。 #上書きに注意
op('trialTable').save('data.tsv')
#INDEXを一つ前に進める
op('INDEX').par.value0 += 1
return
すでに、反応時間の計測結果を格納している、timer_duration (ConstantCHOP)
の値を、 trialTable
(TableDAT)の現在のIndexの位置に合わせて反映して、その後 tsvファイル形式で保存します。
ちなみに、このままだと同じファイル名で実験シーケンスごとにファイルを上書きします。悲しいことが起きる前に、本番はファイル名を変えたり、バックアップなどを取るような運用にしましょう。 大切なものは失ってから気付く事が多いです。
実験シーケンスの初期化
ここまでで、一通りの実験システムが構築できたと思います。最後に実験シーケンスの初期化に関して記載します。しかし、実験条件進行のIndex
は単純増加するだけです。また、実験条件の進行もランダム化されているとは言え、固定化されたまま。trial_table
のデータも初期化しなければなりません。そこで、一発初期化・実験シーケンス開始ボタンをつくります。
panelexec1のスクリプトで、INDEXと実験条件進行のテーブルの再生成を行う。
def onOffToOn(panelValue):
#INDEX を初期値の0にする
op('INDEX').par.value0 = 0
#実験条件進行のテーブルを再生成する
op('generateConditionSequence').run()
return
また、スクリプトを組まずに、Timer
のスタートも制御できます。たとえば、ボタンからNullCHOP
へ接続し、そのNullCHOP
の値を、timer1
のActive
とStart
から参照することで、ボタンを押すとアクティブ化して、かつスタートする。という挙動を作る事ができます。
ここまでで、一通りの実験を執り行えるネットワークを作る事ができました。もちろん、細かい機能や割愛した部分もありますが、なれるとパパッと作成してパイロット試験をしてみることができると思います。
TouchDesignerで組むと嬉しいところ
実験オペレーター用ウインドウを作成する。
ここがTouchDesignerを使っていて、大変ありがたいと思うところです。 TouchDesignerではそれぞれのノード(ブロック)は、OPViewer.COMP
を用いて参照することができます。かつ、それらをさらにまとめて一つのContainer.COMP
としてレイアウトするとができます。つまり、デバックやモニタリング画面を秒速で作成することができます。
例えばtrial_table
でシーケンスの情報を見たい場合は、OPViewer.COMP
で参照先をtrial_table
に指定するとViewer
として見えるようになります。
同じように、実験条件の進行デーブルや、現在のDisplay提示の映像も、同じように対応するコンポーネントの名前を指定するだけでそれぞれを観測するViewerが作れます。
さらに個別に用意したCOMPをまとめるため、空のContainer.COMP
を用意して、そこに下記のように上下で参照を繋ぐとつなぎ元のOPViewer
の内容をまとめて一つのContainer.COMP
で見る事ができます。それぞれの配置は、元のOPViewer
のLayoutタブのX,Y,Width,Heightを変更することで変更できます。まとめたContainerCOMP
を右クリック→Viewで、個別のウィンドウとして開けます。ボタンのクリックなどはこのウィンドウから行う事ができるので、実験のオペレーションはこのウィンドウのみでできるようになります。
実験結果の即時可視化
例えば、DatToCHOP
やTrailCHOP
などを使用すると、実験での観測データや値の時系列の変化をリアルタイムで観察する事ができます。
これは、別の実験を行なった時の観測データとその可視化です。左のTableDAT
を右のDatToCHOP
で可視化しています。このように、データのリアルタイムな可視化を割と簡単に組む事ができるので、実験を行いながらも、その計測したデータの傾向と被験者の様子を一緒に観測する事ができます。実行しながら、気になったデータ列の可視化を実験を止めることなく行う事すら可能です。
おしまい
心理物理実験のためのフレームワークには pythonでのPsychoPyやMatlabのPsychtoolboxなどがありますので、業界の標準?に準拠するべきシチュエーションではそちらを使用した方が良いかもしれません。しかし、 TouchDesignerの映像やシステムを実行しながら改変したり、実験データのリアルタイムでの可視化、観察ができる利点も大いにあるのでは無いかと考えます。
瞬間的に生まれた事象を逃さず観測し、熱いうちに実装し検証する
というのは、様々なことに通じる重要な姿勢と思いますし、 TouchDesignerは結構それに向いるなと思ったのでした。
それでは、Happy Designing!