HTML5 Canvas 画像のサイズ変更(縮小) 高画質? 質問する

HTML5 Canvas 画像のサイズ変更(縮小) 高画質? 質問する

ブラウザで画像のサイズを変更するために HTML5 キャンバス要素を使用しています。品質が非常に低いことがわかりました。次のことがわかりました:<canvas> の拡大縮小時に補間を無効にするしかし、品質の向上には役立ちません。

以下は、私の CSS および JS コードと、Photoshop で呼び出され、キャンバス API で拡大縮小された画像です。

ブラウザで画像を拡大縮小するときに最適な品質を得るにはどうすればいいですか?

注: 大きな画像を小さな画像に縮小し、キャンバス内の色を変更して、その結果をキャンバスからサーバーに送信したいと考えています。

CS: ...

canvas, img {
    image-rendering: optimizeQuality;
    image-rendering: -moz-crisp-edges;
    image-rendering: -webkit-optimize-contrast;
    image-rendering: optimize-contrast;
    -ms-interpolation-mode: nearest-neighbor;
}

JS:

var $img = $('<img>');
var $originalCanvas = $('<canvas>');
$img.load(function() {


   var originalContext = $originalCanvas[0].getContext('2d');   
   originalContext.imageSmoothingEnabled = false;
   originalContext.webkitImageSmoothingEnabled = false;
   originalContext.mozImageSmoothingEnabled = false;
   originalContext.drawImage(this, 0, 0, 379, 500);
});

Photoshop でサイズを変更した画像:

ここに画像の説明を入力してください

キャンバス上でサイズが変更された画像:

ここに画像の説明を入力してください

編集:

以下の提案に従って、複数のステップでダウンスケーリングを試みました。

HTML5 キャンバスで画像のサイズを変更するそしてHtml5 キャンバスの drawImage: アンチエイリアシングを適用する方法

私が使用した関数は次のとおりです:

function resizeCanvasImage(img, canvas, maxWidth, maxHeight) {
    var imgWidth = img.width, 
        imgHeight = img.height;

    var ratio = 1, ratio1 = 1, ratio2 = 1;
    ratio1 = maxWidth / imgWidth;
    ratio2 = maxHeight / imgHeight;

    // Use the smallest ratio that the image best fit into the maxWidth x maxHeight box.
    if (ratio1 < ratio2) {
        ratio = ratio1;
    }
    else {
        ratio = ratio2;
    }

    var canvasContext = canvas.getContext("2d");
    var canvasCopy = document.createElement("canvas");
    var copyContext = canvasCopy.getContext("2d");
    var canvasCopy2 = document.createElement("canvas");
    var copyContext2 = canvasCopy2.getContext("2d");
    canvasCopy.width = imgWidth;
    canvasCopy.height = imgHeight;  
    copyContext.drawImage(img, 0, 0);

    // init
    canvasCopy2.width = imgWidth;
    canvasCopy2.height = imgHeight;        
    copyContext2.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvasCopy2.width, canvasCopy2.height);


    var rounds = 2;
    var roundRatio = ratio * rounds;
    for (var i = 1; i <= rounds; i++) {
        console.log("Step: "+i);

        // tmp
        canvasCopy.width = imgWidth * roundRatio / i;
        canvasCopy.height = imgHeight * roundRatio / i;

        copyContext.drawImage(canvasCopy2, 0, 0, canvasCopy2.width, canvasCopy2.height, 0, 0, canvasCopy.width, canvasCopy.height);

        // copy back
        canvasCopy2.width = imgWidth * roundRatio / i;
        canvasCopy2.height = imgHeight * roundRatio / i;
        copyContext2.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvasCopy2.width, canvasCopy2.height);

    } // end for


    // copy back to canvas
    canvas.width = imgWidth * roundRatio / rounds;
    canvas.height = imgHeight * roundRatio / rounds;
    canvasContext.drawImage(canvasCopy2, 0, 0, canvasCopy2.width, canvasCopy2.height, 0, 0, canvas.width, canvas.height);


}

2 段階のダウンサイジングを使用した場合の結果は次のとおりです。

ここに画像の説明を入力してください

3 段階のダウンサイジングを使用した場合の結果は次のとおりです。

ここに画像の説明を入力してください

4 段階のダウンサイジングを使用した場合の結果は次のとおりです。

ここに画像の説明を入力してください

20 ステップの縮小サイズを使用した場合の結果は次のとおりです。

ここに画像の説明を入力してください

注: 1 ステップから 2 ステップにすると、画像の品質が大幅に向上しますが、プロセスに追加するステップが増えるほど、画像がぼやけてしまいます。

ステップを追加するほど画像がぼやけてしまう問題を解決する方法はありますか?

編集 2013-10-04: GameAlchemist のアルゴリズムを試してみました。以下は Photoshop と比較した結果です。

PhotoShop 画像:

PhotoShop画像

GameAlchemist のアルゴリズム:

GameAlchemistのアルゴリズム

ベストアンサー1

