HTML5 Canvas Thay đổi kích thước (Downscale) Hình ảnh chất lượng cao?


149

Tôi sử dụng các yếu tố canvas html5 để thay đổi kích thước hình ảnh trong trình duyệt của tôi. Hóa ra chất lượng rất thấp. Tôi đã tìm thấy điều này: Vô hiệu hóa Nội suy khi Thu nhỏ <canvas> nhưng nó không giúp tăng chất lượng.

Dưới đây là mã css và js của tôi cũng như hình ảnh được chụp bằng Photoshop và được chia tỷ lệ trong API canvas.

Tôi phải làm gì để có được chất lượng tối ưu khi thu nhỏ hình ảnh trong trình duyệt?

Lưu ý: Tôi muốn thu nhỏ hình ảnh lớn thành hình nhỏ, sửa đổi màu trong khung vẽ và gửi kết quả từ khung vẽ đến máy chủ.

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

Hình ảnh thay đổi kích thước với photoshop:

nhập mô tả hình ảnh ở đây

Hình ảnh thay đổi kích thước trên vải:

nhập mô tả hình ảnh ở đây

Biên tập:

Tôi đã cố gắng thu hẹp quy mô trong hơn một bước như đề xuất trong:

Thay đổi kích thước hình ảnh trong khung vẽ HTML5canvas vẽ Html5: cách áp dụng khử răng cưa

Đây là chức năng tôi đã sử dụng:

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


}

Đây là kết quả nếu tôi sử dụng kích thước xuống 2 bước:

nhập mô tả hình ảnh ở đây

Đây là kết quả nếu tôi sử dụng kích thước xuống 3 bước:

nhập mô tả hình ảnh ở đây

Đây là kết quả nếu tôi sử dụng kích thước xuống 4 bước:

nhập mô tả hình ảnh ở đây

Đây là kết quả nếu tôi sử dụng kích thước xuống 20 bước:

nhập mô tả hình ảnh ở đây

Lưu ý: Hóa ra từ 1 bước đến 2 bước có sự cải thiện lớn về chất lượng hình ảnh nhưng bạn càng thêm nhiều bước vào quy trình thì hình ảnh càng trở nên mờ hơn.

Có cách nào để giải quyết vấn đề mà hình ảnh trở nên mờ hơn khi bạn thêm nhiều bước không?

Chỉnh sửa 2013-10-04: Tôi đã thử thuật toán của GameAlchemist. Đây là kết quả so với Photoshop.

Ảnh PhotoShop:

Ảnh Photo

Thuật toán của GameAlchemist:

Thuật toán của GameAlchemist


2
Bạn có thể thử tăng dần tỷ lệ hình ảnh của mình: stackoverflow.com/questions/18761404/
Khăn

1
bản sao có thể của bản vẽ canvas Html5: cách áp dụng khử răng cưa . Xem nếu không hoạt động. Nếu hình ảnh lớn và giảm xuống kích thước nhỏ, bạn sẽ cần thực hiện theo các bước (xem hình ảnh ví dụ trong liên kết)

2
@confile tắt nội suy sẽ làm cho nó tồi tệ nhất. Bạn muốn giữ cho kích hoạt. Nhìn vào liên kết tôi cung cấp ở trên. Tôi chỉ ra cách sử dụng các bước để thu nhỏ hình ảnh lớn hơn và giữ chất lượng. Và như Scott nói rằng bạn muốn ưu tiên chất lượng hơn tốc độ.

1
@ Ken-AbdiasSoftware Tôi đã thử bạn tiếp cận nhưng vấn đề là nó sẽ trở nên tồi tệ hơn khi tôi sử dụng nhiều vòng hơn cho quy mô từng bước. Bất cứ ý tưởng làm thế nào để khắc phục điều đó?
thiệu

3
Chắc chắn cơ hội sao chép chức năng của một phần mềm chỉnh sửa ảnh chuyên nghiệp đắt tiền bằng HTML5 là khá mong manh? Bạn có thể có thể đến gần (ish), nhưng chính xác là nó hoạt động trong Photoshop mà tôi tưởng tượng sẽ là không thể!
Liam

Câu trả lời:


171

Vì vấn đề của bạn là hạ thấp hình ảnh của bạn, không có lý do nào để nói về phép nội suy - đó là về việc tạo pixel-. Vấn đề ở đây là downsampling.

