HTML5 Canvas image contrast
HTML5 Canvas image contrast
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.
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:
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.
Answer by karlphillip for HTML5 Canvas image contrast
You can take a look at the OpenCV docs to see how you could accomplish this: Brightness and contrast adjustments.
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(y,x)[c] = saturate_cast( alpha*( image.at(y,x)[c] ) + beta ); } } }
which I imagine you are capable of translating to javascript.
Answer by Schahriar SaffarShargh for HTML5 Canvas image contrast
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!
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:
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.
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)
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]
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 ) :
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; } }
Answer by Jay for HTML5 Canvas image contrast
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.
(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
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.
So, if a pixel had an input value of 3, 1, 0 for RGB, the output pixel would be 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.
Answer by Brian for HTML5 Canvas image contrast
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!
For additional information about the algorithm, read this article and 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
Usage:
var newImageData = contrastImage(imageData, 30);
Hopefully this will be a time-saver for someone. Cheers!
Answer by fforgoso for HTML5 Canvas image contrast
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!
Answer by Escher for HTML5 Canvas image contrast
This javascript implementation complies with the SVG/CSS3 definition of "contrast" (and the following code will render your canvas image identically):
/*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 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; }
Answer by brichins for HTML5 Canvas image contrast
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
Derivation similar to the below; this version is mathematically the same, but runs much faster.
Original answer
Here is a simplified version with explanation of 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
Notes
I have chosen to use a
contrast
range of+/- 100
instead 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.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 aUint8ClampedArray
the range checking is handled for you:
"if you specified a value that is out of the range of [0,255], 0 or 255 will be set instead."
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.
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).
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.
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
canvas {width: 100px; height: 100px} div {text-align:center; width:120px; float:left}
-98% -50% Original +50% +98%
Round-trip
(-98%, +98%) Round-trip
(-50%, +50%) Round-trip
(0% 100x) Round-trip
(+50%, -50%) Round-trip
(+98%, -98%)
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.)
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.
The problem the algorithm solves then is to:
- Chose a constant factor to adjust contrast by
- For each color channel of each pixel, scale "contrast" (distance from average) by that constant factor
Or, as hinted at in the CSS spec, simply choosing the slope and intercept of a line:
Note the term type='linear'
; we are doing linear contrast adjustment in RGB color space, as opposed to a quadratic scaling function, luminence-based adjustment, or histogram matching.
If you recall from geometry class, the formula for a line is y=mx+b
. y
is the final value we are after, the slope m
is the contrast (or factor
), x
is the initial pixel value, and b
is 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 a
is the x-offset shifting the line horizontally.
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). x
is our original value, so all we need is to figure out the slope m
and x-offset a
.
First, the slope m
is "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:
- 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
0
should result in no change between input and output; that is, a 1:1 slope.
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.
So now for y=m(x-a)+b
, we know everything except m
. Recall two more important points from geometry class:
- Lines have the same slope even if their location changes; that is,
m
stays the same regardless of the values ofa
andb
. - The slope of a line can be found using any 2 points on the line
To simplify the slope discussion, let's move the coordinate origin to the x-intercept (-128) and ignore a
and b
for 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).
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 x
and y
directions on our graph.
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, -y
and -x, +y
as shown.
The steeper line (-x, +y) is associated with a positive contrast
adjustment; 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 contrast
will 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).
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:
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) + 128
QED
The math-minded will notice that the resulting m
or factor
is a scalar (unitless) value; you can use any range you want for contrast
as long as it matches the constant (255
) in the factor
calculation. For example, a contrast
range of +/-100
and factor = (100 + contrast)/(100.01 - contrast)
, which is was I really use to eliminate the step of scaling to 255; I just left 255
in the code at the top to simplify the explanation.
Note about the "magic" 259
The source article uses a "magic" 259, although the author admits he doesn't remember why:
"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 factor
calculation, 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.
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. :)
Fatal error: Call to a member function getElementsByTagName() on a non-object in D:\XAMPP INSTALLASTION\xampp\htdocs\endunpratama9i\www-stackoverflow-info-proses.php on line 72
0 comments:
Post a Comment