HTML5 Canvas Resize (Downscale) 图像质量高吗?

Posted

技术标签:

【中文标题】HTML5 Canvas Resize (Downscale) 图像质量高吗?【英文标题】:HTML5 Canvas Resize (Downscale) Image High Quality? 【发布时间】:2013-09-26 04:51:36 【问题描述】:

我使用 html5 画布元素在我的浏览器中调整图像大小。事实证明,质量非常低。我发现了这个:Disable Interpolation when Scaling a <canvas>,但这无助于提高质量。

下面是我的 css 和 js 代码以及使用 Photoshop 缩放并在 canvas API 中缩放的图像。

在浏览器中缩放图像时,如何才能获得最佳质量?

注意:我想将大图像缩小为小图像,修改画布中的颜色并将结果从画布发送到服务器。

CSS:

canvas, img 
    image-rendering: optimizeQuality;
    image-rendering: -moz-crisp-edges;
    image-rendering: -webkit-optimize-contrast;
    image-rendering: optimize-contrast;
    -ms-interpolation-mode: nearest-neighbor;

JS:

var $img = $('<img>');
var $originalCanvas = $('<canvas>');
$img.load(function() 


   var originalContext = $originalCanvas[0].getContext('2d');   
   originalContext.imageSmoothingEnabled = false;
   originalContext.webkitImageSmoothingEnabled = false;
   originalContext.mozImageSmoothingEnabled = false;
   originalContext.drawImage(this, 0, 0, 379, 500);
);

用 Photoshop 调整大小的图像:

在画布上调整大小的图像:

编辑:

我尝试按照以下建议的多个步骤进行缩减:

Resizing an image in an HTML5 canvas 和 Html5 canvas drawImage: how to apply antialiasing

这是我用过的函数:

function resizeCanvasImage(img, canvas, maxWidth, maxHeight) 
    var imgWidth = img.width, 
        imgHeight = img.height;

    var ratio = 1, ratio1 = 1, ratio2 = 1;
    ratio1 = maxWidth / imgWidth;
    ratio2 = maxHeight / imgHeight;

    // Use the smallest ratio that the image best fit into the maxWidth x maxHeight box.
    if (ratio1 < ratio2) 
        ratio = ratio1;
    
    else 
        ratio = ratio2;
    

    var canvasContext = canvas.getContext("2d");
    var canvasCopy = document.createElement("canvas");
    var copyContext = canvasCopy.getContext("2d");
    var canvasCopy2 = document.createElement("canvas");
    var copyContext2 = canvasCopy2.getContext("2d");
    canvasCopy.width = imgWidth;
    canvasCopy.height = imgHeight;  
    copyContext.drawImage(img, 0, 0);

    // init
    canvasCopy2.width = imgWidth;
    canvasCopy2.height = imgHeight;        
    copyContext2.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvasCopy2.width, canvasCopy2.height);


    var rounds = 2;
    var roundRatio = ratio * rounds;
    for (var i = 1; i <= rounds; i++) 
        console.log("Step: "+i);

        // tmp
        canvasCopy.width = imgWidth * roundRatio / i;
        canvasCopy.height = imgHeight * roundRatio / i;

        copyContext.drawImage(canvasCopy2, 0, 0, canvasCopy2.width, canvasCopy2.height, 0, 0, canvasCopy.width, canvasCopy.height);

        // copy back
        canvasCopy2.width = imgWidth * roundRatio / i;
        canvasCopy2.height = imgHeight * roundRatio / i;
        copyContext2.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvasCopy2.width, canvasCopy2.height);

     // end for


    // copy back to canvas
    canvas.width = imgWidth * roundRatio / rounds;
    canvas.height = imgHeight * roundRatio / rounds;
    canvasContext.drawImage(canvasCopy2, 0, 0, canvasCopy2.width, canvasCopy2.height, 0, 0, canvas.width, canvas.height);



如果我使用 2 步缩小尺寸,结果如下:

如果我使用 3 步缩小尺寸,结果如下:

