使用 javascript 画布调整图像大小(平滑)

我试图调整一些图像的大小与帆布,但我不知道如何平滑他们。 在 Photoshop,浏览器等等。他们使用了一些算法(比如双三次、双线性) ,但我不知道这些算法是否内置在画布中。

这是我的小提琴: http://jsfiddle.net/EWupT/

var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
canvas.width=300
canvas.height=234
ctx.drawImage(img, 0, 0, 300, 234);
document.body.appendChild(canvas);

第一个是一个正常调整大小的图像标记,第二个是画布。注意,画布没有那么光滑。怎样才能达到「流畅」 ?

364278 次浏览

大多数浏览器在调整图像大小时似乎都是使用 使用线性插值而不是双立方体

(更新在规范中增加了一个质量属性,即目前只能在 Chrome 中使用的 imageSmoothingQuality。)

除非一个人选择没有平滑或最近的邻居,浏览器将总是插值后,缩放它的图像作为一个低通滤波器的功能,以避免走样。

双线性使用2x2像素进行插值,而双三次使用4x4,所以通过分步进行,你可以得到接近双三次结果,同时使用双线性插值,在结果图像中可以看到。

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


img.onload = function () {


// set size proportional to image
canvas.height = canvas.width * (img.height / img.width);


// step 1 - resize to 50%
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);


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


// step 3, resize to final size
ctx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5,
0, 0, canvas.width, canvas.height);
}
img.src = "//i.imgur.com/SHo6Fub.jpg";
<img src="//i.imgur.com/SHo6Fub.jpg" width="300" height="234">
<canvas id="canvas" width=300></canvas>

根据调整大小的剧烈程度,如果差异较小,可以跳过步骤2。

在演示中,您可以看到新的结果现在非常类似于图像元素。

我创建了一个库,允许您降低任何百分比,同时保留所有的颜色数据。

Https://github.com/danschumann/limby-resize/blob/master/lib/canvas_resize.js

可以包含在浏览器中的文件。结果将看起来像 Photoshop 或图像魔术,保留所有的颜色数据,平均像素,而不是采取附近的和删除其他。它不用公式来猜测平均值,它需要精确的平均值。

我创建了一个可重用的 Angular 服务来处理高质量的图片/画布的大小调整,以满足任何感兴趣的人: https://gist.github.com/transitive-bullshit/37bac5e741eaec60e983

该服务包括两种解决方案,因为它们各有利弊。Lanczos 卷积方法以较慢的速度为代价获得了较高的质量,而逐步缩放方法产生了合理的抗锯齿结果,并且速度明显更快。

示例用法:

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

基于 K3N 的答案,我重写代码通常为任何人想要的

var oc = document.createElement('canvas'), octx = oc.getContext('2d');
oc.width = img.width;
oc.height = img.height;
octx.drawImage(img, 0, 0);
while (oc.width * 0.5 > width) {
oc.width *= 0.5;
oc.height *= 0.5;
octx.drawImage(oc, 0, 0, oc.width, oc.height);
}
oc.width = width;
oc.height = oc.width * img.height / img.width;
octx.drawImage(img, 0, 0, oc.width, oc.height);

更新 JSFIDLE 演示

这是我的 < strong > ONLINE DEMO

因为 Trung Le Nguyen Nhat 的小提琴根本不正确 (它只是在最后一步使用原始图像)
我自己写了一篇关于性能比较的文章:

小提琴

基本上就是:

img.onload = function() {
var canvas = document.createElement('canvas'),
ctx = canvas.getContext("2d"),
oc = document.createElement('canvas'),
octx = oc.getContext('2d');


canvas.width = width; // destination canvas size
canvas.height = canvas.width * img.height / img.width;


var cur = {
width: Math.floor(img.width * 0.5),
height: Math.floor(img.height * 0.5)
}


oc.width = cur.width;
oc.height = cur.height;


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


while (cur.width * 0.5 > width) {
cur = {
width: Math.floor(cur.width * 0.5),
height: Math.floor(cur.height * 0.5)
};
octx.drawImage(oc, 0, 0, cur.width * 2, cur.height * 2, 0, 0, cur.width, cur.height);
}


ctx.drawImage(oc, 0, 0, cur.width, cur.height, 0, 0, canvas.width, canvas.height);
}

我在前端编写了小型的 js 工具来裁剪和调整图像大小。下面是关于 GitHub 项目的 链接。也可以从最终图像中获取 blob 来发送它。

import imageSqResizer from './image-square-resizer.js'


let resizer = new imageSqResizer(
'image-input',
300,
(dataUrl) =>
document.getElementById('image-output').src = dataUrl;
);
//Get blob
let formData = new FormData();
formData.append('files[0]', resizer.blob);


//get dataUrl
document.getElementById('image-output').src = resizer.dataUrl;

虽然其中一些代码片段很短并且可以工作,但是遵循和理解它们并不容易。

由于我不是一个从堆栈溢出“复制粘贴”的球迷,我希望开发人员了解的代码,他们推到他们的软件,希望你会发现以下有用。

DEMO : 使用 JS 和 HTML Canvas DEMO fiddler 调整图像大小。

您可能会找到3种不同的方法来做这个调整,这将有助于您了解代码是如何工作和为什么。

Https://jsfiddle.net/1b68eldr/93089/

在 GitHub 项目中可以找到演示的完整代码和您可能希望在代码中使用的 TypeScript 方法。

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

