具有半径(blur?)的颜色选择器的算法



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

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

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

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

Blur在技术上是平均的,但它会在扫描区域中找到每个像素的平均值。当你只想要整个区域的平均颜色,而不是每个像素时,这就太复杂了。与寻找平均值的线性扫描相比,使用模糊意味着性能较差,而这正是寻找区域平均颜色所需要的。

你想要的是该区域所有像素的平均值。这方面的流程如下:

  • 定义拾取器半径
  • 计算长度(半径x半径)
  • 定义圆适合的正方形区域
  • 遍历每条线,对每个点执行以下操作:
    • 计算当前x、y到中心的距离
    • 确定它是否在预先计算的长度内
    • 如果没有跳过,如果是,则将像素存储到阵列中
  • 计算像素数,将它们相加,然后除以该圆的平均像素数=>

计算从(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>

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

最新更新