HTML5 캔버스 크기 조정 (다운 스케일) 이미지 고품질?


149

브라우저에서 이미지 크기를 조정하기 위해 html5 canvas 요소를 사용합니다. 품질이 매우 낮습니다. 나는 이것을 발견했다 : <canvas>를 스케일링 할 때 보간을 비활성화 하지만 품질을 높이는 데 도움이되지 않습니다.

아래는 내 CSS 및 js 코드뿐만 아니라 Photoshop에서 호출되고 캔버스 API에서 크기가 조정 된 이미지입니다.

브라우저에서 이미지를 스케일링 할 때 최적의 품질을 얻으려면 어떻게해야합니까?

참고 : 큰 이미지를 작은 이미지로 축소하고 캔버스의 색상을 수정하고 캔버스에서 서버로 결과를 보내려고합니다.

CSS :

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);
});

포토샵으로 이미지 크기 조정 :

여기에 이미지 설명을 입력하십시오

캔버스에서 이미지 크기가 조정되었습니다.

여기에 이미지 설명을 입력하십시오

편집하다:

제안 된대로 여러 단계로 축소를 시도했습니다.

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과 비교 한 결과입니다.

포토샵 이미지 :

포토샵 이미지

GameAlchemist의 알고리즘 :

GameAlchemist의 알고리즘


2
이미지를 점차적으로 확대 해 볼 수 있습니다 : stackoverflow.com/questions/18761404/…
markE

1
의 중복 가능성 HTML5 인 캔버스의 drawImage : 안티 앨리어싱을 적용하는 방법 . 작동하지 않는지 확인하십시오. 이미지가 크고 작은 크기로 축소 된 경우 단계적으로 수행해야합니다 (링크의 이미지 예 참조)

2
@confile 보간 기능을 끄면 최악입니다. 당신은 그 기능을 유지하고 싶습니다. 위에서 제공 한 링크를보십시오. 단계를 사용하여 더 큰 이미지를 축소하고 품질을 유지하는 방법을 보여줍니다. 그리고 Scott은 속도보다 품질을 우선시하고 싶다고 말합니다.

1
@ Ken-AbdiasSoftware 접근하려고 시도했지만 문제는 단계별 확장에 사용하는 라운드가 많을수록 악화된다는 것입니다. 어떻게 고칠 수 있습니까?
confile

3
HTML5를 사용하여 값 비싼 전문 사진 편집 소프트웨어의 기능을 복제 할 가능성은 매우 적습니까? 당신은 아마 가까워 질 수도 있지만 Photoshop에서 작동하는 것처럼 불가능하다고 상상할 수 있습니다!
Liam

답변:


171

문제는 이미지를 축소하는 것이므로 보간에 대해 이야기 할 필요가 없습니다. 즉, 픽셀을 만드는 것입니다. 여기서 문제는 다운 샘플링입니다.

이미지를 다운 샘플링하려면 원본 이미지의 각 p * p 픽셀 제곱을 대상 이미지의 단일 픽셀로 바꿔야합니다.

성능상의 이유로 브라우저는 매우 간단한 다운 샘플링을 수행합니다. 더 작은 이미지를 만들기 위해 소스에서 하나의 픽셀을 선택하고 대상 값을 사용합니다. 어떤 세부 사항을 잊어 버리고 소음을 추가합니다.

그러나 2X 이미지 다운 샘플링은 계산하기가 매우 간단하고 (평균 4 픽셀로 구성) 망막 / HiDPI 픽셀에 사용되므로이 경우는 제대로 처리됩니다. 브라우저는 4 픽셀을 사용하여 하나-.

그러나 2X 다운 샘플링을 여러 번 사용하면 연속적인 반올림 오류로 인해 노이즈가 너무 많이 발생하는 문제에 직면하게됩니다.
더 나쁜 것은 항상 2의 거듭 제곱으로 크기를 조정하지는 않으며 가장 가까운 거듭 제곱 + 마지막 크기 조정은 시끄 럽습니다.

당신이 찾는 것은 완벽한 픽셀 다운 샘플링입니다. 즉, 모든 입력 픽셀을 고려하여 이미지를 리샘플링합니다.
이를 위해서는 각 입력 픽셀에 대해 입력 픽셀의 스케일링 된 투영이 대상 픽셀 내부에 있는지, X 경계, Y 경계 또는 둘 다와 겹치는 지에 따라 1, 2 또는 4 개의 대상 픽셀에 대한 기여도를 계산해야합니다. .
(여기에는 체계가 좋을 것이지만 나는 하나도 없습니다.)