我不明白为什么没有人建议 createImageBitmap

createImageBitmap(
document.getElementById('image'),
{ resizeWidth: 300, resizeHeight: 234, resizeQuality: 'high' }
)
.then(imageBitmap =>
document.getElementById('canvas').getContext('2d').drawImage(imageBitmap, 0, 0)
);

工作得很漂亮(假设您为图像和画布设置了 id)。

下面是我的代码,我希望它对 SO 社区中的某些人可能有用:

可以将目标图像维度作为参数包含在脚本调用中。这将是您的图像宽度或高度的结果值,以较大者为准。较小的尺寸是调整大小保持您的图像长宽比不变。您还可以在脚本中硬编码默认的目标大小。

您可以很容易地更改脚本以适应您的特定需求,例如您想要的输出图像类型(默认为“ image/png”) ,并决定您想要调整图像大小以获得更好结果的百分比步骤(参见代码中的 const centStep)。

   const ResizeImage = ( _ => {


const MAX_LENGTH = 260;     // default target size of largest dimension, either witdth or height
const percentStep = .3;     // resizing steps until reaching target size in percents (30% default)
const canvas = document.createElement("canvas");
const canvasContext = canvas.getContext("2d");
const image = new Image();


const doResize = (callback, maxLength) => {


// abort with error if image has a dimension equal to zero
if(image.width == 0 || image.height == 0) {
return {blob: null, error: "either image width or height was zero "};
}


// use caller dimension or default length if none provided
const length = maxLength == null  ? MAX_LENGTH : maxLength;


canvas.width = image.width;
canvas.height = image.height;
canvasContext.drawImage(image, 0, 0, image.width, image.height);
// if image size already within target size, just copy and return blob
if(image.width <= length && image.height <= length) {
canvas.toBlob( blob => {
callback({ blob: blob, error: null });
}, "image/png", 1);
return;
}


var startDim = Math.max(image.width, image.height);
var startSmallerDim = Math.min(image.width, image.height);


// gap to decrease in size until we reach the target size,
// be it by decreasing the image width or height,
// whichever is largest
const gap = startDim - length;
// step length of each resizing iteration
const step = parseInt(percentStep*gap);
//  no. of iterations
var nSteps = 0;
if(step == 0) {
step = 1;
} else {
nSteps = parseInt(gap/step);
}
// length of last additional resizing step, if needed
const lastStep = gap % step;
// aspect ratio = value by which we'll  multiply the smaller dimension
// in order to keep the aspect ratio unchanged in each iteration
const ratio = startSmallerDim/startDim;


var newDim;          // calculated new length for the bigger dimension of the image, be it image width or height
var smallerDim;     // length along the smaller dimension of the image, width or height
for(var i = 0; i < nSteps; i++) {
// decrease longest dimension one step in pixels
newDim = startDim - step;
// decrease shortest dimension proportionally, so as to keep aspect ratio
smallerDim = parseInt(ratio*newDim);
// assign calculated vars to their corresponding canvas dimension, width or height
if(image.width > image.height) {
[canvas.width, canvas.height]  = [newDim, smallerDim];
} else {
[canvas.width, canvas.height] = [smallerDim, newDim];
}
// draw image one step smaller
canvasContext.drawImage(canvas, 0, 0, canvas.width, canvas.height);
// cycle var startDim for new loop
startDim = newDim;
}


// do last missing resizing step to finally reach target image size
if(lastStep > 0) {
if(image.width > image.height) {
[canvas.width, canvas.height]  = [startDim - lastStep, parseInt(ratio*(startDim - lastStep))];
} else {
[canvas.width, canvas.height] = [parseInt(ratio*(startDim -lastStep)), startDim - lastStep];
}
canvasContext.drawImage(image, 0, 0, canvas.width, canvas.height);
}


// send blob to caller
canvas.toBlob( blob => {
callback({blob: blob, error: null});
}, "image/png", 1);


};


const resize = async (imgSrc, callback, maxLength) => {
image.src = imgSrc;
image.onload = _ => {
doResize(callback, maxLength);
};
};


return { resize: resize }


})();

用法:

ResizeImage.resize("./path/to/image/or/blob/bytes/to/resize", imageObject => {
if(imageObject.error != null) {
// handle errors here
console.log(imageObject.error);
return;
}
// do whatever you want with the blob, like assinging it to
// an img element, or uploading it to a database
// ...
document.querySelector("#my-image").src = imageObject.blob;
// ...
}, 300);

我解决了这个问题,使用比例为画布和图像质量在我的情况下变得非常好。

首先我缩放画布内部的内容:

ctx.scale(2, 2)

然后用 css 扩展画布标签:

#myCanvas { transform: scale(0.5); }
export const resizeImage = (imageFile, size = 80) => {
    

let resolver = ()=>{};


let reader = new FileReader();


reader.onload = function (e) {
let img = document.createElement("img");
img.onload = function (event) {
// Dynamically create a canvas element
let canvas = document.createElement("canvas");


canvas.width=size;
canvas.height=size;


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


// Actual resizing
ctx.drawImage(img, 0, 0, size, size);


// Show resized image in preview element
let dataurl = canvas.toDataURL(imageFile.type);


resolver(dataurl);
}
img.src = e.target.result;
}


reader.readAsDataURL(imageFile);


    

return new Promise((resolve, reject) => {
resolver = resolve;
})
};