WebPとJPEGの圧縮はどこが違うか

WebPGoogleが開発した静止画ファイルフォーマットです。 圧縮形式としては大きく2種類あって非可逆圧縮のLossy WebPと可逆圧縮のLossless WebPです。 用途で考えるとLossyはJPEG相当、LosslessはPNG相当と言えます。 どちらにしても拡張子は.webpが使われるので紛らわしいですが、使われている圧縮アルゴリズムはまったく別物になっています。

ここでは公式サイトにあるLossy WebPとJPEGの比較サンプルの画像を観察して、どこが違うのか見てみた結果を紹介します。 この公式サンプルは5つのPNG画像をそれぞれJPEGとWebPに変換したものですが、WebPのファイルサイズがちょうどJPEGの3割減になるように圧縮オプションが調整されています。 つまり「同じ画質でファイルサイズが3割減」というデモではなく、単純に3割減にした場合の画質比較デモなので、その点はちょっと注意が必要です。 おそらく「3割減にしたけど画質は変わらないでしょう?」と言いたいのだと思います。

JPEGもLossy WebPも内部的にはYCbCr色空間を用いて圧縮しているので、YCbCrヒストグラムを比べてみました。

f:id:tshino:20190501045745p:plain
YCbCrヒストグラムの比較

図は左から、ソースPNGJPEG、WebP。ヒストグラムは上からY、Cb、Crの順です。 JPEGのCbとCrが細かい「くし形」のヒストグラムになっているのはJPEG圧縮でよく見られる特徴で、量子化の影響だと思われます。 WebPの方はY成分にやや幅の広いくし形というか周期的な凹みがあるのを発見しました。これは他の画像サンプルについても同様でした。

この周期的な凹みは何なのか?

どうやらLossy WebPでは256段階の輝度の階調を保つことは出来ず、飛び飛びにしか表せないという制約があるようです。 次の図は、このことを確かめるために0から255までの階調を均等に含むようなグレイスケール画像を最高画質で作り、その輝度ヒストグラムを表示したものです。Lossy WebPではこのように滑らかな階調を表現することには限界があるようです。

f:id:tshino:20190501050051p:plain
グレイスケールの輝度ヒストグラム比較

次に画像のどの部分が実際に違うのか、桜の画像を拡大して目視で比較しました。

f:id:tshino:20190501050206p:plain
桜の画像の拡大

青空の部分では、ソースPNGにあった細かい濃淡がWebPではきれいさっぱりなくなっています。 それが良いのか良くないのかは場合に依るでしょうが、ファイルサイズの削減には効いている部分だと思います。 また、JPEGでは木のまわりの空の部分にJPEG特有の「モスキートノイズ」が現れていますが、WebPではそれがほとんど見られないことも分かります。 どんな圧縮アルゴリズムでこうなっているのかは(詳しく調べていないので)分かりませんが、モスキートノイズが出にくい特性があるようです。

次の図は湖の画像を拡大して、YCbCr色空間の各チャンネルをグレイスケールで表示してみたものです。

f:id:tshino:20190501050346p:plain
湖の画像の拡大
f:id:tshino:20190501050436p:plain
YCbCrのチャンネル画像の比較

チャンネル別の図は上から元のカラー画像、Y成分、Cb成分、Cr成分です。 普通にカラー画像で見ても圧縮によるブロックノイズが目立つ部分ですが、チャンネルに分けてみるとさらに凄いことになっているのが分かります。 CbやCrが大きなモザイク状になっていて、かなり圧縮率に寄与していそうです。

まとめ

Lossy WebPとJPEGの画像のどこがどう違うのか、公式サンプルを観察して気づいた点を紹介しました。 ただ、この公式サンプルはかなり高圧縮率(JPEGのquality値は80)、言い換えると低画質の場合の比較になっているため、それほど圧縮せずに高画質の写真を表示するような場合にはまた異なる傾向になると思われます。

個人的には、モスキートノイズが少ない点でWebPもなかなか良いかなと思いました。ただ、圧縮率によってはかなりアグレッシブにブロックノイズが出るので、注意が要るようです。

なお比較に使ったツールは、私の開発しているCompareというウェブアプリです。 便利ですよ(これが言いたかった)。

