Procedurally generating areas with distinctly higher elevations with Perlin Noise - python

I am trying to learn about Perlin Noise and procedural generation. I am reading through an online tutorial about generating landscapes with noise, but I don't understand part of the author's explanation about making areas with higher elevation.
On this webpage under the "islands" section there is the text
Design a shape that matches what you want from islands. Use the lower shape to push the map up and the upper shape to push the map down. These shapes are functions from distance d to elevation 0-1. Set e = lower(d) + e * (upper(d) - lower(d)).
I want to do this, but I'm not sure what the author means when they're talking about upper and lower shapes.
What could the author mean by "Use the lower shape to push the map up and the upper shape to push the map down"?
Code Example:
from __future__ import division
import numpy as np
import math
import noise
def __noise(noise_x, noise_y, octaves=1, persistence=0.5, lacunarity=2):
"""
Generates and returns a noise value.
:param noise_x: The noise value of x
:param noise_y: The noise value of y
:return: numpy.float32
"""
value = noise.pnoise2(noise_x, noise_y,
octaves, persistence, lacunarity)
return np.float32(value)
def __elevation_map():
elevation_map = np.zeros([900, 1600], np.float32)
for y in range(900):
for x in range(1600):
noise_x = x / 1600 - 0.5
noise_y = y / 900 - 0.5
# find distance from center of map
distance = math.sqrt((x - 800)**2 + (y - 450)**2)
distance = distance / 450
value = __noise(noise_x, noise_y, 8, 0.9, 2)
value = (1 + value - distance) / 2
elevation_map[y][x] = value
return elevation_map

The author means that you should describe the final elevation, fe, of a point in terms of its distance from the centre, d, as well as the initial elevation, e, which was presumably generated by noise.
So, for example, if you wanted your map to look something like a bowl, but maintaining the noisy characteristic of your originally generated terrain, you could use the following functions:
def lower(d):
# the lower elevation is 0 no matter how near you are to the centre
return 0
def upper(d):
# the upper elevation varies quadratically with distance from the centre
return d ** 2
def modify(d, initial_e):
return lower(d) + initial_e * (upper(d) - lower(d))
Note in particular the paragraph starting with "How does this work?", which I found quite illuminating.

Related

Fool-proof algorithm for uniformly distributing points on a sphere's surface?

