HTML5 Canvas Resize (Downscale) изображение высокого качества?


Я использую элементы холста html5 для изменения размера изображений в моем браузере. Получается, что качество очень низкое. Я нашел это: отключить интерполяцию при масштабировании , но это не помогает повысить качество.

Ниже приведен мой css и JS код, а также изображение scalled с Photoshop и масштабируется в API canvas.

что мне нужно сделать, чтобы получить оптимальное качество при масштабировании изображений в браузере?

примечание: Я хочу, чтобы масштаб уменьшите большое изображение до маленького, измените цвет на холсте и отправьте результат с холста на сервер.

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

изображение изменено с помощью photoshop:

изображение изменено на холсте:

Edit:

Я попытался сделать масштабирование в несколько шагов, как это было предложено в:

изменение размера изображения на холсте 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 шагам качество изображения значительно улучшается, но чем больше шагов вы добавляете в процесс, тем более нечетким становится изображение.

есть ли способ решить проблему, что изображение становится более нечетким, чем больше шагов вы добавляете?

Edit 2013-10-04: я попробовал алгоритм GameAlchemist. Вот результат по сравнению с фотошопом.

Фотошоп Изображения:

GameAlchemist по Алгоритм:

10 142

10 ответов:

Так как ваша проблема заключается в уменьшении масштаба изображения, нет смысла говорить об интерполяции, которая заключается в создании пикселя. Проблема здесь заключается в понижении дискретизации.

чтобы понизить пример изображения, нам нужно превратить каждый квадрат пикселей p * p в исходном изображении в один пиксель в целевом изображении.

по причинам производительности браузеры делают очень простую понижающую дискретизацию : чтобы построить меньшее изображение, они просто выберут один пиксель в источнике и используют его значение для назначения. который забывает некоторые детали и добавляет шум.

но есть исключение из этого : поскольку 2x изображение downsampling очень просто вычислить (в среднем 4 пикселя, чтобы сделать один) и используется для retina/HiDPI пикселей, этот случай обрабатывается правильно-браузер использует 4 пикселя, чтобы сделать один-.

но... если вы используете несколько раз 2x downsampling, вы столкнетесь с проблемой, что последовательные ошибки округления добавят слишком много шум.
Что еще хуже, вы не всегда будете изменять размер в два раза, а изменение размера до ближайшей мощности + последнее изменение размера очень шумно.

то, что вы ищете,-это пиксельная идеальная понижающая дискретизация, то есть : повторная выборка изображения, которая будет учитывать все входные пиксели-независимо от масштаба.
Для этого мы должны вычислить для каждого входного пикселя его вклад в один, два или четыре пикселя назначения в зависимости от того, находится ли масштабированная проекция входных пикселей прямо внутри назначение пикселей, перекрывает х границы, изменение границы, или оба.
(Схема была бы хороша здесь, но у меня ее нет. )

вот пример масштаба холста против моего пиксельного идеального масштаба на 1/3 масштаба зомбата.

обратите внимание, что изображение может быть масштабировано в вашем браузере, и есть .jpegized by S. O..
Тем не менее, мы видим, что там гораздо меньше шума, особенно в траве позади вомбата, и ветви справа от него. Шум в мехе делает его больше контрастно, но похоже, что у него белые волосы-в отличие от исходного изображения.
Правильный образ менее броский, но определенно лучше.

enter image description here

вот код для идеального масштабирования пикселей:

результат скрипку : 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 до 40 мс.

быстрая повторная выборка холста с хорошим качеством: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);
}

предложение 1-расширить технологический трубопровод

вы можете использовать шаг вниз, как я описываю в ссылках, на которые вы ссылаетесь, но вы, кажется, используете их неправильно.

шаг вниз не требуется для масштабирования изображений в соотношениях выше 1: 2 (как правило, но не ограничиваясь). Именно там вам нужно сделать кардинальные down-scaling вам нужно разделить его на два (и редко, больше) шага в зависимости от содержимого изображения (в частности, где высокие частоты, такие как тонкие линии происходить.)

каждый раз, когда вы вниз-образец изображения вы потеряете данные и информация. Вы не можете ожидать, что результирующее изображение будет таким же четким, как оригинал.

если вы затем масштабирование изображений во многих шагах вы потеряете много информации в общей сложности, и результат будет плохим, как вы уже заметили.

попробуйте только с одним дополнительным шагом, или в верхней части два.

сверток

в случае Photoshop обратите внимание, что он применяет свертка после повторной выборки изображения, например резкость. Это не просто двухкубическая интерполяция, поэтому для полной эмуляции Photoshop нам нужно также добавить шаги, которые делает Photoshop (с настройкой по умолчанию).

для этого примера я буду использовать свой оригинальный ответ, который вы ссылаетесь в своем посте, но я добавил к нему сверток sharpen, чтобы улучшить качество как почтовый процесс (см. демонстрацию внизу).

