例題16-5:動画ファイルを刺激として使う

B: おや、東京土産の(自主規制)ですね。Aさん東京行ってきたんですか?

A: 私は行ってないんだが、原作者のSが学会で東京に行ってきたとかっていって持ってきた。

B: ああ、そういえばさっきシケた表情で歩いてるのみかけましたよ。来てたんですか。

A: 奴がシケた顔しているのはいつものことだな。

B: それにしてもS先生がお土産を持ってくるってのは珍しいですね。

A: なんでもお子さんのために買ってきたんだけど一口で「おいしくない」って却下されたらしい。で、おこぼれが我々のところに回ってきたってわけだ。

B: へえ。じゃあお子さんには今後もどんどん却下してもらいたいところですね。じゃあいただきまあす。むぐむぐ。

A: …。(動画を見ている)

B: ん、Aさんなんですかその動画。蝶?

A: 前から予告していたVisionEggで動画を使う件だがな、やっとサンプルを書いたんでデモ用の動画を準備しているんだ。問題なく再生できているようだな。よしよし。じゃあ例題16の締めくくり、動画再生の解説を始めるか。テキトーな手持ちMEPG動画がない方はサンプル動画も用意しているのでダウンロードしてください。ちょっとファイルサイズが大きいですがご勘弁を。

B: あれ、予告にはQuickTimeを使うって書いてあったような。

A: んー。実は私が初めてVisionEggを使った頃のpygameにはバグがあったらしくてな、WindowsではMPEG動画の再生ができなかったんだ。それでQuickTimeを使っていたんだが、何気なく今回のサンプルを書く前に確認したらWindowsでもMPEG動画が問題なく再生できるようになったって書いてあるのを見つけてな。試してみたらあっさり出来たんで解説することにした。

B: pygame? 今までもキー入力とか音声再生とかで使ってきたパッケージですよね。

A: その通り。MPEG動画の再生はVisionEggの機能じゃなくてpygameの機能を使う。まあサンプルを見てくれ。

  • 行番号なしのソースファイル→ 16-8.py
  • サンプルMPEG動画(1280x720)→ 16-8a.mpg
  • サンプルMPEG動画(960x720)→ 16-8b.mpg
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#!/usr/bin/env python

import os
import VisionEgg
VisionEgg.start_default_logging(); VisionEgg.watch_exceptions()

from VisionEgg.Core import *
from VisionEgg.Textures import *
from VisionEgg.Text import *
import pygame
import pygame.surface, pygame.locals
import pygame.movie
import OpenGL.GL as gl

import Tkinter
import tkFileDialog

rootwindow = Tkinter.Tk()
rootwindow.withdraw()
fType=[('MPEG1 movie','*.mpg;*.mpeg')]
filename=tkFileDialog.askopenfilename(filetypes=fType,initialdir=os.getcwdu())
rootwindow.destroy()

screen = get_default_screen()

movie = pygame.movie.Movie(filename)

width, height = movie.get_size()
scale_x = screen.size[0]/float(width)
scale_y = screen.size[1]/float(height)
scale = min(scale_x,scale_y) # maintain aspect ratio

# create pygame surface (buffer to draw uncompressed movie data into)
pygame_surface = pygame.surface.Surface((width,height))

# tell the movie to render onto the surface
movie.set_display( pygame_surface )

# create a texture using this surface as the source of texel data
texture = Texture(pygame_surface)

# Create the instance of TextureStimulus

stimulus = TextureStimulus(texture  = texture,
                           position = (screen.size[0]/2,screen.size[1]/2),
                           anchor = 'center',
                           size = (width*scale,height*scale),
                           mipmaps_enabled = 0,
                           texture_min_filter=gl.GL_LINEAR)

texture_object = stimulus.parameters.texture.get_texture_object()

text = Text(text='--------', position = (0,0), font_size=64)

viewport = Viewport(screen=screen,
                    stimuli=[stimulus, text])

flagRun = True
isPlaying = False
while flagRun:
    for e in pygame.event.get():
        if e.type==pygame.locals.KEYDOWN:
            if e.key==pygame.locals.K_SPACE:
                if not isPlaying:
                    movie.play()
                    startTime = VisionEgg.time_func()
                    isPlaying = True
            elif e.key==pygame.locals.K_ESCAPE:
                flagRun = False
    
    if isPlaying and not movie.get_busy():
        isPlaying = False
        movie.rewind()
        text.parameters.text = '--------'
    
    texture_object.put_sub_image( pygame_surface )
    if isPlaying:
        text.parameters.text = str(VisionEgg.time_func() - startTime)
    
    screen.clear()
    viewport.draw()
    swap_buffers()



B: んじゃさっそく実行。おお、なんか市販アプリみたいなダイアログが。こんな凝ったダイアログ表示するようなコードってプログラム中にありましたっけ?

