I am trying to use Javascript to find the darkest region of an image.
So far, this is what I have:
https://jsfiddle.net/brampower/bv78rmz8/
function rgbToHsl(r, g, b) {
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b),
min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return ({
h: h,
s: s,
l: l,
})
}
function solve_darkest(url, callback) {
var image = new Image();
image.src = url;
image.onload = function(){
var canvas = document.createElement('canvas');
canvas.width = 300;
canvas.height = 300;
var context = canvas.getContext("2d");
context.drawImage(image, 0, 0);
var imgData = context.getImageData(0, 0, 300, 300);
var pixel = 0;
var darkest_pixel_lightness = 100;
var darkest_pixel_location = 0;
for (var i = 0; i < imgData.data.length; i += 4) {
red = imgData.data[i + 0];
green = imgData.data[i + 1];
blue = imgData.data[i + 2];
alpha = imgData.data[i + 3];
var hsl = rgbToHsl(red, green, blue);
var lightness = hsl.l;
if (lightness < darkest_pixel_lightness) {
darkest_pixel_lightness = lightness;
darkest_pixel_location = pixel;
}
pixel++;
}
var y = Math.floor(darkest_pixel_location/200);
var x = darkest_pixel_location-(y*200);
callback(x,y);
};
}
image_url = 'http://i.imgur.com/j6oJO8s.png';
solve_darkest(image_url, function(x, y) {
alert('x: '+x+' y: '+y);
});
It won't work in JSFiddle because of the tainted canvas, but hopefully that will give you an idea. For the sample image, my JS is currently returning the following coordinates:
x: 140 y: 117
These are not the correct coordinates. The darkest pixel of this image should be around the following coordinates:
x: 95 y: 204
I just can't figure out why the coordinates are so off. Anyone here that would be willing to shed some light on what I'm doing wrong?
Ok, I just tested your jsfiddle.
For the tainted canvas just change crossOrigin property:
var image = new Image();
image.crossOrigin = "Anonymous";
For the incorrect pixel, there are several problems.
Incorrect canvas size. If the image is smaller than the canvas size, the algorithm tests pixels which are not in the image, but are in the canvas. Since you don't drop the pixels which are transparent, you also test the 0, 0, 0 (RGB) pixel which is supposed to be black #000000.
Incorrect 1-dimensional array to 2-dimensional transformation. The formula you are using is incorrect, because you set the width and height to 300, but use 200 in the formula. I suggest creating a variable and using that as a reference.
If you doubt that the pixel is exactly there, create a small picture, like 5x5 px size and check if the algorithm returns what you expect.
I updated the jsfiddle, I think this is correct now. Also, removed the img element in HTML and just appended the canvas to the body: https://jsfiddle.net/Draznel/597u5h0c/1/
Without the JSFiddle working, my best guess would be that the logic in
var y = Math.floor(darkest_pixel_location/200);
var x = darkest_pixel_location-(y*200);
is incorrect for two reasons.
1) The image is 300 pixels in width/height, not 200
2) The imagedata is ordered by x first and y second
To get the correct x and y coordinates, I think the following code would work:
var x = Math.floor(darkest_pixel_location / imageWidth);
var y = darkest_pixel_location % imageWidth;
A good time to realise the nasty consequences of hard-coded variables...
You're creating a canvas of 300x300 and drawing a png of the same dimensions to it. Unfortunately, you then use 200 to determine the x,y pos given the index of the selected pixel. Hint: make a 300x300 image that's white. Set a single pixel at (95,204) to black. Run your unmodified code and you'll get 95,306. Change to same as image size and you'll get (95,204) as your answer.
So, you can hardly complain that the index into a 300x300 image returns the wrong position when manipulated in a way that would be appropriate for the index into a 200x200 image.
Personally, I'd replace:
var y = Math.floor(darkest_pixel_location/200);
var x = darkest_pixel_location-(y*200);
with
var y = Math.floor(darkest_pixel_location/this.width);
var x = darkest_pixel_location-(y*this.width);
That will then convert an index back into correct x,y coordinates.
That said, the image you've provided actually doesn't have it's darkest spot at the indicated location. The pixel at 95,204 has the value of #37614e, or rgb(55,97,78).
Thus, I hope you can now see the purpose of my suggestion of a single dark pixel in an otherwise light image. You can reduce the number of issues you're trying to debug to one at a time. In this case - "can I convert an index back into 2d coordinates?". Once done, "do I have a dark spot actually where I think it is?"
In your case - the answer to both of these questions was no! Not exactly the most advantageous place from which to start debugging...
some comments were made and a better understanding of the problem at hand gained.
Okay, as per the discussion in the comments - the task is to find the upper-left corner of a darkened region in an image - such a search should average results over an (as yet) unknown area such that local minimums or maximums (dark or light pixels) will not adversely affect the identified area of interest.
Ideally, one may run the code iteratively - try with a block-size of 1, then a block-size of 2, etc, etc, increasing the block-size until the results from two runs are the same or within a certain limit.
I.e if I search with a block-size of 9 and get the location 93,211 and then get the same with a block-size of 10 (whereas all previous block-size values returned a different result) then I'd probably feel fairly confident I'd correctly identified the area of interest.
Here's some code to chew on. You'll notice I've left your function and created another, very similar one. I hope you'll find it suitable. :)
"use strict";
function newEl(tag){return document.createElement(tag)}
function newTxt(txt){return document.createTextNode(txt)}
function byId(id){return document.getElementById(id)}
function allByClass(clss,parent){return (parent==undefined?document:parent).getElementsByClassName(clss)}
function allByTag(tag,parent){return (parent==undefined?document:parent).getElementsByTagName(tag)}
function toggleClass(elem,clss){elem.classList.toggle(clss)}
function addClass(elem,clss){elem.classList.add(clss)}
function removeClass(elem,clss){elem.classList.remove(clss)}
function hasClass(elem,clss){elem.classList.contains(clss)}
// useful for HtmlCollection, NodeList, String types
function forEach(array, callback, scope){for (var i=0,n=array.length; i<n; i++)callback.call(scope, array[i], i, array);} // passes back stuff we need
// callback gets data via the .target.result field of the param passed to it.
function loadFileObject(fileObj, loadedCallback){var a = new FileReader();a.onload = loadedCallback;a.readAsDataURL( fileObj );}
function ajaxGet(url, onLoad, onError)
{
var ajax = new XMLHttpRequest();
ajax.onload = function(){onLoad(this);}
ajax.onerror = function(){console.log("ajax request failed to: "+url);onError(this);}
ajax.open("GET",url,true);
ajax.send();
}
function ajaxPost(url, phpPostVarName, data, onSucess, onError)
{
var ajax = new XMLHttpRequest();
ajax.onload = function(){ onSucess(this);}
ajax.onerror = function() {console.log("ajax request failed to: "+url);onError(this);}
ajax.open("POST", url, true);
ajax.setRequestHeader("Content-type","application/x-www-form-urlencoded");
ajax.send(phpPostVarName+"=" + encodeURI(data) );
}
function ajaxPostForm(url, formElem, onSuccess, onError)
{
var formData = new FormData(formElem);
ajaxPostFormData(url, formData, onSuccess, onError)
}
function ajaxPostFormData(url, formData, onSuccess, onError)
{
var ajax = new XMLHttpRequest();
ajax.onload = function(){onSuccess(this);}
ajax.onerror = function(){onError(this);}
ajax.open("POST",url,true);
ajax.send(formData);
}
function getTheStyle(tgtElement)
{
var result = {}, properties = window.getComputedStyle(tgtElement, null);
forEach(properties, function(prop){result[prop] = properties.getPropertyValue(prop);});
return result;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
window.addEventListener('load', onDocLoaded, false);
function onDocLoaded(evt)
{
// var image_url = 'http://i.imgur.com/j6oJO8s.png';
// var image_url = 'onePixel.png';
var image_url = 'j6oJO8s.png';
byId('theImage').src = image_url;
solve_darkest(image_url, function(x,y){alert('x: '+x+' y: '+y);} );
solve_darkest_2(image_url, function(x,y){alert('x: '+x+' y: '+y);} );
}
function rgbToHsl(r, g, b)
{
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b),
min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return ({
h: h,
s: s,
l: l,
})
}
function solve_darkest(url, callback)
{
var image = new Image();
image.src = url;
image.onload = function()
{
var canvas = document.createElement('canvas');
canvas.width = 300;
canvas.height = 300;
var context = canvas.getContext("2d");
context.drawImage(image, 0, 0);
var imgData = context.getImageData(0, 0, 300, 300);
var pixel = 0;
var darkest_pixel_lightness = 100;
var darkest_pixel_location = 0;
for (var i = 0; i < imgData.data.length; i += 4)
{
var red = imgData.data[i + 0];
var green = imgData.data[i + 1];
var blue = imgData.data[i + 2];
var alpha = imgData.data[i + 3];
var hsl = rgbToHsl(red, green, blue);
var lightness = hsl.l;
if (lightness < darkest_pixel_lightness)
{
darkest_pixel_lightness = lightness;
darkest_pixel_location = pixel;
console.log("Darkest found at index: " + pixel);
}
pixel++;
}
// var y = Math.floor(darkest_pixel_location/200);
// var x = darkest_pixel_location-(y*200);
var y = Math.floor(darkest_pixel_location/this.width);
var x = darkest_pixel_location-(y*this.width);
callback(x,y);
};
}
function solve_darkest_2(url, callback)
{
var image = new Image();
image.src = url;
image.onload = function()
{
var canvas = document.createElement('canvas');
canvas.width = 300;
canvas.height = 300;
var context = canvas.getContext("2d");
context.drawImage(image, 0, 0);
var imgData = context.getImageData(0,0, canvas.width, canvas.height);
var darkest_pixel_luminance = 100;
var darkest_pixel_xPos = 0;
var darkest_pixel_yPos = 0;
for (var y=0; y<canvas.height; y++)
{
for (var x=0; x<canvas.width; x++)
{
var luminance = averagePixels(imgData, x, y, 10);
if (luminance < darkest_pixel_luminance)
{
darkest_pixel_luminance = luminance;
darkest_pixel_xPos = x;
darkest_pixel_yPos = y;
}
}
}
callback(darkest_pixel_xPos,darkest_pixel_yPos);
};
}
function averagePixels(imgData, xPos, yPos, averagingBlockSize)
{
// var ctx = canvas.getContext("2d");
// var imgData = ctx.getImageData( 0, 0, canvas.width, canvas.height );
// imgData
// we average pixels found in a square region, we need to know how many pixels
// are in the region to divide the accumalated totals by the number of samples (pixels) in the
// averaging square
var numPixelsMax = averagingBlockSize * averagingBlockSize;
var numPixelsActual = 0;
var red, green, blue;
red = green = blue = 0;
var rowStride = imgData.width * 4; // add this to an index into the canvas's data to get the pixel
// immediatelly below it.
var x, y;
var initialIndex = ((yPos * imgData.width) + xPos) * 4;
var index = initialIndex;
var pixel = 0;
var darkest_pixel_lightness = 100;
var darkest_pixel_location = 0;
for (y=0; y<averagingBlockSize; y++)
{
index = initialIndex + y * rowStride;
for (x=0; x<averagingBlockSize; x++)
{
if ((x+xPos < imgData.width) && (y+yPos < imgData.height))
{
red += imgData.data[index+0];
green += imgData.data[index+1];
blue += imgData.data[index+2];
numPixelsActual++;
}
index += 4;
}
}
red /= numPixelsActual;
green /= numPixelsActual;
blue /= numPixelsActual;
var hsl = rgbToHsl(red, green, blue);
var luminance = hsl.l;
return luminance;
}
img
{
border: solid 1px red;
}
<h1>300px</h1>
<img id='theImage'/>
Related
Am trying to calculate width and height of object i loaded into canvas. When object is not rotated i get correct left right top bottom values, but when i load rotated object in canvas then i not get correct values , so i wonder what will be the logic or math formula to do achieve it.
how am doing.
initially load image into canvas
get image data from canvas
loop through image data to get only colored pixels by using alpha check
from colored pixel array find min max xy values
var temp_ray = []; // pixel array
for (var y = 0; y < imgData.height; ++y) {
for (var x = 0; x < imgData.width; ++x) {
var index = (y * imgData.width + x) * 4;
if(imgData.data[index+3]){
var xc = (index / 4) % imgData.width;
var yc = Math.floor((index / 4) / imgData.width);
temp_ray.push([xc,yc]);
}
}
}
if(temp_ray.length > 0){
var Xind = MaxMin2darray(temp_ray,0);
var Yind = MaxMin2darray(temp_ray,1);
var W = parseFloat(Xind['max']) - parseFloat(Xind['min']);
var H = parseFloat(Yind['max']) - parseFloat(Yind['min']);
var center_x = Xind['min'] + (W/2);
var center_y = Yind['min'] + (H/2);
// find corners of object
// find *min x , min y
let top_left = temp_ray[Xind['imin']]; // min X priority , min Y // top left
// find max x , *min y
let top_right = temp_ray[Yind['imin']]; // max X, min Y priority , // top right
// find *max x , min y
let bot_right = temp_ray[Xind['imax']]; // max X priority , min Y // bottom right
// find max x , *max y
let bot_left = temp_ray[Yind['imax']]; // max X , max Y priority // bottom left
var dim = {'W':W,'H':H,'CenterX':center_x,'CenterY':center_y,'top_left':top_left,'top_right':top_right,'bot_right':bot_right,'bot_left':bot_left,'Xend':Xind['max'],'Yend':Yind['max'],'Xstart':Xind['min'],'Ystart':Yind['min'],'Xend':Xind['max'],'Yend':Yind['max']};
console.log(dim);
}
and then using min max xy value find corners of object which works with none rotated objects but not work with rotated/tilted objects.
so any idea how to solve this problem
openpnp project is achieving this through opencv, but i think in js we do not have opencv library nor am that pro of java :(.
https://github.com/openpnp/openpnp/blob/develop/src/main/java/org/openpnp/vision/pipeline/stages/DrawRotatedRects.java
jsfiddle: http://jsfiddle.net/4L13vtaj/
In some simple cases (like rectangular objects), you could try to rotate the image until you minimize the number of uncolored pixels.
So you start with your image, and for each of the possible 360°, you compute the ratio. This is not perfect, but "doable" simply in pure js.
Here's a pseudoCode that might help you:
for degree in [0,365]{
rotateOriginalImageBy(degree);
cost[degree] = NemptyPixels/NfilledPixels;
}
predictedDegree = Math.min(cost);
rotateOriginalImageBy(predictedDegree);
compute 2 dimensions;
width = largerDimension;
height = shorterDimension;
Begining of an implementation (I edited your jsfiddle):
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var rotatioDegree = 45;
var imageObject = new Image();
imageObject.onload = function() {
var canvasWidth = imageObject.width;
var canvasHeight = canvasWidth; // not useful since width==height
document.getElementById('canvas').width = canvasWidth;
document.getElementById('canvas').height = canvasWidth;
ctx.clearRect(0, 0, canvasWidth, canvasWidth);
// Move registration point to the center of the canvas
ctx.translate(canvasWidth/2, canvasWidth/2)
ctx.rotate(rotatioDegree*3.1415/180);
ctx.translate(-canvasWidth/2,-canvasWidth/2)
ctx.drawImage(imageObject,0,0);
ctx.translate(canvasWidth/2, canvasWidth/2)
ctx.rotate(-rotatioDegree*3.1415/180);
ctx.translate(-canvasWidth/2,-canvasWidth/2)
var imgData = ctx.getImageData(0, 0, canvasWidth, canvasWidth);
http://jsfiddle.net/4L13vtaj/17/
If this doesn't work, you could implement some image detection techniques (Mathematical morphology for example). But i think this is outside the scope of stackoverflow.
If you work with some approximation, you can have something like that; I hope at least it can provide to you some ideas:
// some pixels in this image are not transparent, so we add a tollerance
// you can try to remove the second condition.
const isNotEmpty = (color) => color && color < 0xffaaaaaa;
function getTop(buff, w, h) {
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
let i = y * w + x;
if (isNotEmpty(buff[i])) {
return {x, y}
}
}
}
}
function getRight(buff, w, h) {
for (let x = w; x >=0; x--) {
for (let y = 0; y < h; y++) {
let i = y * w + x;
if (isNotEmpty(buff[i])) {
return {x, y}
}
}
}
}
function getBottom(buff, w, h) {
for (let y = h; y >= 0; y--) {
for (let x = 0; x < w; x++) {
let i = y * w + x;
if (isNotEmpty(buff[i])) {
return {x, y}
}
}
}
}
function getLeft(buff, w, h) {
for (let x = 0; x < w; x++) {
for (let y = 0; y < h; y++) {
let i = y * w + x;
if (isNotEmpty(buff[i])) {
return {x, y}
}
}
}
}
async function main(imageSource) {
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const imageObject = new Image();
imageObject.src = imageSource;
await new Promise(r => imageObject.onload = r);
const w = canvas.width = imageObject.width;
const h = canvas.height = imageObject.height;
ctx.clearRect(0, 0, w, h);
ctx.drawImage(imageObject, 0, 0);
const imgData = ctx.getImageData(0, 0, w, h);
const buff = new Uint32Array(imgData.data.buffer);
const points = [
getTop(buff, w, h),
getRight(buff, w, h),
getBottom(buff, w, h),
getLeft(buff, w, h)
];
ctx.strokeStyle = "#0000ff"
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
ctx.lineTo(points[1].x, points[1].y);
ctx.lineTo(points[2].x, points[2].y);
ctx.lineTo(points[3].x, points[3].y);
ctx.closePath();
ctx.stroke();
}
main(/* image's url*/);
Here the link for testing: https://codepen.io/zer0/pen/zLxyQV
There are several problem with this approach: as said, with irregular images, it's not precise, in fact you will see the pin are making the image's bounding box a little bit smaller.
But the thing can be worse: try in the link above to use the 2nd image, that is quite irregular, and you will see.
Of course we can compensate, using also a bit more complex algorithm instead this simple one, but the question is: what the expected result for something like the 2nd image? Depends by that you can decide how to proceed.
I am making a map that renders position of game objects (Project Zomboid zombies):
As user zooms out, single dots are no longer useful. Instead, I'd like to render distribution of zombies on an area using red color gradient. I tried to loop over all zombies for every rendered pixel and color it reciprocally to the sum of squared distances to the zombies. The result:
That's way too blurry. Also the results are more influenced by the zombies that are AWAY from the points - I need to influence them more by the zombies that are CLOSE. So what this is is just math. Here's the code I used:
var h = canvas.height;
var w = canvas.width;
// To loop over more than 1 pixel (performance)
var tileSize = 10;
var halfRadius = Math.floor(tileSize/2);
var time = performance.now();
// "Squared" because we didnt unsquare it
function distanceSquared(A, B) {
return (A.x-B.x)*(A.x-B.x)+(A.y-B.y)*(A.y-B.y);
}
// Loop for every x,y pixel (or region of pixels)
for(var y=0; y<h; y+=tileSize) {
for(var x=0; x<w; x+=tileSize) {
// Time security - stop rendering after 1 second
if(performance.now()-time>1000) {
x=w;y=h;break;
}
// Convert relative canvas offset to absolute point on the map
var point = canvasPixeltoImagePixel(x, y);
// For every zombie add sqrt(distance from this point to zombie)
var distancesRoot = 0;
// Loop over the zombies
var zombieCoords;
for(var i=0; i<zombies_length; i++) {
// Get single zombie coordinates as {x:0, y:0}
if((coords=zombies[i].pixel)==null)
coords = zombies[i].pixel = tileToPixel(zombies[i].coordinates[0], zombies[i].coordinates[1], drawer);
// square root is a) slow and b) probably not what I want anyway
var dist = distanceSquared(coords, point);
distancesRoot+=dist;
}
// The higher the sum of distances is, the more intensive should the color be
var style = 'rgba(255,0,0,'+300000000/distancesRoot+')';
// Kill the console immediatelly
//console.log(style);
// Maybe we should sample and cache the transparency styles since there's limited ammount of colors?
ctx.fillStyle = style;
ctx.fillRect(x-halfRadius,y-halfRadius,tileSize,tileSize);
}
}
I'm pretty fine with theoretical explanation how to do it, though if you make simple canvas example with some points, what would be awesome.
This is an example of a heat map. It's basically gradient orbs over points and then ramping the opacity through a heat ramp. The more orbs cluster together the more solid the color which can be shown as an amplified region with the proper ramp.
update
I cleaned up the variables a bit and put the zeeks in an animation loop. There's an fps counter to see how it's performing. The gradient circles can be expensive. We could probably do bigger worlds if we downscale the heat map. It won't be as smooth looking but will compute a lot faster.
update 2
The heat map now has an adjustable scale and as predicted we get an increase in fps.
if (typeof app === "undefined") {
var app = {};
}
app.zeeks = 200;
app.w = 600;
app.h = 400;
app.circleSize = 50;
app.scale = 0.25;
init();
function init() {
app.can = document.getElementById('can');
app.ctx = can.getContext('2d');
app.can.height = app.h;
app.can.width = app.w;
app.radius = Math.floor(app.circleSize / 2);
app.z = genZ(app.zeeks, app.w, app.h);
app.flip = false;
// Make temporary layer once.
app.layer = document.createElement('canvas');
app.layerCtx = app.layer.getContext('2d');
app.layer.width = Math.floor(app.w * app.scale);
app.layer.height = Math.floor(app.h * app.scale);
// Make the gradient canvas once.
var sCircle = Math.floor(app.circleSize * app.scale);
app.radius = Math.floor(sCircle / 2);
app.gCan = genGradientCircle(sCircle);
app.ramp = genRamp();
// fps counter
app.frames = 0;
app.fps = "- fps";
app.fpsInterval = setInterval(calcFps, 1000);
// start animation
ani();
flicker();
}
function calcFps() {
app.fps = app.frames + " fps";
app.frames = 0;
}
// animation loop
function ani() {
app.frames++;
var ctx = app.ctx;
var w = app.w;
var h = app.h;
moveZ();
//ctx.clearRect(0, 0, w, h);
ctx.fillStyle = "#006600";
ctx.fillRect(0, 0, w, h);
if (app.flip) {
drawZ2();
drawZ();
} else {
drawZ2();
}
ctx.fillStyle = "#FFFF00";
ctx.fillText(app.fps, 10, 10);
requestAnimationFrame(ani);
}
function flicker() {
app.flip = !app.flip;
if (app.flip) {
setTimeout(flicker, 500);
} else {
setTimeout(flicker, 5000);
}
}
function genGradientCircle(size) {
// gradient image
var gCan = document.createElement('canvas');
gCan.width = gCan.height = size;
var gCtx = gCan.getContext('2d');
var radius = Math.floor(size / 2);
var grad = gCtx.createRadialGradient(radius, radius, radius, radius, radius, 0);
grad.addColorStop(1, "rgba(255,255,255,.65)");
grad.addColorStop(0, "rgba(255,255,255,0)");
gCtx.fillStyle = grad;
gCtx.fillRect(0, 0, gCan.width, gCan.height);
return gCan;
}
function genRamp() {
// Create heat gradient
var heat = document.createElement('canvas');
var hCtx = heat.getContext('2d');
heat.width = 256;
heat.height = 5;
var linGrad = hCtx.createLinearGradient(0, 0, heat.width, heat.height);
linGrad.addColorStop(1, "rgba(255,0,0,.75)");
linGrad.addColorStop(0.5, "rgba(255,255,0,.03)");
linGrad.addColorStop(0, "rgba(255,255,0,0)");
hCtx.fillStyle = linGrad;
hCtx.fillRect(0, 0, heat.width, heat.height);
// create ramp from gradient
var ramp = [];
var imageData = hCtx.getImageData(0, 0, heat.width, 1);
var d = imageData.data;
for (var x = 0; x < heat.width; x++) {
var i = x * 4;
ramp[x] = [d[i], d[i + 1], d[i + 2], d[i + 3]];
}
return ramp;
}
function genZ(n, w, h) {
var a = [];
for (var i = 0; i < n; i++) {
a[i] = [
Math.floor(Math.random() * w),
Math.floor(Math.random() * h),
Math.floor(Math.random() * 3) - 1,
Math.floor(Math.random() * 3) - 1
];
}
return a;
}
function moveZ() {
var w = app.w
var h = app.h;
var z = app.z;
for (var i = 0; i < z.length; i++) {
var s = z[i];
s[0] += s[2];
s[1] += s[3];
if (s[0] > w || s[0] < 0) s[2] *= -1;
if (s[1] > w || s[1] < 0) s[3] *= -1;
}
}
function drawZ() {
var ctx = app.ctx;
var z = app.z;
ctx.fillStyle = "#FFFF00";
for (var i = 0; i < z.length; i++) {
ctx.fillRect(z[i][0] - 2, z[i][1] - 2, 4, 4);
}
}
function drawZ2() {
var ctx = app.ctx;
var layer = app.layer;
var layerCtx = app.layerCtx;
var gCan = app.gCan;
var z = app.z;
var radius = app.radius;
// render gradients at coords onto layer
for (var i = 0; i < z.length; i++) {
var x = Math.floor((z[i][0] * app.scale) - radius);
var y = Math.floor((z[i][1] * app.scale) - radius);
layerCtx.drawImage(gCan, x, y);
}
// adjust layer for heat ramp
var ramp = app.ramp;
// apply ramp to layer
var imageData = layerCtx.getImageData(0, 0, layer.width, layer.height);
d = imageData.data;
for (var i = 0; i < d.length; i += 4) {
if (d[i + 3] != 0) {
var c = ramp[d[i + 3]];
d[i] = c[0];
d[i + 1] = c[1];
d[i + 2] = c[2];
d[i + 3] = c[3];
}
}
layerCtx.putImageData(imageData, 0, 0);
// draw layer on world
ctx.drawImage(layer, 0, 0, layer.width, layer.height, 0, 0, app.w, app.h);
}
<canvas id="can" width="600" height="400"></canvas>
I'm trying to remove the white/gray pixels of background (a wall) from an image so that the foreground (a hand in my case) should be left.
I tried manipulating canvas pixels but the foreground extraction is not very nice because of shadow or other inconsistencies in background. I even tried with green colored background but it doesn't enhance the performance.
for (i = 0; i < imgData.width * imgData.height * 4; i += 4) {
var r = imgDataNormal.data[i + 0];
var g = imgDataNormal.data[i + 1];
var b = imgDataNormal.data[i + 2];
var a = imgDataNormal.data[i + 3];
// compare rgb levels for green and set alphachannel to 0;
selectedR = *a value 0 - 255*
selectedG = *a value 0 - 255*
selectedB = *a value 0 - 255*
if (r <= selectedR || g >= selectedG || b <= selectedB) {
a = 0;
}
}
Is there any sophisticated method or library to make background of an image transparent?
Converting the pixels from RGB to HSL and then running a threshold filter over the pixels to remove anything (by setting the pixel's alpha to be completely transparent) with a saturation less than 0.12 (measuring saturation on a scale of 0..1 - that value was found via trial-and-error).
JavaScript
function rgbToHsl( r, g, b ){
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
}else{
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h, s, l];
}
function thresholdHsl(pixels,lower,upper){
var d = pixels.data;
var createTest = function( lower, upper ){
return lower <= upper
? function(v){ return lower <= v && v <= upper; }
: function(v){ return lower <= v || v <= upper; };
}
var h = createTest( lower[0], upper[0] );
var s = createTest( lower[1], upper[1] );
var l = createTest( lower[2], upper[2] );
for (var i=0; i<d.length; i+=4) {
var hsl = rgbToHsl( d[i], d[i+1], d[i+2] );
if ( !h(hsl[0]) || !s(hsl[1]) || !l(hsl[2]) ){
d[i+3] = 0;
}
}
}
var img = new Image();
img.onload = function() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.drawImage(img,0,0);
var pixels = ctx.getImageData(0,0,canvas.width,canvas.height);
thresholdHsl(pixels,[0,0.12,0],[1,1,1]);
ctx.putImageData(pixels, 0, 0);
};
img.src = 'Hand.png';
HTML
<canvas id="myCanvas" width="638" height="475"></canvas>
Output
It could be further improved by removing noise from the picture to try to clean up the edges.
There are a few mistakes in your code :
The last line a = 0 does nothing but changing a local variable. You have to modify it directly in the array
After that, to see the result, you have to push back the pixel array to the context
Since you need (in that example) to check for white/gray background (rgb(255, 255, 255)), you need to check if the value of the pixel is superior, not inferior. And using a and operator instead of or allows you here to filter a bigger part of the shadow (which is gray so every colour component has almost the same value) without removing the hand itself, which is more coloured.
Here is an example :
var imgData = context.getImageData(0, 0, canvas.width, canvas.height);
for (i = 0; i < imgData.width * imgData.height * 4; i += 4) {
var r = imgData.data[i + 0];
var g = imgData.data[i + 1];
var b = imgData.data[i + 2];
var a = imgData.data[i + 3];
// compare rgb levels for green and set alphachannel to 0;
selectedR = 105;
selectedG = 105;
selectedB = 105;
if (r >= selectedR && g >= selectedG && b >= selectedB) {
imgData.data[i + 3] = 0;
}
}
context.putImageData(imgData, 0, 0);
That still isn't perfect because of the shadow, but it works.
for this project http://biduleohm.free.fr/ledohm/ (sorry, the user interface is in french but the code is in english) I need an angular gradient but it doesn't exists in native so I've implemented it using a linear gradient on a line and I draw the lines more and more longer to form a triangle. The result is graphically OK but the speed isn't really good (1850 ms for 125 triangles). It's in the tab [Répartition], it redraws if there is a keyup event on one of the inputs, don't be afraid of the apparent slowness, I've limited to maximum one redraw every 2000 ms.
Before I used a simple linear gradient on the whole triangle (but this doesn't match the reality) and the speed was OK, it draws thousands of triangles in less than a second. This function was used :
drawFrontLightForColor : function(x, y, w, h, color) {
var x2 = x - w;
var x3 = x + w;
var gradient = Distri.frontCanvas.createLinearGradient(x2, y, x3, y);
gradient.addColorStop(0, 'rgba(' + color + ', ' + Distri.lightEdgeAlpha + ')');
gradient.addColorStop(0.5, 'rgba(' + color + ', ' + (color == Distri.lightColors.cw ? Distri.lightCenterAlphaCw : Distri.lightCenterAlphaOther) + ')');
gradient.addColorStop(1, 'rgba(' + color + ', ' + Distri.lightEdgeAlpha + ')');
Distri.frontCanvas.fillStyle = gradient;
Distri.frontCanvas.beginPath();
Distri.frontCanvas.moveTo(x, y);
Distri.frontCanvas.lineTo(x2, (y + h));
Distri.frontCanvas.lineTo(x3, (y + h));
Distri.frontCanvas.lineTo(x, y);
Distri.frontCanvas.fill();
Distri.frontCanvas.closePath();
},
Then I switched to this function :
drawFrontLightForColor : function(x, y, w, h, centerColor, edgeColor) {
var ratio = w / h;
var tmpY;
var tmpW;
var x2;
var x3;
var gradient;
Distri.frontCanvas.lineWidth = 1;
for (var tmpH = 0; tmpH < h; tmpH++) {
tmpY = y + tmpH;
tmpW = Math.round(tmpH * ratio);
x2 = x - tmpW;
x3 = x + tmpW;
gradient = Distri.frontCanvas.createLinearGradient(x2, tmpY, x3, tmpY);
gradient.addColorStop(0, edgeColor);
gradient.addColorStop(0.5, centerColor);
gradient.addColorStop(1, edgeColor);
Distri.frontCanvas.beginPath();
Distri.frontCanvas.moveTo(x2, tmpY);
Distri.frontCanvas.lineTo(x, tmpY);
Distri.frontCanvas.lineTo(x3, tmpY);
Distri.frontCanvas.strokeStyle = gradient;
Distri.frontCanvas.stroke();
Distri.frontCanvas.closePath();
}
},
You can find the whole source here
I can't put the beginPath, stroke, closePath out of the loop because of the gradient which is changing every iteration (I've tried but it used the last gradient for every line (which, ironically, is identical to the first function...) which is understandable but not what I want).
I accept any advice (including redo the whole function and modify his caller to outsource some code) to improve the speed let's say 5x (ideally more).
I think you took the wrong way from the start : when doing so much changes of color, you have better operate at the pixel level.
So yes that could be with a webgl pixel shader, but you'll have to fight just to get the boilerplate running ok on all platform (or get a lib to do that for you).
And anyway there's a solution perfect for your need, and fast enough (a few ms) : use raw pixel data, update them one by one with the relevant function, then draw the result.
The steps to do that are :
- create a buffer same size as the canvas.
- iterate through it's pixel, keeping track of the x,y of the point.
- normalize the coordinates so they match your 'space'.
- compute the value for the normalized (x,y) out of all the data that you have.
- write a color (in my example i choose greyscale) out of that value.
- draw the whole buffer to canvas.
I did a jsfiddle, and here's the result with 4 data points :
fiddle is here :
http://jsfiddle.net/gamealchemist/KsM9c/3/
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext('2d');
var width = canvas.width,
height = canvas.height;
// builds an image for the target canvas
function buildImage(targetCanvas, valueForXY, someData) {
var width = targetCanvas.width;
var height = targetCanvas.height;
var tempImg = ctx.createImageData(width, height);
var buffer = tempImg.data;
var offset = 0;
var xy = [0, 0];
function normalizeXY(xy) {
xy[0] = xy[0] / width ;
xy[1] = xy[1] / height;
}
for (var y = 0; y < height; y++)
for (var x = 0; x < width; x++, offset += 4) {
xy[0] = x; xy[1]=y;
normalizeXY(xy);
var val = Math.floor(valueForXY(xy, someData) * 255);
buffer[offset] = val;
buffer[offset + 1] = val;
buffer[offset + 2] = val;
buffer[offset + 3] = 255;
}
ctx.putImageData(tempImg, 0, 0);
}
// return normalized (0->1) value for x,y and
// provided data.
// xy is a 2 elements array
function someValueForXY(xy, someData) {
var res = 0;
for (var i = 0; i < someData.length; i++) {
var thisData = someData[i];
var dist = Math.pow(sq(thisData[0] - xy[0]) + sq(thisData[1] - xy[1]), -0.55);
localRes = 0.04 * dist;
res += localRes;
}
if (res > 1) res = 1;
return res;
}
var someData = [
[0.6, 0.2],
[0.35, 0.8],
[0.2, 0.5],
[0.6, 0.75]
];
buildImage(canvas, someValueForXY, someData);
// ------------------------
function sq(x) {
return x * x
}
In fact the GameAlchemist's solution isn't fast or I do something really wrong. I've implemented this algo only for the top view because the front view is much more complex.
For 120 lights the top view take 100-105 ms with the old code and it take 1650-1700 ms with this code (and moreover it still lacks a few things in the new code like the color for example):
drawTopLightForColor_ : function(canvasW, canvasD, rampX, rampY, rampZ, ledsArrays, color) {
function sq(x) {
return x * x;
}
var tmpImg = Distri.topCanvasCtx.createImageData(canvasW, canvasD);
var rawData = tmpImg.data;
var ledsArray = ledsArrays[color];
var len = ledsArray.length;
var i = 0;
for (var y = 0; y < canvasD; y++) {
for (var x = 0; x < canvasW; x++, i += 4) {
var intensity = 0;
for (var j = 0; j < len; j++) {
intensity += 2 * Math.pow(
sq((rampX + ledsArray[j].x) - x) +
sq((rampZ + ledsArray[j].y) - y),
-0.5
);
}
if (intensity > 1) {
intensity = 1;
}
intensity = Math.round(intensity * 255);
rawData[i] = intensity;
rawData[i + 1] = intensity;
rawData[i + 2] = intensity;
rawData[i + 3] = 255;
}
}
Distri.topCanvasCtx.putImageData(tmpImg, 0, 0);
},
Am I doing something wrong?
Let's say this is my canvas, with an evil-looking face drawn on it. I want to use toDataURL() to export my evil face as a PNG; however, the whole canvas is rasterised, including the 'whitespace' between the evil face and canvas edges.
+---------------+
| |
| |
| (.Y. ) |
| /_ |
| \____/ |
| |
| |
+---------------+
What is the best way to crop/trim/shrinkwrap my canvas to its contents, so my PNG is no larger than the face's 'bounding-box', like below? The best way seems to be scaling the canvas, but supposing the contents are dynamic...? I'm sure there should be a simple solution to this, but it's escaping me, with much Googling.
+------+
|(.Y. )|
| /_ |
|\____/|
+------+
Thanks!
Edited (see comments)
function cropImageFromCanvas(ctx) {
var canvas = ctx.canvas,
w = canvas.width, h = canvas.height,
pix = {x:[], y:[]},
imageData = ctx.getImageData(0,0,canvas.width,canvas.height),
x, y, index;
for (y = 0; y < h; y++) {
for (x = 0; x < w; x++) {
index = (y * w + x) * 4;
if (imageData.data[index+3] > 0) {
pix.x.push(x);
pix.y.push(y);
}
}
}
pix.x.sort(function(a,b){return a-b});
pix.y.sort(function(a,b){return a-b});
var n = pix.x.length-1;
w = 1 + pix.x[n] - pix.x[0];
h = 1 + pix.y[n] - pix.y[0];
var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h);
canvas.width = w;
canvas.height = h;
ctx.putImageData(cut, 0, 0);
var image = canvas.toDataURL();
}
If I understood well you want to "trim" away all the surronding your image / drawing, and adjust the canvas to that size (like if you do a "trim" command in Photoshop).
Here is how I'll do it.
Run thru all the canvas pixels checking if their alpha component is > 0 (that means that something is drawn in that pixel). Alternativelly you could check for the r,g,b values, if your canvas background is fullfilled with a solid color, for instance.
Get te coordinates of the top most left pixel non-empty, and same for the bottom most right one. So you'll get the coordinates of an imaginay "rectangle" containing the canvas area that is not empty.
Store that region of pixeldata.
Resize your canvas to its new dimensions (the ones of the region we got at step 2.)
Paste the saved region back to the canvas.
Et, voilá :)
Accesing pixeldata is quite slow depending on the size of your canvas (if its huge it can take a while). There are some optimizations around to work with raw canvas pixeldata (I think there is an article about this topic at MDN), I suggest you to google about it.
I prepared a small sketch in jsFiddle that you can use as starting point for your code.
Working sample at jsFiddle
Hope I've helped you.
c:.
Here's my take. I felt like all the other solutions were overly complicated. Though, after creating it, I now see it's the same solution as one other's, expect they just shared a fiddle and not a function.
function trimCanvas(canvas){
const context = canvas.getContext('2d');
const topLeft = {
x: canvas.width,
y: canvas.height,
update(x,y){
this.x = Math.min(this.x,x);
this.y = Math.min(this.y,y);
}
};
const bottomRight = {
x: 0,
y: 0,
update(x,y){
this.x = Math.max(this.x,x);
this.y = Math.max(this.y,y);
}
};
const imageData = context.getImageData(0,0,canvas.width,canvas.height);
for(let x = 0; x < canvas.width; x++){
for(let y = 0; y < canvas.height; y++){
const alpha = imageData.data[((y * (canvas.width * 4)) + (x * 4)) + 3];
if(alpha !== 0){
topLeft.update(x,y);
bottomRight.update(x,y);
}
}
}
const width = bottomRight.x - topLeft.x;
const height = bottomRight.y - topLeft.y;
const croppedCanvas = context.getImageData(topLeft.x,topLeft.y,width,height);
canvas.width = width;
canvas.height = height;
context.putImageData(croppedCanvas,0,0);
return canvas;
}
Here's code in ES syntax, short, fast and concise:
/**
* Trim a canvas.
*
* #author Arjan Haverkamp (arjan at avoid dot org)
* #param {canvas} canvas A canvas element to trim. This element will be trimmed (reference)
* #param {int} threshold Alpha threshold. Allows for trimming semi-opaque pixels too. Range: 0 - 255
* #returns {Object} Width and height of trimmed canvcas and left-top coordinate of trimmed area. Example: {width:400, height:300, x:65, y:104}
*/
const trimCanvas = (canvas, threshold = 0) => {
const ctx = canvas.getContext('2d'),
w = canvas.width, h = canvas.height,
imageData = ctx.getImageData(0, 0, w, h),
tlCorner = { x:w+1, y:h+1 },
brCorner = { x:-1, y:-1 };
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
if (imageData.data[((y * w + x) * 4) + 3] > threshold) {
tlCorner.x = Math.min(x, tlCorner.x);
tlCorner.y = Math.min(y, tlCorner.y);
brCorner.x = Math.max(x, brCorner.x);
brCorner.y = Math.max(y, brCorner.y);
}
}
}
const cut = ctx.getImageData(tlCorner.x, tlCorner.y, brCorner.x - tlCorner.x, brCorner.y - tlCorner.y);
canvas.width = brCorner.x - tlCorner.x;
canvas.height = brCorner.y - tlCorner.y;
ctx.putImageData(cut, 0, 0);
return {width:canvas.width, height:canvas.height, x:tlCorner.x, y:tlCorner.y};
}
The top voted answer here, as well as the implementations i found online trim one extra pixel which was very apparent when trying to trim text out of canvas. I wrote my own that worked better for me:
var img = new Image;
img.onload = () => {
var canvas = document.getElementById('canvas');
canvas.width = img.width;
canvas.height = img.height;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
document.getElementById('button').addEventListener('click', ()=>{
autoCropCanvas(canvas, ctx);
document.getElementById('button').remove();
});
};
img.src = 'data:image/png;base64,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';
function autoCropCanvas(canvas, ctx) {
var bounds = {
left: 0,
right: canvas.width,
top: 0,
bottom: canvas.height
};
var rows = [];
var cols = [];
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
for (var x = 0; x < canvas.width; x++) {
cols[x] = cols[x] || false;
for (var y = 0; y < canvas.height; y++) {
rows[y] = rows[y] || false;
const p = y * (canvas.width * 4) + x * 4;
const [r, g, b, a] = [imageData.data[p], imageData.data[p + 1], imageData.data[p + 2], imageData.data[p + 3]];
var isEmptyPixel = Math.max(r, g, b, a) === 0;
if (!isEmptyPixel) {
cols[x] = true;
rows[y] = true;
}
}
}
for (var i = 0; i < rows.length; i++) {
if (rows[i]) {
bounds.top = i ? i - 1 : i;
break;
}
}
for (var i = rows.length; i--; ) {
if (rows[i]) {
bounds.bottom = i < canvas.height ? i + 1 : i;
break;
}
}
for (var i = 0; i < cols.length; i++) {
if (cols[i]) {
bounds.left = i ? i - 1 : i;
break;
}
}
for (var i = cols.length; i--; ) {
if (cols[i]) {
bounds.right = i < canvas.width ? i + 1 : i;
break;
}
}
var newWidth = bounds.right - bounds.left;
var newHeight = bounds.bottom - bounds.top;
var cut = ctx.getImageData(bounds.left, bounds.top, newWidth, newHeight);
canvas.width = newWidth;
canvas.height = newHeight;
ctx.putImageData(cut, 0, 0);
}
<canvas id=canvas style='border: 1px solid pink'></canvas>
<button id=button>crop canvas</button>