例題6-2:いつもより多めに回しております

A: さて、さくっと速度調節の話をするか。わざわざ例題6-2として区切っておいて アレなんだが、大したことは全くない。キー押しで速度を変えられるようにするためにいろいろ書き変えているが、 速度調節に関連しているのは55行目のみだ。

  • 行番号なしのソースファイルをダウンロード→ 06-2a.py

  • 画像ファイルをダウンロード→ 06-1a.png

 1#!/usr/bin/env python
 2# -*- coding: shift-jis -*-
 3
 4from VisionEgg import *
 5from VisionEgg.Core import *
 6from pygame import *
 7from pygame.locals import *
 8from VisionEgg.MoreStimuli import FilledCircle
 9
10import VisionEgg.Text
11import VisionEgg.Textures
12
13from math import sin, cos, pi
14
15screen = get_default_screen()
16screen.parameters.bgcolor = (0.0,0.0,0.0)
17SX,SY= screen.size
18
19stimRadius = 3
20imgRadius = 100
21
22tex = VisionEgg.Textures.Texture('06-1a.png')
23stimBlueCrosses = VisionEgg.Textures.TextureStimulus(
24                       texture = tex, size = tex.size,
25                       anchor='center', position=(SX/2,SY/2))
26
27stimFixation = VisionEgg.MoreStimuli.FilledCircle(
28                       radius = stimRadius,
29                       color = (0.0, 1.0, 0.0),
30                       anchor = 'center',
31                       position = (SX/2,SY/2))
32
33stimTargets = [VisionEgg.MoreStimuli.FilledCircle(
34                    radius = stimRadius,
35                    color = (1.0, 1.0, 0.0),
36                    anchor = 'center',
37                    position = (SX/2+imgRadius*cos((30+120*i)*pi/180.0),
38                                SY/2+imgRadius*sin((30+120*i)*pi/180.0)))
39               for i in range(3)]
40
41stimText = VisionEgg.Text.Text(position=(0,0),font_size=24)
42
43stimList = [stimBlueCrosses, stimFixation]
44stimList.extend(stimTargets)
45stimList.append(stimText)
46
47viewport = Viewport(screen=screen, stimuli=stimList)
48
49startTime = VisionEgg.time_func()
50t = VisionEgg.time_func()
51rfreq = 0.2
52flagLoop = True
53while flagLoop:
54    t = VisionEgg.time_func()
55    stimBlueCrosses.parameters.angle = (360*rfreq)*(t-startTime)
56    for e in event.get():
57        if e.type == KEYDOWN and e.key == K_z:
58            rfreq += 0.05
59        elif e.type == KEYDOWN and e.key == K_x:
60            rfreq -= 0.05
61        elif e.type == KEYDOWN and e.key == K_ESCAPE:
62            flagLoop = False
63    stimText.parameters.text = '%.2f rotation/sec' % (rfreq)
64    screen.clear()
65    viewport.draw()
66    swap_buffers()
67

B: ええと、zとxで回転速度を調節して、ESCで終了するんですね。

A: そう。zかxを押すとrfreqという変数の値が0.05増減する。 これは回転の周波数を表わしていて、0.2なら0.2Hz、すなわち1秒で0.2回転する。1回転するのに必要な秒数に換算すると?

B: ええと、5秒、ですかね。

A: その通り。周波数がx Hzならば1回転に必要な秒数は1/x。 さて、問題の55行目だが、この式はわかるかい?

B: えっと…。(t-startTime)は例題6-1と同じく刺激表示開始からの秒数ですよね。 それに360かけて、rfreqで割る?

A: まずrfreqを忘れて考えてみたらどうだ? つまり360 * (t-startTime)なら?

B: ええと、1秒経てば360になる。ということは、1秒で1回転?

A: その通り。じゃあ、さっきの0.2Hzの場合を考えてみよう。0.2Hzならば 1秒間で0.2回転。つまり1秒間で何度回転する?

B: 1回転360度×0.2=72度ですかね。

A: そこまで計算しなくていい。1秒間に360×0.2度回転するというのが分かればOKだ。 3秒で回転する角度は? また計算してしまわずに掛け算の式で答えるように。

B: 360度×0.2×3秒。

A: そう。一般化すると360度×周波数×経過時間で、指定された経過時刻の回転角度が 計算できる。これが55行目だ。