다음은 1/3 스케일의 zombat에서 캔버스 스케일과 내 픽셀 퍼펙트 스케일의 예입니다.

브라우저에서 사진 크기가 조정될 수 있으며 SO에 의해 .jpegized입니다.
그러나 우리는 특히 웜뱃 뒤의 잔디와 그 오른쪽의 가지에서 소음이 훨씬 적다는 것을 알 수 있습니다. 모피의 소음으로 인해 대비가 높아지지만 소스 사진과 달리 흰 머리카락이있는 것처럼 보입니다.
올바른 이미지는 덜보기 쉽지만 확실히 더 좋습니다.

여기에 이미지 설명을 입력하십시오

픽셀 퍼펙트 다운 스케일링을 수행하는 코드는 다음과 같습니다.

바이올린 결과 : 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;
}

그것이 매우 플로트 버퍼 대상 이미지의 중간 값을 저장할 필요가 있기 때문에, 메모리 탐욕 (-> 우리 결과 캔버스를 계산하는 경우, 우리는 6 시간이 알고리즘에서 소스 이미지의 메모리를 사용하여).
각 소스 픽셀이 대상 크기에 관계없이 사용되므로 getImageData / putImageDate에 대한 비용을 지불해야하기 때문에 상당히 비쌉니다.
그러나이 경우 각 소스 값을 처리하는 것보다 빠를 수있는 방법이 없으며 상황이 그렇게 나쁘지 않습니다 : 웜뱃의 740 * 556 이미지의 경우 처리에는 30 ~ 40ms가 걸립니다.


이미지를 캔버스에 넣기 전에 크기를 조정하면 더 빠를 수 있습니까?
confile

나는 그것을 얻지 못한다 ... 그것은 내가하는 일 인 것 같다. 버퍼와 내가 만든 캔버스 (resCV)는 크기가 조정 된 이미지의 크기를 갖습니다. 나는 그것을 더 빨리 얻는 유일한 방법은 breshensam 같은 정수 계산을 사용하는 것이라고 생각합니다. 그러나 40ms는 비디오 게임 (25fps)의 경우에만 느리며 그리기 응용 프로그램에는 적합하지 않습니다.
GameAlchemist

품질을 유지하면서 알고리즘을 더 빠르게 만들 수있는 기회가 있습니까?
confile

1
0을 사용하여 버퍼 (알고리즘의 최신 부분)를 반올림하려고했습니다. Mat.ceil 대신. 조금 더 빠릅니다. 그러나 어쨌든 get / putImageData에는 약간의 오버 헤드가 있으며 각 픽셀을 처리하는 것을 피할 수는 없습니다.
GameAlchemist

4
좋아, 그래서 나는 코드를 보았다 : 당신은 솔루션과 매우 가까웠다. 두 가지 실수 : tX + 1에 대해 인덱스가 하나씩 해제되었습니다 (+4, +5, +6, +7 대신 + 3, + 4, + 5, + 6) .rgba의 행 변경은 mul 3이 아닌 4까지. 방금 4 개의 임의의 값을 테스트하여 (0.1, 0.15, 0.33, 0.8) 괜찮은 것처럼 보였습니다. 업데이트 된 바이올린은 여기 : jsfiddle.net/gamealchemist/kpQyE/3
GameAlchemist

51

좋은 품질의 빠른 캔버스 리샘플링 : http://jsfiddle.net/9g9Nv/442/

업데이트 : 버전 2.0 (빠른 웹 작업자 + 전송 가능한 개체) -https : //github.com/viliusle/Hermite-resize

/**
 * Hermite resize - fast image resize/resample using Hermite filter. 1 cpu version!
 * 
 * @param {HtmlElement} canvas
 * @param {int} width
 * @param {int} height
 * @param {boolean} resize_canvas if true, canvas will be resized. Optional.
 */
