I'm trying to create some artistic "plots" like the ones below:
The color of the regions do not really matter, what I'm trying to achieve is the variable "thickness" of the edges along the Voronoi regions (espescially, how they look like a bigger rounded blob where they meet in corners, and thinner at their middle point).
I've tried by "painting manually" each pixel based on the minimum distance to each centroid (each associated with a color):
n_centroids = 10
centroids = [(random.randint(0, h), random.randint(0, w)) for _ in range(n_centroids)]
colors = np.array([np.random.choice(range(256), size=3) for _ in range(n_centroids)]) / 255
for x, y in it.product(range(h), range(w)):
distances = np.sqrt([(x - c[0])**2 + (y - c[1])**2 for c in centroids])
centroid_i = np.argmin(distances)
img[x, y] = colors[centroid_i]
plt.imshow(img, cmap='gray')
Or by scipy.spatial.Voronoi, that also gives me the vertices points, although I still can't see how I can draw a line through them with the desired variable thickness.
from scipy.spatial import Voronoi, voronoi_plot_2d
# make up data points
points = [(random.randint(0, 10), random.randint(0, 10)) for _ in range(10)]
# add 4 distant dummy points
points = np.append(points, [[999,999], [-999,999], [999,-999], [-999,-999]], axis = 0)
# compute Voronoi tesselation
vor = Voronoi(points)
# plot
voronoi_plot_2d(vor)
# colorize
for region in vor.regions:
if not -1 in region:
polygon = [vor.vertices[i] for i in region]
plt.fill(*zip(*polygon))
# fix the range of axes
plt.xlim([-2,12]), plt.ylim([-2,12])
plt.show()
Edit:
I've managed to get a somewhat satisfying result via erosion + corner smoothing (via median filter as suggested in the comments) on each individual region, then drawing it into a black background.
res = np.zeros((h,w,3))
for color in colors:
region = (img == color)[:,:,0]
region = region.astype(np.uint8) * 255
region = sg.medfilt2d(region, 15) # smooth corners
# make edges from eroding regions
region = cv2.erode(region, np.ones((3, 3), np.uint8))
region = region.astype(bool)
res[region] = color
plt.imshow(res)
But as you can see the "stretched" line along the boundaries/edges of the regions is not quite there. Any other suggestions?
This is what #JohanC suggestion looks like. IMO, it looks much better than my attempt with Bezier curves. However, there appears to be a small problem with the RoundedPolygon class, as there are sometimes small defects at the corners (e.g. between blue and purple in the image below).
Edit: I fixed the RoundedPolygon class.
#!/usr/bin/env python
# coding: utf-8
"""
https://stackoverflow.com/questions/72061965/create-voronoi-art-with-rounded-region-edges
"""
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import patches, path
from scipy.spatial import Voronoi, voronoi_plot_2d
def shrink(polygon, pad):
center = np.mean(polygon, axis=0)
resized = np.zeros_like(polygon)
for ii, point in enumerate(polygon):
vector = point - center
unit_vector = vector / np.linalg.norm(vector)
resized[ii] = point - pad * unit_vector
return resized
class RoundedPolygon(patches.PathPatch):
# https://stackoverflow.com/a/66279687/2912349
def __init__(self, xy, pad, **kwargs):
p = path.Path(*self.__round(xy=xy, pad=pad))
super().__init__(path=p, **kwargs)
def __round(self, xy, pad):
n = len(xy)
for i in range(0, n):
x0, x1, x2 = np.atleast_1d(xy[i - 1], xy[i], xy[(i + 1) % n])
d01, d12 = x1 - x0, x2 - x1
l01, l12 = np.linalg.norm(d01), np.linalg.norm(d12)
u01, u12 = d01 / l01, d12 / l12
x00 = x0 + min(pad, 0.5 * l01) * u01
x01 = x1 - min(pad, 0.5 * l01) * u01
x10 = x1 + min(pad, 0.5 * l12) * u12
x11 = x2 - min(pad, 0.5 * l12) * u12
if i == 0:
verts = [x00, x01, x1, x10]
else:
verts += [x01, x1, x10]
codes = [path.Path.MOVETO] + n*[path.Path.LINETO, path.Path.CURVE3, path.Path.CURVE3]
verts[0] = verts[-1]
return np.atleast_1d(verts, codes)
if __name__ == '__main__':
# make up data points
n = 100
max_x = 20
max_y = 10
points = np.c_[np.random.uniform(0, max_x, size=n),
np.random.uniform(0, max_y, size=n)]
# add 4 distant dummy points
points = np.append(points, [[2 * max_x, 2 * max_y],
[ -max_x, 2 * max_y],
[2 * max_x, -max_y],
[ -max_x, -max_y]], axis = 0)
# compute Voronoi tesselation
vor = Voronoi(points)
fig, ax = plt.subplots(figsize=(max_x, max_y))
for region in vor.regions:
if region and (not -1 in region):
polygon = np.array([vor.vertices[i] for i in region])
resized = shrink(polygon, 0.15)
ax.add_patch(RoundedPolygon(resized, 0.2, color=plt.cm.Reds(0.5 + 0.5*np.random.rand())))
ax.axis([0, max_x, 0, max_y])
ax.axis('off')
ax.set_facecolor('black')
ax.add_artist(ax.patch)
ax.patch.set_zorder(-1)
plt.show()
Could something like bezier polygon "approximations" help me with this?
An attempt using Bezier curves:
#!/usr/bin/env python
# coding: utf-8
"""
https://stackoverflow.com/questions/72061965/create-voronoi-art-with-rounded-region-edges
"""
import numpy as np
import matplotlib.pyplot as plt
from scipy.spatial import Voronoi, voronoi_plot_2d
from bezier.curve import Curve # https://bezier.readthedocs.io/en/stable/python/index.html
def get_bezier(polygon, n=10):
closed_polygon = np.concatenate([polygon, [polygon[0]]])
# Insert additional points lying along the edges of the polygon;
# this allows us to use higher order bezier curves.
augmented_polygon = np.array(augment(closed_polygon, n))
# The bezier package does not seem to support closed bezier curves;
# to simulate a closed bezier curve, we triplicate the polygon,
# and only evaluate the curve on the inner third.
triplicated_polygon = np.vstack([augmented_polygon, augmented_polygon, augmented_polygon])
bezier_curve = Curve(triplicated_polygon.T, degree=len(triplicated_polygon)-1)
return bezier_curve.evaluate_multi(np.linspace(1./3, 2./3, 100)).T
def augment(polygon, n=10):
new_points = []
for ii, (x0, y0) in enumerate(polygon[:-1]):
x1, y1 = polygon[ii+1]
x = np.linspace(x0, x1, n)
y = np.linspace(y0, y1, n)
new_points.extend(list(zip(x[:-1], y[:-1])))
new_points.append((x1, y1))
return new_points
if __name__ == '__main__':
# make up data points
points = np.random.randint(0, 11, size=(50, 2))
# add 4 distant dummy points
points = np.append(points, [[999,999], [-999,999], [999,-999], [-999,-999]], axis = 0)
# compute Voronoi tesselation
vor = Voronoi(points)
# voronoi_plot_2d(vor)
fig, ax = plt.subplots()
for region in vor.regions:
if region and (not -1 in region):
polygon = np.array([vor.vertices[i] for i in region])
bezier_curve_points = get_bezier(polygon, 40)
ax.fill(*zip(*bezier_curve_points))
ax.axis([1, 9, 1, 9])
ax.axis('off')
plt.show()
Related
By using python, I want to plot 6 points in same diagram where first 3 points are 3 initial (x,y,z) position points and other 3 points are 3 updated position points (X,Y,Z).
The updated points formulas are X =๐ฅ+๐๐๐ฅ , Y=๐ฆ+๐๐y , and Z=๐ง+๐*๐z .
I plotted the 3 initial points. The problem is that all the 3 updated points have same value, then same point in the diagram. How can I make them different?
I think the problem is that in the Updated xyz positions, it takes only this point (x,y,z)=(3,6,9). How can I make these 3 updated position points different?
# Updated Positions and Direction Cosines
############################################
# Request libraries
############################################
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import random
from random import gauss
import math
from math import log, cos, acos
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
############################################
# Initial Positions and Direction Cosines
############################################
# Photons Number
PhotonsTotalNumber = 3
#PhotonsUniqueNumber= np.array(0,PhotonsTotalNumber)
# Initial Direction Cosines
Initial_ux= math.cos(math.radians(90))
Initial_uy= math.cos(math.radians(90))
Initial_uz= math.cos(math.radians(0))
# Initial xyz Positions
Initialx = np.array([1,2,3])
Initialy = np.array([4,5,6])
Initialz = np.array([7,8,9])
for x, y, z in zip(Initialx, Initialy, Initialz):
text = '({},{}, {})'.format(x,y,z)
ax.text(x,y,z, text, zdir=(1, 1, 1))
print(x,y,z)
############################################
# Photon Propagation Equations
############################################
# Random Numbers Function
def get_rand_number(min_value, max_value):
range = max_value - min_value
choice = random.uniform(0,1)
return min_value + range*choice
# Photon path length
r = -math.log(get_rand_number(0,1))/0.15 # 0.15 attenuation coefficients for clear water
# The anisotropy factor g
values = [1,-1]
g = np.random.choice(values)
g = int(g)
# Scattering Angles (theta and phi)
theta = (((1 + g*g - ((1 - g*g)/(1 - g + 2*g*get_rand_number(0,1)))**2)/(2*g)))
phi = 2 * math.radians(180) * get_rand_number(0,1)
Theta = gauss( 0, theta)
Phi = gauss( 0, phi)
############################################
# Updated Positions and Direction Cosines
############################################
# Updated direction cosines
New_ux = math.sin(Theta) * math.cos(Phi)
New_uy = math.sin(Theta) * math.sin(Phi)
New_uz = (Initial_uz/abs(Initial_uz))*math.cos(Theta)
# Updated xyz positions
Updatedx = x + (r * New_ux)
Updatedy = y + (r * New_uy)
Updatedz = z + (r * New_uz)
UpdatedX = np.array ([Updatedx,Updatedx,Updatedx])
UpdatedY = np.array ([Updatedy,Updatedy,Updatedy])
UpdatedZ = np.array ([Updatedz,Updatedz,Updatedz])
for X, Y, Z in zip(UpdatedX, UpdatedY, UpdatedZ):
text = '({},{}, {})'.format(X,Y,Z)
ax.text(X,Y,Z, text, zdir=(1, 1, 1))
ax.scatter(Initialx,Initialy,Initialz, s=100,c='b', label='True Position')
ax.scatter(UpdatedX,UpdatedY,UpdatedZ, s=100,c='g', label='True Position')
ax.set_xlabel("x axis")
ax.set_ylabel("y axis")
ax.set_zlabel("z axis")
plt.show()
####################
I am trying to write a program that given a list of points indicating a path and given a desired number of marks, it should distribute these marks exactly evenly along the path. As it happens the path is cyclical but given an arbitrary point to both start and end at I don't think it affects the algorithm at all.
The first step is to sum up the length of the line segments to determine the total length of the path, and then dividing that by the number of marks to get the desired distance between marks. Easy enough.
The next step is to walk along the path, storing the coordinates of each mark each time you traverse another even multiple's worth of the distance between marks.
In my code, the traversal seems correct but the distribution of marks is not even and does not exactly follow the path. I have created a visualization in matplotlib to plot where the marks are landing showing this (see last section).
path data
point_data = [
(53.8024, 50.4762), (49.5272, 51.8727), (45.0118, 52.3863), (40.5399, 53.0184), (36.3951, 54.7708),
(28.7127, 58.6807), (25.5306, 61.4955), (23.3828, 65.2082), (22.6764, 68.3316), (22.6945, 71.535),
(24.6674, 77.6427), (28.8279, 82.4529), (31.5805, 84.0346), (34.7024, 84.8875), (45.9183, 84.5739),
(57.0529, 82.9846), (64.2141, 79.1657), (71.089, 74.802), (76.7944, 69.8429), (82.1092, 64.4783),
(83.974, 63.3605), (85.2997, 61.5455), (85.7719, 59.4206), (85.0764, 57.3729), (82.0979, 56.0247),
(78.878, 55.1062), (73.891, 53.0987), (68.7101, 51.7283), (63.6943, 51.2997), (58.6791, 51.7438),
(56.1255, 51.5243), (53.8024, 50.4762), (53.8024, 50.4762)]
traversal
import math
number_of_points = 20
def euclid_dist(x1, y1, x2, y2):
return ((x1-x2)**2 + (y1-y2)**2)**0.5
def move_point(x0, y0, d, theta_rad):
return x0 + d*math.cos(theta_rad), y0 + d*math.sin(theta_rad)
total_dist = 0
for i in range(1, len(point_data), 1):
x1, y1 = point_data[i - 1]
x2, y2 = point_data[i]
total_dist += euclid_dist(x1, y1, x2, y2)
dist_per_point = total_dist / number_of_points
length_left_over = 0 # distance left over from the last segment
# led_id = 0
results = []
for i in range(1, len(point_data), 1):
x1, y1 = point_data[i - 1]
x2, y2 = point_data[i]
angle_rads = math.atan2(y1-y2, x1-x2)
extra_rotation = math.pi / 2 # 90deg
angle_output = math.degrees((angle_rads + extra_rotation + math.pi) % (2*math.pi) - math.pi)
length_of_segment = euclid_dist(x1, y1, x2, y2)
distance_to_work_with = length_left_over + length_of_segment
current_dist = dist_per_point - length_left_over
while distance_to_work_with > dist_per_point:
new_point = move_point(x1, y1, current_dist, angle_rads)
results.append((new_point[0], new_point[1], angle_output))
current_dist += dist_per_point
distance_to_work_with -= dist_per_point
length_left_over = distance_to_work_with
visualization code
import matplotlib.pyplot as plt
from matplotlib import collections as mc
import numpy as np
X = np.array([x for x, _, _ in results])
Y = np.array([y for _, y, _ in results])
plt.scatter(X, Y)
for i, (x, y) in enumerate(zip(X, Y)):
plt.text(x, y, str(i), color="red", fontsize=12)
possible_colors = [(1, 0, 0, 1), (0, 1, 0, 1), (0, 0, 1, 1)]
lines = []
colors = []
for i in range(len(point_data) -1 , 0, -1):
x1, y1 = point_data[i - 1]
x2, y2 = point_data[i]
lines.append(((x1, y1), (x2, y2)))
colors.append(possible_colors[i % 3])
lc = mc.LineCollection(lines, colors = colors, linewidths=2)
fig, ax = plt.subplots()
ax.add_collection(lc)
ax.autoscale()
ax.margins(0.1)
plt.show()
visualization result
The key here is to find the segment on the path for each of the points we want to distribute along the path based on the cumulative distance (across segments) from the starting point on the path. Then, interpolate for the point based on the distance between the two end points of the segment in which the point is on the path. The following code does this using a mixture of numpy array processing and list comprehension:
point_data = [
(53.8024, 50.4762), (49.5272, 51.8727), (45.0118, 52.3863), (40.5399, 53.0184), (36.3951, 54.7708),
(28.7127, 58.6807), (25.5306, 61.4955), (23.3828, 65.2082), (22.6764, 68.3316), (22.6945, 71.535),
(24.6674, 77.6427), (28.8279, 82.4529), (31.5805, 84.0346), (34.7024, 84.8875), (45.9183, 84.5739),
(57.0529, 82.9846), (64.2141, 79.1657), (71.089, 74.802), (76.7944, 69.8429), (82.1092, 64.4783),
(83.974, 63.3605), (85.2997, 61.5455), (85.7719, 59.4206), (85.0764, 57.3729), (82.0979, 56.0247),
(78.878, 55.1062), (73.891, 53.0987), (68.7101, 51.7283), (63.6943, 51.2997), (58.6791, 51.7438),
(56.1255, 51.5243), (53.8024, 50.4762), (53.8024, 50.4762)
]
number_of_points = 20
def euclid_dist(x1, y1, x2, y2):
return ((x1-x2)**2 + (y1-y2)**2)**0.5
# compute distances between segment end-points (padded with 0. at the start)
# I am using the OP supplied function and list comprehension, but this
# can also be done using numpy
dist_between_points = [0.] + [euclid_dist(p0[0], p0[1], p1[0], p1[1])
for p0, p1 in zip(point_data[:-1], point_data[1:])]
cum_dist_to_point = np.cumsum(dist_between_points)
total_dist = sum(dist_between_points)
cum_dist_per_point = np.linspace(0., total_dist, number_of_points, endpoint=False)
# find the segment that the points will be in
point_line_segment_indices = np.searchsorted(cum_dist_to_point, cum_dist_per_point, side='right').astype(int)
# then do linear interpolation for the point based on distance between the two end points of the segment
# d0s: left end-point cumulative distances from start for segment containing point
# d1s: right end-point cumulative distances from start for segment containing point
# alphas: the interpolation distance in the segment
# p0s: left end-point for segment containing point
# p1s: right end-point for segment containing point
d0s = cum_dist_to_point[point_line_segment_indices - 1]
d1s = cum_dist_to_point[point_line_segment_indices]
alphas = (cum_dist_per_point - d0s) / (d1s - d0s)
p0s = [point_data[segment_index - 1] for segment_index in point_line_segment_indices]
p1s = [point_data[segment_index] for segment_index in point_line_segment_indices]
results = [(p0[0] + alpha * (p1[0] - p0[0]), p0[1] + alpha * (p1[1] - p0[1]))
for p0, p1, alpha in zip(p0s, p1s, alphas)]
The array cum_dist_to_point is the cumulative (across segments) distance along the path from the start to each point in point_data, and the array cum_dist_per_point is the cumulative distance along the path for the number of points we want to evenly distribute along the path. Note that we use np.searchsorted to identify the segment on the path (by cumulative distance from start) that the point, with a given distance from the start, lies in. According to the documentation, searchsorted:
Find the indices into a sorted array (first argument) such that, if the corresponding elements in the second argument were inserted before the indices, the order would be preserved.
Then, using the OP's plot function (slightly modified because results no longer has an angle component):
def plot_me(results):
X = np.array([x for x, _ in results])
Y = np.array([y for _, y in results])
plt.scatter(X, Y)
for i, (x, y) in enumerate(zip(X, Y)):
plt.text(x, y, str(i), color="red", fontsize=12)
possible_colors = [(1, 0, 0, 1), (0, 1, 0, 1), (0, 0, 1, 1)]
lines = []
colors = []
for i in range(len(point_data) -1, 0, -1):
x1, y1 = point_data[i - 1]
x2, y2 = point_data[i]
lines.append(((x1, y1), (x2, y2)))
colors.append(possible_colors[i % 3])
lc = mc.LineCollection(lines, colors=colors, linewidths=2)
fig, ax = plt.subplots()
ax.add_collection(lc)
ax.autoscale()
ax.margins(0.1)
plt.show()
We have:
plot_me(results)
I need to find the other two points of a rectangle given two points shown in black in the attached drawing. The missing points are showed in yellow width is in red which I do have as well. The rectangle can be arbitrarily rotated.
I also know if the points are on the same side or on opposite sides of the rectangle.
There are four possible arrangements each of which I need to solve for as shown.
In the rectangle, where the rectangle perfectly cuts into two equal triangle.
By applying Pythagoras theorem {hypotenuse^2=base^2+perpendicular^2}.
`base = width of rectangle
perpendicular = length of rectangle
hypotenuse = unknown/desired`
For length of rectangle:
Length of a rectangle = Area รท breadth.
If this is not requested please tell.
As you have added the python tag to your question, I assume you want to solve this problem using python.
The Two Scenarios
either the points given represent a diagonal (1)
or the points given represent an edge (2)
(1) Diagonal Approach:
Diagonal Approach Sketch
(Right here, we use the the theorem of Thales. If you don't know it, just look it up. It's quite useful.)
(2) Edge Approach:
Edge Approach Sketch
Python Code
This is pure calculation. Just change the properties at the very end of this script.
import math
import numpy as np
# lovely method by mujjiga
# from https://stackoverflow.com/questions/55816902/finding-the-intersection-of-two-circles
# returns intersections of two circles
def get_intersections(x0, y0, r0, x1, y1, r1):
# circle 1: (x0, y0), radius r0
# circle 2: (x1, y1), radius r1
d = math.sqrt((x1-x0)**2 + (y1-y0)**2)
# non intersecting
if d > r0 + r1:
return None
# One circle within other
if d < abs(r0-r1):
return None
# coincident circles
if d == 0 and r0 == r1:
return None
else:
a = (r0**2-r1**2+d**2)/(2*d)
h = math.sqrt(r0**2-a**2)
x2 = x0+a*(x1-x0)/d
y2 = y0+a*(y1-y0)/d
x3 = x2+h*(y1-y0)/d
y3 = y2-h*(x1-x0)/d
x4 = x2-h*(y1-y0)/d
y4 = y2+h*(x1-x0)/d
return (x3, y3, x4, y4)
# returns None or 4 possible points
def get_unknown_points(p1, p2, width, is_diagonal):
# convert tuples/lists to nummpy arrays
p1 = np.array(p1, dtype=float)
p2 = np.array(p2, dtype=float)
# vector from p1 to p2
p1_to_p2 = p2 - p1
# magnitude/length of this vector
length = np.linalg.norm(p2 - p1)
if is_diagonal:
mid = p1 + 0.5 * p1_to_p2
mid_radius = length * 0.5
points = get_intersections(
p1[0], p1[1], width, mid[0], mid[1], mid_radius)
# no intersections found
if points is None:
return None
other_points = get_intersections(
p2[0], p2[1], width, mid[0], mid[1], mid_radius)
# return the two different possibilities
possibilities = []
possibilities.append(
((points[0], points[1]), (other_points[0], other_points[1])))
possibilities.append(
((points[2], points[3]), (other_points[2], other_points[3])))
return possibilities
# p1 and p2 do not represent the diagonal
else:
# get a perpendicular vector regarding p1_to_p2 (taken from https://stackoverflow.com/questions/33658620/generating-two-orthogonal-vectors-that-are-orthogonal-to-a-particular-direction)
perpendicular_vector = np.random.randn(2)
perpendicular_vector -= perpendicular_vector.dot(
p1_to_p2) * p1_to_p2 / np.linalg.norm(p1_to_p2)**2
# make length of vector correspond to width
perpendicular_vector /= np.linalg.norm(perpendicular_vector)
perpendicular_vector *= width
# add this vector to p1 and p2 and return both possibilities
possibilities = []
possibilities.append(
((p1[0] + perpendicular_vector[0], p1[1] + perpendicular_vector[1]), (p2[0] + perpendicular_vector[0], p2[1] + perpendicular_vector[1])))
possibilities.append(
((p1[0] - perpendicular_vector[0], p1[1] - perpendicular_vector[1]), (p2[0] - perpendicular_vector[0], p2[1] - perpendicular_vector[1])))
return possibilities
# change these properties
p1 = (4, 5)
p2 = (5, 1)
diagonal = True
width = 1
points = get_unknown_points(p1, p2, width, diagonal)
# output
if points is None:
print("There are no points that can be calculated from the points given!")
else:
print(
f"Possibilty 1: \n\tPoint1: {points[0][0]} \n\tPoint2: {points[0][1]}")
print(
f"Possibilty 2: \n\tPoint1: {points[1][0]} \n\tPoint2: {points[1][1]}")
Visualization
If you want to see the results visually, just append these lines of code to the end of the upper script.
import matplotlib.pyplot as plt
# displaying results
if points is not None:
p1 = np.array(p1, dtype=float)
p2 = np.array(p2, dtype=float)
fig, ax = plt.subplots()
ax.set_xlim((-1, 10))
ax.set_ylim((-1, 10))
if diagonal:
dist = np.linalg.norm(p2 - p1)
mid = p1 + 0.5 * (p2 - p1)
mid_radius = dist * 0.5
circle2 = plt.Circle(mid, mid_radius, color='orange', fill=False)
circle3 = plt.Circle(p1, width, color='g', fill=False)
circle1 = plt.Circle(p2, width, color='g', fill=False)
ax.add_artist(circle1)
ax.add_artist(circle2)
ax.add_artist(circle3)
plt.plot(p1, p2, '.', color='black')
print(points[0][1])
plt.plot([points[0][0][0], points[0][1][0]], [
points[0][0][1], points[0][1][1]], '.', color='red')
plt.plot([points[1][0][0], points[1][1][0]], [
points[1][0][1], points[1][1][1]], '.', color='blue')
plt.gca().set_aspect('equal', adjustable='box')
plt.show()
Results
Diagonal Approach Example
Edge Approach Example
I've been trying to push the boundaries of matplotlib's patches and instruct it to draw a rounded FancyArrowPatch with a directional arrow on its midpoint. This would prove incredibly useful in a network representation I am trying to create.
My coding hours with python are not yet in the double digit, so I can't say I have a clear understanding of matplotlib's patches.py, but I have narrowed down the solution to two possible strategies:
the smart, possibly pythonic way: create a custom arrowstyle class which further requires a modification of the _get_arrow_wedge() function to include a midpoint coordinates. This may be beyond my possibilities for now, or
the lazy way: extract the midpoint coordinates from an elicited FancyArrowPatch and draw the desired arrowstyle on such coordinates.
Of course, so far I've chosen the lazy way. I did some early experimenting with extracting the midpoint coordinates of a curved FancyArrowPatch using get_path() and get_path_in_displaycoord(), but I can't seem to predict the precise midpoint coordinates. Some help would be very appreciated.
My fiddling so far:
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch
n1 = (2,3)
n2 = (4,6)
# Try with multiple arc radius sizes, draw a separate plot each time
for rad in range(20):
#setup figure
figure = plt.figure()
ax = plt.subplot(111)
plt.annotate('rad:' + str(rad/25.),xy=(2,5))
# create rounded fancyarrowpatch
t = FancyArrowPatch(posA=n1,posB=n2,
connectionstyle='arc3,rad=%s'%float(rad/25.),
arrowstyle='->',
shrinkA=0,
shrinkB=0,
mutation_scale=0.5)
# extract vertices from get_path: points P#
path = t.get_path().vertices.tolist()
lab, px, py = ['P{0}'.format(i) for i in range(len(path))], [u[0] for u in path],[u[1] for u in path]
for i in range(len(path)):
plt.annotate(lab[i],xy=(px[i],py[i]))
# extract vertices from get_path_in_displaycoord (but they are useless) : points G#
newpath = t.get_path_in_displaycoord()
a,b = newpath[0][0].vertices.tolist(), newpath[0][1].vertices.tolist()
a.extend(b)
glab, gx, gy = ['G{0}'.format(i) for i in range(len(a))], [u[0] for u in a],[u[1] for u in a]
for i in range(len(a)):
plt.annotate(glab[i],xy=(gx[i],gy[i]))
#point A: start
x1, y1 = n1
plt.annotate('A',xy=(x1,y1))
#point B:end
x2, y2 = n2
plt.annotate('B',xy=(x2,y2))
#point M: the 'midpoint' as defined by class Arc3, specifically its connect() function
x12, y12 = (x1 + x2) / 2., (y1 + y2) / 2.
dx, dy = x2 - x1, y2 - y1
cx, cy = x12 + (rad/100.) * dy, y12 - (rad/100.) * dx
plt.annotate('M',xy=(cx,cy))
#point O : midpoint between M and P1, the second vertex from get_path
mx,my = (cx + px[1])/2., (cy + py[1])/2.
plt.annotate('O',xy=(mx,my))
ax.add_patch(t)
plt.scatter([x1,cx,x2,mx,gx].extend(px),[y1,cy,y2,my,gy].extend(py))
plt.show()
EDIT: taking onboard #cphlewis suggestions: I tried to reconstruct the Bezier curve:
def bezcurv(start,control,end,tau):
ans = []
for t in tau:
B = [(1-t)**2 * start[i] + 2*(1-t)*t*end[i] + (t**2)*control[i] for i in range(len(start))]
ans.append(tuple(B))
return ans
I thus add the generated line to the original plot:
tau = [time/100. for time in range(101)]
bezsim = bezcurv(n1,n2,(cx,cy),tau)
simx,simy = [b[0] for b in bezsim], [b[1] for b in bezsim]
The green line below is (should be?) the reconstructed bezier curve, though it's clearly not.
After much struggling, I convinced myself that to solve this I had to part away from the FancyArrowPatch suite and create something from scratch. Here is a working solution that, far from fulfilling any perfectionist spirit, satisfied me:
import matplotlib.pyplot as plt
import numpy as np
from numpy.random import seed, randint
# Build function that connects two points with a curved line,
# and an arrow on the middle of it
seed(1679)
narrow = 3
rad_one = 50
numpoints = 3
random_points = list(randint(1,20,[numpoints,4]))
rpoints = [[(a,b),(c,d)] for a,b,c,d in random_points]
def curvline(start,end,rad,t=100,arrows=1,push=0.8):
#Compute midpoint
rad = rad/100.
x1, y1 = start
x2, y2 = end
y12 = (y1 + y2) / 2
dy = (y2 - y1)
cy = y12 + (rad) * dy
#Prepare line
tau = np.linspace(0,1,t)
xsupport = np.linspace(x1,x2,t)
ysupport = [(1-i)**2 * y1 + 2*(1-i)*i*cy + (i**2)*y2 for i in tau]
#Create arrow data
arset = list(np.linspace(0,1,arrows+2))
c = zip([xsupport[int(t*a*push)] for a in arset[1:-1]],
[ysupport[int(t*a*push)] for a in arset[1:-1]])
dt = zip([xsupport[int(t*a*push)+1]-xsupport[int(t*a*push)] for a in arset[1:-1]],
[ysupport[int(t*a*push)+1]-ysupport[int(t*a*push)] for a in arset[1:-1]])
arrowpath = zip(c,dt)
return xsupport, ysupport, arrowpath
def plotcurv(start,end,rad,t=100,arrows=1,arwidth=.25):
x, y, c = curvline(start,end,rad,t,arrows)
plt.plot(x,y,'k-')
for d,dt in c:
plt.arrow(d[0],d[1],dt[0],dt[1], shape='full', lw=0,
length_includes_head=False, head_width=arwidth)
return c
#Create figure
figure = plt.figure()
ax = plt.subplot(111)
for n1,n2 in rpoints:
#First line
plotcurv(n1,n2,rad_one,200,narrow,0.5)
#Second line
plotcurv(n2,n1,rad_one,200,narrow,0.5)
ax.set_xlim(0,20)
ax.set_ylim(0,20)
plt.show
I have tested it with three random couple of points, plotting back and forth lines. Which gives the figure below:
The function allows for the user to set a number of desired arrow-heads, and it places them evenly on the plotted Bezier, making sure the appropriate direction is represented. However, because the Bezier curve is not exactly an 'arc', I heuristically push the start of the arrow-heads to make them look more centered. Any improvement to this solution will be greatly appreciated.
I have a bunch of cross plots with two sets of data and have been looking for a matploltib way of highlighting their plotted regions with smoothed polygon outlines.
At the moment i just use Adobe Illustrator and amend saved plot, but this is not ideal. Example:
I'd be grateful for any pointers/links to examples.
Cheers
Here, you have an example. I was written the main ideas, but obviously, you could do it better.
A short explanations:
1) You need to compute the convex-hull (http://en.wikipedia.org/wiki/Convex_hull)
2) With the hull, you could scale it to keep all your data inside.
3) You must to interpolate the resulting curve.
The first part was done in http://wiki.scipy.org/Cookbook/Finding_Convex_Hull. The second one is trivial. The third one is very general, and you could perform any method, there are a lot of different ways to do the same. I took the #Jaime's approach (Smooth spline representation of an arbitrary contour, f(length) --> x,y), which I think it's a very good method.
I hope it help you...
#Taken from http://wiki.scipy.org/Cookbook/Finding_Convex_Hull
import numpy as n, pylab as p, time
def _angle_to_point(point, centre):
'''calculate angle in 2-D between points and x axis'''
delta = point - centre
res = n.arctan(delta[1] / delta[0])
if delta[0] < 0:
res += n.pi
return res
def _draw_triangle(p1, p2, p3, **kwargs):
tmp = n.vstack((p1,p2,p3))
x,y = [x[0] for x in zip(tmp.transpose())]
p.fill(x,y, **kwargs)
def area_of_triangle(p1, p2, p3):
'''calculate area of any triangle given co-ordinates of the corners'''
return n.linalg.norm(n.cross((p2 - p1), (p3 - p1)))/2.
def convex_hull(points, graphic=False, smidgen=0.0075):
'''
Calculate subset of points that make a convex hull around points
Recursively eliminates points that lie inside two neighbouring points until only convex hull is remaining.
:Parameters:
points : ndarray (2 x m)
array of points for which to find hull
graphic : bool
use pylab to show progress?
smidgen : float
offset for graphic number labels - useful values depend on your data range
:Returns:
hull_points : ndarray (2 x n)
convex hull surrounding points
'''
if graphic:
p.clf()
p.plot(points[0], points[1], 'ro')
n_pts = points.shape[1]
assert(n_pts > 5)
centre = points.mean(1)
if graphic: p.plot((centre[0],),(centre[1],),'bo')
angles = n.apply_along_axis(_angle_to_point, 0, points, centre)
pts_ord = points[:,angles.argsort()]
if graphic:
for i in xrange(n_pts):
p.text(pts_ord[0,i] + smidgen, pts_ord[1,i] + smidgen, \
'%d' % i)
pts = [x[0] for x in zip(pts_ord.transpose())]
prev_pts = len(pts) + 1
k = 0
while prev_pts > n_pts:
prev_pts = n_pts
n_pts = len(pts)
if graphic: p.gca().patches = []
i = -2
while i < (n_pts - 2):
Aij = area_of_triangle(centre, pts[i], pts[(i + 1) % n_pts])
Ajk = area_of_triangle(centre, pts[(i + 1) % n_pts], \
pts[(i + 2) % n_pts])
Aik = area_of_triangle(centre, pts[i], pts[(i + 2) % n_pts])
if graphic:
_draw_triangle(centre, pts[i], pts[(i + 1) % n_pts], \
facecolor='blue', alpha = 0.2)
_draw_triangle(centre, pts[(i + 1) % n_pts], \
pts[(i + 2) % n_pts], \
facecolor='green', alpha = 0.2)
_draw_triangle(centre, pts[i], pts[(i + 2) % n_pts], \
facecolor='red', alpha = 0.2)
if Aij + Ajk < Aik:
if graphic: p.plot((pts[i + 1][0],),(pts[i + 1][1],),'go')
del pts[i+1]
i += 1
n_pts = len(pts)
k += 1
return n.asarray(pts)
if __name__ == "__main__":
import scipy.interpolate as interpolate
# fig = p.figure(figsize=(10,10))
theta = 2*n.pi*n.random.rand(1000)
r = n.random.rand(1000)**0.5
x,y = r*p.cos(theta),r*p.sin(theta)
points = n.ndarray((2,len(x)))
points[0,:],points[1,:] = x,y
scale = 1.03
hull_pts = scale*convex_hull(points)
p.plot(x,y,'ko')
x,y = [],[]
convex = scale*hull_pts
for point in convex:
x.append(point[0])
y.append(point[1])
x.append(convex[0][0])
y.append(convex[0][1])
x,y = n.array(x),n.array(y)
#Taken from https://stackoverflow.com/questions/14344099/numpy-scipy-smooth-spline-representation-of-an-arbitrary-contour-flength
nt = n.linspace(0, 1, 100)
t = n.zeros(x.shape)
t[1:] = n.sqrt((x[1:] - x[:-1])**2 + (y[1:] - y[:-1])**2)
t = n.cumsum(t)
t /= t[-1]
x2 = interpolate.spline(t, x, nt)
y2 = interpolate.spline(t, y, nt)
p.plot(x2, y2,'r--',linewidth=2)
p.show()
There are some useful papers, eg.:
http://repositorium.sdum.uminho.pt/bitstream/1822/6429/1/ConcaveHull_ACM_MYS.pdf
Also, you could try with: http://resources.arcgis.com/en/help/main/10.1/index.html#//007000000013000000
I don't know nothing about arcgis, but it looks fine.
I came across this and implemented easy to use functions as well as a couple of alternatives/improvements.
Improvements:
use a periodic interpolation which ensures smooth
use quadratic interpolation
now works for only positive points as well
using an alternative to the deprecated scipy.interpolate.spline function
Alternatives:
many different and configurable interpolation schemes
a rounded-corner convex hull version
Hope this helps someone along the way.
import sklearn.preprocessing
import sklearn.pipeline
import scipy.spatial
import numpy as np
def calculate_hull(
X,
scale=1.1,
padding="scale",
n_interpolate=100,
interpolation="quadratic_periodic",
return_hull_points=False):
"""
Calculates a "smooth" hull around given points in `X`.
The different settings have different drawbacks but the given defaults work reasonably well.
Parameters
----------
X : np.ndarray
2d-array with 2 columns and `n` rows
scale : float, optional
padding strength, by default 1.1
padding : str, optional
padding mode, by default "scale"
n_interpolate : int, optional
number of interpolation points, by default 100
interpolation : str or callable(ix,iy,x), optional
interpolation mode, by default "quadratic_periodic"
Inspired by: https://stackoverflow.com/a/17557853/991496
"""
if padding == "scale":
# scaling based padding
scaler = sklearn.pipeline.make_pipeline(
sklearn.preprocessing.StandardScaler(with_std=False),
sklearn.preprocessing.MinMaxScaler(feature_range=(-1,1)))
points_scaled = scaler.fit_transform(X) * scale
hull_scaled = scipy.spatial.ConvexHull(points_scaled, incremental=True)
hull_points_scaled = points_scaled[hull_scaled.vertices]
hull_points = scaler.inverse_transform(hull_points_scaled)
hull_points = np.concatenate([hull_points, hull_points[:1]])
elif padding == "extend" or isinstance(padding, (float, int)):
# extension based padding
# TODO: remove?
if padding == "extend":
add = (scale - 1) * np.max([
X[:,0].max() - X[:,0].min(),
X[:,1].max() - X[:,1].min()])
else:
add = padding
points_added = np.concatenate([
X + [0,add],
X - [0,add],
X + [add, 0],
X - [add, 0]])
hull = scipy.spatial.ConvexHull(points_added)
hull_points = points_added[hull.vertices]
hull_points = np.concatenate([hull_points, hull_points[:1]])
else:
raise ValueError(f"Unknown padding mode: {padding}")
# number of interpolated points
nt = np.linspace(0, 1, n_interpolate)
x, y = hull_points[:,0], hull_points[:,1]
# ensures the same spacing of points between all hull points
t = np.zeros(x.shape)
t[1:] = np.sqrt((x[1:] - x[:-1])**2 + (y[1:] - y[:-1])**2)
t = np.cumsum(t)
t /= t[-1]
# interpolation types
if interpolation is None or interpolation == "linear":
x2 = scipy.interpolate.interp1d(t, x, kind="linear")(nt)
y2 = scipy.interpolate.interp1d(t, y, kind="linear")(nt)
elif interpolation == "quadratic":
x2 = scipy.interpolate.interp1d(t, x, kind="quadratic")(nt)
y2 = scipy.interpolate.interp1d(t, y, kind="quadratic")(nt)
elif interpolation == "quadratic_periodic":
x2 = scipy.interpolate.splev(nt, scipy.interpolate.splrep(t, x, per=True, k=4))
y2 = scipy.interpolate.splev(nt, scipy.interpolate.splrep(t, y, per=True, k=4))
elif interpolation == "cubic":
x2 = scipy.interpolate.CubicSpline(t, x, bc_type="periodic")(nt)
y2 = scipy.interpolate.CubicSpline(t, y, bc_type="periodic")(nt)
else:
x2 = interpolation(t, x, nt)
y2 = interpolation(t, y, nt)
X_hull = np.concatenate([x2.reshape(-1,1), y2.reshape(-1,1)], axis=1)
if return_hull_points:
return X_hull, hull_points
else:
return X_hull
def draw_hull(
X,
scale=1.1,
padding="scale",
n_interpolate=100,
interpolation="quadratic_periodic",
plot_kwargs=None,
ax=None):
"""Uses `calculate_hull` to draw a hull around given points.
Parameters
----------
X : np.ndarray
2d-array with 2 columns and `n` rows
scale : float, optional
padding strength, by default 1.1
padding : str, optional
padding mode, by default "scale"
n_interpolate : int, optional
number of interpolation points, by default 100
interpolation : str or callable(ix,iy,x), optional
interpolation mode, by default "quadratic_periodic"
plot_kwargs : dict, optional
`matplotlib.pyplot.plot` kwargs, by default None
ax : `matplotlib.axes.Axes`, optional
[description], by default None
"""
if plot_kwargs is None:
plot_kwargs = {}
X_hull = calculate_hull(
X, scale=scale, padding=padding, n_interpolate=n_interpolate, interpolation=interpolation)
if ax is None:
ax= plt.gca()
plt.plot(X_hull[:,0], X_hull[:,1], **plot_kwargs)
def draw_rounded_hull(X, padding=0.1, line_kwargs=None, ax=None):
"""Plots a convex hull around points with rounded corners and a given padding.
Parameters
----------
X : np.array
2d array with two columns and n rows
padding : float, optional
padding between hull and points, by default 0.1
line_kwargs : dict, optional
line kwargs (used for `matplotlib.pyplot.plot` and `matplotlib.patches.Arc`), by default None
ax : matplotlib.axes.Axes, optional
axes to plat on, by default None
"""
default_line_kwargs = dict(
color="black",
linewidth=1
)
if line_kwargs is None:
line_kwargs = default_line_kwargs
else:
line_kwargs = {**default_line_kwargs, **line_kwargs}
if ax is None:
ax = plt.gca()
hull = scipy.spatial.ConvexHull(X)
hull_points = X[hull.vertices]
hull_points = np.concatenate([hull_points[[-1]], hull_points, hull_points[[0]]])
diameter = padding * 2
for i in range(1, hull_points.shape[0] - 1):
# line
# source: https://stackoverflow.com/a/1243676/991496
norm_next = np.flip(hull_points[i] - hull_points[i + 1]) * [-1, 1]
norm_next /= np.linalg.norm(norm_next)
norm_prev = np.flip(hull_points[i - 1] - hull_points[i]) * [-1, 1]
norm_prev /= np.linalg.norm(norm_prev)
# plot line
line = hull_points[i:i+2] + norm_next * diameter / 2
ax.plot(line[:,0], line[:,1], **line_kwargs)
# arc
angle_next = np.rad2deg(np.arccos(np.dot(norm_next, [1,0])))
if norm_next[1] < 0:
angle_next = 360 - angle_next
angle_prev = np.rad2deg(np.arccos(np.dot(norm_prev, [1,0])))
if norm_prev[1] < 0:
angle_prev = 360 - angle_prev
arc = patches.Arc(
hull_points[i],
diameter, diameter,
angle=0, fill=False, theta1=angle_prev, theta2=angle_next,
**line_kwargs)
ax.add_patch(arc)
if __name__ == '__main__':
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import patches
# np.random.seed(42)
X = np.random.random((20,2))
fig, ax = plt.subplots(1,1, figsize=(10,10))
ax.scatter(X[:,0], X[:,1])
draw_rounded_hull(X, padding=0.1)
draw_hull(X)
ax.set(xlim=[-1,2], ylim= [-1,2])
fig.savefig("_out/test.png")