../_images/16-5-01.png

A: 15から22行目だな。tkFileDialogというのはTkinterの一部で、実行しているOSの標準的なファイル選択ダイアログを開いてくれるモジュールだ。以下のようなメソッドがあって、とても便利だ。

askdirectory ディレクトリを選択する。
askdopenfile, askopenfiles 選択されたファイルを読み込みモード開き、ファイルオブジェクトを返す。askopenfilesは複数のファイルを同時に開いてファイルオブジェクトのリストを返す。
askopenfilenames, askopenfilenames 選択されたファイルの名前を返す。ファイルを開く作業は自分で書かなければならないが、標準の方法でファイルを開くのではない場合はこちらが便利。askopenfilenamesは複数ファイルの選択が可能で、ファイル名を並べたタプルを返す。ただしWindows上ではバグのためファイル名をスペース区切りで並べた文字列を返すので注意(python2.6、2.7系で確認)。
asksaveasfile 選択されたファイルを書き込みモード開き、ファイルオブジェクトを返す。
asksaveasfilename 選択されたファイルの名前を返す。

B: なんだ、こんな便利なのがあるなら 例題5-3 とかこれで済んだのに。なんで早く教えてくれなかったんですか。

A: tkFileDialog使ったらclassの練習にならないだろ。そこはちゃんと考えているのだよ。

B: 例題5が終わってから今までにも機会はいくらでもあったはずですが…。

A: ごちゃごちゃうるさいなあ。ちなみにaskdirectory以外のメソッドはfiletypesという引数を指定できる。これはダイアログで表示させたいファイルの種類を20行目のように拡張子で指定する。

B: (あ、逃げたな) …ええと、これはどういう形式ですかね?

A: ファイルの種類の名称を表す文字列と拡張子をペアにしたタプルを並べたリストを指定する。名称とこの例では拡張子*.mpgのファイルだけを表示させようとしているが、もし*.mpg、.mov、.wmvを選択させたいのならこうする。

(('MPEG','*.mpg'),('QuickTime','*.mov'),('Windows Media','*.wmv'))

B: ん? 外側はタプルでもいいんですか?

A: ああ、それは大丈夫。けど内側はタプルじゃないとダメなんで注意。ちなみにfiletypesを省略するとすべてのファイル(.)が指定されたことになる。

B: ふむふむ。

A: あとは引数initialdir。これはダイアログが開いたときに表示するディレクトリを指定する。ここではos.getcwdu()を使ってカレントディレクトリを得ているね。この例で使っていない引数としてはダイアログのタイトルを指定するtitleなんかがある。詳しくは各自で調べてくれ。

B: 直前の19行目のrootwindow.withdraw()ってなんですか? そういえばさらにその直前のTkinter.Tk()っていうのも初めて見るような気が。

A: ああ、これはだな。tkFileDialogで開くダイアログには親ウィンドウが必要なんだ。

B: 親ウィンドウ?

A: 要するに、tkFileDialogのダイアログを開くためには、必ず何かTkinterのウィンドウが開いていないといけないんだ。Tkinterのウィンドウが一つも開いていない状態でtkFileDialogのメソッドを呼ぶと、仕方がないので小さなTkinterのウィンドウを勝手に開いてからダイアログを表示する。こんな具合に。

../_images/16-5-02.png

B: はぁ、なるほど。でもちゃんと問題なく動いているように見えますが。

A: んん? まあ問題ないといえば問題ないが…。

B: じゃあ別にいいんじゃないかと思いますが。僕ならぜんぜん気にしませんが。

A: そう言われると返す言葉がないな。でも私は気になるんだよ。んで、この小さいウィンドウが出てくるのを防ぐために先回りするのが18行目と19行目。18行目のTkinter.Tk()はTkinterのウィンドウを生成する。そして19行目のwithdraw()メソッドはウィンドウを描画させないようにする働きがある。withdraw()してもウィンドウは存在しているので、tkFileDialogのメソッドを呼んでも勝手にウィンドウを開かれることはない。

B: …。

A: で、用が済んだらこの隠れたウィンドウも廃棄してよい。22行目のdestroy()だね。

B: そこまで面倒なことをしなくてもいいような気がするんだけどなあ。

A: 気持ちの問題だな。この方が美しいだろ。

B: 普段は「動きゃなんでもいいんだよ!」とか叫んでるくせに。

A: まあ気にすんな。さて、やっと本題の動画再生の話を始めるんだが…

B: ああ、そういえば今回のテーマはそうでしたね。忘れてた。

A: (無視して)最初に言った通りMPEGの再生ではpygameに頼るので、まずpygameの準備をする。11行目と12行目でpygame.surfaceとpygame.movieをimport。26行目でpygame.movie.Movieのインスタンスを生成。pygame.movie.Movie()にファイル名を渡してMPEGファイル開くので、tkFileDialog.askopenfileじゃなくてtkFileDialog.askopenfilenameを使ったわけだ。