function resample_single(canvas, width, height, resize_canvas) {
    var width_source = canvas.width;
    var height_source = canvas.height;
    width = Math.round(width);
    height = Math.round(height);

    var ratio_w = width_source / width;
    var ratio_h = height_source / height;
    var ratio_w_half = Math.ceil(ratio_w / 2);
    var ratio_h_half = Math.ceil(ratio_h / 2);

    var ctx = canvas.getContext("2d");
    var img = ctx.getImageData(0, 0, width_source, height_source);
    var img2 = ctx.createImageData(width, height);
    var data = img.data;
    var data2 = img2.data;

    for (var j = 0; j < height; j++) {
        for (var i = 0; i < width; i++) {
            var x2 = (i + j * width) * 4;
            var weight = 0;
            var weights = 0;
            var weights_alpha = 0;
            var gx_r = 0;
            var gx_g = 0;
            var gx_b = 0;
            var gx_a = 0;
            var center_y = (j + 0.5) * ratio_h;
            var yy_start = Math.floor(j * ratio_h);
            var yy_stop = Math.ceil((j + 1) * ratio_h);
            for (var yy = yy_start; yy < yy_stop; yy++) {
                var dy = Math.abs(center_y - (yy + 0.5)) / ratio_h_half;
                var center_x = (i + 0.5) * ratio_w;
                var w0 = dy * dy; //pre-calc part of w
                var xx_start = Math.floor(i * ratio_w);
                var xx_stop = Math.ceil((i + 1) * ratio_w);
                for (var xx = xx_start; xx < xx_stop; xx++) {
                    var dx = Math.abs(center_x - (xx + 0.5)) / ratio_w_half;
                    var w = Math.sqrt(w0 + dx * dx);
                    if (w >= 1) {
                        //pixel too far
                        continue;
                    }
                    //hermite filter
                    weight = 2 * w * w * w - 3 * w * w + 1;
                    var pos_x = 4 * (xx + yy * width_source);
                    //alpha
                    gx_a += weight * data[pos_x + 3];
                    weights_alpha += weight;
                    //colors
                    if (data[pos_x + 3] < 255)
                        weight = weight * data[pos_x + 3] / 250;
                    gx_r += weight * data[pos_x];
                    gx_g += weight * data[pos_x + 1];
                    gx_b += weight * data[pos_x + 2];
                    weights += weight;
                }
            }
            data2[x2] = gx_r / weights;
            data2[x2 + 1] = gx_g / weights;
            data2[x2 + 2] = gx_b / weights;
            data2[x2 + 3] = gx_a / weights_alpha;
        }
    }
    //clear and resize canvas
    if (resize_canvas === true) {
        canvas.width = width;
        canvas.height = height;
    } else {
        ctx.clearRect(0, 0, width_source, height_source);
    }

    //draw
    ctx.putImageData(img2, 0, 0);
}

최고의 품질이 필요합니다
15:08에

18
고정, 나는 "좋음"을 "최고"로 바꿨다. :디. 반면에 최상의 재 샘플링을 원하면 imagemagick을 사용하십시오.
ViliusL

@confile imgur.com은 jsfiddle에서 사용하기에 안전했지만 관리자가 잘못 했습니까? 브라우저에 CORS 치명적인 오류가 발생하기 때문에 품질이 좋지 않습니다. (원격 사이트의 이미지를 사용할 수 없음)
ViliusL

투명 영역이있는 다른 PNG 이미지를 사용할 수 있습니다. 이것에 대한 아이디어가 있습니까?
confile

4
@ confile 당신이 맞았고, 어떤 경우에는 투명한 이미지가 날카로운 부분에 문제가있었습니다. 내 테스트 에서이 사례를 놓쳤다. 고정 크기 조정도 바이올린에 원격 이미지 지원을 고정 : jsfiddle.net/9g9Nv/49
ViliusL

28

제안 1-프로세스 파이프 라인 확장

언급 한 링크에 설명 된 것처럼 스텝 다운을 사용할 수 있지만 잘못된 방식으로 사용하는 것처럼 보입니다.

이미지를 1 : 2 이상의 비율로 조정하기 위해 스텝 다운이 필요하지 않습니다 (일반적으로 이에 국한되지는 않음). 여기 에서는 이미지의 내용에 따라 (특히가는 선과 같이 높은 주파수가 발생하는 경우) 급격한 다운 스케일링을 수행해야하는 경우가 있습니다.

이미지를 다운 샘플링 할 때마다 세부 정보와 정보가 느슨해집니다. 결과 이미지가 원본만큼 선명하지 않을 수 있습니다.

그런 다음 여러 단계로 이미지의 크기를 줄이면 많은 정보를 잃게되며 이미 눈치 채 셨을 때 결과가 나빠질 수 있습니다.

한 번의 추가 단계 또는 두 단계 이상으로 시도하십시오.

컨볼 루션

