2012年09月09日 16:00 [Edit]

ブラウザー上でJPEG圧縮して非可逆性を体感してみる

asin:4797344377
詳解 画像処理プログラミング

C言語で実装する画像処理アルゴリズムのすべて
昌達慶仁

GIMPでJPEGの蚊を退治して"PNG"化する」「javascript - で bilateral filter (選択的ガウスぼかし)を実装してみた」の反響で、「JPEGのqualityを100に設定すればいいんじゃね?」という誤解がかなり見受けられたので。

実際に体験していただければいいかと。


Demo:

File APIを実装しているブラウザーで動きます。ファイルを読み込ませたら、Qualityをいろいろいじってみてください。元画像、JPEG、両者の差の順で表示されます。差の画像をクリックすると差をエンハンスしてくれます。ブラウザー別に以下の制約があります。

  • iOS & Android: ごめんなさい><
  • Chrome: ベスト
  • Safari: canvas.toDataURL('image/jpeg', quality)のquality解釈が独特です。
  • IE: 10以上でないと動かないはず(File APIがない)
  • Firefox & Opera: input[type=range]が未実装なのでUIが使いにくいかも。
Info:
Original / Generated:
Quality:(0-100)
75
Diff:
PSNR=; Size=; Compression=

論考

  • JPEGの品質設定=100がPNGなどのロスレスフォーマットの代わりにならないこともここで体感できます。実際のところ、品質設定=100というのは量子化テーブルを全て1で埋めた場合に過ぎず、DCT(離散コサイン変換)の過程でロスは必ず生じます。
  • で、何が失われるのかを目で確認できるのが本デモの趣旨です。ピーク信号対雑音比(PSNR)と圧縮率は数字で。圧縮前と圧縮後の差は実際の画面で確認できます。
  • 本当はあわせてSSIMも算出したかったのですが、PSNRより重いのでこれは読者の宿題ということで:-p
  • で、実際にいろいろ試してみると、写真と図版で同じ圧縮率設定でもずいぶんとPNSRが変わることがわかります。図版はかなり下がる。デジカメの写真だと50dB以上いくのもざらなのに、たとえば「うぶんちゅ」の画像だと100まで上げても40dB切ったり。

最近のブラウザーの速度は驚異的。JPEG画像、これ都度生成しているのですがあっという魔。最初はキャッシュしなきゃとか思ったのですが、とりあえずのナイーブ実装で充分高速だったのでそのままです。昔はあんなに高度で重い作業がこんな簡単に出来てしまうのには驚くを通り越してあきれます。

Enjoy!

Dan the Lossy Blogger


Demo Source

HTML


JavaScript

(function(global){

if (!global.FileReader) return; /* throw new Error('FileReader not supported'); */

var $ = function(id){ return document.getElementById(id) };
var stubIcon = 'http://blog.livedoor.jp/dankogai/img/1x1.gif';
// http://blog.livedoor.jp/dankogai/img/ajax-loader.gif
var clearImg = function(){
    $('srcImage').src = $('busy').src = stubIcon;
};
var toJpeg = function() {
    var canvas = document.createElement('canvas'),
        ctx = canvas.getContext('2d'),
        src = $('srcImg'),
        dst = $('dstImg'),
        quality = $('quality').value / 100,
        iW = canvas.width = src.width,
        iH = canvas.height = src.height;
    ctx.drawImage(src, 0, 0);
    dst.onload = function(){
        var srcd = ctx.getImageData(0, 0, iW, iH),
            sd = srcd.data;
        ctx.drawImage(dst, 0, 0);
        var dstd = ctx.getImageData(0, 0, iW, iH);
            dd = dstd.data,
            mse = 0, resolution = 3 * iW * iH;
        for (var i = 0; i < iW*iH*4; i+=4){
            var dr = dd[i]   - sd[i],
                dg = dd[i+1] - sd[i+1],
                db = dd[i+1] - sd[i+1];
            mse += dr*dr + dg*dg + db*db;
            dd[i]   = dr+128;
            dd[i+1] = dg+128;
            dd[i+2] = db+128;
        }
        mse /= resolution;
        var pnsr = 10*Math.log(255*255/mse)/Math.log(10),
            jpegsize = (dst.src.length - dst.src.indexOf(',') - 1) * 0.75;
        $('PSNR').innerHTML = pnsr.toPrecision(6) + ' dB';
        $('jpegsize').innerHTML = jpegsize;
        $('compression').innerHTML = (resolution / jpegsize).toPrecision(3);
        var cdiff = $('cdiff');
        cdiff.width = iW;
        cdiff.height = iH;
        cdiff.getContext('2d').putImageData(dstd, 0, 0);
    };
    dst.src = canvas.toDataURL('image/jpeg', quality);
};
var enhance = function() {
    var ctx = this.getContext('2d'),
        iW = this.width,
        iH = this.height,
        rmax=0, gmax=0, bmax=0, rmin=255,gmin=255,bmin=255;
    var imagedata = ctx.getImageData(0, 0, iW, iH),
        dd = imagedata.data;
    for (var i = 0; i < iW*iH*4; i+=4) with(Math){
        rmin = min(rmin, dd[i]);
        gmin = min(gmin, dd[i+1]);
        bmin = min(bmin, dd[i+2]);
        rmax = max(rmax, dd[i]);
        gmax = max(gmax, dd[i+1]);
        bmax = max(bmax, dd[i+2]);
    }
    // console.log(rmin, rmax, gmin, gmax, bmin, bmax);
    var rs = (rmax-rmin)/255 || 1,
        gs = (gmax-gmin)/255 || 1,
        bs = (bmax-bmin)/255 || 1;
    for (var i = 0; i < iW*iH*4; i+=4) {
        dd[i]   = (dd[i]-128)/rs + 128;
        dd[i+1] = (dd[i+1]-128)/gs + 128;
        dd[i+2] = (dd[i+2]-128)/bs + 128;
    }
    ctx.putImageData(imagedata, 0, 0);
};
var readImg = function (file, img, onload) {
    var reader = new FileReader();
    reader.onload = function (ev) {
        img.src = ev.target.result;
    };
    reader.readAsDataURL(file);
    if (onload) img.onload = onload;
    // $('theFiltered').src = stubIcon;
};
var readit = function () {
    var file = $('theFile').files[0];
    var info = {
            name: file.name,
            lastModifiedDate: file.lastModifiedDate,
            size: file.size,
            type: file.type
        };
    $('fileInfo').textContent = JSON.stringify(info, null, '  ');
    if (file.type.indexOf('image') === 0) {
        readImg(file, $('srcImg'), toJpeg);
    } else {
        clearImg();
    }
};

$('theFile').addEventListener('change', readit, false);
$('readIt').addEventListener('click', readit, false);
$('quality').addEventListener('change', function() {
    this.nextSibling.innerHTML=this.value;
    toJpeg();
}, false);
$('cdiff').addEventListener('click', enhance, false);

})(this);

この記事へのトラックバックURL