I've been trying to generate points on the surface of a sphere of radius "inner_radius", such that they're uniformly spread out. The algorithm works as expected for a radius of 1, but generates lesser than expected points for greater radii.
I have looked through similar questions on here, but they seem to be for generating points throughout the volume and not just on the surface of the sphere.
import numpy as np
PI=np.pi
def spherical_to_cartesian(pol_ang,azim_ang,radius): #This function converts given spherical coordinates (theta, phi and radius) to cartesian coordinates.
return np.array((radius*np.sin(pol_ang) * np.cos(azim_ang),
radius*np.sin(pol_ang) * np.sin(azim_ang),
radius*np.cos(pol_ang))
)
def get_electron_coordinates_list(inner_radius,electron_count):
#Algorithm used was mostly taken from https://www.cmu.edu/biolphys/deserno/pdf/sphere_equi.pdf . Explanations in code added by me.
electron_coordinate_list=[]
inner_area=4*(PI*inner_radius**2)
area_per_electron=inner_area/electron_count
pseudo_length_per_electron=np.sqrt(area_per_electron) #This is the side length of a square where the area of it is the area per electron on the sphere.
#Now, we need to get a value of angular space, such that angular space between electrons on latitude and longitude per electron is equal
#As a first step to obtaining this, we must make another value holding a whole number approximation of the ratio between PI and the pseudo_length. This will give the number of
#possible latitudes.
possible_count_of_lats=np.round(PI/pseudo_length_per_electron)
approx_length_per_electron_lat=PI/possible_count_of_lats #This is the length between electrons on a latitude
approx_length_per_electron_long=area_per_electron/approx_length_per_electron_lat #This is the length between electrons on a longitude
for electron_num_lat in range(int(possible_count_of_lats.item())): #The int(somenumpyvalue.item()) is used because Python cannot iterate over a numpy integer and it must be converted to normal int.
pol_ang=PI*(electron_num_lat+0.5)/possible_count_of_lats #The original algorithm recommended pol_ang=PI*(electron_num_lat+0.5)/possible_count_of_lats. The 0.5 appears to be added in order to get a larger number of coordinates.
#not sure if removing the 0.5 affects results. It didnt do so drastically, so what gives? Anyway, this gets the polar angle as PI*(latitudenumber)/totalnumberoflatitudes.
possible_count_of_longs=np.round(2*PI*np.sin(pol_ang)/approx_length_per_electron_long)
for electron_num_long in range(int(possible_count_of_longs.item())):
azim_ang=(2*PI)*(electron_num_long)/possible_count_of_longs #This gets the azimuthal angle as 2PI*longitudenumber/totalnumberoflongitudes
electron_coordinate=spherical_to_cartesian(pol_ang, azim_ang,inner_radius) #Converts the recieved spherical coordinates to cartesian so Manim can easily handle them.
electron_coordinate_list.append(electron_coordinate) #Add this coordinate to the electron_coordinate_list
print("Got coordinates: ",electron_coordinate) #Print the coordinate recieved.
print(len(electron_coordinate_list)," points generated.") #Print the amount of electrons will exist. Comment these two lines out if you don't need the data.
return electron_coordinate_list
get_electron_coordinates_list(1,100)
get_electron_coordinates_list(2,100)
Spherical_to_Cartesian() does nothing other than convert the spherical points to Cartesian.
For 100 points and radius 1, it generates 99 points.
But, only 26 points are made if the radius is 2 and 100 points are requested.
If you can generate points uniformly in the sphere's volume, then to get a uniform distribution on the sphere's surface, you can simply normalize the vectors so their radius equals the sphere's radius.
Alternatively, you can use the fact that independent identically-distributed normal distributions are rotationally-invariant. If you sample from 3 normal distributions with mean 1 and standard deviation 0, and then likewise normalize the vector, it will be uniform on the sphere's surface. Here's an example:
import random
def sample_sphere_surface(radius=1):
x, y, z = (random.normalvariate(0, 1) for i in range(3))
scalar = radius / (x**2 + y**2 + z**2) ** 0.5
return (x * scalar, y * scalar, z * scalar)
To be absolutely foolproof, we can handle the astronomically unlikely case of a division-by-zero error when x, y and z all happen to be zero:
def sample_sphere_surface(radius=1):
while True:
try:
x, y, z = (random.normalvariate(0, 1) for i in range(3))
scalar = radius / (x**2 + y**2 + z**2) ** 0.5
return (x * scalar, y * scalar, z * scalar)
except ZeroDivisionError:
pass
The element of area is, in polar coordinates, sinΘ dΘ dφ. Hence the azimuth angle can be uniformly distributed, while the inclination must be redistributed. Using the inverse transform sampling trick, Θ=arccos(u) where u is drawn uniformly will do.
Hence in Cartesian coordinates, (√(1-u²) cos v, √(1-u²) sin v, u) where u is drawn from [-1,1) and v from [0,2π).

Wrapping a given coordinates (x,y,z) in a twisted structure