Để lấy mẫu một hình ảnh, chúng ta cần biến mỗi ô vuông p * p pixel trong ảnh gốc thành một pixel trong ảnh đích.

Vì lý do biểu diễn, Trình duyệt thực hiện việc lấy mẫu rất đơn giản: để xây dựng hình ảnh nhỏ hơn, họ sẽ chỉ chọn MỘT pixel trong nguồn và sử dụng giá trị của nó cho đích. trong đó 'quên' một số chi tiết và thêm tiếng ồn.

Tuy nhiên, có một ngoại lệ: vì tính năng lấy mẫu hình ảnh 2X rất đơn giản để tính toán (trung bình 4 pixel để tạo một) và được sử dụng cho võng mạc / HiDPI pixel, trường hợp này được xử lý đúng cách - Trình duyệt sử dụng 4 pixel để tạo một-.

NHƯNG ... nếu bạn sử dụng nhiều lần lấy mẫu xuống 2 lần, bạn sẽ phải đối mặt với vấn đề là các lỗi làm tròn liên tiếp sẽ gây ra quá nhiều tiếng ồn.
Tệ hơn nữa, bạn sẽ không thay đổi kích thước bằng sức mạnh của hai và thay đổi kích thước thành sức mạnh gần nhất + thay đổi kích thước lần cuối là rất ồn ào.

Những gì bạn tìm kiếm là một đường xuống hoàn hảo pixel, nghĩa là: lấy mẫu lại hình ảnh sẽ đưa tất cả các pixel đầu vào vào tài khoản - bất kể tỷ lệ nào-.
Để làm điều đó, chúng ta phải tính toán, đối với mỗi pixel đầu vào, sự đóng góp của nó vào một, hai hoặc bốn pixel đích tùy thuộc vào phép chiếu tỷ lệ của các pixel đầu vào nằm ngay bên trong một pixel đích, chồng lên một đường viền X, đường viền Y hoặc cả hai .
(Một kế hoạch sẽ tốt ở đây, nhưng tôi không có.)

Đây là một ví dụ về tỷ lệ canvas so với tỷ lệ pixel hoàn hảo của tôi trên tỷ lệ 1/3 của zombat.

Lưu ý rằng hình ảnh có thể được thu nhỏ trong Trình duyệt của bạn và .jpegized bởi SO.
Tuy nhiên, chúng ta thấy rằng có ít tiếng ồn hơn, đặc biệt là ở bãi cỏ phía sau tử cung và các nhánh bên phải của nó. Tiếng ồn trong bộ lông làm cho nó trở nên tương phản hơn, nhưng có vẻ như anh ta có những sợi lông trắng - giống như hình ảnh nguồn-.
Hình ảnh bên phải ít hấp dẫn hơn nhưng chắc chắn đẹp hơn.

nhập mô tả hình ảnh ở đây

Đây là mã để thực hiện thu nhỏ pixel hoàn hảo:

kết quả fiddle: http://jsfiddle.net/gamealchemist/r6aVp/embedded/result/
fiddle chính nó: 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;
}

khá tham lam bộ nhớ, vì bộ đệm float được yêu cầu lưu trữ các giá trị trung gian của hình ảnh đích (-> nếu chúng ta đếm khung hình kết quả, chúng tôi sử dụng 6 lần bộ nhớ của hình ảnh nguồn trong thuật toán này).
Nó cũng khá đắt, vì mỗi pixel nguồn được sử dụng cho dù kích thước đích là bao nhiêu và chúng tôi phải trả tiền cho getImageData / putImageDate, cũng khá chậm.
Nhưng không có cách nào nhanh hơn xử lý từng giá trị nguồn trong trường hợp này và tình huống không tệ lắm: Đối với hình ảnh 740 * 556 của tôi về tử cung, quá trình xử lý mất từ ​​30 đến 40 ms.


Nó có thể nhanh hơn nếu bạn thu nhỏ hình ảnh trước khi bạn đặt nó vào khung vẽ?
thiệu

tôi không hiểu ... có vẻ như đó là những gì tôi làm. Bộ đệm cũng như canvas tôi tạo (resCV) có kích thước của hình ảnh được chia tỷ lệ. Tôi nghĩ rằng cách duy nhất để có được nó nhanh hơn là sử dụng tính toán số nguyên giống như breshensam. Nhưng 40ms chỉ chậm đối với một trò chơi video (25 khung hình / giây), không phải cho ứng dụng vẽ.
GameAlchemist

