Html HTML5 Canvas 图像对比

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/10521978/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-29 00:19:23  来源:igfitidea点击:

HTML5 Canvas image contrast

htmlimage-processinghtml5-canvascontrast

提问by Schahriar SaffarShargh

I've been writing an image processing program which applies effects through HTML5 canvas pixel processing. I've achieved Thresholding, Vintaging, and ColorGradient pixel manipulations but unbelievably I cannot change the contrast of the image! I've tried multiple solutions but I always get too much brightness in the picture and less of a contrast effect and I'm not planning to use any Javascript libraries since I'm trying to achieve these effects natively.

我一直在编写一个图像处理程序,它通过 HTML5 画布像素处理应用效果。我已经实现了阈值、Vintaging 和 ColorGradient 像素操作,但令人难以置信的是我无法改变图像的对比度!我已经尝试了多种解决方案,但我总是在图片中获得太多亮度而对比度效果却很少,而且我不打算使用任何 Javascript 库,因为我试图以本机方式实现这些效果。

The basic pixel manipulation code:

基本的像素操作代码:

var data = imageData.data;
for (var i = 0; i < data.length; i += 4) {
 //Note: data[i], data[i+1], data[i+2] represent RGB respectively
data[i] = data[i];
data[i+1] = data[i+1];
data[i+2] = data[i+2];
}


Pixel manipulation example

像素操作示例

Values are in RGB mode which means data[i] is the Red color. So if data[i] = data[i] * 2; the brightness will be increased to twice for the Red channel of that pixel. Example:

值处于 RGB 模式,这意味着 data[i] 是红色。所以如果 data[i] = data[i] * 2; 该像素的红色通道的亮度将增加到两倍。例子:

var data = imageData.data;
for (var i = 0; i < data.length; i += 4) {
 //Note: data[i], data[i+1], data[i+2] represent RGB respectively
 //Increases brightness of RGB channel by 2
data[i] = data[i]*2;
data[i+1] = data[i+1]*2;
data[i+2] = data[i+2]*2;
}

*Note: I'm not asking you guys to complete the code! That would just be a favor! I'm asking for an algorithm (even Pseudo code) that shows how Contrast in pixel manipulation is possible!I would be glad if someone can provide a good algorithm for Image Contrast in HTML5 canvas.

*注意:我不是要求你们完成代码!那只会是一种恩惠!我正在要求一种算法(甚至是伪代码)来显示像素操作中的对比度是如何可能的!如果有人可以为 HTML5 画布中的图像对比度提供一个好的算法,我会很高兴。

采纳答案by brichins

