Expanding blocks ( dilation ) in a binary grid - python

For an A* implementation (to generate a path for a 'car' robot), I need to adapt my model to take into account the car's 'width' and hence avoid obstacles.
One idea I got is to expand all obstacles by the car's width, that way all the cells that are too close to an obstacle will be also marked as obstacles.
I tried using two naive algorithms to do this, but it's still too slow (especially on big grids) because it goes through the same cells many times:
unreachable = set()
# I first add all the unreachables to a set to avoid 'propagation'
for line in self.grid:
for cell in line:
if not cell.reachable:
unreachable.add(cell)
for cell in unreachable:
# I set as unreachable all the cell's neighbours in a certain radius
for nCell in self.neighbours( cell, int(radius/division) ):
nCell.reachable = False
Here's the definition of neighbours:
def neighbours(self, cell, radius = 1, unreachables = False):
neighbours = set()
for i in xrange(-radius, radius + 1):
for j in xrange(-radius, radius + 1):
x = cell.x + j
y = cell.y + i
if 0 <= y < self.height and 0 <= x < self.width and (self.grid[y][x].reachable or unreachables )) :
neighbours.add(self.grid[y][x])
return neighbours
Is there any sequential algorithm (or O(n.log(n))) that could do the same thing ?

What you are looking for is what is known as Minkowski sum, and if your obstacles and car are convex, there is a linear algorithm to compute it.

I ended up using convolution product, with my 'map' (a matrix where '1' is an obstacle and '0' is a free cell) as the first operand and a matrix of the car's size and all filled with '1's as the second operand.
The convolution product of those two matrices gives a matrix where the cells that weren't in reach of any obstacle (that is: didn't have any obstacle in the neighberhood) have a value of '0', and those who had at least one obstacle in their neighberhood (that is a cell equal to '1') have a value != 0.
Here's the Python implementation (using scipy for the convolution product) :
# r: car's radius; 1 : Unreachable ; 0 : Reachable
car = scipy.array( [[1 for i in xrange(r)] for j in xrange(r)] )
# Converting my map to a binary matrix
grid = scipy.array( [[0 if self.grid[i][j].reachable else 1 for j in xrange(self.width)] for i in xrange(self.height)] )
result = scipy.signal.fftconvolve( grid, car, 'same' )
# Updating the map with the result
for i in xrange(self.height):
for j in xrange(self.width):
self.grid[i][j].reachable = int(result[i][j]) == 0

Related

Packing problem : Fit cuboids into a larger cuboid with constraints

