Python & Algorithm: How to do simple geometry shape match? - python

Given a set of points (with order), I want to know if its shape is within certain types. The types are:
rectangle = [(0,0),(0,1),(1,1),(1,0)]
hexagon = [(0,0),(0,1),(1,2),(2,1),(2,0),(1,-1)]
l_shape = [(0,0),(0,3),(1,3),(1,1),(3,1),(3,0)]
concave = [(0,0),(0,3),(1,3),(1,1),(2,1),(2,3),(3,3),(3,0)]
cross = [(0,0),(0,-1),(1,-1),(1,0),(2,0),(2,1),(1,1),(1,2),(0,2),(0,1),(-1,1),(-1,0)]
For example, give roratated_rectangle = [(0,0),(1,1),(0,2),(-1,1)]
We'll know it belongs to the rectangle above.
is similar to
Notice:
rotation and edge with different length are considered similar.
Input points are ordered. (so they can be draw by path in polygon module)
How can I do it? Is there any algorithm for this?
What I'm thinking:
Maybe we can reconstruct lines from given points. And from lines, we can get angles of a shape. By comparing the angle series (both clockwise and counterclockwise), we can determine if input points are similar to the types given above.

Your thinking is basically the right idea. You want to compare the sequence of angles in your test shape to the sequence of angles in a predefined shape (for each of your predefined shapes). Since the first vertex of your test shape may not correspond to the first vertex of the matching predefined shape, we need to allow that the test shape's sequence of angles may be rotated relative to the predefined shape's sequence. (That is, your test shape's sequence might be a,b,c,d but your predefined shape is c,d,a,b.) Also, the test shape's sequence may be reversed, in which case the angles are also negated relative to the predefined shape's angles. (That is, a,b,c,d vs. -d,-c,-b,-a or equivalently 2π-d,2π-c,2π-b,2π-a.)
We could try to choose a canonical rotation for the sequence of angles. For example, we could find the lexicographically minimal rotation. (For example, l_shape's sequence as given is 3π/2,3π/2,π/2,3π/2,3π/2,3π/2. The lexicographically minimal rotation puts π/2 first: π/2,3π/2,3π/2,3π/2,3π/2,3π/2.)
However, I think there's a risk that floating-point rounding might result in us choosing different canonical rotations for the test shape vs. the predefined shape. So instead we'll just check all the rotations.
First, a function that returns the sequence of angles for a shape:
import math
def anglesForPoints(points):
def vector(tail, head):
return tuple(h - t for h, t in zip(head, tail))
points = points[:] + points[0:2]
angles = []
for p0, p1, p2 in zip(points, points[1:], points[2:]):
v0 = vector(tail=p0, head=p1)
a0 = math.atan2(v0[1], v0[0])
v1 = vector(tail=p1, head=p2)
a1 = math.atan2(v1[1], v1[0])
angle = a1 - a0
if angle < 0:
angle += 2 * math.pi
angles.append(angle)
return angles
(Note that computing the cosines using the dot product would not be sufficient, because we need a signed angle, but cos(a) == cos(-a).)
Next, a generator that produces all rotations of a list:
def allRotationsOfList(items):
for i in xrange(len(items)):
yield items[i:] + items[:i]
Finally, to determine if two shapes match:
def shapesMatch(shape0, shape1):
if len(shape0) != len(shape1):
return False
def closeEnough(a0, a1):
return abs(a0 - a1) < 0.000001
angles0 = anglesForPoints(shape0)
reversedAngles0 = list(2 * math.pi - a for a in reversed(angles0))
angles1 = anglesForPoints(shape1)
for rotatedAngles1 in allRotationsOfList(angles1):
if all(closeEnough(a0, a1) for a0, a1 in zip(angles0, rotatedAngles1)):
return True
if all(closeEnough(a0, a1) for a0, a1 in zip(reversedAngles0, rotatedAngles1)):
return True
return False
(Note that we need to use a fuzzy comparison because of floating point rounding errors. Since we know the angles will always be in the small fixed range 0 … 2π, we can use an absolute error limit.)
>>> shapesMatch([(0,0),(1,1),(0,2),(-1,1)], rectangle)
True
>>> shapesMatch([(0,0),(1,1),(0,2),(-1,1)], l_shape)
False
>>> shapesMatch([(0,0), (1,0), (1,1), (2,1), (2,2), (0,2)], l_shape)
True
If you want to compare the test shape against all the predefined shapes, you may want to compute the test shape's angle sequence just once. If you're going to test many shapes against the predefined shapes, you may want to precompute the predefined shapes' sequences just once. I leave those optimizations as an exercise for the reader.

Your shapes may not only be rotated, they are also translated and may even be scaled. The orientation of the nodes may also differ. Fo example your original square has an edge length of 1.0 and is defined anticlockwise, whereas your diamond shape has an edge length of 1.414 and is defined clockwise.
You need to find a good reference for comparison. The following should work:
Find the centre of gravity C for each shape.
Determine radial coordinates (r, φ) for all nodes, where the origin of the radial coordinate system is the centre of gravity C.
Normalise the radii for each shape so that the maximum value of r in a shape is 1.0
Ensure that the nodes are oriented anticlockwise, i.e. with increasing angles φ.
Now you have two lists of n radial coordinates. (The cases where the number of nodes in a shape don't match or where there are fewer than three nodes should have been ruled out already.)
Evaluate all n configurations of offsets, where you leave the first array as it is and shift the second. For a four-element array, you compare:
{a1, a2, a3, a4} <=> {b1, b2, b3, b4}
{a1, a2, a3, a4} <=> {b2, b3, b4, b1}
{a1, a2, a3, a4} <=> {b3, b4, b1, b2}
{a1, a2, a3, a4} <=> {b4, b1, b2, b3}
The radial coordinates are floating-point numbers . When you compare values, you should allow for some latitude to cater for inaccuracies introduced by the floating-point math. Because the numbers 0 ≤ r ≤ 1 and −π ≤ φ ≤ π are roughly in the same range, you can used a fixed epsilon for that.
Radii are compared by their normalised value. Angles are compared by their difference to the angle of the previous point in the list. When that difference is negative, we have wrapped around the 360° border and have to adjust it. (We must enforce positive angles differences, because the shape we compare to might not be equally rotated and thus may not have the wrap-around gap.) The angle is allowed to go both forwards and backwards, but must eventually come full circle.
The code has to check n configurations and test all n nodes for each. In practice, mismatches will be found early, so that the code should have okay performance. If you are going to compare a lot of shapes, it might be worth creating the normalised, anticlockwise radial representation for all shapes beforehand.
Anyway, here goes:
def radial(x, y, cx = 0.0, cy = 0.0):
"""Return radial coordinates from Cartesian ones"""
x -= cx
y -= cy
return (math.sqrt(x*x + y*y), math.atan2(y, x))
def anticlockwise(a):
"""Reverse direction when a is clockwise"""
phi0 = a[-1]
pos = 0
neg = 0
for r, phi in a:
if phi > phi0:
pos += 1
else:
neg += 1
phi0 = phi
if neg > pos:
a.reverse()
def similar_r(ar, br, eps = 0.001):
"""test two sets of radial coords for similarity"""
_, aprev = ar[-1]
_, bprev = br[-1]
for aa, bb in zip(ar, br):
# compare radii
if abs(aa[0] - bb[0]) > eps:
return False
# compare angles
da = aa[1] - aprev
db = bb[1] - bprev
if da < 0: da += 2 * math.pi
if db < 0: db += 2 * math.pi
if abs(da - db) > eps:
return False
aprev = aa[1]
bprev = bb[1]
return True
def similar(a, b):
"""Determine whether two shapes are similar"""
# Only consider shapes with same number of points
if len(a) != len(b) or len(a) < 3:
return False
# find centre of gravity
ax, ay = [1.0 * sum(x) / len(x) for x in zip(*a)]
bx, by = [1.0 * sum(x) / len(x) for x in zip(*b)]
# convert Cartesian coords into radial coords
ar = [radial(x, y, ax, ay) for x, y in a]
br = [radial(x, y, bx, by) for x, y in b]
# find maximum radius
amax = max([r for r, phi in ar])
bmax = max([r for r, phi in br])
# and normalise the coordinates with it
ar = [(r / amax, phi) for r, phi in ar]
br = [(r / bmax, phi) for r, phi in br]
# ensure both shapes are anticlockwise
anticlockwise(ar)
anticlockwise(br)
# now match radius and angle difference in n cionfigurations
n = len(a)
while n:
if similar_r(ar, br):
return True
br.append(br.pop(0)) # rotate br by one
n -= 1
return False
Edit: While this solution works, it is overcomplicated. Rob's answer is similar in essence, but uses a straightforward metric: the internal angles between the edges, which takes care of translation and scaling automatically.

Approaching this by linear algebra, each point will be a vector (x,y), which can be multiplied into a rotation matrix to get new coordinates (x1,y1) by the designated angle. There does not appear to be LaTeX support here so I can't write it out clearly, but in essence:
(cos(a) -sin(a);sin(a) cos(a))*(x y) = (x1 y1)
This results in coordinates x1, y1 that are rotated by angle "a".
EDIT: This is perhaps the underlying theory, but you can set up an algorithm to shift the shape point-by-point to (0,0), then compute the cosine of the angle between adjacent points to classify the object.

Related

Computing random pair with Euclidean distance less than certain value

I am trying to generate two (1D) points x1, x2 chosen randomly, and independently, from the uniform distribution U(-1,1) such that the euclidean distance between them is less than a certain value, dist. Here is one solution, but I'm looking for something more efficient:
def point_pair(low_=-1, hight_=1, dist = 0.001):
while(1):
x = np.random.uniform(low=low_, high=hight_)
y = np.random.uniform(low=low_, high=hight_)
length = np.linalg.norm(x-y)
if length <= dist:
return x,y
return 0,0
To generate two scalars whose magnitudes are close to each other:
import numpy as np
def point_pair(low, high, dist):
delta = np.random.uniform(-dist, dist)
a = np.random.uniform(low + dist, high - dist)
b = np.random.choice((-1, 1)) * a + delta
return a, b
Your scalar a is generated almost in the same way, but where the bounds are not [-1, 1) but [-1 + dist, 1 - dist). For b, you generate a new scalar which is uniformly distributed from -dist to +dist. This represents the bounds on the maximum distance away from a in either direction on the real line that you allow. Then b is simply k*a + delta, where again, delta is any value between -dist and +dist, and k is either -1 or 1.
This will ensure that both a and b are in [-1, 1) and that their magnitudes are similar, or ||a| - |b|| <= dist.
Note
The np.random.uniform(low, high) function always returns values in [low, high) so if you want to also include your upper bound, you'll need to use a different method.
use polar coordinates, your random numbers are angle and distance.

Signed angle between directed line segments

I have an algorithm in which I need to work out the signed angle (-180 to 180) between edges in a graph. I've done some research and found plenty of specific answers but can't figure out how to relate them to my situation (e.g. this question which uses atan2, however the OP wanted only positive angles)
I've tried implementing a few different ways (using atan2 or arccos) but I'm struggling to relate the examples to my specific problem. I've tried treating the edges as vectors but got strange results.
Given a graph with points (A, B, C, D, E), and the average of those points (avg)... how do I find the signed angle between one of those points (e.g. A) and the other points (e.g. B, C, D, E), taking the angle from the current origin (A) to the 'avg' point as equal to 0 degrees. Example below...
...in this example, the anti-clockwise angle from (A, avg) to (A, B) would be positive something (between 0 and 180), and the angle from (A, avg) to (A, E) would be negative something (between 0 and -180).
Ideally I want a formula which I could also apply to defining any of the points as the origin, for example taking point C as the origin.. the 'zero angle' would be (C, avg) and the angle between (C, avg) and (C, A) would be negative (0 to -180) and the angle between (C, avg) and (C, E) would be positive (0 to 180).
I haven't studied math beyond high-school so I find it hard to decipher equations with symbols I don't understand.
UPDATE: Thought I'd clean this up to make it more obvious what the conclusion was.
I made two small changes to the accepted answer, resulting in the below snippet:
def angle(vertex, start, dest):
AhAB = math.atan2((dest.y - vertex.y), (dest.x - vertex.x))
AhAO = math.atan2((start.y - vertex.y), (start.x - vertex.x))
AB = AhAB - AhAO
# in between 0-math.pi = do nothing, more than math.pi = +(-2 * math.pi), less than zero = do nothing
AB = math.degrees(AB + (-2 * math.pi if AB > math.pi else (2 * math.pi if AB < 0 - math.pi else 0)))
return AB
...the final one-liner may be a bit much to grok after a few months of not working on this, so I turned it into it's own function, taking the result of AB = AhAB - AhAO as it's argument...
def calc(ab):
if ab > math.pi:
return ab + (-2 * math.pi)
else:
if ab < 0 - math.pi:
return ab + (2 * math.pi)
else:
return ab + 0
I thought this was a little clearer to read, though more lines.
The final function in full:
def angle(vertex, start, dest):
"""Calculates the signed angle between two edges with the same origin.
Origin is the 'vertex' argument, 'start' is the bounding point of the edge to calculate the angle from.
Positively signed result means anti-clockwise rotation about the vertex."""
def calc_radians(ab):
if ab > math.pi:
return ab + (-2 * math.pi)
else:
if ab < 0 - math.pi:
return ab + (2 * math.pi)
else:
return ab + 0
AhAB = math.atan2((dest.y - vertex.y), (dest.x - vertex.x))
AhAO = math.atan2((start.y - vertex.y), (start.x - vertex.x))
res = calc_radians(AhAB - AhAO)
return math.degrees(res)
Note: The function assumes the three arguments will all be instances of a typical Point class with x and y attributes.
Also, the example graph above has only positive values, but I am fairly sure that this works with graphs that involve negative values too.
I read your problem statement as follows: given 2 points A and B, and a center O, find the angle A to B as the angle, positive if anticlockwise, between the vectors A→O and A→B.
If my premises are correct, then you can
find the angle between A→B and a horizontal, rightward line passing in A,
find the angle between A→O and a horizontal, rightward line passing in A,
find the angle A to B as the difference of said angles,
normalize the result range so that it's between -π and +π.
What I've said can be visualized as follows
or in code (assuming a Point class with attributes x and y)
AhAB = math.atan2((B.y-A.y), (B.x-A.x)) # -π < AhAB ≤ +π
AhAO = math.atan2((O.y-A.y), (O.x-A.x)) # -π < AhA) ≤ +π
AB = AhAB - AhAO # -2π < AB ≤ +2π
AB = AB + ( 2*math.pi if AB < math.pi else (-2*math.pi if AB> math.pi else 0))
Addendum
Here it is a small code example, the position of the points is just similar to what you can see in the picture
In [18]: from math import atan2, pi
In [21]: class Point():
...: def __init__(self, x, y):
...: self.x, self.y = x, y
...: def __repr__(self):
...: return '(%s, %s)'%(self.x, self.y)
In [22]: A = Point(0.0, 0.0)
In [23]: B = Point(-2.0, 2.0)
In [24]: O = Point(0.0, -3.0)
In [25]: AhAB = atan2((B.y-A.y), (B.x-A.x)) ; print(3/4, AhAB/pi)
0.75 0.75
In [26]: AhAO = atan2((O.y-A.y), (O.x-A.x)) ; print(-1/2, AhAO/pi)
-0.5 -0.5
In [27]: AB = AhAB - AhAO ; print(5/4, AB/pi)
1.25 1.25
In [28]: AB = AB + ( 2*pi if AB < pi else (-2*pi if AB> pi else 0)) ; print(AB/pi)
-0.75
In [29]:
The last line normalize your result AB to be in the correct range -π < AB ≤ π, adding or subtracting 2π that doesn't change the meaning of the measured angle.
The definition of positive and negative angles is heavily depending on the reference system or reference point. Despite of its 'correct' definition, the basic calculation can be pretty much done based on the slope between two points and the resulting angle of incline which can be calculated by applying the inverse tan to the slope.
Applying the inverse tan in programming can be a bit annoying since many programming languages provide two different functions for this:
arctan or atan which is implemented in Python's math.atan() or numpy.atan()
arctan2 or atan2 which is delivered by math.atan2() or numpy.atan2()
Both of these functions, regardless of the implementation in the math module or numpy package, return the calculated angle in radians which is basically based on the number Pi instead of degrees which makes some further conversion necessary. This can either be done manually or by applying a function like numpy.rad2deg(). To get a basic idea of the data points and to get some eye-balled estimation for the calculated results, I suggest plotting the data point by using matplotlib.
Glueing all the before-mentioned considerations into code can look like this:
import pandas as pd
import matplotlib
import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt
# Define some sample data points
coords = {
'A': (1.5, 3.0),
'B': (3.0, 5.0),
'C': (5.5, 4.5),
'D': (5.8, 2.2),
'E': (2.8, 1.2)
}
# Extract data values from `coords` dict
values = np.array(list(coords.values()))
# Calculate the averaged point of all data points
avg = np.mean(values, axis=0)
# Plot sample data for better overview
for k, v in coords.items():
plt.plot(*v, marker='o', linestyle='')
plt.text(*v, k)
plt.plot(*avg, marker='o', linestyle='')
plt.text(*avg, 'avg')
plt.show()
# For further information about slope and angle of incline
# see Wikipedia (https://en.wikipedia.org/wiki/Slope).
# Calculating the angle from `avg` to each point. Please adopt
# to your own needs if needed for other pairs of points.
# Calculate the distance in x- and y-direction from each point to point `avg`
distances_x_y = (values - avg)
# Depending on your definition of the 'reference point' consider using
# distances_x_y = (avg - values)
# For further explanation on `atan` and `atan2` see
# https://stackoverflow.com/q/35749246/3991125 and
# https://en.wikipedia.org/wiki/Atan2 .
# Using a for loop instead of numpy's array/vectors is not very elegant,
# but easy to understand and therefore has potential for improvements.
# Calculate angle from point `avg` to each other point based on distances
angle_radians = np.array([np.arctan2(element[1], element[0]) for element in distances_x_y])
# since `.arctan2()` or `.arctan()` return the angle in radians,
# we need to convert to degrees
angle_degrees = np.rad2deg(angle_radians)
# print results
print(angle_degrees)
If you consider the coordinates x0=xavg-xA, y0=yavg-yA and x=xPoint-xA,y=yPoint-yA, the formula f(x,y) gives the signed angle that is positive as counter clockwise.
f(x,y)=pi()/2*((1+sign(x0))* (1-sign(y0^2))-(1+sign(x))* (1-sign(y^2)))
+pi()/4*((2+sign(x0))*sign(y0)-(2+sign(x))*sign(y))
+sign(x0*y0)*atan((abs(x0)-abs(y0))/(abs(x0)+abs(y0)))
-sign(x*y)*atan((abs(x)-abs(y))/(abs(x)+abs(y)))

Verification of 3D Fractal Slice-Rendering Method

Ray Casting Algorithm
MandelBulb Ray Casting Algorithm Python Example
So, if I understand correctly, the ray casting algorithm requires that an observer be located external to the 3D fractal at which point vectors are drawn from the observer toward a point on the plane normal to the vector and intersecting the origin.
It would seem to me that this would either severely limit the rendered view of the fractal or require stereoscopic 3D reconstruction of the fractal using multiple observer positions (which seems difficult to me). Additionally, no information can be gathered regarding the internal structure of the fractal.
Other Algorithms
Alternatively, Direct Volume Rendering seems intuitive enough however, computationally expensive and potentially inefficient in and of itself. Indirect Volume Rendering using an algorithm such as marching cubes might also employ a bit of a learning curve it seems.
Somewhere in the pdf of the 2nd link it talks about cut plane views in order to see slices of the fractal.
Question:
Why not use cut planes as a rendering method?
1) Using a modified ray tracing algorithm, say we put the observer at point Q at the origin (0, 0, 0).
2) Let us then emit rays from the origin toward the incident plane spanned by y & z point combinations that is slicing the fractal.
3) Calculate the distance to the fractal surface using the algorithm in the 1st link. If the x component of computed distance is within a certain tolerance, dx of the slicing plane, then the y & z coordinates along with the x value of the slicing plane are stored as the x, y, z coordinates. These coordinates are now representative of the surface at that specific slice in x.
4) Let us say that the slicing plane has one degree of freedom in the x direction. By moving the plane in its degree of freedom, we can receive yet another set of x, y, z coordinates for a given slice.
5) The final result is a calculable surface generated by the point cloud created in the previous steps.
6) Additionally, the degree of freedom of the slicing plane can be altered to create an another point cloud which can then be verified against the previous as a means of post-processing.
Please see the image below as a visual aid (the sphere represents the MandelBulb).
Below is my Python code so far, adapted from the first link. I successfully generate the plane of points and am able to get the directions from the origin to the points on the plane. There must be something fundamentally flawed in the distance estimator function because thats where everything breaks down and I get nans for the total distances
def get_plane_points(x, y_res=500, z_res=500, y_min=-10, y_max=10, z_min=-10, z_max=10):
y = np.linspace(y_min, y_max, y_res)
z = np.linspace(z_min, z_max, z_res)
x, y, z = np.meshgrid(x, y, z)
x, y, z = x.reshape(-1), y.reshape(-1) , z.reshape(-1)
P = np.vstack((x, y, z)).T
return P
def get_directions(P):
v = np.array(P - 0)
v = v/np.linalg.norm(v, axis=1)[:, np.newaxis]
return v
#jit
def DistanceEstimator(positions, plane_loc, iterations, degree):
m = positions.shape[0]
x, y, z = np.zeros(m), np.zeros(m), np.zeros(m)
x0, y0, z0 = positions[:, 0], positions[:, 1], positions[:, 2]
dr = np.zeros(m) + 1
r = np.zeros(m)
theta = np.zeros(m)
phi = np.zeros(m)
zr = np.zeros(m)
for _ in range(iterations):
r = np.sqrt(x * x + y * y + z * z)
dx = .01
x_loc = plane_loc
idx = (x < x_loc + dx) & (x > x_loc - dx)
dr[idx] = np.power(r[idx], degree - 1) * degree * dr[idx] + 1.0
theta[idx] = np.arctan2(np.sqrt(x[idx] * x[idx] + y[idx] * y[idx]), z[idx])
phi[idx] = np.arctan2(y[idx], x[idx])
zr[idx] = r[idx] ** degree
theta[idx] = theta[idx] * degree
phi[idx] = phi[idx] * degree
x[idx] = zr[idx] * np.sin(theta[idx]) * np.cos(phi[idx]) + x0[idx]
y[idx] = zr[idx] * np.sin(theta[idx]) * np.sin(phi[idx]) + y0[idx]
z[idx] = zr[idx] * np.cos(theta[idx]) + z0[idx]
return 0.5 * np.log(r) * r / dr
def trace(directions, plane_location, max_steps=50, iterations=50, degree=8):
total_distance = np.zeros(directions.shape[0])
keep_iterations = np.ones_like(total_distance)
steps = np.zeros_like(total_distance)
for _ in range(max_steps):
positions = total_distance[:, np.newaxis] * directions
distance = DistanceEstimator(positions, plane_location, iterations, degree)
total_distance += distance * keep_iterations
steps += keep_iterations
# return 1 - (steps / max_steps) ** power
return total_distance
def run():
plane_location = 2
plane_points = get_plane_points(x=plane_location)
directions = get_directions(plane_points)
distance = trace(directions, plane_location)
return distance
I am eager to hear thoughts on this and what limitations/issues I may encounter. Thanks in advance for the help!
If I am not mistaken, it is not impossible for this algorithm to work. There is inherent potential for problems with any assumptions made about the internal structure of the MandelBulb and what positions an observer is allowed to occupy. That is, if the observer is known to initially be in a zone of convergence then the ray tracing algorithm with return nothing meaningful since the furthest distance that could be measured is 0. This is due to the fact that the current ray tracing algorithm terminates upon first contact with the surface. It is likely this could be altered, however.
Rather than slicing the fractal with plane P, it might make more sense to prevent the termination of the ray upon first contact and instead, terminate based on a distance thats known to exist past the surface of the MandelBulb.