I am trying to wrap (x,y,z) coordinates on a Mobius strip (a topological structure obtained by twisting once and connecting the ends of a strip).
The structure (for which I can create xyz coordinates) is as follow (from RG) using following code.
import numpy as np
import matplotlib.pyplot as plt
bLength=1.6
numPoints=10
radius = bLength*numPoints / (2 * np.pi)
theta = np.linspace(0,2*np.pi,numPoints,endpoint=False)
dtheta=theta[1]-theta[0]
x0,y0=(radius * np.cos(theta)), (radius * np.sin(theta))
x1,y1=(radius * np.cos(theta+dtheta/2)) , (radius *
np.sin(theta+dtheta/2))
#plt.plot(x0,y0)
#plt.show()
#plt.plot(x1,y1)
#plt.show()
cons0=np.ones(x0.shape)*0
cons1=np.ones(x1.shape)*2
np.savetxt('cooRing1.csv',np.c_[x0,y0,cons0],delimiter=' ')
np.savetxt('cooRing2.csv',np.c_[x1,y1,cons1],delimiter=' ')
cat cooRing1.csv cooRing2.csv > coordinates.csv
[![enter image description here][3]][3]
I want to map above xyz co-ordinates on a Mobius strip.
Following example has been given in an website for a different strip (also the code uses a module which is not publicly available)
import Twister as Twister
import math
def displacement(x, width, wrapping_angle):
"""
Function for converting a nanosheet coordinate into a partly wrapped nanotube
#param x : Coordinates of nanosheet atom
#param width : Width of the nano-sheet
#param wrapping_angle : maximum wrapping angle of the nanotube in radians
"""
# calculate the average radius of the incomplete wrapped tube
radius = width/wrapping_angle
# find the angle of the current atom
angle = (x[2]-width/2.)/radius
# calculate the radius of the current atom
atom_radius = radius+x[1]
# return atom position of the wrapped atom
return numpy.array([x[0], atom_radius*math.cos(angle),atom_radius*math.sin(angle)])
def configuration(n, m, repetition):
"""
Function for generating a moebius molecule
#param n : Chiral vector index
#param m : Chiral vector index
#param repetition : Repetition along z
"""
# build n,m ribbon
ribbon = NanoRibbon(n,m)
ribbon = ribbon.repeat(1,1,repetition)
# get properties of the ribbon
lattice = ribbon.bravaisLattice()
elements = ribbon.elements()
cartesian_coordinates=ribbon.cartesianCoordinates().inUnitsOf(Angstrom)
# calculate the length of the 1-d structure
z_length = numpy.linalg.norm(lattice.primitiveVectors()[2].inUnitsOf(Angstrom))
# calculate twist parameters
rotation_angle_per_z = math.pi /z_length
rotation_axis = numpy.array([0,0,1])
rotation_axis_center = numpy.sum(cartesian_coordinates,axis=0)/len(cartesian_coordinates)
# define a function of one variable, f(c), for displacing the atoms
f = lambda c : Twister.displacement(c, rotation_angle_per_z, rotation_axis,
rotation_axis_center, 0.,z_length)
# apply the function to find new displaced coordinates
cartesian_coordinates = numpy.apply_along_axis(f, 1, cartesian_coordinates)
cartesian_center = numpy.sum(cartesian_coordinates,axis=0)/len(cartesian_coordinates)
cartesian_coordinates = cartesian_coordinates - cartesian_center
# define a function of one variable, f(c), for displacing the atoms
f = lambda c : displacement(c, z_length,2.0*math.pi)
# apply the function to find new displaced coordinates
cartesian_coordinates = numpy.apply_along_axis(f, 1, cartesian_coordinates)
return MoleculeConfiguration(
elements=elements,
cartesian_coordinates=cartesian_coordinates * Angstrom
)
# Instantiate the builder object and choose our title
builder = Builder()
builder.title('Moebius ribbon')
# Set the configuration generator
builder.setConfigurationGenerator(configuration)
# Tube properties group
builder.newGroup('Tube parameters')
builder.integer( 'n', 6, 'n', min=1, max=1000)
builder.integer( 'm', 0, 'm', min=0, max=1000)
builder.integer( 'repetition', 40, 'C-direction', min=1, max=1000)
Is there any similar module in Python, so that I can build the structure I want and also create the xyz coordinates? Or How I can proceed further to complete the code?
There is no possible way to transform the coordinates for one of your 2xN rings into a Möbius strip. That's because an alternating strip with a half twist in it must have an odd number of atoms, while your current rings always have an even number. You'd need to cut one atom out or add one, in order to make the twist work. Maybe you could make a transform that could work by stacking the first and last atoms on top of each other, but I think it would be very ugly (both mathematically and perhaps also in a plot).
While that probably makes it impractical to what you're asking with a 3D-transform, you can instead create coordinates from scratch with the desired twist. Just produce a single array of points for the single edge of the strip (having only one edge is one of the things that makes a Möbius strip strange). The edge makes two loops around the circle, one on each side of the strip, and the twist happens at half the speed of the main rotation.
Here's my stab at it:
bLength=1.6
n = 10
numPoints = 2*n + 1
radius = bLength*numPoints / (4*np.pi) # 4*pi because the edge goes around the circle twice
theta = np.linspace(0, 4*np.pi, numPoints, endpoint=False) # here too
x = (radius + np.cos(theta/2)) * np.cos(theta)
y = (radius + np.cos(theta/2)) * np.sin(theta)
z = np.sin(theta/2)
plt.plot(x, y) # I don't know how to graph in 3d, so only x and y here
plt.show()
This makes a strip of width of 2, just like your current code does. If that's not the correct width, you should multiply the cos terms added to the radius in the calculation of x and y and the sin value used for z by half the desired width.
Note that this while this code may produce what you want, the coordinates are probably nonsense when it comes to physically describing an actual ring of atoms. Some of the distances between the coordinates will be very different than others (e.g on the inside of the strip versus the outside when it's nearly flat), and a real Möbius strip made of atoms (if such a thing can be made at all) would probably fold in some way rather than twisting at a uniform rate (a Möbius strip made of paper doesn't twist this way either). Finding exactly how real atoms would be arranged would be a much more difficult problem (and a problem for a physicist, not a programmer).