問題は画像を縮小することなので、補間(ピクセルの作成)について話すのは意味がありません。ここでの問題はダウンサンプリングです。

画像をダウンサンプリングするには、元の画像内の p * p ピクセルの各正方形を、宛先画像内の 1 つのピクセルに変換する必要があります。

パフォーマンス上の理由から、ブラウザは非常に単純なダウンサンプリングを実行します。つまり、小さい画像を作成するために、ソース内の 1 つのピクセルを選択し、その値を宛先に使用します。これにより、一部の詳細が「忘れられ」、ノイズが追加されます。

ただし、これには例外があります。2 倍の画像ダウンサンプリングは計算が非常に簡単 (平均 4 ピクセルで 1 つ作成) であり、Retina/HiDPI ピクセルに使用されるため、このケースは適切に処理されます (ブラウザーは 4 ピクセルを使用して 1 つを作成します)。

しかし、2 倍のダウンサンプリングを複数回使用すると、連続した丸め誤差によってノイズが過剰に増加するという問題に直面します。
さらに悪いことに、常に 2 の累乗でサイズ変更されるわけではなく、最も近い累乗 + 最後のサイズ変更でサイズ変更すると、非常にノイズが多くなります。

求めているのはピクセル パーフェクトなダウンサンプリング、つまり、スケールに関係なくすべての入力ピクセルを考慮した画像の再サンプリングです。
そのためには、入力ピクセルのスケール投影が目的ピクセルの内側にあるか、X 境界、Y 境界、またはその両方に重なっているかに応じて、入力ピクセルごとに 1 つ、2 つ、または 4 つの目的ピクセルへの寄与を計算する必要があります。
(スキームがあれば便利ですが、私は持っていません。)

これは、ゾンバットの 1/3 スケールにおけるキャンバス スケールとピクセル パーフェクト スケールの例です。

画像はブラウザで拡大縮小され、SO によって .jpeg 化されることに注意してください。
それでも、ウォンバットの後ろの草や右側の枝など、特にノイズがかなり少なくなっていることがわかります。毛皮のノイズによってコントラストが強くなっていますが、元の画像とは異なり、ウォンバットには白い毛があるように見えます。
右の画像は目立たないものの、明らかによりきれいです。

ここに画像の説明を入力してください

ピクセルパーフェクトなダウンスケーリングを行うコードは次のとおりです。

フィドル結果:http://jsfiddle.net/gamealchemist/r6aVp/embedded/result/
フィドル自体:http://jsfiddle.net/gamealchemist/r6aVp/

// scales the image by (float) scale < 1
// returns a canvas containing the scaled image.
function downScaleImage(img, scale) {
    var imgCV = document.createElement('canvas');
    imgCV.width = img.width;
    imgCV.height = img.height;
    var imgCtx = imgCV.getContext('2d');
    imgCtx.drawImage(img, 0, 0);
    return downScaleCanvas(imgCV, scale);
}

