JavaScript - 从图像的某个区域获取平均颜色

时间:2017-06-15 00:28:31

标签: javascript jquery colors html5-canvas color-picker

我需要使用JavaScript从图像的矩形区域获取平均颜色。

我尝试使用tracking.js,但不允许指定区域而不是单个像素。

2 个答案:

答案 0 :(得分:7)

如你所说从图像中获取矩形区域的颜色,我会假设您需要获得给定区域的平均颜色,而不是颜色一个像素。

无论如何,两者都以非常相似的方式完成:

从图像或画布中获取单个像素的颜色/值

要获得单个像素的颜色,首先要将该图像绘制到画布上:

const image = document.getElementById('image');
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const width = image.width;
const height = image.height;

canvas.width = width;
canvas.height = height;

context.drawImage(image, 0, 0, width, height);

然后像这样获得单个像素的值:

const data = context.getImageData(X, Y, 1, 1).data;

// RED   = data[0]
// GREEN = data[1]
// BLUE  = data[2]
// ALPHA = data[3]

从图像或画布上的区域获取平均颜色/值

您需要使用相同的CanvasRenderingContext2D.getImageData()来获取更宽(多像素)区域的值,您可以通过更改其第三个和第四个参数来完成。该功能的签名是:

ImageData ctx.getImageData(sx, sy, sw, sh);
  • sx:将从中提取ImageData的矩形左上角的x坐标。
  • sy:将从中提取ImageData的矩形左上角的y坐标。
  • sw:将从中提取ImageData的矩形的宽度。
  • sh:将从中提取ImageData的矩形的高度。

您可以看到它返回ImageData个对象whatever that is。这里的重要部分是该对象具有.data属性,其中包含我们所有的像素值。

但是,请注意,.data属性是一维Uint8ClampedArray,这意味着所有像素的组件都已展平,因此您得到的内容如下所示:< / p>

让我们假设你有一个像这样的2x2图像:

 RED PIXEL |       GREEN PIXEL
BLUE PIXEL | TRANSPARENT PIXEL

然后,你会得到这样的:

[ 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 0, 0, 0, 0 ]  
|   1ST PIXEL   |   2ND PIXEL   |   3RD PIXEL   | 4TH  PIXEL |        

让我们看看它在行动

在此示例中,您可以透明地在50 × 50px 100 × 100px图片上移动PNG区域/画笔。该区域的平均颜色将显示在图片下方的两个样本中,一个包含alpha频道的平均值,另一个包含该频道的平均颜色:

&#13;
&#13;
const brush = document.getElementById('brush');
const avgSolidColor = document.getElementById('avgSolidColor');
const avgAlphaColor = document.getElementById('avgAlphaColor');
const avgSolidWeighted = document.getElementById('avgSolidWeighted');
const image = document.getElementById('image');
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const width = image.width;
const height = image.height;

canvas.width = width;
canvas.height = height;

context.drawImage(image, 0, 0, width, height);

document.onmousemove = function(e) {

  const pageX = Math.max(Math.min(e.clientX - 25, 110), 10);
  const pageY = Math.max(Math.min(e.clientY - 25, 110), 10);

  brush.style.left = pageX + 'px';
  brush.style.top = pageY + 'px';

  const imageX = pageX - 10;
  const imageY = pageY - 10;

  let R = 0;
  let G = 0;
  let B = 0;
  let A = 0;
  let wR = 0;
  let wG = 0;
  let wB = 0;
  let wTotal = 0;

  const data = context.getImageData(imageX, imageY, 50, 50).data;
  
  const components = data.length;
  
  for (let i = 0; i < components; i += 4) {
    // A single pixel (R, G, B, A) will take 4 positions in the array:
    const r = data[i];;
    const g = data[i + 1];
    const b = data[i + 2];
    const a = data[i + 3];
    
    // Update components for solid color and alpha averages:
    R += r;
    G += g;
    B += b;
    A += a;
    
    // Update components for alpha-weighted average:
    const w = a / 255;
    wR += r * w;
    wG += g * w;
    wB += b * w;
    wTotal += w;
  }
  
  const pixelsPerChannel = components / 4;
  
 // The | operator is used here to perform an integer division:

  R = R / pixelsPerChannel | 0;
  G = G / pixelsPerChannel | 0;
  B = B / pixelsPerChannel | 0;
  wR = wR / wTotal | 0;
  wG = wG / wTotal | 0;
  wB = wB / wTotal | 0;

  // The alpha channel need to be in the [0, 1] range:

  A = A / pixelsPerChannel / 255;
  
  avgSolidColor.style.background = `rgb(${ R }, ${ G }, ${ B })`;
  avgAlphaColor.style.background = `rgba(${ R }, ${ G }, ${ B }, ${ A })`;
  avgSolidWeighted.style.background = `rgb(${ wR }, ${ wG }, ${ wB })`;
};
&#13;
html {
  cursor: crosshair;
  font-size: 0;
}

