Причина
Деякі зображення дуже важко взяти за вибірку та інтерполювати, наприклад, цей із кривими, коли потрібно перейти від великого розміру до малого.
Здається, що браузери зазвичай використовують дволінійну (вибірку 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() {
const oc = document.createElement('canvas');
const octx = oc.getContext('2d');
oc.width = this.width;
oc.height = this.height;
const steps = (oc.width / canvas.width)>>1;
octx.filter = `blur(${steps}px)`;
octx.drawImage(this, 0, 0);
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:
CanvasRenderingContext2D.filter
api.CanvasRenderingContext2D.filter
On Standard Track, Experimental
https:
DESKTOP > |Chrome |Edge |Firefox |IE |Opera |Safari
:----------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------
filter ! | 52 | ? | 49 | - | - | -
MOBILE > |Chrome/A |Edge/mob |Firefox/A |Opera/A |Safari/iOS|Webview/A
:----------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------
filter ! | 52 | ? | 49 | - | - | 52
! = Experimental
Data from MDN - "npm i -g mdncomp" (c) epistemex
Оновлення 2017: Тепер у специфікаціях визначено нову властивість для встановлення якості передискретизації:
context.imageSmoothingQuality = "low|medium|high"
Наразі це підтримується лише в Chrome. Фактичні методи, що застосовуються на рівні, залишаються за рішенням постачальника, але розумно вважати Lanczos за "високий" або щось рівнозначне за якістю. Це означає, що зниження може бути взагалі пропущено, або можна використовувати більші кроки з меншою кількістю перемальовок, залежно від розміру зображення та
Підтримка imageSmoothingQuality
:
CanvasRenderingContext2D.imageSmoothingQuality
api.CanvasRenderingContext2D.imageSmoothingQuality
On Standard Track, Experimental
https:
DESKTOP > |Chrome |Edge |Firefox |IE |Opera |Safari
:----------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:
imageSmoothingQuality !| 54 | ? | - | ? | 41 | Y
MOBILE > |Chrome/A |Edge/mob |Firefox/A |Opera/A |Safari/iOS|Webview/A
:----------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:
imageSmoothingQuality !| 54 | ? | - | 41 | Y | 54
! = Experimental
Data from MDN - "npm i -g mdncomp" (c) epistemex
браузер. До того часу ..:
Кінець передачі
Рішення полягає у використанні знижувального рівня для отримання належного результату. Пониження означає, що ви зменшуєте розмір кроками, щоб обмежений діапазон інтерполяції охоплював достатньо пікселів для вибірки.
Це дозволить отримати хороші результати також при дволінійній інтерполяції (насправді вона поводиться приблизно як бікубічна при цьому), а накладні витрати мінімальні, оскільки на кожному кроці менше пікселів для вибірки.
Ідеальний крок - перейти до половини роздільної здатності на кожному кроці, поки ви не встановите цільовий розмір (спасибі Джо Мейбл за згадку про це!).
Модифікована скрипка
Використання прямого масштабування, як у вихідному питанні:
Використовуючи зниження, як показано нижче:
У цьому випадку вам потрібно буде піти у три кроки:
На кроці 1 ми зменшуємо зображення наполовину, використовуючи позаекранне полотно:
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 повторно використовує позаекранне полотно і знову малює зображення, зменшене до половини:
octx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5);
І ми ще раз малюємо до основного полотна, знову зменшеного до половини, але до остаточного розміру:
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))