B: なるほど。

A: 続いて34行目でpygame.surface.Surfaceのインスタンスを生成。この時にサーフェスのサイズを引数として指定しないといけないので、28行目から31行目でムービーのサイズからサーフェスのサイズを計算している。

B: ちょ、ちょっと待ってください。サーフェスっていきなり出てきましたけど何ですか?

A: おっと。そこから説明が必要か。面倒くさいなあ。VisionEgg.Textures.Textureみたいなもの。これでOK?

B: へ? それだけ?

A: 要するに画像ファイルを読み込んだり何かを描き込んだり出来る小さな板のようなもので、スクリーン上の位置に指定して配置することができる。重ね合わせの順番とかの考え方もVisionEgg.Textures.Textureと一緒。pygameのサーフェスは透明色の指定にカラーキーを使えるとか違いもあるんだけど。

B: カラーキー?

A: 指定された特定の色を透明とみなして描画しない機能のことだ。その辺に興味があればpygameを勉強してもらえばいい。28行目の解説に戻るが、まず28行目のget_size()メソッドで読み込んだMPEGムービーの画面サイズを取得する。ムービーを画面いっぱいに拡大して表示するために、29行目と30行目でムービーのサイズとスクリーンのサイズの比を求める。ムービーとスクリーンの縦横比が一致するとは限らないので、31行目でムービーが画面からはみ出ないように小さいほうの倍率を選ぶ。

B: ムービーを拡大しないんだったらこの処理は不要ってことですか?

A: そうだな。実験で使うんだったらムービーを制作する時点で実際に使用するサイズにしておくべきだと思うから、ちゃんとそう準備しておいた場合はムービーのサイズをそのままサーフェスの作成に使えばよい。

B: ふむふむ。

A: さて、やっと34行目だな。pygame.surface.Surface()でpygameのサーフェスを生成。サーフェスの幅と高さを並べたタプルを引数に指定する。

B: ふう、長かったですね。

A: まだまだ。続いて37行目でpygame.movie.Movieのset_dispya()メソッドでムービーとサーフェスを結び付ける。これで、ムービーを再生すると自動的にサーフェスにムービーがレンダリングされる。

B: レンダリング。つい最近出てきましたね。なんだっけ。

A: ムービーがサーフェスに描かれるってこった。そして最後に40行目でpygameのサーフェスからVisionEggのテクスチャを生成する。これでムービーとVisionEggのテクスチャが結び付けられる。

B: なんだか頭がクラクラしてきました。

A: これで準備完了、と言いたいところだがもう一つあった。後でVisionEggのテクスチャを更新するためにテクスチャへのインターフェースを確保しておく。51行目のget_texture_object()ね。これはVisionEgg.Textures.Textureのメソッドだな。

B: 全然わからなくなってきたんですが…

A: なんだか妙な日本語だな。ともかく一連の流れは定番なんでコピペして使えばいいよ。今度こそ準備が終わったので、次は再生してみる。65行目、ムービーが再生されていない状態でスペースキーが押されたらpygame.movie.Moiveのplay()メソッドを呼び出してムービーを再生する。そして76行目のようにしてムービーが再生されているpygameのサーフェスをVisionEggのテクスチャに転送すればVisionEggでのムービー再生が実現する。これも定番の処理だからコピペすればいいよ。

B: うーん。難しい。

A: あと、言っておくべき点があるとすれば71行目と73行目。pygame.movie.Movieのget_busy()メソッドはムービーが再生中か否かを返す。rewind()メソッドはムービーを先頭まで巻き戻す。サンプルプログラム中では、これらのメソッドを使って再生中の経過時間の表示を更新したり、再生終了後にもう一度再生できる状態を準備したりとった作業をしている。そんなに難しいことはやっていないので各自で読み解いてほしい。

B: えー。

A: この後まだたくさん解説することが残ってるんだ。そのくらい手抜きさせてくれ。そうそう、サンプルプログラム中では使っていないけど再生を一時停止するstop()というメソッドもある。だいたいこれくらいのメソッドを知っていれば実験には間に合うんじゃないかな。pygame.movieのメソッドの全貌を知りたい人はpygameの解説をあたってほしい。

B: ぶーぶー。

A: 文句言ってないでサンプルを実行してみてくれ。MPEGファイルを選択した後、スペースキーを押すと再生を開始する。終了はESCキーだ。まず16-8aから再生してみて。

B: はいはい。よいしょ、と。

../_images/16-5-03.jpg

B: …カエルですね。どっからこんなムービー入手してくるんですか。

A: 著作権上の問題があるムービーは配布できないからな。私のコレクションの中から問題ないものを探したらコレが見つかったので。

B: (…コレクション?)