Photoshop의 경우 이미지를 다시 샘플링 한 후 선명하게와 같이 컨벌루션을 적용합니다. 이중 입방 형 보간 만이 아니라 Photoshop을 완전히 에뮬레이트하려면 Photoshop이 수행하는 단계 (기본 설정)를 추가해야합니다.

이 예에서는 게시물에서 언급 한 원래 답변을 사용하지만 게시 프로세스의 품질을 향상시키기 위해 선명하게 컨벌루션을 추가했습니다 (아래 데모 참조).

다음은 선명하게 필터를 추가하는 코드입니다 (일반 컨볼 루션 필터를 기반으로합니다-선명하게하기위한 가중치 행렬과 효과의 발음을 조정하는 혼합 요소를 넣습니다).

용법:

sharpen(context, width, height, mixFactor);

mixFactor값은 [0.0, 1.0] 사이의 값이며 선명 효과-축소 규칙을 다운 플레이 할 수 있습니다. 크기가 작을수록 효과가 덜 필요합니다.

기능 ( 이 코드를 기반으로 함 ) :

function sharpen(ctx, w, h, mix) {

    var weights =  [0, -1, 0,  -1, 5, -1,  0, -1, 0],
        katet = Math.round(Math.sqrt(weights.length)),
        half = (katet * 0.5) |0,
        dstData = ctx.createImageData(w, h),
        dstBuff = dstData.data,
        srcBuff = ctx.getImageData(0, 0, w, h).data,
        y = h;
        
    while(y--) {

        x = w;

        while(x--) {

            var sy = y,
                sx = x,
                dstOff = (y * w + x) * 4,
                r = 0, g = 0, b = 0, a = 0;

            for (var cy = 0; cy < katet; cy++) {
                for (var cx = 0; cx < katet; cx++) {

                    var scy = sy + cy - half;
                    var scx = sx + cx - half;

                    if (scy >= 0 && scy < h && scx >= 0 && scx < w) {

                        var srcOff = (scy * w + scx) * 4;
                        var wt = weights[cy * katet + cx];

                        r += srcBuff[srcOff] * wt;
                        g += srcBuff[srcOff + 1] * wt;
                        b += srcBuff[srcOff + 2] * wt;
                        a += srcBuff[srcOff + 3] * wt;
                    }
                }
            }

            dstBuff[dstOff] = r * mix + srcBuff[dstOff] * (1 - mix);
            dstBuff[dstOff + 1] = g * mix + srcBuff[dstOff + 1] * (1 - mix);
            dstBuff[dstOff + 2] = b * mix + srcBuff[dstOff + 2] * (1 - mix)
            dstBuff[dstOff + 3] = srcBuff[dstOff + 3];
        }
    }

    ctx.putImageData(dstData, 0, 0);
}

이 조합을 사용한 결과는 다음과 같습니다.

여기 온라인 데모

결과 다운 샘플링 및 컨벌루션 선명 화

블렌드에 추가 할 선명도의 정도에 따라 기본 "흐리게"에서 매우 선명하게 결과를 얻을 수 있습니다.

선명하게 변형

제안 2-저수준 알고리즘 구현

최상의 결과 품질을 얻으려면 저수준으로 가서 예를 들어이 새로운 알고리즘을 구현하는 것이 좋습니다.

IEEE의 보간 종속 이미지 다운 샘플링 (2011)을 참조하십시오 .
다음은 논문 전체에 대한 링크입니다 (PDF) .

현재이 알고리즘은 JavaScript AFAIK에 구현되어 있지 않으므로이 작업에 참여하고 싶을 때는 한 단계 더 나아가십시오.

본질은 (논문에서 발췌)

요약

본 논문에서는 비트율이 낮은 이미지 코딩을 위해 보간 지향적 적응 다운 샘플링 알고리즘이 제안되었다. 이미지가 주어지면, 제안 된 알고리즘은 저해상도 이미지를 얻을 수 있으며, 이로부터 입력 이미지와 동일한 해상도를 갖는 고품질 이미지가 보간 될 수있다. 보간 프로세스와 독립적 인 기존 다운 샘플링 알고리즘과 달리 제안 된 다운 샘플링 알고리즘은 다운 샘플링을 보간 프로세스에 연결합니다. 결과적으로, 제안 된 다운 샘플링 알고리즘은 입력 이미지의 원래 정보를 최대로 유지할 수있다. 다운 샘플링 된 이미지는 JPEG로 공급됩니다. 그리고 나서 전체 변형 (TV) 기반 포스트 프로세싱이 압축 해제 된 저해상도 이미지에 적용된다. 궁극적으로실험 결과는 제안 된 알고리즘에 의해 다운 샘플링 된 이미지를 이용하여 훨씬 더 높은 품질의 보간 된 이미지가 달성 될 수 있음을 입증한다. 또한, 제안 된 알고리즘은 낮은 비트 레이트 이미지 코딩을 위해 JPEG보다 우수한 성능을 달성 할 수 있습니다.

