見出し画像

TouchDesignerで心理学実験システム.004:反応を取得する

前回の記事の続きです。TouchDesignerで心理学実験・認知実験などを実装する方法をストループ課題を題材に説明していきます。

前回は条件を変えてタスクを繰り返し実行出来るようにしました。今回は実験に対する反応をキーボードで取得し、結果表示を反映する部分を作ります。

画像11

ここまでで、基本的な画面遷移・実験条件に対応した視覚刺激を作ってきました。改めて、実験パラダイムを確認すると、実験参加者の回答として、「文字の色が赤なら[1] , 色が緑なら[2] , 色が青なら[3] 」としています。

画像2

第一回の記事にもあるように、文字の意味と文字色のように同時に目にするふたつの情報が干渉するストループ干渉が起きている場合(例として右の「赤・青・黄)の場合には、その刺激に対する反応時間が遅くなったり、回答正解率が下がることが予想されます。

これらの傾向を実験参加者の反応時間、およびキーボード回答から計測していくことになります。

キーボードで反応の取得

・キーボード回答:[1]、[2]、[3] キーボード押下(それぞれ、RED, GREEN, BLUE回答)
・ 視覚刺激表示から、キー押下までの時間(※1)

ここでも、まずは空のbaseコンポーネントを作って、その中で作っていきます。キー押下データの取得は、keyboard in CHOPkeyboard in DATの両方で取得することができます。(細かい話になりますが、DATのcallback関数実行のほうが、chopデータの変化をchop excute DATで検出したうえでのonValueChange関数よりも早いので、ここではkeyboard in DATを使います。)

keyboard in DATKeysに「1, 2, 3」と入力することで、[1]、[2]、[3]キーイベントのみにトリガーされるようにします。これで、[1]、[2]、[3]キーのいずれかが押された時 keyboardin1_callbacksonKeyが実行されます。

画像3

さらに、Maximum Linesを2にすることで、もっとの最近のキーイベントのみをDATの出力にします。

画像4

まずは、「1, 2, 3」の回答を「RED, GREEN, BLUE」に対応させて出力させます。前の章でもやっていた、select DATを使用して必要なセルを抽出する方法と、テーブルを作っておいて対応する値を出力する方法を用います。

画像5

この「RED or GREEN or BLUE」とユーザー回答として出力したいのですが、このままだと回答しなかった場合も、以前のキー入力が残ってしまいます。なので、別のtableDATを用意して、タスク開始時に無回答としての’NO_INPUT‘を入れておいて、対応するキーイベントが来たら、値を更新するようにします。このtableDATを1つ目の出力としてoutDATにつなぎます。この table_answer (tableDAT)の値を、タスク開始時 / キーボード入力時に更新すればよいことになります。

画像6

タスク開始時の初期化

タスク開始時のトリガーとして、in CHOPを用意して、そこが0から1になったら、値を初期化することにします。なので、in1(in CHOP)chop Excure DATをつけて、パラメータでoff to onをONにします。図でbutton1を接続しているのは挙動チェックのためです。

画像7

ここのin1(in CHOP)に上の階層にあった、Startボタンを接続します。これで、タスク開始時にin1(in CHOP)に0->1になるトリガーが入力されます。

画像10

その上で、chopexec1内のコードのonOffToOnを下記のようにします。これで、タスク開始時は、まだ入力がない状態として初期化できます。

def onOffToOn(channel, sampleIndex, val, prev):
   op('table_answer')[0,0] = 'NO_INPUT'
   return

先程作っていた「RED or GREEN or BLUE」回答が更新されるselect3の値で、キーイベントがあったら、table_answerの中身を更新するので、keyboardin1_callbacks内のonKeyの中に、下記のコードを追加します。なお、ここではキーボードが押された瞬間でトリガーするので、キーボードが押された状態(state = 1)のときのみ反応するようにします。

def onKey(dat, key, character, alt, lAlt, rAlt, ctrl, lCtrl, rCtrl, shift, lShift, rShift, state, time, cmd, lCmd, rCmd):
   if( state == 1):
     op('table_answer')[0,0] = op('select3')[0,0]
   return

これで、タスク開始時には値が初期化され、ユーザー回答があったら値が更新され、その回答結果がtableDATとして出力されます。

反応時間計測

次に同じkeyboardin1_callbacksonKey関数実行をトリガーにして、ユーザーの反応時間を計測します。ここでは、視覚刺激に対する反応時間を計測したいので、

- 視覚刺激が表示された時間
- キーボードが押された時間

の二つを計測し、差分を出すことで経過時間を測定します。


ここでTouchDesignerでよく使われるabsTime.secondsを当初使おうとしていたのですが、気になる点があったので、現在は pythonスクリプト上でtime.time()を呼んで経過時間を測定しています。