Bạn có thấy cơ hội nào để làm cho thuật toán của mình nhanh hơn trong khi vẫn giữ được chất lượng không?
hạn

1
tôi đã cố gắng làm tròn bộ đệm (phần mới nhất của thuật toán) bằng 0 | thay vì Mat.ceil. Nó nhanh hơn một chút. Nhưng dù sao cũng có khá nhiều chi phí với get / putImageData và một lần nữa, chúng ta không thể tránh để xử lý từng pixel.
GameAlchemist

4
Ok, vì vậy tôi đã xem mã: bạn đã ở rất gần giải pháp. Hai sai lầm: các chỉ mục của bạn bị tắt bởi một cho tX + 1 (chúng là + 3, + 4, + 5, + 6 thay vì +4, +5, +6, +7) và thay đổi dòng trong rgba là một mul bằng 4, không phải 3. Tôi chỉ kiểm tra 4 giá trị ngẫu nhiên để kiểm tra (0,1, 0,15, 0,33, 0,8) có vẻ ổn. fiddle cập nhật của bạn ở đây: jsfiddle.net/gamealchemist/kpQyE/3
GameAlchemist 27/11/13

51

Mẫu vải nhanh với chất lượng tốt: http://jsfiddle.net/9g9Nv/442/

Cập nhật: phiên bản 2.0 (nhanh hơn, nhân viên web + đối tượng có thể chuyển nhượng) - 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);
}

Tôi cần chất lượng tốt nhất
confile

18
Đã sửa, tôi đã thay đổi "tốt" thành "tốt nhất", bây giờ có ổn không? : D. Mặt khác, nếu bạn muốn lấy mẫu lại tốt nhất có thể - hãy sử dụng hình ảnh.
ViliusL

@confile imgur.com đã an toàn để sử dụng trong jsfiddle, nhưng quản trị viên đã làm gì sai? Bạn không thấy chất lượng tốt, vì trình duyệt của bạn gây ra lỗi nghiêm trọng CORS. (không thể sử dụng hình ảnh từ các trang web từ xa)
ViliusL

Được rồi, bạn có thể sử dụng bất kỳ hình ảnh PNG nào khác với các khu vực trong suốt. Bất cứ ý tưởng về điều này?
confile

4
@ thông tin bạn đã đúng, trong một số trường hợp hình ảnh trong suốt có vấn đề ở các khu vực sắc nét. Tôi đã bỏ lỡ những trường hợp này với bài kiểm tra của tôi. Đã sửa lỗi thay đổi kích thước cũng cố định hỗ trợ hình ảnh từ xa trên fiddle: jsfiddle.net/9g9Nv/49
ViliusL

28

Gợi ý 1 - mở rộng đường ống quy trình

Bạn có thể sử dụng bước xuống như tôi mô tả trong các liên kết bạn đề cập nhưng bạn dường như sử dụng chúng sai cách.

Không cần phải giảm tỷ lệ hình ảnh theo tỷ lệ trên 1: 2 (thông thường, nhưng không giới hạn). Đó là nơi bạn cần thực hiện giảm quy mô mạnh mẽ , bạn cần chia nó thành hai bước (và hiếm khi, nhiều hơn) tùy thuộc vào nội dung của hình ảnh (đặc biệt là khi xảy ra tần số cao như các đường mỏng).

Mỗi khi bạn xuống mẫu, bạn sẽ mất thông tin và thông tin. Bạn không thể mong đợi hình ảnh kết quả rõ ràng như ban đầu.

Nếu sau đó bạn thu nhỏ hình ảnh theo nhiều bước, bạn sẽ mất rất nhiều thông tin và kết quả sẽ kém như bạn đã nhận thấy.

Hãy thử chỉ với một bước thêm, hoặc ở đầu hai.

Kết luận

Trong trường hợp Photoshop thông báo rằng nó áp dụng tích chập sau khi hình ảnh được lấy mẫu lại, chẳng hạn như làm sắc nét. Không chỉ là phép nội suy hai khối diễn ra, do đó, để mô phỏng hoàn toàn Photoshop, chúng ta cũng cần thêm các bước mà Photoshop đang thực hiện (với thiết lập mặc định).