종이에서 스냅 샷

(자세한 내용, 공식 등은 제공된 링크 참조)


이것은 또한 훌륭한 솔루션입니다. 감사합니다!
confile

이것은 훌륭한 솔루션입니다. 투명한 영역이있는 png 파일에서 시도했습니다. 결과는 다음과 같습니다. jsfiddle.net/confile/5CD4N 작동 시키려면 어떻게해야하는지 아십니까?
confile

1
이것은 천재입니다! 하지만 정확히 무엇을하고 있는지 설명해 주시겠습니까? lol .. 나는 모든 것을 알고 싶어한다 .. 아마도 배울 자원이 있을까?
carinlynchin 2016 년

1
@Carine은 주석 필드가 열악 할 수 있지만 약간 축소하면 픽셀 그룹을 다시 샘플링하여 해당 그룹을 나타내는 새로운 픽셀을 평균화합니다. 이는 사실상 전체적으로 약간의 흐림 현상을 일으키는 저역 통과 필터입니다. 선명도의 손실을 보상하기 위해 단순히 선명하게하는 회선을 적용하십시오. 선명도가 매우 뚜렷하기 때문에 이미지와 혼합하여 선명도 수준을 제어 할 수 있습니다. 통찰력을주는 희망.

21

캔버스 만 사용하려면 여러 단계를 거치는 것이 가장 좋습니다. 그러나 그것은 아직 좋은 것은 아닙니다. 더 나은 품질을 위해서는 순수한 js 구현이 필요합니다. 가변 품질 / 속도의 고속 다운 스케일러 인 pica를 출시했습니다 . 즉, ~ 0.1s에서 1280 * 1024px, 1s에서 5000 * 3000px 이미지의 크기를 최고 품질로 조정합니다 (3 개의 로브가있는 랑 조스 필터). Pica에는 데모 가 있으며 , 여기에서 이미지, 품질 수준으로 재생하고 모바일 장치에서 사용해 볼 수도 있습니다.

Pica는 아직 언샵 마스크를 가지고 있지 않지만 곧 추가 될 예정입니다. 크기 조정을위한 고속 회선 필터를 구현하는 것보다 훨씬 쉽습니다.


16

왜 캔버스를 사용하여 이미지 크기를 조정합니까? 최신 브라우저는 모두 Photoshop에서 사용하는 것과 동일한 프로세스 (올바른 작업을 수행하는 경우)와 같은 쌍 입방 보간법을 사용하며 캔버스 프로세스보다 빠르게 수행합니다. 원하는 이미지 크기 만 지정하십시오 (비례 적으로 크기를 조정하려면 하나의 치수, 높이 또는 너비 만 사용하십시오).

이는 이후 버전의 IE를 포함한 대부분의 브라우저에서 지원됩니다. 이전 버전 에는 브라우저 별 CSS가 필요할 수 있습니다 .

이미지의 크기를 조정하는 간단한 함수 (jQuery 사용)는 다음과 같습니다.

function resizeImage(img, percentage) {
    var coeff = percentage/100,
        width = $(img).width(),
        height = $(img).height();

    return {"width": width*coeff, "height": height*coeff}           
}

그런 다음 반환 된 값을 사용하여 하나 또는 두 차원의 이미지 크기를 조정하십시오.

분명히 다른 수정 작업을 수행 할 수 있지만 작업이 완료됩니다.

이 페이지의 콘솔에 다음 코드를 붙여 넣어 Gravatars에 어떤 일이 발생하는지보십시오.

function resizeImage(img, percentage) {
    var coeff = percentage/100,
        width = $(img).width(),
        height = $(img).height();

    return {"width": width*coeff, "height": height*coeff}           
}

$('.user-gravatar32 img').each(function(){
  var newDimensions = resizeImage( this, 150);
  this.style.width = newDimensions.width + "px";
  this.style.height = newDimensions.height + "px";
});

2
또한 하나의 치수 만 지정하면 (현대) 브라우저가 이미지의 자연 종횡비를 자동으로 유지합니다.
André Dion