如果我使用 4 步缩小尺寸,结果如下:

如果我使用 20 步缩小尺寸,结果如下:

注意:事实证明,从 1 步到 2 步,图像质量有很大的提高,但是您添加到该过程中的步骤越多,图像变得越模糊。

有没有办法解决图片越多步骤越模糊的问题?

编辑 2013-10-04:我尝试了 GameAlchemist 的算法。这是与 Photoshop 相比的结果。

PhotoShop 图片:

GameAlchemist 的算法:

【问题讨论】:

您可以尝试逐步缩放您的图像:***.com/questions/18761404/… Html5 canvas drawImage: how to apply antialiasing 的可能副本。看看是否有效。如果图像较大并缩小到较小尺寸,则需要分步进行(请参阅链接中的示例图像) @confile 关闭插值会使情况变得更糟。您希望保持启用状态。看看我上面提供的链接。我在那里展示了如何使用步骤来缩小更大的图像并保持质量。正如 Scott 所说,您希望质量优先于速度。 @Ken-AbdiasSoftware 我试过你的方法,但问题是我用于逐步缩放的轮数越多,它会变得更糟。知道如何解决这个问题吗? 使用 HTML5 复制昂贵的专业照片编辑软件的功能的机会真的很小吗?您可能可以接近(ish),但正如它在 Photoshop 中工作的那样,我想这是不可能的! 【参考方案1】:

这是改进的 Hermite 调整大小过滤器,它使用 1 个工作人员,这样窗口就不会冻结。

https://github.com/calvintwr/blitz-hermite-resize

const blitz = Blitz.create()

/* Promise */
blitz(
    source: DOM Image/DOM Canvas/jQuery/DataURL/File,
    width: 400,
    height: 600
).then(output => 
    // handle output
)catch(error => 
    // handle error
)

/* Await */
let resized = await blitz(...)

/* Old school callback */
const blitz = Blitz.create('callback')
blitz(..., function(output) 
    // run your callback.
)

【讨论】:

【参考方案2】:

我真的尽量避免遍历图像数据,尤其是在较大的图像上。因此,我想出了一种相当简单的方法,可以通过一些额外的步骤来体面地减小图像大小,而不受任何限制或限制。 此例程下降到所需目标大小之前的最低可能半步。然后它将它放大到目标大小的两倍,然后再缩小一半。一开始听起来很有趣,但结果非常好,而且很快就到了那里。

function resizeCanvas(canvas, newWidth, newHeight) 
  let ctx = canvas.getContext('2d');
  let buffer = document.createElement('canvas');
  buffer.width = ctx.canvas.width;
  buffer.height = ctx.canvas.height;
  let ctxBuf = buffer.getContext('2d');
  

  let scaleX = newWidth / ctx.canvas.width;
  let scaleY = newHeight / ctx.canvas.height;

  let scaler = Math.min(scaleX, scaleY);
  //see if target scale is less than half...
  if (scaler < 0.5) 
    //while loop in case target scale is less than quarter...
    while (scaler < 0.5) 
      ctxBuf.canvas.width = ctxBuf.canvas.width * 0.5;
      ctxBuf.canvas.height = ctxBuf.canvas.height * 0.5;
      ctxBuf.scale(0.5, 0.5);
      ctxBuf.drawImage(canvas, 0, 0);
      ctxBuf.setTransform(1, 0, 0, 1, 0, 0);
      ctx.canvas.width = ctxBuf.canvas.width;
      ctx.canvas.height = ctxBuf.canvas.height;
      ctx.drawImage(buffer, 0, 0);

      scaleX = newWidth / ctxBuf.canvas.width;
      scaleY = newHeight / ctxBuf.canvas.height;
      scaler = Math.min(scaleX, scaleY);
    
    //only if the scaler is now larger than half, double target scale trick...
    if (scaler > 0.5) 
      scaleX *= 2.0;
      scaleY *= 2.0;
      ctxBuf.canvas.width = ctxBuf.canvas.width * scaleX;
      ctxBuf.canvas.height = ctxBuf.canvas.height * scaleY;
      ctxBuf.scale(scaleX, scaleY);
      ctxBuf.drawImage(canvas, 0, 0);
      ctxBuf.setTransform(1, 0, 0, 1, 0, 0);
      scaleX = 0.5;
      scaleY = 0.5;
    
   else
    ctxBuf.drawImage(canvas, 0, 0);

  //wrapping things up...
  ctx.canvas.width = newWidth;
  ctx.canvas.height = newHeight;
  ctx.scale(scaleX, scaleY);
  ctx.drawImage(buffer, 0, 0);
  ctx.setTransform(1, 0, 0, 1, 0, 0);

