WebPを解析する

WebPとは何か?

 webpとはGoogleが、JPEGより圧縮が高いと強制的に使わせてくるフォーマットである。解析するまでもなくフォーマットは仕様書に書いてある。

 圧縮形式は、動画圧縮のVP8のIフレームと書いてあるので、VP8も読まないと行けないと言う……。(Youtubeが動画圧縮に使っているのは今はVP9/AV1だが) VP8の仕様は、RFC6386にあるのだがそれは取りあえず放置する。

 WebPの仕様書はGoogleにあるのだが、単なるRIFFフォーマットだよ。実は、RIFFフォーマットはMicrosoftのWAVやAVIが使っているフォーマット形式なのである。さてはパーサーを使い回ししたな。とはいえ構造はシンプルである。AVIの様に複雑な入れ子にはなっていない。しかし仕様書が不親切で、RIFFの仕様は省略したからRIFFの仕様書を読んで補正しろという仕様になっている。

 そう言うことでWebPを実装する場合はRIFFリーダーから実装しないといけないらしい。WebPはLittle Endianを採用しており、Big Endianを採用しているPNG、JPEGと真っ向から対立したいらしい。

閑話:Little Endian と BigEndianとは何ぞや?

 メモリ上の数字の格納形式である。コンピュータはbitと言う単位で数字を記憶している。一般的なコンピュータのbitはオンとオフしかないので、要するに1と0の2つだけである(量子コンピュータは違う)。これを8つ束ねたもの1byteと言う単位で管理している。つまり、一般的には1byte=8bitである(一般的では無い場合もあるがそれは置いておく)。

 しかし1byteでは0から255もしくは-127から128までの極めて小さな数字しか扱えないので、一度に扱える数字を 16bit(2byte) 32bit(4byte) 64bit(8byte) と拡張されてきた歴史がある。

 問題は、このように複数のbyteを束ねた時にどのように数字を格納しているかだ?

 16000と言う数字を16進数に変換すると0x3E80で表現される。この数字にメモリに格納した場合、1byte目が0x3E、2byte目が0x80になると思うだろう?実はそうではないのである。この扱いは処理系により異なり、1byte目が0x80、2byte目が0x3Eになる場合もあるのだ。

 処理系により扱いが異なると言うことはファイルを扱うときエンディアンを意識しないとまともに動かないのである。

 大昔のMACが使って居たモトローラの68000シリーズは、最初で説明した0x3E、0x80の順で格納されている。大きい方から順番に格納するので、ビッグエンディアン(Big Endian)と呼ぶ。一方IntelのCPUは小さい方から格納するため0xE3 0x80になる。これをリトルエンディアン(Litte Endian)と呼ぶ。

 ビッグエンディアンとリトルエンディアン

 さらにbit格納形式……(ここはもういいやTIFFやると出てくる気がした、あと同期通信と非同期通信でbit順が逆になったわ……嫌な思い出)そのため、TIFFでは、Big endianの場合ヘッダがMMで始まり、Littele endianの場合ヘッダがIIで始まる。恐らくモトローラのMとインテルのIだろう。

画像フォーマットとエンディアン

  • ビッグエンディアン

    • JPEG

    • PNG

  • リトルエンディアン

    • BMP

    • GIF

    • WebP

  • 両方

    • TIFF(JPEGのExifを含む)

 勘弁して……。

 閑話終了

WebPのヘッダ

 WebPに何が格納できるかは一応書いてある。RIFFフォーマットの時点でなんでもありな気がする……。

  • 可逆/非可逆に両対応(JPEGも一応対応しているけど)

  • メタデータ(Exif,XML)

  • 透過率(アルファチャネル)

  • カラープロファイル(ICC Profile 今、解析途中だぞ)

  • アニメーション

 PNGとJPEGの良いとこどりしようとしている様だ。格納されているヘッダのパッケージは以下になっている

  • uint16 符号無し16bit 0-65535

  • uint24 符号無し 24bit

  • uint32 符号無し 32bit

  • Four CC 四文字のキーワード(チャンクの識別子に使う)

  • 1-based 符号

 RIFFフォーマットを使い回しているので基本的なヘッダは以下になる。

+------------------+ 0000
|     Four CC      |       4byteの識別子
+------------------+ 0004
|      Size        |       チャンクサイズ uint32
+------------------+ 0008
|     Payload      |       ペイロード(実態データ)
+------------------+ Size

 ファイルの頭は絶対にこうなる

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 0000
|      'R'      |      'I'      |      'F'      |      'F'      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 0004
|                         ファイルサイズ                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 0008
|      'W'      |      'E'      |      'B'      |      'P'      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 000C
|                いろいろなデータ                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ファイルサイズ

 つまり頭4byteを読み込み"RIFF"で始まるなら8byte目から4byteを読み込み"WEBP"なら、Webpである訳だ。ここに"WAVE"と書いてあるとwavであり、"AVI "ならAVIである

 その次に続くのは、"VP8 "、"VP8L"、"VP8X"のいずれからしい。こうなるのかな?

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 000C
|      'V'      |      'P'      |      '8'      |      ' '      | 4byte目がLだとlossless
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 0010
|                  チャンクサイズ ファイルサイズ- 12かと        |
+---------------------------------------------------------------+ 001C
|                            vp8 データ                         | 
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ファイルサイズ

 ちなみにVP8は通常の圧縮法であり、VP8Lは可逆圧縮(ロスレス)である。拡張データ(ICC Profileやアニメーション)を扱う場合はVP8Xを使う。