38
크기 조정 된 이미지를 서버로 보내야 할 수도 있습니다.
Sergiu Paraschiv

2
@ Sergiu : 필요하지는 않지만 매우 작은 이미지에서 매우 큰 이미지로 이동하면 서버에서도 큰 결과를 얻지 못할 수 있습니다.
Robusto

2
@Robusto 나중에 이미지를 캔버스에 넣고 나중에 서버로 보내야합니다. 큰 이미지를 작은 이미지로 축소하고 캔버스에서 색상을 수정 한 후 결과를 서버로 보내려고합니다. 내가 어떻게해야한다고 생각하니?
confile

9
@Robusto 이것이 문제입니다. 클라이언트에 작은 이미지를 표시하는 것은 쉽습니다. img.width nad img.height는 너무 사소합니다. 서버에서 다시하지 않고 한 번만 축소하고 싶습니다.
15:00에

8

실제로 이미지 자체의 크기를 조정해야 하지만 파일 크기를 줄여야하는 사람들에게는 정답이 아닙니다 .

고객이 종종 "비 압축"JPEG로 업로드 한 "카메라에서 직접"사진에 문제가있었습니다.

잘 알려지지 않은 것은 캔버스가 JPEG 품질을 변경하기 위해 (대부분의 브라우저에서 2017) 지원한다는 것입니다

data=canvas.toDataURL('image/jpeg', .85) # [1..0] default 0.92

이 트릭을 사용하면> 10Mb가있는 4k x 3k 사진을 1 또는 2Mb로 줄일 수 있습니다. 필요에 따라 다릅니다.

이봐


4

고품질 이미지 / 캔버스 크기 조정을위한 재사용 가능한 각도 서비스는 다음과 같습니다. https://gist.github.com/fisch0920/37bac5e741eaec60e983

이 서비스는 랑 조스 컨볼 루션 및 단계별 다운 스케일링을 지원합니다. 회선 방식은 속도가 느려도 품질이 높지만 단계적 다운 스케일링 방식은 합리적으로 앤티 앨리어싱 된 결과를 생성하고 훨씬 빠릅니다.

사용법 예 :

angular.module('demo').controller('ExampleCtrl', function (imageService) {
  // EXAMPLE USAGE
  // NOTE: it's bad practice to access the DOM inside a controller, 
  // but this is just to show the example usage.

  // resize by lanczos-sinc filter
  imageService.resize($('#myimg')[0], 256, 256)
    .then(function (resizedImage) {
      // do something with resized image
    })

  // resize by stepping down image size in increments of 2x
  imageService.resizeStep($('#myimg')[0], 256, 256)
    .then(function (resizedImage) {
      // do something with resized image
    })
})

4

이것은 1 명의 작업자를 활용하여 창이 얼지 않도록 개선 된 Hermite 크기 조정 필터입니다.

https://github.com/calvintwr/blitz-hermite-resize

const blitz = Blitz.create()

/* Promise */
blitz({
    source: DOM Image/DOM Canvas/jQuery/DataURL/File,
    width: 400,
    height: 600
}).then(output => {
    // handle output
})catch(error => {
    // handle error
})

/* Await */
let resized = await blizt({...})

/* Old school callback */
const blitz = Blitz.create('callback')
blitz({...}, function(output) {
    // run your callback.
})

3

다운 샘플링을 수행하기 위해 픽셀 데이터에 직접 액세스하고 반복 할 필요가없는 솔루션을 찾았습니다. 이미지의 크기에 따라 리소스를 많이 사용하므로 브라우저의 내부 알고리즘을 사용하는 것이 좋습니다.

의 drawImage () 함수는, 선형 보간을 이용하고, 가장 가까운 - 이웃 재 샘플링 방법. 즉 당신이 절반 이상 원래 크기를 크기를 조정하지 않을 때 잘 작동합니다 .

한 번에 최대 절반의 크기 만 조정하도록 루프하면 픽셀 데이터에 액세스하는 것보다 결과가 훨씬 좋고 빠릅니다.