【讨论】:

【参考方案3】:

而不是 .85,如果我们添加 1.0。你会得到准确的答案。

data=canvas.toDataURL('image/jpeg', 1.0);

您可以获得清晰明亮的图像。请检查

【讨论】:

【参考方案4】:

我找到了一个不需要直接访问像素数据并循环通过它来执行下采样的解决方案。根据图像的大小,这可能会占用大量资源,最好使用浏览器的内部算法。

drawImage() 函数使用线性插值、最近邻重采样方法。 当您不将大小调整到原始大小的一半以上时效果很好

如果您循环一次只调整最大一半的大小,结果会非常好,并且比访问像素数据快得多。

此函数一次将采样减少一半,直到达到所需的大小:

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

【讨论】:

能否请您发布一个 jsfiddle 和一些生成的图像? 在底部的链接中,您可以找到使用此技术生成的图像【参考方案5】:

为什么要使用画布来调整图像大小?现代浏览器都使用双三次插值——与 Photoshop 使用的过程相同(如果你做得对的话)——它们比画布过程更快。只需指定您想要的图像大小(仅使用一个维度,高度或宽度,按比例调整大小)。

大多数浏览器都支持此功能,包括更高版本的 IE。早期版本may require browser-specific CSS。

调整图像大小的简单函数(使用 jQuery)如下所示:

function resizeImage(img, percentage) 
    var coeff = percentage/100,
        width = $(img).width(),
        height = $(img).height();

    return "width": width*coeff, "height": height*coeff           

然后只需使用返回的值在一个或两个维度上调整图像的大小。

显然,您可以进行不同的改进,但这可以完成工作。

将以下代码粘贴到此页面的控制台中,然后观察 gravatar 会发生什么:

function resizeImage(img, percentage) 
    var coeff = percentage/100,
        width = $(img).width(),
        height = $(img).height();

    return "width": width*coeff, "height": height*coeff           


$('.user-gravatar32 img').each(function()
  var newDimensions = resizeImage( this, 150);
  this.style.width = newDimensions.width + "px";
  this.style.height = newDimensions.height + "px";
);

【讨论】:

另请注意,如果您只指定一个维度,(现代)浏览器将自动保持图像的自然纵横比。 也许他需要将调整大小的图像发送到服务器。 @Sergiu:没有必要,但请注意,如果您要从非常小的图像转换为非常大的图像,即使从服务器上也不会得到很好的结果。 @Robusto 之后我需要将图像放在画布中,稍后再将其发送到服务器。我想将大图像缩小为小图像,修改画布中的颜色并将结果发送到服务器。你觉得我应该怎么做? @Robusto 这就是问题所在。在客户端上显示小图像很容易。 img.width nad img.height 是如此微不足道。我只想在服务器上缩小一次,而不是再次缩小。【参考方案6】:

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

您可能会发现 3 种不同的方法来调整大小,这将帮助您了解代码的工作原理以及原因。

https://jsfiddle.net/1b68eLdr/93089/

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

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 size 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
        
    ;

【讨论】:

【参考方案7】:

也许你可以试试这个,这是我在我的项目中经常使用的。这样你不仅可以获得高质量的图像,还可以获得画布上的任何其他元素。

/* 
 * @parame canvas => canvas object
 * @parame rate => the pixel quality
 */