Web Workerをローカル環境でも使う

Web Workerはブラウザ上でバックグラウンドスレッドを使う仕組みです。UIの応答性を保ったまま画像処理のように時間のかかる処理を実行できるため、画像比較ツールでは積極的に利用しています。

var worker = new Worker('worker.js');

のように簡単にワーカーを生成できますが、問題はChromeのローカル環境(ローカルにあるHTMLファイルをブラウザで開いた状態)です。次のようなエラーになってしまいます(例外が発生)。

Failed to construct 'Worker':
cannot be accessed from origin 'null'.

これを回避する方法を紹介します。なお、ググるChrome--allow-file-access-from-filesという起動オプションを付ければ解決するという情報が見つかりますが、これをユーザーに要求するのはちょっと面倒なので、ここでは特別な手間が要らない方法を示します。

var newWorkerViaBlob = function(relativePath) {
  var baseURL = window.location.href.replace(/\\/g, '/').replace(/\/[^\/]*$/, '/');
  var array = ['importScripts("' + baseURL + relativePath + '");'];
  var blob = new Blob(array, {type: 'text/javascript'});
  var url = window.URL.createObjectURL(blob);
  return new Worker(url);
};
var worker = newWorkerViaBlob('worker.js');

これだけです。これはChromeを含むほとんどのブラウザで動きますが、今度はMSIEのローカル環境でSecurityErrorというエラーになります(例外が発生)。
そこで次のコードが結論です。

var newWorker = function(relativePath) {
  try {
    return newWorkerViaBlob(relativePath);
  } catch (e) {
    return new Worker(relativePath);
  }
};
var worker = newWorker('worker.js');

1つの方法で例外が発生したら別の方法を試す、というわけです。
ちなみに、Firefoxでは new Worker と newWorkerViaBlob のどちらでも動きますが、パスに".."が含まれる場合(../worker.jsとか)は new Worker だとダメなようです。このとき例外は発生せずに静かに無視されてしまう(スクリプトはロードされず、中身のない動かないワーカーが生成される)ので、上に書いた try〜catch の順序を逆にするとうまくいかないので注意が必要です。

画像の3次元色分布をグリグリ回すツール

コツコツ開発している画像比較ツールに「3次元色分布」という機能を追加しました

https://user-images.githubusercontent.com/732920/28751902-401b0e4e-754e-11e7-97dd-d70f6de62c94.png

これは、画像のピクセルをRGB色空間の中に3次元プロットする機能で、マウスでグリグリ回せます。楽しいです。
普段見ている写真が意外と狭い範囲の色しか含んでいないこととか、白飛びとか色成分が飽和した写真の色がどう「変形」しているかとか、手に取るように観察できます。

この画像比較ツールはJavaScriptだけで実装されているのでブラウザ上で完全にオフラインで使えます。
最初は速度的に WebGL が必要になるかと思ってましたが、とりあえず JavaScript だけでやってみたら、全ピクセルを普通のJavaScriptコードで処理しても問題ない速さで動きました。最近のブラウザはすごい。

使ってみて感想とか要望とかあったら教えてもらえると嬉しいです。
https://github.com/tshino/compare

画像比較をたすけるツールを作った

画像を並べて比較することに特化したビューワを作ってみました。


Compare.htmlで画像を比較している様子

デモページはこちら→ https://tshino.github.io/compare/compare.html
コード→ https://github.com/tshino/compare
ウェブアプリなのでブラウザ上で動きます。オフラインでも使えます。

最初はただ並べてズームできるだけでしたが、あとからヒストグラム表示やPSNRの計算機能などを追加したりしました。
もともとJavaScriptHTML5で何か作ってみたいと思っていたところ、こんなツールのアイデアを思いついたので早速作ってみたという次第。なのでいろいろ稚拙な作りだと思います。
JavaScriptで画像を扱う方法やUIの作り方、Web Workerの使い方など、一つ一つ調べながらのコーディングなので面白いです。

効率的な文字列連結

3つ以上の文字列を何気なく + 演算子で連結すると、必ずしも効率的には動作しないコードになります。

