【问题标题】:Linear interpolation on canvas画布上的线性插值
【发布时间】:2018-02-25 01:34:00
【问题描述】:

我正在尝试了解图像重采样方法的工作原理。我已经阅读/观看了几页/视频,我想我明白了。但是,我找不到任何关于如何实现它的工作示例。所以我想我应该从基础开始:1D 上的最近邻重采样。

这很简单,我想我明白了。 JSFiddle Demo.

function resample() {

    var widthScaled   = Math.round(originalPixels.width * scaleX);
    var sampledPixels = context.createImageData(widthScaled, originalPixels.height);

    for (var i = 0; i < sampledPixels.data.length; i+=4) {

        var position  = index2pos(sampledPixels, i);
        var origPosX  = Math.floor(position.x / scaleX);
        var origColor = getPixel(originalPixels, origPosX, position.y);

        setPixel(sampledPixels, position.x, position.y, origColor);
    }

    loadImage(context, sampledPixels);
}

接下来,我继续进行线性插值。以为这也很简单,但我遇到了问题。首先,如何处理最后一个像素(标记为红色)?它只有一个相邻像素。其次,与 Photoshop 相比,我的结果太锐利了。我的方法有缺陷,还是 PS 做了一些额外的工作? JSFiddle Demo.

function resample() {

    var sampledPixels = context.createImageData(originalPixels.width * scaleX, originalPixels.height);

    for (var i = 0; i < sampledPixels.data.length; i+=4) {

        var position  = index2pos(sampledPixels, i);
        var origPosX  = position.x / scaleX;

        var leftPixelPosX  = Math.floor(origPosX);
        var rightPixelPosX = Math.ceil(origPosX);

        var leftPixelColor  = getPixel(originalPixels, leftPixelPosX, position.y);
        var rightPixelColor = getPixel(originalPixels, rightPixelPosX, position.y);

        var weight = origPosX % 1;
        var color  = mix(leftPixelColor[0], rightPixelColor[0], weight);
            color  = [color, color, color, 255];

        setPixel(sampledPixels, position.x, position.y, color);
    }

    loadImage(context, sampledPixels);
}

function mix(x, y, a) {
    return x * (1 - a) + y * a;
}

