最佳RGB组合将图像转换为黑白“阈值”

时间:2017-07-17 19:38:30

标签: javascript canvas

我需要构建一个简单的应用程序,将彩色图像或灰度图像转换为黑白图像,我正在考虑循环每个像素并检查RGB值以及是否所有这些都小于特定值(比方说20)将像素重绘为黑色,如果大于该值,则将像素重绘为白色。这样的事情。

function blackWhite(context, canvas) {
    var imgData = context.getImageData(0, 0, canvas.width, canvas.height);
        var pixels  = imgData.data;
        for (var i = 0, n = pixels.length; i < n; i += 4) {
        if (pixels[i] <= 20 || pixels[i+1] <= 20 || pixels[i+2] <= 20){ 
              pixels[i  ] = 0;        // red
           pixels[i+1] = 0;        // green
           pixels[i+2] = 0;        // blue
        }else{
              pixels[i  ] = 255;        // red
           pixels[i+1] = 255;        // green
           pixels[i+2] = 255;        // blue
        }
    }
    //redraw the image in black & white
    context.putImageData(imgData, 0, 0);
  }

最大的问题是,红色,绿色和蓝色的正确组合是什么,将像素定义为黑色,这考虑到人眼感知颜色不同,作为我们眼睛的一个例子,它更多重要的是绿色而不是红色和蓝色,我已经尝试了一些值,但我没有接近黑色和图像就像你可以通过将扫描仪中的纸张数字化为黑白而得到的图像。

当然,如果有更快的方法,我会完全欣赏它。

3 个答案:

答案 0 :(得分:3)

我相信你要找的是相对亮度。虽然不是最先进的阈值处理方法,但它更符合人类感知光线的方式,这就是我认为你想要的。

https://en.wikipedia.org/wiki/Relative_luminance

根据维基百科文章,亮度可按如下方式计算:

let lum = .2126 * red + .7152 * green + .0722 * blue

这个值将是一个的一小部分,所以如果你想在中间分开它,使用阈值为.5

修改

真正的问题在于选择阈值。并非所有图像都以相同的方式点亮,并且具有较低亮度(即,更多黑色)的像素的图片将受益于较低的阈值。 您可以考虑使用一些技术,例如分析图像的直方图。

答案 1 :(得分:2)

<强>更新

我没有正确阅读问题所以我更新了答案以反映这个问题。将旧的答案留作那些感兴趣的点。

要创建最简单的阈值滤波器,只需对RGB通道求和,如果超过阈值,则make像素为白色,否则为黑色。

// assumes canvas and ctx defined;
// image to process, threshold level range 0 - 255
function twoTone(image, threshold) {
  ctx.drawImage(image,0,0):
  const imgD = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const d = imgD.data;
  var v,i = 0;
  while (i < d.length) {
    v = (d[i++] + d[i++] + d[i]) < (threshold * 3) ? 0 : 255;
    i -= 2;
    d[i++] = d[i++] = d[i++] = v;
    i++;
  }
  ctx.putImageData(imgD, 0, 0);
}

但还有其他方法。通过对上述内容的修改,您可以在阈值处创建渐变。这软化了上述方法可以产生的硬边界。

有时您需要快速执行此功能,或者由于跨源安全限制,您可能无法访问像素数据。在这种情况下,您可以使用堆栈复合操作approch,通过在连续的&#34;乘法&#34;中分层图像来创建阈值。和#34;打火机&#34; globalCompositeOperations。虽然这种方法可以产生高质量的结果,但输入值有点模糊,如下例所示。如果要匹配特定阈值和截止宽度,则必须校准它。

演示

更新

由于答案形式中有更多信息,我更新了代码以保持比较公平。

我已经更新了演示版,以包含熊的 K3N 图片,并提供了3种通过平均值查找阈值的方法。 (我修改了K3N的答案以适应演示。它在功能上是一样的)。顶部的按钮允许您从两个图像和显示尺寸中进行选择,最后三个使用三种方法查找并应用阈值。

使用滑块更改阈值和截止值以及适用的金额值。