B: うーん。こうやって一歩ずつ解説されると簡単なような気がしますが、 解説なしでいきなり読んでわかるかなあ。

A: はは。自分で書いたプログラムを数ヵ月後に読んですぐに理解できないことが あるよ。こういうのはメモと取ったりしながら丁寧に考えるのが大切だ。あと、自分でプログラムを書くときはコメントをつけて おくと後で読むのが断然楽になる。

B: ふむふむ。…ところでAさん、さっきから気になってるんですが。

A: ん? 何だ?

B: このプログラム、回転速度を切り替えるときに一瞬カクッとなるのが気になるんですけど。

A: ああ、そりゃ速度が変われば刺激表示開始からt秒後の回転角が変わるからな。当然だ。

B: 滑らかに切り替わるようには出来ないんですか?

A: ふむ。B君はどう書き変えたらいいかわかるか?

B: あれ、Aさんはわからないんですか?

A: そうやって挑発してもダメ。ヒントをやるから…って、ヒント出すの難しいな。 B君に教えていない用語や機能を使わないといけない。

B: なんだ、じゃあぼくにわかるわけないんじゃないですか。さっさと教えてください。

A: 仕方ないなあ。要するに、刺激表示開始から秒数で計算するんじゃなくて、 1フレームの間に回転させるべき角度を計算して毎フレーム加算していけばいい。意味わかるか?

B: フレームというのがよく分かりません。

A: OK。コンピュータの画面は、1秒間に60回などのペースで高速に画面を描き換える 事でアニメーションを表示している。この描き換えの頻度を リフレッシュレート という。単位はHz。1秒間に60回 描き換えているならリフレッシュレートは60Hzだ。

B: あ、それはなんか聞いたことがあります。

A: 例題1-4でVisionEggのViewport、Screenとswap_buffers()の関係を解説したが、 あの時に「swap_buffers()で入れ換える」と言ったのがこの描き換えにあたる。コンピュータの画面は超高速で絵を入れ替える 「紙芝居」なわけだが、この紙芝居の絵一枚一枚をフレームと呼ぶ。

B: ふむふむ。

A: 1枚のフレームを表示して、次のフレームを表示するときに刺激を何度させて 描画すればよいかを計算できれば、B君の目的は達成できるわけだ。

B: ははぁ。なんとなくわかったようなわからないような。

A: 問題は、1フレーム表示して、次のフレームを表示するまで何秒かかるのか、だ。 毎回こう言うのは面倒くさいのでここではIFI (inter-frame interval:フレーム間時間)と呼んでおく。 IFIが分かれば、1秒間に回転させる角度×IFIで1フレーム間に回転させる角度が計算できる。

B: IFIってちょっとカッコよさげですね。IFI、IFI。

A: 問題はこのIFIをどうやって調べるかなのだが…。

B: 60Hzだったら1秒間に60フレームだから1÷60でいいんじゃないんですか? ええと、16ミリ秒くらい?

A: 60Hzなら、な。B君は今目の前にある画面が1秒間に何回描き換えられているかわかるのかね。

B: ぐっ。コントロールパネルか何かで調べられませんでしたっけ。

A: まあドライバがタコじゃなければな。ここでWindowsのコントロールパネルを…とか いう話をしてもいいんだが、それでは他のOSでは通用しないので、VisionEggのログ機能を使ってIFIを調べる方法を紹介しておこう。

B: …他のOSに配慮する気あったんですか?

A: 出来る範囲でな。その範囲は恐ろしく狭いかもしれないが。今まで解説していなかったんだが、 VisionEggにはログを取る機能がある。まあプログラムを見てもらった方が早いと思うので、上のサンプルプログラムを ちょいちょいと改造してみよう。

  • 行番号なしのソースファイルをダウンロード→ 06-2b.py

  • 画像ファイルをダウンロード→ 06-1a.png

 1#!/usr/bin/env python
 2# -*- coding: shift-jis -*-
 3
 4from VisionEgg import *
 5from VisionEgg.Core import *
 6from pygame import *
 7from pygame.locals import *
 8from VisionEgg.MoreStimuli import FilledCircle
 9
