在 HTML5 画布中调整图像大小

IT技术 javascript html canvas image-resizing
2021-01-22 18:28:33

我正在尝试使用 javascript 和 canvas 元素在客户端创建一个缩略图图像,但是当我缩小图像时,它看起来很糟糕。它看起来好像在 Photoshop 中缩小了尺寸,重采样设置为“最近的邻居”而不是双三次。我知道有可能让它看起来正确,因为这个网站也可以使用画布来做这件事。我已经尝试使用与“[Source]”链接中所示相同的代码,但它看起来仍然很糟糕。有什么我遗漏的,需要设置的一些设置还是什么?

编辑:

我正在尝试调整 jpg 的大小。我尝试在链接的网站和 photoshop 中调整相同的 jpg 大小,缩小后看起来不错。

这是相关的代码:

reader.onloadend = function(e)
{
    var img = new Image();
    var ctx = canvas.getContext("2d");
    var canvasCopy = document.createElement("canvas");
    var copyContext = canvasCopy.getContext("2d");

    img.onload = function()
    {
        var ratio = 1;

        if(img.width > maxWidth)
            ratio = maxWidth / img.width;
        else if(img.height > maxHeight)
            ratio = maxHeight / img.height;

        canvasCopy.width = img.width;
        canvasCopy.height = img.height;
        copyContext.drawImage(img, 0, 0);

        canvas.width = img.width * ratio;
        canvas.height = img.height * ratio;
        ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height);
    };

    img.src = reader.result;
}

编辑2:

似乎我弄错了,链接的网站在缩小图像尺寸方面并没有做得更好。我尝试了建议的其他方法,但没有一个看起来更好。这就是不同的方法导致的结果:

Photoshop:

替代文字

canvas:

替代文字

带有图像渲染的图像:优化质量设置并按宽度/高度缩放:

替代文字

带有图像渲染的图像:优化质量设置并使用 -moz-transform 缩放:

替代文字

画布在 pixastic 上调整大小:

替代文字

我想这意味着 firefox 没有像它应该的那样使用双三次采样。我只需要等到他们真正添加它。

编辑3:

原图

6个回答

那么如果所有浏览器(实际上,Chrome 5 给了我很好的浏览器)都不能给你足够好的重采样质量,你会怎么做?然后你自己实现它们!哦,拜托,我们正在进入 Web 3.0、HTML5 兼容浏览器、超级优化的 JIT javascript 编译器、多核 (†) 机器、拥有大量内存的新时代,你害怕什么?嘿,javascript中有java这个词,所以应该保证性能,对吧?看,缩略图生成代码:

// returns a function that calculates lanczos weight
function lanczosCreate(lobes) {
    return function(x) {
        if (x > lobes)
            return 0;
        x *= Math.PI;
        if (Math.abs(x) < 1e-16)
            return 1;
        var xx = x / lobes;
        return Math.sin(x) * Math.sin(xx) / x / xx;
    };
}

// elem: canvas element, img: image element, sx: scaled width, lobes: kernel radius
function thumbnailer(elem, img, sx, lobes) {
    this.canvas = elem;
    elem.width = img.width;
    elem.height = img.height;
    elem.style.display = "none";
    this.ctx = elem.getContext("2d");
    this.ctx.drawImage(img, 0, 0);
    this.img = img;
    this.src = this.ctx.getImageData(0, 0, img.width, img.height);
    this.dest = {
        width : sx,
        height : Math.round(img.height * sx / img.width),
    };
    this.dest.data = new Array(this.dest.width * this.dest.height * 3);
    this.lanczos = lanczosCreate(lobes);
    this.ratio = img.width / sx;
    this.rcp_ratio = 2 / this.ratio;
    this.range2 = Math.ceil(this.ratio * lobes / 2);
    this.cacheLanc = {};
    this.center = {};
    this.icenter = {};
    setTimeout(this.process1, 0, this, 0);
}