Rotating 1D numpy array of radial intensities into 2D array of spacial intensities

I have a numpy array filled with intensity readings at different radii in a uniform circle (for context, this is a 1D radiative transfer project for protostellar formation models: while much better models exist, my supervisor wasnts me to have the experience of producing one so I understand how others work).
I want to take that 1d array, and "rotate" it through a circle, forming a 2D array of intensities that could then be shown with imshow (or, with a bit of work, aplpy). The final array needs to be 2d, and the projection needs to be Cartesian, not polar.
I can do it with nested for loops, and I can do it with lookup tables, but I have a feeling there must be a neat way of doing it in numpy or something.
Any ideas?
EDIT:
I have had to go back and recreate my (frankly horrible) mess of for loops and if statements that I had before. If I really tried, I could probably get rid of one of the loops and one of the if statements by condensing things down. However, the aim is not to make it work with for loops, but see if there is a built in way to rotate the array.
impB is an array that differs slightly from what I stated it was before. Its actually just a list of radii where particles are detected. I then bin those into radius bins to get the intensity (or frequency if you prefer) in each radius. R is the scale factor for my radius as I run the model in a dimensionless way. iRes is a resolution scale factor, essentially how often I want to sample my radial bins. Everything else should be clear.
radJ = np.ndarray(shape=(2*iRes, 2*iRes)) # Create array of 2xRadius square
for i in range(iRes):
n = len(impB[np.where(impB[:] < ((i+1.) * (R / iRes)))]) # Count number of things within this radius +1
m = len(impB[np.where(impB[:] <= ((i) * (R / iRes)))]) # Count number of things in this radius
a = (((i + 1) * (R / iRes))**2 - ((i) * (R / iRes))**2) * math.pi # A normalisation factor based on area.....dont ask
for x in range(iRes):
for y in range(iRes):
if (x**2 + y**2) < (i * iRes)**2:
if (x**2 + y**2) >= (i * iRes)**2: # Checks for radius, and puts in cartesian space
radJ[x+iRes,y+iRes] = (n-m) / a # Put in actual intensity bins
radJ[x+iRes,-y+iRes] = (n-m) / a
radJ[-x+iRes,y+iRes] = (n-m) / a
radJ[-x+iRes,-y+iRes] = (n-m) / a
Nested loops are a simple approach for that. With ri_data_r and y containing your radius values (difference to the middle pixel) and the array for rotation, respectively, I would suggest:
from scipy import interpolate
import numpy as np
y = np.random.rand(100)
ri_data_r = np.linspace(-len(y)/2,len(y)/2,len(y))
interpol_index = interpolate.interp1d(ri_data_r, y)
xv = np.arange(-1, 1, 0.01) # adjust your matrix values here
X, Y = np.meshgrid(xv, xv)
profilegrid = np.ones(X.shape, float)
for i, x in enumerate(X[0, :]):
for k, y in enumerate(Y[:, 0]):
current_radius = np.sqrt(x ** 2 + y ** 2)
profilegrid[i, k] = interpol_index(current_radius)
print(profilegrid)
This will give you exactly what you are looking for. You just have to take in your array and calculate an symmetric array ri_data_r that has the same length as your data array and contains the distance between the actual data and the middle of the array. The code is doing this automatically.
I stumbled upon this question in a different context and I hope I understood it right. Here are two other ways of doing this. The first uses skimage.transform.warp with interpolation of desired order (here we use order=0 Nearest-neighbor). This method is slower but more precise and needs less memory then the second method.
The second one does not use interpolation, therefore is faster but also less precise and needs way more memory because it stores each 2D array containing one tilt until the end, where they are averaged with np.nanmean().
The difference between both solutions stemmed from the problem of handling the center of the final image where the tilts overlap the most, i.e. the first one would just add values with each tilt ending up out of the original range. This was "solved" by clipping the matrix in each step to a global_min and global_max (consult the code). The second one solves it by taking the mean of the tilts where they overlap, which forces us to use the np.nan.
Please, read the Example of usage and Sanity check sections in order to understand the plot titles.
Solution 1:
import numpy as np
from skimage.transform import warp
def rotate_vector(vector, deg_angle):
# Credit goes to skimage.transform.radon
assert vector.ndim == 1, 'Pass only 1D vectors, e.g. use array.ravel()'
center = vector.size // 2
square = np.zeros((vector.size, vector.size))
square[center,:] = vector
rad_angle = np.deg2rad(deg_angle)
cos_a, sin_a = np.cos(rad_angle), np.sin(rad_angle)
R = np.array([[cos_a, sin_a, -center * (cos_a + sin_a - 1)],
[-sin_a, cos_a, -center * (cos_a - sin_a - 1)],
[0, 0, 1]])
# Approx. 80% of time is spent in this function
return warp(square, R, clip=False, output_shape=((vector.size, vector.size)))
def place_vectors(vectors, deg_angles):
matrix = np.zeros((vectors.shape[-1], vectors.shape[-1]))
global_min, global_max = 0, 0
for i, deg_angle in enumerate(deg_angles):
tilt = rotate_vector(vectors[i], deg_angle)
global_min = tilt.min() if global_min > tilt.min() else global_min
global_max = tilt.max() if global_max < tilt.max() else global_max
matrix += tilt
matrix = np.clip(matrix, global_min, global_max)
return matrix
Solution 2:
Credit for the idea goes to my colleague Michael Scherbela.
import numpy as np
def rotate_vector(vector, deg_angle):
assert vector.ndim == 1, 'Pass only 1D vectors, e.g. use array.ravel()'
square = np.ones([vector.size, vector.size]) * np.nan
radius = vector.size // 2
r_values = np.linspace(-radius, radius, vector.size)
rad_angle = np.deg2rad(deg_angle)
ind_x = np.round(np.cos(rad_angle) * r_values + vector.size/2).astype(np.int)
ind_y = np.round(np.sin(rad_angle) * r_values + vector.size/2).astype(np.int)
ind_x = np.clip(ind_x, 0, vector.size-1)
ind_y = np.clip(ind_y, 0, vector.size-1)
square[ind_y, ind_x] = vector
return square
def place_vectors(vectors, deg_angles):
matrices = []
for deg_angle, vector in zip(deg_angles, vectors):
matrices.append(rotate_vector(vector, deg_angle))
matrix = np.nanmean(np.array(matrices), axis=0)
return np.nan_to_num(matrix, copy=False, nan=0.0)
Example of usage:
r = 100 # Radius of the circle, i.e. half the length of the vector
n = int(np.pi * r / 8) # Number of vectors, e.g. number of tilts in tomography
v = np.ones(2*r) # One vector, e.g. one tilt in tomography
V = np.array([v]*n) # All vectors, e.g. a sinogram in tomography
# Rotate 1D vector to a specific angle (output is 2D)
angle = 45
rotated = rotate_vector(v, angle)
# Rotate each row of a 2D array according to its angle (output is 2D)
angles = np.linspace(-90, 90, num=n, endpoint=False)
inplace = place_vectors(V, angles)
Sanity check:
These are just simple checks which by no means cover all possible edge cases. Depending on your use case you might want to extend the checks and adjust the method.
# I. Sanity check
# Assuming n <= πr and v = np.ones(2r)
# Then sum(inplace) should be approx. equal to (n * (2πr - n)) / π
# which is an area that should be covered by the tilts
desired_area = (n * (2 * np.pi * r - n)) / np.pi
covered_area = np.sum(inplace)
covered_frac = covered_area / desired_area
print(f'This method covered {covered_frac * 100:.2f}% '
'of the area which should be covered in total.')
# II. Sanity check
# Assuming n <= πr and v = np.ones(2r)
# Then a circle M with radius m <= r should be the largest circle which
# is fully covered by the vectors. I.e. its mean should be no less than 1.
# If n = πr then m = r.
# m = n / π
m = int(n / np.pi)
# Code for circular mask not included
mask = create_circular_mask(2*r, 2*r, center=None, radius=m)
m_area = np.mean(inplace[mask])
print(f'Full radius r={r}, radius m={m}, mean(M)={m_area:.4f}.')
Code for plotting:
import matplotlib.pyplot as plt
plt.figure(figsize=(16, 8))
plt.subplot(121)
rotated = np.nan_to_num(rotated) # not necessary in case of the first method
plt.title(
f'Output of rotate_vector(), angle={angle}°\n'
f'Sum is {np.sum(rotated):.2f} and should be {np.sum(v):.2f}')
plt.imshow(rotated, cmap=plt.cm.Greys_r)
plt.subplot(122)
plt.title(
f'Output of place_vectors(), r={r}, n={n}\n'
f'Covered {covered_frac * 100:.2f}% of the area which should be covered.\n'
f'Mean of the circle M is {m_area:.4f} and should be 1.0.')
plt.imshow(inplace)
circle=plt.Circle((r, r), m, color='r', fill=False)
plt.gcf().gca().add_artist(circle)
plt.gcf().gca().legend([circle], [f'Circle M (m={m})'])