function setCanvasSize(canvas, rate) 
    const scaleRate = rate;
    canvas.width = window.innerWidth * scaleRate;
    canvas.height = window.innerHeight * scaleRate;
    canvas.style.width = window.innerWidth + 'px';
    canvas.style.height = window.innerHeight + 'px';
    canvas.getContext('2d').scale(scaleRate, scaleRate);

【讨论】:

【参考方案8】:

对于真正需要调整图像本身大小的人来说,这不是正确的答案,只是为了缩小文件大小

我遇到了“直接来自相机”图片的问题,我的客户经常以“未压缩”JPEG 格式上传这些图片。

不太为人所知的是,画布支持(在大多数浏览器 2017 中)更改 JPEG 的质量

data=canvas.toDataURL('image/jpeg', .85) # [1..0] default 0.92

通过这个技巧,我可以将 >10Mb 的 4k x 3k 图片减少到 1 或 2Mb,当然这取决于您的需求。

look here

【讨论】:

【参考方案9】:

由于您的问题是缩小图像,因此讨论插值(即创建像素)毫无意义。这里的问题是下采样。

要对图像进行下采样,我们需要将原始图像中 p * p 像素的每个正方形转换为目标图像中的单个像素。

出于性能原因,浏览器会进行非常简单的下采样:为了构建较小的图像,它们只会在源中选择一个像素并将其值用于目标。它“忘记”了一些细节并增加了噪音。

但有一个例外:因为 2X 图像下采样非常易于计算(平均 4 个像素来制作一个)并且用于视网膜/HiDPI 像素,所以这种情况处理得当 - 浏览器确实使用了 4像素制作一个-。

但是...如果您多次使用 2X 下采样,您将面临连续舍入误差会增加太多噪音的问题。 更糟糕的是,您不会总是按 2 的幂调整大小,并且调整到最接近的幂 + 最后一次调整大小非常嘈杂。

您寻求的是像素完美的下采样,即:对图像进行重新采样,将所有输入像素都考虑在内-无论比例如何-。 为此,我们必须针对每个输入像素计算其对一个、两个或四个目标像素的贡献,具体取决于输入像素的缩放投影是否正好在目标像素内、是否与 X 边界、Y 边界重叠,或两者都重叠. (这里有一个方案很好,但我没有。)

这是一个画布比例与我的像素完美比例在 1/3 的 zombat 比例上的示例。

请注意,图片可能会在您的浏览器中缩放,并由 S.O..jpegized.. 然而,我们看到噪音要小得多,尤其是在袋熊身后的草丛中,以及它右侧的树枝上。毛皮中的噪点使它更加对比鲜明,但看起来他有白毛 - 与源图片不同 -。 右图不那么吸引人,但绝对更好。

这是进行像素完美缩小的代码:

小提琴结果: http://jsfiddle.net/gamealchemist/r6aVp/embedded/result/ 小提琴本身:http://jsfiddle.net/gamealchemist/r6aVp/

// scales the image by (float) scale < 1
// returns a canvas containing the scaled image.
function downScaleImage(img, scale) 
    var imgCV = document.createElement('canvas');
    imgCV.width = img.width;
    imgCV.height = img.height;
    var imgCtx = imgCV.getContext('2d');
    imgCtx.drawImage(img, 0, 0);
    return downScaleCanvas(imgCV, scale);


