PNGデコーダの調査結果:ESP8266でのデコードは厳しいかも

ESP8266でJPEGGIFがデコードできましたので、PNGも…と思い、仕様や既存のライブラリを調査していました。
その結果をここでまとめておきます。

結論としては、ESP8266でのPNG画像のデコードは不可能ではないものの、RAMの消費量や現時点で使えるデコーダライブラリの状況からは、JPEGやGIFに比べると課題が多いと言えます。

まず、PNGの圧縮方法について触れておきます。
PNGは、原理的には「ビットマップ画像をZIPで圧縮した」イメージに近いです。
圧縮アルゴリズムはDeflateと呼ばれているもので、ZIP、gzip等で使われているものと同じです。
Deflate圧縮は、出現頻度の高いビット列に短い符号を割り当てるハフマン符号化と、過去に既出のビットパターンを参照することでデータの繰り返しを圧縮するLZ77を組み合わせたものです。

Image compression
Deflate
SWFバイナリ編集のススメ番外編 (zlib 伸張) 前編 | GREE Engineers’ Blog
SWFバイナリ編集のススメ番外編 (zlib 伸張) 中編 | GREE Engineers’ Blog

次に、Deflate圧縮の特徴ですが、過去に既出のパターンがあったかどうかを調べるための辞書が必要となります。
どれくらい過去までさかのぼるか(スライディングウインドウ)で辞書のサイズが決まってきますが、PNGではこのサイズは最大32768となっています。
従って、デコード側では32768バイトのデータを保持しておく必要があります。
これだけでRAMのデータ用領域を半分近く消費してしまいますので、ESP8266のメモリ内でのデコード処理はなかなか困難です。
現実的にはファイルなどに書き出して過去のデータを参照可能にしておく必要がありそうです。

また、PNGは単に画像をDeflate圧縮するのではなく、スキャンライン単位で前処理を適用することで、圧縮率を高めています。
前処理は可逆変換で、ピクセル間の差分のみを記録する方法が4通り用意されています。

PNG画像を自力で読む

この前処理は、デコード時に逆の処理を行って元の画像を復元しなければなりませんので、最低でも1スキャンライン分は、画像をバッファしておく必要があります。

ということで、ESP8266でオンメモリでPNG画像をデコードするのは困難ではないかと思われます。
SPIFFSなどの外部記憶を使う必要があるでしょう。

次に、既存のPNGデコーダを調べてみました。

標準のデコーダはlibpngで、Deflate圧縮にはzlibを使用しています。

PNGをデコードするのに必要なファイルだけをlibpngとzlibから抽出し、ARM系のマイコンであるSTM32F4で動作させた例が以下で紹介されています。

ねむいさんのぶろぐ | STM32F4シリーズを使ってみる5 -libpngを実装する-

libpng+zlibはESP8266で使うにはやや大規模すぎます。
zlibのcontribフォルダに、puffというより小規模なDeflateの実装があります。
puffはブートローダなど、より制約の厳しいソフトウェアで使うことが想定されています。
ただし、提供されているAPIはデータを一度に全て解凍しますので、画面全体を納めるバッファが必要です。
組み込みで使うには、漸進的に解凍するような処理を自分で書く必要があるでしょう。

zlib/contrib/puff at master · madler/zlib

puffはzlibよりもスピードは遅いですが、これでPNGデコーダを作ることもできます。
そのようなデコーダの例としてlpngがあります。
ただし、puffをそのまま使っていますので、復号後の画像を保持できるだけのバッファが必要になります。

apankrat/lpng

zlib以外のDeflateアルゴリズムの実装、特に伸張部分のみの実装の一覧がWikipediaにあります。

DEFLATE – Wikipedia, the free encyclopedia

この中で、組み込み向けと思われるものにminizがあります。
画像をPNGファイルに書き出す関数は組み込まれていますが、PNG画像を展開する関数は実装されていません。