thumbnailer.prototype.process1 = function(self, u) {
    self.center.x = (u + 0.5) * self.ratio;
    self.icenter.x = Math.floor(self.center.x);
    for (var v = 0; v < self.dest.height; v++) {
        self.center.y = (v + 0.5) * self.ratio;
        self.icenter.y = Math.floor(self.center.y);
        var a, r, g, b;
        a = r = g = b = 0;
        for (var i = self.icenter.x - self.range2; i <= self.icenter.x + self.range2; i++) {
            if (i < 0 || i >= self.src.width)
                continue;
            var f_x = Math.floor(1000 * Math.abs(i - self.center.x));
            if (!self.cacheLanc[f_x])
                self.cacheLanc[f_x] = {};
            for (var j = self.icenter.y - self.range2; j <= self.icenter.y + self.range2; j++) {
                if (j < 0 || j >= self.src.height)
                    continue;
                var f_y = Math.floor(1000 * Math.abs(j - self.center.y));
                if (self.cacheLanc[f_x][f_y] == undefined)
                    self.cacheLanc[f_x][f_y] = self.lanczos(Math.sqrt(Math.pow(f_x * self.rcp_ratio, 2)
                            + Math.pow(f_y * self.rcp_ratio, 2)) / 1000);
                weight = self.cacheLanc[f_x][f_y];
                if (weight > 0) {
                    var idx = (j * self.src.width + i) * 4;
                    a += weight;
                    r += weight * self.src.data[idx];
                    g += weight * self.src.data[idx + 1];
                    b += weight * self.src.data[idx + 2];
                }
            }
        }
        var idx = (v * self.dest.width + u) * 3;
        self.dest.data[idx] = r / a;
        self.dest.data[idx + 1] = g / a;
        self.dest.data[idx + 2] = b / a;
    }

    if (++u < self.dest.width)
        setTimeout(self.process1, 0, self, u);
    else
        setTimeout(self.process2, 0, self);
};
thumbnailer.prototype.process2 = function(self) {
    self.canvas.width = self.dest.width;
    self.canvas.height = self.dest.height;
    self.ctx.drawImage(self.img, 0, 0, self.dest.width, self.dest.height);
    self.src = self.ctx.getImageData(0, 0, self.dest.width, self.dest.height);
    var idx, idx2;
    for (var i = 0; i < self.dest.width; i++) {
        for (var j = 0; j < self.dest.height; j++) {
            idx = (j * self.dest.width + i) * 3;
            idx2 = (j * self.dest.width + i) * 4;
            self.src.data[idx2] = self.dest.data[idx];
            self.src.data[idx2 + 1] = self.dest.data[idx + 1];
            self.src.data[idx2 + 2] = self.dest.data[idx + 2];
        }
    }
    self.ctx.putImageData(self.src, 0, 0);
    self.canvas.style.display = "block";
};

...使用它您可以产生这样的结果!

img717.imageshack.us/img717/8910/lanczos358.png

所以无论如何,这是您示例的“固定”版本:

img.onload = function() {
    var canvas = document.createElement("canvas");
    new thumbnailer(canvas, img, 188, 3); //this produces lanczos3
    // but feel free to raise it up to 8. Your client will appreciate
    // that the program makes full use of his machine.
    document.body.appendChild(canvas);
};

现在是时候将您最好的浏览器放在那里,看看哪一个最不可能增加您客户的血压!

嗯,我的讽刺标签在哪里?

(因为很多部分的代码是基于Anrieff Gallery Generator的,是不是也包含在GPL2下?我不知道)

实际上由于 javascript 的限制,不支持多核。

对不起,忘记了!我已经编辑了回复。无论如何它不会很快,bicubic 应该更快。更不用说我使用的算法不是通常的 2 向调整大小(逐行,水平然后垂直),所以它更慢。
2021-03-11 18:28:33
你太棒了,值得无数的惊喜。
2021-03-21 18:28:33
这会产生不错的结果,但在最新版本的 Chrome 中,1.8 MP 图像需要 7.4 秒...
2021-03-30 18:28:33
像这样的方法如何获得如此高的分数??所示的解决方案完全没有考虑用于存储颜色信息的对数刻度。127,127,127 的 RGB 是 255、255、255 亮度的四分之一而不是一半。解决方案中的下采样导致图像变暗。遗憾的是,这是关闭的,因为有一种非常简单快捷的缩小尺寸的方法,可以产生比 Photoshop(OP 必须设置错误的首选项)示例更好的结果
2021-04-06 18:28:33
我实际上尝试过自己实现它,就像你一样,从开源图像编辑器复制代码。由于我无法找到有关该算法的任何可靠文档,因此我很难对其进行优化。最后,我的有点慢(调整图像大小需要几秒钟)。当我有机会时,我会试试你的,看看它是否更快。我认为 webworkers 现在使多核 javascript 成为可能。我打算尝试使用它们来加快速度,但是我在弄清楚如何将其变成多线程算法时遇到了麻烦
2021-04-09 18:28:33