A: さて、この画面で注意してもらいたいことがあるんだが、右側が一部真っ黒だろう? サンプルを終了して、今度はムービーファイルを適当な動画再生アプリケーションで再生してみてほしい。

B: んじゃMedia Player Classic Home Cinema(MPC-HC)で再生します。…あれ、右側が黒くない。

../_images/16-5-04.jpg

A: そう。pygame.movieの厄介なところはどんなMPEGムービーでも再生できるわけじゃない、ていうかまともに再生できるMPEGムービーがほとんどないって事なんだ。

B: えっ、なんですかそれ!

A: ちなみにこのムービーの場合は画面サイズ(1280×720)に問題があるみたいで、同じムービーをピクセル数が4:3になるように960×720にクロップしてやると正常に表示される。クロップ後のムービーが16-8b.mpgなんで、こいつを再生して確認してみてほしい。

B: 確かにこちらはサンプルプログラムでもMPC-HCでも同じように再生されますね。

A: MPEG1やMPEG2のムービーを持っている人は、このサンプルプログラムで再生してみて正常に再生できないことを確認してみてほしい。単にエラーメッセージが出てプログラムが停止するだけならいいけど、最悪の場合スピーカーから 破壊的な音 が鳴り響いて大変なことになりかねないので十分に注意してほしい。

B: そんなこと言われたら誰も試しませんよ! どういうムービーなら正常に再生されるんですか。

A: pygameのメーリングリストでも議論されていたりするんだけど、結局多くの環境で正常に再生できることが確認されているのは オーディオがmp2、ビデオがmpeg1でフレーム間予測がない(I-frameのみ) ムービーだ。今のところ、私が試した限りこのフォーマットで再生できなかったことはない。

B: ??? さっぱりわからないんですけど、その条件を満たすムービーはどうやって作ればいいんですかね?

A: いろんな方法があるだろうが、フリーで入手できるツールとしては ffmpeg を紹介しておきたい。ffmpegはコマンドシェルに慣れていない人にはちょっととっつきにくいツールだけど、解説が豊富にあるのでwebを検索してほしい。ffmpegを使って、以下のように変換すれば目的のフォーマットの動画が得られる。ffmpegのコーデック指定の書き方はffmpegのバージョンによって違うので、自分がダウンロードしたffmpegでの書き方を確認してほしい。大抵のバージョンのffmpegならffmpegとだけタイプしたらオプションが、ffmpeg -formatsとタイプすれば使用できるコーデックが表示されるはず。

ffmpeg -i 変換前のムービーファイル -vb ビデオのビットレート -ab オーディオのビットレート
    -vcodec mpeg1video -acodec mp2 -intra 変換後のムービーファイル

B: 赤い文字のところがポイントなんですよね。他のオプションは?

A: -iは変換前ファイルの指定に必要だし、ビデオやオーディオのビットレートの指定(-vb、-ab)は省略した時の標準的なビットレートだと低すぎてとても実験に使えないような品質になってしまうと思う。まあムービーの画面の大きさにもよるが。

B: ふうん。動画投稿サイトの低品質がどうとか高品質がどうとかいうアレですね。

A: どうとかこうとかいうアレでは何のことやらさっぱりわからんが、何となく言わんとしてることはわかる。ムービーの変換についてはそういうサイトに投稿している人たちが詳しいだろう。さて、これでMPEGムービー再生の解説はおしまい。次はQuickTime。

B: あれ、ffmpegの解説はもう終了?

A: ffmpegの解説はweb上に山ほどあるから。それに皆さんお気に入りの変換ツールがあるだろうし。

B: 最後にひとつだけ。ムービーを再生する前に、再生される位置に黒い四角が表示されてますけど、これって実験に使うときには都合悪くないですかね?

A: ああ、そりゃ都合悪いこともあるだろうな。だったら再生していないときは消しておけばいい。

B: どうやって?

A: おいおい、もう例題16なんだからそのくらいわかりなさい。結局VisionEgg.Textures.TextureStimulusで表示してるだろう? VisionEggの刺激を画面に描画するか否かを制御するパラメータは?

B: えーと、parameters.on。

A: その通り。再生していないときにはparameters.on=Falseにしておけばいい。

B: なるほど。

A: さて、QuickTimeの再生なんだが、QuickTimeの再生にはちょっとした面倒がある。

B:

A: まず、WindowsではQuickTimeのDLLがインストールされているディレクトリがモジュールのソースに決め打ちで書かれているので、他の場所にQuickTimeがインストールされていると正常に動作しない。特に 64bitのWindowsでは標準でインストールされるディレクトリがソースに書かれているディレクトリと異なる ので、ほぼ確実に動作しない。

B: ほぼ確実って、ダメじゃないですかそんなの。

A: モジュールのソースを一か所書き換えるだけで動く。Pythonをインストールしたディレクトリのlib\site-packages\VisionEgg\qtlowlevel.pyというファイルをテキストエディタで開くと、冒頭に以下のような記述がある。2行目のctypes.CDLLの引数に注目してほしい。

