HTML5 Полотно, розмір зображення (зменшення масштабу) Висока якість зображення?


149

Я використовую елементи полотна html5 для зміни розміру зображень у своєму браузері. Виявляється, якість дуже низька. Я виявив це: вимкнути інтерполяцію, коли масштабувати <canvas>, але це не сприяє підвищенню якості.

Нижче мій код css та js, а також зображення, розширене за допомогою Photoshop та масштабування в полотні API.

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

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

CSS:

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

JS:

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


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

Змінено розмір зображення за допомогою фотошопу:

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

Змінено розмір зображення на полотні:

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

Редагувати:

Я намагався зробити зменшення масштабу в декількох кроках, як було запропоновано в:

Зміна розміру зображення на полотні HTML5 та малюнок канти Html5: як застосувати антиалізинг

Це функція, яку я використав:

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

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

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

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

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


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

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

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

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

    } // end for


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


}

Ось результат, якщо я використовую розмір у 2 кроки вниз:

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

Ось результат, якщо я використовую розмір на 3 кроки вниз:

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

Ось результат, якщо я використовую розмір 4 кроків вниз:

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

Ось результат, якщо я використовую розмір 20 кроків вниз:

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

Примітка. Виходить, що від 1 кроку до 2 кроків значно покращується якість зображення, але чим більше кроків ви додаєте до процесу, тим зображення стає нечіткішим.

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

Редагувати 2013-10-04: Я спробував алгоритм GameAlchemist. Ось результат порівняно з Photoshop.

Зображення PhotoShop:

Зображення PhotoShop

Алгоритм GameAlchemist:

Алгоритм GameAlchemist


2
Ви можете спробувати поступово масштабування зображення: stackoverflow.com/questions/18761404 / ...
MARKE

1
можливий дублікат малюнка канти Html5: як застосувати антиалізинг . Подивіться, якщо це не працює. Якщо зображення великі та зменшені до невеликого розміру, вам потрібно буде робити це поетапно (див. Приклад зображень за посиланням)

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

1
@ Ken-AbdiasSoftware Я спробував вам підійти, але проблема полягає в тому, що вона погіршиться тим більше раундів, які я використовую для поетапного масштабування. Будь-яка ідея, як це виправити?
confile

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

Відповіді:


171

Оскільки ваша проблема полягає в тому, щоб зменшити масштаб зображення, немає сенсу говорити про інтерполяцію - що стосується створення пікселів. Проблема тут - зменшення розміру.

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

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

І все ж є виняток із цього: оскільки вимір 2X зображення дуже простий для обчислення (в середньому 4 пікселі, щоб зробити один) і використовується для сітківки / HiDPI пікселів, цей випадок обробляється належним чином - браузер використовує 4 пікселі для створення одно-.

АЛЕ ... якщо ви кілька разів використовуватимете 2-кратне зменшення тиску, ви зіткнетеся з проблемою, що послідовні помилки округлення додадуть занадто багато шуму.
Що ще гірше, ви не завжди будете змінювати розмір потужністю дві, а зміна розміру до найближчої потужності + останнє зміна дуже шумно.

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

чи бачите ви шанс зробити свій алгоритм швидшим, зберігаючи якість?
confile

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

Мені потрібна найкраща якість
confile

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

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

добре, ви можете використовувати будь-яке інше зображення PNG з прозорими ділянками. Будь-яка ідея з цього приводу?
confile

4
@confile ви мали рацію, у деяких випадках прозорі зображення мали проблеми у гострих місцях. Я пропустив ці випадки своїм тестом. Також виправлено фіксовану підтримку віддаленого зображення на скрипці: jsfiddle.net/9g9Nv/49
ViliusL

28

Пропозиція 1 - продовжити технологічну трубопровід

Ви можете скористатися спадною схемою, як я описав у посиланнях, на які ви посилаєтесь, але, здається, ви використовуєте їх неправильно.

Крок вниз не потрібен для масштабування зображень до співвідношень вище 1: 2 (як правило, але не обмежується). Саме там вам потрібно зробити різке масштабування вниз, потрібно розділити його на два (а рідше і більше) кроки залежно від вмісту зображення (зокрема там, де трапляються високі частоти, такі як тонкі лінії).

Щоразу, коли ви знімаєте зображення, ви втрачаєте деталі та інформацію. Ви не можете очікувати, що отримане зображення буде таким же чітким, як оригінал.

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

Спробуйте лише один додатковий крок, або два вершини.

Звитки