Trigonometry to find landscape visibility

I'm trying to find the degrees of visibility of natural environments from a number of viewpoints. I have a DEM, and a raster defining the natural environments. For each cell I have slope, height, aspect and distance from viewpoint. I want to calculate the angle between the viewpoint and each cell.
I originally did this by getting the x,y,z coordinates for the viewpoint and the both the bottom and top of a sloped cell. Then get the 3D distance between each coordinate and use these distances to get the angles. I would then sum for each cell to get total degrees of visibilty.
Problem is that this approach only works when the bearing between the viewpoint and the visible cell is in line with the x and y axis of the coordinate system. This because to calculate the points coordinates for the cell corners I'm subtracting and adding the cell resolution/2 to the cell centroid.
Code below
def Cell_Loc (VPX, VPY, VPZ, cell_x, cell_y, cell_z, aspect, cell_resolution, bearing):
import math
from math import sqrt
#Get change in height of cell using slope and cell resolution (trig)
AspectTan = math.tan(math.radians(aspect))
Opposite = (AspectTan * cell_res)
#Get coordinates for cell corners
CloseCornerX = cent_x - 2.5
CloseCornerY = cent_y - 2.5
FarCornerX = cent_x + 2.5
FarCornerY = cent_y + 2.5
CloseCornerZ = cent_z - (Opposite/2)
FarCornerZ = cent_z + (Opposite/2)
#Get Distance between coordinates
VP = (VPX, VPY, VPZ) #data point 1
cellcrner1 = (CloseCornerX, CloseCornerY, CloseCornerZ) #data point 2
cellcrner2 = (FarCornerX, FarCornerY, FarCornerZ)
dist_to_far_corner = sqrt(sum( (VP - cellcrner2)**2 for VP, cellcrner2 in zip(VP, cellcrner2)))
dist_to_close_corner = sqrt(sum( (VP - cellcrner1)**2 for VP, cellcrner1 in zip(VP, cellcrner1)))
cell_dist = sqrt(sum( (cellcrner1 - cellcrner2)**2 for cellcrner1, cellcrner2 in zip(cellcrner1, cellcrner2)))
#Input above distances into ViewAngle to find angle
def ViewAngle (a, b, c):
"calculates the viewing angle of each visible cell"
import math
a2 = a*a
b2 = b*b
c2 = c*c
VA = (a2-b2+c2)/ (2.0 * (a*c))
rads = math.acos(VA)
return math.degrees(rads)
How can I improve this code, or maybe another approach is a better way of doing it. The output I need is to say "for this viewpoint, there are X amount of degrees for visible natural environments". Again I have, DEM, visible areas from viewshed analysis, slope, aspect and distances between viewpoint and cell centroid. I use python programming language and have ArcGIS10.2 with arcpy
One improvement would be to use coordinates and inner products instead of distances to calculate angles: this would help avoid some (though not all) expensive and messy square roots. So if your view point is at v=(x,y,z), and two other points are P=(a,b,c) and Q=(d,e,f), the subtended angle is acos(sqrt(((V-P)(V-Q))^2/(V-P)^2(V-Q)^2))=acos(sqrt(<(V-P),(V-Q)>^2/<(V-P),(V-P)><(V-Q),(V-Q)>)) where the (internal) products are inner products, i.e., say PQ = ad + be + cf. Also, some of the products and inner products can be 'recycled' instead of recalculating.

