I got a list of points by extracting the edge of a image, like that:But it is not well ordered, so if I connect it as a line, it will be:
Thus I want to sort this list if points. Like, start with point_0, find which one has the shortest distance with it, say, point_3, then find which one's closest to point_3 then continue...
To sort the points, I wrote this:
import matplotlib.pyplot as plt
import numpy as np
import math
def dist(now, seek):
return math.sqrt((now[0] - seek[0])**2 + (now[1] - seek[1])**2)
def sortNearest(x, y):
if len(x) != len(y):
raise Exception('Error! Array length do not match!')
return False
xNew = []; yNew = []
nearest = 0 #record which point is nearest
now = [x[0], y[0]] #start point index
seekValue = 0
while len(x) > 0:
distance = (max(x) - min(x)) + (max(y) - min(y))
for seek in range(len(x)): # other
temp = dist(now, [x[seek], y[seek]])
if temp < distance and temp != 0.0:
distance = temp
seekValue = x[seek]
xNew.append(now[0]);
yNew.append(now[1]);
if len(x) > 0:
x.remove(now[0])
y.remove(now[1])
if len(x) > 0:
nearest = x.index(seekValue)
now = [x[nearest], y[nearest]]
x = list(xNew); y = list(yNew)
return xNew, yNew
x, y = getBorder('large.png', maxRes = 125)
x, y = sortNearest(x, y)
But that doesn't work well, I came up with this:
Which is obviously incorrect, if I zoom in, see:
If my code runs what I want, point_644 should connect 620 or 675, any but 645... What's wrong with it?
Well, point 644 cannot connect to point 620, because 620 is already part of your path.
As for why it connects to 645 instead of the closer 675: in your loop, you aren't actually remembering the index of the closest point, you're only remembering its x coordinate. After the loop, you then locate an arbitrary point with the same x coordinate - it could be anywhere on a vertical line going through the desired point.
I don't know how I would do this in python 3.x, so please forgive changes that I have not made from python 2.7. You'll also want to figure out what point you'd like to start with:
def find_distance(point1, point2):
distance = sqrt(square(point1[0]-point2[0]) + square(point1[1] - point2[1]))
return distance
x, y = getBorder('large.png', maxRes = 125)
points_in_border = [(i,j) for i, j in zip(x,y)]
current_point = points_in_border.pop([0])
points_in_order = [current_point]
while len(points_in_border) > 0:
min_distance = 10000
for point in points_in_border:
if find_distance(current_point, point) < min_distance:
closest_point = point
min_distance = find_distance(current_point, point)
points_in_border.remove(closest_point)
current_point = closest_point
points_in_order.append(closest_point)
I think what you want to do can be optimized with numpy and scipy:
import numpy as np
import scipy.spatial.distance as distance
import matplotlib.pyplot as plt
points = np.random.random((6,2))
dists =distance.pdist(points)
m=np.argsort(distance.squareform(dists))[:,1:]
order = [0,m[0,0]]
next_point = order[-1]
while len(order)<len(points):
row = m[next_point]
i = 0
while row[i] in order:
i += 1
order.append(row[i])
next_point = order[-1]
order.append(0)
ordered=points[order]
plt.plot(ordered[:,0], ordered[:,1], 'o-')
The idea underlying this code is the following. First you calculate all the distances. Then you use argsort to get the indices that would order each row. You can remove the first column, as each point is closest to itself. We know that. Then you look which is the next closest point and you add it to the list order if the point is not there yet. You then go to the row corresponding to this point, and look for the next point. And so on.
If what you are only interested in is just sorting the enclosing set of points, you can use ConvexHull to find them:
ch = ConvexHull(points)
plt.plot(points[ch.vertices,0], points[ch.vertices,1], 'o-')
Related
I have a Python project where I need to redraw a line many times with the points in random places but keeping the line's shape and point count roughly the same. The final output will be using polygonal points and not Bezier paths (though I wouldn't be opposed to using Bezier as an intermediary step).
This animation is demonstrating how the points could move along the line to different positions while maintaining the general shape.
I also have a working example below where I'm moving along the line and picking random new points between existing points (the red line, below). It works okay, but I'd love to hear some other approaches I might take if someone knows of a better one?
Though this code is using matplotlib to demonstrate the line, the final program will not.
import numpy as np
from matplotlib import pyplot as plt
import random
from random import (randint,uniform)
def move_along_line(p1, p2, scalar):
distX = p2[0] - p1[0]
distY = p2[1] - p1[1]
modX = (distX * scalar) + p1[0]
modY = (distY * scalar) + p1[1]
return [modX, modY]
x_coords = [213.5500031,234.3809357,255.211853,276.0427856,296.8737183,317.7046204,340.1997681,364.3751221,388.5505066,414.8896484,444.5192261,478.5549622,514.5779419,545.4779053,570.3830566,588.0241699,598.2469482,599.772583,596.758728,593.7449341,590.7310791,593.373291,610.0373535,642.1326294,677.4451904,710.0697021,737.6887817,764.4020386,791.1152954,817.8284912,844.541687,871.2550049,897.9682007,924.6813965,951.3945923,978.1078491,1009.909546,1042.689941,1068.179199,1089.543091]
y_coords = [487.3099976,456.8832703,426.4565125,396.0297852,365.6030273,335.1763,306.0349426,278.1913452,250.3477478,224.7166748,203.0908051,191.2358704,197.6810608,217.504303,244.4946136,276.7698364,312.0551453,348.6885986,385.4395447,422.1904297,458.9414063,495.5985413,527.0128479,537.1477661,527.6642456,510.959259,486.6988525,461.2799683,435.8611145,410.4422913,385.023468,359.6045532,334.18573,308.7669067,283.3480835,257.929184,239.4429474,253.6099091,280.1803284,310.158783]
plt.plot(x_coords,y_coords,color='b')
plt.scatter(x_coords,y_coords,s=2)
new_line_x = []
new_line_y = []
for tgt in range(len(x_coords)-1):
#tgt = randint(0, len(x_coords)-1)
next_pt = tgt+1
new_pt = move_along_line([x_coords[tgt],y_coords[tgt]], [x_coords[next_pt],y_coords[next_pt]], uniform(0, 1))
new_line_x.append(new_pt[0])
new_line_y.append(new_pt[1])
plt.plot(new_line_x,new_line_y,color='r')
plt.scatter(new_line_x,new_line_y,s=10)
ax = plt.gca()
ax.set_aspect('equal')
plt.show()
Thank you very much!
I'm not sure if this is the most optimal way to do this but essentially you want to follow these steps:
Calculate the distance of the entire path, and the distance between all the points. Then for each point, tally the distances to that point.
Generate a new set of random points along the path starting with 0, then for each pair of points calculate a random distance: random value between 0 and 1 * total length of the path.
Sort these distances from smallest to largest.
For each random distance loop over the distances find the index where the random distance is > than distance i, and less than distance i+1. Interpolate new x and y values from these points.
from matplotlib import pyplot as plt
from scipy.interpolate import interp1d
import numpy
import random
import math
x_coords = [195.21,212.53,237.39,270.91,314.21,368.43,434.69,514.1,607.8,692.69,746.98,773.8,776.25,757.45,720.52,668.55,604.68,545.37,505.79,487.05,490.27,516.58,567.09,642.93,745.2,851.5,939.53,1010.54,1065.8,1106.58,1134.15,1149.75,1154.68]
y_coords = [195.34,272.27,356.59,438.98,510.14,560.76,581.52,563.13,496.27,404.39,318.83,242.15,176.92,125.69,91.02,75.48,81.62,113.49,168.57,239.59,319.29,400.38,475.6,537.67,579.32,586.78,558.32,504.7,436.69,365.05,300.55,253.95,236.03]
n_points = 100
x_coords = numpy.array(x_coords)
x_min = x_coords.min()
x_max = x_coords.max()
x_range = x_max - x_min
distances = []
tallied_distances = [0]
tallied_distance = 0
for i in range(0, len(x_coords) -1):
xi = x_coords[i]
xf = x_coords[i + 1]
yi= y_coords[i]
yf = y_coords[i+1]
d = math.sqrt((xf-xi)**2 + (yf-yi)**2)
tallied_distance += d
tallied_distances.append(tallied_distance)
random_distances_along_line = [0]
for i in range(0, n_points-2):
random_distances_along_line.append(random.random()*tallied_distance)
random_distances_along_line.sort()
new_x_points = [x_coords[0]]
new_y_points = [y_coords[0]]
for i in range(0, len(random_distances_along_line)):
dt = random_distances_along_line[i]
for j in range(0, len(tallied_distances)-1):
di = tallied_distances[j]
df = tallied_distances[j+1]
if di < dt and dt < df:
difference = dt - di
xi = x_coords[j]
xf = x_coords[j+1]
yi = y_coords[j]
yf = y_coords[j+1]
xt = xi+(xf-xi)*difference/(df-di)
yt = yi+(yf-yi)*difference/(df-di)
new_x_points.append(xt)
new_y_points.append(yt)
new_x_points.append(x_coords[len(x_coords)-1])
new_y_points.append(y_coords[len(y_coords)-1])
plt.plot(new_x_points, new_y_points)
plt.scatter(new_x_points, new_y_points,s=2)
ax = plt.gca()
ax.set_aspect('equal')
plt.show()
I'm currently really stuck with some of my code and I can't seem to find the issue. Here is what I am trying to do:
I have a big outer circle in which I want to display smaller dots. These dots should be randomly distributed but should not overlap, thus they should have a minimum distance to each other.
What I have tried is to first randomly generate a point, check wether it is in the outer circle and if it is, append it to the final list of dot positions. Then another point is created, checked if in circle and then it should be checked if the dot has a minimum distance to the other dot(s) in the final list.
However, I seem to have some issues with my code as it will not run through whenever I set the required distances higher than 1. I have changed multiple things, but I cannot make it work.
Does anyone have an idea about what the problem might be?
Here's what I have been trying:
import random
import numpy as np
import math
#Variables
radiusOC = 57
size_obj = 7
required_dist = 5
no_stimuli = 3
def CreatePos(radiusOC, size_obj, required_dist, no_stimuli):
final_list = []
def GenRandPos(radiusOC,size_obj):
"""
Takes the radius of the outer circle and generates random dots within this radius. Then checks if the the dots are located
within the outer circle.
"""
while True:
xPos = random.randint(-radiusOC,radiusOC)
yPos = random.randint(-radiusOC,radiusOC)
# check if in Circle
on_circle = (xPos- 0)**2 + (yPos-0)**2
if (radiusOC-size_obj)**2 >= on_circle:
print("Still in circle",on_circle, xPos, yPos )
position = [xPos, yPos]
break
else:
print("Not in circle",on_circle, xPos, yPos )
continue
return position
def CheckSurrounding(position, final_list, required_dist):
"""
Takes dot positions that are in the visual field, the list of positions, and the distances dots are required to have from each other.
It is checked if there are dots close by or not.
"""
X1 = position[0]
Y1 = position[1]
dist_list = []
for elem in final_list:
for i in elem:
X2 = elem[0]
Y2 = elem[1]
dist = math.sqrt((X1-X2)**2 + (Y1-Y2)**2)
dist_list.append(dist)
if all(dist_list) >= required_dist:
return position
else:
return None
# append the first dot to the list
position = GenRandPos(radiusOC, size_obj)
final_list.append(position)
# now append the rest of the dots if they have a certain distance to each other
while len(final_list) < no_stimuli:
position = GenRandPos(radiusOC, size_obj)
if CheckSurrounding(position, final_list, required_dist) != None:
position = CheckSurrounding(position, final_list, required_dist)
final_list.append(position)
else:
continue
return final_list
ยดยดยด
In the line
if all(dist_list) >= required_dist:
all(dist_list) will be either True or False, which is numerically equivalent to either 1 or 0. If required_dist is greater than 1 the inequality will never be satisfied. I think that you intended this to be
if all(dist_list >= required_dist):
but this will not work since you cannot compare a list dist_list to a number required_dist. To fix it, convert dist_list to a numpy array:
if np.all(np.array(dist_list) >= required_dist):
By the way, the random points you are selecting will always have integer coordinates since you are using random.randint(), I am not sure if this is intentional.
The whole code can be made more efficient by using numpy arrays. For example:
import numpy as np
def CreatePos(radiusOC, size_obj, required_dist, no_stimuli):
final_list = []
def GenRandPos(radiusOC, size_obj):
"""
Takes the radius of the outer circle and generates
random dots within this radius. Then checks if the dots are
located within the outer circle.
"""
while True:
position = (2 * np.random.random(2) - 1) * radiusOC
# check if in Circle
if (radiusOC - size_obj)**2 >= (position**2).sum():
return position
def CheckSurrounding(position, final_list, required_dist):
"""
Takes dot positions that are in the visual field,
the list of positions, and the distances dots are
required to have from each other.
It is checked if there are dots close by or not.
"""
final_arr = np.array(final_list)
dist = ((np.array(final_list) - position)**2).sum(axis=1)
if np.all(np.array(dist) >= required_dist**2):
return position
# append the first dot to the list
position = GenRandPos(radiusOC, size_obj)
final_list.append(position)
# now append the rest of the dots if they have a certain distance to each other
while len(final_list) < no_stimuli:
position = GenRandPos(radiusOC, size_obj)
if CheckSurrounding(position, final_list, required_dist) is not None:
final_list.append(position)
return final_list
Note that this returns a list of points with coordinates given by floats, not integers.
Sample usage:
#Variables
radiusOC = 57
size_obj = 7
required_dist = 3
no_stimuli = 400
final_list = np.array(CreatePos(radiusOC, size_obj, required_dist, no_stimuli))
Plot the resulting points:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(7,7))
ax = fig.add_subplot(111)
ax.set_aspect("equal")
plt.scatter(f[:, 0], f[:, 1])
plt.show()
This gives:
I would add a condition in the final while loop so it can break if a new point cannot be found after some number of attempts. Otherwise, it may end up running indefinitely.
You need to figure out a way to estimate the max number of points given the distance. This can be extrapolated from the circle packing problem. https://planetcalc.com/7473/ I will comment if I have an easy check to do.
import numpy as np
from matplotlib import pyplot as plt
from scipy.spatial.distance import cdist
def random_in_circle(num_points=1000, R=1, min_dist=0.2):
assert min_dist < R, "Min distance between points must be smaller than the radius of outer circle"
assert R / (2 ** (num_points - 1)) < min_dist, "Min dist is too large"
points = []
while len(points) < num_points:
a = np.random.rand() * 2 * np.pi # random angle
r = R * np.random.rand() # random radius
point = r * np.array([np.cos(a), np.sin(a)])
if len(points) == 0:
points.append(point)
elif np.all(cdist([point], points) > min_dist):
points.append(point)
return np.vstack(points)
points = random_in_circle(num_points=1000, min_dist=0.01)
plt.scatter(points[:, 0], points[:, 1])
plt.show()
I'm trying to create a list of N random (x,y,z) points using python, in a way that each point is at least a distance r away from any other point.
I'm super new to programming and so far I'm only able to generate x, y, and z separately (then put together) by using
import random
def RandX(start, end, num):
res = []
for j in range(num):
res.append(random.randint(start, end))
return res
num = N
start = 0
end = 100
print(RandX(start, end, num))
but I have no idea how to control or check the positions of the points(x, y, z) so that the points are a distance away from each other.
To check the distance between two points (x,y,z) and (a,b,c) (stored as tuples), you can try:
def distance(p1,p2):
d=0
for i in range(3):
d+=(p1[i]-p2[i])**2
return d**(1/2)
Once you generate xyz randomly, you can set the following:
p1=x,y,z
p2=a,b,c
If your numbers are not too large, while this is inefficient, you can generate random numbers until they satisfy the distance condition.
Here is my solution: all we need is a distance function and a loop to generate random points and check for minimum distance criteria within our already-generated list:
def dist(new_point, points, r_threshold):
for point in points:
dist = np.sqrt(np.sum(np.square(new_point-point)))
if dist < r_threshold:
return False
return True
def RandX(N, r_threshold):
points = []
scope = np.arange(0,10,0.1)
while len(points) < N:
new_point = np.random.choice(scope, 3)
if dist(new_point, points, r_threshold):
points.append(new_point)
return points
For example:
RandX(5, 4)
[array([3.5, 2.6, 7.6]),
array([9.9, 0.1, 7.2]),
array([4. , 2.8, 0.3]),
array([0.2, 7.4, 5.1]),
array([7.4, 6.3, 5.2])]
Something like this. (It can be optimised but should serve you as very first version)
from collections import namedtuple
import random
import math
Point = namedtuple('Point', ' x y z')
MIN = 0
MAX = 1000
def fill_points_list(points, number_of_required_points, min_distance):
def _get_distance(p1, p2):
return math.sqrt(sum([(a - b) ** 2 for a, b in zip(p1, p2)]))
while len(points) < number_of_required_points:
temp = Point(random.randint(MIN, MAX), random.randint(MIN, MAX), random.randint(MIN, MAX))
count = 0
for p in points:
if _get_distance(temp, p) > min_distance:
count += 1
else:
break
if len(points) == count:
points.append(temp)
number_of_required_points = 9
min_distance = 51
points = []
fill_points_list(points, number_of_required_points, min_distance)
print(points)
output
[Point(x=771, y=590, z=226), Point(x=385, y=835, z=900), Point(x=551, y=294, z=800), Point(x=824, y=306, z=333), Point(x=892, y=548, z=879), Point(x=520, y=660, z=384), Point(x=409, y=193, z=331), Point(x=411, y=706, z=300), Point(x=272, y=116, z=719)]
You could try to generate randomly a number of points, and then filter them based on the distance criteria. The numpy and sklearn packages can be helpful make the process more efficient. You could imagine something like this:
import numpy as np
from sklearn.metrics.pairwise import euclidean_distances
r = 2
# Generate 100 points (3-tuples) between 0 and 10
points = np.random.randint(0,100,[1000,3])
# Pairwise distances between points
distances = euclidean_distances(points)
# "Remove" distance to itself by setting to a distance of r+1 (to discard it later)
distances += np.identity(len(distances)) * (r+1)
# Retrieve the distance to the closest point
min_dist = np.min(distances,axis=1)
# Filter your set of points
filtered_points = points[min_dist>r]
This should run pretty fast.
For a list of (x, y) points, I am trying to find the nearby points for each point.
from collections import defaultdict
from math import sqrt
from random import randint
# Generate a list of random (x, y) points
points = [(randint(0, 100), randint(0, 100)) for _ in range(1000)]
def is_nearby(point_a, point_b, max_distance=5):
"""Two points are nearby if their Euclidean distance is less than max_distance"""
distance = sqrt((point_b[0] - point_a[0])**2 + (point_b[1] - point_a[1])**2)
return distance < max_distance
# For each point, find nearby points that are within a radius of 5
nearby_points = defaultdict(list)
for point in points:
for neighbour in points:
if point != neighbour:
if is_nearby(point, neighbour):
nearby_points[point].append(neighbour)
Is there any way I can index points to make the above search faster? I feel there must be some faster way than O(len(points)**2).
Edit: the points in general could be floats, not just ints
this is a version with a fixed grid where each gridpoint holds the number of samples that are there.
the search can then be reduced to just the space around the point in question.
from random import randint
import math
N = 100
N_SAMPLES = 1000
# create the grid
grd = [[0 for _ in range(N)] for __ in range(N)]
# set the number of points at a given gridpoint
for _ in range(N_SAMPLES):
grd[randint(0, 99)][randint(0, 99)] += 1
def find_neighbours(grid, point, distance):
# this will be: (x, y): number of points there
points = {}
for x in range(point[0]-distance, point[0]+distance):
if x < 0 or x > N-1:
continue
for y in range(point[1]-distance, point[1]+distance):
if y < 0 or y > N-1:
continue
dst = math.hypot(point[0]-x, point[1]-y)
if dst > distance:
continue
if grd[x][y] > 0:
points[(x, y)] = grd[x][y]
return points
print(find_neighbours(grid=grd, point=(45, 36), distance=5))
# -> {(44, 37): 1, (45, 33): 1, ...}
# meadning: there is one neighbour at (44, 37) etc...
for further optimzation: the tests for x and y could be precalculated for a given gridsize - the math.hypot(point[0]-x, point[1]-y) would not have to be done then for every point.
and it may be a good idea to replace the grid with a numpy array.
UPDATE
if your points are floats you can still create an int grid to reduce the search space:
from random import uniform
from collections import defaultdict
import math
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
#property
def x_int(self):
return int(self.x)
#property
def y_int(self):
return int(self.y)
def __str__(self):
fmt = '''{0.__class__.__name__}(x={0.x:5.2f}, y={0.y:5.2f})'''
return fmt.format(self)
N = 100
MIN = 0
MAX = N-1
N_SAMPLES = 1000
# create the grid
grd = [[[] for _ in range(N)] for __ in range(N)]
# set the number of points at a given gridpoint
for _ in range(N_SAMPLES):
p = Point(x=uniform(MIN, MAX), y=uniform(MIN, MAX))
grd[p.x_int][p.y_int].append(p)
def find_neighbours(grid, point, distance):
# this will be: (x_int, y_int): list of points
points = defaultdict(list)
# need to cast a slightly bigger net on the upper end of the range;
# int() rounds down
for x in range(point[0]-distance, point[0]+distance+1):
if x < 0 or x > N-1:
continue
for y in range(point[1]-distance, point[1]+distance+1):
if y < 0 or y > N-1:
continue
dst = math.hypot(point[0]-x, point[1]-y)
if dst > distance + 1: # account for rounding... is +1 enough?
continue
for pt in grd[x][y]:
if math.hypot(pt.x-x, pt.y-y) <= distance:
points[(x, y)].append(pt)
return points
res = find_neighbours(grid=grd, point=(45, 36), distance=5)
for int_point, points in res.items():
print(int_point)
for point in points:
print(' ', point)
the output looks something like this:
(44, 36)
Point(x=44.03, y=36.93)
(41, 36)
Point(x=41.91, y=36.55)
Point(x=41.73, y=36.53)
Point(x=41.56, y=36.88)
...
for convenience Points is now a class. may not be necessary though...
depending on how dense or sparse your points are you could also represent the grid as a dictionary pointing to a list or Points...
also the find_neighbours function accepts a starting point consisting of ints only in that version. this might also be refined.
and there is much room for improvement: the range of the y axis can be restricted using trigonometry. and for the points way inside the circle there is no need for an individual check; detailed checking only needs to be done close to the outer rim of the circle.
I am currently trying to identify peaks on a randomly generated plot that I have created.
My code is as follows:
x_range = np.arange(0,100,0.5) #my x values
for i in len(ys): #ys is my range of y values on the chart
for j in range(start,len(ys)): #Brute forcing peak detection
temp.append(ys[j])
check = int(classtest.isPeak(temp)[0])
if check == 1:
xval = temp.index(max(temp)) #getting the index
xlist = x_range.tolist()
plt.plot(xlist[xval],max(temp),"ro")
start = start + 1
temp = []
However when plotting the values on the graph, it seems to plot the correct y position, but not x. Here is an example of what is happening:
I am really not sure what is causing this problem, and I would love some help.
Thanks
Remember that temp is getting shorter and shorter as start increases.
So the index, xval, corresponding to a max in temp is not in itself the correct index into x_range. Rather, you have to increase xval by start to find the corresponding index in x_range:
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(2016)
N = 100
ys = (np.random.random(N)-0.5).cumsum()
xs = np.linspace(0, 100, len(ys))
plt.plot(xs, ys)
start = 0
temp = []
for i in range(len(ys)): #ys is my range of y values on the chart
for j in range(start,len(ys)): #Brute forcing peak detection
temp.append(ys[j])
xval = temp.index(max(temp)) #getting the index
plt.plot(xs[xval+start], max(temp),"ro")
start = start + 1
temp = []
plt.show()
While that does manage to place the red dots at points on the graph, as you can
see the algorithm is placing a dot at every point on the graph, not just at local
maxima. Part of the problem is that when temp contains only one point, it is
of course the max. And the double for-loop ensures that every point gets
considered, so at some point temp contains each point on the graph alone as a
single point.
A different algorithm is required. A local max can be identified as any
point which is bigger than its neighbors:
ys[i-1] <= ys[i] >= ys[i+1]
therefore, you could use:
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(2016)
N = 100
ys = (np.random.random(N)-0.5).cumsum()
xs = np.linspace(0, 100, len(ys))
plt.plot(xs, ys)
idx = []
for i in range(1, len(ys)-1):
if ys[i-1] <= ys[i] >= ys[i+1]:
idx.append(i)
plt.plot(xs[idx], ys[idx], 'ro')
plt.show()
Note that scipy.signal.argrelextrema or scipy.signal.argrelmax can also be used to find local maximums:
from scipy import signal
idx = signal.argrelextrema(ys, np.greater)
plt.plot(xs[idx], ys[idx], 'ro')
produces the same result.