一番簡単な画像フォーマット

世の中には画像フォーマットが沢山ある。データを圧縮してサイズが小さくなるフォーマットや、様々なメタデータを持てるフォーマットなど色々だが、自分で作るとなると何が一番楽だろうか。 ある意味ではsvgだろうが、今回はビットマップということにする。svgでrectを置きまくる? いやそういう趣旨ではない。

なんだろうかとは言ったが、これには確実な答えがある。PNMだ。

PNM (画像フォーマット) - Wikipedia

このフォーマットは冗談抜きに中身をエディタで表示しても画像が見える。上記リンクのWikipediaから引用すると、一番単純な白黒画像(PBM)は以下のようになる。

P1
# This is an example bitmap of the letter "J"
6 10
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
1 0 0 0 1 0
0 1 1 1 0 0
0 0 0 0 0 0
0 0 0 0 0 0

カラーのPPM形式は以下だ。 アスキー形式だとコメントを入れられるところも人間が扱うことを考慮している雰囲気がする。

P3
# The P3 means colors are in ASCII, then 3 columns and 2 rows, then 255 for max color, then RGB triplets
3 2
255
255 0 0
0 255 0
0 0 255
255 255 0
255 255 255
0 0 0

というわけでこの説明を書き始めたのだが、たらたら思いつくままに書いているとわかりにくい文章になったので系統立てて書くことにする。 まず、PNMフォーマットというのは総称で、実際には大きく分けて3つの形式があり、それぞれが二値画像、グレースケール、RGBカラーに対応している。 さらに、それらのそれぞれがアスキーとバイナリの2つのフォーマットで保存することができる。持っている情報は同じだ。

二値画像、PBM

最初に紹介した形式だ。ファイルの最初はP1\nで始まる必要がある。その後画像のサイズが横 縦の順で書かれ、その後0または1が続く。 注意すべきことは、ビットが立っているところが黒で、それ以外が白ということだ。通常255が一番明るい色なのでこれは少し混乱を生むかも知れない。

P1
# This is an example bitmap of the letter "J"
6 10
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
1 0 0 0 1 0
0 1 1 1 0 0
0 0 0 0 0 0
0 0 0 0 0 0

ところで、これは全てのPNMアスキー形式に共通することだが、改行はフォーマット識別子以外のどこで行われても許される。これはWikipediaを読んでも書いていなかったので適当なPNM画像を作って改行を入れながらビューワで見ることにした。大抵どこに入れても見ることができた。規格を参照していないのでそうあるべきかはわからないが、そうなっている方が便利なのは間違いない。このせいでファイル読み込み関数が少し面倒になってしまうのだが。

PBMのバイナリ形式はこれもまた厄介で、ビットごとにピクセルが対応している。だが、画像の一列ごとにパディングが入り、LSBは放置される。要するに上の画像をバイナリにすると以下のようになるだろうということだ。

50 31 0a 36 20 31 30 0a 08 08 08 08 08 08 88 70 00 00
P  1  \n  6     1  0 \n 以下参照
0 0 0 0 1 0 0 0 -> 0x08
0 0 0 0 1 0 0 0 -> 0x08
0 0 0 0 1 0 0 0 -> 0x08
0 0 0 0 1 0 0 0 -> 0x08
0 0 0 0 1 0 0 0 -> 0x08
0 0 0 0 1 0 0 0 -> 0x08
1 0 0 0 1 0 0 0 -> 0x88
0 1 1 1 0 0 0 0 -> 0x70
0 0 0 0 0 0 0 0 -> 0x00
0 0 0 0 0 0 0 0 -> 0x00

それぞれの行の後ろに、8の倍数になるまで関係のないビットが追加される。一行の終わりでそのような処理がされるが、今回は行ごとのピクセルが6つしかないので、全てのバイトがパディング入りになる。

ところでご覧の通り、バイナリ形式であっても画像のサイズはASCIIで保存されている。これはおそらく、整数値をバイナリで保存するときにそのサイズや符号の有無を定義するのが難しいとかそういう理由だろう。ちなみにコメントもバイナリ形式に含めることができて(今知った)、バイナリであっても#に相当するASCIIコードから\nまでは飛ばされるらしい。

グレイスケール、PGM

グレイスケールも基本は同じだ。ただ、最大値を記しておける点が異なっている。この最大値と同じ値のとき、そのピクセルは白くなる。

Wikipediaから例を引用すると、

P2
# Shows the word "FEEP" (example from Netpbm man page on PGM)
24 7
15
0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
0  3  3  3  3  0  0  7  7  7  7  0  0 11 11 11 11  0  0 15 15 15 15  0
0  3  0  0  0  0  0  7  0  0  0  0  0 11  0  0  0  0  0 15  0  0 15  0
0  3  3  3  0  0  0  7  7  7  0  0  0 11 11 11  0  0  0 15 15 15 15  0
0  3  0  0  0  0  0  7  0  0  0  0  0 11  0  0  0  0  0 15  0  0  0  0
0  3  0  0  0  0  0  7  7  7  7  0  0 11 11 11 11  0  0 15  0  0  0  0
0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0

まあ、これだけだ。今回はバイナリの時にパディングが入ることはなく、書くピクセルが1バイトで保存される。要するに255階調しか色調は存在しないのだ。一応、2バイトへの拡張は存在するが、エンディアン関係の問題があって微妙な扱いになっているように書かれている。

カラー、PPM

同じだ。Wikipediaの例ではピクセルごとに改行されているが、そうしないといけないというわけではない。

P3
# The P3 means colors are in ASCII, then 3 columns and 2 rows, then 255 for max color, then RGB triplets
3 2
255
255 0 0
0 255 0
0 0 255
255 255 0
255 255 255
0 0 0

pnm++

というわけでこれは簡単なので自分で画像を作るときはいっちょ使ってみるかと思ったのだがC++用ライブラリがさほど充実していないように見える。ちゃんと検索していないだけかもしれない。恐らくみんなその場でざーっと書いてしまっているのだろう。それでいいと思う。ただ、私もそれを一応したので、ここに設計含めて説明を載せておこうとも思う。

まず、こんなに単純なフォーマットなのだから、コードはそんなに長くならないだろうと思った。1000行行かないのではないかと(これは後で盛大に超えてしまったので正確な推測ではなかった)。なので単一ファイルのヘッダーオンリーライブラリにすることにした。これならインストールなどの問題は限りなくなくなる。コピーして置いてインクルードするだけだ。こんなに簡単なことはない。

次に、C++なのだから、速度は最低限は出て欲しい。なので、データの局所性のために画像データは1次元配列で持つことにした。それをラップして二次元配列のように振る舞わせる。at(size_t, size_t)は導入するとして、operator[]が一行分のrangeをラップしたプロキシクラスを返すことにする。

そして、それぞれの画像フォーマットから出てくるデータは、pixelクラスをtemplateとして受け取るクラスにする。writeはそれでオーバーロードして、readpixel型をtemplateとして受け取ることにしよう。

ということで作った。ライセンスはMIT、要求はC++11、STL以外の依存はなし。適当にマンデルブロ集合などを描いて遊んでいる。

GitHub - ToruNiina/pnm: pnm format(pbm, pgm, ppm) IO for modern C++ (single header only library)