Generate random points on a surface of the cylinder

I want to generate random points on the surface of cylinder such that distance between the points fall in a range of 230 and 250. I used the following code to generate random points on surface of cylinder:
import random,math
H=300
R=20
s=random.random()
#theta = random.random()*2*math.pi
for i in range(0,300):
theta = random.random()*2*math.pi
z = random.random()*H
r=math.sqrt(s)*R
x=r*math.cos(theta)
y=r*math.sin(theta)
z=z
print 'C' , x,y,z
How can I generate random points such that they fall with in the range(on the surfaceof cylinder)?
This is not a complete solution, but an insight that should help. If you "unroll" the surface of the cylinder into a rectangle of width w=2*pi*r and height h, the task of finding distance between points is simplified. You have not explained how to measure "distance along the surface" between points on the top of the cylinder and the side- this is a slightly tricky bit of geometry.
As for computing the distance along the surface when we created an artificial "seam", just use both (x1-x2) and (w -x1+x2) - whichever gives the shorter distance is the one you want.
I do think that #VincentNivoliers' suggestion to use Poisson disk sampling is very good, but with the constraints of h=300 and r=20 you will get terrible results no matter what.
The basic way of creating a set of random points with constraints in the positions between them, is to have a function that modulates the probability of points being placed at a certain location. this function starts out being a constant, and whenever a point is placed, forbidden areas surrounding the point are set to zero. That is difficult to do with continuous variables, but reasonably easy if you discretize your problem.
The other thing to be careful about is the being on a cylinder part. It may be easier to think of it as random points on a rectangular area that repeats periodically. This can be handled in two different ways:
the simplest is to take into consideration not only the rectangular tile where you are placing the points, but also its neighbouring ones. Whenever you place a point in your main tile, you also place one in the neighboring ones and compute their effect on the probability function inside your tile.
A more sophisticated approach considers the probability function then convolution of a kernel that encodes forbidden areas, with a sum of delta functions, corresponding to the points already placed. If this is computed using FFTs, the periodicity is anatural by product.
The first approach can be coded as follows:
from __future__ import division
import numpy as np
r, h = 20, 300
w = 2*np.pi*r
int_w = int(np.rint(w))
mult = 10
pdf = np.ones((h*mult, int_w*mult), np.bool)
points = []
min_d, max_d = 230, 250
available_locs = pdf.sum()
while available_locs:
new_idx = np.random.randint(available_locs)
new_idx = np.nonzero(pdf.ravel())[0][new_idx]
new_point = np.array(np.unravel_index(new_idx, pdf.shape))
points += [new_point]
min_mask = np.ones_like(pdf)
if max_d is not None:
max_mask = np.zeros_like(pdf)
else:
max_mask = True
for p in [new_point - [0, int_w*mult], new_point +[0, int_w*mult],
new_point]:
rows = ((np.arange(pdf.shape[0]) - p[0]) / mult)**2
cols = ((np.arange(pdf.shape[1]) - p[1]) * 2*np.pi*r/int_w/mult)**2
dist2 = rows[:, None] + cols[None, :]
min_mask &= dist2 > min_d*min_d
if max_d is not None:
max_mask |= dist2 < max_d*max_d
pdf &= min_mask & max_mask
available_locs = pdf.sum()
points = np.array(points) / [mult, mult*int_w/(2*np.pi*r)]
If you run it with your values, the output is usually just one or two points, as the large minimum distance forbids all others. but if you run it with more reasonable values, e.g.
min_d, max_d = 50, 200
Here's how the probability function looks after placing each of the first 5 points:
Note that the points are returned as pairs of coordinates, the first being the height, the second the distance along the cylinder's circumference.

calculate turning points / pivot points in trajectory (path)

