Html5 canvas drawImage: як застосувати згладжування


82

Будь ласка, подивіться на наступний приклад:

http://jsfiddle.net/MLGr4/47/

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

img = new Image();
img.onload = function(){
    canvas.width = 400;
    canvas.height = 150;
    ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 400, 150);
}
img.src = "http://openwalls.com/image/1734/colored_lines_on_blue_background_1920x1200.jpg";

Як бачите, зображення не згладжується, хоча кажуть, що drawImage застосовує згладжування автоматично. Я пробував багато різних способів, але, здається, це не працює. Не могли б ви сказати мені, як я можу отримати згладжене зображення? Дякую.

Відповіді:


178

Причина

Деякі зображення дуже важко взяти за вибірку та інтерполювати, наприклад, цей із кривими, коли потрібно перейти від великого розміру до малого.

Здається, що браузери зазвичай використовують дволінійну (вибірку 2х2) інтерполяцію з елементом полотна, а не двокубічну (дискретизація 4х4) з причин (ймовірних) ефективності.

Якщо крок занадто великий, тоді просто не вистачає пікселів для вибірки, що відображається в результаті.

З точки зору сигналу / DSP ви можете побачити це як занадто високе порогове значення фільтра низьких частот, що може призвести до псевдоніму, якщо в сигналі багато високих частот (деталей).

Рішення

Оновлення 2018:

Ось акуратний трюк, який можна використовувати для браузерів, що підтримують filterвластивість у 2D-контексті. Це попередньо розмиває зображення, яке, по суті, є таким самим, як і повторна вибірка, а потім зменшується. Це дозволяє робити великі кроки, але потрібні лише два кроки та дві нічиї.

Попереднє розмиття, використовуючи кількість радіусів (оригінальний розмір / розмір призначення / 2) як радіус (можливо, вам доведеться відрегулювати це евристично на основі браузера та непарних / парних кроків - тут показано лише спрощено):

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

if (typeof ctx.filter === "undefined") {
 alert("Sorry, the browser doesn't support Context2D filters.")
}

const img = new Image;
img.onload = function() {

  // step 1
  const oc = document.createElement('canvas');
  const octx = oc.getContext('2d');
  oc.width = this.width;
  oc.height = this.height;

  // steo 2: pre-filter image using steps as radius
  const steps = (oc.width / canvas.width)>>1;
  octx.filter = `blur(${steps}px)`;
  octx.drawImage(this, 0, 0);

  // step 3, draw scaled
  ctx.drawImage(oc, 0, 0, oc.width, oc.height, 0, 0, canvas.width, canvas.height);

}
img.src = "//i.stack.imgur.com/cYfuM.jpg";
body{ background-color: ivory; }
canvas{border:1px solid red;}
<br/><p>Original was 1600x1200, reduced to 400x300 canvas</p><br/>
<canvas id="canvas" width=400 height=250></canvas>

Підтримка фільтра як ogf жовтень / 2018:

Оновлення 2017: Тепер у специфікаціях визначено нову властивість для встановлення якості передискретизації:

context.imageSmoothingQuality = "low|medium|high"

Наразі це підтримується лише в Chrome. Фактичні методи, що застосовуються на рівні, залишаються за рішенням постачальника, але розумно вважати Lanczos за "високий" або щось рівнозначне за якістю. Це означає, що зниження може бути взагалі пропущено, або можна використовувати більші кроки з меншою кількістю перемальовок, залежно від розміру зображення та

Підтримка imageSmoothingQuality:

браузер. До того часу ..:
Кінець передачі

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

Це дозволить отримати хороші результати також при дволінійній інтерполяції (насправді вона поводиться приблизно як бікубічна при цьому), а накладні витрати мінімальні, оскільки на кожному кроці менше пікселів для вибірки.

Ідеальний крок - перейти до половини роздільної здатності на кожному кроці, поки ви не встановите цільовий розмір (спасибі Джо Мейбл за згадку про це!).

Модифікована скрипка

Використання прямого масштабування, як у вихідному питанні:

ЗВИЧАЙНЕ ЗМЕНШЕНО ЗМЕНШЕННЯ

Використовуючи зниження, як показано нижче:

ВНИЗ КРОКОВЕ ЗОБРАЖЕННЯ

У цьому випадку вам потрібно буде піти у три кроки:

На кроці 1 ми зменшуємо зображення наполовину, використовуючи позаекранне полотно:

// step 1 - create off-screen canvas
var oc   = document.createElement('canvas'),
    octx = oc.getContext('2d');

oc.width  = img.width  * 0.5;
oc.height = img.height * 0.5;

octx.drawImage(img, 0, 0, oc.width, oc.height);

Крок 2 повторно використовує позаекранне полотно і знову малює зображення, зменшене до половини:

// step 2
octx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5);

І ми ще раз малюємо до основного полотна, знову зменшеного до половини, але до остаточного розміру:

// step 3
ctx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5,
                  0, 0, canvas.width,   canvas.height);

Порада:

Ви можете розрахувати загальну кількість необхідних кроків, використовуючи цю формулу (вона включає останній крок для встановлення цільового розміру):

steps = Math.ceil(Math.log(sourceWidth / targetWidth) / Math.log(2))

4
Працюючи з деякими дуже великими початковими зображеннями (8000 x 6000 і вище), я вважаю корисним в основному повторити крок 2, поки не отримаю коефіцієнт 2 бажаного розміру.
Joe Mabel

Працює як шарм! Спасибі!
Влад Цепелєв

1
Мене бентежить різниця між 2-м і 3-м кроком ... хтось може пояснити?
carinlynchin

1
@Carine це дещо складно, але полотно намагається врятувати png якомога швидше. Файл png внутрішньо підтримує 5 різних типів фільтрів, які можуть покращитись при стисненні (gzip), але для того, щоб знайти найкращу комбінацію, усі ці фільтри повинні бути протестовані на кожен рядок зображення. Це забирає багато часу для великих зображень і може заблокувати браузер, тому більшість браузерів просто використовують фільтр 0 і виштовхують його, сподіваючись отримати певне стиснення. Ви можете зробити цей процес вручну, але це, очевидно, трохи більше роботи. Або запустіть його через службові API, такі як tinypng.com.

1
@Kaiido це не забувається, і "копіювання" відбувається дуже повільно. Якщо вам потрібна прозорість, швидше використовувати clearRect () та використовувати main або alt. полотно як ціль.

12

Я настійно рекомендую pica для таких завдань. Його якість перевершує багаторазове зменшення розмірів і одночасно є досить швидкою. Ось демонстрація .


4
    var getBase64Image = function(img, quality) {
    var canvas = document.createElement("canvas");
    canvas.width = img.width;
    canvas.height = img.height;
    var ctx = canvas.getContext("2d");

    //----- origin draw ---
    ctx.drawImage(img, 0, 0, img.width, img.height);

    //------ reduced draw ---
    var canvas2 = document.createElement("canvas");
    canvas2.width = img.width * quality;
    canvas2.height = img.height * quality;
    var ctx2 = canvas2.getContext("2d");
    ctx2.drawImage(canvas, 0, 0, img.width * quality, img.height * quality);

    // -- back from reduced draw ---
    ctx.drawImage(canvas2, 0, 0, img.width, img.height);

    var dataURL = canvas.toDataURL("image/png");
    return dataURL;
    // return dataURL.replace(/^data:image\/(png|jpg);base64,/, "");
}

1
який діапазон значень параметра "якість"?
serup

між нулем та
Іван Родрігес,

4

На додаток до відповіді Кена, тут є ще одне рішення для зменшення вибірки навпіл (так що результат виглядає добре, використовуючи алгоритм браузера):

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

3

Якщо хтось ще шукає відповідь, є інший спосіб, за допомогою якого ви можете використовувати фонове зображення замість drawImage (). Ви таким чином не втратите жодної якості зображення.

JS:

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");
   var url = "http://openwalls.com/image/17342/colored_lines_on_blue_background_1920x1200.jpg";

    img=new Image();
    img.onload=function(){

        canvas.style.backgroundImage = "url(\'" + url + "\')"

    }
    img.src="http://openwalls.com/image/17342/colored_lines_on_blue_background_1920x1200.jpg";

робоча демонстрація


2

Я створив багаторазовий сервіс 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
    })
})
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.