【问题标题】:sampling an image a tile at a time using canvas, getImageData and a Web Worker使用画布、getImageData 和 Web Worker 一次对图像进行采样
【发布时间】:2015-04-20 23:44:11
【问题描述】:

我正在尝试构建一个简单的基于 HTML5 画布的图像处理器,它获取图像并生成它的平铺版本,每个平铺是底层图像区域的平均颜色。

这很容易在 Web Worker 的上下文之外执行,但我想使用一个 worker,以免阻塞 ui 处理线程。数据采用的 Uint8ClampedArray 形式让我对如何逐块处理它感到头疼。

下面是一个 plunk,展示了我到目前为止所做的事情以及它是如何不起作用的。

http://plnkr.co/edit/AiHmLM1lyJGztk8GHrso?p=preview

相关代码在worker.js中

这里是:

onmessage = function (e) {
    var i,
        j = 0,
        k = 0,
        data = e.data,
        imageData = data.imageData,
        tileWidth = Math.floor(data.tileWidth),
        tileHeight = Math.floor(data.tileHeight),
        width = imageData.width,
        height = imageData.height,
        tile = [],
        len = imageData.data.length,
        offset,
        processedData = [],
        tempData = [],
        timesLooped = 0,
        tileIncremented = 1;

    function sampleTileData(tileData) {
        var blockSize = 20, // only visit every x pixels
            rgb = {r:0,g:0,b:0},
            i = -4,
            count = 0,
            length = tileData.length;

            while ((i += blockSize * 4) < length) {
                if (tileData[i].r !== 0 && tileData[i].g !== 0 && tileData[i].b !== 0) {
                    ++count;
                    rgb.r += tileData[i].r;
                    rgb.g += tileData[i].g;
                    rgb.b += tileData[i].b;
                }
            }

            // ~~ used to floor values
            rgb.r = ~~(rgb.r/count);
            rgb.g = ~~(rgb.g/count);
            rgb.b = ~~(rgb.b/count);

            processedData.push(rgb);
    }

    top:
    for (; j <= len; j += (width * 4) - (tileWidth * 4), timesLooped++) {

        if (k === (tileWidth * 4) * tileHeight) {
            k = 0;
            offset = timesLooped - 1 < tileHeight ? 4 : 0;
            j = ((tileWidth * 4) * tileIncremented) - offset;
            timesLooped = 0;
            tileIncremented++;
            sampleTileData(tempData);
            tempData = [];
            //console.log('continue "top" loop for new tile');
            continue top;
        }

        for (i = 0; i < tileWidth * 4; i++) {
            k++;
            tempData.push({r: imageData.data[j+i], g: imageData.data[j+i+1], b: imageData.data[j+i+2], a: imageData.data[j+i+3]});
        }

        //console.log('continue "top" loop for new row per tile');

    }

    postMessage(processedData);
};

我确信有一种更好的方法可以从标记的 for 循环开始完成我正在尝试做的事情。因此,任何替代方法或建议将不胜感激。

更新:
我采取了不同的方法来解决这个问题:

http://jsfiddle.net/TunMn/425/

关闭,但没有。

我知道问题出在哪里,但我不知道如何修改它。同样,我们将不胜感激任何帮助。