// scales the canvas by (float) scale < 1
// returns a new canvas containing the scaled image.
function downScaleCanvas(cv, scale) 
    if (!(scale < 1) || !(scale > 0)) throw ('scale must be a positive number <1 ');
    var sqScale = scale * scale; // square scale = area of source pixel within target
    var sw = cv.width; // source image width
    var sh = cv.height; // source image height
    var tw = Math.floor(sw * scale); // target image width
    var th = Math.floor(sh * scale); // target image height
    var sx = 0, sy = 0, sIndex = 0; // source x,y, index within source array
    var tx = 0, ty = 0, yIndex = 0, tIndex = 0; // target x,y, x,y index within target array
    var tX = 0, tY = 0; // rounded tx, ty
    var w = 0, nw = 0, wx = 0, nwx = 0, wy = 0, nwy = 0; // weight / next weight x / y
    // weight is weight of current source point within target.
    // next weight is weight of current source point within next target's point.
    var crossX = false; // does scaled px cross its current px right border ?
    var crossY = false; // does scaled px cross its current px bottom border ?
    var sBuffer = cv.getContext('2d').
    getImageData(0, 0, sw, sh).data; // source buffer 8 bit rgba
    var tBuffer = new Float32Array(3 * tw * th); // target buffer Float32 rgb
    var sR = 0, sG = 0,  sB = 0; // source's current point r,g,b
    /* untested !
    var sA = 0;  //source alpha  */    

    for (sy = 0; sy < sh; sy++) 
        ty = sy * scale; // y src position within target
        tY = 0 | ty;     // rounded : target pixel's y
        yIndex = 3 * tY * tw;  // line index within target array
        crossY = (tY != (0 | ty + scale)); 
        if (crossY)  // if pixel is crossing botton target pixel
            wy = (tY + 1 - ty); // weight of point within target pixel
            nwy = (ty + scale - tY - 1); // ... within y+1 target pixel
        
        for (sx = 0; sx < sw; sx++, sIndex += 4) 
            tx = sx * scale; // x src position within target
            tX = 0 |  tx;    // rounded : target pixel's x
            tIndex = yIndex + tX * 3; // target pixel index within target array
            crossX = (tX != (0 | tx + scale));
            if (crossX)  // if pixel is crossing target pixel's right
                wx = (tX + 1 - tx); // weight of point within target pixel
                nwx = (tx + scale - tX - 1); // ... within x+1 target pixel
            
            sR = sBuffer[sIndex    ];   // retrieving r,g,b for curr src px.
            sG = sBuffer[sIndex + 1];
            sB = sBuffer[sIndex + 2];

            /* !! untested : handling alpha !!
               sA = sBuffer[sIndex + 3];
               if (!sA) continue;
               if (sA != 0xFF) 
                   sR = (sR * sA) >> 8;  // or use /256 instead ??
                   sG = (sG * sA) >> 8;
                   sB = (sB * sA) >> 8;
               
            */
            if (!crossX && !crossY)  // pixel does not cross
                // just add components weighted by squared scale.
                tBuffer[tIndex    ] += sR * sqScale;
                tBuffer[tIndex + 1] += sG * sqScale;
                tBuffer[tIndex + 2] += sB * sqScale;
             else if (crossX && !crossY)  // cross on X only
                w = wx * scale;
                // add weighted component for current px
                tBuffer[tIndex    ] += sR * w;
                tBuffer[tIndex + 1] += sG * w;
                tBuffer[tIndex + 2] += sB * w;
                // add weighted component for next (tX+1) px                
                nw = nwx * scale
                tBuffer[tIndex + 3] += sR * nw;
                tBuffer[tIndex + 4] += sG * nw;
                tBuffer[tIndex + 5] += sB * nw;
             else if (crossY && !crossX)  // cross on Y only
                w = wy * scale;
                // add weighted component for current px
                tBuffer[tIndex    ] += sR * w;
                tBuffer[tIndex + 1] += sG * w;
                tBuffer[tIndex + 2] += sB * w;
                // add weighted component for next (tY+1) px                
                nw = nwy * scale
                tBuffer[tIndex + 3 * tw    ] += sR * nw;
                tBuffer[tIndex + 3 * tw + 1] += sG * nw;
                tBuffer[tIndex + 3 * tw + 2] += sB * nw;
             else  // crosses both x and y : four target points involved
                // add weighted component for current px
                w = wx * wy;
                tBuffer[tIndex    ] += sR * w;
                tBuffer[tIndex + 1] += sG * w;
                tBuffer[tIndex + 2] += sB * w;
                // for tX + 1; tY px
                nw = nwx * wy;
                tBuffer[tIndex + 3] += sR * nw;
                tBuffer[tIndex + 4] += sG * nw;
                tBuffer[tIndex + 5] += sB * nw;
                // for tX ; tY + 1 px
                nw = wx * nwy;
                tBuffer[tIndex + 3 * tw    ] += sR * nw;
                tBuffer[tIndex + 3 * tw + 1] += sG * nw;
                tBuffer[tIndex + 3 * tw + 2] += sB * nw;
                // for tX + 1 ; tY +1 px
                nw = nwx * nwy;
                tBuffer[tIndex + 3 * tw + 3] += sR * nw;
                tBuffer[tIndex + 3 * tw + 4] += sG * nw;
                tBuffer[tIndex + 3 * tw + 5] += sB * nw;
            
         // end for sx 
     // end for sy

    // create result canvas
    var resCV = document.createElement('canvas');
    resCV.width = tw;
    resCV.height = th;
    var resCtx = resCV.getContext('2d');
    var imgRes = resCtx.getImageData(0, 0, tw, th);
    var tByteBuffer = imgRes.data;
    // convert float32 array into a UInt8Clamped Array
    var pxIndex = 0; //  
    for (sIndex = 0, tIndex = 0; pxIndex < tw * th; sIndex += 3, tIndex += 4, pxIndex++) 
        tByteBuffer[tIndex] = Math.ceil(tBuffer[sIndex]);
        tByteBuffer[tIndex + 1] = Math.ceil(tBuffer[sIndex + 1]);
        tByteBuffer[tIndex + 2] = Math.ceil(tBuffer[sIndex + 2]);
        tByteBuffer[tIndex + 3] = 255;
    
    // writing result to canvas.
    resCtx.putImageData(imgRes, 0, 0);
    return resCV;

