Related
I am unsure of what math to use here, as I am very inexperienced in the area of using math along with coding to solve problems such as this, so I was wondering if anyone here could either give me some pointers or give me somewhere to look at this specific problem. I have the x and y trajectory of the single point (or ball) and when it moves, it acts like a line, moving from one place in which is has to stop, then bouncing off of it (reflecting the trajectory), and going in that bounced trajectory. I just need the algorithm to give a true/false (boolean) to whether the current slope will come in contact with the rectangle. I have the 4 edge points of the rectangle and the middle point of the rectangle if needed.
After some back-and-forth in the comments, here is a function that may better suit OP's purpose. The is_intersect() function takes as input the position of the point on the trajectory, its direction, and a quadrilateral, and returns true if the point is on an intersecting trajectory, false otherwise.
pos is a table containing the position of the point, of the form:
pos = { x=x1, y=y1 }
dir is a number containing a positive angle in radians (0 <= θ < 2π) with respect to the positive x-axis, representing the direction of travel of the point.
quad is a table representing a quadrilateral, of the form:
quad = {{x=x1, y=y1}, {x=x2, y=y2}, {x=x3, y=y3}, {x=x4, y=y4}}
Note that it would be a simple matter, and perhaps desirable, to adapt the code to use integer-indexed tables instead, such as pos = {x1, y1} and quad = {{x1, y1}, {x2, y2},...}.
This function works for quadrilaterals situated anywhere in the plane, and for trajectory points situated anywhere in the plane. It works by finding the positive angles with respect to the positive x-axis of a line through the trajectory point and each of the corners of the quadrilateral. The function returns true if the direction angle is in this range.
function is_intersect(pos, dir, quad)
local theta_corner, theta_min, theta_max
for i = 1, 4 do
local x, y = quad[i].x, quad[i].y
-- Find angle of line from pos to corner
theta_corner = math.atan((y - pos.y) / (x - pos.x))
-- Adjust angle to quadrant
if x < pos.x then
theta_corner = theta_corner + math.pi
elseif y < pos.y then
theta_corner = theta_corner + 2*math.pi
end
-- Keep max and min angles
if (not theta_min) or theta_corner < theta_min then
theta_min = theta_corner
end
if (not theta_max) or theta_corner > theta_max then
theta_max = theta_corner
end
end
-- Compare direction angle with max and min angles
return theta_min <= dir and dir <= theta_max
end
Here is a sample interaction:
> test = {{x = 1, y = 1}, {x = 1, y = 2}, {x = 2, y = 2}, {x = 2, y = 1}}
> position = {x = 3, y = 3}
> pi = math.pi
> is_intersect(position, 5*pi/4, test)
true
> angle = math.atan(.5)
> -- checking corners
> is_intersect(position, pi + angle, test)
true
> is_intersect(position, 3*pi/2 - angle, test)
true
> -- checking slightly inside corners
> is_intersect(position, 1.01*(pi + angle), test)
true
> is_intersect(position, .99*(3*pi/2 - angle), test)
true
> -- checking slightly outside corners
> is_intersect(position, .99*(pi + angle), test)
false
> is_intersect(position, 1.01*(3*pi/2 - angle), test)
false
How it Works
This section is for OP's benefit. Read no further if you do not want a Trigonometry refresher.
In this diagram, there are two points with directions represented by a green and a yellow arrow. The red lines connect the points with the corners of the rectangle. The is_intersect() function works by calculating the angles, measured from the positive x-axis, as in the diagram, to the lines connecting the point to each of the corners of the rectangle. You can see that there will be four such lines for each point, but only two are marked in the diagram. One of these is the largest such angle, and the other is the smallest. The direction of travel for the point is specified by an angle, also measured from the positive x-axis. If this angle is between the angles to the two red lines, then the point is on an intersecting trajectory with the rectangle. The green point is on an intersecting trajectory: the angle from the positive x-axis to the line of travel for the green point (what we might call its velocity vector) is between the other two angles for this point. But the yellow point is not on an intersecting trajectory: the angle from the positive x-axis to the line of travel for the yellow point is larger than both of the other two angles.
Now, in this diagram there are four triangles. Each of the triangles has an angle at the origin of the coordinate system. We define the tangent of this angle to be the ratio of the length of the side opposite the angle (the vertical leg of the triangle) to the length of the side adjacent to the angle (the horizontal leg). That is:
tan(A) = y/x
Furthermore, the angle A is the arctangent of y/x:
A = atan(y/x)
From this diagram, it can be seen that to find the direction angle of the line connecting the point on the trajectory to the corner of the rectangle, we can calculate the angle A from the triangle, and add 270°. We really add 3π/2 radians. For a variety of reasons which I will not go into now, radians are better. Once you get used to them, you will never use degrees for any sort of calculations ever again. If you ever study Calculus, you will have to use radians. But the practical issue at the moment is that Lua's trig functions take arguments in radians, and report angles in radians.
So, the angle A is atan(x/y). How to find x and y? By subtracting the value of the x-coordinate for the point from the x-coordinate of the rectangle corner we can find x. Similarly, by subtracting the value of the y-coordinate of the point from the y-coordinate of the rectangle corner, we can find y.
We still need to know which quadrant the angle is in so that we know how much to add to A. The quadrants are traditionally labelled with the Roman numerals from Ⅰ to Ⅳ, starting from the upper right quadrant of the x-y axis. The angle in the diagram is in quadrant Ⅳ. We can tell which quadrant the angle is in by checking to see if the point is to the left or right of the corner of the rectangle, and above or below the corner of the rectangle.
With this information, the direction angles to each of the corners from the point can be found. The largest and smallest are kept and compared with the direction angle for the trajectory of the point to see if the trajectory will intersect the rectangle.
The above exposition is a pretty good description of what the code in the is_intersect() function is doing. There is a subtlety in that the subtraction to find the sides of the triangles gives negative side-lengths in some cases. This is a normal issue in trigonometric calculations; the code needs to know how the atan() function being used handles negative values. The code under the comment -- Adjust angle to quadrant takes this into account.
For the case of a coordinate system with the origin at the upper left, you simply need to measure angles in a clockwise fashion instead. This is analagous to flipping the original coordinate system upside down. By the way, the original coordinate system (with the origin at the lower-left) is the one usually found in Mathematics, and is called a right-handed coordinate system. The other system, with the origin at the upper-left, is called a left-handed coordinate system.
Addendum
In an attempt to help OP understand these functions, I provided this simple testing code. What follows is a further attempt to explicate these tests.
The above diagram shows in part the situation represented in the tests. There is a 1X1 rectangle located in quadrant I. Note that these are screen coordinates, with the origin at the upper left. All angles are measured in a clockwise direction from the positive x-axis.
In the tests you will find:
pos1 = { x = 0, y = 0 }
pos3 = { x = 3, y = 3 }
These are two positions from which we wish to test the functions. The first position is at the origin, the second is at the location (3, 3). The red lines in the diagram help us to see the direction angles from the test points to the corners of the rectangle.
First note that A = atan(1 / 2), or A = atan(.5). This angle will show up again, so I have defined the constants:
angle = math.atan(.5)
pi = math.pi
Next notice that the angle B is (π/2 - A) radians. Convince yourself that this is true by looking at the symmetry of the situation. There is no need to convert any of these angles to degrees, in fact do not do this! This will only cause problems; the usual trig functions expect arguments in radians, and return angles in radians.
Continuing, if you look closely you should be able to see that the angle C is (π + A) radians, and the angle D is (3π/2 - A) radians.
So we have, for example, these tests:
{ pass = true, args = { pos1, pi/4, rect } },
{ pass = true, args = { pos3, 5*pi/4, rect } },
The first of these tests the trajectory from position pos1, (0, 0), with a direction angle of π/4 radians. This should intersect the corner nearest pos1. The second tests the trajectory from position pos3, (3, 3), with a direction angle of 5π/4 radians. This should intersect the corner nearest pos3. So both tests should return true.
There are also tests like:
{ pass = true, args = { pos3, pi + angle, rect } },
{ pass = true, args = { pos3, 3*pi/2 - angle, rect } },
{ pass = true, args = { pos3, 1.1*(pi + angle), rect} },
{ pass = false, args = { pos3, 1.1*(3*pi/2 - angle), rect } },
{ pass = false, args = { pos3, .99*(pi + angle), rect } },
{ pass = true, args = { pos3, .99*(3*pi/2 - angle), rect } },
Now pi + angle and 3*pi/2 - angle are the direction angles to the outside corners of the rectangle from pos3, as previously noted. So the first two tests should return true, since we expect trajectories directed at the corners to intersect the rectangle. The remaining tests make the direction angles larger or smaller. Looking back at the diagram, note that if the direction angle of the trajectory from pos3 is made larger than C, there should be an intersection, but if it is made smaller than C, there should not be an intersection. Similarly, if the direction angle of the trajectory from pos3 is made larger than D, there should be no intersection, while if it is is made smaller, there should be an intersection.
All of this is just to explain what is happening in the tests, so that you can understand them and write tests of your own. In practice, you only need to specify the quadrilateral with the quad table, a position with the pos table, and a direction with dir, which is a number in radians. You specified straight-line motion; curvilinear motion is more involved.
After all of this, I still strongly suggest, as I did earlier in the comments, that you consider using #Gene's function. To use this function you specify a direction vector instead of an angle, using a table: dir = {x = x1, y = y1). This will save you many troubles, and as you mentioned earlier, you already have x- and y-components for the direction of the trajectory. Converting this information to an angle will require you to understand how to make the correct adjustments for the quadrant of the angle. Don't mess with this right now.
Here are the tests of position pos3 for Gene's function:
{ pass = true, args = { pos3, {x=-1.01, y=-2}, rect} },
{ pass = false, args = { pos3, {x=-0.99, y=-2}, rect } },
{ pass = false, args = { pos3, {x=-2.01, y=-1}, rect } },
{ pass = true, args = { pos3, {x=-1.99, y=-1}, rect } },
Instead of worrying about angles, and referring back to the last diagram, note that arrows from pos3 to the outside corners of the rectangle can be drawn by going 1 in the negative x-direction, and 2 in the negative y-direction, or by going 2 in the negative x-direction and 1 in the negative y-direction. So to test for slightly off-corner intersections, as before, the x-components of the trajectory direction vectors are modified. For example, from pos3, the trajectory {-2, -1} should intersect at the corner nearest the y-axis. So, the trajectory {-2.01, -1} should not intersect with this corner, and the trajectory {-1.99, -1} should intersect.
One caveat. Notice that there are four tests, and not six as before. Gene's function will report that there is no intersection from pos3 with the trajectory vector {-2, -1}. This is not incorrect, but just a difference in the way his code handles corner cases. In fact, I should probably remove the tests that directly test at the corners of the rectangle for my function, and just test near the corners, which is what really matters. Both functions report the same results for all other cases. The tiniest bit away from a corner, and both functions report false; the tiniest bit inside the corners, and both functions report true.
IMO atan is a bad idea. It's expensive, and the quadrant adjustment is way too hard.
You can simplify the accepted code by using atan2 rather than atan, but that's still too expensive.
You don't really care about the angle. You care about the signs of the angles between the direction vector and the extreme vectors from current position to box corners.
For this, cross-products are cheaper, simpler, and better-suited. If you have two vectors a and b, then the quantity:
s = a.x * b.y - a.y * b.x
is positive if the acute angle from a to b is positive (counter-clockwise) and vice versa.
You need only to verify that the acute angles formed by the direction vector and lines from the current point to each of the box corners include at least one positive and one negative result.
function is_intersect(pos, dir, quad)
local n_pos, n_neg = 0, 0
for i = 1, 4 do
local dx, dy = quad[i].x - pos.x, quad[i].y - pos.y
local s = dir.x * dy - dir.y * dx
if s > 0 then n_pos = n_pos + 1 end
if s < 0 then n_neg = n_neg + 1 end
end
return n_pos > 0 and n_neg > 0
end
Sorry I'm not a Lua programmer. This is a best guess at Lua syntax using the already-accepted answer as a pattern.
I am building a simulation in which there is world made of many squares. There are also objects designated as "suns", which illuminate the squares and update their "received intensity" each step.
For example:
In this image, the distance between the centres of squares is 32 units, and the received intensity (or R.I.) of each square is calculated with this formula:
R.I. = 100 / (distance between light source and square)^2
Written in a generic(and terrible) programming language, a function that calculates R.I. may look like this:
(define (calc-RI sq-x sq-y sn-x sn-y)
(return (/ 100
(+ (square (- sq-x sn-x))
(square (- sq-y sn-y))
)
)
)
)
...and, to accommodate multiple suns, the R.I. for each sun will be calculated separately and added together for each square. This is all well and good, until I felt the need to introduce a warping mechanic to this simulation: objects that move "outside" the edge of the world will "re-enter" the world from the other side, like in the game Asteroid.
This not only need to apply to other objects in the simulation, but also light.
So, what should the new function be for calculating the R.I. of each square? (Given that the function takes in the x and y coordinate of one square and one sun, and the width and height of the world, what would be the return of the given square's R.I. under the sun's influence?)
You can solve this by imagining that the grid is surrounded by copies of the original grid, one layer for each wrap. For each square in the original grid, calculate the light intensity that falls on each corresponding square, and sum the results.
In the above diagram, the black grid represents the world, and the blue grid represents the copies of the world grid. To find the light intensity for one wrap at the green square, add the simple intensities calculated at each of the blue squares to the simple intensity calculated for the green square. To calculate another wrap, add another layer of copies.
Here is some Lua code that implements this. Stars are represented in tables of the form:
stars = { { x=x1, y=y1 }, { x=x2, y=y2 },... }
In this implementation, a square containing a star is given an intensity of -1. This could be changed so that a square containing a star accumulates intensity like any other square. To use the functions, define a stars table, and call light_universe() with the grid_size (grids are taken to be square here), square_size, stars table, and number of wraps desired. Set wraps to zero, nil, or just omit this parameter, to obtain simple intensity calculations with no wrapping. The function light_universe() returns a table containing intensities for each square of the world grid. You can view the results by calling show_intensities() with the table returned from light_universe(), and the grid_size of the table. Note that the upper-left corner of the world grid has coordinate (1, 1).
The calculate_intensity() function first calculates the field_size of the field of grid copies. This is the size of the blue grid in the diagram, and is an odd multiple of grid_size, e.g., for 1 wrap the field_size is 3. Next, the world coordinates of the current star are transformed to the field coordinates star_x and star_y (the coordinates with respect to the blue grid in the diagram). Then the locations within the field corresponding to x and y are iterated over. These are the locations of the blue squares of the diagram. The first location is in the upper-left grid of the field, and has field coordinates that are equal to the world coordinates of the square of interest. For each location, the intensity is calculated and added to the running total, which is returned when the iteration is complete.
-- Intensity calculation with wraps
-- wraps = nil or wraps = 0 for simple calculation
function calculate_intensity(star, x, y, grid_size, square_size, wraps)
wraps = wraps or 0
local field_size = grid_size * (2 * wraps + 1) -- odd number of grids
local star_x = star.x + wraps * grid_size -- cdts of star in field
local star_y = star.y + wraps * grid_size
local intensity = 0
-- x and y are cdts wrt world grid, but also wrt first grid in field
for field_y = y, field_size, grid_size do
for field_x = x, field_size, grid_size do
local dx = square_size * (star_x - field_x)
local dy = square_size * (star_y - field_y)
local dist_sq = dx * dx + dy * dy
intensity = intensity + 100 / dist_sq
end
end
return intensity
end
function light_universe(grid_size, square_size, stars, wraps)
wraps = wraps or 0
local grid_intensities = {}
for i, star in ipairs(stars) do
for y = 1, grid_size do
grid_intensities[y] = grid_intensities[y] or {}
for x = 1, grid_size do
if x == star.x and y == star.y then
grid_intensities[y][x] = -1
elseif grid_intensities[y][x] ~= -1 then
grid_intensities[y][x] = (grid_intensities[y][x] or 0) +
calculate_intensity(star, x, y, grid_size, square_size, wraps)
end
end
end
end
return grid_intensities
end
function show_intensities(grid, grid_size)
for y = 1, grid_size do
for x = 1, grid_size do
local intensity = grid[y][x]
local fmt
if intensity ~= -1 then
fmt = (string.format("%10.5f", intensity))
else
fmt = string.format("%-10s", " Star")
end
io.write(fmt)
end
print()
end
end
Here is an interaction showing intensity with no wraps, corresponding to the example from your question.
> stars = { { x=1, y=1 } }
> light_grid = light_universe(3, 32, stars, 0)
> show_intensities(light_grid, 3)
Star 0.09766 0.02441
0.09766 0.04883 0.01953
0.02441 0.01953 0.01221
Here is the same situation with one wrap:
> light_grid = light_universe(3, 32, stars, 1)
> show_intensities(light_grid, 3)
Star 0.17054 0.16628
0.17054 0.12440 0.12023
0.16628 0.12023 0.11630
And with two wraps:
> light_grid = light_universe(3, 32, stars, 2)
> show_intensities(light_grid, 3)
Star 0.20497 0.20347
0.20497 0.15960 0.15811
0.20347 0.15811 0.15664
Here is a 7X7 grid with two stars, and one wrap:
> stars = { { x=1, y=1 }, { x=7, y=4 } }
> light_grid = light_universe(7, 32, stars, 1)
> show_intensities(light_grid, 7)
Star 0.13085 0.05729 0.04587 0.04728 0.06073 0.13366
0.14064 0.08582 0.05424 0.04640 0.04971 0.06411 0.09676
0.09676 0.06411 0.04971 0.04640 0.05424 0.08582 0.14064
0.13366 0.06073 0.04728 0.04587 0.05729 0.13085 Star
0.08469 0.05562 0.04574 0.04433 0.05218 0.08190 0.13222
0.06715 0.05619 0.04631 0.04302 0.04635 0.05627 0.06728
0.13073 0.08043 0.05075 0.04294 0.04439 0.05432 0.08347
This issue I can't really solve till now although I read through several articles already - hope somebody can help here.
Facts (know variables):
Two moving objects on earth surface, both with current know latitude/longitude coordinates.
The speed of both objects is know as well (in m/s).
The direction (angle) of one object is know.
Now I want to calculate the direction (angle) of the second moving object needed to intersect with (hit) the other moving object.
As the distance between the objects is small (in the range of only 5-20 km) and no very high accuracy is needed, it is OK to consider the earth surface as plane.
Therefore I already tried working with this great library:
http://www.codeproject.com/Articles/990452/Interception-of-Two-Moving-Objects-in-D-Space
But I don't really get that to work as I don't know how to convert speed in m/s back and forth to latitude/longitude velocity vectors.
To better understand the problem here an example with values:
Moving object 1 (runner):
Current location: latitude: 38.565, longitude: -98.513
Speed: 100 m/s
Direction: 270°
Moving object 2 (chaser):
Current location: latitude: 38.724, longitude: -98.449
Speed: 150 m/s
Direction: To be calculated
Any help on that would be highly appreciated, thanks in advance!
If the distances are small and you convert the latitude/longitude coordinates to x,y coordinates on a flat plane as you suggest, using e.g. the answers to this question, then the math needed to solve this problem is quite straightforward.
The image below shows the location of chaser and target at time 0 (red and blue dot), their velocity (the circles show their range after time 1, 2, 3...) and the trajectory of the target (blue ray).
The green curve contains all locations where target and chaser could be at the same moment if they keep moving at their current velocity. The intersection of this curve with the target's trajectory is the interception point (pink dot).
If the interception happens after time t, then the distance travelled by chaser and target is t.vc and t.vt. We also know the distance d between the chaser and target at time 0, and the angle α between the line segment connecting chaser and target and the target's trajectory. If we enter these into the cosine rule we get:
(t . vc)2 = (t . vt)2 + d2 - 2 . d . t . vt . cos(α)
Which transforms to this quadratic equation when we solve for time t:
(vc2 - vt2) . t2 + (2 . d . vt . cos(α)) . t - d2 = 0
If we solve this using the quadratic formula:
t = (- b ± √(b2 - 4 . a . c)) / (2 . a)
where:
a = vc2 - vt2
b = 2 . d . vt . cos(α)
c = - d2
a non-negative discriminant means interception is possible, and the root obtained by using addition in the quadratic formula is the first or only time of interception.
As you can see in the image above, if the chaser is slower than the target, there are potentially two interception points if the target moves towards the chaser (blue ray intersects with green curve), but none if the target moves away from the chaser. Using addition in the quadratic formula gives the first interception point (using subtraction would give the second).
We can then calculate the position of the target after time t, which is the interception point, and the direction from the chaser to this point.
The JavaScript code snippet below demonstrates this method, with the values from both images. It uses angles in radians and the orientation: 0 = right, π/2 = up, π = left, -π/2 = down. The case where the chaser and target have equal velocity (and the curve is a straight line) is solved using the isosceles triangle theorem, because otherwise it would lead to a division by zero in the quadratic equation.
function intercept(chaser, target) {
var dist = distance(chaser, target);
var dir = direction(chaser, target);
var alpha = Math.PI + dir - target.dir;
// EQUAL VELOCITY CASE: SOLVE BY ISOSCELES TRIANGLE THEOREM
if (chaser.vel == target.vel) {
if (Math.cos(alpha) < 0) return NaN; // INTERCEPTION IMPOSSIBLE
return (dir + alpha) % (Math.PI * 2);
}
// GENERAL CASE: SOLVE BY COSINE RULE AND QUADRATIC EQUATION
var a = Math.pow(chaser.vel, 2) - Math.pow(target.vel, 2);
var b = 2 * dist * target.vel * Math.cos(alpha);
var c = -Math.pow(dist, 2);
var disc = Math.pow(b, 2) - 4 * a * c;
if (disc < 0) return NaN; // INTERCEPTION IMPOSSIBLE
var time = (Math.sqrt(disc) - b) / (2 * a);
var x = target.x + target.vel * time * Math.cos(target.dir);
var y = target.y + target.vel * time * Math.sin(target.dir);
return direction(chaser, {x: x, y: y});
function distance(p, q) {
return Math.sqrt(Math.pow(p.x - q.x, 2) + Math.pow(p.y - q.y, 2));
}
function direction(p, q) {
return Math.atan2(q.y - p.y, q.x - p.x);
}
}
var chaser = {x: 196, y: -45, vel: 100};
var target = {x: 139, y: -312, vel: 75, dir: 0.1815142422};
document.write(intercept(chaser, target) + "<br>"); // -1.015 rad = -58.17°
var chaser = {x: 369, y: -235, vel: 37.5};
var target = {x: 139, y: -376, vel: 75, dir: 0.1815142422};
document.write(intercept(chaser, target) + "<br>"); // -1.787 rad = -102.4°
other interception points
The green curve effectively divides the 2D plane into a zone where the target will arrive first, and a zone where the chaser will arrive first. If you want the chaser and the target to move at constant speed and collide (imagine e.g. a torpedo being fired at a moving ship) then you'd aim for a point on the curve, where the two will arrive at the same time, as explained above.
However, if the chaser can go to a point and wait there for the target to arrive (imagine e.g. a person trying to catch a bus), then every point on the target's trajectory that is within the "chaser's zone" could be the interception point.
In the first image (slower target) the curve is a circle around the target, and once the target moves out of this circle (to the right of the interception point indicated in pink), the chaser could always get there first and wait for the target. This could be useful if you want a safety margin in case the chaser's or target's speed isn't constant.
In the second image (faster target) the curve is a circle around the chaser, and every point on the target's trajectory inside this circle could be the interception point. The chaser could e.g. move perpendicular to the target's trajectory, in order to minimise the distance travelled, or aim at a point halfway between the first and last interception point to maximise the waiting time.
How can I go about trying to order the points of an irregular array from top left to bottom right, such as in the image below?
Methods I've considered are:
calculate the distance of each point from the top left of the image (Pythagoras's theorem) but apply some kind of weighting to the Y coordinate in an attempt to prioritise points on the same 'row' e.g. distance = SQRT((x * x) + (weighting * (y * y)))
sort the points into logical rows, then sort each row.
Part of the difficulty is that I do not know how many rows and columns will be present in the image coupled with the irregularity of the array of points. Any advice would be greatly appreciated.
Even though the question is a bit older, I recently had a similar problem when calibrating a camera.
The algorithm is quite simple and based on this paper:
Find the top left point: min(x+y)
Find the top right point: max(x-y)
Create a straight line from the points.
Calculate the distance of all points to the line
If it is smaller than the radius of the circle (or a threshold): point is in the top line.
Otherwise: point is in the rest of the block.
Sort points of the top line by x value and save.
Repeat until there are no points left.
My python implementation looks like this:
#detect the keypoints
detector = cv2.SimpleBlobDetector_create(params)
keypoints = detector.detect(img)
img_with_keypoints = cv2.drawKeypoints(img, keypoints, np.array([]), (0, 0, 255),
cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
points = []
keypoints_to_search = keypoints[:]
while len(keypoints_to_search) > 0:
a = sorted(keypoints_to_search, key=lambda p: (p.pt[0]) + (p.pt[1]))[0] # find upper left point
b = sorted(keypoints_to_search, key=lambda p: (p.pt[0]) - (p.pt[1]))[-1] # find upper right point
cv2.line(img_with_keypoints, (int(a.pt[0]), int(a.pt[1])), (int(b.pt[0]), int(b.pt[1])), (255, 0, 0), 1)
# convert opencv keypoint to numpy 3d point
a = np.array([a.pt[0], a.pt[1], 0])
b = np.array([b.pt[0], b.pt[1], 0])
row_points = []
remaining_points = []
for k in keypoints_to_search:
p = np.array([k.pt[0], k.pt[1], 0])
d = k.size # diameter of the keypoint (might be a theshold)
dist = np.linalg.norm(np.cross(np.subtract(p, a), np.subtract(b, a))) / np.linalg.norm(b) # distance between keypoint and line a->b
if d/2 > dist:
row_points.append(k)
else:
remaining_points.append(k)
points.extend(sorted(row_points, key=lambda h: h.pt[0]))
keypoints_to_search = remaining_points
Jumping on this old thread because I just dealt with the same thing: sorting a sloppily aligned grid of placed objects by left-to-right, top to bottom location. The drawing at the top in the original post sums it up perfectly, except that this solution supports rows with varying numbers of nodes.
S. Vogt's script above was super helpful (and the script below is entirely based on his/hers), but my conditions are narrower. Vogt's solution accommodates a grid that may be tilted from the horizontal axis. I assume no tilting, so I don't need to compare distances from a potentially tilted top line, but rather from a single point's y value.
Javascript below:
interface Node {x: number; y: number; width:number; height:number;}
const sortedNodes = (nodeArray:Node[]) => {
let sortedNodes:Node[] = []; // this is the return value
let availableNodes = [...nodeArray]; // make copy of input array
while(availableNodes.length > 0){
// find y value of topmost node in availableNodes. (Change this to a reduce if you want.)
let minY = Number.MAX_SAFE_INTEGER;
for (const node of availableNodes){
minY = Math.min(minY, node.y)
}
// find nodes in top row: assume a node is in the top row when its distance from minY
// is less than its height
const topRow:Node[] = [];
const otherRows:Node[] = [];
for (const node of availableNodes){
if (Math.abs(minY - node.y) <= node.height){
topRow.push(node);
} else {
otherRows.push(node);
}
}
topRow.sort((a,b) => a.x - b.x); // we have the top row: sort it by x
sortedNodes = [...sortedNodes,...topRow] // append nodes in row to sorted nodes
availableNodes = [...otherRows] // update available nodes to exclude handled rows
}
return sortedNodes;
};
The above assumes that all node heights are the same. If you have some nodes that are much taller than others, get the value of the minimum node height of all nodes and use it instead of the iterated "node.height" value. I.e., you would change this line of the script above to use the minimum height of all nodes rather that the iterated one.
if (Math.abs(minY - node.y) <= node.height)
I propose the following idea:
1. count the points (p)
2. for each point, round it's x and y coordinates down to some number, like
x = int(x/n)*n, y = int(y/m)*m for some n,m
3. If m,n are too big, the number of counts will drop. Determine m, n iteratively so that the number of points p will just be preserved.
Starting values could be in alignment with max(x) - min(x). For searching employ a binary search. X and Y scaling would be independent of each other.
In natural words this would pin the individual points to grid points by stretching or shrinking the grid distances, until all points have at most one common coordinate (X or Y) but no 2 points overlap. You could call that classifying as well.
Think of a 2D grid, e.g. in the size of 1000x1000 cells, which is used as the map of a level in a game. This map is dynamically filled with game objects during runtime. Now we need to calculate the probability of placing a new object into the a given x/y position in this grid.
What I have already is an int array the holds the number of game objects in close distance to the cell at x/y. The index of this array represents the cell distance to the given cell, and each value in the array tells the number of game objects in the grid at that distance. So for example the array could look like this:
0, 0, 1, 2, 0, 3, 1, 0, 4, 0, 1
This would mean that 0 objects are in the grid cell at x/y itself, 0 objects are in the direct neighbour cells, 1 object is in a cell with a distance of two cells, 2 objects are in the cells of a distance of three cells, and so on. The following figure illustrates this example:
The task now is to calculate how likely it is to place a new object at x/y, based on the values in this array. The algorithm should be something like this:
if at least one object is already closer than min, then the probability must be 0.0
else if no object is within a distance of max, then the probability must be 1.0
else the probability depends on how many objects are close to x/y, and how many.
So in other words: if there is at least one game object already very close, we don't want a new one. On the other hand if there is no object within a max radius, we want a new object in any case. Or else we want to place a new object with a probability depending on how many other objects there are close to x/y -- the more objects are close, and the closer they are, the less likely we want to place a new object.
I hope my description was understandable.
Can you think of an elegent algorithm or formula to calculate this probability?
PS: Sorry for the title of this question, I don't know how to summarize my question better.
One approach I'd consider is to compute a "population density" for that square. The lower the population density, the higher the probability that you would place an item there.
As you say, if there is an item at (x,y), then you can't place an item there. So consider that a population density of 1.0.
At the next level out there are 8 possible neighbors. The population density for that level would be n/8, where n is the number of items at that level. So if there are 3 objects that are adjacent to (x,y), then the density of that level is 3/8. Divide that by (distance+1).
Do the same for all levels. That is, compute the density of each level, divide by (distance+1), and sum the results. The divisor at each level is (distance*8). So your divisors are 8, 16, 24, etc.
Once you compute the results, you'll probably want to play with the numbers a bit to adjust the probabilities. That is, if you come up with a sum of 0.5, that space is likely pretty crowded. You wouldn't want to use (1-density) as your probability for generating an item. But the method I outline above should give you a single number to play with, which should simplify the problem.
So the algorithm looks something like:
total_density = 0;
for i = 0; i < max; ++i
if (i == 0)
local_density = counts[i]
else
local_density = counts[i]/(i*8); // density at that level
total_density = total_density + (local_density/(i+1))
If dividing the local density by (i+1) over-exaggerates the effect of distance, consider using something like log(i+1) or sqrt(i+1). I've found that to be useful in other situations where the distance is a factor, but not linearly.
lets assume your array's name is distances.
double getProbability()
{
for(int i=0 ; i<min ; i++)
{
if(distances[i]!=0) return 0;
}
int s = 0;
bool b = true;
for(int i=min ; i<max ; i++)
{
b = b && (distances[i]==0)
s+= distances[i]/(i+1);
}
if(b) return 1;
for(int i=0 ; i<distances.Count() ; i++)
{
s+= distances[i]/(i+1);
}
else return (float)s/totalObjectNum;
}
This approach calculates a weighted sum of those objects in a distance > min and <= max.
Parallel an upper limit is calculated (called normWeight) which depends only on max.
If at least one object is in a distance > min and <= max then
the probability closest to 1 would be 1-(1/normWeight) for 1 object on the outer ring.
The minimal probability would be 1-((normWeight-1)/normWeight). E.g. for
max-1 objects on the outer ring.
The calculation of the weighted sum can be modified by calculating different values for the variable delta.
float calculateProbabilty()
{
vector<int> numObjects; // [i] := number of objects in a distance i
fill numObjects ....
// given:
int min = ...;
int max = ...; // must be >= min
bool anyObjectCloserThanMin = false;
bool anyObjectCloserThanMax = false;
// calculate a weighted sum
float sumOfWeights = 0.0;
float normWeight = 0.0;
for (int distance=0; distance <= max; distance++)
{
// calculate a delta-value for increasing sumOfWeights depending on distance
// the closer the object the higher the delta
// e.g.:
float delta = (float)(max + 1 - distance);
normWeight += delta;
if (numObjects[distance] > 0 && distance < min)
{
anyObjectCloserThanMin = true;
break;
}
if (numObjects[distance] > 0)
{
anyObjectCloserThanMax = true;
sumOfWeights += (float)numObjects[distance] * delta;
}
}
float probability = 0.0;
if (anyObjectCloserThanMin)
{
// if at least one object is already closer than min, then the probability must be 0.0
probability = 0.0;
}
else if (!anyObjectCloserThanMax)
{
// if no object is within a distance of max, then the probability must be 1.0
probability = 1.0;
}
else
{
// else the probability depends on how many objects are close to x/y
// in this scenario normWeight defines an upper limited beyond that
// the probability becomes 0
if (sumOfWeights >= normWeight)
{
probability = 0.0;
}
else
{
probability = 1. - (sumOfWeights / normWeight);
// The probability closest to 1 would be 1-(1/normWeight) for 1 object on the outer ring.
// The minimal probability would be 1-((normWeight-1)/normWeight). E.g. for
// max-1 objects on the outer ring.
}
}
return probability;
}
A simple approach could be:
1 / (sum over the number of all neighbours in [min, max] weighted by their distance to x/y + 1).
By weighted I mean that the number of those neighbours whose distance to x/y is smaller is multiplied by a bigger factor that the number of those, that are not so close. As weight you could for example take (max+1)-distance.
Note that once you compute the object density (see "population density" or "weighted sum of those objects in a distance" in previous answers), you still need to transform this value to a probability in which to insert new objects (which is not treated so comprehensivelly in other answers).
The probability function (PDF) needs to be defined for all possible values of object density, i.e. on closed interval [0, 1], but otherwise it can be shaped towards any goal you desire (see illustrations), e.g.:
to move the current object density towards a desired maximum object density
to keep overall probability of insertion constant, while taking the local object density into account
If you want to experiment with various goals (PDF function shapes - linear, quadratic, hyperbole, circle section, ...), you might wish to have a look at factory method pattern so you can switch between implementations while calling the same method name, but I wanted to keep things simpler in my example, so I implemented only the 1st goal (in python):
def object_density(objects, min, max):
# choose your favourite algorithm, e.g.:
# Compute the density for each level of distance
# and then averages the levels, i.e. distance 2 object is
# exactly 8 times less significant from distance 1 object.
# Returns float between 0 and 1 (inclusive) for valid inputs.
levels = [objects[d] / (d * 8) for d in range(min, max + 1)]
return sum(levels) / len(levels)
def probability_from_density(desired_max_density, density):
# play with PDF functions, e.g.
# a simple linear function
# f(x) = a*x + b
# where we know 2 points [0, 1] and [desired_max_density, 0], so:
# 1 = 0 + b
# 0 = a*desired_max_density + b
# Returns float betwen 0 and 1 (inclusive) for valid inputs.
if density >= desired_max_density:
return 0.0
a = -1 / desired_max_density
b = 1
return a * density + b
def main():
# distance 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
objects = [0, 0, 1, 2, 0, 3, 1, 0, 4, 0, 1]
min = 2
max = 5
desired_max_density = 0.1
if sum(objects[:min]): # when an object is below min distance
return 0.0
density = object_density(objects, min, max) # 0,0552
probability = probability_from_density(desired_max_density, density) # 0,4479
return probability
print(main())