5.32. 保存と圧縮¶
ここまでのすべてのページでは、カメラ上の画像を扱ってきました。フレームバッファに取り込んだり、MicroPythonのヒープ上に確保したり、imageモジュールのメソッドで操作したり、IDEのプレビューに表示したり、同じスクリプト内の後続ステージに渡したりしてきました。しかし、ほとんどのアプリケーションではどこかの時点でその逆の操作が必要になります。つまり、現在RAM上にある画像を、SDカード、USBホスト、ネットワークといった永続的な場所に置き、カメラ以外の何かがそれを読み取れるようにすることです。
imageモジュールはこの作業のために2つの経路を提供します。save(保存)経路は画像をファイルシステム上のファイルに書き込み、ファイル形式は拡張子によって選択され、エンコードの詳細はメソッドが処理します。to-format(フォーマット変換)経路はエンコードされたバイトストリームを含む Image オブジェクトを返し、ファイルシステムに一切触れることなくストリーミングやネットワーク呼び出しに渡すのに適しています。それぞれ異なるアプリケーションに適合しますが、どちらも内部で同じ圧縮エンジンの上に構築されています。
5.32.1. ファイルへの保存¶
save() は画像を指定したパスでファイルシステムに書き込みます。
img.save("/sdcard/capture.jpg")
img.save("/sdcard/capture.bmp")
img.save("/sdcard/region.jpg", roi=(40, 60, 200, 150), quality=85)
形式はファイルの拡張子から選択されます。認識される拡張子は5つあります。.bmp は Windowsビットマップ(ロスレス、無圧縮、取り込んだピクセルをバイト単位でそのまま)を書き込みます。.pgm は ポータブルグレイマップ(ロスレス、グレースケールのみ)を書き込みます。.ppm は ポータブルピックスマップ(ロスレス、RGB)を書き込みます。.jpg と .jpeg はどちらもJPEG(非可逆、圧縮あり)を書き込みます。受け取り側の画像は、選択したコンテナに対して既に正しいカラー形式になっている必要があります。カラー画像を .pgm として保存しようとするとエラーになります。
roi は、他のすべてのimageモジュールメソッドの roi キーワードと同じように、保存対象を画像の部分矩形に制限します。デフォルトでは画像全体が対象です。JPEG圧縮済みの画像を保存する場合、このキーワードは無視されます。なぜなら、ディスク上の形式は既にフレーム全体をカバーしており、クロップを通して再エンコードすると、既存の圧縮済みバイトを保存するという目的が損なわれるからです。
quality は 0 から 100 までのJPEG圧縮品質で、出力がJPEGの場合にのみ意味を持ちます(ロスレス形式ではこのキーワードは無視されます)。デフォルトの 50 は ほとんどの アプリケーションにとって適切なバランスです。70 から 85 はより高い視覚品質を求める範囲、30 から 50 は小さなサムネイルや帯域幅が制約された伝送に適した範囲、90 以上は画像を手作業で検査する場合や、圧縮アーティファクトに敏感な後続アルゴリズムに通す場合のための範囲です。
受け取り側の画像が返されるため、呼び出しを連結できます。img.save("/sdcard/x.jpg").draw_string(0, 0, "saved") のようにです。返されるオブジェクトはメモリ上の同じ画像であり、保存は副作用として行われます。
典型的な用途は キャプチャ&ログ パターンです。トリガーが発火し(ブロブが検出される、ボタンが押される、タイマーが経過する)、スクリプトがフレームを取り込み、ファイル名にタイムスタンプを付加し、save() を呼び出して画像をSDカードに書き出します。IDEのプレビューは動作を続け、次のトリガーが発火し、保存されたファイルが蓄積されていきます。
5.32.2. メモリへのエンコード¶
保存先がファイルシステムではなく、ネットワーク接続、シリアルポート、別のモジュールの入力である場合、アプリケーションはエンコードされたバイトストリームをディスク上ではなく メモリ上 に必要とします。to_jpeg() と to_png() はまさにそれを生成します。
encoded = img.to_jpeg(quality=80, copy=True)
bytes_to_send = encoded.bytearray()
sock.send(bytes_to_send)
デフォルトの動作はインプレース変換です。受け取り側がJPEG(またはPNG)画像に変換され、同じオブジェクトが返されます。copy=True を指定すると、変換は新たに確保されたヒープオブジェクトに書き込まれます。copy_to_fb=True を指定すると、出力はフレームバッファに格納されます。この選択肢は、他のあらゆる変換メソッドが提供するものと同じです。デフォルトではインプレース、後で元の画像が必要な場合はコピーします。
quality と subsampling は、save経路が公開するのと同じJPEGチューニングのつまみです。subsampling はクロマサブサンプリング方式を選択します。image.JPEG_SUBSAMPLING_AUTO は選択した品質に最適なものを選びます。image.JPEG_SUBSAMPLING_444 はクロマをフル解像度に保ちます(ファイルは最大、色精度は最良)。image.JPEG_SUBSAMPLING_422 と image.JPEG_SUBSAMPLING_420 は一方または両方の軸に沿ってクロマ解像度を半分にします(ファイルは小さくなり、通常の視聴距離では見えない程度のわずかな色のソフト化が生じます)。アプリケーションに特別な必要がない限り、デフォルトの AUTO が適切な選択です。
to_png() によるPNGは ロスレス ですが、エンコードが遅く、写真コンテンツに対してはJPEGより大きなファイルを生成します(写真コンテンツはPNGの予測方式では圧縮効率が悪いためです)。画像が線画、スクリーンショット、または取り込んだフレームの上に描画された硬いエッジのグラフィックを含む場合はPNGを使用してください。ロスレスエンコードが、JPEGなら柔らかくしてしまう鋭いエッジを保持します。それ以外の場合はJPEGが適切なデフォルトです。
to_jpeg() と to_png() はどちらも、他の変換メソッドが受け取るのと同じ描画スタイルの位置引数とスケールキーワード(x_scale、y_scale、roi、rgb_channel、alpha、color_palette、alpha_palette、hint)を受け取ります。そのため、同じ呼び出しでソースのスケール変更、クロップ、またはパレットマッピングされたバージョンを1ステップでエンコードできます。compress() は to_jpeg() の従来の表記であり、両者は同じ引数を取り同じ結果を生成します。
5.32.3. 圧縮で得られるもの¶
JPEG対raw(生データ)のトレードオフの裏にある数値は、一度きちんと検討してみる価値があります。
320×240のRGB565フレームは153,600バイトです(QVGAでの1取り込みフレーム)。640×480フレームは614,400バイト、1280×960フレームは2,457,600バイトです。これらはデスクトップやスマートフォンのディスプレイと比べれば大きくありませんが、合計で数MBのRAMしか持たないカメラ、書き込み帯域幅が有限なSDカード、そして通常はUSB CDC、UART、または控えめな速度のワイヤレスモジュール上で動作するホストリンクという文脈では、相当な大きさです。
quality=50 のJPEGは、写真として取り込んだフレームを通常10倍から20倍に圧縮します。すなわち、614 KBの640×480フレームは30~60 KBのエンコードされたバイトストリームになります。quality=85 では圧縮率は5倍から10倍に下がります(同じフレームで60~120 KB)。quality=10 では、アーティファクトだらけですがまだ判別可能で、圧縮率は30倍から50倍に達します(12~20 KB)。
これらの数値は、保存したフレームで実際に何が できる かを決定します。10 MB/sを維持するSDカード経路は、quality=50 のJPEGエンコードされたVGAコンテンツを毎秒30フレーム、余裕を持って処理できます(約1~2 MB/s)。同じコンテンツを非圧縮で保存するには18 MB/s以上が必要で、これはカメラのファイルシステム経路がカードに対して維持できる速度を超えています。CDC経由で1 MB/sでJPEGエンコードされたフレームを取得するUSBホストは、30~60 KBのフレームをおおよそ毎秒15~30フレーム受信します。同じレートで生フレームを取得すると、毎秒1~2フレームしか得られません。
要するに、圧縮メソッドは単に保存のための便利機能ではありません。それは取り込んだフレームを、アプリケーションが気にかけるフレームレートでカメラの外部で 使える ものにするものです。適切な圧縮を選択すること(一般的なログには品質50のJPEG、品質重視の作業には80、線画の取り込みにはPNG)は、自明でないあらゆるカメラアプリケーションにおける日常的な作業の一部です。