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


149

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

Ниже мой код CSS и JS, а также изображение, масштабированное с помощью Photoshop и масштабированное в Canvas 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. Вот результат по сравнению с фотошопом.

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

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

Алгоритм GameAlchemist:

Алгоритм GameAlchemist


2
Вы можете попробовать постепенно масштабировать свое изображение: stackoverflow.com/questions/18761404/…
markE

1
Возможный дубликат Html5 canvas drawImage: как применять сглаживание . Посмотрите, не работает ли это. Если изображения большие и уменьшены до маленького размера, вам нужно будет сделать это поэтапно (см. Примеры изображений в ссылке)

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

1
@ Ken-AbdiasSoftware Я попробовал подход, но проблема в том, что чем больше я использую для пошагового масштабирования, тем хуже будет. Есть идеи, как это исправить?
конфил

3
Конечно, шансы тиражировать функциональность дорогого профессионального программного обеспечения для редактирования фотографий с использованием HTML5 довольно малы? Вы, вероятно, можете приблизиться (иш), но именно так, как это работает в Photoshop, я думаю, было бы невозможно!
Лиам

Ответы:


171

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

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

По соображениям производительности браузеры выполняют очень простую понижающую дискретизацию: для создания меньшего изображения они просто выбирают ОДИН пиксель в источнике и используют его значение для пункта назначения. который «забывает» некоторые детали и добавляет шум.

Тем не менее, есть исключение: так как понижающую дискретизацию изображения 2X очень просто вычислить (в среднем 4 пикселя на единицу) и она используется для пикселей Retina / HiDPI, этот случай обрабатывается должным образом - браузер использует 4 пикселя для один-.

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

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

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

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

введите описание изображения здесь

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

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


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

я не понимаю ... кажется, это то, что я делаю. Буфер и созданный мной холст (resCV) имеют размер масштабированного изображения. Я думаю, что единственный способ сделать это быстрее - использовать целочисленные вычисления, подобные брешенам. Но 40 мс медленнее только для видеоигры (25 к / с), но не для приложения для рисования.
GameAlchemist

Видите ли вы шанс сделать ваш алгоритм быстрее, сохраняя при этом качество?
конфил

1
я попытался округлить буфер (последняя часть алгоритма), используя 0 | вместо Mat.ceil. Это немного быстрее. Но в любом случае с get / putImageData есть некоторые накладные расходы, и опять же, мы не можем избежать обработки каждого пикселя.
GameAlchemist

4
Итак, я посмотрел код: вы были очень близки к решению. Две ошибки: ваши индексы были отключены по одному для tX + 1 (они были + 3, + 4, + 5, + 6 вместо +4, +5, +6, +7), и изменение строки в rgba - это муль 4, а не 3. Я только что проверил 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);
}

Мне нужно лучшее качество
конфил

18
исправлено, я изменил "хорошо" на "лучше", теперь это нормально? : D. С другой стороны, если вы хотите наилучшую возможную повторную выборку - используйте imagemagick.
ViliusL

@confile imgur.com было безопасно использовать в jsfiddle, но администраторы сделали что-то не так? Вы не видите хорошего качества, потому что ваш браузер выдает CORS фатальную ошибку. (нельзя использовать изображение с удаленных сайтов)
ViliusL

Хорошо, вы можете использовать любое другое изображение PNG с прозрачными областями. Есть идеи по этому поводу?
confile

4
@ Убедитесь, что вы были правы, в некоторых случаях прозрачные изображения имели проблемы в резких областях. Я пропустил эти случаи с моим тестом. Исправлено изменение размера, а также исправлена ​​поддержка удаленного образа на скрипке: 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 - реализация алгоритма низкого уровня

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

См. Интерполяционно-зависимая понижающая дискретизация изображений (2011) от IEEE.
Вот ссылка на статью полностью (PDF) .

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

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

Аннотация

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

Снимок из бумаги

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


Это тоже отличное решение. Спасибо!
confile

Это отличное решение. Я попробовал это на png файлах с прозрачными областями. Вот результат: jsfiddle.net/confile/5CD4N Вы знаете, что нужно сделать, чтобы это работало?
confile

1
это гений! но, пожалуйста, вы можете объяснить, что именно вы делаете? лол .. я очень хочу узнать все подробности ... может быть, ресурсы для изучения?
Каринлынчин

1
@Carine, что может быть немного для плохого поля комментариев :), но, уменьшая масштаб, повторно выбирает группу пикселей, чтобы усреднить новый, представляющий эту группу. По сути, это фильтр нижних частот, который вносит некоторое размытие в целом. Чтобы компенсировать потерю резкости, просто примените затачивающуюся свертку. Поскольку резкость может быть очень выраженной, мы можем вместо этого смешать ее с изображением, чтобы мы могли контролировать уровень резкости. Надеюсь, что это дает некоторое представление.

21

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

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


16

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

Это поддерживается большинством браузеров, включая более поздние версии IE. Более ранние версии могут требовать специфичного для браузера CSS .

Простая функция (использующая jQuery) для изменения размера изображения будет выглядеть так:

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

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

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

Очевидно, что вы можете внести некоторые улучшения, но это сделает работу.

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

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
Также обратите внимание, что если вы укажете только одно измерение, (современный) браузер автоматически сохранит естественное соотношение сторон изображения.
Андре Дион

38
Возможно, ему нужно отправить измененное изображение на сервер.
Серджиу Параскив

2
@Sergiu: Не обязательно, но учтите, что если вы переходите от очень маленького изображения к очень большому, вы не получите отличных результатов даже с сервера.
Робусто,

2
@Robusto После этого мне нужно поместить изображение на холст и позже отправить его на сервер. Я хочу уменьшить большое изображение до маленького, изменить цвет на холсте и отправить результат на сервер. Что ты думаешь я должен сделать?
конфил

9
@Robusto Это проблема. Показывать маленькое изображение на клиенте легко. img.width nad img.height так тривиально. Я хочу уменьшить его только один раз, а не снова на сервере.
конфиле

8

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

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

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

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

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

Смотри сюда


4

Вот многоразовый сервис Angular для высококачественного изменения размера изображения / холста: 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

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

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 и некоторые полученные изображения?
конфил

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

вместо 0,85 , если мы добавим 1,0 . Вы получите точный ответ.

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 fiddler.

Вы можете найти 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
        }
    };
}}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.