.. title:: Pythonで心理実験 - 例題17-3 例題17-3:複数のWebカメラを同時に使う ============================================ **A:** さて、最後は気楽に遊びますか。予告通り複数台のwebカメラを同時に使ってみる。もうちょっと高いパフォーマンスが出るかと思っていたけどちょっと期待外れだった。一部まだ解説したことがない機能を使っているが、基本的なポイントは解説済みなものばかりだから多分何も難しいことはないと思う。 **B:** うっ、いきなりプレッシャー。 + 行番号なしのソースファイルをダウンロード→ `17-5.py `_ .. literalinclude:: source/17-5.py :language: python :encoding: shift-jis :linenos: :lineno-match: **A:** このサンプルでは3台のwebカメラを同時に使用する。動かしてみたい人はぜひなんとか3台のwebカメラをかき集めてください。例によって画面に現在のフレーム番号が表示されるので、3台とも画面に向けて撮影してみるか。Bくんちょっと手伝って。 **B:** はいはい、こっちのハブにつないだらいいですね? **A:** あー、それはPC本体のフロントパネルのUSBポートにつないでくれたまえ。理由は後で説明する。 **B:** えっと、じゃあこっちにまわして…、ああっ、ひっかけて倒しちゃった。 **A:** ずぼらせずに一度こっちから通したまえ。こうやって…と、まあこんなもんだろ。 **B:** じゃあ実行しますよー。それっ。 **A:** うーん、もうちょっとカメラを近づけないと数字が読めないな。まあ今回はこれでいいか。 **B:** あれ、なんだか妙に数字がゆっくりしていませんか? **A:** その理由も説明するよ。適当なところでストップして、保存された動画を再生して。適当なところで一時停止してくれ。 **B:** はいはい。…えーと、こんなもんかな? .. figure:: img/17-3-01.jpg **A:** このサンプルでは3台のwebカメラから画像を取得して、動画の左上、右上、左下に配置している。右下はVisionEggのスクリーンバッファを読みだして縮小して配置している。要するに、今まさにPCからディスプレイに送信された画面の内容のコピーだ。 **B:** へええ。こんなこと出来るんだ。でもこれ、カメラ画像とずいぶん数値に開きがありますよね。カメラ画像に映ってる値は569に見えるけど、右下は573だ。 **A:** PCからディスプレイにデータが送信されて、それが実際に表示されて、さらにそれがカメラに撮影されて、PCに送られてきて、って経路をたどるとそれだけラグがあるってことだな。別にこういう事を調べたくて始めた企画じゃないんだけどなかなか興味深い。 **B:** ディスプレイは60fpsだから4フレームのずれは…、ええと、66msくらいですか。どの時点で一番時間を食っているんですかね? **A:** さあ? それがわかったら面白いがそこに真っ向勝負を挑む根性も機材も私ぁ持ち合わせていないな。気が向いたら少し突っつくことはあるかも知れんが。 **B:** うーん、すごい世界だなあ。 **A:** プログラムの解説を少し。15行目から20行目はカメラの初期化。VideoCapture.Device()の引数を0、1、2と順番に指定するとPCに接続されているwebカメラを順番に開いていく。どのカメラが何番目になるかはOSが決めるんで動かしてみないとわからん。戻り値のVideoCapture.Deviceのインスタンスを後で使いやすいようにリストに放りこんである。 **B:** try - except文は? **A:** このプログラムでは最終的に保存する動画ファイルを640x480にしていて、個々のカメラ画像は320x240にしている。カメラから取得する時点で320x240に設定できれば帯域幅を節約できるんでぜひ設定しておきたいところだが、カメラによってはこの解像度をサポートしていない場合がある。そこでエラーが起きたときに止まってしまわないようにtry文を使っているわけだ。 **B:** え? 解像度の設定に失敗してもそのまま先に進んでいいんですか? それから帯域幅っていうのは? **A:** 一気に質問するな。まず解像度の設定に失敗した場合だが、単にカメラの標準の解像度でデータが送られてくる。そういう事態が生じた場合を考慮して60行目から62行目で解像度の変更処理をしているんだが、その話はまた後で。ええと、もう一つは… **B:** 帯域幅。 **A:** 帯域幅か。要するにデータを転送するときに、どのくらいのペースでデータを転送できるかって話だ。水を流すのと同じだな。風呂の水を入れ替えるときに、栓を抜いても排水溝の幅が限られているから一定のペースでしか水は流れない。 **B:** 風呂の水が減ってきたら水圧が下がって一定のペースじゃなくなると思うんですが。 **A:** うるさい。とにかく水道には一定時間にこれだけしか水を流せませんっていう限界がある。PCにおけるデータ転送も同じこと。webカメラがデータ転送に使うUSB2.0の帯域幅は理論上480Mbpsだ。 **B:** ふーん、帯域幅っていうのか。覚えておこう。 **A:** うーん、習慣的に帯域幅って言うんだけど、どうもあまり適切な使い方ではないようだからただ覚えられても困るかな。 **B:** へ? **A:** いや、脱線がひどくなるから詳しくは説明しないけど、この辺りの言葉使いは結構難しいんだよね。繰り返すけど習慣的に帯域幅と言うんでここではそう言っておくけど、よかったら自分で調べてみてくれ。 **B:** んー。習慣的に使うんならぼくはそれでいいです。 **A:** そうか。んで帯域幅だが、USB2.0は480Mbps。bpsはbit毎秒のことなんで、byteに直すと60MB/s。今回使用したカメラは全部640x480が標準解像度なんだが、640x480で1ピクセル4byteだとすると640x480x4=約1.23MB(1k=1000で計算)。30fpsなら1秒あたりこれを30回送らないといけないからx30で約36.9MB/s。カメラが3台あるんだからx3で約111MB/s。 **B:** あれ、60MB/sを超えちゃったじゃないですか。 **A:** そう。超えるんだよ。だから640x480、30fpsのカメラ3台の画像をUSB2.0で転送するのは無理なんだよ。 **B:** でもサンプルプログラムは動いてるんですよね、なんで? **A:** だからそのために解像度を320x240にしたって言ってるだろ。340x240なら単純計算で640x480の1/4になるから111÷4≒約27.7MB/s。実際には今回使ったカメラのうち1台は320x240に変更できない機種だからそいつは約36.9MB/s、残り2台はそれぞれ約9.2MB/s、全部合わせて55.3MB/s。 **B:** おおー、ギリギリでしたね。 **A:** 実はこの **USB2.0の最大転送速度はホストコントローラー1つあたりの数値なので、複数のホストコントローラがあるPCなら別々のホストコントローラーに接続すればさらに多くのカメラを接続することが出来る** 。 **B:** USBのホストコントローラー? なんですかそれ。 **A:** PCの内部に搭載されているLSIだ。デバイスマネージャでこんな風に確認することが出来る。「詳細設定」を見たら実際にどれだけ帯域幅を食っているかもわかるぞ。 .. figure:: img/17-3-02.png **B:** へえ、こんな画面初めて見ました。webカメラは1%しか使っていませんが。 **A:** 今は撮影してないから帯域幅を使ってないのは当たり前だろ。 **B:** あ、そうか。それにしてもシステムにより予約済みってのが20%もあるんですね。ということは60MB/sを全てwebカメラには使えないということ? **A:** その通り。恐らくwebカメラ自身も画像データ以外にデータをやり取りしているはずだから、さらに画像データ転送に使える帯域幅は狭くなる。 **B:** じゃあさっきの55.3MB/sってのはオーバーしてるんじゃないんですか? 60MB/sの20%は12MB/sで、20%はシステムが使っちゃているんですよね? **A:** だからさっきwebカメラをつなぐときに、どこにつなぐか指示しただろ。このPCはバックパネルのUSBポートとフロントパネルのUSBポートはそれぞれ別のホストコントローラーに接続されている。複数のホストコントローラーに分散させたんだよ。 **B:** なるほど、そんな配慮が。 **A:** 他にもカメラによってはカメラ内部で動画を圧縮して転送するデータのサイズを小さくして、PC側で復元してってのをやっている。1920x1080で30fpsのフルハイビジョン動画を撮れるwebカメラなんかも販売されているが、同じように計算したらそれだけで248.8MB/sだから絶対にUSB2.0じゃ転送できない。 **B:** うへえ、暴力的な数字だな。 **A:** 今回は簡単に解説を済ませるつもりだったが長くなりそうだなこりゃ。とにかくそんなわけで320x240に設定できるカメラであればそう設定しておこうというのが15から20行目。22行目は各カメラの画像を張り付けるためのImage.Imageのインスタンスを生成している。あとは大体前回と同じなのですっ飛ばして、60から64行目。ここのポイントはImage.Imageのメソッドpasteとresize。getImageの戻り値もImage.Imageのインスタンスなんだから、Image.Imageのメソッドであるresizeをそのまま使える。これで確実に320x240にしておいて、貼り付け先のImage.Imageインスタンスのメソッドpasteに渡す。pasteは貼り付け先の画像から実行して、引数に貼り付け元の画像と貼り付け位置を示すタプルを指定する。他にも指定できる引数があるけどそれはhelpを見てくれ。 **B:** ええと、boxっていう引数が貼り付け位置ですか? **A:** あ、失礼。その通り。 **B:** 60行目から62行目で3台のカメラの画像をそれぞれ貼り付けているんですね。で、63行目は? **A:** このget_framebuffer_as_imageは :doc:`例題8-3 <08-3>` で一度出てきているんだが、まあ覚えていないよな。VisionEggで現在表示している画面のバッファを読みだすメソッドだ。戻り値はImage.Imageのインスタンスなんで同じようにresizeしてpasteすればよい。以上で解説はおしまい。 **B:** ふう、なかなか大変でした。 **A:** …その様子だとすでに忘れているな? **B:** へ? 何をです? **A:** なぜVisionEggで表示している現在のフレーム番号がゆっくりなのかってことだよ。 **B:** ああ! すっかり忘れていました。 **A:** ここまでの話が長かったからな。フレーム番号がゆっくりになってしまう理由は単純。動画ファイルへの書き出しの作業が間に合っていないからだよ。log.csvを開いて2行目以降のタイムスタンプの差分を見てみたまえ。 **B:** ええっと、もうExcelでいいですよね。ごにょごにょ。 **A:** …。 **B:** あれ? なんだかフレームの間隔が短いのと長いのが交互に並んでいますよ? 短い方はだいたい0.03だから、長い方が遅れているのか。 **A:** そう。理由はわかるか? **B:** 2フレームに1回書き出しをしているんだから、書き出しが原因? **A:** 何で今回の書き出しにはそんなに時間がかかるかわかれば合格。 **B:** え? えーと、なんだろう。カメラがたくさんあるから? **A:** うむ。確かにカメラの台数が増えるとそれだけ処理は重くなるが、ここでの犯人はget_framebuffer_as_image。こいつが猛烈に時間を食うんだ。 **B:** へー。そうなんですか。 **A:** ここで考えられる対策が二つ。第一は、動画のフレームレートを下げること。今は30fpsでやっているが、60の約数なら簡単に変更できるんだから15fpsとか10fpsにする。第二はget_framebuffer_as_imageを使うのを諦める事。実験に使う場合、実際に出しているVisionEggの画面そのものを保存しておけるというのはメリットだが、今どんな刺激が出ているかわかればいいって用途も多いだろう。そんな場合は、動画のどこかにメッセージを書き込んでおけばいい。 **B:** そんなことが出来るんですか。 **A:** PILのImageDrawを使えば簡単なことだ。そんなわけで書き直したサンプルがこれ。 + 行番号なしのソースファイルをダウンロード→ `17-6.py `_ .. literalinclude:: source/17-6.py :language: python :encoding: shift-jis :linenos: :lineno-match: **A:** ほとんど変わっていないんであまり解説することはない。まず3行目でImageDrawをimport。ImageDrawは :doc:`例題8-2 <08-2>` 、 :doc:`8-3 <08-3>` で出てきているが、Image.Image形式の画像に図形や文字を描画するためのモジュール。23行目でメッセージ出力用の小さなImageを作っておいて、24行目でImageDraw.Drawクラスのインスタンスを生成して書き込み準備を済ませておく。詳しくは例題8-2を見てほしい。いや、例題8-2は大して詳しくないので検索する方がいい。 **B:** ちょっとは自分で解説しましょうよ… **A:** (無視して)26行目からはffmpegの起動コマンドだが、ちょっと次の布石としてMPEG1じゃなくてH.264で保存するように書き換えている。細かいパラメータはググって見つけたページから拝借したものだけど、どのページだったかわかんなくなってしまった。 **B:** (はーっ) **A:** で、後は72行目。メッセージ出力用のミニ画像に黒い長方形を描いて塗りつぶしている。こうしないと前に出力したメッセージが残っちゃうからね。rectangle()の引数はまあだいたいわかると思うんだけど、最初のタプルは長方形の左上、右下のX座標、Y座標を順番に並べたもの。fillに与えている文字列は塗りつぶしに使う色の指定。HTMLを知っている人はすうぐピンと来たと思うけど、R、G、Bの順に16進数で明るさを指定する。赤なら#FF0000、白なら#FFFFFF。黄色なら#FFFF00ってな感じ。 **B:** HTMLの色指定はぼくでも知ってます。 **A:** で、どんなメッセージを書くかなんだけど、ここでは単に現在のフレーム番号を書き込んでいる。最初の引数は位置を示すタプル、続く引数は書き込む文字列。 **B:** なんかフレーム番号ばかりで飽きてきましたね。 **A:** むっ。動作確認だからこれでいいんだよ。さて、動かしてみるぞ。 .. figure:: img/17-3-03.jpg **B:** おお、字が大きくて読みやすくなった。右下の字は小さいけど。 **A:** ImageDraw.Draw.textで文字の大きさを指定するのはImageFontっていうモジュールを使わないといけなくてちょっと厄介なんだよ。今回はPython2.7で実行しているわけだが、Python2.7に対応しているPIL1.1.7のバイナリ配布版がImageFontに対応していないんだ。まあ勘弁してくれ。 **B:** いろいろ面倒ですねえ。 **A:** ちょっと気になるのは右下とそれ以外の画面で9フレームもずれているところ。理由がよくわからない。さっきも言ったけど今はこの問題に踏み込む気力も機材もないので「面白いなあ」で済ませておく。 **B:** 「面白いなあ」って、実験に使う人は面白いどころじゃないでしょ! **A:** ま、複数台のwebカメラの内1台はディスプレイそのものに向けておくんだね。3台のカメラ画面はほぼ同期しているので、それで刺激と参加者の行動の対応をとれば十分だろう。 **B:** うーん。すっきりしない。 **A:** 最後にもう一つ遊びを。今度は動画のfpsを下げて、その分解像度を上げてみる。 + 行番号なしのソースファイルをダウンロード→ `17-7.py `_ .. literalinclude:: source/17-7.py :language: python :encoding: shift-jis :linenos: :lineno-match: **A:** 主な変更点はまず14から17行目。前回はカメラの解像度=動画の解像度を縦横それぞれ1/2だったが、今回はカメラを640x480、動画を1024x768にしている。この変更に対応させるためにあちこちいじっているがそれは省略。本当は動画の解像度を1280x960にしたかったんだが、その辺は後で。 **B:** どうせうまくいかなかったんでしょ。 **A:** 後でって言ってんだろ。32行目、ffmpegに与えるオプションでフレームレートを指定するオプション-rに10を指定、10fpsの動画を作る。17-5.pyまで使っていたMPEG1では10fpsを指定できないのでH.264に切り替えたというわけだ。 **B:** なるほど。 **A:** 10fpsでの書き込みに対応するために、84行目のifは「6で割った余りが0」という条件に変更されている。これで6フレームに1回、すなわち60フレームに10回の書き込み。フレームは60fpsなんだから書き込みは10fps。 **B:** ふむふむ。 **A:** あとは特に意味はないけどグレーティングを画面に表示してみた。それで、60フレームに1回乱数でグレーティングの運動方向を決定して、運動方向を文字列として動画に出力している。まあ遊びだな。グレーティングは :doc:`例題16-1 <16-1>` を参考のこと。後はほぼ17-6.pyと同じ。以上、解説終わり。 **B:** じゃあ動かしてみますよー。 .. figure:: img/17-3-04.jpg **A:** 本来は1024x768のサイズなんですがこのサイトのレイアウトだとはみ出るので縮小しました。縮小すると右下の文字が読めなくなっちゃったので原寸大まで拡大したものを赤枠の中に示してあります。 **B:** おお、left/rightって切り替わるのが面白いですね。冷静に考えたら特に面白くもないんだけど、ずっとただ数字だけだったから面白く感じてしまいます。 **A:** もうちょっと優しい言い方はないのか。 **B:** えー。Aさんへの愛情があふれてると思いませんか? **A:** 愛ねえ。愛があるならとりあえず食い物を買ってきてほしいな。さすがに空腹でしゃべる気力までなくなってきた。 **B:** あ、そういえばAさん昨日からずっと食べてないんでしたっけ。じゃあ何か買ってきましょう。 **A:** …その手は? **B:** お駄賃は? **A:** …。こんだけ渡しておくから、学生室のおやつも一緒に買ってくること。おつりはおやつ募金箱へ入れておくこと! **B:** へーい。何でもいいですかあ? **A:** なんだか激しく糖分が足りない感じなんで、甘い物買ってきてくれ。 **B:** 了解~。(ガラガラ ピシャッ) **A:** ふう。さすがに疲れたな。ええと、読者の皆様。今回は特に中途半端な内容ですけど、それほど時間的な正確さを必要としないのであればこのwebカメラ複数台同時記録ってのはそこそこ使えるのではないかと思います。Core i7 920のマシンで17-7.pyを実行するとちょっと処理落ちが生じるので、かなりのハイスペック機じゃないと解像度の高い動画の保存は難しいと思われます。640x480か800x600程度の動画にして、USBホストコントローラーの問題に気を付ければ、4台同時くらいはいけるんじゃないでしょうか。お役にたてば幸いです。それでは、今回はこれにて。 補足 ~~~~ 17-5.pyや17-6.pyを実行中のPC画面を上に掲載した写真のように複数台のwebカメラで撮影すると、動画の同じフレームに合成されたカメラの画像に異なるフレーム番号が写っている場合があります。これは仕方のないことなのですが、なぜ仕方がないのか直観的にわかりやすいように補足しておきます。 例えば30fpsのカメラであれば、1000ms/30frame=33.3msに一回撮影を行います。いつ撮影を行うかは個々のカメラが決定していているので、下図のカメラ1、カメラ2のようにタイミングがずれている可能性があります。このような状態になると、カメラ1とカメラ2が撮影した画像に写っているフレーム番号がずれてしまいます。また、カメラが撮影を行うときには非常に短い時間センサーを露光しますが、下図のカメラ3のようにこの時間帯にPCの画面更新が重なると前後のフレームが重なってぼけた画像になってしまいます。感度があまり高くないセンサーを使っているカメラは露光時間が長い傾向にありますし、撮影状況に応じて露光時間を変化させる機能がある機種では撮影中に露光時間が変化して途中から下図カメラ3の状態になってしまうこともあります。 .. figure:: img/17-3-05.png