【问题讨论】:

    标签: javascript image-processing canvas resampling pixel-manipulation


    【解决方案1】:

    像素的线性插值

    过滤并没有真正的对与错,因为结果是主观的,结果的质量取决于你,是否足够好,或者你觉得还有改进的空间。

    还有各种各样的过滤方法,最近邻法、线性、双线性、多项式、样条、Lanczos……每一种都可以有很多变化。还有一些因素,比如过滤输出格式是什么;屏幕,打印,视频。质量优先于速度还是内存效率。以及当硬件无论如何都会为您实时完成时,为什么要升级。

    看来你已经掌握了正确的线性过滤基础

    更新更正。线性和双线性是指同一类型的插值,双线性是2D,线性是1D

    处理最后一个像素

    在丢失像素的情况下,有几种选择,

    • 假设颜色继续,所以只复制最后一个像素。
    • 假设下一个像素是背景、边框颜色或某些预定义的边缘颜色。
    • 环绕到另一侧的像素(平铺地图的最佳选择)
    • 如果您知道有背景图片,请使用其像素
    • 只需删除最后一个像素(图像大小会小 1 个像素)

    PS结果

    对我来说,PhotoShop 结果看起来像是一种双线性过滤形式,尽管它应该保持原始像素颜色,所以使用了一些更复杂的东西。在不知道方法是什么的情况下,您将很难匹配它。

    最佳结果的光谱

    良好的过滤将找到特定点的频谱,并根据该信息重建丢失的像素。

    如果您不将一行像素视为值,而是将其视为体积,那么一行像素会形成波形。任何复杂的波形都可以分解为一组更简单的基本纯音(频率)。然后,您可以通过在特定点添加所有频率来获得良好的近似值。

    使用这种方法的滤波器通常用傅立叶或 FFT(快速傅立叶变换)表示,并且需要比标准线性插值更多的处理。

    RGB 值代表什么。

    每个红色、绿色和蓝色通道代表该通道强度/亮度的平方根。 (这是一个接近的通用近似值)因此,当您进行插值时,您需要转换为正确的值,然后进行插值,然后再转换回对数值。

    正确插值

    function interpolateLinear(pos,c1,c2){ // pos 0-1, c1,c2 are objects {r,g,b}
        return {
           r : Math.sqrt((c2.r * c2.r + c1.r * c1.r) * pos + c1.r * c1.r),
           g : Math.sqrt((c2.g * c2.g + c1.g * c1.g) * pos + c1.g * c1.g),
           b : Math.sqrt((c2.b * c2.b + c1.b * c1.b) * pos + c1.b * c1.b),
        };
    }
    

    请务必注意,绝大多数数字处理软件都不能正确插值。这部分是由于开发人员对输出格式的无知(为什么我会尽可能地谈论它),部分是由于遵守那些仅仅为了显示图像而苦苦挣扎的旧计算机,更不用说处理它了(尽管我不买那个借口)。

    HTML5 也不例外,几乎在所有插值中都会错误地插值像素值。这会产生暗带,其中存在强烈的色调对比和较暗的总亮度,用于放大和缩小的图像。一旦您注意到错误,它将永远让您烦恼,因为今天的硬件很容易胜任。

    为了说明错误插值的严重程度,下图显示了正确的(顶部)和使用 SVG 过滤器(底部)插值的 canvas 2D API。

    二维线性插值(双线性)

    通过依次执行每个轴来完成沿两个轴的插值。首先沿 x 轴插值,然后沿 y 轴插值。您可以将其作为 2 道工序或单道工序来完成。

    以下函数将在任何子像素坐标处进行插值。此功能不是为速度而构建的,还有很大的优化空间。

    // Get pixel RGBA value using bilinear interpolation.
    // imgDat is a imageData object, 
    // x,y are floats in the original coordinates
    // Returns the pixel colour at that point as an array of RGBA
    // Will copy last pixel's colour
    function getPixelValue(imgDat, x,y, result = []){ 
        var i;
        // clamp and floor coordinate
        const ix1 = (x < 0 ? 0 : x >= imgDat.width ? imgDat.width - 1 : x)| 0;
        const iy1 = (y < 0 ? 0 : y >= imgDat.height ? imgDat.height - 1 : y | 0;
        // get next pixel pos
        const ix2 = ix1 === imgDat.width -1 ? ix1 : ix1 + 1;
        const iy2 = iy1 === imgDat.height -1 ? iy1 : iy1 + 1;
        // get interpolation position 
        const xpos = x % 1;
        const ypos = y % 1;
        // get pixel index
        var i1 = (ix1 + iy1 * imgData.width) * 4;
        var i2 = (ix2 + iy1 * imgData.width) * 4;
        var i3 = (ix1 + iy2 * imgData.width) * 4;
        var i4 = (ix2 + iy2 * imgData.width) * 4;
    
        // to keep code short and readable get data alias
        const d = imgDat.data;
    
        for(i = 0; i < 3; i ++){
            // interpolate x for top and bottom pixels
            const c1 = (d[i2] * d[i2++] - d[i1] * d[i1]) * xpos + d[i1] * d[i1 ++];
            const c2 = (d[i4] * d[i4++] - d[i3] * d[i3]) * xpos + d[i3] * d[i3 ++];
    
            // now interpolate y
            result[i] = Math.sqrt((c2 - c1) * ypos + c1);
        }
    
        // and alpha is not logarithmic
        const c1 = (d[i2] - d[i1]) * xpos + d[i1];
        const c2 = (d[i4] - d[i3]) * xpos + d[i3];
        result[3] = (c2 - c1) * ypos + c1;
        return result;
    }
    
    
     const upScale = 4;
     // usage
     const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
     const imgData2 = ctx.createImageData(ctx.canvas.width * upScale, ctx.canvas.height * upScale);
     const res = new Uint8ClampedArray(4);
     for(var y = 0; y < imgData2.height; y++){
         for(var x = 0; x < imgData2.width; x++){
             getPixelValue(imgData,x / upScale, y / upScale, res);
             imgData2.data.set(res,(x + y * imgdata2.width) * 4);
         }
     }
    

    8 次高档画布示例

    该示例使用上述函数将测试图案放大 8 倍。显示三个图像。然后将原来的64乘8,计算出来的upscale使用对数双线性插值,然后使用canvas标准API drawImage进行upScale(使用默认插值,双线性)。

    // helper functions create canvas and get context
    const CImage = (w = 128, h = w) => (c = document.createElement("canvas"),c.width = w,c.height = h, c);
    const CImageCtx = (w = 128, h = w) => (c = CImage(w,h), c.ctx = c.getContext("2d"), c);
    // iterators
    const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); }; 
    const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };
    
    const upScale = 8;
    var canvas1 = CImageCtx(64,8);
    var canvas2 = CImageCtx(canvas1.width * upScale, canvas1.height * upScale);
    var canvas3 = CImageCtx(canvas1.width * upScale, canvas1.height * upScale);
    
    
        // imgDat is a imageData object, 
        // x,y are floats in the original coordinates
        // Returns the pixel colour at that point as an array of RGBA
        // Will copy last pixel's colour
        function getPixelValue(imgDat, x,y, result = []){ 
            var i;
            // clamp and floor coordinate
            const ix1 = (x < 0 ? 0 : x >= imgDat.width ? imgDat.width - 1 : x)| 0;
            const iy1 = (y < 0 ? 0 : y >= imgDat.height ? imgDat.height - 1 : y) | 0;
            // get next pixel pos
            const ix2 = ix1 === imgDat.width -1 ? ix1 : ix1 + 1;
            const iy2 = iy1 === imgDat.height -1 ? iy1 : iy1 + 1;
            // get interpolation position 
            const xpos = x % 1;
            const ypos = y % 1;
            // get pixel index
            var i1 = (ix1 + iy1 * imgData.width) * 4;
            var i2 = (ix2 + iy1 * imgData.width) * 4;
            var i3 = (ix1 + iy2 * imgData.width) * 4;
            var i4 = (ix2 + iy2 * imgData.width) * 4;
    
            // to keep code short and readable get data alias
            const d = imgDat.data;
    
            // interpolate x for top and bottom pixels
            for(i = 0; i < 3; i ++){
                const c1 = (d[i2] * d[i2++] - d[i1] * d[i1]) * xpos + d[i1] * d[i1 ++];
                const c2 = (d[i4] * d[i4++] - d[i3] * d[i3]) * xpos + d[i3] * d[i3 ++];
    
                // now interpolate y
                result[i] = Math.sqrt((c2 - c1) * ypos + c1);
            }
    
            // and alpha is not logarithmic
            const c1 = (d[i2] - d[i1]) * xpos + d[i1];
            const c2 = (d[i4] - d[i3]) * xpos + d[i3];
            result[3] = (c2 - c1) * ypos + c1;
            return result;
        }
         const ctx = canvas1.ctx;    
         var cols = ["black","red","green","Blue","Yellow","Cyan","Magenta","White"];
         doFor(8,j => eachOf(cols,(col,i) => {ctx.fillStyle = col; ctx.fillRect(j*8+i,0,1,8)}));
         eachOf(cols,(col,i) => {ctx.fillStyle = col; ctx.fillRect(i * 8,4,8,4)});
         
         const imgData = ctx.getImageData(0, 0, canvas1.width, canvas1.height);
         const imgData2 = ctx.createImageData(canvas1.width * upScale, canvas1.height * upScale);
         const res = new Uint8ClampedArray(4);
         for(var y = 0; y < imgData2.height; y++){
             for(var x = 0; x < imgData2.width; x++){
                 getPixelValue(imgData,x / upScale, y / upScale, res);
                 imgData2.data.set(res,(x + y * imgData2.width) * 4);
             }
         }
         canvas2.ctx.putImageData(imgData2,0,0);
         function $(el,text){const e = document.createElement(el); e.textContent = text; document.body.appendChild(e)};
       
         document.body.appendChild(canvas1);
         $("div","Next Logarithmic upscale using linear interpolation * 8");
         document.body.appendChild(canvas2);
         canvas3.ctx.drawImage(canvas1,0,0,canvas3.width,canvas3.height);
         document.body.appendChild(canvas3);
         $("div","Previous Canvas 2D API upscale via default linear interpolation * 8");
         $("div","Note the overall darker result and dark lines at hue boundaries");
         
    canvas { border : 2px solid black; }

    【讨论】:

    • 我明白了。 PS 的采样是双线性的,但我认为将其应用于 1D 图像会产生类似于线性插值的结果。也许差异是由于采样方法(双线性)。一旦我有一个有效的线性插值,我将继续进行双线性插值。顺便说一句,我将在我的pinch effect 上应用双线性插值以获得更平滑的结果。由于我们在我的画布相关问题上遇到过几次,我想让你知道:)
    猜你喜欢
    • 2015-02-22
    • 2011-04-29
    • 2011-05-24
    • 2023-03-19
    • 1970-01-01
    • 1970-01-01
    • 2017-02-22
    • 2014-12-24
    • 2016-07-05
    相关资源
    最近更新 更多