Trong ví dụ này, tôi sẽ sử dụng câu trả lời ban đầu của mình mà bạn đề cập đến trong bài viết của mình, nhưng tôi đã thêm một phép chập vào nó để cải thiện chất lượng như một quy trình đăng bài (xem bản demo ở dưới cùng).

Đây là mã để thêm bộ lọc làm sắc nét (dựa trên bộ lọc tích chập chung - Tôi đặt ma trận trọng số để làm sắc nét bên trong nó cũng như một yếu tố hỗn hợp để điều chỉnh cách phát âm của hiệu ứng):

Sử dụng:

sharpen(context, width, height, mixFactor);

Các mixFactorlà một giá trị giữa [0.0, 1.0] và cho phép bạn làm hạ thấp vai trò tác dụng làm sắc nét - quy tắc-of-thumb: ít kích thước nhỏ của hiệu ứng là cần thiết.

Chức năng (dựa trên đoạn mã này ):

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

Kết quả của việc sử dụng kết hợp này sẽ là:

DEMO TRỰC TUYẾN TẠI ĐÂY

Kết quả giảm mẫu và làm sắc nét

Tùy thuộc vào mức độ sắc nét bạn muốn thêm vào hỗn hợp, bạn có thể nhận được kết quả từ "mờ" mặc định đến rất sắc nét:

Biến thể của sắc nét

Đề xuất 2 - thực hiện thuật toán cấp thấp

Nếu bạn muốn có được kết quả tốt nhất về chất lượng, bạn sẽ cần phải ở cấp độ thấp và xem xét thực hiện ví dụ thuật toán hoàn toàn mới này để thực hiện điều này.

Xem Lấy mẫu hình ảnh phụ thuộc vào nội suy (2011) từ IEEE.
Đây là một liên kết đến bài báo đầy đủ (PDF) .

Không có triển khai thuật toán này trong JavaScript AFAIK tại thời điểm này, vì vậy bạn sẽ sẵn sàng nếu bạn muốn tự mình thực hiện nhiệm vụ này.

Bản chất là (trích từ bài báo):

trừu tượng

Một thuật toán lấy mẫu thích ứng theo hướng nội suy được đề xuất cho mã hóa hình ảnh tốc độ bit thấp trong bài báo này. Đưa ra một hình ảnh, thuật toán đề xuất có thể thu được hình ảnh có độ phân giải thấp, từ đó hình ảnh chất lượng cao có cùng độ phân giải như hình ảnh đầu vào có thể được nội suy. Khác với các thuật toán lấy mẫu xuống truyền thống, độc lập với quy trình nội suy, thuật toán lấy mẫu xuống được đề xuất có bản lề lấy mẫu xuống quy trình nội suy. Do đó, thuật toán lấy mẫu xuống được đề xuất có thể duy trì thông tin ban đầu của hình ảnh đầu vào ở mức độ lớn nhất. Hình ảnh được lấy mẫu xuống sau đó được đưa vào JPEG. Sau đó, một xử lý bài dựa trên tổng biến thể (TV) được áp dụng cho hình ảnh có độ phân giải thấp được giải nén. Cuối cùng,Kết quả thử nghiệm xác minh rằng sử dụng hình ảnh được ghép xuống bằng thuật toán đề xuất, có thể đạt được hình ảnh nội suy với chất lượng cao hơn nhiều. Ngoài ra, thuật toán được đề xuất có thể đạt được hiệu suất vượt trội hơn JPEG để mã hóa hình ảnh tốc độ bit thấp.

Ảnh chụp từ giấy

(xem liên kết được cung cấp cho tất cả các chi tiết, công thức, v.v.)


Đây cũng là một giải pháp tuyệt vời. Cảm ơn bạn!
hạn

Đây là một giải pháp tuyệt vời. Tôi đã thử nó trên các tập tin png với các khu vực trong suốt. Đây là kết quả: jsfiddle.net/confile/5CD4N Bạn có biết phải làm gì để nó hoạt động không?
confile

1
đây là GENIUS! nhưng xin vui lòng bạn có thể giải thích chính xác những gì bạn đang làm? lol .. tôi hoàn toàn muốn biết những thứ bên trong ... có lẽ là tài nguyên để học?
carinlynchin