вот код для добавления фильтра резкость (это на основе общего фильтра свертки - я поставил матрицу веса для резкости внутри него, а также коэффициент смешивания, чтобы настроить произношение эффекта):

использование:

sharpen(context, width, height, mixFactor);

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

в результатом использования этой комбинации будет:

ОНЛАЙН ДЕМО ЗДЕСЬ

Result downsample and sharpen convolution

в зависимости от того, сколько резкости вы хотите добавить в смесь, вы можете получить результат от "размытого" по умолчанию до очень резкого:

Variations of sharpen

предложение 2-реализация алгоритма низкого уровня

если вы хотите получить лучший результат по качеству, вам нужно будет перейти на низкий уровень и рассмотреть возможность реализации для пример этого совершенно нового алгоритма для этого.

посмотреть Интерполяционно-Зависимая Понижающая Дискретизация Изображения (2011) от IEEE.
вот ссылка на статью в полном объеме (PDF).

в настоящее время нет реализаций этого алгоритма в JavaScript AFAIK, поэтому вы находитесь в полной готовности, если хотите бросить себя на эту задачу.

суть есть (выдержки из бумага):

Аннотация

предложен интерполяционно-ориентированный адаптивный алгоритм нисходящей выборки для низкоскоростного кодирования изображений в этой статье. Учитывая изображение, предложенный алгоритм способен получить изображение низкого разрешения, из что высокое качество изображения с тем же разрешением, что и вход изображение может быть интерполировано. Отличается от традиционного алгоритмы вниз-забора, которые независимы от интерполяция процесс, предложенный алгоритм вниз-выборки шарниров вниз-выборка к процессу интерполяции. Следовательно, предложенный алгоритм нисходящей выборки способен поддерживать оригинал информация входного изображения в наибольшей степени. Вниз-пробы затем изображение подается в JPEG. Общая вариация (ТВ) на основе сообщения затем обработка применяется к распакованному изображению с низким разрешением. В конечном счете, обработанное изображение интерполируется для поддержания оригинальное решение входное изображение. экспериментальные результаты проверки что использовать изображение уменьшено с помощью предложенного алгоритма, интерполированное изображение с гораздо более высоким качеством может быть достигнуто. Кроме того, предложенный алгоритм способен достичь более высокой производительности, чем JPEG для низкоскоростного кодирования изображений.

Snapshot from paper

(см. предоставленную ссылку для всех деталей, формул и т. д.)

зачем использовать холст для изменения размеров изображений? Современные браузеры все используют бикубическую интерполяцию - тот же процесс, который используется 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}           
}

EDIT изменить image до img чтобы соответствовать функции args. ^)^

затем просто используйте возвращенное значение для изменения размера изображения в одном или обоих измерениях.

очевидно, что есть различные уточнения, которые вы могли бы сделать, но это получает работу.

дополнительное соглашение

вставьте следующий код в консоль этой страницы и посмотрите, что случается с граватарами:

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

Если вы хотите использовать только холст, лучший результат будет с несколькими downsteps. Но пока это не очень хорошо. Для лучшего качества вам нужна чистая реализация js. Мы только что выпустили pica - высокоскоростной downscaler с переменным качеством / скоростью. Вкратце, он изменяет размер 1280*1024px в ~0.1 s, и изображение 5000*3000px в 1s, с самым высокомарочным (фильтр lanczos с 3 лепестками). Пика имеет демо, где вы можете играть со своими изображениями, уровнями качества и даже попробовать его мобильные средства.

PICA еще не имеет нерезкой маски, но это будет добавлено очень скоро. Это гораздо проще, чем реализовать высокоскоростной фильтр свертки для изменения размера.

Не правильный ответ для людей, которым действительно нужно изменить размер самого изображения,но просто уменьшить размер файла.

У меня была проблема с фотографиями "прямо с камеры", которые мои клиенты часто загружали в "несжатый" JPEG.

Не так хорошо известно, что canvas поддерживает (в большинстве браузеров 2017) изменение качества JPEG

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

С помощью этого трюка я мог бы уменьшить 4K x 3K pics с >10 МБ до 1 или 2 Мб, конечно, это зависит на ваши нужды.

посмотреть здесь

вот многоразовый угловой сервис для изменения размера изображения / холста высокого качества:https://gist.github.com/fisch0920/37bac5e741eaec60e983

сервис поддерживает свертку lanczos и пошаговое масштабирование. Метод свертки имеет более высокое качество за счет снижения скорости, в то время как поэтапный подход к масштабированию дает достаточно сглаженные результаты и значительно быстрее.

пример использования:

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

это улучшенный фильтр изменения размера Hermite, который использует 1 работника, чтобы окно не замерзало.

https://github.com/calvintwr/Hermite-resize

Я нашел решение, которое не нуждается в непосредственном доступе к данным пикселей и цикле через него для выполнения понижающей дискретизации. В зависимости от размера изображения, это может быть очень ресурсоемким, и было бы лучше использовать внутренние алгоритмы браузера.

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

кредиты этот пост

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>