使用带 JavaScript 的Hermite过滤器快速调整图像大小/重新采样算法支持透明度,提供良好的质量。预览:

在此处输入图片说明

更新:在 GitHub 上添加了 2.0 版(更快,网络工作者 + 可传输对象)。最后我让它工作了!

Git:https : //github.com/viliusle/Hermite-resize
演示:http : //viliusle.github.io/miniPaint/

/**
 * 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);
}
你也会分享 webworkers 版本吗?可能由于设置开销,小图像的速度较慢,但​​对于较大的源图像可能有用。
2021-03-10 18:28:33
也许您可以包含指向 miniPaint 演示和 Github 存储库的链接?
2021-03-15 18:28:33
@ViliusL 啊,现在我想起了为什么网络工作者不能很好地工作。他们之前没有共享内存,现在仍然没有!也许有一天当他们设法解决它时,您的代码会被使用(那个,或者人们可能会改用 PNaCl)
2021-03-17 18:28:33
巨大的差异和不错的表现。非常感谢你!之前之后
2021-03-26 18:28:33
添加了演示、git 链接以及多核版本。顺便说一句,我没有花太多时间优化多核版本......我相信单版本优化得很好。
2021-04-09 18:28:33

试试pica - 这是一个高度优化的调整器,具有可选择的算法。演示

例如,第一篇文章的原始图像使用 Lanczos 过滤器和 3px 窗口在 120 毫秒内调整大小,或者使用 Box 过滤器和 0.5 像素窗口在 60 毫秒内调整大小。对于 17mb 的巨大图像,5000x3000px 调整大小在桌面上需要大约 1 秒,在移动设备上需要 3 秒。

此线程中很好地描述了所有调整大小的原则,并且 pica 没有添加火箭科学。但它针对现代 JIT-s 进行了很好的优化,并且可以开箱即用(通过 npm 或 bower)。此外,它在可用时使用网络工作者以避免界面冻结。

我还计划尽快添加反锐化蒙版支持,因为它在缩小后非常有用。

我知道这是一个旧线程,但它可能对某些人(例如我自己)在几个月后第一次遇到此问题时有用。

这是一些每次重新加载图像时调整图像大小的代码。我知道这根本不是最佳选择,但我提供它作为概念证明。

另外,很抱歉将 jQuery 用于简单的选择器,但我对语法感到太舒服了。

$(document).on('ready', createImage);
$(window).on('resize', createImage);

var createImage = function(){
  var canvas = document.getElementById('myCanvas');
  canvas.width = window.innerWidth || $(window).width();
  canvas.height = window.innerHeight || $(window).height();
  var ctx = canvas.getContext('2d');
  img = new Image();
  img.addEventListener('load', function () {
    ctx.drawImage(this, 0, 0, w, h);
  });
  img.src = 'http://www.ruinvalor.com/Telanor/images/original.jpg';
};
html, body{
  height: 100%;
  width: 100%;
  margin: 0;
  padding: 0;
  background: #000;
}
canvas{
  position: absolute;
  left: 0;
  top: 0;
  z-index: 0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Canvas Resize</title>
  </head>
  <body>
    <canvas id="myCanvas"></canvas>
  </body>
</html>

我的 createImage 函数在加载文档时被调用一次,之后每次窗口收到调整大小事件时都会调用它。

我在 Mac 上的 Chrome 6 和 Firefox 3.6 中对其进行了测试。这种“技术”就像夏天吃冰淇淋一样吃处理器,但它确实有效。

我已经提出了一些算法来对 html canvas 像素数组进行图像插值,这些算法在这里可能有用:

https://web.archive.org/web/20170104190425/http://jsperf.com:80/pixel-interpolation/2

这些可以被复制/粘贴,并且可以在网络工作者内部使用来调整图像大小(或任何其他需要插值的操作 - 我目前正在使用它们来去除图像)。

我没有在上面添加 lanczos 的东西,所以如果你愿意,可以随意添加它作为比较。