이 함수는 원하는 크기에 도달 할 때까지 한 번에 절반으로 다운 샘플링합니다.

  function resize_image( src, dst, type, quality ) {
     var tmp = new Image(),
         canvas, context, cW, cH;

     type = type || 'image/jpeg';
     quality = quality || 0.92;

     cW = src.naturalWidth;
     cH = src.naturalHeight;

     tmp.src = src.src;
     tmp.onload = function() {

        canvas = document.createElement( 'canvas' );

        cW /= 2;
        cH /= 2;

        if ( cW < src.width ) cW = src.width;
        if ( cH < src.height ) cH = src.height;

        canvas.width = cW;
        canvas.height = cH;
        context = canvas.getContext( '2d' );
        context.drawImage( tmp, 0, 0, cW, cH );

        dst.src = canvas.toDataURL( type, quality );

        if ( cW <= src.width || cH <= src.height )
           return;

        tmp.src = dst.src;
     }

  }
  // The images sent as parameters can be in the DOM or be image objects
  resize_image( $( '#original' )[0], $( '#smaller' )[0] );

jsfiddle 및 결과 이미지를 게시 할 수 있습니까?
confile

하단의 링크에서이 기술을 사용하여 결과 이미지를 찾을 수 있습니다
Jesús Carrera

1

어쩌면 남자는 이것을 시도 할 수 있습니다. 이는 항상 내 프로젝트에서 사용합니다.이 방법으로 고품질 이미지뿐만 아니라 캔버스의 다른 요소를 얻을 수 있습니다.

/* 
 * @parame canvas => canvas object
 * @parame rate => the pixel quality
 */
function setCanvasSize(canvas, rate) {
    const scaleRate = rate;
    canvas.width = window.innerWidth * scaleRate;
    canvas.height = window.innerHeight * scaleRate;
    canvas.style.width = window.innerWidth + 'px';
    canvas.style.height = window.innerHeight + 'px';
    canvas.getContext('2d').scale(scaleRate, scaleRate);
}

0

1.0 을 더하면 .85 대신에 . 정확한 답변을 얻을 수 있습니다.

data=canvas.toDataURL('image/jpeg', 1.0);

선명하고 밝은 이미지를 얻을 수 있습니다. 확인해주십시오


0

나는 특히 큰 이미지에서 이미지 데이터를 거치지 않도록 노력합니다. 따라서 몇 가지 추가 단계를 사용하여 제한이나 제한없이 이미지 크기를 적당히 줄이는 간단한 방법을 생각해 냈습니다. 이 루틴은 원하는 목표 크기 이전의 가능한 최저 절반 단계로 내려갑니다. 그런 다음 대상 크기의 두 배로 확장 한 다음 다시 절반으로 조정합니다. 처음에는 우스운 것처럼 들리지만 결과는 놀라 울 정도로 좋으며 신속하게 진행됩니다.

function resizeCanvas(canvas, newWidth, newHeight) {
  let ctx = canvas.getContext('2d');
  let buffer = document.createElement('canvas');
  buffer.width = ctx.canvas.width;
  buffer.height = ctx.canvas.height;
  let ctxBuf = buffer.getContext('2d');
  

  let scaleX = newWidth / ctx.canvas.width;
  let scaleY = newHeight / ctx.canvas.height;

  let scaler = Math.min(scaleX, scaleY);
  //see if target scale is less than half...
  if (scaler < 0.5) {
    //while loop in case target scale is less than quarter...
    while (scaler < 0.5) {
      ctxBuf.canvas.width = ctxBuf.canvas.width * 0.5;
      ctxBuf.canvas.height = ctxBuf.canvas.height * 0.5;
      ctxBuf.scale(0.5, 0.5);
      ctxBuf.drawImage(canvas, 0, 0);
      ctxBuf.setTransform(1, 0, 0, 1, 0, 0);
      ctx.canvas.width = ctxBuf.canvas.width;
      ctx.canvas.height = ctxBuf.canvas.height;
      ctx.drawImage(buffer, 0, 0);

      scaleX = newWidth / ctxBuf.canvas.width;
      scaleY = newHeight / ctxBuf.canvas.height;
      scaler = Math.min(scaleX, scaleY);
    }
    //only if the scaler is now larger than half, double target scale trick...
    if (scaler > 0.5) {
      scaleX *= 2.0;
      scaleY *= 2.0;
      ctxBuf.canvas.width = ctxBuf.canvas.width * scaleX;
      ctxBuf.canvas.height = ctxBuf.canvas.height * scaleY;
      ctxBuf.scale(scaleX, scaleY);
      ctxBuf.drawImage(canvas, 0, 0);
      ctxBuf.setTransform(1, 0, 0, 1, 0, 0);
      scaleX = 0.5;
      scaleY = 0.5;
    }
  } else
    ctxBuf.drawImage(canvas, 0, 0);

  //wrapping things up...
  ctx.canvas.width = newWidth;
  ctx.canvas.height = newHeight;
  ctx.scale(scaleX, scaleY);
  ctx.drawImage(buffer, 0, 0);
  ctx.setTransform(1, 0, 0, 1, 0, 0);
}