if os.name=='nt':
    QTMLClient = ctypes.CDLL(r'C:\Program Files\QuickTime\QTSystem\QTMLClient.dll')
elif sys.platform.startswith('darwin'):
    # There was once a functional Mac QuickTime implementation, but it
    # used a combination of the Python stdlib's quicktime module and
    # some C extensions based on the Carbon QuickTime interface. Given
    # the inevitable long-term ultimate demise of Carbon, it would be
    # foolish to spend much time on the Carbon implementation. On the
    # other hand, the newer implementation will require someone who
    # knows or learns the new QTKit bindings, which come included with PyObjC.
    raise NotImplementedError('QuickTime support is not implemented for Mac OS X.')

B: あー、C:\Program Files\QuickTimeにインストールされていることが前提になってますね。

A: 少なくとも現行のQuickTimeは32bitなので、64bitのWindowsなら標準でProgram FilesじゃなくてProgram Files (X86)にインストールされる。

B: じゃあqtlowlevel.pyのProgram FilesをProgram Files (X86)に書き換えたら…。

A: 正常に動くようになる。それだけ。

B: 一回書き換えたら後はずっと正常に動くんですよね?

A: もちろん。

B: じゃ、あまり大した手間じゃないですね。

A: まあそうなんだが、慣れてない人が使う時にハードルが高くなるよな。そうそう、もちろんQuickTimeのインストールディレクトリを自分で変更している人はそれに合わせて書き換えてください。

B: なんだかその下に何やら書いていますね。darwin?

A: Macで実行する場合の処理だな。私ぁMacは持ってないんで確認できないんだけど、要するにMacOS XではQuickTimeは使えないよってことらしい。

B: えー。QuickTimeってAppleですよね?

A: 事情はそこに書いてある通りだよ。良くも悪くもAppleらしいと言うか。MPEGならOS問わず再生できるはずだから、MPEGを使えばいいんでないかな。

B: じゃあなんでQuickTimeの解説するんですか。

A: 最初に書いただろ。少し前のバージョンのWindows版PygameではMPEGの再生に問題があったんだよ。だから今回のサンプルはもともとQuickTimeで用意したんだ。せっかく用意したんだから使わないと癪に障る。

B: 相変わらず大人げないですな。

A: さて、上記のqtlowlevel.pyの問題はすでに解決されているとする。そうそう、当然QuickTimeもインストールされてないと次のサンプルは動きません。QuickTimeをインストールしてない方はインストールしておいてください。私はイヤだけど。

B: そんなこと言われなくても…って、ちょ。最後なんて言いました?

A: QuickTime入れるのヤなんだよ。Apple Software Updateとかわけわからんものを勝手にインストールするし。正直なところ実験に使うPCには入れたくない。MPEGが再生できるようになったから私ぁ今後MPEG一択だな。

B: また反感を買いそうなことを。

A: そうかなあ。QuickTime嫌いな人は結構多いと思うけど。配布終了しちゃったみたいだけどQuickTime Alternativeとかあったし。あ、あともう一つ。以下のサンプルは実質Windowsで動かすことを想定していますので念のため。

  • 行番号なしのソースファイル→ 16-9.py
  • サンプルQuickTime動画(1280x720)→ 16-9.mov
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#!/usr/bin/env python

import os
import VisionEgg
VisionEgg.start_default_logging(); VisionEgg.watch_exceptions()

from VisionEgg.Core import *
from VisionEgg.Textures import *
from VisionEgg.Text import *
from VisionEgg.QuickTime import new_movie_from_filename, MovieTexture
import pygame
import pygame.locals

import Tkinter
import tkFileDialog

rootwindow = Tkinter.Tk()
rootwindow.withdraw()
fType=[('QuickTime movie','*.mov')]
filename = tkFileDialog.askopenfilename(filetypes=fType,initialdir=os.getcwdu())
filename = filename.replace('/','\\')
rootwindow.destroy()

screen = get_default_screen()

movie = new_movie_from_filename(filename) # movie is type Carbon.Qt.Movie
bounds = movie.GetMovieBox()

width = bounds.right-bounds.left  #width = bounds[2]-bounds[0]
height = bounds.bottom-bounds.top #height = bounds[3]-bounds[1]

scale_x = screen.size[0]/float(width)
scale_y = screen.size[1]/float(height)
scale = min(scale_x,scale_y) # maintain aspect ratio

movie_texture = MovieTexture(movie=movie)

stimulus = TextureStimulus(
    texture=movie_texture,
    position = (screen.size[0]/2.0,screen.size[1]/2.0),
    anchor = 'center',
    mipmaps_enabled = False, # can't do mipmaps with QuickTime movies
    shrink_texture_ok = True,
    size = (width*scale, height*scale),
    )