10import VisionEgg.Text
11import VisionEgg.Textures
12
13from math import sin, cos, pi
14
15start_default_logging()
16watch_exceptions()
17
18screen = get_default_screen()
19screen.parameters.bgcolor = (0.0,0.0,0.0)
20SX,SY= screen.size
21
22stimRadius = 3
23imgRadius = 100
24
25tex = VisionEgg.Textures.Texture('06-1a.png')
26stimBlueCrosses = VisionEgg.Textures.TextureStimulus(
27                       texture = tex, size = tex.size,
28                       anchor='center', position=(SX/2,SY/2))
29
30stimFixation = VisionEgg.MoreStimuli.FilledCircle(
31                       radius = stimRadius,
32                       color = (0.0, 1.0, 0.0),
33                       anchor = 'center',
34                       position = (SX/2,SY/2))
35
36stimTargets = [VisionEgg.MoreStimuli.FilledCircle(
37                    radius = stimRadius,
38                    color = (1.0, 1.0, 0.0),
39                    anchor = 'center',
40                    position = (SX/2+imgRadius*cos((30+120*i)*pi/180.0),
41                                SY/2+imgRadius*sin((30+120*i)*pi/180.0)))
42               for i in range(3)]
43
44stimText = VisionEgg.Text.Text(position=(0,0),font_size=24)
45
46stimList = [stimBlueCrosses, stimFixation]
47stimList.extend(stimTargets)
48stimList.append(stimText)
49
50viewport = Viewport(screen=screen, stimuli=stimList)
51
52frameTimer = VisionEgg.Core.FrameTimer()
53startTime = VisionEgg.time_func()
54t = VisionEgg.time_func()
55rfreq = 0.2
56flagLoop = True
57while flagLoop:
58    t = VisionEgg.time_func()
59    stimBlueCrosses.parameters.angle = (360*rfreq)*(t-startTime)
60    for e in event.get():
61        if e.type == KEYDOWN and e.key == K_z:
62            rfreq += 0.05
63        elif e.type == KEYDOWN and e.key == K_x:
64            rfreq -= 0.05
65        elif e.type == KEYDOWN and e.key == K_ESCAPE:
66            flagLoop = False
67    stimText.parameters.text = '%.2f rotation/sec' % (rfreq)
68    screen.clear()
69    viewport.draw()
70    swap_buffers()
71    frameTimer.tick()
72frameTimer.log_histogram()
73
74

A: まず15から16行目。これはおまじないのようなもので、ログを取れとVisionEggに 指示する時の定番の書き方。例題1-4でもちらっと解説した通り。続いて52行目のVisionEgg.Core.FrameTimerというクラスだが、 これが今回のポイント。これは今回の目的にズバリそのままのIFIを計算するクラスだ。swap_buffers()の後にtick()というメソッドを 呼び出すと、swap_buffers()を実行した時刻を記録してくれる。71行目だね。 そしてプログラムの最後、ここでは72行目でlog_histogram()というメソッドを実行すると、記録しておいた時刻からIFIを計算してくれる。 さて、実行してみよう。

B: よいしょっと。

A: 適当に速度を上げ下げさせたら終了して。

B: 終了しました。

A: 実行ディレクトリにVisionEgg.logというファイルがあるだろう? その一番最後に こういう表示があるはずだ。

Mean IFI was 16.70 msec (59.89 fps), longest IFI was 60.08 msec.
histogram:
  1448                                              *
  1287                                              *
  1126                                              *
   965                                              *
   805                                              *
   644                                              *
   483                                              *
   322                                              *
   161                                              *
     0                                    *    *    *    *         *    *
 Time:    0    2    4    6    8   10   12   14   16   18   20   22   24 +(msec)
Total:      0    0    0    0    0    0    1    15  +++   3    0    1    2

B: ありますね。何ですか、これ?

A: これがIFIを実際に計測した結果のヒストグラムだ。 1行目に書かれているように、平均IFIは16.70ミリ秒。59.89fps。fpsってのはframes per secondの略で、1秒間に描かれた フレーム数の平均。fpsとリフレッシュレートはあくまで別の概念なんだが、ここでは同じことだと思ってもらって良い。 最後のlongest IFI was 60.08msecというのは前のフレームを描いてから60.08ミリ秒空いてしまったフレームがあったと いうことだね。下の*が縦に並んでいるのはIFIの分布を示したヒストグラムだ。16ミリ秒から18ミリ秒の範囲に1400フレーム 以上が含まれていて、その前後にパラパラとやや短かったり長かったりするフレームが分布しているのが分かる。

B: え、リフレッシュレートって一定値じゃないんですか?