A faster option (based on Escher's approach) is:

更快的选择(基于埃舍尔的方法)是:

function contrastImage(imgData, contrast){  //input range [-100..100]
    var d = imgData.data;
    contrast = (contrast/100) + 1;  //convert to decimal & shift range: [0..2]
    var intercept = 128 * (1 - contrast);
    for(var i=0;i<d.length;i+=4){   //r,g,b,a
        d[i] = d[i]*contrast + intercept;
        d[i+1] = d[i+1]*contrast + intercept;
        d[i+2] = d[i+2]*contrast + intercept;
    }
    return imgData;
}

Derivation similar to the below; this version is mathematically the same, but runs much faster.

类似下面的推导;这个版本在数学上是相同的,但运行速度要快得多。



Original answer

原答案

Here is a simplified version with explanationof an approach already discussed(which was based on this article):

这是一个简化版本,解释已经讨论过的方法(基于本文):

function contrastImage(imageData, contrast) {  // contrast as an integer percent  
    var data = imageData.data;  // original array modified, but canvas not updated
    contrast *= 2.55; // or *= 255 / 100; scale integer percent to full range
    var factor = (255 + contrast) / (255.01 - contrast);  //add .1 to avoid /0 error

    for(var i=0;i<data.length;i+=4)  //pixel values in 4-byte blocks (r,g,b,a)
    {
        data[i] = factor * (data[i] - 128) + 128;     //r value
        data[i+1] = factor * (data[i+1] - 128) + 128; //g value
        data[i+2] = factor * (data[i+2] - 128) + 128; //b value

    }
    return imageData;  //optional (e.g. for filter function chaining)
}

Notes

笔记

  1. I have chosen to use a contrastrange of +/- 100instead of the original +/- 255. A percentage value seems more intuitive for users, or programmers who don't understand the underlying concepts. Also, my usage is always tied to UI controls; a range from -100% to +100% allows me to label and bind the control value directly instead of adjusting or explaining it.

  2. This algorithm doesn't include range checking, even though the calculated values can far exceed the allowable range- this is because the array underlying the ImageData object is a Uint8ClampedArray. As MSDN explains, with a Uint8ClampedArraythe range checking is handled for you:

  1. 我选择使用一个contrast范围+/- 100而不是原始+/- 255. 对于用户或不了解基本概念的程序员来说,百分比值似乎更直观。此外,我的使用总是与 UI 控件相关;从 -100% 到 +100% 的范围允许我直接标记和绑定控制值,而不是调整或解释它。

  2. 该算法不包括范围检查,即使计算出的值可能远远超出允许范围- 这是因为 ImageData 对象基础的数组是Uint8ClampedArray. 正如 MSDN 解释的那样Uint8ClampedArray为您处理范围检查:

"if you specified a value that is out of the range of [0,255], 0 or 255 will be set instead."

“如果您指定的值超出 [0,255] 的范围,则将改为设置 0 或 255。”

Usage

用法

Note that while the underlying formula is fairly symmetric (allows round-tripping), data is lost at high levels of filtering because pixels only allow integer values. For example, by the time you de-saturate an image to extreme levels (>95% or so), all the pixels are basically a uniform medium gray (within a few digits of the average possible value of 128). Turning the contrast back up again results in a flattened color range.

请注意,虽然底层公式相当对称(允许往返),但数据在高级别过滤时会丢失,因为像素只允许整数值。例如,当您将图像去饱和度到极端水平(> 95% 左右)时,所有像素基本上都是均匀的中等灰度(在 128 的平均可能值的几位以内)。再次调高对比度会导致颜色范围变平。

Also, order of operations is important when applying multiple contrast adjustments - saturated values "blow out" (exceed the clamped max value of 255) quickly, meaning highly saturating and then de-saturating will result in a darker image overall. De-saturating and then saturating however doesn't have as much data loss, because the highlight and shadow values get muted, instead of clipped (see explanation below).

此外,在应用多个对比度调整时,操作顺序很重要 - 饱和值快速“溢出”(超过钳位最大值 255),这意味着高度饱和然后去饱和会导致整体图像更暗。然而,去饱和然后饱和不会有太多的数据丢失,因为高光和阴影值被静音,而不是被剪裁(见下面的解释)。

Generally speaking, when applying multiple filters it's better to start each operation with the original data and re-apply each adjustment in turn, rather than trying to reverse a previous change - at least for image quality. Performance speed or other demands may dictate differently for each situation.

一般来说,当应用多个过滤器时,最好以原始数据开始每个操作,然后依次重新应用每个调整,而不是试图逆转之前的更改——至少对于图像质量而言。性能速度或其他要求可能因每种情况而异。

Mandrill contrast examples

山魈对比示例

Code Example:

代码示例:

function contrastImage(imageData, contrast) {  // contrast input as percent; range [-1..1]
    var data = imageData.data;  // Note: original dataset modified directly!
    contrast *= 255;
    var factor = (contrast + 255) / (255.01 - contrast);  //add .1 to avoid /0 error.

    for(var i=0;i<data.length;i+=4)
    {
        data[i] = factor * (data[i] - 128) + 128;
        data[i+1] = factor * (data[i+1] - 128) + 128;
        data[i+2] = factor * (data[i+2] - 128) + 128;
    }
    return imageData;  //optional (e.g. for filter function chaining)
}

$(document).ready(function(){
  var ctxOrigMinus100 = document.getElementById('canvOrigMinus100').getContext("2d");
  var ctxOrigMinus50 = document.getElementById('canvOrigMinus50').getContext("2d");
  var ctxOrig = document.getElementById('canvOrig').getContext("2d");
  var ctxOrigPlus50 = document.getElementById('canvOrigPlus50').getContext("2d");
  var ctxOrigPlus100 = document.getElementById('canvOrigPlus100').getContext("2d");
  
  var ctxRoundMinus90 = document.getElementById('canvRoundMinus90').getContext("2d");
  var ctxRoundMinus50 = document.getElementById('canvRoundMinus50').getContext("2d");
  var ctxRound0 = document.getElementById('canvRound0').getContext("2d");
  var ctxRoundPlus50 = document.getElementById('canvRoundPlus50').getContext("2d");
  var ctxRoundPlus90 = document.getElementById('canvRoundPlus90').getContext("2d");
  
  
  var img = new Image();
  img.onload = function() {
    //draw orig
    ctxOrig.drawImage(img, 0, 0, img.width, img.height, 0, 0, 100, 100); //100 = canvas width, height
    
    //reduce contrast
    var origBits = ctxOrig.getImageData(0, 0, 100, 100);
    contrastImage(origBits, -.98);
    ctxOrigMinus100.putImageData(origBits, 0, 0);
    
    var origBits = ctxOrig.getImageData(0, 0, 100, 100);
    contrastImage(origBits, -.5);
    ctxOrigMinus50.putImageData(origBits, 0, 0);
    
    // add contrast
    var origBits = ctxOrig.getImageData(0, 0, 100, 100);
    contrastImage(origBits, .5);
    ctxOrigPlus50.putImageData(origBits, 0, 0);
    
    var origBits = ctxOrig.getImageData(0, 0, 100, 100);
    contrastImage(origBits, .98);
    ctxOrigPlus100.putImageData(origBits, 0, 0);
    
    
    //round-trip, de-saturate first
    origBits = ctxOrig.getImageData(0, 0, 100, 100);
    contrastImage(origBits, -.98);
    contrastImage(origBits, .98);
    ctxRoundMinus90.putImageData(origBits, 0, 0);
    
    origBits = ctxOrig.getImageData(0, 0, 100, 100);
    contrastImage(origBits, -.5);
    contrastImage(origBits, .5);
    ctxRoundMinus50.putImageData(origBits, 0, 0);
    
    //do nothing 100 times
    origBits = ctxOrig.getImageData(0, 0, 100, 100);
    for(i=0;i<100;i++){
      contrastImage(origBits, 0);
    }
    ctxRound0.putImageData(origBits, 0, 0);
    
    //round-trip, saturate first
    origBits = ctxOrig.getImageData(0, 0, 100, 100);
    contrastImage(origBits, .5);
    contrastImage(origBits, -.5);
    ctxRoundPlus50.putImageData(origBits, 0, 0);
    
    origBits = ctxOrig.getImageData(0, 0, 100, 100);
    contrastImage(origBits, .98);
    contrastImage(origBits, -.98);
    ctxRoundPlus90.putImageData(origBits, 0, 0);
  };
  
  img.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAMAAABHPGVmAAADAFBMVEX0RydFRjuPweRoak+awuFzbknzUj9SV0T0RRVtb1lcak7zUTSUmX94hnOYoHFacmpCPS9zwvOBkG5Zbl7xVCCVxuhnfnV7fFOnoqU3OTFQZVNNXlCmxeBtfWlhaFlbXUmfyOVrh3/vXUiBfUcrKCF9wu50fXB4fGVlcGJIUEvtSDCUvd2Up4uJnYBUX0A7QzjtW1tYZUlqY0U1Miivy+BfdXJ/hWtuc2eNjWSCkWFbYVVhcU6Pl2tsf13xVUxyueeir4N+lICIkXdIUjuxr7JyfH1iemiHhFT5Rx+7zN2Xn2KCf11jeFl1jYhSZ11OTkGJxe6Kt9ybrp6fqZBkfoZ/in53jntBTUN4h2FPTDFFRy+GwOeju9iVp35dWj1uh25gZWqNmF9JWUX3SjOvxNtyjZlYeJdqrNeMn3F2dFmNj1KWkktFV1W7vbd/l4ydkIVOXWDgVEjwWzlqb3WXq6/Bp6OHlJuWn5aKmotSZ2t/jVWRgFKmkU92eUF+rdaKrNCnsqVnh5BqeEyHh0POmzxiZDuotbqkrW+lnmvEo1BafqZ3gYnmZWKonFyZjlvvZ1Xev0hlu+5aksKyr5RLaI+YkmrXZmqYnFa3l0tXVDZpXzGFoqWlkZa3hYHrbmbXuFGLo5Z4hU3UqkdhbUBvbThxoM29u6SVoaNVbXziXlRVVVRZndPNsl1/uOHCsbB5mJWSjZE9PyWWs9egpX+st36lm3eysnJ6bTdxkb5gcYfSWlMfGhxpwPdloLp8mqWCi4++s344SVKih0aYfT6JcTy/TzwsLzZAMR+drM2/kJGrnpBafntHYnk1PUNfVC3SUCpfr+XTc4OGeXnjaHSlqV+sgzm9mpviSTlRg7Rrh7HmdXbCdXDSeW5AVWy2pWHZa1ynt5Hge5DNiouMgIbGtGq6w86+ZFmkV06+jzxOQyXWiKKzcGeFosRgkajdrzqddHOEcFN6SCTRig2dRCbjRiK1ahXMnLV6VzuEXFyeaiy8voxgPh/CfBK6lK6ciHeFa2q3ecQCAAAjI0lEQVRo3iRVf2gbZRjOGUroaO5iTq8mx6WGxDTBpGGNl+baFJOGpXhtLs4mzY+RxjvMTJNQAw5tTuJsjnCiKAiTsNSBoVFGRWpsyJBuheps/oiNIhEEHQQFR9Hi2lFq14J+6sMH33HHve/7vD+eV9KO7Jav6nTVfL6cFuhiZYU/UzmTNCvNbkIbJ1B0HJ12E74FrRdDFxgkgfgwBlkKrG36tFoEO+dbY9ZaGGJktJtrmUu+hdmg3Y0Nmp2LdaVIizCvFFPF4YLkpFy9WtTpdOl0viRUK2yFpyrOsEW5uIChLVxrIUaMC1oMR7ksgjMkjobi4MIQn5pEsr6cOoRkGBJpBRKbl5Z9pGFBHjQHzcqusuFQsmFRGXZ0+GJeslu+kS+lBSFP06UqTYtjcYof01hmUR/mXcPOzA6ZvCTJQFaGUfs9fivHWT1qJhDKJsjcEo4wiDZHtgKB5cRy4pI2Lp8YIeTmcaUTnocbIlxX9vMiL6xI2rvtok6odPg0neJTYpVyOQthftZ4aTmhZRBjKIubEMiPk4jMpIayIavJ6gkxrcCty0ubawGEJAPLm8trr1z6Y2lzOeHjxs5qzEbY7l6P1XVUfV0s6XQFnpLsttt5oZRKpdOlEptiKZ6OVobOElo1ksNa5zASRWUQTiYCATK7xLSWSDLRWtq8fP7y8iVfC/rCt7m5mUgktCSJYAEUw0zucY1RA/dSsDNJTcVYlk8W8nlJu3yjmq/k2U4lla+wPM+OrRSkQ2rvKIliWhLL4iiSg0xMNrAUWP5jOcEgy3ubfyQubyImtTVE5pAsYwqZOEaN+9WQWnY1MkBopNL+nv6+usslOhsuuLAyB5xUK4U0O9kQOwIrUvmVWmT67JgfHYI4DpJZTR61TKaIKqDQKqkFfhJqda6VWMJQT8TjUaNciOM4k9/jiQIoIDUyKpMW5vqdFCvWO7Qz6ehJ83MUKHw1nU5R5XQJpKo0VChIr8qGIpGoAhx/TVFTNJvgKdpUQNcDJB7KqlW5AOOJAJsej4eDojarAoIUCigalXlqNQXkV1w1jivtDidP0XXluK5U5PslN/JzVSGv69AlgRallamiTCPzN8FvEUXTFm2qFDab4n/4FX5U4YnW/jtNGyCnCkUi5Rv7J7vWmoKDIBXgA/mNoL/6Vkou18T6A5eSoni2KImAOUkL777zDl2ZpHlW2a8ag0wyK/RvfCrV9m7z5KTZtAGoVAoVsKzGEc4D2WzDGky9bTw8PT48OP0BtelRz79sOJnf2CuPycMs7FDW63SHH6AKZUk1JayUUg6xmE/RIp3v6ZH6QUw4JAtZVfrt6uHp4f7h4fGOCYP0tppN4RnycB6PCpqfHx9SbcMHB8Pzxwenyh0NET+LAxCEN2iZmFY67esPlFLRORhzshWJcEMQ0pNsp0E3XC44SQ0MQP5VyGZVezirbefLw9PmHPBCbuPjMj1w8y+iNWi4J+y16vXJ04Pjk8PTU/mOafHDOMOgCBlfDbonYkkYnqAqLFunpAVpv6QslIr5fl2p02nAohPuGzSARuc8HEoYxzzbwunB4f3Dwx9MG3pjb6Rpa9aioOa1SFkaj25sbOA/HACcfpXTjy6a3STqzRmC8t6ZCSrspGjKOQ7TKTa/kpcIc8JwOiUUHWyn46Rpe2wwGMRQNOddtKj1evzwPyv3bBsKPwE19fpapNau1dq9YS4yHNHrZcfg62ROFcJmPoyToyjmm0XNFioZg9c7rJi0jHlA5XnJbaGtK5fSOoF2pkSXE7bPzwQXnkdaS2uzGpCQ0D7gsq/XN3cjmjNNcM8Vd6PtI/k095pOZbOBF03r9g7iXTcbRnCvL240PBeG+5LJcLI7E0vyNGx3FouS2zeAcqXSAtxolHSlyZTDbggaZjEExGQfUls39Bv/wX9WymmN+vbubi1yNHyVwCHOmLGGrDs7W6YtbTz4qtsoY2QGr1zrtiyau65uzNJ9QFN0QckDWbnd1glCzzudfLpRSoNKwQ4i2Of1erVo3DKNqq2ZzNZWJpNdWHh11qRoHp3st4+O7o14VEME2AAtJsNkEa07+GoQz6CjBEaMeONds8VZX5+BKTEZc5b6K5WKZHclBeQxVS9R9QbLlh7ABTgYXJz2xlHES/R64xyOqhnjyz65mtNbd3ua+6/t3/7Tx3hVGu3CWhDDEHnw1ZlFjUmLoASGy71yi91icXVnZrousyuZhGkHOySp6srptK6fFTssC3YZLFLzU4Q9KCfPkV4ZITXOEoOG4MjbWEBrQvRz905O/j4+/uvtpVyLw02ake9f+uyDx37tlWUx4EOuWZCfkxOxriVMwV1XjIVFsFAc/LRkF+wsXXVSpEG3daZ6qKTTPTGxOGLADFovbuUG7d/b7YOG0dVVBjGFRu7t37129/j26nIrlPnm4t7e3lsXv1i1qTItBGxLwjholGN9MaWSguszgEa9HoMtlKMgKbfTQk9HLDlZGoZddZ5PJgeDSoPh3PMEpsVDJqvfD5nuvHVxa1uVMRrunhxd++349wEDY3vrxfce/hffvfjW9RyDY5rxEa+bMLrNix++YVHOzFjqrgegv4Cyj0mK5XQqVUhVikJJl4cbIF8wOzY68rKBxI0hyLpz58qPP/64d+v8hT391tsv3W0eXTs+/P2vkNW298Sjjz7+6IXXby0FrmczS1oGnT6jwdxueczi+viN51wPzN1YLOkc7uFXwMQPT+VT9UYq1Uk56Tcd/evvw30yVGs0DHAmq/XOj9/dvAniffrTz/2m76/dP9n/4P63v/0C1RQfPf7UU0+evwWwd+VOxpRFNMSZabtl0R2LKWOuriVW78bqSWk4yVdA4XU6YbIkpNIdIPdAWeZ7+/ocBK5ZxVHT1pUrVy6++PATT1x49sIfR9Cfv9zfP7r2yLe/XbtXG3jomRfOn//0iYdv3rz54sVcNpdF1ZowEbbLqeQsDMOWJOARp8CknKlI2lVhuFzV0ZPJyR66KDbenbI7pwbtfpnMqIbufPLdd7euf3ThUYDH7kf+/Pnu/t1fHvn2659+L597/KnLTz70LHDy3i2MvP6NNYuMjstGCWPc7IorxfrMG10XHYu5YlSY/4cG841po47D+HG9E1oo0KPHOVbWa2vHtrYoZ1cL7dk/i3S1Zf2zpV5XuIaFWWCEhMboFKlQgy4GR9oMlk4lJQUdNg2Rzb+DJSaiKGJZZnQhLGwmakIk0YzgK43fW+K96Mv73PM83+d79yuC46Oui4Ov4+fe7r08aLVaKY/ZIzZ5hFX/3Jd96VQqFdsJ0BhJP7r819b48l/LG+W7xcmtv2+SOh1G+v2y2NmlpfX19NWrtfXixlKrydDtdEAadkfHdb1B393MNncjGYZ3Ua5zLnPvZcp1+RVPz2WzK5xhlC0nTlzKL72VWpRpCwnMSJPvFn/f3HrwYPmWXL67tfngJorRNAiMLcVsizLb0tWjp47WeYlDIUeUMHTb9d2Q/3VYXt3NXi8y8+mVDDOjqex89qJG1FvpabD2DjElT0mPHt1/J2Vbyi9qI5GAzmikfyoub8SLD36fjMvlc1vLAoSk6VjMlloEx+ATbETVFo1WtYaCQQPBGfT2jo57do6zdk9bEJwynxvVUJ9qBimP/ZNnPWaKp0zPSFtb9q+lF22FdMoWQTFdl5v+tlicOrJbPLIhL5Orx7/4AKVpiOVh8us7SzurL58dGZk/+05WyhkAc/LesN5ZYXeYfuxM5qAnYaV5CKfODc1Qva4fzQ2mSmmFVCo90NeXltkiibxWgLS7jd8Vt6bkxeLAuFwuV0/99iKKKYx0IKKVyVLppYJtafXl2v6q0Hy2/55er7effB9+r3cnX6ns5HIILqKGZnqtVPOFyxrrhbc9BGeyO021qoO1H76VAqtighIM/HqvOK6WF3fVW3GAjM29i2JGI4ZC8rLYrE0rW9859WLbTdX5tnmD3iC12+2Gjk5rs9XZ/EoyicyE8SszzJDGpTn36Qx18VXKYyXqDzrbDr38/FvrAAjEIgUU1SmM78nH1UeK8qkxgMSn5l5AhTLCeGltMWAA5GwbfA9ns47z54eHO5xWO1FpfbaZo5JEM8KLfEMMbh7kD0wM4i5PRUVDg/PaU4YDVZ/nYX5taCARoVEUbQfIlHp3V65Wg13lU3OvkUa3gqQFSAHUpPKnTp09O/9C9p0XgnrpyWBQX2m3VnSw3L3u6QzC4zyvwX24hhLjvS6R024iOnvYp1oO311Pr8siKJooYKggZTM+DhrK1YIS+dgXr9FuN0aDkkIBLIP52jlV2zbyzgc354PDQb1Tn9UbTuo7DHpv0jKNMJQGd/X09EAclMt87U1zM+Gsb2g5/OH30DAIhQwEMDSC6bDN+BPqsrL4FKAglIE/jMCg/SRaiAi1X4/FTq0ejar6g8HgS8Gs81qFw+AkOIKT5KZzCONjcCXP2M3UBGPydJorLrga8JaWQ4cvpbRpmRakBDAsgpLYpno8XrZbPr4Rl5eVqQdWIHYSihIJkAIkJkuNnIBTU1twPgtKCOt1R0OHk7Jy08dyAHFp+DBuxl24+NqFH03EhFnMtByUStbuLPohUZgvnU5BCpB4Ody9XD2uhqLEAaIQ2kgGUFKIRICsqhwhRygU0g9DFfVtQU5KcFxzstGCMHwpw2RwivLgLleltcHTA8mblL676UWtLQ9SSB2GPYSo1cehhrc3NgTIFytGYChoVIcJkHWbLLW0Cm2cVwHkZLb/ZIfD0MGBXdO5XCMiEvdQVCbDizW4xkU5r33SzZkqpJJ9dwFQeAiB0GkSxTbHAYKoNzaENsYnFxRQeQUWCJACJA9idvYfa82qVPP9Hef1Uj2UxaCXshyb81oQimGmcT4XHsKZixqJuaPC02B2Ur61S/nFRW3aFoE1D02E8doafwTcmtu+D6GAXe0KNIIaaR1Kk0JVtLLUal1Nq0qV7R/O9hv0wX4xUcmKuonmZNKC8JnqMP5DdRLHJ6hpymQ2TVhNImX1vnQ+pfUnYloSo4XNhbZvPR0vK0OeuH97ShivuXYIQwGvRlroYywC784RgLTWOvrrQw6DQRoNmaTRplwuyULjv2KSOMPnNAwvFlOueqmzouZQiUj5Jbyu/GRi1ibslHaArGyMlwNkbPv+2P8QzK3QoaQgZDbil9mqztTU1YWC8yrQ4Qi1OQzRqJezNLGsBRkc9IhEvDKsuXLY19MpxnsaKpW8WLmWT+W1fvSzAqrAsPZfjZGVsUegJkfG9rbVADk+0F6IKABCCpBYQutPxarOeOv62vqz2RBhCIWi99gDLOGVsBZLM/KvxzN4hQ9Xh8OEiBl9vfrDHoopaRGt5W0wXuhsDDWC8W6A/DL1CEBu7+2NwSzLAYIa3e0YDXbZZlHIfqmu7oTwR0mtNHveYSAIluCiFouXtUjqEaX4oEe5T3Slel9O5DtWKlaWKJWMr+TrtA22ij8xW1AABEJZGZsqFyDbe5CJAAnAl4QONjHpnw3QfmG4Hj/R6gxFVQSUJVoT5VggsF5vNMoifzItjJIrFZX68NFcY2mFSAknUabk6zuyfD7lRxMJhRFCAYhaXXYcOTJ2f+82QJDJdhQDiMJtpAsAIbW21b7n9u9nHdBGCJ4T7t+UlEjqJWy0FRnsMTOj35zO8NUlFvx0LgyeZapL9j12aTEPfpGJhO4hBH3jiUfKQMmtbbBLXo7MtaMK9wLmdrvpRIIkIwA5U7dfUuflvBIvwcLzWxq5psbG1qi3SYKIGkwNYj7D88pROKg9WVriOx328V/fuLtYuLNuI9GFQFeXTicoOQ5KIJPtsXIBsoAqumATGxWBBLil9ceunulTRSVNLEd0c1D1+lyp1wJXY2NjKfInZZ7o8TAininhw5mPfGLf6Wo4ud9Yk2ln87IIVFrRpQi4FSsDIAQZ2P5ne7IcQZBJgLTDq8xoTLymI7VaWX71cFVNTU2rlwWnWJMwuvWSRom3qUlSily4OPHxhJkwi0XKTObYR77SJ6vDvup9N26sywppGdwpAW7puowAgevnW//s7amPy8tuBbCuBUyhcOv+WKBJmXZ9qepMXU1TjbeVhadvkkgsJgvrrav3llqaGv/jwHpik4bDaJk4WDk4g3/SUsemXTIgaa1wUIgSU8VVCbMXm40GlTQjUQ5unoxIEyNxkIEZQcI8eFBJRDYTe5Aa9DC2EEyGkRDnFh0HMxJnOHhYdlvih03ogcvr+977Xvt7SCSVejpz+tLpE5cuHX31hQ28658wa7ogmY/+hBtAZJvNxoucmO2CGBq77bfPD+mRX7wNvEVwQIQY7IZXyCcIuGfIi1JnUOaoy8W8pLxeisKZII4j8VYkOZP6e/nK0dj+WGye7u+3a3QMTZZX5/xq3i9pFR5iUOa0P0dhXL2N3d1d8LJ1jScABF5cHcUxCMEV+iQEcd+RI0eGPM6zLhas6/KcBUKUiWFZJNlqzdyKQOtxO3X7wTjUG+x4v52GY+1m0W1LLxISKE9o4fd5dBRBphp7f/48mUamLvgdEvzJARHuMMjufo0JQdrk8RivH4P1Aym8Hg8KowpSLIwrksslHyffJ5Mzlx/aNWPYNolN0MBkc32O2AAQUeYkiRcdF3oRRH8u+2uveRwxgIM5UEoSOzaO64EUzoeDNI7jJhxHqQHm4UPUfI0xMsdQnJkPskglF2+13gPGzf0TdnsVwyYARQNMlta/LsohLSd2JNi6GlebhXlZs43GrEGPNAa1nAzbKMpSm7gKRD7RLEvjB2BVRsBcRqd5wMxCH4eijE43hvyOb7WSM8nUiVPz4/MUjWF2FhohAStX14tuPtTTlmS5DTdOnAUmO1ONxslRvXVvUCtFCZilLHE9kJDuSZqmBV/4wAGjN9j1FqiBukwowzA6igWQOFCJpO6vlEoPdOMkoJB26KDK1Uzabcu721xNromSLBENcNdOttHs1QMIfKRGJYLowA0Uic5t0lg47DOZfB78OgWh5WQYIMEAWAyW8cXC1lYrF6kkU6VXUGtgVZIkgQpZrmcyX/2dkE2So/DAco3YQ2BPprIFAyy+yIsdWUsoqsj5h4c3rn4KY3AGNgnGoSEPzhiDThdjHvGgb8w6iqECSByuXC7+rXLrypXtbQxSi6QtglCtVpcyxWGlyIu1PAxfUcQ1q1X/PdsoIGAyQhRVrdaRVyXC7U7zBxNL9fCdMBv0GeFiUA3j6dvHDDhHnAz1MkYhj3K5HMBElleelUr7Ndi7LgtauLdEVtcTcz3pRaWjKpwoyrCNO13hm9MGa0F0dFStg1A3HOAsdTGU2AzXl27cuTN0dwiKPRbSF3LSO4DqxnSBAPWfCWBUoOReKT0bM5FljBTqNGkK11fTabca6oGCVlQUuVaw6hFDtpk1GKxTNW4DpmWb7EDGp/Oh4urq5j0fLVhYk8l5EULRbKb6+vqO7WPMFBWIIS/+g3xbWVm+XXkwUSZ1pqowYhGwKgbH60QxOhni1Q1ZUWprs1ZQvNB8bpietrbFD7zNFv0QdbuLajSdTqzes9CQK+GgxWO5ex4UcbogHVGXOQaeRRYWFkCT5eVvlcitAPiwbtewFrzbDdXrmXR6eHIyqn7gCaX2o2BFkN7m7D+SzDW0aSiK45FuDWmdHQqVOZewKl1bUeNji0StD6gIWoVZHT6YhZmIUKXthhqEboUimhU2NIjPVqrRbDo/zA9LFNRaq1OjQ8ZoUyxSRSxTCzLQgQw8mSf5khDuj/85Nzc5/2szhl2umWEFfpFZvygO5btFIc+ywUDACUosnJ+mV9DEThPR4KDw9aBkxSoklUrFyhPl8kTm+ciVNbV1c5YufazP45VA4Xl+kyzIP5RIVffZS8CAL2PBCOlyfxhVlq2NSJosD/FD4FnwP1k1aFnZFrQ4BwkTzN4GDCNMWDtmajdht5BKJXa+HIuBmstXX8FX6+WaOueCbfVtL5emi8K9/AWoy7Aid49+DsOLjphJMhwOG83xL/LiKk0Z2nTwgsgKm1iWFbigBIY+mJ04Sq9AiUX4etxAGDDM0I6BkmQydv9+qhy7fPV9Lbjy9Ls5tGcv5QwW02peVFk+H2HFqgdxlxlxIW6StIEU48DMkUPDrBgRI7xYPJpXBZblnAEuUF8P2YKFhECbcZwgDAaiGjMYkGQyNcuBGfZidW1tHaxxFOagBtMBSSqqebA9eFEc/QxDQ7JcJEBmKR++VnUrP2RNkgX+gsBrqqpybW2BIGw/OGgCRWkcNcAMNuhRjaT6k8l+nXT5z8jCYzUe796Td691nbY70+mixAqsoF3//DGEdEBBYGwy7ja63Uazy0zODMuRiMyKAg8PSdFeH5Se4yyNzbB+0QS9mzbhuAHSBScCKmYxqYlzO0ZWH6hbAI1SK+w1+TnOJ0kqOx3K2WxmsxnZDgwjKHHrouBGx8DrUVHUNJ5VoxCq9MTCWRiv5+nefXYaXYWbCLQdpSFZGzEk2a8HQMqZkSXr59Sfbu2BhmxuU1s6rRYlfjobisdJUk+W2WZ0zzLgAniQtNANTeZ1jKCqrCQxTK/Vus3i8VJ2B1SGWIWaaKqJOoYh/f2ppA6JlV9kqq/sPrz14cMN0LxuqXuZVlmh81suFBqIFxIJN2l069nSC99h7EsUCok+W/i7rMkaz7OzISm+Zq/H6rFSvVt2OVAUCo877NCPIpVkSldSicWen6neX2OYd+7ZxT1dXTcb/c2KT5kecyGusVyoUJqcHI+T40Bxk26y9HuykIj3QWP/ndEUoPBy1BfkOOaJ1ep1dHrs/pM1KEGbCMzRUgOQ1H2oiA4pZ15U41jLOhDS09N6CncGBVX6lkO2u8amwBwqlCDGS+PjpUk4PmWzY2EbtKq5Lxqv/VR4tpNp9vn8bxth/7HBgVOUfxftaMApO/w7NiHA+C+lPDFye8ktbO7muW96jvec9tNOSfFn9aUkQWan/mb7CjC+DiglwPb6NZXN2Yw2s+sbw2qsIgV9fuYkWDi+aOPhww0Y7QCndBtFOQAyvwVJ6iWpVACSybxfsnD1vIeb79zZ03r2LtiXxUEd0mGzkWQWKCQJiEICGFOPBuIJss9s3I5ke6MMo0pSlOH8nRxzzXut64TXjm00DHb6rXaqqWbL8pZ/NZphaGplGMdPDG+te7ZiH9r50qI70G0sWIOmRckm5+C4MoI5R30oBNmHNdTZOZ5jaGC1To6T3uCg7V617HbZaTLciXU25od7RHcLhe5l3qaQQ2s6d2OQi0Z3Ldp6TtHjOaIg78//8zzv874+r0qBBAZcC7t/3lV1B7ZDAT5k9gsT/f3fvkPcKYMSCPiPQ/d+r125fOUzQFy7d2fos2tgl2ywSDbJmffmZ7a+emfr9tQUMTUD57VrsgM89lKPtY/o0KoNhi5ktfFfSH59AEH5RTUXykgbfMEsCjRJWvsPywMD+qH7P94f0p/c+xh+w1+79tnKiV6vhxS22S7Zxi83ua0pwjr1XpJMWsnozNrt22trdCJh0arVcHWo1SMGAyj5/JOboGMBlGzeCn0Q9HkDwSDGyALNWfshuy4B5D7MD/3JncjKFWhCARXe2cBgu+c65KasVoKYss5zSZKUo9FowmExWDTaPpLknobGmQUgmdWbUFN+/bfg3zDmcr4gbPcKOEtRouX7/tPDMpQR5YLvfhK5//NORGHAO5ABkCvpFmmdmrf209y8laZlwUJR/kQi4XFMcOq+Ptkx1mF4DiAgZfXzm4s3FxYePFyam9v27i6d2XGMlQUrIXNHrsu2yzYwvTIVISR6YCimt11ShBwVyS0iSVjnKfFCFKy04IGqBGnjsWjBptWesTGANFZByueri4q/tndzn875lhrLBSeGyaxfniBP02VlsVL8b0MGoAKDDf1XJMfHxwdACPfCPMmRVgKL1UlhDUoS1AszCt2Tp7XqsY4u6LGNIQ2gNFYbAHmwu5vdngv5MpnseaFQZAWWpeX6IWQrtDtsiDLmlWsDkFKXlbAr3tLrm1aOILa2OEqM4ZilXytQMJHNo6NXjSZNe7t2eqTLAp0DZKPRyABkQVGyu+vzBr2+LECceIxmWVYmxXgE0ngcGI+NlyMfIzblhU0x2OPtXdRFUiTnORErsiynfYGaZBh+tPObn64CBM5O1RbLtOc5JNPIZEANCFl48GDJp5Oyfy5JmQLuxATq4ICiBGczveLSK4Mj5UgZxlYMQfSXkAHXYZGUaY6VRfAVR7NUlGInzev8Rz8EOnWzIEXdMW0YG/MgGcUUyMJNWOezYV/uTy/v40EKVmeEgwNMxOJNCIzehuhdaRekFTBsegSW4HSrpa3TrIjZCy2RThKCwFKC32zmRwOBq4YujVarMYxZxhwO5CyT2cg0FhdXgbO7lPNmf8tJks9dcB5gWpllD/y4vxWP34mcuE4i6fSOawi5bHsb0uDHlXL8AisWRRxP4fbTOs3RMxTFTA6aJSgZXuPLXSOa2ZGOMaLLsYZkzvhMo1pdqsIOL1v1eXO5XCAg8RL4ixVEjLEf4H4RO4xXmvuRyE66XCvfH1gZiKy4SkfOIoYVcHveHcOK1nlGkEXGgK57e71hCVpnGo1GO93heBaigph5PrOxEQhWVxd3fcGsN5sN5SQ+WCg4CwyHsZj/4MBux+tYLKZc8aNKJd/cT1cqlRYGCZVKud2lFi7SXJKmorTADJrNowFUB6cjs0CZ7ph2PDs9oUBGR6VwAJQAwzfny+ay0vl5we8sOEWWZQpOv92d+hvH8BjcGBg8tZxYDMNL7pQ7FcdwcJkVahBBW4r+N9B1KdypM8wCY7YD9iWOiWkHYjYzvLQePqsuLYWDPigqWV/ouH4uOWO8hB3gnosiHnNX3PFUPv83DuNDBPA4IFL5UiqfKrkhMU6tgoWkpmgKvMW/rDMavaYRYLRr1ACZniBACWMeXR/8SKouhtBANRT05uY2T2Pn4RbP8347xtRxrHjgdldK+XQln4+XSuCfPfc+WMTddIMHL4rMqyT7hEwzfhQ1+zvDnZ1GowmUzLZ3EGqOcxAkIpgHzSgqrQer0BIOhEJeb/bY6TwOHRcKBdbPshQr4AVnCYLhSkf2Ins7f+xFajv7e5H9Wjzewi+KxboogrdkGSBAWb8aftloMqnaYZuihpNEYoIgkEl0chDtXJeCErre2RnS+ea2jefHwKryvB3HcZbFIOKxo2bFVXat1FZqOzVXrba/l95rHrqLuHhab6cpUpYTTJFB16/CwYKq22SabWvTal6Bo2r1UxxHIFSUYRhUCgRQs/kNVAoFje8aex8eb3vDGwEct4PhOISgcNS8U0una+VaJO3a29uvNA/jpzFMFE9BCksQFGowoC+HwzrdW8bu7tlu2Dtq2rXchJokAUJ7PDS4S1pnJoVBNByWjqHdva06vsj5AnZ+GSCAKcRKR0fNZtp1L11Jp9N7laP8Yct5Ua/j9WKdltlJVJhEdSYdnIyBEmgwmgAyotWSBEEokGhClsGV0JVmBAaF7zNiNKpUpuG/vAF+Y2N5GTg4GFaKV+IVmCXgtyYw4q14K3YRi4lFkfKzftSJ8uGQoVf1Yu8XqvdVbbPDj0LhUlthmbcmrUi0a/q2JzHICAkLnUAtI5Yuo2rYpHp415STspnl5WU7D1rckKz5eL4E+XUUz+f34TWkmZOJYX7MDlkYCIQ7vaHwrd7uL1Svtb/f1t72aJva8nRPsi/Z82oScTjW1jwO8BkdjXosFlBiUkHnfvPG8LZ3m8+cKRiwFMxteKTy7jxk8X7erUx1N++225cVtRu+oC7kfevWux9+qWprG36zrf2ZHm0HbFi0Pcnkq8jE2gwtOzy36SgV9chEAiDdw93Dqu2Hx5tStZGxL8MwcCscOwys0MAU5vK/drZ8luGrPshKb++tD69/ePf6h23XX+p5RtPToX2pJ9kDN/LCE1tPkU89BRtf5Q9UVrKv5/nHn3n8+ut3b7x53bTpDYfCqGLOf+3ceV5Xnv83NHyuC4fhM97NzVt3byhdzF++/u516JQ+Cn+xeTT56iNPwuORfwDmIxlqcXq9nQAAAABJRU5ErkJggg==";
  
});
canvas {width: 100px; height: 100px}
div {text-align:center; width:120px; float:left}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div>
  <canvas id="canvOrigMinus100" width="100" height="100"></canvas>
  -98%
</div>

<div>
  <canvas id="canvOrigMinus50" width="100" height="100"></canvas>
  -50%
</div>

<div>
  <canvas id="canvOrig" width="100" height="100"></canvas>
  Original
</div>

<div>
  <canvas id="canvOrigPlus50" width="100" height="100"></canvas>
  +50%
</div>

<div>
  <canvas id="canvOrigPlus100" width="100" height="100"></canvas>
  +98%
</div>

  <hr/>

<div style="clear:left">
  <canvas id="canvRoundMinus90" width="100" height="100"></canvas>
  Round-trip <br/> (-98%, +98%)
</div>

<div>
  <canvas id="canvRoundMinus50" width="100" height="100"></canvas>
  Round-trip <br/> (-50%, +50%)
</div>

<div>
  <canvas id="canvRound0" width="100" height="100"></canvas>
  Round-trip <br/> (0% 100x)
</div>

<div>
  <canvas id="canvRoundPlus50" width="100" height="100"></canvas>
  Round-trip <br/> (+50%, -50%)
</div>

<div>
  <canvas id="canvRoundPlus90" width="100" height="100"></canvas>
  Round-trip <br/> (+98%, -98%)
</div>

Explanation

解释

(Disclaimer - I am not an image specialist or a mathematician. I am trying to provide a common-sense explanation with minimal technical details. Some hand-waving below, e.g. 255=256 to avoid indexing issues, and 127.5=128, for simplifying the numbers.)

免责声明 - 我不是图像专家或数学家。我试图用最少的技术细节提供常识性的解释。下面有些挥手,例如 255=256 以避免索引问题,以及 127.5=128,用于简化数字。

Since, for a given pixel, the possible number of non-zero values for a color channel is 255, the "no-contrast", average value of a pixel is 128(or 127, or 127.5 if you want argue, but the difference is negligible). For purposed of this explanation, the amount of "contrast" is the distance from the current value to the average value (128). Adjusting the contrast means increasing or decreasing the difference between the current value and the average value.

因为,对于给定的像素,颜色通道的非零值的可能数量是 255,“无对比度”,像素的平均值是 128(或 127,或 127.5,如果你想争论,但差异可以忽略不计)。为了说明的目的,“对比度”的量是从当前值到平均值(128)的距离。调整对比度意味着增加或减少当前值与平均值之间的差异。

The problem the algorithm solves then is to:

该算法解决的问题是:

  1. Chose a constant factor to adjust contrast by
  2. For each color channel of each pixel, scale "contrast" (distance from average) by that constant factor
  1. 选择一个常数因子来调整对比度
  2. 对于每个像素的每个颜色通道,按该常数因子缩放“对比度”(与平均值的距离)

Or, as hinted at in the CSS spec, simply choosing the slope and intercept of a line:

或者,如CSS 规范中所暗示的,只需选择一条线的斜率和截距:

<feFuncR type="linear" slope="[amount]" intercept="-(0.5 * [amount]) + 0.5"/>

<feFuncR type="linear" slope="[amount]" intercept="-(0.5 * [amount]) + 0.5"/>

Note the term type='linear'; we are doing linear contrast adjustment in RGB color space, as opposed to a quadratic scaling function, luminence-basedadjustment, or histogram matching.

注意这个词type='linear';我们在RGB 颜色空间中进行线性对比度调整,而不是二次缩放函数、基于亮度的调整或直方图匹配

If you recall from geometry class, the formula for a line is y=mx+b. yis the final value we are after, the slope mis the contrast (or factor), xis the initial pixel value, and bis the intercept of the y-axis (x=0), which shifts the line vertically. Recall also that since the y-intercept is not at the origin (0,0), the formula can also be represented as y=m(x-a)+b, where ais the x-offset shifting the line horizontally.

如果你回忆一下几何课,一条线的公式是y=mx+by是我们所追求的最终值,斜率m是对比度(或factor),x是初始像素值,b是 y 轴 (x=0) 的截距,它垂直移动线。还记得因为 y 轴截距不在原点 (0,0),所以公式也可以表示为y=m(x-a)+b,其中a是水平移动线的 x 偏移量。

Formula for slope of a line

直线斜率公式

For our purposes, this graph represents the input value (x-axis) and the result (y-axis). We already know that b, the y-intercept (for m=0, no contrast) must be 128 (which we can check against the 0.5 from the spec - 0.5 * the full range of 256 = 128). xis our original value, so all we need is to figure out the slope mand x-offset a.

出于我们的目的,此图表示输入值(x 轴)和结果(y 轴)。我们已经知道b,y 轴截距(对于m=0,没有对比度)必须是 128(我们可以对照规范中的 0.5 - 0.5 * 256 = 128 的完整范围来检查)。x是我们的原始值,所以我们只需要计算出斜率m和 x-offset a

First, the slope mis "rise over run", or (y2-y1)/(x2-x1)- so we need 2 points known to be on the desired line. Finding these points requires bringing a few things together:

首先,斜率m是“上升超过运行”,或者(y2-y1)/(x2-x1)- 所以我们需要已知的 2 个点位于所需的线上。找到这些要点需要将几件事放在一起:

  • Our function takes the shape of a line-intercept graph
  • The y-intercept is at b = 128- regardless of the slope (contrast).
  • The maximum expected 'y' value is 255, and the minimum is 0
  • The range of possible 'x' values is 256
  • A neutral value should always stay neutral: 128 => 128 regardless of slope
  • A contrast adjustment of 0should result in no change between input and output; that is, a 1:1 slope.
  • 我们的函数采用线截图的形状
  • y 轴截距在b = 128- 与斜率(对比度)无关。
  • 最大预期 'y' 值为 255,最小值为 0
  • 可能的“x”值的范围是 256
  • 中性值应始终保持中性:128 => 128 与斜率无关
  • 对比度调整0应该不会导致输入和输出之间发生变化;也就是 1:1 的斜率。

Taking all these together, we can deduce that regardless of the contrast (slope) applied, our resulting line will be centered at (and pivot around) 128,128. Since our y-intercept is non-zero, the x-intercept is also non-zero; we know the x-range is 256 wide and is centered in the middle, so it must be offset by half of the possible range: 256 / 2 = 128.

将所有这些放在一起,我们可以推断出,无论应用的对比度(斜率)如何,我们生成的线都将以 为中心(并围绕 旋转)128,128。由于我们的 y 轴截距非零,因此 x 轴截距也非零;我们知道 x 范围是 256 宽并且在中间居中,所以它必须偏移可能范围的一半:256 / 2 = 128。

Contrast function slopes

对比函数斜率

So now for y=m(x-a)+b, we know everything except m. Recall two more important points from geometry class:

所以现在y=m(x-a)+b,除了m. 回忆一下几何课中更重要的两点:

  • Lines have the same slope even if their location changes; that is, mstays the same regardless of the values of aand b.
  • The slope of a line can be found using any 2 points on the line
  • 即使它们的位置发生变化,线也具有相同的斜率;即,m保持不变不管值的ab
  • 可以使用直线上的任意 2 个点找到直线的斜率

To simplify the slope discussion, let's move the coordinate origin to the x-intercept (-128) and ignore aand bfor a moment. Our original line will now pivot through (0,0), and we know a second point on the line lies away the full range of both x(input) and y(output) at (255,255).

为了简化斜率讨论,让我们将坐标原点移动到 x 轴截距 (-128) 并暂时忽略ab。我们原来的线现在将通过 (0,0),我们知道线上的第二个点位于(255,255) 处(x输入)和y(输出)的整个范围。

We'll let the new line pivot at (0,0), so we can use that as one of the points on the new line that will follow our final contrast slope m. The second point can be determined by moving the current end at (255,255) by some amount; since we are limited to a single input (contrast) and using a linear function, this second point will be moved equally in the xand ydirections on our graph.

我们将让新线在 (0,0) 处旋转,因此我们可以将其用作新线上将遵循最终对比度斜率的点之一m。第二个点可以通过将 (255,255) 处的当前端点移动一定量来确定;由于我们仅限于单个输入 ( contrast) 并使用线性函数,因此第二个点将在图上的xy方向上等量移动。

Adjusting the contrast slope

Adjusting the contrast slope

The (x,y) coordinates of the 4 possible new points will be 255 +/- contrast. Since increasing or decreasing both x and y would keep us on the original 1:1 line, let's just look at +x, -yand -x, +yas shown.

4 个可能的新点的 (x,y) 坐标将为255 +/- contrast。由于增加或减少 x 和 y 会使我们保持在原始的 1:1 线上,让我们看看+x, -y-x, +y如图所示。

The steeper line (-x, +y) is associated with a positive contrastadjustment; it's (x,y) coordinates are (255 - contrast,255 + contrast). The coordinates of the shallower line (negative contrast) are found the same way. Notice that the biggest meaningful value of contrastwill be 255- the most that the initial point of (255,255) can be translated before resulting in a vertical line (full contrast, all black or white) or a horizontal line (no contrast, all gray).

较陡的线 (-x, +y) 与正contrast调整相关;它的 (x,y) 坐标是 ( 255 - contrast, 255 + contrast)。较浅线(负contrast)的坐标以相同的方式找到。请注意,的最大有意义的值contrast将是 255- (255,255) 的初始点在生成垂直线(全对比度,全黑或白)或水平线(无对比度,全灰色)之前最多可以平移.

So now we have the coordinates of two points on our new line - (0,0) and (255 - contrast,255 + contrast). We plug this into the slope equation, and then plug that into the full line equation, using all the parts from before:

所以现在我们有了新直线上两点的坐标 - (0,0) 和 ( 255 - contrast, 255 + contrast)。我们将其代入斜率方程,然后将其代入整线方程,使用之前的所有部分:

y = m(x-a) + b

m= (y2-y1)/(x2-x1)=>
((255 + contrast) - 0)/((255 - contrast) - 0)=>
(255 + contrast)/(255 - contrast)

a = 128
b = 128

y = (255 + contrast)/(255 - contrast) * (x - 128) + 128QED

y = m(x-a) + b

m= (y2-y1)/(x2-x1)=>
((255 + contrast) - 0)/((255 - contrast) - 0)=>
(255 + contrast)/(255 - contrast)

a = 128
b = 128

y = (255 + contrast)/(255 - contrast) * (x - 128) + 128QED

The math-minded will notice that the resulting mor factoris a scalar (unitless) value; you can use any range you want for contrastas long as it matches the constant (255) in the factorcalculation. For example, a contrastrange of +/-100and factor = (100 + contrast)/(100.01 - contrast), which is was I really use to eliminate the step of scaling to 255; I just left 255in the code at the top to simplify the explanation.

有数学头脑的人会注意到结果morfactor是一个标量(无单位)值;您可以使用任何您想要的范围contrast,只要它与计算中的常数 ( 255)匹配即可factor。例如,一个and的contrast范围,这是我真正用来消除缩放到 255 的步骤;我只是留在顶部的代码以简化解释。+/-100factor = (100 + contrast)/(100.01 - contrast)255



Note about the "magic" 259

关于“魔法”的注意事项 259

The source articleuses a "magic" 259, although the author admits he doesn't remember why:

文章出处采用了“神奇”的259,虽然作者承认,他不记得为什么:

"I can't remember if I had calculated this myself or if I've read it in a book or online.".

“我不记得这是我自己计算过的,还是我在书上或网上读过的。”。

259 should really be 255 or perhaps 256 - the number of possible non-zero values for each channel of each pixel. Note that in the original factorcalculation, 259/255 cancels out - technically 1.01, but final values are whole integers so 1 for all practical purposes. So this outer term can be discarded. Actually using 255 for the constant in the denominator, though, introduces the possibility of a Divide By Zero error in the formula; adjusting to a slightly larger value (say, 259) avoids this issue without introducing significant error to the results. I chose to use 255.01 instead as the error is lower and it (hopefully) seems less "magic" to a newcomer.

259 应该真的是 255 或 256 - 每个像素的每个通道的可能非零值的数量。请注意,在原始factor计算中,259/255 抵消了 - 从技术上讲是 1.01,但最终值是整数,因此对于所有实际目的来说都是 1。所以这个外部术语可以被丢弃。然而,实际上在分母中使用 255 作为常数会在公式中引入除以零错误的可能性;调整到稍微大一点的值(比如 259)可以避免这个问题,而且不会给结果带来显着的误差。我选择使用 255.01,因为错误较低,并且(希望)对新手来说似乎不那么“神奇”。

As far as I can tell though, it doesn't make much difference which you use- you get identical values except for minor, symmetric differences in a narrow band of low contrast values with a low positive contrast increase. I'd be curious to round-trip both versions repeatedly and compare to the original data, but this answer already took way too long. :)

