CSS contrast filter - javascript
How does css filter contrast will work ? Is there a formula? I want to reproduce in javascript and I need a formula.
For example css filter brightness(2) take each pixel and multiply by 2, but for contrast I don't have any idea
Thanks
Multiply by 2 is a contrast filter. All multiplication and division of an images RGB values affects the contrast.
The function I like to use is a exponential ease function where the power controls the contrast.
function contrastPixel(r,g,b,power) {
r /= 255; // normalize channels
g /= 255;
b /= 255;
var rr = Math.pow(r,power); // raise each to the power
var gg = Math.pow(r,power);
var bb = Math.pow(r,power);
r = Math.floor((rr / (rr + Math.pow(1 - r, power)))*255);
g = Math.floor((gg / (gg + Math.pow(1 - g, power)))*255);
b = Math.floor((bb / (bb + Math.pow(1 - b, power)))*255);
return {r,g,b};
}
Using it
var dat = ctx.getPixelData(0,0,100,100);
var data = dat.data;
var i = 0;
while(i < data.length){
var res = contrastPixel(data[i],data[i+1],data[i+2],power);
data[i++] = res.r;
data[i++] = res.g;
data[i++] = res.b;
i++;
}
ctx.putImageData(dat,0,0);
The argument power controls the contrast.
power = 1; // no change to the image
0 < power < 1; // reduces contrast
1 < power; // increases contrast
Because the scaling of power is logarithmic it can be hard to control with a linear slider. To give the slider a linear feel use the following instructions to get a value from a slider
For a slider with a min -100 and max 100 and center 0 (0 being no contrast change) get the contrast power value using
power = Math.pow(((Number(slider.value)* 0.0498) + 5)/5,Math.log2(10));
It's not perfectly linear, and the range is limited but will cover most needs.
The test image shows the results. Center bottom is the original. Using the scale in the paragraph above from left to right slider values of -100, -50, 50, 100
Related
How to improve accuracy of a FeedForward Neural Network?
I want to draw StackOverflow's logo with this Neural Network: The NN should ideally become [r, g, b] = f([x, y]). In other words, it should return RGB colors for a given pair of coordinates. The FFNN works pretty well for simple shapes like a circle or a box. For example after several thousands epochs a circle looks like this: Try it yourself: https://codepen.io/adelriosantiago/pen/PoNGeLw However since StackOverflow's logo is far more complex even after several thousands of iterations the FFNN's results are somewhat poor: From left to right: StackOverflow's logo at 256 colors. With 15 hidden neurons: The left handle never appears. 50 hidden neurons: Pretty poor result in general. 0.03 as learning rate: Shows blue in the results (blue is not in the orignal image) A time-decreasing learning rate: The left handle appears but other details are now lost. Try it yourself: https://codepen.io/adelriosantiago/pen/xxVEjeJ Some parameters of interest are synaptic.Architect.Perceptron definition and learningRate value. How can I improve the accuracy of this NN? Could you improve the snippet? If so, please explain what you did. If there is a better NN architecture to tackle this type of job could you please provide an example? Additional info: Artificial Neural Network library used: Synaptic.js To run this example in your localhost: See repository
By adding another layer, you get better results : let perceptron = new synaptic.Architect.Perceptron(2, 15, 10, 3) There are small improvements that you can do to improve efficiency (marginally): Here is my optimized code: const width = 125 const height = 125 const outputCtx = document.getElementById("output").getContext("2d") const iterationLabel = document.getElementById("iteration") const stopAtIteration = 3000 let perceptron = new synaptic.Architect.Perceptron(2, 15, 10, 3) let iteration = 0 let inputData = (() => { const tempCtx = document.createElement("canvas").getContext("2d") tempCtx.drawImage(document.getElementById("input"), 0, 0) return tempCtx.getImageData(0, 0, width, height) })() const getRGB = (img, x, y) => { var k = (height * y + x) * 4; return [ img.data[k] / 255, // R img.data[k + 1] / 255, // G img.data[k + 2] / 255, // B //img.data[(height * y + x) * 4 + 3], // Alpha not used ] } const paint = () => { var imageData = outputCtx.getImageData(0, 0, width, height) for (let x = 0; x < width; x++) { for (let y = 0; y < height; y++) { var rgb = perceptron.activate([x / width, y / height]) var k = (height * y + x) * 4; imageData.data[k] = rgb[0] * 255 imageData.data[k + 1] = rgb[1] * 255 imageData.data[k + 2] = rgb[2] * 255 imageData.data[k + 3] = 255 // Alpha not used } } outputCtx.putImageData(imageData, 0, 0) setTimeout(train, 0) } const train = () => { iterationLabel.innerHTML = ++iteration if (iteration > stopAtIteration) return let learningRate = 0.01 / (1 + 0.0005 * iteration) // Attempt with dynamic learning rate //let learningRate = 0.01 // Attempt with non-dynamic learning rate for (let x = 0; x < width; x += 1) { for (let y = 0; y < height; y += 1) { perceptron.activate([x / width, y / height]) perceptron.propagate(learningRate, getRGB(inputData, x, y)) } } paint() } const startTraining = (btn) => { btn.disabled = true train() } EDIT : I made another CodePen with even better results: https://codepen.io/xurei/pen/KKzWLxg It is likely to be over-fitted BTW. The perceptron definition: let perceptron = new synaptic.Architect.Perceptron(2, 8, 15, 7, 3)
Taking some insights from the lecture/slides of Bhiksha Raj (from slides 62 onwards), and summarizing as below: Each node can be assumed like a linear classifier, and combination of several nodes in a single layer of neural networks can approximate any basic shapes. For example, a rectangle can be formed by 4 nodes for each lines, assuming each nodes contributes to one line, and the shape can be approximated by the final output layer. Falling back to the summary of complex shapes such as circle, it may require infinite nodes in a layer. Or this would likely hold true for a single layer with two disjoint shapes (A non-overlapping triangle and rectangle). However, this can still be learnt using more than 1 hidden layers. Where, the 1st layer learns the basic shapes, followed by 2nd layer approximating their disjoint combinations. Thus, you can assume that this logo is combination of disjoint rectangles (5 rectangles for orange and 3 rectangles for grey). We can use atleast 32 nodes in 1st hidden layer and few nodes in the 2nd hidden layer. However, we don't have control over what each node learns. Hence, a few more number of neurons than required neurons should be helpful.
Why does my Perlin Noise Generator make this squiggly pattern?
I've started learning about perlin noise generation, and I wanted to try to make my own generator in JavaScript. To get me started, I've been following along with this youtube tutorial. to try and copy their basic implementation. I've also been reading this article. I've provided a jsFiddle of my implementation, which shows what I'm doing and what the output is. Instead of the smoothly-flowing, bubbling noise I see in the youtube tutorial, I get tightly-constricted black squiggles. Here's a picture: Here's my generator code: var p = [151,160,137,91,90,15, 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180, 151,160,137,91,90,15, 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180] function generatePerlinNoise(x, y){ // Determine gradient cell corners var xi = Math.floor(x) & 255; var yi = Math.floor(y) & 255; var g1 = p[p[xi] + yi]; var g2 = p[p[xi + 1] + yi]; var g3 = p[p[xi] + yi + 1]; var g4 = p[p[xi + 1] + yi + 1]; // Get point within gradient cell var xf = x - Math.floor(x); var yf = y - Math.floor(y); // Get dot products at each corner of the gradient cell var d1 = generateGradient(g1, xf, yf); var d2 = generateGradient(g2, xf - 1, yf); var d3 = generateGradient(g3, xf, yf - 1); var d4 = generateGradient(g4, xf - 1, yf - 1); var xFade = fade(xf); var yFade = fade(yf); var x1Interpolated = linearInterpolate(xFade, d1, d2); var x2Interpolated = linearInterpolate(xFade, d3, d4); var noiseValue = linearInterpolate(yFade, x1Interpolated, x2Interpolated); return noiseValue; } function generateGradient(hash, x, y){ switch(hash & 3){ case 0: return x + y; case 1: return -x + y; case 2: return x - y; case 3: return -x - y; default: return 0; } } // This is the fade function described by Ken Perlin function fade(t){ return t * t * t * (t * (t * 6 - 15) + 10); } function linearInterpolate(amount, left, right){ return ((1-amount) * left + (amount * right)) } I'm utilizing the generator function by dividing the pixel x and y values by my canvas height, and scaling by a frequency variable: var freq = 12; var noise = generatePerlinNoise((x/canvas.offsetHeight)*freq, (y/canvas.offsetHeight)*freq); noise = Math.abs(noise); I'm currently just using the noise value to generate a greyscale color value: var blue = Math.floor(noise * 0xFF); // Scale 255 by our noise value, // and take it's integer portion var green = Math.floor(noise * 0xFF); var red = Math.floor(noise * 0xFF); data[i++] = red; data[i++] = green; data[i++] = blue; data[i++] = 255; The point of this for me is to learn more about noise generation and javascript. I've tried to think through the problem and made some observations: There are no visible artifacts, so it seems like my fade function is working fine. There don't seem to be any repeating patterns, so that's a good sign. I go generate a complete range of values in the greyscale - not just black and white. The general issue seems to be how the gradient at each pixel affects its neighbors: Mine seem to clump together in snake-like ropes of fixed widths. It seems like the gradient vector options supplied and the permutation table used to randomly-ish select them would govern this, but mine are an exact copy from the tutorial: The same 4 vectors each pointing into a quadrant at 45 degrees, and the standard permutation table. This leaves me stumped as to figuring out what the cause is. My general suspicions boil down to: I've messed up the algorithm somewhere in a subtle way that I keep overlooking. (Most likely) There's a subtle difference in the way JavaScript does something that i'm overlooking. (Maybe) I'm generating noise correctly, but incorrectly applying the result to the RGB values used in my canvas image data. (Least likely) I'd really like to get to the bottom of this. Thanks in advance for your help! :) Also: I DO think this pattern is cool, and this is a learning exercise, so if anyone can share insight into why I'm getting this pattern specifically, that'd be great!
Calculate end rotation circles
I have one circle, which grows and shrinks by manipulating the radius in a loop. While growing and shrinking, I draw a point on that circle. And within the same loop, increasing the angle for a next point. The setup is like this: let radius = 0; let circleAngle = 0; let radiusAngle = 0; let speed = 0.02; let radiusSpeed = 4; let circleSpeed = 2; And in the loop: radius = Math.cos(radiusAngle) * 100; // creating new point for line let pointOnCircle = { x: midX + Math.cos(circleAngle) * radius, y: midY + Math.sin(circleAngle) * radius }; circleAngle += speed * circleSpeed; radiusAngle += speed * radiusSpeed; This produces some kind of flower / pattern to be drawn. After unknown rotations, the drawing line connects to the point from where it started, closing the path perfectly. Now I would like to know how many rotations must occure, before the line is back to it's beginning. A working example can be found here: http://codepen.io/anon/pen/RGKOjP The console logs the current rotations of both the circle and the line.
Full cycle is over, when both radius and point returns to the starting point. So speed * circleSpeed * K = 360 * N speed * radiusSpeed * K = 360 * M Here K is unknown number of turns, N and M are integer numbers. Divide the first equation by the second circleSpeed / radiusSpeed = N / M If speed values are integers, divide them by LCM to get minimal valid N and M values, if they are rational, multiply them to get integer proportion. For your example minimal integers N=1,M=2, so we can get K = 360 * 1 / (0.02 * 2) = 9000 loop turns
Changing the color of the ball to a random colour every time it hits the wall
I want to change color of the dot every time it hits the wall. I saw few solutions, but I don't know why mine isn't working. Here's the part responsible for changing color: function chColor() { hex = Math.floor(Math.random() * 100000 + 1); color = '"' + "#" + hex + '"'; color.toString(); return color; } And here is not working fiddle: https://jsfiddle.net/vpzd7ye6/
Colour overkill This answer is way overkill and I was about to discard it, but, why not give a Better random colour for those who may need it... (|:D When random seams less random Selecting a random totally random colour for us humans does not work that well if you wish to have a clear distinct change that attracts the eye from one colour to the next. The problem is that many of the colour values available are very close to white or black and a totally random colour may be very close to the last making the change impossible to notice. Simple random colour So first the simple random colour picks a totally random colour from all of the 16,777,216 possible colours. function randColor(){ return "#" + (Math.floor(Math.random() * 0x1000000) + 0x1000000) .toString(16) .substr(1); } The function works by finding a random 24 bit number (0 - 0xFFFFFF same as 0 - 16777216). Because numbers do not have leading zeros added all random values below 0x100000 will, when converted to hex have less than 6 digit (the CSS colour value requires 3 or 6 digit Hex value) so I add 0x1000000 to ensure that the number is > 0x100000 and will always have 7 digits. As the first (left most) digit is not needed I use substr to remove it. Then added the "#" and return. The function Number.toString(radix) takes an argument called radix which specifies the base the number is converted to. It defaults if not supplied to 10 (base ten is what we humans use). The hex colour is base 16 so that must be specified. The toString radix argument can be any value from 2 (binary) to 36 (which uses characters 0-9 then A-Z) Less is more Random Ironically we humans tend to find less random sequences more random than true random sequences. Many music players use this type of random sequence when they have the play set to shuffle, to ensure tracks are not played twice in a row or that repeated sequences of tracks are played by random chance. To make the random colour seem more random and distinct you should track the last colour return so that you can ensure you are not close to that colour. You should also use a random colour that stays away from the whites and blacks. var randColour = (function(){ // as singleton so closure can track last colour var lastHue = 0; const minChange = 360 / 3; // Hue distance from last random colour var lastVal = false; // for low high saturation and luminance const randomAmount = 360 / 3; // random hue range const minVal = 20; // how far from full black white to stay (percent) const minSat = 80; // how far from grey to stay (percent) return function(){ var hueChange = Math.random() * randomAmount ; hueChange *= Math.random() < 0.5 ? -1 : 1; // randomly move forward or backward lastHue += 360 + hueChange + minChange; // move away from last colour lastHue %= 360; // ensure colour is in valid range var sat = (Math.random() * (100 - minSat)) + minSat; // get saturation (amount of colour) var val = (Math.random() * (50 - minVal * 2)) * 1.5; // get value (luminance) // switch between high and low and luminance lastVal = ! lastVal; if(lastVal){ val = minVal + val; } else { val = 100 - minVal - val; } return "hsl(" + lastHue.toFixed(0) + "," + sat.toFixed(0) + "%," + val.toFixed(0) + "%)"; } })(); // call singleton This function returns a random colour but ensures that the saturation and value stay within a specific range and that the hue is at least 1/3 around the colour wheel from the last value. It cycles between low and low and high luminance values to make the colour change as clear as possible. Closure is used to keep track of the last colour returned. The function returns the random colour as a CSS hsl(hue, saturation, luminance) color string. There are two constants that control the colour sequence. minVal set to 20 is the percentage to stay away from full black or full white. Valid range is 0-~50 and minSat set to 80 is how far to stay away from grays in percent. const minSat = 80; // how far from grey to stay (percent) Compare change To compare the two methods the following demo shows side by side a set of random colours using both methods, then flashes a new random colour 4 times a second. The simple random colour will appear from time to time to miss a change. I leave it up to you to pick which side is which. var canvas = document.createElement("canvas"); canvas.width = 620; canvas.height = 200; var ctx = canvas.getContext("2d"); document.body.appendChild(canvas); var randColour = (function(){ // as singleton so closure can track last colour var lastHue = 0; const minChange = 360 / 3; // min hue change var lastVal = false; // for low high saturation and luminance const randomAmount = 360 / 3; // amount of randomness const minVal = 20; // how far from full black white to stay (percent) const minSat = 80; // how far from grey to stay (percent) return function(){ var hueChange = Math.random() * randomAmount ; hueChange *= Math.random() < 0.5 ? -1 : 1; // randomly move forward or backward lastHue += 360 + hueChange + minChange; // move away from last colour lastHue %= 360; // ensure colour is in valid range var sat = (Math.random() * (100 - minSat)) + minSat; // get saturation (amount of colour) var val = (Math.random() * (50 - minVal * 2)) * 1.5; // get value (luminance) // switch between high and low and luminance lastVal = ! lastVal; if(lastVal){ val = minVal + val; } else { val = 100 - minVal - val; } return "hsl(" + lastHue.toFixed(0) + "," + sat.toFixed(0) + "%," + val.toFixed(0) + "%)"; } })(); // call singleton function randColor(){ return "#" + (Math.floor(Math.random() * 0x1000000) + 0x1000000) .toString(16) .substr(1); } const grid = 16; var gridX = 0; var gridY = 0; var bigSize = grid - (grid / 3 ) * 2; const xStep = Math.floor((canvas.width - 12) / (grid * 2)); const yStep = Math.floor(canvas.height / grid); var count = 0; function drawRandomColour(){ ctx.fillStyle = randColor(); // simple random colour ctx.fillRect(gridX * xStep, gridY * yStep, xStep, yStep); ctx.fillStyle = randColour(); // smart random colour ctx.fillRect(gridX * xStep + canvas.width / 2, gridY * yStep, xStep, yStep); if(count < grid * grid - 1){ // fill the grid gridX += 1; // move to next grid if(gridX > grid-1){ gridX = 0; gridY += 1; gridY %= grid; } count += 1; setTimeout(drawRandomColour,1); // quickly fill grid return; // done for now } // if grid is full pick a random grid loc and request the next random colour gridY = gridX = (grid / 3); setTimeout(centerChange,250); // every quarter second } function centerChange(){ ctx.fillStyle = randColor(); // simple random colour ctx.fillRect(gridX * xStep, gridY * yStep, xStep * bigSize, yStep * bigSize); ctx.fillStyle = randColour(); // smart random colour ctx.fillRect(gridX * xStep + canvas.width / 2, gridY * yStep, xStep * bigSize, yStep * bigSize); setTimeout(centerChange,250); // every quarter second } drawRandomColour(); // start it up. Compare sequences This demo just draws the random colours as a sequence of random values. Examine the sequences to see how often you see two or more colours in a row that are hard to distinguish between. You will find that the sequence on the left has more similar sequences than the one on the right. Expand demo to full-page view to see both sequences. Click to redraw sequences. var canvas = document.createElement("canvas"); canvas.width = 1240; canvas.height = 800; var ctx = canvas.getContext("2d"); document.body.appendChild(canvas); var randColour = (function(){ // as singleton so closure can track last colour var lastHue = 0; const minChange = 360 / 3; // min hue change var lastVal = false; // for low high saturation and luminance const randomAmount = 360 / 3; // amount of randomness const minVal = 20; // how far from full black white to stay (percent) const minSat = 80; // how far from grey to stay (percent) return function(){ var hueChange = Math.random() * randomAmount ; hueChange *= Math.random() < 0.5 ? -1 : 1; // randomly move forward or backward lastHue += 360 + hueChange + minChange; // move away from last colour lastHue %= 360; // ensure colour is in valid range var sat = (Math.random() * (100 - minSat)) + minSat; // get saturation (amount of colour) var val = (Math.random() * (50 - minVal * 2)) * 1.5; // get value (luminance) // switch between high and low and luminance lastVal = ! lastVal; if(lastVal){ val = minVal + val; } else { val = 100 - minVal - val; } return "hsl(" + lastHue.toFixed(0) + "," + sat.toFixed(0) + "%," + val.toFixed(0) + "%)"; } })(); // call singleton function randColor(){ return "#" + (Math.floor(Math.random() * 0x1000000) + 0x1000000) .toString(16) .substr(1); } const grid = 32; var gridX = 0; var gridY = 0; const xStep = Math.floor((canvas.width - 12) / (grid * 2)); const yStep = Math.floor(canvas.height / grid); var count = 0; function drawRandomColour(){ ctx.fillStyle = randColor(); // simple random colour ctx.fillRect(gridX * xStep, gridY * yStep, xStep, yStep); ctx.fillStyle = randColour(); // smart random colour ctx.fillRect(gridX * xStep + canvas.width / 2, gridY * yStep, xStep, yStep); if(count < grid * grid - 1){ // fill the grid gridX += 1; // move to next grid if(gridX > grid-1){ gridX = 0; gridY += 1; gridY %= grid; } count += 1; setTimeout(drawRandomColour,1); // quickly fill grid return; // done for now } } drawRandomColour(); // start it up. // redraw on click canvas.addEventListener("click",function(){ if(count >= grid * grid - 1){ gridX = gridY = count = 0; drawRandomColour(); }else { gridX = gridY = count = 0; } }); Summary Though the second random function is not perfect, It can from time to time get colours that within a context appear similar this occurs much less than by pure random. When it is important for the user to notice a visual changes the best approch is to cycle two complementary colours (hue + 180) with one having a high luminance and the other a low. The CSS hsl colour string makes it easy to pick luminance and hue and when you want random colours gives you better control.
The problem is in converting number to hex color string. So you can write a function that converts random number to valid hex color (see what this function does): function convertToColor(num){ return '#' + ('00000' + (num | 0).toString(16)).substr(-6); } and then just use it in chColor function: function chColor() { number = Math.floor(Math.random() * 100000 + 1); color = convertToColor(number); return color; } Here is the link with working example: jsfiddle Also it makes sense to rename function to getRandomColor: function getRandomColor(){ number = Math.floor(Math.random() * 100000 + 1); return convertToColor(number); }
Please check updated fiddle I just changed your chColor function on this: function chColor() { color = "#" + ((1 << 24) * Math.random() | 0).toString(16); return color; }
I have updated the fiddle. Link https://jsfiddle.net/vpzd7ye6/2/ I made the following change to your change color function; function chColor() { hex = Math.floor(Math.random() * 1000000) + 1; color = '' + '#' + hex + ''; return color; }
Auto adjust brightness/contrast to read text from images
I was wondering if anyone can point me in the right direction to auto adjust brightness/contrast of an image taken from phone camera using javascript to make reading of text from the image easier. Appreciate any help, Many thanks.
To automatically adjust an image we could use a histogram that we generate from the image, and then use a threshold to find a black/white point to use to scale the pixel values to their max in opposite ends. In HTML5 we would need to use the canvas element in order to read pixel information. #Building a histogram A histogram is an overview of which values are most represented in an image. For brightness-contrast we would be interested in the luma value (the perceived lightness of a pixel). Example luma histogram To calculate a luma value we can use REC.709 (AKA BT.709, recommended, used here) or REC.601 formulas. Y = 0.299 * R + 0.587 * G + 0.114 * B We need to convert this to an integer (iluma = Math.round(luma);), otherwise we would get a hard time building the histogram which is based on integer values [0, 255] for storage (see example code below). The strategy to determine which range to use can vary, but for simplicity we can choose a threshold strategy based on a minimum representation of pixels in both end. Red line showing example threshold To find the darkest based on a threshold we would scan from left to right and when we get a luma value above threshold use that as minimum value. If we get to center (or even just 33% in) we could abort and default to 0. For the brightest we would do the same but from right to left and defaulting to 255 if no threshold is found. You can of course use different threshold values for each end - it's all a game of trial-and-error with the values until you find something that suits your scenario. We should now have two values representing the min-max range: Min-max range based on threshold #Scaling the general luma level First calculate the scale factor we need to use based on the min-max range: scale = 255 / (max - min) * 2 We will always subtract min from each component even if that means it will clip (if < 0 set the value to 0). When subtracted we scale each component value using the scale factor. The x2 at the end is to compensate for the variations between luma and actual RGB values. Play around with this value like the others (here just an arbitrary example). We do this for each component in each pixel (0-clip and scale): component = max(0, component - min) * scale When the image data is put back the contrast should be max based on the given threshold. #Tips You don't have to use the entire image bitmap to analyze the histogram. If you deal with large image sources scale down to a small representation - you don't need much as we're after the brightest/darkest areas and not single pixels. You can brighten and add contrast an image using blending modes with it self, such as multiply, lighten, hard-light/soft-light etc. (<= IE11 does not support blending modes). Adjust the formula for these, and just experiment. #Example This works on a buffer showing the techniques described above. There exist more complex and accurate methods, but this is given as a proof-of-concept (licensed under CC-3.0-by-sa, attribution required). It starts out with a 10% threshold value. Use slider to see the difference in result using the threshold. The threshold can be calculated via other methods than the one shown here. Experiment! Run snippet using Full page - var ctx = c.getContext("2d"), img = new Image; // some demo image img.crossOrigin =""; // needed for demo img.onload = setup; img.src = "//i.imgur.com/VtNwHbU.jpg"; function setup() { // set canvas size based on image c.width = this.width; c.height = this.height; // draw in image to canvas ctx.drawImage(this, 0, 0); // keep the original for comparsion and for demo org.src = c.toDataURL(); process(this, +tv.value); } function process(img, thold) { //thold = % of hist max var width = img.width, height = img.height, idata, data, i, min = -1, max = -1, // to find min-max maxH = 0, // to find scale of histogram scale, hgram = new Uint32Array(width); // histogram buffer (or use Float32) // get image data idata = ctx.getImageData(0, 0, img.width, img.height); // needed for later data = idata.data; // the bitmap itself // get lumas and build histogram for(i = 0; i < data.length; i += 4) { var luma = Math.round(rgb2luma(data, i)); hgram[luma]++; // add to the luma bar (and why we need an integer) } // find tallest bar so we can use that to scale threshold for(i = 0; i < width; i++) { if (hgram[i] > maxH) maxH = hgram[i]; } // use that for threshold thold *= maxH; // find min value for(i = 0; i < width * 0.5; i++) { if (hgram[i] > thold) { min = i; break; } } if (min < 0) min = 0; // not found, set to default 0 // find max value for(i = width - 1; i > width * 0.5; i--) { if (hgram[i] > thold) { max = i; break; } } if (max < 0) max = 255; // not found, set to default 255 scale = 255 / (max - min) * 2; // x2 compensates (play with value) out.innerHTML = "Min: " + min + " Max: " + max + " Scale: " + scale.toFixed(1) + "x"; // scale all pixels for(i = 0; i < data.length; i += 4) { data[i ] = Math.max(0, data[i] - min) * scale; data[i+1] = Math.max(0, data[i+1] - min) * scale; data[i+2] = Math.max(0, data[i+2] - min) * scale; } ctx.putImageData(idata, 0, 0) } tv.oninput = function() { v.innerHTML = (tv.value * 100).toFixed(0) + "%"; ctx.drawImage(img, 0, 0); process(img, +tv.value) }; function rgb2luma(px, pos) { return px[pos] * 0.299 + px[pos+1] * 0.587 + px[pos+2] * 0.114 } <label>Threshold: <input id=tv type=range min=0 max=1 step= 0.01 value=0.1></label> <span id=v>10%</span><br> <canvas id=c></canvas><br> <div id=out></div> <h3>Original:</h3> <img id=org>