【问题标题】:Best RGB combination to convert image into Black and White "threshold"将图像转换为黑白“阈值”的最佳 RGB 组合
【发布时间】:2017-07-17 19:38:29
【问题描述】:

我需要构建一个简单的应用程序,将彩色图像或灰度图像转换为黑白图像,我正在考虑循环遍历每个像素并检查 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);
  }

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

当然,如果有更快的方法可以做到这一点,我将不胜感激。

【问题讨论】:

  • 您只想要打开和关闭,而不是灰度。对吗?
  • 天真地说,我会说 128,但更详细的答案是对单个图像的直方图进行初步分析,以确定合适的阈值。
  • 是的,实际上灰度在这个意义上不同于黑白
  • 是的,完全不知道这个词,谢谢

标签: javascript canvas


【解决方案1】:

我相信您正在寻找的是相对亮度。虽然不是最先进的阈值方法,但它更好地遵循人类感知光的方式,这正是我认为你想要的。

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

根据维基百科的文章,亮度可以计算如下:

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

这个值是 1 的一小部分,所以如果你想在中间分割它,使用 0.5 的阈值

编辑

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

【讨论】:

  • 谢谢,这真的很有帮助,如果你知道如何分析直方图,那就太好了,但到目前为止,这已经足够了。
  • 创建这样的直方图实际上并不难。查看本教程,了解如何创建直方图。 billmill.org/the_histogram.html 获得直方图数组后,您可以使用平衡直方图阈值算法对其进行分析。该算法将返回一个阈值供您在分析中使用。 en.wikipedia.org/wiki/Balanced_histogram_thresholding
  • 还应该提到你应该检查你得到的值的范围。如果您的阈值算法等使用介于 0 和 1 之间的值,请保持一致。祝您好运。
  • 抱歉,您的计算不正确。使用通道 R、G、B 为相对亮度lum = Math.sqrt(R * R * 0.2126 + G * G * 0.7152 + B * B * 0.0722) 正确的 Lum 值虽然这仍然不能为您提供正确的值并且不考虑媒体类型(例如监视器类型、打印格式、环境)
  • @Blindman67 que??这看起来像一个游戏编码器的公式 :) 但是,它不会产生正确的结果。要线性化颜色数据,您将使用具有归一化 RGB(或灰度)值的反伽马,即。您首先需要知道使用的伽玛(通常作为固定值存储在文件中,或存储在嵌入式 ICC 配置文件中)-除非可以接受近似值,否则没有通用方法(无论如何,这是对非线性数据所做的) )。我的 2 美分。
【解决方案2】:

(我正在为未来的访问者添加此内容。)对于将图像转换为黑白,其中亮度优势、伽马等特性未知,“Otsu's method”往往会提供良好的结果。

这是一种相当简单的算法,它使用图像的亮度直方图与像素计数相结合来找到基于聚类的最佳阈值。

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

构建直方图

所以我们需要做的第一件事就是建立一个直方图。这将需要使用平坦的 33.3% 因子或在 Khauri 的回答中使用 Rec.709(用于 HD)公式(也可以使用 Rec.601)将 RGB 转换为亮度。请注意,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 的方法并获得图像的黑白版本。

翻译成我们会做的 JavaScript(方法部分的源代码来自同上的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。

【讨论】:

    【解决方案3】:

    更新

    我没有正确阅读问题,因此我更新了答案以反映问题。将留下旧答案作为那些感兴趣的人的兴趣点。

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

    // 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);
    }
    

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

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

    演示

    更新

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

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

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

    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);
    
    }
    .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;
    }
    <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>

    旧答案

    使用 2D API 最快的颜色到黑白

    颜色转BW最快的方法如下

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

    并提供了良好的结果。

    由于总是存在关于哪种方法是正确方法的争论,其余的答案和 sn-ps 让您比较各种方法,看看您更喜欢哪种方法。

    BW 转化比较。

    许多人倾向于将感知转换用作线性 RGB->BW 或对数 RGB->BW,并且对此深信不疑。个人认为它被高估了,需要经验的眼睛来检测。

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

    演示展示了一些转换方法,包括常见的感知线性和对数,一些方法通过“ctx.getImageData”使用直接像素处理,其他方法通过 2D API 使用 GPU 进行处理(快 100 倍)

    启动sn -p 图像将被加载然后被处理。所有版本完成后,它们将与原始版本一起显示。点击图片查看使用了什么函数以及处理图片所花费的时间。

    图片来源:由 Wiki 提供。署名:公共领域。

    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);
    
    }	
    canvas {border : 2px solid black;}
    body {font-family : arial; font-size : 12px; }
    &lt;div id="info"&gt;&lt;/div&gt;

    【讨论】:

    • 这是很好的信息。我认为 OP 正在寻找阈值效应而不是灰度。我相信他的意图是找到一种最快的方法来使像素为全黑或全白的图像二值化。
    • @KhauriMcClain 哈哈,是的,你是对的。我应该阅读问题而不是问题标题和答案。哦,好吧,没有伤害。
    • @Blindman67 嗨。所以从这个意义上说,没有建议对颜色进行加权(ej:0.7152 R .. 等等)进行阈值处理?顺便说一句,这很棒
    • @OscarMuñoz 对于阈值没有标准。
    猜你喜欢
    • 2023-03-14
    • 1970-01-01
    • 2013-03-17
    • 2020-01-22
    • 1970-01-01
    • 2017-12-16
    • 2013-09-17
    • 2010-12-07
    相关资源
    最近更新 更多