std::string a = "Hello";
const char b[] = "World";
char c = '!';
std::string x =
    "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
    "<message>"
        + a + ',' + b + c
    + "</message>";

実際は、std::basic_string の実装が賢ければ、(たぶん)文字数が少ない場合にヒープを使わないとか、メモリを多めに確保して再確保の回数を減らすとかしてくれるし、コンパイラもムーブとか戻り値最適化とかしてくれるので、そういうのにおまかせで良い場合は + 演算子を使いまくれば良いと思います。

そうでない場合のために concat() という関数を書きました。

std::string x = concat(
    "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
    "<message>",
        a, ',', b, c,
    "</message>");

引数に連結したい std::string や const char* や char を並べます。

実装は以下の通り。

#include <cstddef>
#include <cstring>
#include <string>

namespace detail {
struct ref
{
    const char* ptr;
    std::size_t size;
    ref(const char* s) : ptr(s), size(std::strlen(s)) {}
    ref(const std::string& s) : ptr(s.c_str()), size(s.size()) {}
    ref(const char& c) : ptr(&c), size(1) {}
};
} //namespace detail

template <typename... Args>
std::string concat(Args&& ...args)
{
    detail::ref refs[] = { detail::ref(args)... };
    std::size_t n = 0;
    for (detail::ref& r: refs)
    {
        n += r.size;
    }
    std::string s;
    s.reserve(n);
    for (detail::ref& r: refs)
    {
        s.append(r.ptr, r.size);
    }
    return s;
}

最初は Variadic Template の練習用に書き始めたのですが、結局何の変哲もないコードになりました。

C++で構造体の名前付き初期化

C言語のライブラリを使っていると、構造体をデフォルト初期化してから、一部のメンバー変数だけ書き換えて利用することがしばしばあります。

option o = {};

o.name = "hello";
o.size = 1234;

func(o);

option 構造体のメンバー変数の仕様は一部だけが決まっていて、他のメンバー変数は実装詳細として隠されていたりします。
また、一部のメンバー変数だけ値を設定したい場合があります。
このような構造体の値を式の中でインラインで書く方法を考えてみました。
C++ではこうすれば良さそうです。

func([]{ option o = {};
        o.name = "hello";
        o.size = 1234;
        return o; }());

ただ、タイプ量が増えている上に、唐突な return の「おまじない感」がぬぐえません。
マクロで抽象化してみます。

func(MAKE_STRUCT( option, name = "hello", size = 1234 ));
func(MAKE_STRUCT( option, name = "abc" ));
func(MAKE_STRUCT( option, size = 42 ));

こんな風に書ければ良いと思います。余計な記号も変数名も要らないのでスッキリしました。
あとはこのマクロを実装するだけです。
ついでなので、古いC++でも使えるように考えてみました。たぶん、同じマクロがこんな感じに展開されれば良さそうです。

func(make_struct<option>((
        named_arg / &option::name = "hoge",
        named_arg / &option::size = 1234)));

古いC++ではラムダ式を使えないので、演算子オーバーロードを使って名前付き引数のようなものを作って、なんとかする作戦です。

#define PP_CAT(a,b) PP_CAT_(a,b)
#define PP_CAT_(a,b) a##b
#define PP_SIZE(...) PP_SIZE_(__VA_ARGS__,10,9,8,7,6,5,4,3,2,1)
#define PP_SIZE_(a,b,c,d,e,f,g,h,i,j,k,...) k
#define PP_J(x,y,...) PP_CAT(PP_J,PP_SIZE(_,##__VA_ARGS__))(x,y,##__VA_ARGS__)
#define PP_J1(x,y)
#define PP_J2(x,y,a) x(y,a)
#define PP_J3(x,y,a,...) x(y,a) PP_J2(x,y,__VA_ARGS__)
#define PP_J4(x,y,a,...) x(y,a) PP_J3(x,y,__VA_ARGS__)
#define PP_J5(x,y,a,...) x(y,a) PP_J4(x,y,__VA_ARGS__)
#define PP_J6(x,y,a,...) x(y,a) PP_J5(x,y,__VA_ARGS__)
#define PP_J7(x,y,a,...) x(y,a) PP_J6(x,y,__VA_ARGS__)
#define PP_J8(x,y,a,...) x(y,a) PP_J7(x,y,__VA_ARGS__)
#define PP_J9(x,y,a,...) x(y,a) PP_J8(x,y,__VA_ARGS__)
#define PP_J10(x,y,a,...) x(y,a) PP_J9(x,y,__VA_ARGS__)