不过,据我所知,您使用哪种方法并没有太大区别- 除了在低对比度值的窄带中出现微小的对称差异外,您会得到相同的值,并且正对比度增加较低。我很想反复往返两个版本并与原始数据进行比较,但是这个答案已经花费了太长时间。:)

回答by Brian

After trying the answer by Schahriar SaffarShargh, it wasn't behaving like contrast should behave. I finally came across this algorithm, and it works like a charm!

在尝试了 Schahriar SaffarShargh 的答案后,它的行为不像对比应该表现的那样。我终于遇到了这个算法,它就像一个魅力!

For additional information about the algorithm, read this articleand it's comments section.

有关该算法的更多信息,请阅读本文及其评论部分。

function contrastImage(imageData, contrast) {

    var data = imageData.data;
    var factor = (259 * (contrast + 255)) / (255 * (259 - contrast));

    for(var i=0;i<data.length;i+=4)
    {
        data[i] = factor * (data[i] - 128) + 128;
        data[i+1] = factor * (data[i+1] - 128) + 128;
        data[i+2] = factor * (data[i+2] - 128) + 128;
    }
    return imageData;
}

Usage:

用法:

var newImageData = contrastImage(imageData, 30);

Hopefully this will be a time-saver for someone. Cheers!

希望这对某人来说可以节省时间。干杯!