texture_object = stimulus.parameters.texture.get_texture_object()

text = Text(text='--------', position = (0,0), font_size=64)

viewport = Viewport(screen=screen,
                    stimuli=[stimulus, text])

flagRun = True
isPlaying = False
while flagRun:
    for e in pygame.event.get():
        if e.type==pygame.locals.KEYDOWN:
            if e.key==pygame.locals.K_SPACE:
                if not isPlaying:
                    movie.GoToBeginningOfMovie()
                    movie.StartMovie()
                    startTime = VisionEgg.time_func()
                    isPlaying = True
            elif e.key==pygame.locals.K_ESCAPE:
                flagRun = False
    
    if isPlaying and movie.IsMovieDone():
        isPlaying = False
        text.parameters.text = '--------'
    
    movie.MoviesTask(0)
    if isPlaying:
        text.parameters.text = str(VisionEgg.time_func() - startTime)
    
    screen.clear()
    viewport.draw()
    swap_buffers()



B: じゃあサンプルムービーもダウンロードして、と。今度は蝶ですね。

A: 先ほどMPEGの再生でうまくいかなかった1280×720なんだがきちんと再生されている点に注目してほしい。

B: あ、そう言われれば。これは長所ですね。QuickTimeだとコンデジで録画したムービーをそのまま再生できますし。

../_images/16-5-05.jpg

A: ダイアログでファイルを選択してスペースで再生、ESCで終了はさっきと同じ。処理内容もだいたい同じなので、MPEG用のサンプル16-8.pyと違う点を集中的に取り上げよう。まず10行目でVisionEgg.QuickTimeからいくつかimport。そして21行目がつまらない事なのだが、VisionEgg.QuickTime.new_movie_from_filename()はtkFileDialog.askopenfilename()で返されるファイルの書式ではエラーになってしまう。具体的には/を\に置き換えないといけない。そこで文字列型のreplace()メソッドを使って/を\に置き換えている。\はエスケープ文字なので’\\’と書くかr’\’と書かないといけない点に注意。

B: 面倒くさいですねえ。

A: 続いてムービーの画面の大きさを取得する部分。QuickTimeのバージョンの問題なのだと思うが、VisionEgg公式のサンプルの書き方は現行のQuickTimeだとエラーになる。これまた具体的に言うと、27行目のGetMovieBox()メソッドによって従来はムービーの上下左右の座標が格納されたシーケンス型の値が得られたみたいなんだが、現行のものではこれらの値にデータ属性としてアクセスするようになっている。そこでムービーの大きさを得るためには29行目、30行目のように書かないといけない。ちまみに29行目、30行目にコメントとして書かれている部分がVisionEgg公式サンプルの書き方だ。

B: ちまみに?

A: う、うるさい。ちょっとろれつが回らなかっただけだ。例によってムービーを拡大縮小しないのであればこの辺りは不要な処理だ。あとは36行目。VisionEgg.QuickTime.MovieTexture()でテクスチャを作成し、これを38行目でVisionEgg.Textures.TextureStimulusに結び付ける。これで準備完了。

B: ふむふむ。

A: あとはキー押しに対して動画の再生や巻き戻し、テクスチャの更新をするだけ。再生開始はStartMovie()、巻き戻しはGoToBeinningOfMovie()、再生中かどうかの確認はIsMovieDone()。MPEGとちょっと違うので二つのサンプルをよく見比べてほしい。

B: このStartMovie()とかは何かのクラスのメソッドですよね? なんていうクラスかソースを読んでもわからないんですが…。

A: ん。このソースを読んだだけではわからんな。VisionEgg.QuickTime.new_movie_from_filename()で生成されるのはVisionEgg.qtmovie.Movieのインスタンスだ。だからソース中のmovieという変数から呼び出しているメソッドはVisionEgg.qtmovie.Movieのメソッドということになる。

B: ありがとうございます。あと、もうひとつよくわからないんですが…

A: 何?

B: 47行目のtexture_objectですが、この変数って後で全く使われてないんじゃないですか?

A: えっ、そんなはずは…。 ぐは、使ってないな。

B: じゃあ47行目は不要?

A: そのようだな。実際コメントアウトしても動く。B君に指摘されてしまうとは。

B: へへへ。成長しました?

A: むう。認めざるを得ないな。そうそう、ひとつ言い忘れたけど…

B: あれ、サンプルプログラムを修正しないんですか?

A: 行番号がずれて面倒くさいから修正しない。動くんだから問題ないだろ。

B: 舌の根も乾かぬうちにまたそんなこと言うかなこのおっさんは…

A: (無視して)テクスチャの更新は72行目のMovieTask()というメソッドで行う。これを言っとかないとな。ここでテクスチャが更新されるので47行目のテクスチャオブジェクトへのインターフェースが不要なわけだ。