VP8X

 VP8X形式はこうなるらしい。

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 000C
|      'V'      |      'P'      |      '8'      |      'X'      |
+---------------------------------------------------------------+ 0010
|                   ヘッダサイズ(0A 00 00 00)                   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 0014
|Rsv|I|L|E|X|A|R|                   Reserved                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 0018
|          Canvas Width Minus One               |             ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 002C
...  Canvas Height Minus One    |     次のチャンク             | 001E
+-------------------------------+                               |
|                                                               | 
|                                                               |
+---------------------------------------------------------------+

 画像サイズを24bitで格納……嫌がらせか?

 フラグは以下の様になる

  • Rsv 予約 常に 0

  • I         ICC profile

  • L        アルファチャネルあり

  • E        Exifデータあり

  • X        XMLのメタデータあり

  • A        アニメーションあり(マルチフレーム)

  • R        アニメーション用の予約bit 常に0

 キャンバスサイズ(Width-1,Height-1) 恐らくアニメーションを実装する時のフレームサイズだろう。なお、VP8Xで始まっているが中身が無いものもある。

メタデータ

  1. カラープロファイル "ICCP"でヘッダが始まる。中身はICC Profileそのまま(画像の表示に必要なので最初の方に来る)

  2. メタデータ、"EXIF"と"XML "で始まるものがある。ちなみにEXIFの中身はTIFF。この2つのプロファイルはファイルの最後の方に置かれる(画像の表示に不要なデータ)

  3. アルファチャネル "ALPH"出始まる

  4. アニメーションフレーム "ANIM'"と"ANMF"

 ICC ProfileとEXIFの話をすると一日終わるのですっ飛ばす。アニメーションの話になると。

アニメーション