1
@Carine có thể là một chút nhiều cho một trường nhận xét kém :) nhưng, thu nhỏ lại một nhóm các pixel để lấy trung bình một pixel mới đại diện cho nhóm đó. Đây thực sự là một bộ lọc thông thấp giới thiệu một số mờ tổng thể. Để bù cho sự mất độ sắc nét, chỉ cần áp dụng một phép chập nhọn. Vì độ sắc nét có thể rất rõ rệt, chúng ta có thể trộn nó với hình ảnh để chúng ta có thể kiểm soát mức độ sắc nét. Hy vọng rằng cung cấp cho một số hiểu biết.

21

Nếu bạn chỉ muốn sử dụng canvas, kết quả tốt nhất sẽ có nhiều bước. Nhưng đó chưa phải là điều tốt. Để có chất lượng tốt hơn, bạn cần thực hiện js thuần túy. Chúng tôi vừa phát hành pica - downscaler tốc độ cao với chất lượng / tốc độ thay đổi. Nói tóm lại, nó thay đổi kích thước 1280 * 1024px trong ~ 0,1s và hình ảnh 5000 * 3000px trong 1 giây, với chất lượng cao nhất (bộ lọc lanczos với 3 thùy). Pica có bản demo , nơi bạn có thể chơi với hình ảnh của mình, mức chất lượng và thậm chí thử nó trên thiết bị di động.

Pica chưa có mặt nạ unsharp, nhưng điều đó sẽ được bổ sung rất sớm. Điều đó dễ dàng hơn nhiều so với thực hiện bộ lọc tích chập tốc độ cao để thay đổi kích thước.


16

Tại sao sử dụng canvas để thay đổi kích thước hình ảnh? Tất cả các trình duyệt hiện đại đều sử dụng phép nội suy hai chiều - cùng một quy trình được sử dụng bởi Photoshop (nếu bạn thực hiện đúng) - và chúng thực hiện nhanh hơn quy trình canvas. Chỉ cần chỉ định kích thước hình ảnh bạn muốn (chỉ sử dụng một chiều, chiều cao hoặc chiều rộng, để thay đổi kích thước theo tỷ lệ).

Điều này được hỗ trợ bởi hầu hết các trình duyệt, bao gồm cả các phiên bản IE sau này. Các phiên bản trước có thể yêu cầu CSS dành riêng cho trình duyệt .

Một hàm đơn giản (sử dụng jQuery) để thay đổi kích thước hình ảnh sẽ như thế này:

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

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

Sau đó, chỉ cần sử dụng giá trị được trả về để thay đổi kích thước hình ảnh theo một hoặc cả hai chiều.

Rõ ràng có những tinh chỉnh khác nhau mà bạn có thể thực hiện, nhưng điều này hoàn thành công việc.

Dán đoạn mã sau vào bảng điều khiển của trang này và xem điều gì xảy ra với 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
Cũng lưu ý rằng nếu bạn chỉ chỉ định một chiều, trình duyệt (hiện đại) sẽ tự động duy trì tỷ lệ khung hình tự nhiên của hình ảnh.
André Dion

38
Có lẽ anh ta cần gửi hình ảnh đã thay đổi kích thước đến một máy chủ.
Sergiu Paraschiv

2
@Sergiu: Không cần thiết, nhưng lưu ý rằng nếu bạn đang chuyển từ một hình ảnh rất nhỏ sang một hình ảnh rất lớn, bạn sẽ không nhận được kết quả tuyệt vời ngay cả từ một máy chủ.
Robusto

2
@Robusto Tôi cần đặt hình ảnh vào khung vẽ sau đó và gửi nó đến máy chủ sau này. Tôi muốn thu nhỏ một hình ảnh lớn thành một hình nhỏ, sửa đổi màu sắc trong một khung vẽ và gửi kết quả đến máy chủ. Bạn nghĩ tôi nên làm gì?
thiệu

9
@Robusto Đây là vấn đề. Hiển thị một hình ảnh nhỏ trên máy khách là dễ dàng. img. thong nad img.height rất tầm thường. Tôi muốn thu nhỏ nó xuống một lần và không lặp lại trên máy chủ.
thiệu

8

Không phải là câu trả lời đúng cho những người thực sự cần thay đổi kích thước hình ảnh, mà chỉ để thu nhỏ kích thước tệp .