A: そのあたりをきちんと話すと長ーくなってしまうんだが、ここで集計して いるのはtick()メソッドが実行された間隔であって、画面の描き換えそのものではない。OSが何か処理をするために プログラムの実行が後回しにされたりすると、IFIが突発的に長くなってしまうことがある。

B: うーん。そういうものなんですか。

A: そういうものなのさ。マルチタスクOSを使う以上これは仕方がない事だ。 とにかく、このPCではIFIは16.70ミリ秒、リフレッシュレートは59.89Hz。まあこの辺りは誤差の範囲で60Hzと考える のが妥当だろう。そうするとIFIは1/60ミリ秒。

B: うーん。なんか釈然としないんですが。具体的にどの時点で遅れたとかわからないんですか?

A: VisionEgg.Core.FrameTimerのインスタンスを作る時にsave_all_frametimes = Trueという 引数を設定すると、すべてのフレーム時刻を記録するようになる。これを設定しておいたうえで、最後にframeTimer.get_all_frametimes() メソッドを呼び出すと、すべてのフレーム時刻を保存した長大なリストが得られる。これをデータ処理すればどこで 遅延が生じたかチェックできるが、油断しているとあっという間に膨大なデータになってしまうので注意が必要だ。 ちなみにnumpyとmatplotlibというパッケージを使ってグラフ化した例を示しておこう。

../_images/06-2-01.png

A: グラフの縦軸と重なって分かりにくいが、プログラムを起動した直後に50ミリ秒 近いIFIが生じ、その後10ミリ秒から30ミリ秒程度の範囲でちょっと荒れているが、その後はほぼ16.7ミリ秒で安定している。 PCによって違うのでどこまで一般化出来るかわからないんだが、私が持っている数台のPCで確認した範囲では、最初の 数十フレームはこんな具合に荒れがちなんだがその後は安定している事が非常に多い。そうそう、縦軸の単位は秒、横軸の 単位はフレームね。このPCのリフレッシュレートは60Hzだったので、横軸60が1秒にあたる。

B: へえ、pythonでこんなグラフ描けるんですね。このサンプルプログラムは見せて もらえないんですか?

A: さっきも言ったけどnumpyとmatplotlibというパッケージを使っていて、 これの使い方を解説するだけで例題数回分は欲しいくらいだ。matplotlibはmatlabとの互換性の関係でいずれは紹介したいと 思っているけど、今回は話が脱線していくだけなのでパスさせてほしい。さて、 今の目的は、速度を切り換えてもカクッとしないようにすることだった。そして、そのためにはフレーム毎に「1秒間に回転させる角度×IFI」 度ずつ加算していくことだった。IFIが1/60だったんだから、最初のサンプル06-2a.pyの55行目はどう書き換えればいい?

B: ええと、1秒間に回転させる角度は 360 * rfreq だから、(360*rfreq)*(1.0/60.0)ですか?

A: 正解。書き換えたものを改めて掲載するまでもないと思うから、ダウンロード 出来るようにしておくよ( 06-2c.py )。 もちろん06-2b.pyを実行してみてリフレッシュレートが 60Hzではなかった人は、自分のPCに合うようにここの数字を書き換える必要があるので注意してほしい

B: じゃあ、実行してみますか。…おお、確かにカクッとしない。

A: ふむ。じゃあ今回の例題はこのくらいにしておくかな。時間の話にしても、 テクスチャの話にしても、まだまだ解説しておかなきゃいけないことはたくさんある。それはまた例題を改めて。

補足その1

IFI推移のグラフを書くのに使用したプログラムを一応ダウンロード出来るようにしておきます( 06-2d.py )。 プログラムの実行にはVisionEggを実行できる環境の他にpythonのmatplotlibパッケージがセットアップされている必要があります。

補足その2

VisionEgg.Core.Screenにはリフレッシュレートを測定するmeasure_refresh_rate()と システムのリフレッシュレート設定を問い合わせるquery_refresh_rate()というメソッドがあります。 単にリフレッシュレートを知りたいだけならば、これらのメソッドを使えば目的は達成できますが、 本文の方法で一度はヒストグラムを描いてIFIの分布を確認することが望ましいと思います。

from VisionEgg.Core import *
screen = get_default_screen()
print screen.measure_reflesh_rate(average_over_seconds=0.1)
print screen.query_reflesh_rate()