回答by Schahriar SaffarShargh

I found out that you have to use the effect by separating the darks and lights or technically anything that is less than 127 (average of R+G+B / 3) in rgb scale is a black and more than 127 is a white, therefore by your level of contrast you minus a value say 10 contrast from the blacks and add the same value to the whites!

我发现你必须通过分离黑暗和光明来使用这种效果,或者技术上任何小于 127(R+G+B / 3 的平均值)的 rgb 比例是黑色,大于 127 是白色,因此根据您的对比度级别,您减去一个值,例如与黑色的对比度为 10,并将相同的值添加到白色!

Here is an example: I have two pixels with RGB colors, [105,40,200] | [255,200,150] So I know that for my first pixel 105 + 40 + 200 = 345, 345/3 = 115 and 115 is less than my half of 255 which is 127 so I consider the pixel closer to [0,0,0] therefore if I want to minus 10 contrast then I take away 10 from each color on it's average Thus I have to divide each color's value by the total's average which was 115 for this case and times it by my contrast and minus out the final value from that specific color:

这是一个例子:我有两个 RGB 颜色的像素,[105,40,200] | [255,200,150] 所以我知道对于我的第一个像素 105 + 40 + 200 = 345, 345/3 = 115 和 115 小于我 255 的一半,即 127 所以我认为像素更接近 [0,0,0]因此,如果我想减去 10 对比度,那么我从每种颜色的平均值中减去 10 因此,我必须将每种颜色的值除以在这种情况下为 115 的总平均值,并用我的对比度对其进行乘以减去最终值那个特定的颜色:

For example I'll take 105 (red) from my pixel, so I divide it by total RGB's avg. which is 115 and times it by my contrast value of 10, (105/115)*10 which gives you something around 9 (you have to round it up!) and then take that 9 away from 105 so that color becomes 96 so my red after having a 10 contrast on a dark pixel.

例如,我将从我的像素中取 105(红色),所以我将它除以总 RGB 的平均值。这是 115 乘以我的对比度值 10,(105/115)*10,它给你大约 9(你必须把它四舍五入!)然后把 9 从 105 中取出,这样颜色就变成了 96,所以我的在暗像素上具有 10 对比度后的红色。

So if I go on my pixel's values become [96,37,183]! (note: the scale of contrast is up to you! but my in the end you should convert it to some scale like from 1 to 255)

所以如果我继续我的像素值变成 [96,37,183]!(注意:对比度的比例由您决定!但我最后您应该将其转换为从 1 到 255 的某种比例)

For the lighter pixels I also do the same except instead of subtracting the contrast value I add it! and if you reach the limit of 255 or 0 then you stop your addition and subtraction for that specific color! therefore my second pixel which is a lighter pixel becomes [255,210,157]

对于较亮的像素,我也做同样的事情,只是我没有减去对比度值,而是添加它!如果您达到 255 或 0 的限制,那么您将停止对该特定颜色的加法和减法!因此,我的第二个像素较亮的像素变为 [255,210,157]