【问题讨论】:

    标签: javascript html image canvas web-worker


    【解决方案1】:

    方法 1:手动计算每个图块的平均值

    这是您可以尝试的一种方法:

    • 只需要读取,稍后可以使用硬件加速进行更新
    • 对每一行使用异步调用(如果图像很宽,则使用平铺)

    这会给出准确的结果,但速度较慢并且取决于 CORS 限制。

    示例

    您可以在下面看到原始图像闪烁。这表明异步方法有效,因为它允许 UI 在处理块中的图块时进行更新。

    window.onload = function() {
      var img = document.querySelector("img"),
          canvas = document.querySelector("canvas"),
          ctx = canvas.getContext("2d"),
          w = img.naturalWidth, h = img.naturalHeight,
          
          // store average tile colors here:
          tileColors = [];
      
      // draw in image
      canvas.width = w; canvas.height = h;
      ctx.drawImage(img, 0, 0);
      
      // MAIN CALL: calculate, when done the callback function will be invoked
      avgTiles(function() {console.log("done!")});
      
      // The tiling function
      function avgTiles(callback) {
      
        var cols = 8,          // number of tiles (make sure it produce integer value
            rows = 8,          //  for tw/th below:)
            tw = (w / cols)|0, // pixel width/height of each tile
            th = (h / rows)|0,
            x = 0, y = 0;
        
        (function process() {  // for async processing
        
          var data, len, count, r, g, b, i;
          
          while(x < cols) {    // get next tile on x axis
            r = g = b = i = 0;
            data = ctx.getImageData(x * tw, y * th, tw, th).data;  // single tile
            len = data.length;
            count = len / 4;
            while(i < len) {   // calc this tile's color average
              r += data[i++];  // add values for each component
              g += data[i++];
              b += data[i++];
              i++
            }
            
            // store average color to array, no need to write back at this point
            tileColors.push({
              r: (r / count)|0,
              g: (g / count)|0, 
              b: (b / count)|0
            });
    
            x++;               // next tile
          }
          y++;                 // next row, but do an async break below:
          
          if (y < rows) {
            x = 0;
            setTimeout(process, 9);  // call it async to allow browser UI to update
          }
          else {
            
            // draw tiles with average colors, fillRect is faster than setting each pixel:
            for(y = 0; y < rows; y++) {
              for(x = 0; x < cols; x++) {
                var col = tileColors[y * cols + x];   // get stored color
                ctx.fillStyle = "rgb(" + col.r + "," + col.g + "," + col.b + ")";
                ctx.fillRect(x * tw, y * th, tw, th);
              }
            }
            
            // we're done, invoke callback
            callback()
          }
        })();   // to self-invoke process()
      }
    };
    <canvas></canvas>
    <img src="http://i.imgur.com/X7ZrRkn.png" crossOrigin="anonymous">

    方法 2:让浏览器完成工作

    我们还可以让浏览器利用插值和采样来完成整个工作。

    当浏览器缩小图像时,它会计算每个新像素的平均值。如果我们在放大时关闭线性插值,我们将得到每个平均像素为方形块:

    • 按比例缩小图像,将图块数作为像素数
    • 关闭图像平滑
    • 将小图像重新缩放到所需大小

    这将比第一种方法快很多倍,并且您将能够使用受 CORS 限制的图像。请注意,它可能不如第一种方法准确,但是,可以通过逐步缩小图像来提高准确度,每一步缩小一半。

    示例

    window.onload = function() {
      var img = document.querySelector("img"),
          canvas = document.querySelector("canvas"),
          ctx = canvas.getContext("2d"),
          w = img.naturalWidth, h = img.naturalHeight;
      
      // draw in image
      canvas.width = w; canvas.height = h;
      
      // scale down image so number of pixels represent number of tiles,
      // here use two steps so we get a more accurate result:
      ctx.drawImage(img, 0, 0, w, h, 0, 0, w*0.5, h*0.5);    // 50%
      ctx.drawImage(canvas, 0, 0, w*0.5, h*0.5, 0, 0, 8, 8); // 8 tiles
      
      // turn off image-smoothing
      ctx.imageSmoothingEnabled = 
      ctx.msImageSmoothingEnabled = 
      ctx.mozImageSmoothingEnabled = 
      ctx.webkitImageSmoothingEnabled = false;
      
      // scale image back up
      ctx.drawImage(canvas, 0, 0, 8, 8, 0, 0, w, h);
    };
    <canvas></canvas>
    <img src="http://i.imgur.com/X7ZrRkn.png" crossOrigin="anonymous">

    【讨论】:

    • 感谢您抽出宝贵时间回答。我会看看我能用这种方法得到什么。
    • @damo-s 添加了一个 2. 方法,该方法必须更简单、更快,但不那么准确(可以通过分几个步骤缩小图像使其更准确)。
    • 第一种方法最终引导我走向成功。再次感谢!
    猜你喜欢
    • 1970-01-01
    • 2015-05-03
    • 2012-10-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-08
    相关资源
    最近更新 更多