.. title:: Pythonで心理実験 - 例題7-1 例題7-1:VisionEggのサンプルを読む ============================================== **B:** あの、前から疑問に思っていたんですが。 **A:** ん、何が? **B:** AさんはVisionEggのプログラミングをどうやって勉強したんですか? **A:** んー。最初はVisionEggのサンプルから初めて、疑問に思ったことはソースコードを直接読んで、かな。まずはサンプルから始めればいいよ。 **B:** へ? サンプルなんてあったんですか? どこに? **A:** sourceforgeのVisionEggのダウンロードページ( `http://sourceforge.net/projects/visionegg/files/visionegg/ `_ )になかったか? どれどれ…。ああ、今(2011/06/09現在)はソースを取得しないといけないのか。 .. figure:: img/07-1-01.png **B:** どれをダウンロードすればいいんですか? **A:** visionegg-1.2.1.zipってやつだな。visionegg-1.2.1.tar.gzもファイルをまとめる方式が違うだけで中身は同じなんだが、Windowsの場合zipなら特別なソフトをインストールしなくても扱うことが出来るのでお勧めかな。 **B:** じゃあダウンロード、っと。 *------------------------------ 翌日 ------------------------------* **B:** あの、AさんAさん。 **A:** どうした? **B:** あの、サンプルのmouseTarget.pyっていうのを見てみたんですが、Aさんのプログラムと全然違うんですけど。 + 行番号なしのソースファイルはVisionEggの配布サイトからダウンロードしてください。 .. code-block:: python :linenos: #!/usr/bin/env python """Control a target with the mouse, get SDL/pygame events.""" # Variables to store the mouse position mouse_position = (320.0, 240.0) last_mouse_position = (0.0,0.0) ############################ # Import various modules # ############################ import VisionEgg VisionEgg.start_default_logging(); VisionEgg.watch_exceptions() from VisionEgg.Core import * from VisionEgg.FlowControl import Presentation, FunctionController from VisionEgg.MoreStimuli import * from VisionEgg.Text import * from math import * import pygame ################################# # Initialize the various bits # ################################# # Initialize OpenGL graphics screen. screen = get_default_screen() # Set the background color to white (RGBA). screen.parameters.bgcolor = (1.0,1.0,1.0,1.0) # Create an instance of the Target2D class with appropriate parameters. target = Target2D(size = (25.0,10.0), anchor = 'center', color = (0.0,0.0,0.0,1.0)) # Set the target color (RGBA) black text = Text( text = "Press Esc to quit, arrow keys to change size of target.", position = (screen.size[0]/2.0,5), anchor='bottom', color = (0.0,0.0,0.0,1.0)) # Create a Viewport instance viewport = Viewport(screen=screen, stimuli=[target,text]) ################ # Math stuff # ################ def cross_product(b,c): """Cross product between vectors, represented as tuples of length 3.""" det_i = b[1]*c[2] - b[2]*c[1] det_j = b[0]*c[2] - b[2]*c[0] det_k = b[0]*c[1] - b[1]*c[0] return (det_i,-det_j,det_k) def mag(b): """Magnitude of a vector.""" return b[0]**2.0 + b[1]**2.0 + b[2]**2.0 def every_frame_func(t=None): # Get mouse position global mouse_position, last_mouse_position just_current_pos = mouse_position (x,y) = pygame.mouse.get_pos() y = screen.size[1]-y # convert to OpenGL coords mouse_position = (x,y) if just_current_pos != mouse_position: last_mouse_position = just_current_pos # Set target position target.parameters.position = mouse_position # Set target orientation b = (float(last_mouse_position[0]-mouse_position[0]), float(last_mouse_position[1]-mouse_position[1]), 0.0) orientation_vector = cross_product(b,(0.0,0.0,1.0)) target.parameters.orientation = atan2(orientation_vector[1], orientation_vector[0])/math.pi*180.0 # Set target size global target_w, target_h global up, down, left, right amount = 0.02 if up: target_w = target_w+(amount*target_w) elif down: target_w = target_w-(amount*target_w) elif right: target_h = target_h+(amount*target_h) elif left: target_h = target_h-(amount*target_h) target_w = max(target_w,0.0) target_h = max(target_h,0.0) target.parameters.size = (target_w, target_h) ############################################# # Create event handler callback functions # ############################################# # target size global variables target_w = 50.0 target_h = 10.0 # key state global variables up = 0 down = 0 left = 0 right = 0 def keydown(event): global up, down, left, right if event.key == pygame.locals.K_ESCAPE: quit(event) elif event.key == pygame.locals.K_UP: up = 1 elif event.key == pygame.locals.K_DOWN: down = 1 elif event.key == pygame.locals.K_RIGHT: right = 1 elif event.key == pygame.locals.K_LEFT: left = 1 def keyup(event): global up, down, left, right if event.key == pygame.locals.K_UP: up = 0 elif event.key == pygame.locals.K_DOWN: down = 0 elif event.key == pygame.locals.K_RIGHT: right = 0 elif event.key == pygame.locals.K_LEFT: left = 0 # Create an instance of the Presentation class. This contains the # the Vision Egg's runtime control abilities. p = Presentation(go_duration=('forever',), viewports=[viewport]) def quit(event): p.parameters.go_duration = (0,'frames') p.parameters.handle_event_callbacks = [(pygame.locals.QUIT, quit), (pygame.locals.KEYDOWN, keydown), (pygame.locals.KEYUP, keyup)] ############################################################# # Connect the controllers with the variables they control # ############################################################# p.add_controller(None, None, FunctionController(during_go_func=every_frame_func) ) ####################### # Run the stimulus! # ####################### p.go() **A:** ああ、これはVisionEgg.FlowControlを使うやり方だな。この方法については今まで全く解説していなかったからな。 **B:** VisionEgg.FlowControl? なんですかそれ。 **A:** 簡単にいえば、今までの例題でwhileループを使って刺激を描画したりキー入力を処理していたけど、 こういった実験の流れを管理してくれるクラスだ。 **B:** えぇ、なんだか便利そうに聞こえますが、なんで今まで教えてくれなかったんですか? **A:** いろいろ理由があるけど、一番の理由は **関数の定義が出来ないと利用できない** からだ。 つまり、例題5で紹介した関数定義のテクニックを使いこなせるところまでpythonの学習が進んでいないと使えないんだよ。 **B:** ははあ、なるほど。 **A:** おまけに例題5-1で「出来るだけ使うな」って言った **global** が62行目と82行目、83行目、115行目、128行目に出てきている。 変数のスコープのことをある程度理解していないとここでglobalを使う意義がわからないだろうから、さらにハードルが高い。 **B:** なんだかよくわかりませんが、少しは考えて例題を選んでるんですね。 **A:** 「少しは」とは失礼な。そんなわけで、関数の定義や変数のスコープは理解しているという前提で、ざっと解説するぞ。 このプログラムの鍵は140行目のVisionEgg.FlowControl.Presentation()だ。これはさっき言った通り実験の流れを管理するクラスのインスタンスを生成するもので、 メソッドgo()を呼ぶと自動的に実験を行ってくれる。160行目だね。問題はどういう風に実験を「流す」のかだけど、それを指定しているのが45行目以降の関数群だ。 **B:** ふむふむ。 **A:** まず、60行目のevery_frame_func()。 **B:** って、いきなり60行目からですか。 **A:** その手前は小道具だからね。60行目のevery_frame_func()は、画面を描き換えるたびに 必ず実行してほしい処理が書いてある。例題6までのプログラムの書き方では、whileループの中でscreen.clear()、viewport.draw()、swap_buffers()の定番トリオを実行する 前にする処理だね。ただ、定番トリオの処理はPresentationがやってくれるので、ここに書く必要はない。 **B:** ずいぶんたくさん処理がありますね。 **A:** このサンプルではマウスとキーボードの操作に合わせて刺激の位置や大きさを変更している。その処理が結構凝ってるからね。 それで、このevery_frame_func()を画面描画のたびに実行してほしいとPresentationに登録するのが154行目のadd_controller()メソッドだ。 こうやって登録しておけば、go()した後は自動的にevery_frame_func()を呼び出してくれる。 **B:** このプログラム、160行目のgo()でいきなり終わっていますが、go()はいつ終了するんですか? **A:** VisionEgg.FlowControl.Presentationのデータ属性parameters.go_durationに設定しておく。 140行目にgo_duration=('forever',)って書いてあるのがそれだな。 **B:** foreverって、永遠にってことですか? **A:** その通り。 **B:** じゃあ終わんないじゃないですか! **A:** 話は最後まで聞きたまえ。parameters.go_durationには時間を指定することもできるし、ひとまず'forever'と設定しておいて、 実行中に「○秒後に終了したい」という状態になったらその秒数を設定しなおすこともできる。このプログラムではESCキーを押すかウィンドウを閉じる操作をしたら終了するように なっているんだが、そういうふうに何秒後に終了するか未定の場合は'forever'にしておくとよい。 **B:** なるほど。ところで('forever' **,** )の **,** は何ですか? **A:** ああ、終了までの時間や終了までに表示するフレーム数を指定する場合、数値と単位を並べて指定するんだ。 このプログラムでは144行目に(0,'frames')という値を設定しているのがそれだね。 0フレーム後に終了、すなわち直ちに終了するということだ。秒で指定する場合は(0.5,'seconds')のように書く。 'forever'の場合は単位は要らないわけだ。 **B:** ははあ。カンマの後ろになにもないのはちょっと気持ち悪いですが、意味はわかりました。 **A:** 144行目の話が出てきたところで、146行目のparameters.handle_event_callbacksの話をしておくか。 VisionEgg.FlowControl.Presentationを使ったプログラミングでは、キーボードのキー押しといったイベントの処理は自動的に行われる。 ただ、どのイベントが起こったときに、どのような処理を行うのかは当然プログラマが指示してやらないといけない。 それがparameters.handle_event_callbacksというデータ属性の役割で、「処理したいイベント」と「行わせたい処理を記述した関数」をタプルで まとめたものを並べたリストを設定しておく。こうしておくと後はVisionEgg.FlowControl.Presentationがよきにはからってくれる。 **B:** ええと、登録されているイベントは…、pygame.locals.KEYDOWNは今までの例題で出てきましたよね。 pygame.locals.KEYUPというのもなんとなく想像できますが、pygame.locals.QUITっていうのは何ですか? **A:** pygameのウィンドウを閉じる操作をしたときに生じるイベントだ。Windowsならウィンドウの右上の×ボタンを押した時などに生じる。 pygame.locals.QUITが生じると、146行目の設定に従ってquit()という関数が呼ばれる。quit()の定義はすぐ上の143行目だね。 ここでparameters.go_durationを0フレームに設定している。これで直ちにgo()メソッドの動作が停止する。 **B:** うーん、処理があちこち飛び回ってややこしいなあ。quit()の引数のeventってなんですか? **A:** 文字通り、parameters.handle_event_callbacksで呼び出された関数には引数として生じたイベントが渡されるんだ。 114行目のkeydown()関数や127行目のkeyup()が良い例だね。押されていたキーが離されたら、pygame.locals.KEYUPというイベントが生じる。parameters.handle_event_callbacksを通じて keyup()関数が呼び出される。keyup()関数では、引数として渡されたeventのデータ属性keyを見ることによって、どのキーが 離されたかを知ることができる。 **B:** うーん、うーん。 **A:** これらの関数ではキー押しの状態をup、down、left、rightという変数に保存しているが、 **これらの変数は関数内で値を代入されているから 例題5-1で説明したとおりローカル変数として解釈される。それでは関数が終了すると同時にこれらの変数が放棄されてしまい、後でevery_frame_func()でそれらの値を利用できなくなってしまう。 そこで、115行目、118行目で「これらの変数はglobalである」と宣言して、変数が放棄されるのを防いでいるわけだ** 。 **B:** うは。ついていけなくなりました。これ、全部マスターしないといけないんですか? **A:** いや、この辺りはプログラマの好みが分かれるところだろうね。例題5までに紹介したような whileループを使う方法がわかりやすいという人は、VisionEgg.FlowControl.Presentationをわざわざ使う必要はない。オブジェクト指向の プログラミングが好みの人なんかはVisionEgg.FlowControl.Presentationを使う方がしっくりくるだろうね。 **B:** オブジェクト指向? **A:** うーん、私も完全に独学なのできちんと説明する自信がないんだが。 そうだなあ、下手にいいかげんな説明を書くとアレなんで自分で勉強してくれ。 **B:** あ、逃げた。 **A:** 「いいかげんな解説」がモットーだからね。私自身の好みを言えば、今までの例題で紹介してきた 書き方の方がやりやすい。人間相手の心理実験では何種類もの教示を画面に表示したりする必要があるけど、そういう「刺激を表示している」 以外の状態が何種類もある実験のプログラムをVisionEgg.FlowControl.Presentationで書くのは面倒だと思う。 VisionEgg.FlowControl.Presentationでそのあたりをうまく処理するためにはbetwee_presentation()などのメソッドをうまく活用しないといけない。と、思う。 私はVisionEggをいじり始めて早々にそっち方面の努力は放棄してしまったので、興味がある人はぜひ自力で何とかしてほしい。 **B:** これまた他力本願な。 **A:** あくまで自分が独学してきたことをまとめているだけだからね。VisionEggの全機能を網羅した 解説を作ろうなんて企んだらただでさえ滞りがちな自分の仕事ができなくなっちまう。 **B:** ふうん。なんだか大変ですねえ。 **A:** なんだ、その棒読みなセリフは。ま、とにかくこの程度のことが分かっていれば、VisionEgg.FlowControl.Presentationを使っている サンプルプログラムは一応読めるはず。というわけで、例題7はこれにて終了。 **B:** へ、例題7-2は? **A:** visionegg-1.0-demosには他にも参考になるプログラムがたくさん含まれているが、 それはまた別の例題として取り上げたい。そんなわけで、また次回。