Auto adjust brightness/contrast to read text from images - javascript

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>

Related

Getting X and Y coordinates from tile ID

I'm stumped on what is probably some pretty simple math. I need to get the X and Y coordinates from each tiles referenced ID. The grid below shows the order the ids are generated in. Each tile has a width and height of 32. Number ones x & y would be equal to (0,0). This is for a game I'm starting to make with canvas using a tileset.
1|2|3
4|5|6
7|8|9
So far for X, I've come up with...
(n % 3) * 32 - 32 // 3 is the width of the source image divded by 32
And for Y...
(n / 3) * 32
This is obviously wrong, but It's the closest I've come, and I don't think I'm too far off from the actual formula.
Here is my actual code so far:
function startGame() {
const canvas = document.getElementById("rpg");
const ctx = canvas.getContext("2d");
const tileSet = new Image();
tileSet.src = "dungeon_tiles.png";
let map = {
cols: 10,
rows: 10,
tsize: 32,
getTileX: function(counter, tiles) {
return ((tiles[counter] - 1) % 64) * 32;
},
getTileY: function(counter, tiles) {
return ((tiles[counter] - 1) / 64) * 32;
}
};
let counter = 0;
tileSet.onload = function() {
for (let c = 0; c < map.cols; c++) {
for (let r = 0; r < map.rows; r++) {
let x = map.getTileX(counter, mapObj.layers[0].data); // mapObj.layers[0].data is the array of values
let y = map.getTileY(counter, mapObj.layers[0].data);
counter += 1;
ctx.drawImage(
tileSet, // image
x, // source x
y, // source y
map.tsize, // source width
map.tsize, // source height
r * map.tsize, // target x
c * map.tsize, // target y
map.tsize, // target width
map.tsize // target height
);
}
}
};
}
If 1 is (0,0) and each tile is 32*32, then finding your horizontal position is a simple 32*(t-1) where t is your tile number. t-1 because your tiles start from 1 instead of 0. Now, you have 3 tiles per row so you want to reset every 3, so the final formula for your x is 32*((t-1)%3).
For the vertical position it's almost the same, but you want to increase your position by 32 only once every 3 tiles, so this is your y: 32*floor((t-1)/3).
floor((t-1)/3) is simply integer division since the numbers are always positive.
If I understand this correctly, you want to get the 1|2|3 values based on x, y correct? You can do something like this:
((y * total # of rows) + x) + 1
This would convert the 2D x, y index to a single index which is, as you stated, 1|2|3. This formula is based on your example where count starts at 1 and not 0. If you want to convert it to 0 base, just remove the + 1.
If you have the width and height, or probably location of input/character, you can have a GetX(int posX) and GetY(int posY) to get the x and y based on the position. Once you have converted the position to x, y values, use the formula above.
int GetX(int posX)
{
return (posX / 32);
}
int GetY(int posY)
{
return (posY / 32);
}
int GetIndex(int posX, int posY)
{
return ((GetY(posY) / totalRows) + GetX(posX)) + 1;
}

Smooth jagged pixels

I've created a pinch filter/effect on canvas using the following algorithm:
// iterate pixels
for (var i = 0; i < originalPixels.data.length; i+= 4) {
// calculate a pixel's position, distance, and angle
var pixel = new Pixel(affectedPixels, i, origin);
// check if the pixel is in the effect area
if (pixel.dist < effectRadius) {
// initial method (flawed)
// iterate original pixels and calculate the new position of the current pixel in the affected pixels
if (method.value == "org2aff") {
var targetDist = ( pixel.dist - (1 - pixel.dist / effectRadius) * (effectStrength * effectRadius) ).clamp(0, effectRadius);
var targetPos = calcPos(origin, pixel.angle, targetDist);
setPixel(affectedPixels, targetPos.x, targetPos.y, getPixel(originalPixels, pixel.pos.x, pixel.pos.y));
} else {
// alternative method (better)
// iterate affected pixels and calculate the original position of the current pixel in the original pixels
var originalDist = (pixel.dist + (effectStrength * effectRadius)) / (1 + effectStrength);
var originalPos = calcPos(origin, pixel.angle, originalDist);
setPixel(affectedPixels, pixel.pos.x, pixel.pos.y, getPixel(originalPixels, originalPos.x, originalPos.y));
}
} else {
// copy unaffected pixels from original to new image
setPixel(affectedPixels, pixel.pos.x, pixel.pos.y, getPixel(originalPixels, pixel.pos.x, pixel.pos.y));
}
}
I've struggled a lot to get it to this point and I'm quite happy with the result. Nevertheless, I have a small problem; jagged pixels. Compare the JS pinch with Gimp's:
I don't know what I'm missing. Do I need to apply another filter after the actual filter? Or is my algorithm wrong altogether?
I can't add the full code here (as a SO snippet) because it contains 4 base64 images/textures (65k chars in total). Instead, here's a JSFiddle.
One way to clean up the result is supersampling. Here's a simple example: https://jsfiddle.net/Lawmo4q8/
Basically, instead of calculating a single value for a single pixel, you take multiple value samples within/around the pixel...
let color =
calcColor(x - 0.25, y - 0.25) + calcColor(x + 0.25, y - 0.25) +
calcColor(x - 0.25, y + 0.25) + calcColor(x + 0.25, y + 0.25);
...and merge the results in some way.
color /= 4;

CSS contrast filter

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

HTML5 Canvas : Spawn randomly positioned objects outside of the canvas

What I am trying to learn is how to spawn objects outside of the canvas from various directions...that is left, right, top, bottom.
For (i = 0; i < 10; i++) {
ctx.beginPath();
ctx.arc(Math.random()*Window.outerWidth, Math.random()*Window.outerHeight 30, 0, 2 * Math.PI, false);
ctx.fillStyle = 'black';
ctx.fill();
ctx.closePath();
}
So basically imagine objects appearing from some random positions at the top, left, right and botton edges of the canvas screen. Maybe even the corners. So the point is im having some issues with understanding the logic behind how this works. How do I achieve something like that?
Please bear in mind that I am not just looking for answers but a resource for learning. If you answer then please do so with teaching in mind and not 'points'.
This Math.random() * Window.outerWidth is fast write of :
var min = 0, max = Window.outerWidth;
Math.random() * (max - min + 1) + min;
To understand it, look this function :
function getRandom(min, max) { //You get number between [min, max]
return Math.floor(Math.random() * (max - min + 1)) + min;
}
If you want to add circle inside canvas, you need to set :
var min = 0,
max = Window.outerWidth;
Your code remove min variable. You get :
Math.random() * (max - 0) + 0; // replace min by 0
Math.random() * max // remove 0
Math.random() * Window.outerWidth // max = Window.outerWidth
You get : Math.random() * Window.outerWidth.
Links :
Generating random whole numbers in JavaScript in a specific range?

Mirroring right half of webcam image

I saw that you have helped David with his mirroring canvas problem before. Canvas - flip half the image
I have a similar problem and hope that maybe you could help me.
I want to apply the same mirror effect on my webcam-canvas, but instead of the left side, I want to take the RIGHT half of the image, flip it and apply it to the LEFT.
This is the code you've posted for David. It also works for my webcam cancas. Now I tried to change it, so that it works for the other side, but unfortunately I'm not able to get it.
for(var y = 0; y < height; y++) {
for(var x = 0; x < width / 2; x++) { // divide by 2 to only loop through the left half of the image.
var offset = ((width* y) + x) * 4; // Pixel origin
// Get pixel
var r = data[offset];
var g = data[offset + 1];
var b = data[offset + 2];
var a = data[offset + 3];
// Calculate how far to the right the mirrored pixel is
var mirrorOffset = (width - (x * 2)) * 4;
// Get set mirrored pixel's colours
data[offset + mirrorOffset] = r;
data[offset + 1 + mirrorOffset] = g;
data[offset + 2 + mirrorOffset] = b;
data[offset + 3 + mirrorOffset] = a;
}
}
Even if the accepted answer you're relying on uses imageData, there's absolutely no use for that.
Canvas allows, with drawImage and its transform (scale, rotate, translate), to perform many operations, one of them being to safely copy the canvas on itself.
Advantages is that it will be way easier AND way way faster than handling the image by its rgb components.
I'll let you read the code below, hopefully it's commented and clear enough.
The fiddle is here :
http://jsbin.com/betufeha/2/edit?js,output
One output example - i took also a mountain, a Canadian one :-) - :
Original :
Output :
html
<canvas id='cv'></canvas>
javascript
var mountain = new Image() ;
mountain.onload = drawMe;
mountain.src = 'http://www.hdwallpapers.in/walls/brooks_mountain_range_alaska-normal.jpg';
function drawMe() {
var cv=document.getElementById('cv');
// set the width/height same as image.
cv.width=mountain.width;
cv.height = mountain.height;
var ctx=cv.getContext('2d');
// first copy the whole image.
ctx.drawImage(mountain, 0, 0);
// save to avoid messing up context.
ctx.save();
// translate to the middle of the left part of the canvas = 1/4th of the image.
ctx.translate(cv.width/4, 0);
// flip the x coordinates to have a mirror effect
ctx.scale(-1,1);
// copy the right part on the left part.
ctx.drawImage(cv,
/*source */ cv.width/2,0,cv.width/2, cv.height,
/*destination*/ -cv.width/4, 0, cv.width/2, cv.height);
// restore context
ctx.restore();
}

Categories

Resources