Tôi gặp vấn đề với hình ảnh "trực tiếp từ máy ảnh", rằng khách hàng của tôi thường tải lên bằng JPEG "không nén".

Không phải ai cũng biết rằng, canvas hỗ trợ (trong hầu hết các trình duyệt 2017) để thay đổi chất lượng của JPEG

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

Với thủ thuật này, tôi có thể giảm 4k x 3k bức ảnh với> 10Mb xuống còn 1 hoặc 2Mb, chắc chắn nó phụ thuộc vào nhu cầu của bạn.

nhìn đây


4

Dưới đây là dịch vụ Angular có thể tái sử dụng để thay đổi kích thước hình ảnh / canvas chất lượng cao: https://gist.github.com/fisch0920/37bac5e741eaec60e983

Dịch vụ này hỗ trợ tích chập lanczos và thu hẹp từng bước. Phương pháp tích chập có chất lượng cao hơn với chi phí chậm hơn, trong khi phương pháp thu hẹp thông minh từng bước tạo ra kết quả khử răng cưa hợp lý và nhanh hơn đáng kể.

Ví dụ sử dụng:

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

Đây là bộ lọc thay đổi kích thước Hermite cải tiến sử dụng 1 worker để cửa sổ không bị đóng băng.

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

Tôi đã tìm thấy một giải pháp không cần truy cập trực tiếp vào dữ liệu pixel và lặp qua nó để thực hiện lấy mẫu xuống. Tùy thuộc vào kích thước của hình ảnh, điều này có thể rất tốn tài nguyên và sẽ tốt hơn nếu sử dụng thuật toán bên trong của trình duyệt.

Hàm drawImage () đang sử dụng phương pháp lấy mẫu tuyến tính, nội suy tuyến tính gần nhất. Điều đó hoạt động tốt khi bạn không thay đổi kích thước xuống hơn một nửa kích thước ban đầu .

Nếu bạn lặp lại để chỉ thay đổi kích thước tối đa một nửa tại một thời điểm, kết quả sẽ khá tốt và nhanh hơn nhiều so với truy cập dữ liệu pixel.

Hàm này giảm xuống còn một nửa cho đến khi đạt được kích thước mong muốn:

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

Bạn có thể vui lòng gửi một jsfiddle và một số hình ảnh kết quả?
hạn

Trong liên kết ở phía dưới, bạn có thể tìm thấy hình ảnh kết quả bằng cách sử dụng kỹ thuật này
Jesús Carrera

1

Có lẽ người đàn ông bạn có thể thử cái này, thứ mà tôi luôn sử dụng trong dự án của mình. Bằng cách này, bạn không chỉ có được hình ảnh chất lượng cao, mà bất kỳ yếu tố nào khác trên khung vẽ của bạn.

/* 
 * @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

thay vì 0,85 , nếu chúng ta thêm 1.0 . Bạn sẽ nhận được câu trả lời chính xác.

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

Bạn có thể có được hình ảnh rõ ràng và tươi sáng. Hãy kiểm tra


0

Tôi thực sự cố gắng tránh chạy qua dữ liệu hình ảnh, đặc biệt là trên các hình ảnh lớn hơn. Do đó, tôi đã đưa ra một cách khá đơn giản để giảm kích thước hình ảnh mà không có bất kỳ hạn chế hoặc giới hạn nào bằng cách sử dụng một vài bước bổ sung. Thói quen này giảm xuống nửa bước thấp nhất có thể trước kích thước mục tiêu mong muốn. Sau đó, nó chia tỷ lệ lên gấp đôi kích thước mục tiêu và một nửa nữa. Thoạt nghe có vẻ buồn cười, nhưng kết quả rất tốt và nhanh chóng đến đó.

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

DEMO : Thay đổi kích thước hình ảnh với fiddler JS và HTML Canvas Demo.

Bạn có thể tìm thấy 3 phương pháp khác nhau để thực hiện thay đổi kích thước này, điều đó sẽ giúp bạn hiểu cách mã đang hoạt động và tại sao.

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

Mã đầy đủ của cả bản demo và phương thức TypeScript mà bạn có thể muốn sử dụng trong mã của mình, có thể được tìm thấy trong dự án GitHub.

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

Đây là mã cuối cùng:

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
        }
    };
}}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.