WebP Lossless はちゃんと Lossless してます


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/e6827f68-4f3e-da18-52cc-7c7c0baf34de.png)

世間には WebP Lossless が本当は Lossy(劣化) なのでは疑惑があるようで、本当に劣化するの？しないの？と気になり調べたメモです。 (2020年1月31日投稿)

結論はタイトルの通りで libwebp の古いバージョン含め Lossless(劣化しない)を確認しました。その際に気付いたいくつかの注意点も紹介します。

# はじめに注意

- WebP は 8bit 色深度のみ対応です。16bit 画像を WebP に変換すると bit 数を減らす分だけ劣化します。
- Lossless エンコーダのオプション機能に、Near Lossless があります。わずかに劣化を覚悟して圧縮率を高めます。後の方で改めて解説します。デフォルトoffです。

## 色深度 8bit, 16bit

色深度の 8bit と 16 bit の違いを図にしました。表現できる色数が段違いです。
Web 上では今でも 8bit 画像が支配的ですが、最近は 16bit 画像もだいぶ流通してきています。

| | grayscale | RGB |
|---|---|---|
|8bit|![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/0ae516db-5db4-640e-904c-59504b2c4da6.png)  2^8=256色 | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/3f193d48-23f5-5ea7-f65e-2e9bef6cf610.png) 256^3=16,777,216色(約1678万色)|
|16bit| ![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/5fd454fe-4ae9-3908-d16b-cf74fb5a9b8e.png) 2^16=65,536色 |![image.png](https://qiita-image-store.s3.amazonaws.com/0/1798/6d978dfd-be40-86b0-f188-673438c2585f.png) 65,536^3=28,147,497,6710,656色(約281兆色) |

(この記事作成時点では) Google 検索の "WebP Lossless" でトップに出てくるサイトが Lossless も Lossy(劣化する)と主張してますが、この色深度の罠にかかったケースです。

- WebP lossless might be lossy
  - https://wave.hatenablog.com/entry/2016/04/30/070900 [^1]

[^1]: 2020年6月2日に 16bitの話がある旨、追記されていました。良かった。

```
「WebPのロスレス圧縮と言っているものは、実は非可逆圧縮」ということが判明しました。
```

実験に使った画像 SOURCE.tif を(恐らくブラウザで表示できるよう) PNG 形式に変換した画像ファイルを公開してくれてるので確認できますが 16bit 画像なんですよね。

```shell:
% identify 20160429044735.png
20160429044735.png PNG 640x427 640x427+0+0 16-bit sRGB 1.55473MiB 0.000u 0:00.001
```

WebP への変換で 8bitに圧縮されるので、その差分が出ているだけです。以下のキリの良い数値 128 は、16bit => 8bit 変換の丸め誤差で説明がつきます。

```shell:
identify -format %[max] diff_im.png
128
全面真っ黒なら0が返ってくるはずであるが、128が返却された。おかしい。
```

ただ、今どき 16bit駄目なの？ と言われると確かに厳しいですし、引っかかりやすい罠なので気付きを与えてくれる良い記事です。

かくいう自分も、この記事を書いてる途中で同じ罠にハマりました。＿|￣|◯!||!

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr"> あ。。。ミス。16ビットだった。。 </p>&mdash; ＼助けよや／ (@yoya) <a href="https://twitter.com/yoya/status/1222962138005700608">2020年1月31日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 仕様

WebP は殆どの場合 Lossy 方式の方が使われていて、YUV420 サブサンプリングによる劣化のイメージが強いのですが、Lossless WebP は Lossy な WebP とは全く異なる独自の処理をしています。

- https://developers.google.com/speed/webp/docs/compression#lossless_webp
- https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification

Lossy も Lossless も同じ .webp 拡張子で RIFF コンテナの一番外側は同じですが、その内容は全く別物です。Lossless の方は PNG の圧縮方式に似ていて、更にいくつかのアイデアを持ち込んでいます。

| | |
|---|---|
| Predictor (Spatial) Transform |  グラデーションを平たく補正する。PNG でいう差分フィルタに近い。縦横斜めにそれらの間の角度で12方向+補正なしで13の予測モード |
| Color (de-correlation) Transform |  緑からの差で赤を、緑または赤との差で青を表す。JPEG 等の YCbCr と違って単純な加減算なので桁落ちしない |
| Subtract Green Transform |  緑からの差で赤と青を表す。Color Transform の特殊ケース |
| Color Indexing (palettes) Transform | 256色以下の時は自動でパレット形式にする |
| Color Cache Coding |  場所に応じてパレット色を少しずつ変更して、その差分を持つ。(適応パレット？) |
| LZ77 Backward Reference | 後方参照つき LZ77 ハフマン符号 |

値のレンジの扱いが難しくて不具合が心配になりますが、理屈上は計算誤差が入りません。
圧縮でよく採用される DCT周波数ドメイン変換とか、YUV420 クロマサブサンプリングのような劣化につながる処理もありません。

# 実装

実装といえば webm project の libwebp ですね。

- https://chromium.googlesource.com/webm/libwebp
- https://github.com/webmproject/libwebp

Lossless に該当するコードはこちらです。

- https://chromium.googlesource.com/webm/libwebp/+/refs/tags/v1.0.3/src/enc/vp8l_enc.c

cwebp コマンドの -lossless オプションで試せます。

```shell:
% cwebp -lossless input.png -o output.webp
```

尚、libwebp-0.1.99 (2012年1月)で Lossless 対応したので、0.1.3 以前だと使えません。

## Lossless 実験

では、実際に試してみましょう。
今回、libwebp 1.0.3 の cwebp コマンドを使います。

### レナさん画像

さて、みんな大好きレナさん画像で試しましょう。

```shell:
% cwebp -lossless lena_std.png -o lena_std.png.webp
File:      lena_std.png
Dimension: 512 x 512
Output:    427902 bytes (13.06 bpp)
Lossless-ARGB compressed size: 427902 bytes
  * Header size: 3802 bytes, image data size: 424074
  * Lossless features used: PREDICTION CROSS-COLOR-TRANSFORM SUBTRACT-GREEN
  * Precision Bits: histogram=4 transform=4 cache=0
% 
```

| lena_std.png  | lena_std.png.webp.png |
|---|---|
![lena_std.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/de2f1498-8371-42b5-4dc9-095cfd152a6d.png) | ![lena_std.png.webp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/73e0cdde-3df1-968b-342d-fcae13b4f7b3.png) |

見た目で違いが分からないので、ImageMagick の compare コマンドを使って比較します。

```shell:
% compare -metric PSNR lena_std.png lena_std.png.webp NULL:
inf
```

PSNR の inf は画像の完全一致を表します。
つまり、WebP との相互変換で劣化していない事を確認できました。

### パターン画像

レナさん画像がたまたま Lossless処理と相性が良い可能性もあるので、わざと色差が出やすい、かつクロマサブサンプリングで潰れそうなパターン画像を作ってみます。

```shell:
% convert -size 200x200 xc:white \
    -channel red -fx "i%2" -channel green -fx "(i+j)%2" \
    -channel blue -fx "j%2" pattern.png
```

| pattern.png | (一部拡大) |
|---|---|
| ![pattern.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/5b8888a9-8166-f85e-a45e-ca054ec31e59.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/0e655f0a-65e5-a55b-ae4b-dbc8eb17da5b.png) | 

```shell:
% cwebp -lossless pattern.png -o pattern.png.webp
(略)
% compare -metric PSNR pattern.png  pattern.png.webp NULL:
inf
```

これも大丈夫そうですね。劣化してません。

### ランダム画像

更に、念の為ランダム画像でも試しておきます。
エントロピーが高すぎると何かの値がはみ出て、元の値を維持できなくなる、みたいな可能性も気になりますので。

```shell:
% convert -depth 8 -size 200x200 xc:white -fx "rand()" rand.png
```

|rand.png|rand.png.webp.png|
|---|---|
|![rand.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/a1528026-5db8-7fd6-dc88-b43ad8a2d2b1.png) | ![rand.png.webp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/6817f2e4-b5d2-e84d-9db3-5a68afa8a9cd.png) | 

```shell:
% cwebp -lossless rand.png -o rand.png.webp
(略)
% compare -metric PSNR rand.png rand.png.webp NULL:
inf
```

ランダム画像でも完全一致しました。

尚、ImageMagick はデフォルトで 16bit画像を生成するので、テスト画像作る際には　-depth 8 が大事です。16bit画像だと WebP Lossless への変換で 8bit に変換されて、その分の差分が出てしまいます。

## 昔のバージョン

昔のバージョンの libwebp でも試してみました。 まず、$HOME/libwebp に入手できる限りのバージョンを用意します。

```shell:
% ls $HOME/libwebp/
0.1.2	0.2.0	0.3.1	0.4.2	0.5.0	0.6.0	1.0.1	1.1.0
0.1.3	0.2.1	0.4.0	0.4.3	0.5.1	0.6.1	1.0.2
0.1.99	0.3.0	0.4.1	0.4.4	0.5.2	1.0.0	1.0.3
% ls $HOME/libwebp/1.1.0/
bin	include	lib	share
% ls $HOME/libwebp/1.1.0/bin/
cwebp	dwebp
```

全バージョンの cwebp を実行します。

```shell:
% for v in `ls $HOME/libwebp/ | grep -e "\d.\d.*"` ; \
    do echo $v ; \
      $HOME/libwebp/$v/bin/cwebp -lossless rand.png -o $v.webp ; \
    done
(略)
% ls
0.1.99.webp	0.4.0.webp	0.5.0.webp	1.0.0.webp	rand.png
0.2.0.webp	0.4.1.webp	0.5.1.webp	1.0.1.webp
0.2.1.webp	0.4.2.webp	0.5.2.webp	1.0.2.webp
0.3.0.webp	0.4.3.webp	0.6.0.webp	1.0.3.webp
0.3.1.webp	0.4.4.webp	0.6.1.webp	1.1.0.webp
```

0.1.2, 0.1.3 にはまだ cwebp に -lossless オプションがなく未対応です。
変換できた分の画像を compare コマンドで比較します。

```shell:
% for f in *.webp ; \
    do echo -n "$f => " ; \
      compare -metric PSNR rand.png $f NULL: ; echo ; \
    done
0.1.99.webp => inf
0.2.0.webp => inf
0.2.1.webp => inf
0.3.0.webp => inf
0.3.1.webp => inf
0.4.0.webp => inf
0.4.1.webp => inf
0.4.2.webp => inf
0.4.3.webp => inf
0.4.4.webp => inf
0.5.0.webp => inf
0.5.1.webp => inf
0.5.2.webp => inf
0.6.0.webp => inf
0.6.1.webp => inf
1.0.0.webp => inf
1.0.1.webp => inf
1.0.2.webp => inf
1.0.3.webp => inf
1.1.0.webp => inf
```

Lossless 対応した最初の 0.1.99(2012年1月) から最新版の 1.1.0 (2020年1月) まで、一通り劣化しない(Lossless)と思って良いでしょう。

# Near Lossless

Lossless の追加オプション的な機能として Near Lossless があり、こちらはわずかに劣化します。
libwebp-0.5.0 (2015年12月)以降の cwebp -near_lossless オプションで試せます。

```shell:
% cwebp -near_lossless   0 input.png -o output-0.webp
% cwebp -near_lossless  50 input.png -o output-50.webp
% cwebp -near_lossless 100 input.png -o output-100.webp
```

指定するパラメータ値は 0〜100 の範囲。0 が一番劣化の大きな Near Lossless、100 が純粋な Lossless です。
自分は自然だと思いますが、人によっては、0 と 100 で逆を想像しそうなので要注意。

<del>ところで仕様どこ？ > Google さん</del>

## 実装

対応するコードはこちらです。

- https://chromium.googlesource.com/webm/libwebp/+/refs/tags/v1.0.3/src/enc/near_lossless_enc.c

具体的には上下左右ピクセルと比べて差が大きい場所は多少雑で良いだろうと値を量子化(切り上げ処理)します。Near がダブルミーニングっぽくて面白いですね。

- https://chromium.googlesource.com/webm/libwebp/+/refs/tags/v1.0.3/src/enc/near_lossless_enc.c#29
   -  指定されたビットで足切りします。

```c:
// Quantizes the value up or down to a multiple of 1<<bits (or to 255),
// choosing the closer one, resolving ties using bankers' rounding.
static uint32_t FindClosestDiscretized(uint32_t a, int bits) {
  const uint32_t mask = (1u << bits) - 1;
  const uint32_t biased = a + (mask >> 1) + ((a >> bits) & 1);
  assert(bits > 0);
  if (biased > 0xff) return 0xff;
  return biased & ~mask;
}
// Applies FindClosestDiscretized to all channels of pixel.
static uint32_t ClosestDiscretizedArgb(uint32_t a, int bits) {
  return
      (FindClosestDiscretized(a >> 24, bits) << 24) |
      (FindClosestDiscretized((a >> 16) & 0xff, bits) << 16) |
      (FindClosestDiscretized((a >> 8) & 0xff, bits) << 8) |
      (FindClosestDiscretized(a & 0xff, bits));
}
```

- https://chromium.googlesource.com/webm/libwebp/+/refs/tags/v1.0.3/src/enc/near_lossless_enc.c#92
  - 上下左右のピクセルと差が小さい(スムーズな)場合はそのまま。大きい場合に上記の ClosestDiscretizedArgb を呼びます。

```c:
      for (x = 1; x < xsize - 1; ++x) {
        if (IsSmooth(prev_row, curr_row, next_row, x, limit)) {
          argb_dst[x] = curr_row[x];
        } else {
          argb_dst[x] = ClosestDiscretizedArgb(curr_row[x], limit_bits);
        }
```

あと、パレット画像の時は Near Lossless を無効化して、ただの Lossless として処理します。

- https://chromium.googlesource.com/webm/libwebp/+/refs/tags/v1.0.3/src/enc/vp8l_enc.c#1084

```c:
  // we disable near-lossless quantization if palette is used.
  const int near_lossless_strength = enc->use_palette_ ? 100
                                   : enc->config_->near_lossless;
```

## Near Lossless 実験

### レナさん画像

レナさん画像では差分が出ました。

| lena_std.png | lena_std.png-near50.webp.png |lena-near-diff.png |
|---|---|---|
![lena_std.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/de2f1498-8371-42b5-4dc9-095cfd152a6d.png) | ![lena_std.png-near50.webp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/7ea4ecf7-10d4-e9ee-3378-2743f7578716.png) |![lena-near-diff.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/7db8d0d2-feb3-33b3-b519-a82fd1394ad8.png) |

全体的にうすく差分が出ています。

```shell:
% cwebp -near_lossless 50 lena_std.png -o lena_std.png-near50.webp
Saving file 'lena_std.png-near50.webp'
File:      lena_std.png
Dimension: 512 x 512
Output:    203494 bytes (6.21 bpp)
Lossless-ARGB compressed size: 203494 bytes
  * Header size: 2896 bytes, image data size: 200573
  * Lossless features used: PREDICTION CROSS-COLOR-TRANSFORM SUBTRACT-GREEN
  * Precision Bits: histogram=4 transform=4 cache=10
% compare -metric PSNR lena_std.png lena_std.png-near50.webp lena-near-diff.png
41.7624
```

ファイルサイズはだいぶ減りました。なお、 -near_lossless は 0〜100 のレンジがあり、今回は 50 を指定しています。

```shell:
% ls -lh lena_std.png.webp lena_std.png-near50.webp
-rw-r--r--  1 yoya  staff   199K  1 31 17:27 lena_std.png-near50.webp
-rw-r--r--  1 yoya  staff   418K  1 31 02:59 lena_std.png.webp
```

### パターン画像

先ほどでっち上げたパターン画像では差分なしです。さきほど説明した通り、パレット画像は Near Lossless 機能が無効になります。

```shell:
% cwebp -near_lossless 50 pattern.png -o pattern.png-near50.webp
(略)
% compare -metric PSNR pattern.png pattern.png-near50.webp NULL:
inf
```

なので、256色以下の画像だと完全一致します。

### ランダム画像

ランダム画像は差分が出ます。

```shell:
% cwebp -near_lossless 50 rand.png -o rand.png-near50.webp
(略)
% compare -metric PSNR rand.png rand.png-near50.webp NULL:
40.8382
```

ファイルサイズはそこそこ減っています。

```shell:
% ls -l rand.png.webp rand.png-near50.webp
-rw-r--r--  1 yoya  staff   77680  1 31 04:40 rand.png-near50.webp
-rw-r--r--  1 yoya  staff  120084  1 31 04:40 rand.png.webp
```

# 調査TODO

- 透明度つきも調べてみよう。
- もうちょっと色んなパターンの画像で試すべき
- グレースケールは？ ２５６色に収まるのでパレット形式になるし、色差でR,Bが常に0なので圧縮がよく効くので問題ない？ (予想)