&#13;
&#13;
const image = new Image;
const imageSrcs = ["https://upload.wikimedia.org/wikipedia/en/2/24/Lenna.png", "//i.imgur.com/tbRxrWA.jpg"];
var scaleFull = false;
var imageBWA;
var imageBWB;
var imageBWC;
var amountA = -1;
var thresholdA = -1;
var thresholdB = -1;
var cutoffC = -1;
var thresholdC = -1;


start();
//Using stacked global composite operations.
function twoTone(bw, amount, threshold) {
  bw.ctx.save();
  bw.ctx.globalCompositeOperation = "saturation";
  bw.ctx.fillStyle = "#888"; // no saturation
  bw.ctx.fillRect(0, 0, bw.width, bw.height);
  amount /= 16;
  threshold = 255 - threshold;

  while (amount-- > 0) {
    bw.ctx.globalAlpha = 1;
    bw.ctx.globalCompositeOperation = "multiply";
    bw.ctx.drawImage(bw, 0, 0);
    const a = (threshold / 127);
    bw.ctx.globalAlpha = a > 1 ? 1 : a;
    bw.ctx.globalCompositeOperation = "lighter";
    bw.ctx.drawImage(bw, 0, 0);
    if (a > 1) {
      bw.ctx.globalAlpha = a - 1 > 1 ? 1 : a - 1;
      bw.ctx.drawImage(bw, 0, 0);
      bw.ctx.drawImage(bw, 0, 0);
    }
  }
  bw.ctx.restore();
}

// Using per pixel processing simple threshold.
function twoTonePixelP(bw, threshold) {
  const imgD = bw.ctx.getImageData(0, 0, bw.width, bw.height);
  const d = imgD.data;
  var i = 0;
  var v;
  while (i < d.length) {
    v = (d[i++] + d[i++] + d[i]) < (threshold * 3) ? 0 : 255;
    i -= 2;
    d[i++] = d[i++] = d[i++] = v;
    i++;
  }
  bw.ctx.putImageData(imgD, 0, 0);
}

//Using per pixel processing with cutoff width
function twoTonePixelCutoff(bw, cutoff, threshold) {
  if (cutoff === 0) {
    twoTonePixelP(bw, threshold);
    return;
  }
  const eCurve = (v, p) => {
    var vv;
    return (vv = Math.pow(v, 2)) / (vv + Math.pow(1 - v, 2))
  }
  const imgD = bw.ctx.getImageData(0, 0, bw.width, bw.height);
  const d = imgD.data;
  var i = 0;
  var v;
  const mult = 255 / cutoff;
  const offset = -(threshold * mult) + 127;
  while (i < d.length) {
    v = ((d[i++] + d[i++] + d[i]) / 3) * mult + offset;
    v = v < 0 ? 0 : v > 255 ? 255 : eCurve(v / 255) * 255;
    i -= 2;
    d[i++] = d[i++] = d[i++] = v;
    i++;
  }
  bw.ctx.putImageData(imgD, 0, 0);
}