B: 引数の0はなんですか?

A: さあ? ヘルプを見ても何も書かれていないし。公式サンプルもこう書いてあるから。ソースを解析すりゃたぶんわかると思うんだけど、そこまでする気ないんで。

B: …。(あきれ顔)

A: ま、だいたいこんなもんかな。これで例題16終了、例題16は全体的に難産だった。

B: あの、ちょっと聞きたいことが。

A: なんだよ、人が解放感に浸ろうとしたときに。

B: これ、16-9.movを再生した直後は蝶の羽が動かないですよね。でも同じファイルをMPC-HCで再生すると最初から滑らかに羽が動くんですけど。

A: へ? どれどれ…

B: …。

A: …。

B: …。 あの、Aさん?

A: うーん、どうも最初に再生するときにもたつくみたいだなあ。確かにB君の指摘通り最初は少しもたついている。が、一度最後まで再生した後もう一度スペースキーを押して再生すると、今度は最初から滑らかに再生される。 実は最初のファイル選択ダイアログでファイル名を直接入力すれば拡張子*.mov以外のムービーでも再生できるんだが、それで16-8a.mpgなんかを再生してもやはり最初の一回だけ再生がおかしい。

B: これはさすがにまずいんじゃないですか?

A: とりあえず対策としては、読み込んだ後表示する前に一度画面に描画せずに再生することかなあ。まあそこまでして無理にQuickTimeを使わなくてもMPEGにすればいいと思うが。

B: 今まで素朴に「わーい、ムービー再生できてる」と思ってたんですけど、再生のタイミングってどれだけ正確なんですかね? そこがしっかりしていないと怖くてシビアな実験には使えないような気が。

A: むむむ…。実験の内容によってはアウトだろうな。ちょっとこれは宿題にしといてくれ。時間的な正確さを問わない用途なら今回のサンプルで特に問題ない。

B: どのくらい正確に再生できてるか確認する方法ってあるんですか?

A: うーむ。まあぱっと思いつくものであれば、各フレームに今なんフレーム目かを入れたムービーを作って、再生を開始してから一定時間経過した時点でムービーを一時停止して、その時に表示されているフレームが適切かどうか確認するとか。そんなに難しいプログラムじゃない。

B: 難しくなんならさっさと書けばいいのに

A: 何か言ったか? とにかく、もうこの例題16-5もずいぶん長くなったし、ムービー再生の時間的な正確さは宿題ということにしといていったん閉じさせてくれ。

B: はーい。

A: あーあ、気持ちよく終われると思ったのになあ。ぐすん。

補足

pygame.movie.Movieにはget_frameという現在のフレームを取得するメソッドがあります。これを利用して簡易版の時間精度評価をしました。本来なら本編の最後にA氏が述べているようにまさに表示されている画面をgrabして確認すべきですが、そこまでちゃんとしたものを書けるのがいつになるかわかりませんので。

  • testmovie.mpg (注意!約6MB):16-10a.pyおよび16-10b.pyで使用されているサンプルムービー。フレームレート25FPSで600フレーム(=24秒)あり、フレーム毎に現在何フレーム目かが表示されています。

  • 16-10a.py :testmovie.mpgを再生し、ランダムなタイミングで停止させるという作業を100回繰り返します。各再生ごとに以下の値を記録したCSVファイルを出力します。

    • 停止させる目標時刻(sec)。
    • 実際に停止した時刻(sec)。
    • 停止させる目標時刻にFPS(25で固定)をかけた値。
    • 実際に停止した時刻にFPS(25で固定)をかけた値。期待されるフレーム番号を示している。
    • 実際に停止した時にget_frameメソッドが出力したフレーム番号。
  • 16-10b.py :testmovie.mpgを一回再生し、VisionEggでの刺激描画を行う直前に毎回get_frameメソッドを実行してその時の時刻とともに記録します。最終的に各描画直前の時点での以下の値を保持したpylab.arrayインスタンスが得られます。プログラム終了時に最初の40サンプル分について2番目の値と3番目の値と、2番目の値の少数を切り上げた値をプロットした折れ線グラフを描画します。IPythonから実行すると、プログラム終了後にすべてのサンプルを確認することができます。

    • データを取得した時刻(sec)
    • データを取得した時刻にFPS(25で固定)をかけた値。期待されるフレーム番号を示している。
    • get_frameメソッドが出力したフレーム番号。

Core i7 920 + Win7(x64)機で実行してみたところ、期待されるフレーム番号の小数点以下を切り上げた値がget_frameで得られる値と大部分のサンプルで一致します。16-10a.pyをフルスクリーンで実行すると両者の値が一致する割合が82%、ウィンドウモードでは79%でした。一致しなかったサンプルではすべてget_frameで得られるフレーム番号が期待される値を切り上げたものより1大きい(1フレーム先に進んでいる)値でした。

