5.33. ImageIOストリーム

save()to_jpeg()単一フレーム のI/Oケースに対応します。アプリケーションがフレームをキャプチャし、エンコードして、どこかへ送り出すというものです。これとは別の種類のアプリケーションでは シーケンス のケースが必要になります。多数のフレームを自然なキャプチャレートで連続して記録し、後から取り出せる場所に保存し、正しい速度で再生するというものです。トレーニングデータ収集スクリプトは機械学習パイプライン向けに数百の例フレームをキャプチャし、検査ステーションのログはトレーサビリティのためにキャプチャしたすべての部品を記録し、開発スクリプトは以前にライブでキャプチャしたデータに対して新しいアルゴリズムをテストするために保存済みのシーケンスを再生します。

ImageIO クラスはimageモジュールのレコーダー/プレーヤーです。1つのストリームは Image フレーム(サイズやピクセルフォーマットが異なる場合もあります)のシーケンスを、それぞれのフレーム間隔とともに保持するため、再生時に元のフレームレートを再現できます。バッキングストアは2種類あります。ファイルシステム上のファイルか、RAM内の固定サイズのバッファです。

5.33.1. 2種類のバッキングストア

ファイルストリーム は電源を切っても記録を保持し、そのサイズはバッキングするストレージによってのみ制限されます。ストリームは16バイトのマジックヘッダー OMV IMG STR Vx.y で始まり、その後にフレームごとのチャンクが続きます。現在のライターは V2.0 を出力しますが、リーダーは後方互換性のために V1.0 および V1.1 のファイルも引き続き受け付けます。ファイルパスはコンストラクタの引数で、modeはファイルオープンモードです(既存のストリームを読むには 'r' 、切り詰めて新規に書き込むには 'w' )。

# Recording to /sdcard/run.bin
stream = image.ImageIO("/sdcard/run.bin", "w")
for _ in range(120):
    img = csi0.snapshot()
    stream.write(img)
stream.close()

メモリストリーム は構築時に割り当てられたRAMバッファ内に存在します。コンストラクタはパスの代わりに (w, h, pixformat) の3要素タプルを受け取り、 mode 引数は 事前に割り当てるフレームスロット数 になります。バッファは指定された寸法でちょうどその枚数分のフレームに合わせてサイズが決められ、いったん割り当てると 拡張できません 。最後のスロットを超えて書き込むと EOFError が発生し、スロットごとのバッファより大きいフレームを書き込むと ValueError が発生します。メモリストリームは、アプリケーションがファイルシステムを経由せずに 記録 を下流ステージに引き渡す必要がある場合(たとえばトリガーアンドリプレイのパターンに使う直近フレームの短いリングバッファなど)に適したツールです。

# Pre-allocate space for 32 QVGA RGB565 frames in RAM
stream = image.ImageIO((320, 240, image.RGB565), 32)
for _ in range(32):
    stream.write(csi0.snapshot())

圧縮ピクセルフォーマット(image.JPEGimage.PNG )の場合、スロットごとのサイズは1ピクセルあたり2ビットと見積もられます。見積もりより大きくエンコードされたフレームは書き込み時に ValueError を発生させるため、高品質のJPEGを保存しようとするアプリケーションは、スロット数を多めに割り当てるか、先に低い品質でエンコードする必要があります。

type()image.ImageIO.FILE_STREAM または image.ImageIO.MEMORY_STREAM を返すため、下流のコードは与えられたバッキングストアに応じて適応できます。

5.33.2. 記録

write() はキャプチャした Image をファイルストリームに追加(またはメモリストリームの現在のスロットに格納)し、オフセットを1つ進めます。同じ呼び出しで前回の書き込みからの フレーム間隔 も記録されるため、再生側はフレーム間で適切な時間だけ一時停止でき、記録の自然なフレームレートが保たれます。

1つのファイルストリーム内に異なる種類のフレームを混在させることができます。記録ではRGB565のキャプチャ、グレースケールの切り抜き、JPEGエンコードされたサムネイルを自由に混在させることができ、リーダーはそれぞれを元のサイズとフォーマットでデコードします。メモリストリームは同種です(すべてのスロットがコンストラクタで指定された (w, h, pixformat) を共有します)。そのため、メモリ記録は1つのフレーム構成に制限されます。

write() はストリームオブジェクトを返すため、呼び出しを連鎖できます。ファイルストリームの末尾以外のオフセットに書き込むと、ファイルの残りが切り詰められます。これは保存済みシーケンスを編集するのに便利ですが、次の書き込み位置が以前の seek() によって意図せず移動していた場合は危険です。

sync() はファイルストリームの保留中の書き込みをディスクにフラッシュします(メモリストリームでは何もしません)。記録が長時間にわたる場合、ファイルがクローズされる前にカメラが再起動して記録の末尾を失うのを避けるため、定期的に呼び出すべきです。デストラクタは ImageIO がスコープ外になったときに自動的にストリームをクローズしますが、明示的に close() を呼ぶのが適切な作法です。

5.33.3. 再生

read() は現在のオフセットにあるフレームを読み込み、オフセットを進めて、新しい Image を返します。 copy_to_fb=True (デフォルト)の場合、受信したフレームはフレームバッファに残るため、返された画像はIDEプレビューを通じて描画可能です。 copy_to_fb=False の場合、フレームはMicroPythonのヒープに配置されます。

# Loop a recorded stream at its natural frame rate
stream = image.ImageIO("/sdcard/run.bin", "r")
while True:
    img = stream.read()
    # img is now in the frame buffer; the IDE shows it
    # and the script can run any analysis it likes