body {
  margin: 10px;
}

#image {
  border: 1px solid #000;
  border-bottom: none;
  width: 150px;
  box-sizing: border-box;
}

#brush {
  position: absolute;
  top: 0;
  left: 0;
  background: rgba(0, 255, 255, .5);
  pointer-events: none;
  width: 50px;
  height: 50px;
}

.sample {
  float: left;
  box-sizing: border-box;
  border: 1px solid #000;
  border-right: none;
  width: 50px;
  height: 25px;
}

.sample:last-child {
  border-right: 1px solid #000;
}
&#13;
<img id="image" src="data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7" />

<div id="brush"></div>

<div>
  <div class="sample" id="avgSolidColor"></div>
  <div class="sample" id="avgAlphaColor"></div>
  <div class="sample" id="avgSolidWeighted"></div>
</div>
&#13;
&#13;
&#13;

⚠️注意如果我尝试使用较长的数据URI,如果我包含外部图像或答案大于允许值,我会使用小数据URI来避免Cross-Origin问题。

这些平均颜色看起来很奇怪,不是吗? (@ SMT&#39;评论)

如果将画笔移动到左上角,您会看到.avgSolidColor的颜色(第一个)几乎是黑色。这是因为该区域中的大多数像素都是完全透明的,因此它们的值恰好或非常接近0, 0, 0, 255。这意味着,对于我们处理的每个广告,RGB都不会改变或改变很少,而pixelsPerChannel仍会将其考虑在内,所以我们最后将一个小数字(因为我们为大多数人添加0)除以一个大数字(画笔中的像素总数),这给了我们一个非常接近{{1}的值(黑色)。

例如,如果我们只有两个像素00, 0, 0, 255,我们可能会认为255, 0, 0, 0频道的平均值为R。但是,它将是255。但不要担心,我们会看到下一步该怎么做。

另一方面,(0 + 255) / 2 | 1 = 127的颜色(第二个)几乎是白色。嗯,实际上并不是真的,它只是看起来白色,因为我们现在正在使用alpha通道,它接近.avgAlphaColor,它几​​乎是透明的,我们只是看到页面的背景,在这种情况下是白色。

Alpha加权平均值(@ SMT&#39评论解决方案)

然后,我们可以做些什么来解决这个问题?好吧,事实证明我们只需要使用alpha通道作为我们(现在加权)平均值的权重:

这意味着,如果某个像素为0r, g, b, a位于a区间,我们将更新变量,如下所示:

[0, 255]

请注意像素越透明(const w = a / 255; // w is in the interval [0, 1] wR += r * w; wG += g * w; wB += b * w; wTotal += w; 越接近0),我们在计算中关注它的值就越少。

答案 1 :(得分:0)

不确定使用画布和ImageData在DOM中这种技术是否可行,但是在使用Flash和Actionscript的日子里,这是一个很好的捷径。 在我的用例中,所讨论的区域是统一的:所有区域都是大小相同的正方形,例如我有1280x720 来自网络摄像头或视频的BitmapData(相当于ImageData),我需要平均颜色才能形成一个网格,例如代表输入图像的16x9区域。 我所做的就是:将整个输入图像绘制为一个位图,其像素大小值等于区域列和行的数量。例如。将1280x720像素的图片绘制为16x9像素的位图。 然后,只需使用本机方法即可在缩小的图像中获得恰好一个像素的颜色。 此方法与手动操作一样精确,并且速度快得多,代码也简单得多。