function OtsuMean(image, type) {
  // Otsu's method, from: https://en.wikipedia.org/wiki/Otsu%27s_Method#Variant_2
  //
  // The input argument pixelsNumber is the number of pixels in the given image. The 
  // input argument histogram is a 256-element histogram of a grayscale image 
  // different gray-levels.
  // This function outputs the threshold for the image.
  function otsu(histogram, pixelsNumber) {
    var sum = 0, sumB = 0, wB = 0, wF = 0, mB, mF, max = 0, between, threshold = 0;
    for (var i = 0; i < 256; i++) {
      wB += histogram[i];
      if (wB === 0) continue;
      wF = pixelsNumber - wB;
      if (wF === 0) break;
      sumB += i * histogram[i];
      mB = sumB / wB;
      mF = (sum - sumB) / wF;
      between = wB * wF * Math.pow(mB - mF, 2);
      if (between > max) {
        max = between;
        threshold = i;
      }
    }
    return threshold>>1;
  }
  const imgD = image.ctx.getImageData(0, 0, image.width, image.height);
  const d = imgD.data;
  var histogram = new Uint16Array(256);
  if(type == 2){
    for(var i = 0; i < d.length; i += 4) {
      histogram[Math.round(d[i]*.2126+d[i+1]*.7152+d[i+2]*.0722)]++;
    }
  }else{
    for(var i = 0; i < d.length; i += 4) {
      histogram[Math.round(Math.sqrt(d[i]*d[i]*.2126+d[i+1]*d[i+1]*.7152+d[i+2]*d[i+2]*.0722))]++;
    }
  }
  

  return otsu(histogram, image.width * image.height);
}
// finds mean via the perceptual 2,7,1 approx rule rule
function calcMean(image, rule = 0){
  if(rule == 2 || rule == 3){
    return OtsuMean(image, rule);
  }
  const imgD = image.ctx.getImageData(0, 0, image.width, image.height);
  const d = imgD.data;
  var i = 0;
  var sum = 0;
  var count = 0

  while (i < d.length) {
    if(rule == 0){
      sum += d[i++] * 0.2 + d[i++] * 0.7 + d[i++] * 0.1;
      count += 1;
    }else{
      sum += d[i++] + d[i++] + d[i++];
      count += 3;
    }
    i++;
  }

  return (sum / count) | 0;

}

// creates a canvas copy of an image.
function makeImageEditable(image) {
  const c = document.createElement("canvas");
  c.width = (image.width / 2) | 0;
  c.height = (image.height / 2) | 0;
  c.ctx = c.getContext("2d");
  c.ctx.drawImage(image, 0, 0, c.width, c.height);
  return c;
}
function updateEditableImage(image,editable) {
  editable.width = (image.width / (scaleFull ? 1 : 2)) | 0;
  editable.height = (image.height / (scaleFull ? 1 : 2)) | 0;
  editable.ctx.drawImage(image, 0, 0, editable.width, editable.height);
}





// load test image and when loaded start UI
function start() {
  image.crossOrigin = "anonymous";
  image.src = imageSrcs[0];
  imageStatus.textContent = "Loading image 1";
  image.onload = ()=>{
    imageBWA = makeImageEditable(image);
    imageBWB = makeImageEditable(image);
    imageBWC = makeImageEditable(image);
    canA.appendChild(imageBWA);
    canB.appendChild(imageBWB);
    canC.appendChild(imageBWC);
    imageStatus.textContent = "Loaded image 1.";
    startUI();
  }
}
function selectImage(idx){
  imageStatus.textContent = "Loading image " + idx;
  image.src = imageSrcs[idx];
  image.onload = ()=>{
    updateEditableImage(image, imageBWA);
    updateEditableImage(image, imageBWB);
    updateEditableImage(image, imageBWC);
    thresholdC = thresholdB = thresholdA = -1; // force update
    imageStatus.textContent = "Loaded image " + idx;
  }
}
function toggleScale(){
  scaleFull = !scaleFull;
  imageStatus.textContent = scaleFull ? "Image full scale." : "Image half scale"; 
  updateEditableImage(image, imageBWA);
  updateEditableImage(image, imageBWB);
  updateEditableImage(image, imageBWC);
  thresholdC = thresholdB = thresholdA = -1; // force update
}
function findMean(e){

      imageBWB.ctx.drawImage(image, 0, 0, imageBWB.width, imageBWB.height);
      var t = inputThresholdB.value = inputThresholdC.value = calcMean(imageBWB,e.target.dataset.method);
      imageStatus.textContent = "New threshold calculated " + t + ". Method : "+ e.target.dataset.name;
      thresholdB = thresholdC = -1;
};
  