这是相当内存贪婪,因为需要一个浮点缓冲区来存储目标图像的中间值(-> 如果我们计算结果画布,我们会使用源图像的 6 倍内存这个算法)。 它也很昂贵,因为无论目标大小如何都使用每个源像素,而且我们必须为 getImageData / putImageDate 付费,而且速度也很慢。 但是在这种情况下,没有办法比处理每个源值更快,而且情况还不错:对于我的 740 * 556 袋熊图像,处理需要 30 到 40 毫秒。

【讨论】:

在将图像放入画布之前缩放图像会更快吗? 我不明白...看来这就是我所做的。缓冲区以及我创建的画布(resCV)具有缩放图像的大小。我认为让它更快的唯一方法是使用类似 breshensam 的整数计算。但 40 毫秒仅对于视频游戏 (25 fps) 来说很慢,对于绘图应用程序而言则不然。 您是否有机会在保持质量的同时让您的算法更快? 我尝试使用 0 | 对缓冲区(算法的最新部分)进行舍入。而不是 Mat.ceil。它有点快。但无论如何,get/putImageData 有相当多的开销,而且我们无法避免处理每个像素。 好的,所以我看了代码:你离解决方案很近。两个错误:对于 tX+1,您的索引偏离了一个(它们是 +3、+4、+5、+6 而不是 +4、+5、+6、+7),并且 rgba 中的换行是 mul由 4,而不是 3。我刚刚测试了 4 个随机值来检查(0.1、0.15、0.33、0.8)它似乎没问题。你更新的小提琴在这里:jsfiddle.net/gamealchemist/kpQyE/3【参考方案10】:

context.scale(xScale, yScale)

<canvas id="c"></canvas>
<hr/>
<img id="i" />

<script>
var i = document.getElementById('i');

i.onload = function()
    var width = this.naturalWidth,
        height = this.naturalHeight,
        canvas = document.getElementById('c'),
        ctx = canvas.getContext('2d');

    canvas.width = Math.floor(width / 2);
    canvas.height = Math.floor(height / 2);

    ctx.scale(0.5, 0.5);
    ctx.drawImage(this, 0, 0);
    ctx.rect(0,0,500,500);
    ctx.stroke();

    // restore original 1x1 scale
    ctx.scale(2, 2);
    ctx.rect(0,0,500,500);
    ctx.stroke();