"ANIM"はアニメーションのメインコントロールらしい。正直、GIF89aのパクリだよなこれ。

 
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0000
|      'A'      |      'N'      |      'I'      |      'M'      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0004
|                  ヘッダサイズ(06 00 00 00) か?                 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0008
|                       Background Color                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0010
|          Loop Count           |    次のヘッダ                    +0012
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  1. Background Color バックグラウンドカラー透過色(アルファ値が設定されていないときに使う

  2.  Loop Count 繰り返し回数(0の場合は、永久に繰り返す)

フレームデータはANMFに入れる

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0000
|      'A'      |      'N'      |      'M'      |      'F'      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0004
|                         データサイズ                          |               
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0008
|                        Frame X                |             ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +000C
...          Frame Y            |   Frame Width Minus One     ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0010
...             |           Frame Height Minus One              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0014
|                 Frame Duration                |  Reserved |B|D|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0018
|                         Frame Data                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • 始点 Frame X, FrameY

  • フレームのサイズ Width - 1, Height -1

  • 停止時間 Frame Duration (ミリ秒で指定)

  • 混合方法 B  0=アルファを使う(混合する) 1=アルファを使わない(上書き)

  • 廃棄方法 D 0=そのまま 1=背景色で塗りつぶす (次のフレームを描画する前の処理)

  • Frame Data ここには"ALPH" + "VP8 " もしくは "VP8L"のチャンクが来ると思われる

アルファチャネル

 アルファチャネルはVP8Lでは使うなとある、理由はVP8L自体がアルファチャネルを含んでいるかららしい。

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0000
|      'A'      |      'L'      |      'P'      |      'H'      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0004
|                          Chunk Size                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0008
|Rsv| P | F | C |     Alpha Bitstream...                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +Chunksize
  • P(2bit) アルファチャネルの適用方法

    • 0b00 = そのまま

    • 0b01= レベルリダクション

  • F(2bit) 前処理の方法 どうも差分の取り方らしい。

    • 0b00(0) なし

    • 0b01(1) 水平方向のフィルタ (x-1,y)との差分 d

    • 0b10(2) 垂直方向のフィルタ (x,y-1)との差分 d

    • 0b11(3) グラデーションフィルタ (x-1,y) +  (x,y-1) -  (x-1 ,y-1) ただし、0 < dの時は0   d > 255の時は255。

    • 最終値は (d + α) % 256

  • C(2bit) 圧縮方法

    • 0b00 = なし (width * height byteのベタな情報)

    • 0b01=  WebP Lossless (VP8Lで圧縮)

  • Alpha Bitstream (先程の方法のデータが格納)

VP8

 肝の部分になるVP8圧縮の部分である。YUV420固定らしい。444など選択肢は無いようだ。4x4のブロックに分割され、さらにY16x16, U, V=8x8の単位で処理される。選択肢を減らして実装を簡単にするのとベンチマークスコアを上げるための小細工だろう。(JPEGはオプションが多すぎてテストが面倒)配信用と割り切っているので保存には向かない様である。あくまでもインターネット配信用のフォーマットなのだろう。

 圧縮にはJPEGと同じでデジタルコサイン変換を使って居る。なお、仕様はソースコードを読めと……。

 その後の処理がJPEGと異なるようである。JPEGはハフマン符号化もしくは算術符号を使う(実は特許の問題とパフォーマンスの問題がありほとんど使われて居ない)がVP8は、算術符号の一種ブーリアン・エントロピー・コーダーのみを使っているらしい。

VP8L

 ロスレスなので、ハフマン符号で圧縮し、YUVに変換せず、RGBのままで格納しいるらしい。ややこしい。このアルゴリズムは最適化しないと圧縮率が上がらない。

 仕様書はここにあった。

                     1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0000 (000C)
|      'V'      |      'P'      |      '8'      |      'L'      | 
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0004 (0010)
|                          チャンクサイズ                       |
+---------------------------------------------------------------+ +0008 (0014)
|     0x2f      |        width(14bit)       |   height(14bit)                              | 
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 
       |A| ver | bitstream data...                             |
+-+-+-+-+-+-+-+-+                                               |
|                                                               |
    ..............
|                                                               |
+---------------------------------------------------------------+ チャンクサイズ

A Alpha used flag 0 = not use RGB 1 = use RGBA
V version(3bit)

Next Transform present

 1bitを読んで、1が来たら次の2bit読む。無い場合は変換しない。

  • Predictor Transform = 0 上下左右の色から予測値を計算する(モードが14ある)

  • COLOR_TRANSFORM = 1 RGBの数値差を利用する

  • SUBTRACT_GREEN = 2 緑の値を基準にして赤と青を計算

  • COLOR_INDEXING_TRANSFORM = 3 色変換テーブルを利用(256色以下が前提で、GIFからの変換を意識したものみたい) 8ビットの変換テーブルサイズが続き、その後に変換用テーブルのデータ(24*テーブルサイズ)が続く。 16色以下の場合、4bitx2で2ピクセルを利用する。4色の場合2bitx4ピクセル、2色の場合1bitx8ピクセル。

 後に対応した画像データが続く。緑の差分を取るエンコーダの性質から画素はGRBAの順序で格納されるらしい。

画像データの圧縮と伸長

 変換後(変換しない場合も)XY=16x16のブロックを単位としてハフマン符号化する。その前に、LZ77前方参照が可能場合は行う。

 LZ77前方参照は何ピクセル前に何ピクセル同じ色が続いていると記録するもので、それを更にハフマン符号化すると簡略化したdefrate……。

 カラーキャッシュの利用。直前に出てきた色を配列に入れておき符号化する(キャッシュのサイズは2^1 ~ 2^11)

 ハフマン符号化にはCanonical Huffman codeを用い、五種類のハフマンテーブルが用いられる。

  1. 緑、後方参照長、カラーキャッシュ

  2. アルファチャネル

  3.  後方参照距離(ほぼ固定テーブル)

 1のテーブルには緑の画素 (0-255) 256- 279(0-23) までが後方参照長、280以上がカラーキャッシュの順でハフマン符号化されていると言うことになる。

 この仕組みはGIFに似ていて(GIFはLZW) 要するにLZWの間にLZ77を差し込んでいる。しかし制御はややこしい。

  1.  255以下の場合は緑のデータとして処理して、続いてRed Blue Alphaのデータを取得する

  2. 256-279、この数字はビット長のみが入っているので、そのビット長を読みこむ。ビット長と続けて読んだデータから数が計算できる。後方参照距離テーブルを使い、距離を読み込み、その分の色を埋める。

  3. 280以上の場合、-280し、カラーキャッシュのテーブルからデータを取り出す。

<spatially-coded image> ::= <meta huffman><entropy-coded image>
<entropy-coded image> ::= <color cache info><huffman codes><lz77-coded image>
<meta huffman> ::= 1-bit value 0 |
                   (1-bit value 1; <entropy image>)
<entropy image> ::= 3-bit subsample value; <entropy-coded image>
<color cache info> ::= 1 bit value 0 |
                       (1-bit value 1; 4-bit value for color cache size)
<huffman codes> ::= <huffman code group> | <huffman code group><huffman codes>
<huffman code group> ::= <huffman code><huffman code><huffman code>
                         <huffman code><huffman code>
                         See "Interpretation of Meta Huffman codes" to
                         understand what each of these five Huffman codes are
                         for.
<huffman code> ::= <simple huffman code> | <normal huffman code>
<simple huffman code> ::= see "Simple code length code" for details
<normal huffman code> ::= <code length code>; encoded code lengths
<code length code> ::= see section "Normal code length code"
<lz77-coded image> ::= ((<argb-pixel> | <lz77-copy> | <color-cache-code>)
                       <lz77-coded image>) | ""

 上読んだ方が速いな……。



この記事が気に入ったらサポートをしてみませんか?