#define MAKE_STRUCT(type,...) \
    MAKE_STRUCT_1(type,PP_J(MAKE_STRUCT_2,type,##__VA_ARGS__))

#if __cplusplus > 201103L
#define MAKE_STRUCT_1(type,m) []{ type x = {}; (void)0 m; return x; }()
#define MAKE_STRUCT_2(type,member) ,x.member
#else
#define MAKE_STRUCT_1(type,m) ts::make_struct<type>((ts::no_assign m))
#define MAKE_STRUCT_2(type,member) ,ts::named_arg/&type::member

namespace ts {
template <typename T,typename M> T make_struct(M m)
{
    T x = {};
    m.apply(x);
    return x;
}
struct no_assign_t
{
    template <typename T> void apply(T&) const {}
    template <typename X> X operator ,(X x) const { return x; }
};
const no_assign_t no_assign = {};
template <typename A,typename B> struct assign2
{
    A a;
    B b;
    template <typename T> void apply(T& x) const
    {
        a.apply(x);
        b.apply(x);
    }
};
template <typename T,typename M,typename V> struct assign1
{
    M (T::*mp);
    const V& v;
    void apply(T& x) const { x.*mp = v; }
    template <typename X> assign2<assign1,X> operator ,(X x) const
    {
        assign2<assign1,X> a = { *this, x };
        return a;
    }
};
template <typename T,typename M> struct key
{
    M (T::*mp);
    template <typename V>
    assign1<T,M,V> operator =(const V& v) const
    {
        assign1<T,M,V> m = { mp, v };
        return m;
    }
};
struct named_arg_t
{
    template <typename T,typename M>
    key<T,M> operator /(M (T::*mp)) const
    {
        key<T,M> m = { mp };
        return m;
    }
};
const named_arg_t named_arg = {};
} //namespace ts
#endif

マクロなので引数の数に上限があって、とりあえず9個までという貧乏仕様です。
冒頭に書いた目的に限れば、割と使えるのではないでしょうか?
ちなみに以下のブログ記事から着想を得ました。

http://d.hatena.ne.jp/osyo-manga/20120202/1328108867

追記(2014/06/13)

なんと、親切な人が添削してくれました。

勉強になりました。

ちなみに、__VA_ARGS__の前の##はGCC拡張です。ばれた。
MAKE_STRUCT( type ) って書いても動くようにする悪あがきでした。

ループ変数を回すだけのFORマクロ

普段、配列と std::vector ばかり使うので、BOOST_FOREACH よりも、単純にループ変数を回すコードが簡単に書ける方が嬉しい。

for (int i = 0, n = v.size(); i < n; ++i)
{
  ...
}
FOR_INDEX (int i, v)
{
  ...
}

そのためのマクロを書いてみた。

#include <cstddef>
#include <iostream>

template <class C>
  std::size_t size(const C &c) { return c.size(); }
template <class A, std::size_t N>
  std::size_t size(const A (&)[N]) { return N; }

#define FOR_INDEX(i,c) \
  for (std::size_t FOR_i = 0, FOR_n = size(c); FOR_i < FOR_n; ++FOR_i) \
    if (bool FOR_c = false);else \
      for (i = FOR_i; !FOR_c; FOR_c = true)

int main()
{
  const char* a[] = { "One", "Two", "Three" };
  
  FOR_INDEX(int i, a)
  {
    std::cout << i + 1 << ":" << a[i] << std::endl;
  }
}

/*
1:One
2:Two
3:Three
*/

追記: breakとcontinueのこと忘れてたので修正。これでいいはず。

#define FOR_INDEX(i,c) \
  if (bool FOR_c = false);else \
    for (std::size_t FOR_i = 0, FOR_n = size(c); !FOR_c && FOR_i < FOR_n; ++FOR_i, FOR_c = !FOR_c) \
      for (i = FOR_i; !FOR_c; FOR_c = true)