I'm trying to come up with an algorithm that will determine turning points in a trajectory of x/y coordinates. The following figures illustrates what I mean: green indicates the starting point and red the final point of the trajectory (the entire trajectory consists of ~ 1500 points):
In the following figure, I added by hand the possible (global) turning points that an algorithm could return:
Obviously, the true turning point is always debatable and will depend on the angle that one specifies that has to lie between points. Furthermore a turning point can be defined on a global scale (what I tried to do with the black circles), but could also be defined on a high-resolution local scale. I'm interested in the global (overall) direction changes, but I'd love to see a discussion on the different approaches that one would use to tease apart global vs local solutions.
What I've tried so far:
calculate distance between subsequent points
calculate angle between subsequent points
look how distance / angle changes between subsequent points
Unfortunately this doesn't give me any robust results. I probably have too calculate the curvature along multiple points, but that's just an idea.
I'd really appreciate any algorithms / ideas that might help me here. The code can be in any programming language, matlab or python are preferred.
EDIT here's the raw data (in case somebody want's to play with it):
mat file
text file (x coordinate first, y coordinate in second line)
You could use the Ramer-Douglas-Peucker (RDP) algorithm to simplify the path. Then you could compute the change in directions along each segment of the simplified path. The points corresponding to the greatest change in direction could be called the turning points:
A Python implementation of the RDP algorithm can be found on github.
import matplotlib.pyplot as plt
import numpy as np
import os
import rdp
def angle(dir):
"""
Returns the angles between vectors.
Parameters:
dir is a 2D-array of shape (N,M) representing N vectors in M-dimensional space.
The return value is a 1D-array of values of shape (N-1,), with each value
between 0 and pi.
0 implies the vectors point in the same direction
pi/2 implies the vectors are orthogonal
pi implies the vectors point in opposite directions
"""
dir2 = dir[1:]
dir1 = dir[:-1]
return np.arccos((dir1*dir2).sum(axis=1)/(
np.sqrt((dir1**2).sum(axis=1)*(dir2**2).sum(axis=1))))
tolerance = 70
min_angle = np.pi*0.22
filename = os.path.expanduser('~/tmp/bla.data')
points = np.genfromtxt(filename).T
print(len(points))
x, y = points.T
# Use the Ramer-Douglas-Peucker algorithm to simplify the path
# http://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm
# Python implementation: https://github.com/sebleier/RDP/
simplified = np.array(rdp.rdp(points.tolist(), tolerance))
print(len(simplified))
sx, sy = simplified.T
# compute the direction vectors on the simplified curve
directions = np.diff(simplified, axis=0)
theta = angle(directions)
# Select the index of the points with the greatest theta
# Large theta is associated with greatest change in direction.
idx = np.where(theta>min_angle)[0]+1
fig = plt.figure()
ax =fig.add_subplot(111)
ax.plot(x, y, 'b-', label='original path')
ax.plot(sx, sy, 'g--', label='simplified path')
ax.plot(sx[idx], sy[idx], 'ro', markersize = 10, label='turning points')
ax.invert_yaxis()
plt.legend(loc='best')
plt.show()
Two parameters were used above:
The RDP algorithm takes one parameter, the tolerance, which
represents the maximum distance the simplified path
can stray from the original path. The larger the tolerance, the cruder the simplified path.
The other parameter is the min_angle which defines what is considered a turning point. (I'm taking a turning point to be any point on the original path, whose angle between the entering and exiting vectors on the simplified path is greater than min_angle).
I will be giving numpy/scipy code below, as I have almost no Matlab experience.
If your curve is smooth enough, you could identify your turning points as those of highest curvature. Taking the point index number as the curve parameter, and a central differences scheme, you can compute the curvature with the following code
import numpy as np
import matplotlib.pyplot as plt
import scipy.ndimage
def first_derivative(x) :
return x[2:] - x[0:-2]
def second_derivative(x) :
return x[2:] - 2 * x[1:-1] + x[:-2]
def curvature(x, y) :
x_1 = first_derivative(x)
x_2 = second_derivative(x)
y_1 = first_derivative(y)
y_2 = second_derivative(y)
return np.abs(x_1 * y_2 - y_1 * x_2) / np.sqrt((x_1**2 + y_1**2)**3)
You will probably want to smooth your curve out first, then calculate the curvature, then identify the highest curvature points. The following function does just that:
def plot_turning_points(x, y, turning_points=10, smoothing_radius=3,
cluster_radius=10) :
if smoothing_radius :
weights = np.ones(2 * smoothing_radius + 1)
new_x = scipy.ndimage.convolve1d(x, weights, mode='constant', cval=0.0)
new_x = new_x[smoothing_radius:-smoothing_radius] / np.sum(weights)
new_y = scipy.ndimage.convolve1d(y, weights, mode='constant', cval=0.0)
new_y = new_y[smoothing_radius:-smoothing_radius] / np.sum(weights)
else :
new_x, new_y = x, y
k = curvature(new_x, new_y)
turn_point_idx = np.argsort(k)[::-1]
t_points = []
while len(t_points) < turning_points and len(turn_point_idx) > 0:
t_points += [turn_point_idx[0]]
idx = np.abs(turn_point_idx - turn_point_idx[0]) > cluster_radius
turn_point_idx = turn_point_idx[idx]
t_points = np.array(t_points)
t_points += smoothing_radius + 1
plt.plot(x,y, 'k-')
plt.plot(new_x, new_y, 'r-')
plt.plot(x[t_points], y[t_points], 'o')
plt.show()
Some explaining is in order:
turning_points is the number of points you want to identify
smoothing_radius is the radius of a smoothing convolution to be applied to your data before computing the curvature
cluster_radius is the distance from a point of high curvature selected as a turning point where no other point should be considered as a candidate.
You may have to play around with the parameters a little, but I got something like this:
>>> x, y = np.genfromtxt('bla.data')
>>> plot_turning_points(x, y, turning_points=20, smoothing_radius=15,
... cluster_radius=75)
Probably not good enough for a fully automated detection, but it's pretty close to what you wanted.
A very interesting question. Here is my solution, that allows for variable resolution. Although, fine-tuning it may not be simple, as it's mostly intended to narrow down
Every k points, calculate the convex hull and store it as a set. Go through the at most k points and remove any points that are not in the convex hull, in such a way that the points don't lose their original order.
The purpose here is that the convex hull will act as a filter, removing all of "unimportant points" leaving only the extreme points. Of course, if the k-value is too high, you'll end up with something too close to the actual convex hull, instead of what you actually want.
This should start with a small k, at least 4, then increase it until you get what you seek. You should also probably only include the middle point for every 3 points where the angle is below a certain amount, d. This would ensure that all of the turns are at least d degrees (not implemented in code below). However, this should probably be done incrementally to avoid loss of information, same as increasing the k-value. Another possible improvement would be to actually re-run with points that were removed, and and only remove points that were not in both convex hulls, though this requires a higher minimum k-value of at least 8.
The following code seems to work fairly well, but could still use improvements for efficiency and noise removal. It's also rather inelegant in determining when it should stop, thus the code really only works (as it stands) from around k=4 to k=14.
def convex_filter(points,k):
new_points = []
for pts in (points[i:i + k] for i in xrange(0, len(points), k)):
hull = set(convex_hull(pts))
for point in pts:
if point in hull:
new_points.append(point)
return new_points
# How the points are obtained is a minor point, but they need to be in the right order.
x_coords = [float(x) for x in x.split()]
y_coords = [float(y) for y in y.split()]
points = zip(x_coords,y_coords)
k = 10
prev_length = 0
new_points = points
# Filter using the convex hull until no more points are removed
while len(new_points) != prev_length:
prev_length = len(new_points)
new_points = convex_filter(new_points,k)
Here is a screen shot of the above code with k=14. The 61 red dots are the ones that remain after the filter.
The approach you took sounds promising but your data is heavily oversampled. You could filter the x and y coordinates first, for example with a wide Gaussian and then downsample.
In MATLAB, you could use x = conv(x, normpdf(-10 : 10, 0, 5)) and then x = x(1 : 5 : end). You will have to tweak those numbers depending on the intrinsic persistence of the objects you are tracking and the average distance between points.
Then, you will be able to detect changes in direction very reliably, using the same approach you tried before, based on the scalar product, I imagine.
Another idea is to examine the left and the right surroundings at every point. This may be done by creating a linear regression of N points before and after each point. If the intersecting angle between the points is below some threshold, then you have an corner.
This may be done efficiently by keeping a queue of the points currently in the linear regression and replacing old points with new points, similar to a running average.
You finally have to merge adjacent corners to a single corner. E.g. choosing the point with the strongest corner property.

Categories