Recently, I have been trying to create code to fill a polygon of any shape with color. I have gotten as far as being able to fill a shape that has lines of only one border size correctly, though I have found myself unable to do anything more than that. The problem is that the code does not know when to consider a line of pixels greater than that which it expects as a vertical or horizontal border of the shape. I am going through each pixel of the shape from left to right and checking if any of the pixels have any form of color by checking if the alpha value is 0 or not. Once it finds a pixel that does have an alpha value of anything other than 0, it moves forward a single pixel and then uses the even/odd technique to determine whether the point is inside part of the polygon or not (it makes an infinite line to the right and determines if the number of collisions with colored lines is odd, and if it is, the point is inside the polygon). In general, we consider a single, lone pixel to count as a single line, and we consider a horizontal line of more than one pixel to be two lines because of how often horizontal lines will be part of a border or not. Take the following scenario:
Here, the red dot is the point (pixel) we begin testing from. If we did not consider that horizontal line in the middle to be two points (as is shown by the red lines and x's), we would only have two points of intersection and therefore would not fill the pixel despite the fact that we most definitely do want to fill that pixel. As stated earlier, however, this brings up another problem with a different scenario:
In this case, if we do count a horizontal line of more than one pixel to be two separate lines, we end up not filling any areas with borders that are thicker than the expected thickness. For your reference, the function to handle this is as follows:
//imgData is essentially a WebImage object (explained more below) and r, g, and b are the color values for the fill color
function fillWithColor(imgData, r, g, b) {
//Boolean determining whether we should color the given pixel(s) or not
var doColor = false;
//Booleans determining whether the last pixel found in the entire image was colored
var blackLast = false;
//Booleans determining whether the last 1 or 2 pixels found after a given pixel were colored
var foundBlackPrev, foundBlackPrev2 = false;
//The number of colored pixels found
var blackCount = 0;
//Loop through the entire canvas
for(var y = 0; y < imgData.height; y += IMG_SCALE) {
for(var x = 0; x < imgData.width; x += IMG_SCALE) {
//Test if given pixel is colored
if(getAlpha(imgData, x, y) != 0) {
//If the last pixel was black, begin coloring
if(!blackLast) {
blackLast = true;
doColor = true;
}
} else {
//If the current pixel is not colored, but the last one was, find all colored lines to the right
if(blackLast){
for(var i = x; i < imgData.width; i += IMG_SCALE) {
//If the pixel is colored...
if(getAlpha(imgData, i, y) != 0) {
//If no colored pixel was found before, add to the count
if(!foundBlackPrev){
blackCount++;
foundBlackPrev = true;
} else {
//Otherwise, at least 2 colored pixels have been found in a row
foundBlackPrev2 = true;
}
} else {
//If two or more colored pixels were found in a row, add to the count
if(foundBlackPrev2) {
blackCount++;
}
//Reset the booleans
foundBlackPrev2 = foundBlackPrev = false;
}
}
}
//If the count is odd, we start coloring
if(blackCount & 1) {
blackCount = 0;
doColor = true;
} else {
//If the last pixel in the entire image was black, we stop coloring
if(blackLast) {
doColor = false;
}
}
//Reset the boolean
blackLast = false;
//If we are to be coloring the pixel, color it
if(doColor) {
//Color the pixel
for(var j = 0; j < IMG_SCALE; j++) {
for(var k = 0; k < IMG_SCALE; k++) {
//This is the same as calling setRed, setGreen, setBlue and setAlpha functions from the WebImage API all at once (parameters in order are WebImage object equivalent, x position of pixel, y position of pixel, red value, green value, blue value, and alpha value)
setRGB(imgData, x + j, y + k, r, g, b, 255);
}
}
}
}
}
}
//Update the image (essentially the same as removing all elements from the given area and calling add on the image)
clearCanvas();
putImageData(imgData, 0, 0, imgData.width, imgData.height);
//Return the modified data
return imgData;
}
Where...
imgData is the collection of all of the pixels in the given area (essentially a WebImage object)
IMG_SCALE is the integer value by which the image has been scaled up (which gives us the scale of the pixels as well). In this example, it is equal to 4 because the image is scaled up to 192x256 (from 48x64). This means that every "pixel" you see in the image is actually comprised of a 4x4 block of identically-colored pixels.
So, what I'm really looking for here is a way to determine whether a given colored pixel that comes after another is part of a horizontal border or if it is just another piece comprising the thickness of a vertical border. In addition, if I have the wrong approach to this problem in general, I would greatly appreciate any suggestions as to how to do this more efficiently. Thank you.
I understand the problem and I think you would do better if you would switch your strategy here. We know the following:
the point of start is inside the shape
the color should be filled for every pixel inside the shape
So, we could always push the neighbors of the current point into a queue to be processed and be careful to avoid processing the same points twice, this way traversing all the useful pixels and including them into the coloring plan. The function below is untested.
function fillColor(pattern, startingPoint, color, boundaryColor) {
let visitQueue = [];
let output = {};
if (startingPoint.x - 1 >= 0) visitQueue.push({startingPoint.x - 1, startingPoint.y});
if (startingPoint.x + 1 < pattern.width) visitQueue.push({startingPoint.x + 1, startingPoint.y});
if (startingPoint.y + 1 < pattern.height) visitQueue.push({startingPoint.x, startingPoint.y + 1});
if (startingPoint.y - 1 >= 0) visitQueue.push({startingPoint.x, startingPoint.y - 1});
let visited = {};
while (visitQueue.length > 0) {
let point = visitQueue[0];
visitQueue.shift();
if ((!visited[point.x]) || (visited[point.x].indexOf(point.y) < 0)) {
if (!visited[point.x]) visited[point.x] = [];
visited[point.x].push(point.y);
if (isBlank(pattern, point)) { //you need to implement isBlank
if (!output[point.x]) output[point.x] = [];
output[point.x].push(point.y);
if (point.x + 1 < pattern.width) visitQueue.push({point.x + 1, point.y});
if (point.x - 1 >= 0) visitQueue.push({point.x - 1, point.y});
if (point.y + 1 < pattern.height) visitQueue.push({point.x, point.y + 1});
if (point.y - 1 >= 0) visitQueue.push({point.x, point.y - 1})
}
}
}
return output;
}
As far as I understood you cannot "consider a horizontal line of more than one pixel to be two lines". I don't think you need to count black pixels the way you do, rather count groups of 1 or more pixels.
I would also tidy the code by avoiding using the "doColor" boolean variable. You could rather move the coloring code to a new function color(x,y) and call it straight away.
const ctx = document.querySelector("canvas").getContext("2d");
//ctx.lineWidth(10);//-as you asked we are setting greater border or line width,BUT "LINEWIDTH" IS NOT WORKING IN INBUILT STACKOVERFLOW SNIPPET USE IT IN A FILE I THINK STACKOVERFLOW IS NOT UP-TO-DATE,IN ANY IDE UNCOMENT THIS
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(250, 70);
ctx.lineTo(270, 120);
ctx.lineTo(170, 140);
ctx.lineTo(190, 80);
ctx.lineTo(100, 60);
ctx.lineTo(50, 130);
ctx.lineTo(20, 20);
ctx.stroke();
function getMousePosition(canvas, event) {
let rect = canvas.getBoundingClientRect();
let mx = event.clientX - rect.left;
let my = event.clientY - rect.top;
console.log("Coordinate x: " + mx, "Coordinate y: " + my);
floodFill(ctx, mx, my, [155, 0, 255, 255], 128);
}
let canvasElem = document.querySelector("canvas");
canvasElem.addEventListener("mousedown", function(e) {
getMousePosition(canvasElem, e);
});
function getPixel(imageData, x, y) {
if (x < 0 || y < 0 || x >= imageData.width || y >= imageData.height) {
return [-1, -1, -1, -1]; // impossible color
} else {
const offset = (y * imageData.width + x) * 4;
return imageData.data.slice(offset, offset + 4);
}
}
function setPixel(imageData, x, y, color) {
const offset = (y * imageData.width + x) * 4;
imageData.data[offset + 0] = color[0];
imageData.data[offset + 1] = color[1];
imageData.data[offset + 2] = color[2];
imageData.data[offset + 3] = color[0];
}
function colorsMatch(a, b, rangeSq) {
const dr = a[0] - b[0];
const dg = a[1] - b[1];
const db = a[2] - b[2];
const da = a[3] - b[3];
return dr * dr + dg * dg + db * db + da * da < rangeSq;
}
function floodFill(ctx, x, y, fillColor, range = 1) {
// read the pixels in the canvas
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
// flags for if we visited a pixel already
const visited = new Uint8Array(imageData.width, imageData.height);
// get the color we're filling
const targetColor = getPixel(imageData, x, y);
// check we are actually filling a different color
if (!colorsMatch(targetColor, fillColor)) {
const rangeSq = range * range;
const pixelsToCheck = [x, y];
while (pixelsToCheck.length > 0) {
const y = pixelsToCheck.pop();
const x = pixelsToCheck.pop();
const currentColor = getPixel(imageData, x, y);
if (!visited[y * imageData.width + x] &&
colorsMatch(currentColor, targetColor, rangeSq)) {
setPixel(imageData, x, y, fillColor);
visited[y * imageData.width + x] = 1; // mark we were here already
pixelsToCheck.push(x + 1, y);
pixelsToCheck.push(x - 1, y);
pixelsToCheck.push(x, y + 1);
pixelsToCheck.push(x, y - 1);
}
}
// put the data back
ctx.putImageData(imageData, 0, 0);
}
}
<canvas></canvas>
This is based on other answers
note:"LINEWIDTH" IS NOT WORKING IN INBUILT STACKOVERFLOW SNIPPET USE IT IN A FILE I THINK STACKOVERFLOW IS NOT UP-TO-DATE,
But it works well in simple HTML,JS website
Related
const coords = [
{
name: "Rijnstraat vervolg",
points: [
[695, 500],
[680, 480],
[580, 475],
[520, 460],
],
width: 10,
types: [types.car, types.truck, types.pedestrian, types.bike],
oneway: true,
},
...
]
I have an array that looks like the above and I want to make a function that generates a path (along the other paths, which are the black lines in the image) from a black or gray circle to another black or gray circle. So I want the function to take in a start and end point (black or gray circle) and return an array of points that follow the already existings paths. (Which are sort of like roads)
And the function can be described as someone who is trying to get to somewhere.
I already tried a recursive function that looks like this:
function calculatePathToShop(startPoint, shopPoint) {
const targetShopPoint = findClosestPointOnPath(shopPoint);
const targetPathIndex = findPathByPoint(targetShopPoint);
const connectedPaths = calculateConnectedPaths(targetPathIndex);
let startPathIndex = -1;
connectedPaths.forEach(path => {
const pathPoints = coords[path].points;
pathPoints.forEach(pathPoint => {
if (comparePoints(startPoint.point, pathPoint)) startPathIndex = path;
});
});
if (startPathIndex == -1) return false;
let startPathPoints = coords[startPathIndex].points;
let targetPathPoints = coords[targetPathIndex].points;
if (!comparePoints(startPoint.point, startPathPoints[0])) startPathPoints.reverse();
ctx.strokeStyle = "rgba(255, 0, 0, .05)";
}
This one generated a path (along the existing ones) to a shop point, which is almost the same as a gray point. But this worked for some starting points, but the rest would just straight up fail
So does anyone know an algorithm, or has a function/solution that I can use to generate the path that someone can walk along the road (the black lines in the image)
Full coords array, and part of my already existing code is found here: https://raw.githubusercontent.com/CodeFoxDev/people-simulation/main/func/paths.js
(The rest of the code is in the github repo itself)
Fixed step interpolation
To interpolate a line segment you divide the vector from the start pointing to the end by the number of steps.
EG
steps = 100;
start = {x: 50, y: 100}
end = {x: 150, y: 300}
step = {x: (end.x - start.x) / steps, y: (end.y - start.y) / steps};
Then loop that number of steps adding the vector to a position initialized to the start point.
points = []; // array of interpolated points
point = {...start} // set start position.
while (steps--) {
points.push({...point});
point.x += vec.x;
point.y += vec.y;
}
points.push({...end}); // last point at end
This will create different spacing for different line lengths.
Fixed distance interpolation
To get a constant spacing between points you will need to use the lines' length to get the number of steps.
pixelsPerStep = 2; // distance between points.
start = {x: 50, y: 100}
end = {x: 150, y: 300}
step = {x: end.x - start.x, y: end.y - start.y};
lineSteps = Math.hypot(step.x, step.y) / pixelsPerStep;
points = []; // array of interpolated points
for (i = 0; i < lineSteps ; i += 1) {
u = i / lineSteps;
points.push({x: start.x + step.x * u, y: start.y + step.y * u});
}
// check to add end point
Note that the last point may or may not be at the correct distance. Due to rounding errors in floating point numbers you will need to check if the last point is close to the correct spacing and whether or not to include it.
eg from code above
// add last point if within (0.01 * pixelsPerStep) pixels of correct spacing
if (Math.abs(lineSteps - i) < 0.01) {
points.push({...end});
}
Note Use the overflow lineSteps - i when interpolating many line segments, to carry the correct start offset to each subsequent line segment.
Example
The code below is an example of a constant spaced set of points interpolated from another set of points.
The example draws the new points in black dots. The original points are rendered in red.
Note that the distance between new points is constant and thus may not fall on the original (red) points.
Note that there is a check at the end to test if a last point should be added.
const ctx = canvas.getContext("2d");
const P2 = (x, y) => ({x, y});
const points = [
P2(100,90),
P2(300,210),
P2(350,110),
P2(50,10),
P2(6,219),
];
const interpolatedPoints = interpolatePath(points, 35);
drawPoints(interpolatedPoints, 2);
ctx.fillStyle = "RED";
drawPoints(points);
function drawPoints(points, size = 1) {
ctx.beginPath();
for (const p of points) {
ctx.rect(p.x - size, p.y - size, size * 2 + 1, size * 2 + 1);
}
ctx.fill();
}
function interpolatePath(path, pixelStep) {
const res = [];
var p2, i = 1, overflow = 0;
while (i < path.length) {
const p1 = path[i - 1];
p2 = path[i];
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const len = Math.hypot(dx, dy) / pixelStep;
let j = overflow;
while (j < len) {
const u = j / len;
res.push(P2(p1.x + dx * u, p1.y + dy * u));
j++;
}
overflow = j - len;
i++;
}
// add last point if close to correct distance
if (Math.abs(overflow) < 0.01) {
res.push(P2(p2.x, p2.y));
}
return res;
}
<canvas id="canvas" width="400" height="400"></canvas>
I am using a recursive flood fill algorithm in javascript and I am not sure how to avoid exceeding the max call stack size. This is a little project that runs in the browser.
I got the idea from here: https://guide.freecodecamp.org/algorithms/flood-fill/
I chose this algorithm because it's easy to understand and so far I like it because it's pretty quick.
x and y are the 2-d coordinates from the top-left, targetColor and newColor are each a Uint8ClampedArray, and id = ctx.createImageData(1,1); that gets its info from newColor.
function floodFill2(x, y, targetColor, newColor, id) {
let c = ctx.getImageData(x, y, 1, 1).data;
// if the pixel doesnt match the target color, end function
if (c[0] !== targetColor[0] || c[1] !== targetColor[1] || c[2] !== targetColor[2]) {
return;
}
// if the pixel is already the newColor, exit function
if (c[0] === newColor[0] && c[1] === newColor[1] && c[2] === newColor[2]) {
// this 'probably' means we've already been here, so we should ignore the pixel
return;
}
// if the fn is still alive, then change the color of the pixel
ctx.putImageData(id, x, y);
// check neighbors
floodFill2(x-1, y, targetColor, newColor, id);
floodFill2(x+1, y, targetColor, newColor, id);
floodFill2(x, y-1, targetColor, newColor, id);
floodFill2(x, y+1, targetColor, newColor, id);
return;
}
If the section is small, this code works fine. If the section is big, only a portion gets filled in and then I get the max call stack size error.
Questions
Is there something that doesn't make sense in the above code? (ie. maybe an issue for code review?)
If the code looks ok, is it the possible that I am simply using an algorithm that is inappropriate to flood fill a large section?
I would like to say that my hope for this question is to have a simple function similar to the one above which will work even for a very large, oddly shaped region but that I suppose is contingent on the generality of the algorithm. Like, am I trying to drive a nail with a screwdriver kind of thing?
Use a stack or Why recursion in JavaScript sucks.
Recursion is just a lazy mans stack. Not only is it lazy, it uses more memory and is far slower than traditional stacks
To top it off (as you have discovered) In JavaScript recursion is risky as the call stack is very small and you can never know how much of the call stack has been used when your function is called.
Some bottle necks while here
Getting image data via getImageData is an intensive task for many devices. It can take just as long to get 1 pixel as getting 65000 pixels. Calling getImageData for every pixel is a very bad idea. Get all pixels once and get access to pixels directly from RAM
Use an Uint32Array so you can process a pixel in one step rather than having to check each channel in turn.
Example
Using a simple array as a stack, each item pushed to the stack is the index of a new pixel to fill. Thus rather than have to create a new execution context, a new local scope and associated variables, closure, and more. A single 64bit number takes the place of a callStack entry.
See demo for an alternative flood fill pixel search method
function floodFill(x, y, targetColor, newColor) {
const w = ctx.canvas.width, h = ctx.canvas.height;
const imgData = ctx.getImageData(0, 0, w, h);
const p32 = new Uint32Array(imgData.data.buffer);
const channelMask = 0xFFFFFF; // Masks out Alpha NOTE order of channels is ABGR
const cInvMask = 0xFF000000; // Mask out BGR
const canFill = idx => (p32[idx] & channelMask) === targetColor;
const setPixel = (idx, newColor) => p32[idx] = (p32[idx] & cInvMask) | newColor;
const stack = [x + y * w]; // add starting pos to stack
while (stack.length) {
let idx = stack.pop();
setPixel(idx, newColor);
// for each direction check if that pixel can be filled and if so add it to the stack
canFill(idx + 1) && stack.push(idx + 1); // check right
canFill(idx - 1) && stack.push(idx - 1); // check left
canFill(idx - w) && stack.push(idx - w); // check Up
canFill(idx + w) && stack.push(idx + w); // check down
}
// all done when stack is empty so put pixels back to canvas and return
ctx.putImageData(imgData,0, 0);
}
Usage
To use the function is slightly different. id is not used and the colors targetColor and newColor need to be 32bit words with the red, green, blue, alpha reversed.
For example if targetColor was yellow = [255, 255, 0] and newColor was blue =[0, 0, 255] then revers RGB for each and call fill with
const yellow = 0xFFFF;
const blue = 0xFF0000;
floodFill(x, y, yellow, blue);
Note that I am matching your function and completely ignoring alpha
Inefficient algorithm
Note that this style of fill (mark up to 4 neighbors) is very inefficient as many of the pixels will be marked to fill and by the time they are popped from the stack it will already have been filled by another neighbor.
The following GIF best illustrates the problem. Filling the 4 by 3 area with green.
First set the pixel green,
Then push to stack if not green right, left, up, down [illustration red, orange, cyan, purple boxes]
Pop bottom and set to green
Repeat
When a location that already is on the stack is added it is inset (just for illustration purpose)
Note that when all pixels are green there are still 6 items on the stack that still need to be popped. I estimate on average you will be processing 1.6 times the number of pixels needed. For a large image 2000sq thats 2million (alot of) pixels
Using an array stack rather than call stack means
No more call stack overflows
Inherently faster code.
Allows for many optimizations
Demo
The demo is a slightly different version as your logic has some problems. It still uses a stack, but limits the number of entries pushed to the stack to be equal to the number of unique columns in the fill area.
Includes alpha in the pixel fill test and pixel write color. Simplifying the pixel read and write code.
Checks against the edges of the canvas rather than filling outside the canvas width (looping back AKA asteroids style)
Reads target color from the canvas at the first x,y pixel
Fills columns from the top most pixel in each column and only branching left or right if the previous left or right pixel was not the target color. This reduces the number of pixels to push the stack by orders of magnitude.
Click to flood fill
function floodFill(x, y, newColor) {
var left, right, leftEdge, rightEdge;
const w = ctx.canvas.width, h = ctx.canvas.height, pixels = w * h;
const imgData = ctx.getImageData(0, 0, w, h);
const p32 = new Uint32Array(imgData.data.buffer);
const stack = [x + y * w]; // add starting pos to stack
const targetColor = p32[stack[0]];
if (targetColor === newColor || targetColor === undefined) { return } // avoid endless loop
while (stack.length) {
let idx = stack.pop();
while(idx >= w && p32[idx - w] === targetColor) { idx -= w }; // move to top edge
right = left = false;
leftEdge = (idx % w) === 0;
rightEdge = ((idx +1) % w) === 0;
while (p32[idx] === targetColor) {
p32[idx] = newColor;
if(!leftEdge) {
if (p32[idx - 1] === targetColor) { // check left
if (!left) {
stack.push(idx - 1); // found new column to left
left = true; //
}
} else if (left) { left = false }
}
if(!rightEdge) {
if (p32[idx + 1] === targetColor) {
if (!right) {
stack.push(idx + 1); // new column to right
right = true;
}
} else if (right) { right = false }
}
idx += w;
}
}
ctx.putImageData(imgData,0, 0);
return;
}
var w = canvas.width;
var h = canvas.height;
const ctx = canvas.getContext("2d");
var i = 400;
const fillCol = 0xFF0000FF
const randI = v => Math.random() * v | 0;
ctx.fillStyle = "#FFF";
ctx.fillRect(0, 0, w, h);
ctx.fillStyle = "#000";
while(i--) {
ctx.fillRect(randI(w), randI(h), 20, 20);
ctx.fillRect(randI(w), randI(h), 50, 20);
ctx.fillRect(randI(w), randI(h), 10, 60);
ctx.fillRect(randI(w), randI(h), 180, 2);
ctx.fillRect(randI(w), randI(h), 2, 182);
ctx.fillRect(randI(w), randI(h), 80, 6);
ctx.fillRect(randI(w), randI(h), 6, 82);
ctx.fillRect(randI(w), randI(h), randI(40), randI(40));
}
i = 400;
ctx.fillStyle = "#888";
while(i--) {
ctx.fillRect(randI(w), randI(h), randI(40), randI(40));
ctx.fillRect(randI(w), randI(h), randI(4), randI(140));
}
var fillIdx = 0;
const fillColors = [0xFFFF0000,0xFFFFFF00,0xFF00FF00,0xFF00FFFF,0xFF0000FF,0xFFFF00FF];
canvas.addEventListener("click",(e) => {
floodFill(e.pageX | 0, e.pageY | 0, fillColors[(fillIdx++) % fillColors.length]);
});
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas" width="2048" height="2048">
Flood fill is a problematic process with respect to stack size requirements (be it the system stack or one managed on the heap): in the worst case you will need a recursion depth on the order of the image size. Such cases can occur when you binarize random noise, they are not so improbable.
There is a version of flood filling that is based on filling whole horizontal runs in a single go (https://en.wikipedia.org/wiki/Flood_fill#Scanline_fill). It is advisable in general because it roughly divides the recursion depth by the average length of the runs and is faster in the "normal" cases. Anyway, it doesn't solve the worst-case issue.
There is also an interesting truly stackless algorithm as described here: https://en.wikipedia.org/wiki/Flood_fill#Fixed-memory_method_(right-hand_fill_method). But the implementation looks cumbersome.
I want to use the Canvas API to create a square grid where each cell has a 1px border on its bottom and a 1px border on its right. The entire grid would then have a 1px border drawn around it, so it'd look like this:
Once the grid exists, I want to write a function that will highlight a given cell. So for example calling highlightCell(1, 3, 'green') would result in this:
I thought this would be simple enough, but I'm having a devil of a time working it out. My problem is that when I account for having to straddle pixel lines -- drawing from eg 3.5 to 7.5, rather than 3 to 7, to not get blurry lines -- the math to figure out coordinates doesn't seem to work the way I expect at the edges, and I get results like this, where the highlight isn't placed correctly.
My math is:
I want a 700x700 px grid divided into 35 cells
Canvas itself is 702x702 px to allow for a 1px border on each side
Cells have a 1px east border and 1px south border so the highlight rectangles are 19x19px
Where w is the width of the canvas in px and h is its height, draw the outside border of the whole grid by going from (0.5, 0.5) --> (w + 0.5, 0.5) --> (w + 0.5, h + 0.5) --> (0.5, h + 0.5) --> (0.5, 0.5), creating a solid line for the first and last pixel row and first and last pixel column. The inside space is then 700x700.
Highlight cell 2,4 by drawing a 19x19 px rectangle at ???. I cannot figure out a value here that consistently works.
I'd appreciate someone explaining what I've done wrong because I'm sure it's something dumb but I just can't see it.
Here's the JS Fiddle of my attempt.
I would do it by separating the logic and the drawing. For example by having a state object and a drawFrame function; like this:
// setup state
const state = {
__arr: [],
__width: 20,
__height: 20,
__cell: {
width: 20,
height: 20,
},
__onUpdate: () => {},
__calcIndex(x, y) {
const index = x + y * this.__width;
if (index >= this.__arr.length || index < 0) {
throw new Error('Invalid index!');
}
return index;
},
init(onUpdate) {
this.__arr = Array(this.__width * this.__height).fill(0);
this.__onUpdate = onUpdate;
},
get(x, y) {
const index = this.__calcIndex(x, y);
return this.__arr[index];
},
set(x, y, value) {
const index = this.__calcIndex(x, y);
this.__arr[index] = value;
this.__onUpdate();
},
};
// setup drawing logic
const canvas = document.createElement('canvas');
document.body.append(canvas);
const ctx = canvas.getContext('2d');
const drawFrame = () => {
const cell = state.__cell;
ctx.lineWidth = 1;
ctx.strokeStyle = 'black';
ctx.fillStyle = 'orangered';
for (let x = 0; x < state.__width; x++) {
for (let y = 0; y < state.__width; y++) {
ctx.strokeRect(cell.width * x, cell.height * y, cell.width, cell.height);
if (state.get(x, y) !== 0) {
ctx.fillRect(1 + cell.width * x, 1 + cell.height * y, cell.width-1, cell.height-1);
}
}
}
}
state.init(drawFrame);
canvas.width = state.__width * state.__cell.width;
canvas.height = state.__height * state.__cell.height;
drawFrame();
state.set(2, 4, 1);
state.set(3, 5, 1);
state.set(2, 6, 1);
state.set(7, 4, 1);
I'm receiving all distances between a random number of points in a 2 dimensional coordinate system.
How can I visualize this as coordinates on a map in my browser?
In case there are many solutions I just want to see the first possible one that my algorithm can come up with.
So here's an extremely easy example:
PointCount = 3
Distances:
0-1 = 2
0-2 = 4
1-2 = 2
Does anyone know an easy way (existing solution/framework maybe) to do it using whatever is out there to make it easier to implement?
I was thinking maybe using the html canvas element for drawing, but I don't know how to create an algorithm that could come up with possible coordinates for those points.
The above example is simplified -
Real distance values could look like this:
(0) (1) (2) (3)
(0) 0 2344 3333 10000
(1) 0 3566 10333
(2) 0 12520
I'm not sure this is relevant for SO, but anyway...
The way to do this is quite simply to place the points one by one using the data:
Pick a random location for the first point (let's say it's 0,0).
The second point is on a circle with radius d(0,1) with the first point as its center, so you can pick any point on the circle. Let's pick (d(0,1),0).
The third point is at the intersection of a circle with radius d(0,2) and center point 1, and a circle with radius d(1,2) and center point 2. You will get either 0, 1, 2 or an infinity of solutions. If the data comes from real points, 0 shouldn't happen. 1 and infinity are edge cases, but you should still handle them. Pick any of the solutions.
The fourth point is at the intersection of 3 circles. Unless you're very unlucky (but you should account for it), there should be only one solution.
Continue like this until all points have been placed.
Note that this doesn't mean you'll get the exact locations of the original points: you can have any combination of a translation (the choice of your first point), rotation (the choice of your second point) and symmetry (the choice of your third point) making the difference.
A quick and dirty implementation (not handling quite a few cases, and tested very little):
function distance(p1, p2) {
return Math.sqrt(Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2));
}
// adapted from https://stackoverflow.com/a/12221389/3527940
function intersection(x0, y0, r0, x1, y1, r1) {
var a, dx, dy, d, h, rx, ry;
var x2, y2;
/* dx and dy are the vertical and horizontal distances between
* the circle centers.
*/
dx = x1 - x0;
dy = y1 - y0;
/* Determine the straight-line distance between the centers. */
d = Math.sqrt((dy * dy) + (dx * dx));
/* Check for solvability. */
if (d > (r0 + r1)) {
/* no solution. circles do not intersect. */
return false;
}
if (d < Math.abs(r0 - r1)) {
/* no solution. one circle is contained in the other */
return false;
}
/* 'point 2' is the point where the line through the circle
* intersection points crosses the line between the circle
* centers.
*/
/* Determine the distance from point 0 to point 2. */
a = ((r0 * r0) - (r1 * r1) + (d * d)) / (2.0 * d);
/* Determine the coordinates of point 2. */
x2 = x0 + (dx * a / d);
y2 = y0 + (dy * a / d);
/* Determine the distance from point 2 to either of the
* intersection points.
*/
h = Math.sqrt((r0 * r0) - (a * a));
/* Now determine the offsets of the intersection points from
* point 2.
*/
rx = -dy * (h / d);
ry = dx * (h / d);
/* Determine the absolute intersection points. */
var xi = x2 + rx;
var xi_prime = x2 - rx;
var yi = y2 + ry;
var yi_prime = y2 - ry;
return [
[xi, yi],
[xi_prime, yi_prime]
];
}
function generateData(nbPoints) {
var i, j, k;
var originalPoints = [];
for (i = 0; i < nbPoints; i++) {
originalPoints.push([Math.random() * 20000 - 10000, Math.random() * 20000 - 10000]);
}
var data = [];
var distances;
for (i = 0; i < nbPoints; i++) {
distances = [];
for (j = 0; j < i; j++) {
distances.push(distance(originalPoints[i], originalPoints[j]));
}
data.push(distances);
}
//console.log("original points", originalPoints);
//console.log("distance data", data);
return data;
}
function findPointsForDistances(data, threshold) {
var points = [];
var solutions;
var solutions1, solutions2;
var point;
var i, j, k;
if (!threshold)
threshold = 0.01;
// First point, arbitrarily set at 0,0
points.push([0, 0]);
// Second point, arbitrarily set at d(0,1),0
points.push([data[1][0], 0]);
// Third point, intersection of two circles, pick any solution
solutions = intersection(
points[0][0], points[0][1], data[2][0],
points[1][0], points[1][1], data[2][1]);
//console.log("possible solutions for point 3", solutions);
points.push(solutions[0]);
//console.log("solution for points 1, 2 and 3", points);
found = true;
// Subsequent points, intersections of n-1 circles, use first two to find 2 solutions,
// the 3rd to pick one of the two
// then use others to check it's valid
for (i = 3; i < data.length; i++) {
// distances to points 1 and 2 give two circles and two possible solutions
solutions = intersection(
points[0][0], points[0][1], data[i][0],
points[1][0], points[1][1], data[i][1]);
//console.log("possible solutions for point " + (i + 1), solutions);
// try to find which solution is compatible with distance to point 3
found = false;
for (j = 0; j < 2; j++) {
if (Math.abs(distance(solutions[j], points[2]) - data[i][2]) <= threshold) {
point = solutions[j];
found = true;
break;
}
}
if (!found) {
console.log("could not find solution for point " + (i + 1));
console.log("distance data", data);
console.log("solution for points 1, 2 and 3", points);
console.log("possible solutions for point " + (i + 1), solutions);
console.log("distances to point 3",
distance(solutions[0], points[2]),
distance(solutions[1], points[2]),
data[i][2]
);
break;
}
// We have found a solution, we need to check it's valid
for (j = 3; j < i; j++) {
if (Math.abs(distance(point, points[j]) - data[i][j]) > threshold) {
console.log("Could not verify solution", point, "for point " + (i + 1) + " against distance to point " + (j + 1));
found = false;
break;
}
}
if (!found) {
console.log("stopping");
break;
}
points.push(point);
}
if (found) {
//console.log("complete solution", points);
return points;
}
}
console.log(findPointsForDistances([
[],
[2344],
[3333, 3566],
[10000, 10333, 12520],
]));
console.log(findPointsForDistances([
[],
[2],
[4, 2],
]));
console.log(findPointsForDistances([
[],
[4000],
[5000, 3000],
[3000, 5000, 4000]
]));
console.log(findPointsForDistances([
[],
[2928],
[4938, 3437],
[10557, 10726, 13535]
]));
var nbPoints, i;
for (nbPoints = 4; nbPoints < 8; nbPoints++) {
for (i = 0; i < 10; i++) {
console.log(findPointsForDistances(generateData(nbPoints)));
}
}
Fiddle here: https://jsfiddle.net/jacquesc/82aqmpnb/15/
Minimum working example. Remember that in canvas coordinates, the y value is inverted but you could do something like:
y = canvasHeight - y
If you also have negative points then if would take a little bit of extra work. Also it may be helpful in that case to draw lines and tick marks to visualize the axis.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
let scale = 10;
let radius = 10;
function point(x, y) {
ctx.fillRect(x*scale, y*scale, radius, radius);
}
// test
point(10, 15);
point(20, 8);
<html>
<body>
<canvas id="canvas" width=1000 height=1000></canvas>
</body>
</html>
There are plenty of libraries out there.
chartist.js is easy to use and responsive JavaS cript library. I used it last year for basic charts after trying many others but it was the only one that scaling easily in different screen sizes.
chartJS is another better looking library.
And you can use html5 canvas it's easy and fun but it will take time especially in scaling.
To scale and position, you should use the minimum and maximum values for x and y.
Good luck
Say I have this image:
I'd like to recognize the position of the red ball in the image, I could measure the size of the ball(in pixel) in ahead.
I know that I could draw the image to a canvas, then I could get the pixel color data with context.getImageData, but then what should I do? which algorithm sould I use? I'm new to image processing, thanks a lot.
Here's code dedicated to getting that ball position. The output position will logged to the console so have your JS console open! This code has some values in it that you can play with. I chose some that work for your image such as the rough diameter of the ball being 14 pixels and the threshold for each colour component.
I saved the image as "test.jpg" but you can change the code to the correct image path on line 11.
<!DOCTYPE html>
<html>
<body>
<canvas width="800" height="600" id="testCanvas"></canvas>
<script type="text/javascript">
var img = document.createElement('img');
img.onload = function () {
console.log(getBallPosition(this));
};
img.src = 'test.jpg';
function getBallPosition(img) {
var canvas = document.getElementById('testCanvas'),
ctx = canvas.getContext('2d'),
imageData,
width = img.width,
height = img.height,
pixelData,
pixelRedValue,
pixelGreenValue,
pixelBlueValue,
pixelAlphaValue,
pixelIndex,
redThreshold = 128,
greenThreshold = 40,
blueThreshold = 40,
alphaThreshold = 180,
circleDiameter = 14,
x, y,
count,
ballPosition,
closestBallCount = 0,
closestBallPosition;
// Draw the image to the canvas
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0);
// Get the image data
imageData = ctx.getImageData(0, 0, width, height);
pixelData = imageData.data;
// Find the ball!
for (y = 0; y < height; y++) {
// Reset the pixel count
count = 0;
// Loop through the pixels on this line
for (x = 0; x < width; x++) {
// Set the pixel data starting point
pixelIndex = (y * width * 4) + (x * 4);
// Grab the red pixel value
pixelRedValue = pixelData[pixelIndex];
pixelGreenValue = pixelData[pixelIndex + 1];
pixelBlueValue = pixelData[pixelIndex + 2];
pixelAlphaValue = pixelData[pixelIndex + 3];
// Check if the value is within out red colour threshold
if (pixelRedValue >= redThreshold && pixelGreenValue <= greenThreshold && pixelBlueValue <= blueThreshold && pixelAlphaValue >= alphaThreshold) {
count++;
} else {
// We've found a pixel that isn't part of the red ball
// so now check if we found any red data
if (count === circleDiameter) {
// We've found our ball
return {
x: x - Math.floor(circleDiameter / 2),
y: y
};
} else {
// Any data we found was not our ball
if (count < circleDiameter && count > closestBallCount) {
closestBallCount = count;
closestBallPosition = {
x: x - Math.floor(circleDiameter / 2),
y: y
};
}
count = 0;
}
}
}
}
return closestBallPosition;
}
</script>
</body>
</html>
Well i would go and cluster pixels of that color. For example, you could have a look up table where you store red (or in the range of a treshold) pixels (coordinates being the look up key) and an integer value being the cluster id whenever you encounter a pixel without any known red neighbours it starts a new cluster, all other red pixels get the cluster id of a red pixel they are the neighbour of. Depending of you algorithms kernel:
A) XXX B) X
XOX XOX
XXX X
you might need to deal (case B) with a pixel connecting two prior not connected clusters. You would have to replace the cluster id of one of that clusters.
After that you have clusters of pixels. These you can analyse. In case of a round shape i would look for the median in x and y for each cluster and check if all the pixels of that cluster are in the radius.
This will fail if the red ball (or part of it) is in front of another red object. You would than need more complex algorithms.