半径颜色选择器的算法(模糊?)

时间:2015-02-26 19:06:22

标签: javascript algorithm canvas

我想从画布上的特定点采样颜色。而不只是一个像素的颜色:

context.getImageData(x, y, 1, 1).data;

我想从这一点周围的像素中采样颜色。我想像Quasimondo's StackBlur这样的东西是一个合适的算法,用于计算某个半径点周围的平均颜色。模糊算法是正确的方法吗?对于这种用途,是否有任何特定的模糊算法优于其他算法?

客观答案将比较周围像素的算术平均值与模糊图像或加权平均值。

1 个答案:

答案 0 :(得分:1)

模糊在技术上是平均的,但它会在扫描的区域中找到平均每像素。当您只想要整个区域的平均颜色而不是每个像素时,这会过于复杂。使用模糊意味着与线性扫描相比性能较差,无法找到平均值,这是查找该区域平均颜色所需的。

你想要的是该区域中所有像素的平均值。一个过程就像:

  • 定义选择器半径
  • 计算长度(半径x半径)
  • 定义圆圈适合的方形区域
  • 遍历每一行,为每个点做:
    • 计算从当前x,y到中心的距离
    • 确定它是否在预先计算的长度内
    • 如果没有跳过,如果是,则将像素存储到数组
  • 计算像素数,将它们相加并除以count =>该圈子的平均像素

计算从(x,y)到中心的距离:

var length = radius * radius;          // max distance from center
var diffX = Math.abs(x - centerX); 
var diffY = Math.abs(y - centerY);
var dist = diffX*diffX + diffY*diffY;  // distance center -> x,y

if (dist <= length) { ...add sample to array... }

// ...
// do average: sum of all r, sum of all g etc.. 
// r divided on number of entries in array, etc. (round of values)

(提示:在这种情况下你不需要设置距离)

// vars and load some image
var vcanvas = document.querySelector("canvas"),
    vctx = vcanvas.getContext("2d"),
    canvas = document.createElement("canvas"),
    ctx = canvas.getContext("2d"),
    w = 500, h = 300,
    img = new Image();
    
img.crossOrigin = "";
img.onload = prep;
img.src = "//i.imgur.com/SetDGOB.jpg";

// setup and prepare image for canvases
function prep() {
  vcanvas.width = canvas.width = w;
  vcanvas.height = canvas.height = h;
  
  ctx.drawImage(img, 0, 0, w, h);  // draw in off-screen canvas
  vcanvas.style.backgroundImage = "url(" + canvas.toDataURL() + ")";  // set as bg to visual canvas
  vctx.font = "16px sans-serif";   // to draw values to screen
  vctx.lineWidth = 8;              // lupe ring width
  
  // sample image on mouse move
  vcanvas.onmousemove = function(e) {
    var rect = vcanvas.getBoundingClientRect(),  // correct mouse pos.
        x = e.clientX - rect.left,
        y = e.clientY - rect.top,
        radius = 6,                              // sample radius
        zoom = 4,                                // zoom (for visuals only)
        sx = (w * zoom - w) / w,                 // calc scale factors
        sy = (h * zoom - h) / h,
        avg = sample(x, y, radius);              // sample area (average)
    
    // draw zoomed circle
    vctx.clearRect(0, 0, w, h);
    if (null == avg) return;                     // nothing to show
    
    vctx.beginPath();
    vctx.arc(x, y, radius * zoom, 0, 2*Math.PI);
    vctx.fill();
    
    //vctx.scale(zoom, zoom);
    vctx.translate(-x * sx, -y * sy);
    vctx.globalCompositeOperation = "source-atop";
    vctx.drawImage(canvas, 0, 0, w*zoom, h*zoom);
    vctx.setTransform(1,0,0,1,0,0);
    vctx.globalCompositeOperation = "source-over";

    // draw black ring
    vctx.beginPath();
    vctx.arc(x, y, radius * zoom + vctx.lineWidth * 0.5, 0, 2*Math.PI);
    vctx.strokeStyle = "#555";
    vctx.closePath();
    vctx.stroke();
    
    // draw average color ring
    vctx.beginPath();
    vctx.arc(x, y, radius * zoom + vctx.lineWidth * 0.5 + 1, 0, 2*Math.PI);
    vctx.strokeStyle = "rgb(" + avg.r + "," + avg.g + "," + avg.b + ")";
    vctx.closePath();
    vctx.stroke();

    vctx.fillStyle = "#000";
    vctx.fillText("x:" + x + " y:" + y + " " + vctx.strokeStyle, 12, 22);

    vctx.fillStyle = "#0f0";
    vctx.fillText("x:" + x + " y:" + y + " " + vctx.strokeStyle, 10, 20);

  }
}

// This will do the color sampling from the circle
function sample(cx, cy, radius) {
  var r = 0, g = 0, b = 0, a = 0, cnt = 0,  // initialize
      length = radius * radius,             // calc max distance from center
      region,                               // region with pixels to sample
      idata, buffer, len,                   // data buffers
      i, p, x, y, dx, dy, dist;
  
  // calc region:
  region = {
    x: cx - radius,
    y: cy - radius,
    w: Math.min(w-cx+radius, radius*2)|0,
    h: Math.min(h-cy+radius, radius*2)|0
  };

  // check and adjust region to fit inside canvas area
  if (region.x < 0) {region.w + region.x; region.x = 0}
  if (region.y < 0) {region.h + region.y; region.y = 0}
  if (region.w < 1 || region.h < 1 ) return null;
  
  // get buffer for region
  idata = ctx.getImageData(region.x|0, region.y|0, region.w|0, region.h|0);
  buffer = idata.data;
  len = buffer.length;
  
  // iterate region and sample pixels with circle
  for(y = 0; y < region.h; y++) {
    for(x = 0; x < region.w; x++) {
      dx = radius - x;
      dy = radius - y;
      dist = Math.abs(dx*dx + dy*dy); // dist. from center to current x,y in buffer
      
      // add value if within circle
      if (dist <= length) {
        p = (y * region.h + x)*4;
        r += buffer[p];
        g += buffer[p+1];
        b += buffer[p+2];
        a += buffer[p+3];
        cnt++;
      }
    }
  }
  
  // no samples? (should never happen!)
  if (!cnt) return null;
  
  // calculate and return average
  return {
    r: (r / cnt + 0.5)|0,
    g: (g / cnt + 0.5)|0,
    b: (b / cnt + 0.5)|0,
    a: (a / cnt + 0.5)|0
  }
}
canvas {cursor:crosshair}
<canvas></canvas>

(有一些方法可以通过存储整个图像的缓冲区而不是屏幕外的画布来优化这些代码,根据偏移等获取区域,但为了简单起见我保持这种方式(?)... )。