同じマシンでウィンドウモードで16-10b.pyを実行した結果が以下のグラフです。赤がget_frameで得られたフレーム番号で、緑が期待されるフレーム番号の小数点以下を切り上げた値です。両者が一致すればまったくズレがないわけですが、数フレームに一回、実際に表示されるフレーム(赤)が先行している状態が続いています。VisionEggのフレームレート(60Hz)とムービーのFPS(25)が一致しないので、多少のずれは仕方がないかも知れません。

../_images/16-5-06.png

とりあえず一台のPCでしか確認していませんが、これを見る限り時間的な精度についてそう悲観することはなさそうです。というのは、ムービーを使って提示するような刺激であれば前後のフレームで映っているオブジェクトの位置が大きく変化することはないからです。不連続なシーンを数秒ずつ次々と再生するような刺激で、シーンの切り替わりからの反応時間などを非常に正確に測る必要がある場合などでは注意が必要でしょう。

さらに補足

やはりget_frame、put_sub_image、swap_bufferの時間差が 気になって仕方がない ので実際に表示したバッファを読みだして期待されたフレームが表示されているか確認しました。

  • 16-10c.py :testmovie.mpgを再生し、ランダムなタイミングで停止させるという作業を100回繰り返します。各再生ごとに以下の値を記録したCSVファイルを出力します。なお、 testmovie.mpgはこのサンプルのために現在表示しているフレーム番号を示すバーコードを埋め込みましたので、2011/10/14以前にtestmovie.mpgをダウンロードした方は上のリンクからもう一度ダウンロードしてください。

    • 停止させる目標時刻(sec)。
    • 最後のput_sub_imageが終了した直後の時刻(sec)。
    • 最後のswap_buffersが終了した直後の時刻(sec)。
    • 停止させる目標時刻にFPS(25で固定)をかけた値。
    • 最後のput_sub_imageが終了した直後の時刻にFPS(25で固定)をかけた値。
    • 最後のswap_buffersが終了した直後の時刻にFPS(25で固定)をかけた値。
    • 最後のput_sub_imageを実行する直前にget_frameメソッドが出力したフレーム番号。
    • 最後のput_sub_imageを実行した直後にget_frameメソッドが出力したフレーム番号。
    • 最後にswap_buffersを実行した直後のフロントバッファを読み出して埋め込まれたフレーム番号を復元した値。

ちょっと複雑ですが、確認すべきことは2点あります。まず、get_frameメソッドで得られるフレーム番号が正確にフロントバッファに読み込まれているフレームと一致しているか否かです。これを確認するためにIPythonで16-10c.pyを実行した後に変数dataのdata[:,8]とdata[:,7]またはdata[:,8]とdata[:,6]の差を見ます。

../_images/16-5-07.png

get_frameメソッドが返すフレーム番号は最初のフレームが0番のようです。一方、画面に埋め込んだバーコードは最初のフレームが1を示しているので、差が1であれば狙い通りのフレームがフロントバッファにレンダリングされていると考えられます。結果はご覧のとおり、ほぼ狙い通りのフレームがレンダリングされていますが、時々1フレームずれるようです。put_sub_imageが時間を食うからdata[:,7]との差よりdata[:,6]との差の方が正確だろうと思っていたのですが、必ずしもそうとは言えないようです。この原因を詳しく調べるにはput_sub_imageの内部を解析する必要がありそうですが、ちょっとそこまで時間をかけられません。

もうひとつ確認すべき点は、どの時点の時刻から計算したフレーム番号がもっとも実際にフロントバッファにレンダリングされているフレームを正確に反映しているかです。これを確認するためにdata[:,8]とceil(data[:,5])またはdata[:,8]とceil(data[:,4])の差を見ます。

../_images/16-5-08.png

16-10a.py、16-10b.pyでceilするとフレーム番号を8割程度正しく予想できそうだという手ごたえを感じていたのですが、実際のフロントバッファと比較するとそれほど精度は高くないようです。差が正の値になるということは、実際にフロントバッファにレンダリングされているフレームが予想されたフレームより先行しているということを意味しています。

何とも微妙な結果になってしまいましたが、とにかく1フレームのずれも許されないような精度で実験をするのであればムービーに何か目印を仕込んでおいてフロントバッファを読みだすのが確実だと思われます。そこまでの精度を求めないのであれば、get_frameで十分でしょう。ずれてもせいぜい1~2フレームと思われます。もっとも、スペックの低いPCやグラフィックチップで実行する場合は大きくずれる可能性もあります。

ずいぶんいろいろやってしまったので、補足ではなく新たに例題を立ててやるべきだったかも知れません。ここまでやると 今度はフロントバッファの内容と実際の画面はどれだけ正確に同期しているのかも気になってきました が、そこまでやるとなるとハードを組まないといけないのでここらで撤退します。