I have a 3D numpy array which represents a mask. The "1" elements of this mask are areas where no calculation is done (blue areas in the figure below). The "0" elements of the mask are areas for which a calculation is made.
Each calculation is imperatively realized on a cuboid block of minimal edges Nmin. So I try to find the best distribution of cuboids to fill the "0" regions of this mask.
Here is an example of mask : mask.npy
And its 3D representation :
mask = np.load('mask.npy')
fig = plt.figure()
ax1 = fig.add_subplot(projection='3d')
ax1.voxels(mask)
plt.show()
It is a packing problem. The goal is to pack various cuboids into a larger cuboid with some constraints (the blue areas). There are a lot of possible combinations.
The way I see it to solve this problem is to determine first the largest "0" area and then repeat this process until the mask is completely filled.
I already did this kind of thing in 2D (see fdgrid), but I'm stuck on this 3D problem. how can I find the largest block (cuboid) of '0' ?
EDIT 1:
I have a draft solution that is a bit brutal and very slow with this 50x50x50 grid.
import time
def cuboid_list(mask, Nmin=3):
""" List of all cuboids than could fit in the domain.
List is formated as (volume, (Lx, Ly, Lz)). """
size = mask[mask==0].size
cuboids = [( i*j*k, (i, j, k)) for i, j, k in itertools.product(range(Nmin, nx), range(Nmin, ny), range(Nmin, nz)) if i*j*k <= size]
cuboids.sort(reverse=True)
return cuboids
def search_biggest(mask, cuboids):
"""Search biggest cuboid. """
t0 = time.perf_counter()
tested = 0
origin, size = None, None
for _, (Lx, Ly, Lz) in cuboids:
for o in itertools.product(range(nx-Lx), range(ny-Ly), range(nz-Lz)):
tested += 1
if (mask[o[0]:o[0]+Lx, o[1]:o[1]+Ly, o[2]:o[2]+Lz] == 0).all():
origin, size = o, (Lx, Ly, Lz)
break
if origin:
print(f'{tested} configurations tested in {time.perf_counter() - t0:.3f} s.')
break
return origin, size
# Load mask
mask = np.load('mask.npy')
# List of all cuboids
cuboids = cuboid_list(mask)
# Search biggest
sub = search_biggest(mask, cuboids)
In this particular case, this leads to 2795610 configurations tested in 164.984 s ! The largest cuboid has origin=(16, 16, 0) and size=(33, 33, 49).
To make the algorithm usable for larger geometries (e.g. 1024x1024x512 grid), I need to speed up this process.
EDIT 2:
def search_biggest(mask, cuboids):
"""Search biggest cuboid. """
# List of nonzero indexes
forbid = set([tuple(i) for i in np.argwhere(mask != 0)])
t0 = time.perf_counter()
tested = 0
origin, size = None, None
for _, (Lx, Ly, Lz) in cuboids:
# remove cuboid with origin in another cuboid and iterate
for o in set(itertools.product(range(nx-Lx), range(ny-Ly), range(nz-Lz))).difference(forbid):
tested += 1
if (mask[o[0]:o[0]+Lx, o[1]:o[1]+Ly, o[2]:o[2]+Lz] == 0).all():
origin, size = o, (Lx, Ly, Lz)
break
if origin:
print(f'{tested} configurations tested in {time.perf_counter() - t0:.3f} s.')
break
return origin, size
Reducing the possibilities leads to 32806 configurations tested in ~2.6s.
Any ideas ?
EDIT 3:
def search_biggest(mask, cuboids):
"""Search biggest cuboid. """
# List of nonzero indexes
forbid = set([tuple(i) for i in np.argwhere(mask != 0)])
t0 = time.perf_counter()
tested = 0
origin, size = None, None
for _, (Lx, Ly, Lz) in cuboids:
tested += 1
tmp = [i for i in itertools.product(range(nx-Lx), range(ny-Ly), range(nz-Lz)) if i not in forbid]
tmp = [i for i in tmp if
all(x not in forbid for x in itertools.product(range(i[0], i[0]+Lx), range(i[1], i[1]+Ly), range(i[2], i[2]+Lz)))]
if tmp:
origin, size = tmp[0], (Lx, Ly, Lz)
print(f'{tested} configurations tested in {time.perf_counter() - t0:.3f} s.')
break
return origin, size
Simple is better than complex ! Alternative implementation of search_biggest leads to 5991 configurations tested in 1.017 s. !
Maybe, I can reduce the list of cuboids to speed up things, but I think this is not the way to go. I tried with a 500x500x500 grid and stopped the script after an hour without a result...
I have two other possible formulations in mind:
Fill the cuboid with elementary cuboids of size Nmin x Nmin x Nmin then merge the cuboids having common faces. The problem is that in some situations, it is not possible to fill an empty region only with cuboids of this size.
Focus the search for empty rectangles on a plane (x, y, z=0) then extend them along z until a constraint is encountered. Repeat this operation for z $\in$ [1, nz-1] then merge cuboids that can be merged. The problem is that I can end up with some empty regions smaller than an elementary cuboid.
Always in search for a better way to solve this problem...
Given that the constraints are cuboids, we can simplify checking the possibilities by skipping the rows/columns/layers where nothing changes. For example, a 2D mask might look like this, with the redundant rows/columns marked:
v v
...11.
>...11.
1.....
>1.....
......
which can be reduced to
..1.
1...
....
We just need to keep track of the true lengths of each reduced cell, and use that to calculate the real volume.
However, this means we need to take a different approach to finding the largest cuboid; we can't (easily) pre-generate them all and try in order of volume, since the true sizes vary depending on the point we're testing from.
The code below just tries all possible cuboids and keeps track of the largest one, and seems plenty fast.
There are also some off-by-one errors in the original code, which I've avoided or fixed: range(Nmin, nx) -> range(Nmin, nx+1) in cuboid_list(), and range(nx-Lx) -> range(nx-Lx+1) in search_biggest(). The largest cuboid actually has size=(34, 34, 50).
import itertools
import time
import numpy as np
class ReducedSolver:
def __init__(self, mask):
self.splits = tuple(
np.unique(
# find the edges of the constraint regions in each axis
[0, mask.shape[ax], *np.where(np.diff(mask, axis=ax, prepend=0))[ax]]
)
for ax in range(3)
)
# extract exactly one value (the lowest corner) for each reduced region
self.mask = np.zeros([len(split) - 1 for split in self.splits], dtype=mask.dtype)
for i, x in enumerate(self.splits[0][:-1]):
for j, y in enumerate(self.splits[1][:-1]):
for k, z in enumerate(self.splits[2][:-1]):
self.mask[i, j, k] = mask[x, y, z]
# less readable:
# self.mask = mask[np.ix_(self.splits[0][:-1], self.splits[1][:-1], self.splits[2][:-1])]
# true sizes of each region
self.sizes = [np.diff(split) for split in self.splits]
def solve(self):
"""Return list of cuboids in the format (origin, size), using a greedy approach."""
nx, ny, nz = self.mask.shape
mask = self.mask.copy()
result = []
while np.any(mask == 0):
t0 = time.perf_counter()
tested = 0
max_volume = 0
# first corner of the cuboid
for x1, y1, z1 in itertools.product(range(nx), range(ny), range(nz)):
if mask[x1, y1, z1]:
continue
# opposite corner of the cuboid
for x2, y2, z2 in itertools.product(range(x1, nx), range(y1, ny), range(z1, nz)):
tested += 1
# slices are exclusive on the the end point
slc = (slice(x1, x2 + 1), slice(y1, y2 + 1), slice(z1, z2 + 1))
# np.any doesn't short-circuit
if any(np.nditer(mask[slc])):
continue
true_size = tuple(np.sum(size[s]) for size, s in zip(self.sizes, slc))
volume = np.prod(true_size)
if volume > max_volume:
max_volume = volume
origin = (
self.splits[0][x1],
self.splits[1][y1],
self.splits[2][z1],
)
# keep track of the region in the reduced mask with `slc`
biggest = ((origin, true_size), slc)
print(f"{tested} configurations tested in {(time.perf_counter() - t0)*1000:.2f} ms.")
result.append(biggest[0])
# mark this cuboid as filled
mask[biggest[1]] = 1
return result
# Load mask
mask = np.load("mask.npy")
# Solve on the simplified grid
reduced = ReducedSolver(mask)
sol = reduced.solve()
On my machine, finding the biggest cuboid on the example mask takes ~1s with search_biggest(), but is ~4ms with my code, and the entire solve takes ~20ms for 16 cuboids.