-1

context.scale(xScale, yScale)

<canvas id="c"></canvas>
<hr/>
<img id="i" />

<script>
var i = document.getElementById('i');

i.onload = function(){
    var width = this.naturalWidth,
        height = this.naturalHeight,
        canvas = document.getElementById('c'),
        ctx = canvas.getContext('2d');

    canvas.width = Math.floor(width / 2);
    canvas.height = Math.floor(height / 2);

    ctx.scale(0.5, 0.5);
    ctx.drawImage(this, 0, 0);
    ctx.rect(0,0,500,500);
    ctx.stroke();

    // restore original 1x1 scale
    ctx.scale(2, 2);
    ctx.rect(0,0,500,500);
    ctx.stroke();
};

i.src = 'https://static.md/b70a511140758c63f07b618da5137b5d.png';
</script>

-1

데모 : JS 및 HTML Canvas Demo 피들러로 이미지 크기 조정.

이 크기 조정을 수행하는 3 가지 방법을 찾을 수 있는데, 이는 코드 작동 방식과 이유를 이해하는 데 도움이됩니다.

https://jsfiddle.net/1b68eLdr/93089/

코드에서 사용할 데모 및 TypeScript 메서드의 전체 코드는 GitHub 프로젝트에서 찾을 수 있습니다.

https://github.com/eyalc4/ts-image-resizer

이것이 최종 코드입니다.

export class ImageTools {
base64ResizedImage: string = null;

constructor() {
}

ResizeImage(base64image: string, width: number = 1080, height: number = 1080) {
    let img = new Image();
    img.src = base64image;

    img.onload = () => {

        // Check if the image require resize at all
        if(img.height <= height && img.width <= width) {
            this.base64ResizedImage = base64image;

            // TODO: Call method to do something with the resize image
        }
        else {
            // Make sure the width and height preserve the original aspect ratio and adjust if needed
            if(img.height > img.width) {
                width = Math.floor(height * (img.width / img.height));
            }
            else {
                height = Math.floor(width * (img.height / img.width));
            }

            let resizingCanvas: HTMLCanvasElement = document.createElement('canvas');
            let resizingCanvasContext = resizingCanvas.getContext("2d");

            // Start with original image size
            resizingCanvas.width = img.width;
            resizingCanvas.height = img.height;


            // Draw the original image on the (temp) resizing canvas
            resizingCanvasContext.drawImage(img, 0, 0, resizingCanvas.width, resizingCanvas.height);

            let curImageDimensions = {
                width: Math.floor(img.width),
                height: Math.floor(img.height)
            };

            let halfImageDimensions = {
                width: null,
                height: null
            };

            // Quickly reduce the size by 50% each time in few iterations until the size is less then
            // 2x time the target size - the motivation for it, is to reduce the aliasing that would have been
            // created with direct reduction of very big image to small image
            while (curImageDimensions.width * 0.5 > width) {
                // Reduce the resizing canvas by half and refresh the image
                halfImageDimensions.width = Math.floor(curImageDimensions.width * 0.5);
                halfImageDimensions.height = Math.floor(curImageDimensions.height * 0.5);

                resizingCanvasContext.drawImage(resizingCanvas, 0, 0, curImageDimensions.width, curImageDimensions.height,
                    0, 0, halfImageDimensions.width, halfImageDimensions.height);

                curImageDimensions.width = halfImageDimensions.width;
                curImageDimensions.height = halfImageDimensions.height;
            }

            // Now do final resize for the resizingCanvas to meet the dimension requirments
            // directly to the output canvas, that will output the final image
            let outputCanvas: HTMLCanvasElement = document.createElement('canvas');
            let outputCanvasContext = outputCanvas.getContext("2d");

            outputCanvas.width = width;
            outputCanvas.height = height;

            outputCanvasContext.drawImage(resizingCanvas, 0, 0, curImageDimensions.width, curImageDimensions.height,
                0, 0, width, height);

            // output the canvas pixels as an image. params: format, quality
            this.base64ResizedImage = outputCanvas.toDataURL('image/jpeg', 0.85);

            // TODO: Call method to do something with the resize image
        }
    };
}}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.