2つのキーワードが再生の動作を制御します。 loop=True (ファイルストリームのデフォルト)は記録の末尾に達したときに読み取りポインタを先頭に戻すため、呼び出しが None を返すことはありません。 loop=False は記録を読み尽くすと None を返し、呼び出し側のループが終了します。 pause=True (デフォルト)は書き込み時に記録されたフレーム間隔が経過するまで呼び出しをブロックするため、再生フレームレートが元のキャプチャフレームレートと一致します。 pause=False は即座に戻るため、元のタイミングを尊重せずにできるだけ速く記録を処理したい解析パイプラインに便利です。

同じループパターンはメモリストリームでも機能しますが、 loop は無視されます。メモリストリームの末尾を超えて読み込むと EOFError が発生します。メモリリングで折り返しを行いたい場合の想定パターンは、明示的に seek() でゼロに戻すことです。

5.33.5. ホストで再生可能な記録

ImageIOストリームは、記録を カメラ上で 再生する場合に適したツールです。キャプチャしたすべてのフレームをそのネイティブなピクセルフォーマットで保持し、フレーム間隔を正確に記録するため、下流のスクリプトが損失なくステップ送り、シーク、再解析を行えます。しかし、記録を ホスト (ワークステーション、スマートフォン、Webプレーヤー)で再生可能にする必要がある場合には適していません。ホストはOpenMVのオンディスクのマジックヘッダー形式ではなく、 標準的な ビデオコンテナを期待します。

ホストで再生可能なケースには、2つの別々のモジュールが対応します。 mjpeg モジュールはMotion JPEGを記録します。これはJPEG圧縮されたフレームのシーケンスを単一のAVI形式コンテナにまとめたもので、VLC、QuickTime、ffmpeg、そして標準のWebビデオタグがそのまま再生できます。 gif モジュールはアニメーションGIFを記録します。これは非圧縮(またはパレット圧縮)されたフレームのシーケンスにフレームごとの明示的な遅延を付けたもので、アニメーションGIFを扱える任意のWebブラウザや画像ビューアで再生できます。

mjpeg モジュールは 長い 記録に自然な選択肢です。JPEG圧縮によりファイルサイズが管理可能な範囲に保たれるため(設定した品質での to_jpeg() と同程度のサイズがフレームごとに続きます)、長時間のキャプチャセッションでもSDカードの容量の範囲内に収まります。使い方は ImageIO の記録によく似ています。

import mjpeg

m = mjpeg.Mjpeg("/sdcard/run.mjpeg")
while running:
    m.add_frame(csi0.snapshot(), quality=85)
m.close()

mjpeg.Mjpeg は他の画像メソッドが受け取るのと同じ描画スタイルの位置引数とスケールキーワードを受け取るため、記録時にフレームごとにスケーリング、切り抜き、パレットマッピングを行えます。コンストラクタの width および height 引数はメイン フレームバッファの寸法をデフォルトとし、出力解像度を固定します。追加されるすべてのフレームは(アスペクト比を保ったまま)それに合わせてスケーリングされます。 sync() は長時間の記録中にファイルをディスクにフラッシュし、 close() はコンテナを確定します。正しくクローズされていないMotion JPEGファイルは再生できないため、この作法は重要です。

gif モジュールは、技術に詳しくない視聴者にそのまま共有する 短い 記録に自然な選択肢です。デモ用にキャプチャした数秒のアクション、ドキュメント用のアニメーションイラスト、チャットメッセージに埋め込むイベントクリップなどです。GIFフレームは非圧縮(または7ビットの色深度でパレット圧縮)で格納されるため、ファイルは1秒あたりMotion JPEGよりもはるかに大きくなり、数秒を超える記録にはこのフォーマットは適しませんが、結果は任意のブラウザにそのまま貼り付けられます。

import gif

g = gif.Gif("/sdcard/clip.gif")
while running:
    g.add_frame(csi0.snapshot(), delay=10)
g.close()

add_frame()delay 引数はフレームごとの表示時間をセンチ秒で指定します( 10 は1フレームあたり100ミリ秒、すなわち10fps)。これは標準的なGIF再生制御です。コンストラクタの loop キーワードは、生成されたクリップがビューアで自動ループするかどうかを設定します(デフォルトは True で、一般的な「アニメーションGIF」の期待に合致します)。

3つの記録経路で、それぞれが一般的なケースを網羅します。カメラ上での再処理にはImageIO、長いホスト再生可能な記録にはMotion JPEG、短いホスト再生可能なクリップにはアニメーションGIFです。これらの選択は 誰が記録を再生するか に帰着します。カメラ自体で動作する下流ステージはImageIOを読み込み、ホストのワークステーションやWebビューアはMJPEGまたはGIFを読み込みます。

5.33.6. トリガーアンドリプレイのパターン

有用なパターンの一つは、メモリストリームとトリガー条件を組み合わせるものです。カメラは count スロットのメモリリングバッファに継続的に記録し、毎回最も古いスロットを上書きします。トリガー条件が発火すると(ブロブがフレームに入る、動きイベントがしきい値を超える、ボタンが押されるなど)、アプリケーションはリングの内容(直近の count フレーム)をスナップショットとして取得し、SDカード上のファイルストリームに書き込みます。その結果は プリトリガー記録 となり、カメラが実際に気づいたイベントの の数秒だけでなく、 の数秒もキャプチャできます。これは単純な「トリガー時にキャプチャ」するレコーダーの古典的な限界です。

実装はストリームクラスを手にすれば簡単です。固定サイズのメモリストリームがリングとして機能し(オフセットがスロット数に達したら明示的に seek() でゼロに戻します)、メインループは反復ごとにそこへキャプチャし、トリガーハンドラはメモリストリームをフレームごとに読み出して、トリガーのタイムスタンプにちなんで名付けたファイルストリームにそれぞれを書き込みます。