Kalman filter for visual tracking of a ball sliding on a gutter

I'm working on a project where a robot needs to keep a ball at a desired position on a gutter. The gutter is fixed at one end and held at the other end by the robot’s effector which therefore decides of its orientation and, as a consequence, of the position of the ball. The robot's effector can move up or down vertically. The robot's controller is deep learning based and was trained in simulation. Once trained I deployed it on the physical robot to perform the task on the real world. The position of the ball and velocity of the ball are both needed by the controller. The ball position is obtained through visual tracking with a low quality webcam which means that the observations are noisy. The velocity is obtained by computing the difference in ball position between two timesteps (~0.1s). However, noise in the ball position produce large error in the velocity computation. Even if the controller is performing ok with noisy observations, I'd like to improve it. So I started looking into the Kalman filter.
My state is composed of the ball position (1d) and velocity (1d), however I'm measuring the ball position only with the camera. My state model looks like this :
with
For the command U, I considered 4 cases and derived the acceleration for each :
u is the effector height (which is controlled) below or above the equilibrium position of the gutter and = 0.06 is the friction coefficient plastic/plastic (both ball and gutter are in plastic) that I found to be given the most realstic behavior (see figure below). In code i'm checking the sign of and to know which equation to use. I recorded a trajectory of the robot performing the task to validate the model (prediction step). However, the model doesn't seems to be very accurate : . The predicted ball position doesn't fi the true (measured) position a all. However the predicted speed is smoother than the naively computed one (measured). So my first question is : Is something wrong with my prediction step ? Even when adding air friction the predicted ball trajectory is still very wrong. Anyway, I tried to add the Update step to see how it goes :
Process covariance matrix
With :
I chose the value of Q without knowing too much what to put.
kalman Gain
where :
H and R are the identity matrix.
Correction
where :
. Since I can't directly measure it, I put .
But once again, things doesn't behave nicely :. The kalman filtered position is not smoothed and the kalman filtered velocity doesn't make sense. I have a hard time figuring out what I am doing wrong. I suspect that the prediction step is not right and that I should choose more fitted values for R and Q, but I don't really understand their role and how to chose them. Did I used the kalman filter correclty ? Where did I screwed up ? In case the problem is due to my implementation here is my code :
def kf_predict(X, P, A, Q, B, U):
X = dot(A, X) + dot(B, U)
P = dot(A, dot(P, A.T)) + Q
return(X,P)
error_est_pos = 3
error_est_vel = 2
error_mes_pos = 1
dt = 0.1
g = 9.81 * 100
theta = 0
mu = 0.06
# Initialization of state matrices
X = np.array([[0.25*56], [0.0]])
P = np.diag((error_est_pos**2, error_est_vel**2))
A = np.array([[1, dt], [0, 1]])
Q = 0.1*np.eye(X.shape[0])
B = np.array([[1/2*(dt*dt)],[dt]])
# Measurement matrices
Y = np.array([ball_pos[0] + 0*np.random.randn(1)[0]])
H = np.identity(2)
R = np.diag([error_mes_pos**2])
d = 45
for obs,u in zip(ball_pos,effec_pos):
theta = np.arctan(u/d)
tmp = np.abs(theta)
if X[1] == 0 and u ==0:
U = np.array(0)
else :
if theta * X[1][0] >= 0 and theta >= 0: # case 1
U = np.array(g*(np.sin(tmp) - mu*np.cos(tmp)))
elif theta * X[1][0] < 0 and theta >= 0: # case 2
U = np.array(g*(np.sin(tmp) + mu*np.cos(tmp)))
elif theta * X[1][0] < 0 and theta <= 0: # case 3
U = np.array(g*( - np.sin(tmp) - mu*np.cos(tmp)))
elif theta * X[1][0] >= 0 and theta <= 0: # case 4
U = np.array(g*( - np.sin(tmp) + mu*np.cos(tmp)))
(X, P) = kf_predict(X, P, A, Q, B, U)
# Calculating the Kalman Gain
S = H.dot(P).dot(H.T) + R
K = P.dot(H).dot(inv(S))
Y = np.array([[obs + np.random.randn(1)[0]],[0.0]])
# Update the State Matrix
# Combination of the predicted state, measured values, covariance matrix and Kalman Gain
X = X + K.dot(Y - H.dot(X))
# Update Process Covariance Matrix
P = (np.identity(len(K)) - K.dot(H)).dot(P)
Thanks in advance for any tips or solutions.