;

i.src = 'https://static.md/b70a511140758c63f07b618da5137b5d.png';
</script>

【讨论】:

【参考方案11】:

高质量的快速画布重采样:http://jsfiddle.net/9g9Nv/442/

更新: 2.0 版(更快,网络工作者 + 可转移对象)-https://github.com/viliusle/Hermite-resize

/**
 * 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);

【讨论】:

我需要最好的质量 已修复,我将“好”改为“最佳”,现在可以了吗? :D。另一方面,如果您想要最好的重新采样 - 使用 imagemagick。 @confile imgur.com 在 jsfiddle 中使用是安全的,但是管理员做错了什么?您看不到质量好,因为您的浏览器给出了 CORS 致命错误。 (不能使用来自远程站点的图像) 好的,您可以使用任何其他具有透明区域的 PNG 图像。对此有什么想法吗? @confile 你是对的,在某些情况下,透明图像在锐利区域存在问题。我的测试错过了这些案例。修复了调整大小也修复了小提琴上的远程图像支持:jsfiddle.net/9g9Nv/49【参考方案12】:

建议 1 - 扩展流程管道

您可以使用我在您引用的链接中描述的降压,但您似乎以错误的方式使用它们。

将图像缩放到 1:2 以上的比例(通常但不限于)不需要降级。这是您需要进行剧烈缩小的地方,您需要根据图像的内容将其分成两个(很少,更多)步骤(特别是在高频,如薄出现线条)。

每次对图像进行下采样时,都会丢失细节和信息。您不能期望生成的图像与原始图像一样清晰。

如果您随后在多个步骤中缩小图像,您总共会丢失大量信息,并且结果会很差,正如您已经注意到的那样。

尝试多走一步,或多走两步。

卷积

在 Photoshop 的情况下,它会在图像重新采样后应用卷积,例如锐化。这不仅仅是发生双三次插值,因此为了完全模拟 Photoshop,我们还需要添加 Photoshop 正在执行的步骤(使用默认设置)。

对于此示例,我将使用您在帖子中引用的原始答案,但我已为其添加了锐化卷积以提高后期处理的质量(请参阅底部的演示)。

这是添加锐化过滤器的代码(它基于通用卷积过滤器 - 我将用于锐化的权重矩阵以及用于调整效果发音的混合因子放入其中):

用法:

sharpen(context, width, height, mixFactor);

mixFactor 是介于 [0.0, 1.0] 之间的值,允许您淡化锐化效果 - 经验法则:尺寸越小,需要的效果就越少。

功能(基于this snippet):

function sharpen(ctx, w, h, mix) 

    var weights =  [0, -1, 0,  -1, 5, -1,  0, -1, 0],
        katet = Math.round(Math.sqrt(weights.length)),
        half = (katet * 0.5) |0,
        dstData = ctx.createImageData(w, h),
        dstBuff = dstData.data,
        srcBuff = ctx.getImageData(0, 0, w, h).data,
        y = h;
        
    while(y--) 

        x = w;

        while(x--) 

            var sy = y,
                sx = x,
                dstOff = (y * w + x) * 4,
                r = 0, g = 0, b = 0, a = 0;

            for (var cy = 0; cy < katet; cy++) 
                for (var cx = 0; cx < katet; cx++) 

                    var scy = sy + cy - half;
                    var scx = sx + cx - half;

                    if (scy >= 0 && scy < h && scx >= 0 && scx < w) 

                        var srcOff = (scy * w + scx) * 4;
                        var wt = weights[cy * katet + cx];

                        r += srcBuff[srcOff] * wt;
                        g += srcBuff[srcOff + 1] * wt;
                        b += srcBuff[srcOff + 2] * wt;
                        a += srcBuff[srcOff + 3] * wt;
                    
                
            

            dstBuff[dstOff] = r * mix + srcBuff[dstOff] * (1 - mix);
            dstBuff[dstOff + 1] = g * mix + srcBuff[dstOff + 1] * (1 - mix);
            dstBuff[dstOff + 2] = b * mix + srcBuff[dstOff + 2] * (1 - mix)
            dstBuff[dstOff + 3] = srcBuff[dstOff + 3];
        
    

    ctx.putImageData(dstData, 0, 0);

使用这种组合的结果是:

ONLINE DEMO HERE

根据您想要添加到混合中的锐化程度,您可以获得从默认“模糊”到非常锐利的结果:

建议 2 - 低级算法实现

如果您想在质量方面获得最佳结果,您需要进入底层并考虑实施例如这个全新的算法来做到这一点。

请参阅 IEEE 的 Interpolation-Dependent Image Downsampling (2011)。Here is a link to the paper in full (PDF)。

目前在 javascript AFAIK 中没有此算法的实现,因此如果您想全身心投入到这项任务中,那么您就得手忙脚乱了。

本质是(摘自论文):

摘要

提出了一种面向插值的自适应下采样算法 本文用于低比特率图像编码。给定一张图片, 所提出的算法能够获得低分辨率图像,从 与输入具有相同分辨率的高质量图像 图像可以插值。不同于传统的 下采样算法,它独立于 插值过程中,所提出的下采样算法铰链 下采样到插值过程。因此, 提出的下采样算法能够保持原始 最大程度地获取输入图像的信息。下采样 然后将图像输入JPEG。基于总变体 (TV) 的帖子 然后将处理应用于解压缩的低分辨率图像。 最终,对处理后的图像进行插值以保持 输入图像的原始分辨率。 实验结果验证 通过所提出的算法利用下采样图像, 可以得到更高质量的插值图像。除了, 所提出的算法能够实现优于 JPEG 用于低比特率图像编码。

(有关所有详细信息、公式等,请参阅提供的链接)

【讨论】:

这是一个很好的解决方案。我在具有透明区域的 png 文件上进行了尝试。结果如下:jsfiddle.net/confile/5CD4N你知道如何让它工作吗? 这是天才!但请你能解释一下你到底在做什么吗?大声笑..我完全想知道来龙去脉...也许可以学习资源? @Carine 对于一个糟糕的评论字段来说可能有点多:) 但是,按比例缩小会重新采样一组像素以平均代表该组的新像素。这实际上是一个低通滤波器,总体上会引入一些模糊。为了补偿锐度的损失,只需应用锐化卷积。由于锐化可能非常明显,我们可以将其与图像混合,这样我们就可以控制锐化水平。希望能提供一些见解。【参考方案13】:

如果您只想使用画布,最好的结果将是多个降级。但这还不够好。为了获得更好的质量,您需要纯 js 实现。我们刚刚发布了pica - 具有可变质量/速度的高速降频器。简而言之,它在 ~0.1s 内调整 1280*1024px 的大小,在 1s 内调整 5000*3000px 图像的大小,质量最高(lanczos 过滤器具有 3 个 lobes)。 Pica 有demo,您可以在其中玩转您的图像、质量级别,甚至可以在移动设备上试用。

Pica 还没有反锐化蒙版,但很快就会添加。这比实现高速卷积滤波器来调整大小要容易得多。

【讨论】:

【参考方案14】:

这是一个可重用的 Angular 服务,用于调整高质量图像/画布大小:https://gist.github.com/fisch0920/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
    )
)

【讨论】:

以上是关于HTML5 Canvas Resize (Downscale) 图像质量高吗?的主要内容,如果未能解决你的问题,请参考以下文章

HTML5 Canvas 中图像大小调整的界限

html5 Canvas 如何自适应屏幕大小?

html5 Canvas 如何自适应屏幕大小?

html5中怎么根据内容高低来调整画布大小

如何使用类和函数为html5-canvas上的多个对象设置动画?

无法在 html5 中使用 Kineticjs