As you add more contrast it will lighten the lighter colors and darken the darker and therefore adds contrast to your picture!

当您添加更多对比度时,它会使较浅的颜色变亮并使较暗的颜色变暗,从而为您的图片增加对比度!

Here is a sample Javascript code ( I haven't tried it yet ) :

这是一个示例 Javascript 代码(我还没有尝试过):

var data = imageData.data;
for (var i = 0; i < data.length; i += 4) {
 var contrast = 10;
 var average = Math.round( ( data[i] + data[i+1] + data[i+2] ) / 3 );
  if (average > 127){
    data[i] += ( data[i]/average ) * contrast;
    data[i+1] += ( data[i+1]/average ) * contrast;
    data[i+2] += ( data[i+2]/average ) * contrast;
  }else{
    data[i] -= ( data[i]/average ) * contrast;
    data[i+1] -= ( data[i+1]/average ) * contrast;
    data[i+2] -= ( data[i+2]/average ) * contrast;
  }
}

回答by Escher

This javascript implementation complies with the SVG/CSS3 definition of "contrast" (and the following code will render your canvas image identically):

此 javascript 实现符合 SVG/CSS3 对“对比度”的定义(以下代码将呈现您的画布图像相同):

/*contrast filter function*/
//See definition at https://drafts.fxtf.org/filters/#contrastEquivalent
//pixels come from your getImageData() function call on your canvas image
contrast = function(pixels, value){
    var d = pixels.data;
    var intercept = 255*(-value/2 + 0.5);
    for(var i=0;i<d.length;i+=4){
        d[i] = d[i]*value + intercept;
        d[i+1] = d[i+1]*value + intercept;
        d[i+2] = d[i+2]*value + intercept;
        //implement clamping in a separate function if using in production
        if(d[i] > 255) d[i] = 255;
        if(d[i+1] > 255) d[i+1] = 255;
        if(d[i+2] > 255) d[i+2] = 255;
        if(d[i] < 0) d[i] = 0;
        if(d[i+1] < 0) d[i+1] = 0;
        if(d[i+2] < 0) d[i+2] = 0;
    }
    return pixels;
}

回答by Jay

By vintaging I assume your trying to apply LUTS..Recently I have been trying to add color treatments to canvas windows. If you want to actually apply "LUTS" to the canvas window I believe you need to actually map the array that imageData returns to the RGB array of the LUT.

通过复古,我假设您正在尝试应用 LUTS。最近我一直在尝试为画布窗口添加颜色处理。如果您想将“LUTS”实际应用于画布窗口,我相信您需要将 imageData 返回的数组实际映射到 LUT 的 RGB 数组。

(From Light illusion) As an example the start of a 1D LUT could look something like this: Note: strictly speaking this is 3x 1D LUTs, as each colour (R,G,B) is a 1D LUT

(来自光错觉)例如,一维 LUT 的开始可能看起来像这样: 注意:严格来说,这是 3 个一维 LUT,因为每种颜色(R、G、B)都是一维 LUT

R, G, B 
3, 0, 0 
5, 2, 1 
7, 5, 3 
9, 9, 9

Which means that:

意思就是:

For an input value of 0 for R, G, and B, the output is R=3, G=0, B=0 
For an input value of 1 for R, G, and B, the output is R=5, G=2, B=1 
For an input value of 2 for R, G, and B, the output is R=7, G=5, B=3 
For an input value of 3 for R, G, and B, the output is R=9, G=9, B=9

Which is a weird LUT, but you see that for a given value of R, G, or B input, there is a given value of R, G, and B output.

这是一个奇怪的 LUT,但您会看到对于给定的 R、G 或 B 输入值,存在 R、G 和 B 输出的给定值。

So, if a pixel had an input value of 3, 1, 0 for RGB, the output pixel would be 9, 2, 0.

因此,如果一个像素的 RGB 输入值为 3, 1, 0,则输出像素将为 9, 2, 0。

During this I also realized after playing with imageData that it returns a Uint8Array and that the values in that array are decimal. Most 3D LUTS are Hex. So you first have to do some type of hex to dec conversion on the entire array before all this mapping.

在此期间,我在使用 imageData 后还意识到它返回一个 Uint8Array 并且该数组中的值是十进制的。大多数 3D LUTS 是十六进制的。因此,在所有这些映射之前,您首先必须对整个数组进行某种类型的十六进制到十进制转换。

回答by karlphillip

You can take a look at the OpenCV docs to see how you could accomplish this: Brightness and contrast adjustments.

您可以查看 OpenCV 文档以了解如何完成此操作:亮度和对比度调整

Then there's the demo code:

然后是演示代码:

 double alpha; // Simple contrast control: value [1.0-3.0]
 int beta;     // Simple brightness control: value [0-100]

 for( int y = 0; y < image.rows; y++ )
 { 
      for( int x = 0; x < image.cols; x++ )
      { 
          for( int c = 0; c < 3; c++ )
          {
              new_image.at<Vec3b>(y,x)[c] = saturate_cast<uchar>( alpha*( image.at<Vec3b>(y,x)[c] ) + beta );
          }
      }
 }

which I imagine you are capable of translating to javascript.

我想你有能力翻译成 javascript。

回答by fforgoso

This is the formula you are looking for ...

这是您正在寻找的公式...

var data = imageData.data;
if (contrast > 0) {

    for(var i = 0; i < data.length; i += 4) {
        data[i] += (255 - data[i]) * contrast / 255;            // red
        data[i + 1] += (255 - data[i + 1]) * contrast / 255;    // green
        data[i + 2] += (255 - data[i + 2]) * contrast / 255;    // blue
    }

} else if (contrast < 0) {
    for (var i = 0; i < data.length; i += 4) {
        data[i] += data[i] * (contrast) / 255;                  // red
        data[i + 1] += data[i + 1] * (contrast) / 255;          // green
        data[i + 2] += data[i + 2] * (contrast) / 255;          // blue
    }
}

Hope it helps!

希望能帮助到你!