// start the UI
function startUI() {
  imageControl.className = "imageSel";
  selImage1Btn.addEventListener("click",(e)=>selectImage(0));
  selImage2Btn.addEventListener("click",(e)=>selectImage(1));
  togFullsize.addEventListener("click",toggleScale);
  findMean1.addEventListener("click",findMean);
  findMean2.addEventListener("click",findMean);
  findMean3.addEventListener("click",findMean);

  // updates top image
  function update1() {
    if (amountA !== inputAmountA.value || thresholdA !== inputThresholdA.value) {
      amountA = inputAmountA.value;
      thresholdA = inputThresholdA.value;
      inputAmountValueA.textContent = amountA;
      inputThresholdValueA.textContent = thresholdA;
      imageBWA.ctx.drawImage(image, 0, 0, imageBWA.width, imageBWA.height);
      twoTone(imageBWA, amountA, thresholdA);
    }
    requestAnimationFrame(update1);
  }
  requestAnimationFrame(update1);

  // updates center image
  function update2() {
    if (thresholdB !== inputThresholdB.value) {
      thresholdB = inputThresholdB.value;
      inputThresholdValueB.textContent = thresholdB;
      imageBWB.ctx.drawImage(image, 0, 0, imageBWB.width, imageBWB.height);
      twoTonePixelP(imageBWB, thresholdB);
    }
    requestAnimationFrame(update2);
  }
  requestAnimationFrame(update2);

  // updates bottom image
  function update3() {
    if (cutoffC !== inputCutoffC.value || thresholdC !== inputThresholdC.value) {
      cutoffC = inputCutoffC.value;
      thresholdC = inputThresholdC.value;
      inputCutoffValueC.textContent = cutoffC;
      inputThresholdValueC.textContent = thresholdC;
      imageBWC.ctx.drawImage(image, 0, 0, imageBWC.width, imageBWC.height);
      twoTonePixelCutoff(imageBWC, cutoffC, thresholdC);
    }
    requestAnimationFrame(update3);
  }
  requestAnimationFrame(update3);

}
&#13;
.imageIso {
  border: 2px solid black;
  padding: 5px;
  margin: 5px;
  font-size : 12px;
}
.imageSel {
  border: 2px solid black;
  padding: 5px;
  margin: 5px;
}
#imageStatus {
  margin: 5px;
  font-size: 12px;
}
.btn {
  margin: 2px;
  font-size : 12px;
  border: 1px solid black;
  background : white;
  padding: 5px;
  cursor : pointer;
}
.btn:hover {
  background : #DDD;
}  

body {
  font-family: arial;
  font-siae: 12px;
}

canvas {
  border: 2px solid black;
  padding: 5px;
}
.hide {
  display: none;
}
&#13;
<div class="imageSel hide" id="imageControl">
<input class="btn" id="selImage1Btn" type="button" value="Image 1"></input>
<input class="btn" id="selImage2Btn" type="button" value="Image 2"></input>
<input class="btn" id="togFullsize" type="button" value="Toggle fullsize"></input>
<input class="btn" id="findMean1" type="button" value="Mean M1" data-method=0 data-name="perceptual mean approximation" title="Get the image mean to use as threshold value using perceptual mean approximation"></input>
<input class="btn" id="findMean2" type="button" value="Mean M2" data-method=1 data-name="Pixel RGB sum mean" title="Get threshold value using RGB sum mean"></input>
<input class="btn" id="findMean3" type="button" value="Mean Otsu" data-method=2 data-name="Otsu's method" title="Get threshold value using Otsu's method"></input>
<div id="imageStatus"></div>
</div>



<div class="imageIso">
  Using per pixel processing simple threshold. Quick in terms of pixel processing but produces a hard boundary at the threshold value.<br>
  <div id="canB"></div>
  Threshold<input id="inputThresholdB" type="range" min="1" max="255" step="1" value="128"></input><span id="inputThresholdValueB"></span>
</div>

<div class="imageIso">
  Using per pixel processing with cutoff width. This softens the cutoff boundary by gray scaling the values at the threshold.<br>
  <div id="canC"></div>
  Cutoff width<input id="inputCutoffC" type="range" min="0" max="64" step="0.1" value="8"></input><span id="inputCutoffValueC"></span><br> Threshold
  <input id="inputThresholdC" type="range" min="1" max="255" step="1" value="128"></input><span id="inputThresholdValueC"></span>
</div>