// scales the canvas by (float) scale < 1
// returns a new canvas containing the scaled image.
function downScaleCanvas(cv, scale) {
    if (!(scale < 1) || !(scale > 0)) throw ('scale must be a positive number <1 ');
    var sqScale = scale * scale; // square scale = area of source pixel within target
    var sw = cv.width; // source image width
    var sh = cv.height; // source image height
    var tw = Math.floor(sw * scale); // target image width
    var th = Math.floor(sh * scale); // target image height
    var sx = 0, sy = 0, sIndex = 0; // source x,y, index within source array
    var tx = 0, ty = 0, yIndex = 0, tIndex = 0; // target x,y, x,y index within target array
    var tX = 0, tY = 0; // rounded tx, ty
    var w = 0, nw = 0, wx = 0, nwx = 0, wy = 0, nwy = 0; // weight / next weight x / y
    // weight is weight of current source point within target.
    // next weight is weight of current source point within next target's point.
    var crossX = false; // does scaled px cross its current px right border ?
    var crossY = false; // does scaled px cross its current px bottom border ?
    var sBuffer = cv.getContext('2d').
    getImageData(0, 0, sw, sh).data; // source buffer 8 bit rgba
    var tBuffer = new Float32Array(3 * tw * th); // target buffer Float32 rgb
    var sR = 0, sG = 0,  sB = 0; // source's current point r,g,b
    /* untested !
    var sA = 0;  //source alpha  */    

    for (sy = 0; sy < sh; sy++) {
        ty = sy * scale; // y src position within target
        tY = 0 | ty;     // rounded : target pixel's y
        yIndex = 3 * tY * tw;  // line index within target array
        crossY = (tY != (0 | ty + scale)); 
        if (crossY) { // if pixel is crossing botton target pixel
            wy = (tY + 1 - ty); // weight of point within target pixel
            nwy = (ty + scale - tY - 1); // ... within y+1 target pixel
        }
        for (sx = 0; sx < sw; sx++, sIndex += 4) {
            tx = sx * scale; // x src position within target
            tX = 0 |  tx;    // rounded : target pixel's x
            tIndex = yIndex + tX * 3; // target pixel index within target array
            crossX = (tX != (0 | tx + scale));
            if (crossX) { // if pixel is crossing target pixel's right
                wx = (tX + 1 - tx); // weight of point within target pixel
                nwx = (tx + scale - tX - 1); // ... within x+1 target pixel
            }
            sR = sBuffer[sIndex    ];   // retrieving r,g,b for curr src px.
            sG = sBuffer[sIndex + 1];
            sB = sBuffer[sIndex + 2];

            /* !! untested : handling alpha !!
               sA = sBuffer[sIndex + 3];
               if (!sA) continue;
               if (sA != 0xFF) {
                   sR = (sR * sA) >> 8;  // or use /256 instead ??
                   sG = (sG * sA) >> 8;
                   sB = (sB * sA) >> 8;
               }
            */
            if (!crossX && !crossY) { // pixel does not cross
                // just add components weighted by squared scale.
                tBuffer[tIndex    ] += sR * sqScale;
                tBuffer[tIndex + 1] += sG * sqScale;
                tBuffer[tIndex + 2] += sB * sqScale;
            } else if (crossX && !crossY) { // cross on X only
                w = wx * scale;
                // add weighted component for current px
                tBuffer[tIndex    ] += sR * w;
                tBuffer[tIndex + 1] += sG * w;
                tBuffer[tIndex + 2] += sB * w;
                // add weighted component for next (tX+1) px                
                nw = nwx * scale
                tBuffer[tIndex + 3] += sR * nw;
                tBuffer[tIndex + 4] += sG * nw;
                tBuffer[tIndex + 5] += sB * nw;
            } else if (crossY && !crossX) { // cross on Y only
                w = wy * scale;
                // add weighted component for current px
                tBuffer[tIndex    ] += sR * w;
                tBuffer[tIndex + 1] += sG * w;
                tBuffer[tIndex + 2] += sB * w;
                // add weighted component for next (tY+1) px                
                nw = nwy * scale
                tBuffer[tIndex + 3 * tw    ] += sR * nw;
                tBuffer[tIndex + 3 * tw + 1] += sG * nw;
                tBuffer[tIndex + 3 * tw + 2] += sB * nw;
            } else { // crosses both x and y : four target points involved
                // add weighted component for current px
                w = wx * wy;
                tBuffer[tIndex    ] += sR * w;
                tBuffer[tIndex + 1] += sG * w;
                tBuffer[tIndex + 2] += sB * w;
                // for tX + 1; tY px
                nw = nwx * wy;
                tBuffer[tIndex + 3] += sR * nw;
                tBuffer[tIndex + 4] += sG * nw;
                tBuffer[tIndex + 5] += sB * nw;
                // for tX ; tY + 1 px
                nw = wx * nwy;
                tBuffer[tIndex + 3 * tw    ] += sR * nw;
                tBuffer[tIndex + 3 * tw + 1] += sG * nw;
                tBuffer[tIndex + 3 * tw + 2] += sB * nw;
                // for tX + 1 ; tY +1 px
                nw = nwx * nwy;
                tBuffer[tIndex + 3 * tw + 3] += sR * nw;
                tBuffer[tIndex + 3 * tw + 4] += sG * nw;
                tBuffer[tIndex + 3 * tw + 5] += sB * nw;
            }
        } // end for sx 
    } // end for sy

    // create result canvas
    var resCV = document.createElement('canvas');
    resCV.width = tw;
    resCV.height = th;
    var resCtx = resCV.getContext('2d');
    var imgRes = resCtx.getImageData(0, 0, tw, th);
    var tByteBuffer = imgRes.data;
    // convert float32 array into a UInt8Clamped Array
    var pxIndex = 0; //  
    for (sIndex = 0, tIndex = 0; pxIndex < tw * th; sIndex += 3, tIndex += 4, pxIndex++) {
        tByteBuffer[tIndex] = Math.ceil(tBuffer[sIndex]);
        tByteBuffer[tIndex + 1] = Math.ceil(tBuffer[sIndex + 1]);
        tByteBuffer[tIndex + 2] = Math.ceil(tBuffer[sIndex + 2]);
        tByteBuffer[tIndex + 3] = 255;
    }
    // writing result to canvas.
    resCtx.putImageData(imgRes, 0, 0);
    return resCV;
}

それはとてもメモリを大量に消費します。これは、宛先イメージの中間値を格納するために float バッファが必要なためです (-> 結果のキャンバスを数えると、このアルゴリズムではソース イメージの 6 倍のメモリを使用します)。
また、宛先サイズに関係なく各ソース ピクセルが使用されるためコストも高く、getImageData / putImageDate のコストもかかり、非常に低速です。
ただし、この場合、各ソース値を処理するよりも高速化する方法はありません。状況はそれほど悪くありません。私のウォンバットの 740 * 556 イメージの場合、処理には 30 ~ 40 ミリ秒かかります。

おすすめ記事