How to solve...ValueError: cannot convert float NaN to integer

I'm running quite a complex code so I won't bother with details as I've had it working before but now im getting this error.
Particle is a 3D tuple filled with 0 or 255, and I am using the scipy centre of mass function and then trying to turn the value into its closest integer (as I'm dealing with arrays). The error is found with on the last line... can anyone explain why this might be??
2nd line fills Particle
3rd line deletes any surrounding particles with a different label (This is in a for loop for all labels)
Particle = []
Particle = big_labelled_stack[x_start+20:x_stop+20,y_start+20:y_stop+20,z_start+20:z_stop+20]
Particle = np.where(Particle == i ,255,0)
CoM = scipy.ndimage.measurements.center_of_mass(Particle)
CoM = [ (int(round(x)) for x in CoM ]
Thanks in advance. If you need more code just ask but I dont think it will help you and its very messy.
################## MORE CODE
border = 30
[labelled_stack,no_of_label] = label(labelled,structure_array,output_type)
# RE-LABEL particles now no. of seeds has been reduced! LAST LABELLING
#Increase size of stack by increasing borders and equal them to 0; to allow us to cut out particles into cube shape which else might lye outside the border
h,w,l = labelled.shape
big_labelled_stack = np.zeros(shape=(h+60,w+60,l+60),dtype=np.uint32)
# Creates an empty border around labelled_stack full of zeros of size border
if (no_of_label > 0): #Small sample may return no particles.. so this stage not neccesary
info = np.zeros(shape=(no_of_label,19)) #Creates array to store coordinates of particles
for i in np.arange(1,no_of_label,1):
coordinates = find_objects(labelled_stack == i)[0] #Find coordinates of label i.
x_start = int(coordinates[0].start)
x_stop = int(coordinates[0].stop)
y_start = int(coordinates[1].start)
y_stop = int(coordinates[1].stop)
z_start = int(coordinates[2].start)
z_stop = int(coordinates[2].stop)
dx = (x_stop - x_start)
dy = (y_stop - y_start)
dz = (z_stop - z_start)
Particle = np.zeros(shape=(dy,dx,dz),dtype = np.uint16)
Particle = big_labelled_stack[x_start+30:x_start+dx+30,y_start+30:y_start+dy+30,z_start+30:z_start+dz+30]
Particle = np.where(Particle == i ,255,0)
big_labelled_stack[border:h+border,border:w+border,border:l+border] = labelled_stack
big_labelled_stack = np.where(big_labelled_stack == i , 255,0)
CoM_big_stack = scipy.ndimage.measurements.center_of_mass(big_labelled_stack)
C = np.asarray(CoM_big_stack) - border
if dx > dy:
b = dx
else: #Finds the largest of delta_x,y,z and saves as b, so that we create 'Cubic_Particle' of size 2bx2bx2b (cubic box)
b = dy
if dz > b:
b = dz
CoM = scipy.ndimage.measurements.center_of_mass(Particle)
CoM = [ (int(round(x))) for x in CoM ]
Cubic_Particle = np.zeros(shape=(2*b,2*b,2*b))
Cubic_Particle[(b-CoM[0]):(b+dx-CoM[0]),(b-CoM[1]):(b+dy-CoM[1]),(b-CoM[2]):(b+dz-CoM[2])] = Particle
volume = Cubic_Particle.size # Gives volume of the box in voxels
info[i-1,:] = [C[0],C[1],C[2],i,C[0]-b,C[1]-b,C[2]-b,C[0]+b,C[1]+b,C[2]+b,volume,0,0,0,0,0,0,0,0] # Fills an array with label.No., size of box, and co-ords
else:
print('No particles found, try increasing the sample size')
info = []
Ok, so I have a stack full of labelled particles, there are two things I am trying to do, first find the centre of masses of each particle with respect ot the labelled_stack which is what CoM_big_labelled_stack (and C) does. and stores the co-ords in a list (tuple) called info. I am also trying to create a cubic box around the particle, with its centre of mass as the centre (which is relating to the CoM variable), so first I use the find objects function in scipy to find a particle, i then use these coordinates to create a non-cubic box around the particle, and find its centre of mass.I then find the longest dimension of the box and call it b, creating a cubic box of size 2b and filling it with particle in the right position.
Sorry this code is a mess, I am very new to Python

White non-gird lines on matplotlib plot

I have been working on a project for my math class in which I am creating a Julia set generator. I had finally succeeded in generating it, but when I show the plot and save the image generated there are white lines all over it. They do not line up with x ticks or y ticks. When I view the saved image it had even more white lines. I have been searching to find what these might be, but I have found nothing.
import matplotlib.pyplot as plt
#Set up window with a name and size.
plt.figure("Julia Set Generator by Eric Kapilik", figsize=(7.0,7.0))
#Set Range of axes
plt.xlim([-2,2])
plt.ylim([-2,2])
#Add labels to axes
plt.xlabel("Real Numbers")
plt.ylabel("Imaginary Numbers")
plt.grid(b=False, which = "major", axis = "both")
plt.grid(b=False, which = "minor", axis = "both")
name = input("Save file as... \n")
#Ask for maximum amount of iterations to base colour
#selection off of with fractions.
max = int(input("What is the maximum amount of iterations you wish to run? "))
#Generate an array of colour names to be used to plot points.
#Set seed of array.
colourArray = ["r"]
for colourNum in range (1, max):
#if the place in array is between 0% and 25% then set the colour to red.
#Same method used for other three colours.
if colourNum >= 0 and colourNum <= (max/4):
colourArray.append("r") #red
elif colourNum > (max/4) and colourNum <= (max/2):
colourArray.append("y") #yellow
elif colourNum > (max/2) and colourNum <= ((3*max)/4):
colourArray.append("g") #green
elif colourNum > ((3*max)/4) and colourNum <= max:
colourArray.append("c") #cyan
#Get constant value of which the julia set is based off of.
#The real number component is plotted on the horizontal axis
#of a complex number grid so we will use x.
xConstant = float(input("Enter real number constant component: "))
#The imaginary nuber compenent of a complex number is plotted on the vertical axis,
#so we will use y in our real number grid (for simplicity's sake).
yConstant = float(input("Enter imaginary number constant component: "))
#Title the graph based on the constatn complex number entered.
plt.title(str(xConstant) + " + " + str(yConstant) + "i")
#See the starting coordinates to be tested and conditions
xTest = float(-2)
yTest = float(2)
stop = False
i = 0
xPrevious = xTest
yPrevious = yTest
#Using an escape time algorith, determine the amout of iterations of the recursion
#are needed for the coordinate to be attarcted to infinity.
#Continue doing this while the y value of the coordinate being tested is less
#than or equal to -2.
while yTest >= -2:
#We are following the recursive function of
#f(Z-1) = Z^2 + C
#Where Z is current coordinate, and C is the constant value.
#Reminder: Both Z and C are actually complex numbers but in our case we
#are using them both as real number coordinates on a real number grid.
xCurrent = ((xPrevious**2) - (yPrevious**2)) + xConstant
yCurrent = (2 * xPrevious * yPrevious) + yConstant
#Points that surpass a circle of radius 2 with a centre point at the origin
#are considered to indefinitely escape to infinity.
#So when the radius of the recursive coordinate based off of the tested coordinate
#becomes greater or equal to two we know it will be attaracted to infinity.
radius = xCurrent**2 + yCurrent**2
#"Is the point an escapee?"
if radius >= 2:
#Since the point has been defined as one that esacpes to infintity
#it is considered an escapee, so set that to true.
escapee = True
#"Is the point a prisoner?"
if i == max:
#The point is considered a prisoner if max iterations is reached and
#the point is still within the circle of radius 2.
#The testeed point will be considered a prisoner based off of the amount
#of iterations we selected, it is possible that with more iterations
#that we would find this to be an escapee.
prisoner = True
#If we have not defined what kind of point this is yet, then go to the next
#iteration of the recursion. i is the number of iterations completed for
#the test point.
if escapee == False and prisoner == False:
i = i + 1
#Out with the old, in with the new. Set the current points to the previous
#points for the next iteration.
xPrevious = xCurrent
yPrevious= yCurrent
#If, however, we have defined the point, then colour it based off of
#the amount of iterations using the array of colours generated at the
#beginning to select the colour.
if escapee == True or prisoner == True:
#This sets the black points that are prisoners, this is the body
#of the julia set.
if i == max:
colourPoint = "k,"
else:
#Colour the point and concatenate a ",", which means to plot this point
#as a pixel.
colourPoint = colourArray[i] + ","
#Plot the point! (Most satisfying part)
plt.plot(xTest, yTest, colourPoint)
#Determine the percentage finished, to give user an idea of how the
#renderig is going. (Not nessecary, but appreciable)
percent = int(((yTest-2)/4) * (-100))
print(str(percent) + "%")
#After setting a colour and plotting the point, jump to the next test coordinate.
#Once the end of the line is reached, jump down one.
if xTest >= 2:
xTest = -2
yTest = yTest - 0.01
else:
xTest= xTest + 0.01
#Reset the starting conditions.
i = 0
escapee = False
prisoner = False
xPrevious = xTest
yPrevious = yTest
#Show the beauty.
print("100%")
print("Wait for matplotlib to finish things up...\nWill take a minute...")
plt.show()
plt.savefig(name)
Plot that was generated:
Saved image, with even more white lines:

Generating multiple random (x, y) coordinates, excluding duplicates?

I want to generate a bunch (x, y) coordinates from 0 to 2500 that excludes points that are within 200 of each other without recursion.
Right now I have it check through a list of all previous values to see if any are far enough from all the others. This is really inefficient and if I need to generate a large number of points it takes forever.
So how would I go about doing this?
This is a variant on Hank Ditton's suggestion that should be more efficient time- and memory-wise, especially if you're selecting relatively few points out of all possible points. The idea is that, whenever a new point is generated, everything within 200 units of it is added to a set of points to exclude, against which all freshly-generated points are checked.
import random
radius = 200
rangeX = (0, 2500)
rangeY = (0, 2500)
qty = 100 # or however many points you want
# Generate a set of all points within 200 of the origin, to be used as offsets later
# There's probably a more efficient way to do this.
deltas = set()
for x in range(-radius, radius+1):
for y in range(-radius, radius+1):
if x*x + y*y <= radius*radius:
deltas.add((x,y))
randPoints = []
excluded = set()
i = 0
while i<qty:
x = random.randrange(*rangeX)
y = random.randrange(*rangeY)
if (x,y) in excluded: continue
randPoints.append((x,y))
i += 1
excluded.update((x+dx, y+dy) for (dx,dy) in deltas)
print randPoints
I would overgenerate the points, target_N < input_N, and filter them using a KDTree. For example:
import numpy as np
from scipy.spatial import KDTree
N = 20
pts = 2500*np.random.random((N,2))
tree = KDTree(pts)
print tree.sparse_distance_matrix(tree, 200)
Would give me points that are "close" to each other. From here it should be simple to apply any filter:
(11, 0) 60.843426339
(0, 11) 60.843426339
(1, 3) 177.853472309
(3, 1) 177.853472309
Some options:
Use your algorithm but implement it with a kd-tree that would speed up nearest neighbours look-up
Build a regular grid over the [0, 2500]^2 square and 'shake' all points randomly with a bi-dimensional normal distribution centered on each intersection in the grid
Draw a larger number of random points then apply a k-means algorithm and only keep the centroids. They will be far away from one another and the algorithm, though iterative, could converge more quickly than your algorithm.
This has been answered, but it's very tangentially related to my work so I took a stab at it. I implemented the algorithm described in this note which I found linked from this blog post. Unfortunately it's not faster than the other proposed methods, but I'm sure there are optimizations to be made.
import numpy as np
import matplotlib.pyplot as plt
def lonely(p,X,r):
m = X.shape[1]
x0,y0 = p
x = y = np.arange(-r,r)
x = x + x0
y = y + y0
u,v = np.meshgrid(x,y)
u[u < 0] = 0
u[u >= m] = m-1
v[v < 0] = 0
v[v >= m] = m-1
return not np.any(X[u[:],v[:]] > 0)
def generate_samples(m=2500,r=200,k=30):
# m = extent of sample domain
# r = minimum distance between points
# k = samples before rejection
active_list = []
# step 0 - initialize n-d background grid
X = np.ones((m,m))*-1
# step 1 - select initial sample
x0,y0 = np.random.randint(0,m), np.random.randint(0,m)
active_list.append((x0,y0))
X[active_list[0]] = 1
# step 2 - iterate over active list
while active_list:
i = np.random.randint(0,len(active_list))
rad = np.random.rand(k)*r+r
theta = np.random.rand(k)*2*np.pi
# get a list of random candidates within [r,2r] from the active point
candidates = np.round((rad*np.cos(theta)+active_list[i][0], rad*np.sin(theta)+active_list[i][1])).astype(np.int32).T
# trim the list based on boundaries of the array
candidates = [(x,y) for x,y in candidates if x >= 0 and y >= 0 and x < m and y < m]
for p in candidates:
if X[p] < 0 and lonely(p,X,r):
X[p] = 1
active_list.append(p)
break
else:
del active_list[i]
return X
X = generate_samples(2500, 200, 10)
s = np.where(X>0)
plt.plot(s[0],s[1],'.')
And the results:
Per the link, the method from aganders3 is known as Poisson Disc Sampling. You might be able to find more efficient implementations that use a local grid search to find 'overlaps.' For example Poisson Disc Sampling. Because you are constraining the system, it cannot be completely random. The maximum packing for circles with uniform radii in a plane is ~90% and is achieved when the circles are arranged in a perfect hexagonal array. As the number of points you request approaches the theoretical limit, the generated arrangement will become more hexagonal. In my experience, it is difficult to get above ~60% packing with uniform circles using this approach.
the following method uses list comprehension, but I am generating integers you can use different random generators for different datatypes
arr = [[random.randint(-4, 4), random.randint(-4, 4)] for i in range(40)]

Categories