<div class="imageIso">
  <h2>Means not applied to this image</h2>
  Using stacked global composite operations. The quickest method and does not require secure pixel access. Though threshold and cutoff are imprecise.<br>
  <div id="canA"></div>
  Amount<input id="inputAmountA" type="range" min="1" max="100" step="1" value="75"></input><span id="inputAmountValueA"></span><br> Threshold
  <input id="inputThresholdA" type="range" min="1" max="255" step="1" value="127"></input><span id="inputThresholdValueA"></span>
</div>
&#13;
&#13;
&#13;

旧回答

使用2D API

的黑色和白色的最快颜色

最快的BW颜色方法如下

ctx.drawImage(image,0,0);
ctx.globalCompositeOperation = "saturation";
ctx.fillStyle = "#888";  // no saturation
ctx.fillRect(0,0,image.width,image.height);

并提供了良好的结果。

由于总是存在关于哪种方式是正确方式的争论,其余的答案和片段让您可以比较各种方法并看看您喜欢哪种方法。

BW转换比较。

许多人倾向于使用感知转换作为线性RGB-> BW或对数RGB-> BW并且将发誓。个人评价过高,需要经验性的检测。

技术上没有正确的方法,因为正确的转换方法取决于很多因素,整体图像亮度和对比度,观看环境的环境照明,个人喜好,媒体类型(显示,打印,其他),任何现有图像处理,原始图像源(jpg,png等),相机设置,作者意图,查看上下文(全屏,拇指,亮蓝色边框等)

该演示展示了一些转换方法,包括常见的感知线性和日志,有些方法使用直接像素处理,通过&#34; ctx.getImageData&#34;和其他人通过2D API使用GPU进行处理(快100倍)

启动图片将加载的片段,然后进行处理。完成所有版本后,它们将与原始版本一起显示。单击图像以查看使用的功能以及处理图像所需的时间。

图片来源:由Wiki提供服务。归因:公共领域。

&#13;
&#13;
const methods = {quickBW, quickPerceptualBW, PerceptualLinear, PerceptualLog, directLum, directLumLog}
const image = new Image;

status("Loading test image.");
setTimeout(start,0);

function status(text){
   const d = document.createElement("div");
   d.textContent = text;
   info.appendChild(d);
}

function makeImageEditable(image){
	const c = document.createElement("canvas");
	c.width = image.width;
	c.height = image.height;
	c.ctx = c.getContext("2d");
	c.ctx.drawImage(image,0,0);
	return c;
}
function makeImageSideBySide(image,image1){
	const c = document.createElement("canvas");
	c.width = image.width + image1.width;
	c.height = image.height;
	c.ctx = c.getContext("2d");
	c.ctx.drawImage(image,0,0);
	c.ctx.drawImage(image1,image.width,0);
	return c;
}
function text(ctx, text, y = ctx.canvas.height / 2){
	ctx.font= "32px arial";
	ctx.textAlign = "center";
	ctx.fillStyle = "black";
	ctx.globalCompositeOperation = "source-over";
	ctx.globalAlpha = 1;
	ctx.setTransform(1,0,0,1,0,0);
	ctx.fillText(text,ctx.canvas.width / 2, y+2);
  ctx.fillStyle = "white";
  ctx.fillText(text,ctx.canvas.width / 2, y);
}
	
