Html5 canvas drawImage: как применить сглаживание
Пожалуйста, посмотрите на следующий пример:
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 автоматически применяет сглаживание. Я пробовал много разных способов, но это не похоже на работу. Не могли бы вы сказать мне, как я могу получить сглаженное изображение? Благодарю.
7 ответов
причина
Некоторые изображения просто очень трудно дискретизировать и интерполировать, например, это с кривыми, когда вы хотите перейти от большого размера к маленькому.
Браузеры, как правило, используют билинейную (выборка 2x2) интерполяцию с элементом canvas, а не бикубическую (выборку 4x4) по (вероятным) причинам производительности.
Если шаг слишком велик, то для выборки просто не хватает пикселей, из которых отражается результат.
С точки зрения сигнала /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 = "https://stackru.com/images/91e6a40dc1c566e8e2b3c2c1ed4128390686c87e.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 Oct/2018:
CanvasRenderingContext2D.filter
api.CanvasRenderingContext2D.filter
On Standard Track, Experimental
https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/filter
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://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/imageSmoothingQuality
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
браузер. До тех пор..:
Конец передачи
Решение состоит в том, чтобы использовать понижение, чтобы получить надлежащий результат. Понижение означает, что вы постепенно уменьшаете размер, чтобы ограниченный диапазон интерполяции покрывал достаточное количество пикселей для выборки.
Это позволит получить хорошие результаты также с билинейной интерполяцией (на самом деле она ведет себя очень похоже на бикубичную), и накладные расходы минимальны, так как на каждом шаге требуется меньше пикселей для выборки.
Идеальный шаг - переходить к половине разрешения на каждом шаге, пока вы не установите целевой размер (спасибо Джо Мейбл за упоминание об этом!).
Используя прямое масштабирование, как в оригинальном вопросе:
Используя шаг вниз, как показано ниже:
В этом случае вам нужно будет уйти в 3 шага:
На шаге 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))
В дополнение к ответу Кена, вот еще одно решение для выполнения понижающей дискретизации пополам (поэтому результат выглядит хорошо с использованием алгоритма браузера):
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] );
Кредиты на этот пост
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,/, "");
}
Я создал многоразовый сервис 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
})
})
Если кто-то еще ищет ответ, есть еще один способ, которым вы можете использовать фоновое изображение вместо 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";
Масштабирование для дисплеев с высоким разрешением.
На дисплеях с более высоким разрешением элементы холста могут выглядеть размытыми. Хотя может существовать множество решений, простым первым шагом является одновременное масштабирование размера холста вверх и вниз, используя его атрибуты, стиль и масштаб контекста.
// Get the DPR and size of the canvas
const dpr = window.devicePixelRatio;
const rect = canvas.getBoundingClientRect();
// Set the "actual" size of the canvas
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
// Scale the context to ensure correct drawing operations
ctx.scale(dpr, dpr);
// Set the "drawn" size of the canvas
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${rect.height}px`;