miniz – Single C source file Deflate/Inflate compression library with zlib-compatible API, ZIP archive reading/writing, PNG writing – Google Project Hosting

tinf(Tiny INFlate)も小さなDeflate(からの解凍)の実装です。
他のものと同様、圧縮されたビット列と展開したビット列がオンメモリであるという前提の実装です。
ただ、これくらい短いコードだと、漸進的に解凍するように変更することも容易かもしれません。

jibsen / tinf — Bitbucket

zlibに依存しないPNGデコーダ・エンコーダ実装としては、LodePNGがあります。
内部的に、メモリを確保するためにmallocを使用していますので、そのままでは小規模な組み込みシステムで使用するのはちょっと苦しそうです。

LodePNG

LodePNGのページの中に、さらに小規模なデコーダ実装であるpicopngも紹介されています。
こちらは、メモリを確保するのにSTLのVectorを使用しています。
STL自体は、Arduinoにも移植されていますが、picopngを使用しても、一時的に大量のRAMが必要になるという問題自体の解決にはなりません。
picopngはメモリのフットプリントが小さいわけではなく、1つのファイルに短くまとまったポータブルなデコーダと見るべきで、必ずしも組み込みに向いているわけではなさそうです。

lodev.org/lodepng/picopng.cpp
maniacbug/StandardCplusplus

LodePNGはエンコーダも含んだライブラリですが、デコーダ部分だけを抜き出したupngという派生系があります。
こちらはSTLも必要なく、picopngよりも組み込み向きかもしれません。
とはいえワークメモリ確保にはmallocを使っていますし、全体を一度にデコードするので一時的に大量のRAMが必要になる点も違いはありません。

elanthis/upng

他には、以下のページで日本の方がC++で作成されたエンコーダ/デコーダの実装が公開されています。
こちらは、入力データはバッファへ全て読み込み、デコードは1バイトずつ出力する形態になっています。

YSFLIGHT.COM – PNG Encoder / Decode in C++

という感じで、いろいろデコーダを眺めてみましたが、PNGのデコード処理が

 ハフマン符号の列(不定長の符号)→ピクセル列(復元前)→元画像の復元

というステップを辿るため、基本的にはこれらのステップを直列的につないだデコーダばかりでした。
そのため、元画像の復元前の段階で、画像全体をメモリ上に保持するような内部構造になっています。
実際には復元処理には、1つ上のスキャンラインを保持していれば十分なはずですので、上の3ステップは並列に動けるはずです。
libpngにはそのような実装の解説もされていますが、より小さなデコーダでProgressive Decodeを実装したものは見当たりませんでした。

Reading PNG Images Progressively (PNG: The Definitive Guide)

Deflate圧縮・伸張自体は、データを読みながら出力することは可能です(でなければ巨大なファイルを圧縮・伸張できません)ので、PNGの展開でも同様のことは可能だと思います。
次のリンク先のコードは、zlibを使った、読みながら出力するプログラムの例です。

https://oku.edu.mie-u.ac.jp/~okumura/compression/comptest.c

以上、結局ESP8266で使えそうなPNGデコーダは見つかりませんでしたが、もし実装するとしたら以下のような形態になるのではないかと思います。

・Deflateされたデータを解凍した結果は、一時ファイルに保存する。解凍処理時に32768個前までのデータを参照できることが必要で、ESP8266には十分なRAMが無いため。

・解凍したデータを表示する前に、差分情報から原画像を復元する。これは全データを解凍後に一時ファイルから復元しても良いし、解凍中に平行して行うこともできるはず。

オンメモリでデコードすることも不可能ではないと思いますが、実用性という点では疑問符が付きます。
PNGが登場したのは1990年代後半ですから、PCには既に数十MBのRAMが搭載されていました。
GIFやJPEGの登場時よりも、10倍くらい潤沢なメモリが使えたことになります。
さすがに、100KBに満たないRAMでデコードする場合のことはあまり考慮されていなかったのかもしれません。

コメント