function quickBW(bw){	

	bw.ctx.save();	
	bw.ctx.globalCompositeOperation = "saturation";
	bw.ctx.fillStyle = "#888";  // no saturation
	bw.ctx.fillRect(0,0,bw.width,bw.height);
	bw.ctx.restore();
	return bw;
}
function quickPerceptualBW(bw){	
	bw.ctx.save();	
	bw.ctx.globalCompositeOperation = "multiply";
	var col = "rgb(";
	col += ((255 * 0.2126 * 1.392) | 0) + ",";
	col += ((255 * 0.7152 * 1.392) | 0) + ",";
	col += ((255 * 0.0722 * 1.392) | 0) + ")";
  bw.ctx.fillStyle = col;
	bw.ctx.fillRect(0,0,bw.width,bw.height);
	bw.ctx.globalCompositeOperation = "saturation";
	bw.ctx.fillStyle = "#888";  // no saturation
	bw.ctx.fillRect(0,0,bw.width,bw.height);
	bw.ctx.globalCompositeOperation = "lighter";
  bw.ctx.globalAlpha = 0.5;
  bw.ctx.drawImage(bw,0,0);
	bw.ctx.restore();
	return bw;
}
function PerceptualLinear(bw){	
	const imgD = bw.ctx.getImageData(0,0,bw.width, bw.height);
	const d = imgD.data;
	var i = 0;
	var v;
	while(i < d.length){
		v = d[i++] * 0.2126 + d[i++] * 0.7152 + d[i] * 0.0722;
		i -= 2;
		d[i++] = d[i++] = d[i++] = v;
		i++;
	}
	bw.ctx.putImageData(imgD,0,0);
	return bw;
}
function PerceptualLog(bw){	
	const imgD = bw.ctx.getImageData(0,0,bw.width, bw.height);
	const d = imgD.data;
	var i = 0;
	var v;
	while(i < d.length){
		v = Math.sqrt(d[i] * d[i++] * 0.2126 + d[i] * d[i++] * 0.7152 + d[i] *d[i] * 0.0722);
		i -= 2;
		d[i++] = d[i++] = d[i++] = v;
		i++;
	}
	bw.ctx.putImageData(imgD,0,0);
	return bw;
}

function directLum(bw){	
	const imgD = bw.ctx.getImageData(0,0,bw.width, bw.height);
	const d = imgD.data;
	var i = 0;
	var r,g,b,v;
	while(i < d.length){
		r = d[i++];
		g = d[i++];
		b = d[i];
		v = (Math.min(r, g, b) + Math.max(r, g, b)) / 2.2;
        i -= 2;
		d[i++] = d[i++] = d[i++] = v;
		i++;
	}
	bw.ctx.putImageData(imgD,0,0);
	return bw;
}
function directLumLog(bw){	
	const imgD = bw.ctx.getImageData(0,0,bw.width, bw.height);
	const d = imgD.data;
	var i = 0;
	var r,g,b,v;
	while(i < d.length){
		r = d[i] * d[i++];
		g = d[i] * d[i++];
		b = d[i] * d[i];
		v = Math.pow((Math.min(r, g, b) + Math.max(r, g, b)/2) ,1/2.05);
    i -= 2;
		d[i++] = d[i++] = d[i++] = v;
		i++;
	}
	bw.ctx.putImageData(imgD,0,0);
	return bw;
}


function start(){
  image.crossOrigin = "Anonymous"
	image.src = "https://upload.wikimedia.org/wikipedia/en/2/24/Lenna.png";
  
  status("Image loaded, pre processing.");
	image.onload = ()=>setTimeout(process,0);
}	
function addImageToDOM(element,data){
  element.style.width = "512px"
  element.style.height = "256px"
	element.addEventListener("click",()=>{
		text(element.ctx,"Method : " + data.name + " Time : " + data.time.toFixed(3) + "ms",36);
	});
	document.body.appendChild(element);
}
function process(){
  const pKeys = Object.keys(methods);
	const images = pKeys.map(()=>makeImageEditable(image));
	const results = {};
  status("Convert to BW");
  setTimeout(()=>{
      pKeys.forEach((key,i)=>{
        const now = performance.now();
        methods[key](images[i]);
        results[key] = {};
        results[key].time = performance.now() - now;
        results[key].name = key;
      });
      pKeys.forEach((key,i)=>{
        addImageToDOM(makeImageSideBySide(images[i],image),results[key]);
      })
      status("Complete!");
      status("Click on image that you think best matches");
      status("The original luminance to see which method best suits your perception.");
      status("The function used and the time to process in ms 1/1000th sec");
  },1000);

}	
&#13;
canvas {border : 2px solid black;}
body {font-family : arial; font-size : 12px; }
&#13;
<div id="info"></div>
&#13;
&#13;
&#13;

答案 2 :(得分:2)