時間計測における注意
absTime.seconds
は、 TouchDesignerが起動してからの経過時間を取得できるものです。TouchDesigner上ですでに用意されており、TouchDesignerのコンポートや pythonコード上から読み出して使えます。しかし、このデータの更新頻度が TouchDesigner上の フレームレートに依存しているようので、 フレームレートとは関係なく時間を計測したい場合には注意が必要です。
実際に、Kフレームレートを意図的に落として実行し、場合には、時間の計測時間の分解能が1000 / フレームレート ms程度になってしまうので、フレームレートに依存しているようです。(touchDesigner version 2020.22080 現在)

画像8

TextDatを作り下記のようなスクリプトを書いて、mypyTimerという名前にします。本来は色々な例外処理等を書くべきですが、とりあえずシンプルに書いておきます。startTimer関数では、開始時間を保持しておいて、measureDuration()が呼ばれた時に、現時刻との差分を経過時間として、Constant CHOP (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にある関数を呼ぶことができます。

このあと、mypyTimerstartTimer()関数をbaseコンポーネントの外から実行したいので、あえて、startTimerというtextDATを用意して、単純にmypyTimerstartTimer()を実行するだけのスクリプトを書いておきます。

import mypyTimer
mypyTimer.startTimer()

(なお、コンポーネント外からのスクリプト実行方法としては、Extention Codeという方法もあります。githubにあるサンプルにはその方法を用いたタイマーも用意してあります。)

タイマーの開始

上記の通り、タイマーの開始は視覚刺激 (RED/GREEN/BLUE)の表示タイミングなので、第一回目に作成した画面遷移を制御するTimerCHOPsegment状態の変化でトリガーします。いままで作業していたbase_user_inputから1階層上がり、timer1callbackを使います。

画像9

TimerCHOPの右下にある下矢印のボタンをクリックすることで、callbackのスクリプトを記述することができます。例えば、「segment 4に入ったら観測データを記録して、ファイルに保存する」「あるセグメントに入ったら、外部装置にシリアルを送る、OSCメッセージを送信する」等、シーケンスの進行に応じた挙動をスクリプトで記述できます。

ここでは、視覚刺激(RED/GREEN/BLUE)はsegment 2 (値は0始まり)なので、onSegmentEnterでsegment = 2になったタイミングで、先程用意した、base_user_inputの中のstartTimerを実行するので下記のように書きます。

def onSegmentEnter(timerOp, segment, interrupt):
   if(segment == 2):
       op('base_user_input/startTimer').run()ui
   return

これで、画面遷移で視覚刺激が表示されたタイミングで、タイマーがスタートされます。

キーボード入力タイミングでの時間計測

次に、キーボード入力があったタイミングで、mypyTimer内のmeasureDuration関数を呼べば良いので、既に用意してあるkeyboardin1_callbacksに追記します。import mypyTimer #追記 return

import mypyTimer #追記

def onKey(dat, key, character, alt, lAlt, rAlt, ctrl, lCtrl, rCtrl, shift, lShift, rShift, state, time, cmd, lCmd, rCmd):
   if( state == 1):
       op('table_answer')[0,0] = op('select3')[0,0]
       mypyTimer.measureDuration() #追記
   return

これでkeyboardDATcallbackで時間経過の結果をtimer_duration (constantCHOP)に反映させる事ができました。

ここまでで、実験参加者の回答データと反応時間を計測することができました。タスクの実行とその反応結果を得られる事が確認できます。

正解不正解のフィードバック表示

画像11

ここまでで、実験回答が得られたので、回答が正解か不正解かを画面上にフィードバックする部分を作成します。これまでやってきた、DAT, CHOP, TOPを組み合わせて作ります。(これまでの操作を重複することも多いので文章での説明は割愛しますが、動画を見てもらえればと思います)

1. 実験条件となるDATから'color'に対応する文字列と実験回答データの文字列を比較して、結果をConstantCHOP (名前をisCorrect)にしておきます。
2. 比較結果をもとに、正解ならばOK, 不正解ならばNGを表示するTOPを出力
3. 第一回で作成した画面遷移において、仮で作成したTOPの代わりに 2.のTOPを接続する。

大まかに、以上の手順で正解不正解のフィードバックを、実験タスク内で表示出来るようになりました。

ここで行った作業のように、一度仮で作成したプログラムに、後から柔軟に変更を入れていけることはTouchDesignerのメリットの一つであると思います。

ソースコードはコチラ

ここまでで、実験タスク自体の機能実装は完了しました。次回は実験結果を記録する部分を作っていきます。




この記事が気に入ったらサポートをしてみませんか?