Ray and square/rectangle intersection in 3D

Hei. Are making a game and are looking for a ray intersection onto a square or a rectangle only in 3D space. Have search the web and found many solutions but nothing i can understand have a line and line segment intersection script in 2D but i cant figure out have to make it 3D.
It is not important from what side it intersect the square or rectangle but it must be able to retrive the point of intersection vector so that later can be tested for distance to se if it occurred before or after other intersections on the same ray intersection.
Any examples in python or other similar scripting languages will be greatly appreciated
Edit: Dont know have to modify the 2D to show an exaple but made a new and posting both.
//this is the exaple it test a ray onto a plane then look to se if that point is in the rectangle and saves it to test for distanse later
list Faces; //triangle faces
list Points; //
vector FindPoint(){
//calcute the point of intersection onto the plane and returns it
//if it can intersect
//else return ZERO_VECTOR
}
integer point-in-quadrilateral(){
//return 1 if the point is in the rectangular on the plane
//else return 0
}
default{
state_entry(){
integer n = (Faces != []); //return number of elements
integer x = 0;
while(x < n){
vector intersection = FindPoint( FromList(Faces, x) ); //take out a element and runs it trough the function
if(intersection != ZERO_VECTOR){
integer test = point-in-quadrilateral( FromList(Faces, x) ); //find out if the point is in rectangular
if(test == 1){ //if so
Points += intersection; //save the point
}
}
++x;
}
float first; //the distanse to the box intersection
integer l = (Points != []);
integer d;
while(d < l){
if(Dist( FromList(Points, d) ) < first) //if the new distanse is less then first
return 0; //then end script
++d;
}
}
}
//this is the 2D version
vector lineIntersection(vector one, vector two, vector three, vector four){
float bx = two.x - one.x;
float by = two.y - one.y;
float dx = four.x - three.x;
float dy = four.y - three.y;
float b_dot_d_perp = bx*dy - by*dx;
if(b_dot_d_perp == 0.0) {
return ZERO_VECTOR;
}
float cx = three.x-one.x;
float cy = three.y-one.y;
float t = (cx*dy - cy*dx) / b_dot_d_perp;
if(LineSeg){ //if true tests for line segment
if((t < 0.0) || (t > 1.0)){
return ZERO_VECTOR;
}
float u = (cx * by - cy * bx) / b_dot_d_perp;
if((u < 0.0) || (u > 1.0)) {
return ZERO_VECTOR;
}
}
return <one.x+t*bx, one.y+t*by, 0.0>;
}
The solution is very easy when you define a ray with a point(= vector) and a direction vector, and the rectangle with a point(= vector) and two vectors representing the sides.
Suppose the ray is defined as R0 + t * D, where R0 is the origin of the ray, D is an unit vector representing its direction and t is its length.
The rectangle can be represented with a corner point P0, and two vectors S1 and S2 which should represent the sides (their length being equal to the length of the sides). You will need another vector N normal to its surface, which is equal to the unit vector along the cross product of S1 and S2.
Now, assume the ray intersects the rect at P. Then, the direction of the ray, D must make a nonzero angle with the normal N. This can be verified by checking D.N < 0.
To find the intersection point, assume P = R0 + a * D (the point must be on the ray). You need to find the value of a now. Find the vector P0P. This must be perpendicular to N, which means P0P.N = 0 which reduces to a = ((P0 - R0).N) / (D.N).
Now you need to check if the point is inside the rect or not. To do this, take projection Q1 of P0P along S1 and Q2 of P0P along S2. The condition for the point being inside is then 0 <= length(Q1) <= length(S1) and 0 <= length(Q2) <= length(S2).
This method is appropriate for any type of parallelograms, not only for rectangles. [Update: it looks like this statement about parallelograms created some confusions. In case of parallelograms (which also includes rectangles), the projection on one side should be taken along the other side, not just as a perpendicular line on it (quoting from the last paragraph: "take projection Q1 of P0P along S1 and Q2 of P0P along S2"). It is like working with a coordinate system where the axes are not perpendicular to each other.]
Create a vector equation for a line in R3, then solve for the intersection of that line in the plane of the rectangle that you are testing it against. After that, it's simple enough to test if that point of solution lies within the bounds.
the parameter t of the solution can be found with:
t = (a * (x0 - rx) + b * (y0 - ry) + c * (x0 - rz)) / (a * vx + b * vy + c * vz)
where:
a(x - x0) + b(y - y0) + c(z - z0) = 0
is the equation of the plane that your rectangle lies on
and:
<x, y, z> = <rx + vx * t, ry + vy * t, rz + vz * t>
is the vector equation of the line in question.
note that:
<rx, ry, rz>
is the initial point of the vector equation, and
<vx, vy, vz>
is the direction vector of the above equation
After that, plugging the parameter t into your vector equation will give you the point to test for distance.
You don't say whether the square/rectangle in 3D is aligned with the coordinate axes or not.
Assuming the 3D rectangle R is oriented arbitrarily in space, here is one method.
First interesect your ray r with the plane containing R. This can be accomplished by requiring a
scale factor s to multiply r and place it on the plane of R, and solving for s. This gives you a point p on the plane. Now project the plane, and R and p, on to one of the coordinate
planes {xy, yz, zx}. You only have to avoid projecting perpendicular to the normal vector to the plane, which is always possible. And then solve the point-in-quadrilateral problem in the plane of projection.
Before beginning, check if your line segment lies in the 3D plane of R, and if so, handle that separately.

Categories