(我正在为将来的访问者添加此内容。)将图像转换为黑色&amp;白色,其中诸如亮度优势,伽玛等特征未知,“Otsu's method”倾向于提供良好的结果。

这是一个相当简单的算法,它使用图像的亮度直方图和像素计数来找到最佳的基于簇的阈值。

主要步骤是(来源:同上):

steps

建立直方图

所以我们需要做的第一件事是建立一个直方图。这将需要使用平坦的33.3%因子将RGB转换为亮度,或者如在Khauri的回答中将Rec.709(用于HD)公式转换为亮度(也可以使用Rec.601)。注意,Rec。*因子假定RGB被转换为线性格式;现代浏览器通常将伽玛(非线性)应用于用于画布的图像。但我们在这里忽略它。

平面转换在性能方面可能是有益的,但提供的结果不太准确:

var luma = Math.round((r + g + b) * 0.3333);

虽然Rec.709会给出更好的结果(使用线性数据):

var luma = Math.round(r * 0.2126 + g * 0.7152 + b * 0.0722);

因此,将每个像素转换为整数亮度值,将结果值用作256个大数组中的索引并为索引递增:

var data = ctx.getImageData(0, 0, width, height).data;
var histogram = new Uint16Array(256); // assuming smaller images here, ow: Uint32

// build the histogram using Rec. 709 for luma
for(var i = 0; i < data.length; i++) {
  var luma = Math.round(data[i++] * 0.2126 + data[i++] * 0.7152 + data[i++] * 0.0722);
  histogram[luma]++;   // increment for this luma value
}

查找基于群集的最佳阈值

现在我们有了一个直方图,我们可以将其提供给Oto的方法并获得黑色&amp;白色版的图像。

转换为JavaScript我们会做(来自ibid的基于variant 2的方法部分的来源):

// Otsu's method, from: https://en.wikipedia.org/wiki/Otsu%27s_Method#Variant_2
//
// The input argument pixelsNumber is the number of pixels in the given image. The 
// input argument histogram is a 256-element histogram of a grayscale image 
// different gray-levels.
// This function outputs the threshold for the image.
function otsu(histogram, pixelsNumber) {
  var sum = 0, sumB = 0, wB = 0, wF = 0, mB, mF, max = 0, between, threshold = 0;
  for (var i = 0; i < 256; i++) {
    wB += histogram[i];
    if (wB === 0) continue;
    wF = pixelsNumber - wB;
    if (wF === 0) break;
    sumB += i * histogram[i];
    mB = sumB / wB;
    mF = (sum - sumB) / wF;
    between = wB * wF * Math.pow(mB - mF, 2);
    if (between > max) {
      max = between;
      threshold = i;
    }
  }
  return threshold>>1;
}

// Build luma histogram
var c = document.createElement("canvas"),
    ctx = c.getContext("2d"),
    img = new Image();
img.crossOrigin = "";
img.onload = go;
img.src = "//i.imgur.com/tbRxrWA.jpg";

function go() {
  c.width = this.width;
  c.height = this.height;
  ctx.drawImage(this, 0, 0);
  var idata = ctx.getImageData(0, 0, c.width, c.height);
  var data = idata.data;
  var histogram = new Uint16Array(256);
  
  // build the histogram using flat factors for RGB
  for(var i = 0; i < data.length; i += 4) {
    // note: here we also store luma to red-channel for reuse later.
    var luma = data[i] = Math.round(data[i]*.2126+data[i+1]*.7152+data[i+2]*.0722);
    histogram[luma]++;
  }
  
  // Get threshold
  var threshold = otsu(histogram, c.width * c.height);
  console.log("Threshold:", threshold);
  
  // convert image
  for(i = 0; i < data.length; i += 4) {
    // remember we stored luma to red channel.. or use a separate array for luma values
    data[i] = data[i+1] = data[i+2] = data[i] >= threshold ? 255 : 0;
  }
  
  // show result
  ctx.putImageData(idata, 0, 0);
  document.body.appendChild(c);     // b&w version
  document.body.appendChild(this);  // original image below
}

另见improvements section