Якщо фотошоп помітить, що він застосовує згортку після повторної вибірки зображення, наприклад, різкості. Це не просто двокубічна інтерполяція, яка має місце для того, щоб повністю імітувати 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. Тоді до декомпресованого зображення низької роздільної здатності застосовується тотальна варіація (TV) післяобробки. Зрештою,Експериментальні результати підтверджують, що за допомогою запропонованого алгоритму зображення, зображене в ображеному зображенні, може бути досягнуте інтерпольоване зображення з набагато більш високою якістю. Крім того, запропонований алгоритм здатний досягти більш високої продуктивності, ніж JPEG для кодування зображень з низькою швидкістю передачі.

Знімок з паперу

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


Це також чудове рішення. Дякую!
confile

Це чудове рішення. Я спробував це на png-файлах із прозорими областями. Ось результат: jsfiddle.net/confile/5CD4N У вас є ідеї, що потрібно зробити, щоб це працювало?
confile

1
це геній! але, будь ласка, можете пояснити, що саме ви робите? хаха .. я цілком хочу знати, які плюси і виходи ... можливо ресурси, щоб навчитися?
carinlynchin

1
@Carine, що може бути значно полегшеним для поля коментарів :), але, зменшуючи розмір, повторно виділяє групу пікселів, щоб у середньому був новий, що представляє цю групу. Це фактично фільтр низьких частот, який вводить загальну розмитість. Для компенсації втрати різкості просто застосуйте згортання. Оскільки різкість може бути дуже вираженою, ми можемо замішати її із зображенням, щоб ми могли контролювати рівень різкості. Сподіваюсь, що дає деяке розуміння.

21

Якщо ви хочете використовувати лише полотно, найкращим результатом буде багаторазовий крок. Але це ще не добре. Для кращої якості вам потрібна чиста реалізація js. Ми щойно випустили pica - високошвидкісний знижувальний пристрій зі змінною якістю / швидкістю. Коротше кажучи, вона має розмір 1280 * 1024 пікселів за ~ 0,1 сек і 5000 * 3000 пікселів за 1 секунди, з найвищою якістю (фільтр ланкцо з 3 часточками). У Pica є демонстрація , де ви можете грати зі своїми зображеннями, рівнями якості та навіть приміряти їх на мобільних пристроях.

У Pica ще немає чіткої маски, але вона буде додана дуже скоро. Це набагато простіше, ніж застосувати фільтр високої швидкості згортки для зміни розміру.


16

Навіщо використовувати полотно для зміни розміру зображень? Усі сучасні браузери використовують двоярусну інтерполяцію - той самий процес, який використовує Photoshop (якщо ви робите це правильно) - і вони роблять це швидше, ніж процес полотна. Просто вкажіть потрібний розмір зображення (використовуйте лише один розмір, висоту чи ширину, щоб пропорційно змінити розмір).

Це підтримується більшістю браузерів, включаючи більш пізні версії IE. Більш ранні версії можуть вимагати CSS, що залежить від браузера .

Проста функція (використовуючи jQuery) для зміни розміру зображення буде такою:

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

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

Потім просто використовуйте повернене значення, щоб змінити розмір зображення в одному або обох вимірах.

Очевидно, що ви можете внести різні вдосконалення, але це робить роботу.

Вставте наступний код у консоль цієї сторінки і подивіться, що відбувається з граватарами:

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 Мені потрібно потім покласти зображення на полотно і пізніше надіслати його на сервер. Я хочу масштабувати велике зображення до маленького, змінити колір на полотні та надіслати результат серверу. Як ви думаєте, що мені робити?
confile

9
@Robusto У цьому проблема. Показати невелике зображення на клієнті легко. img.width nad img.the висота настільки тривіальна. Я хочу її масштабувати лише один раз і знову не на сервері.
confile

8

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

У мене виникли проблеми із фотографіями "безпосередньо з камери", які мої клієнти часто завантажували у "нестиснутому" JPEG.

Не так добре відомо, що полотно підтримує (у більшості браузерів 2017 року) для зміни якості JPEG

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

За допомогою цього фокусу я міг би зменшити 4k x 3k фотографії з> 10Mb до 1 або 2Mb, впевнений, що це залежить від ваших потреб.

Послухайте


4

Ось послуга кутового багаторазового використання для високої якості розміру зображення / полотна: https://gist.github.com/fisch0920/37bac5e741eaec60e983

Служба підтримує згортання ланксосу та поетапне зменшення масштабу. Підхід згортання вищої якості за рахунок повільніше, тоді як покроковий підхід до зменшення масштабу дає обґрунтовано антиалійні результати та значно швидший.

Приклад використання:

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

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

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

4

Це вдосконалений фільтр розміру 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 та деякі отримані зображення?
confile

У посиланні внизу ви можете знайти отримані зображення за допомогою цієї методики
Хесус Каррера

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

замість .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

DEMO : Змінення розміру зображень за допомогою скрипта демонстрації JS та HTML Canvas.

Ви можете знайти три різних способи змінити цей розмір, який допоможе вам зрозуміти, як працює код і чому.

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.