创建具有圆形区域边缘的 Voronoi 艺术
Create Voronoi art with rounded region edges
我正在尝试创建一些艺术“情节”,如下所示:
区域的颜色并不重要,我想要实现的是沿着 Voronoi 区域的边缘的可变“厚度”(特别是,它们看起来像一个更大的圆形斑点,它们在角,中点更薄)。
我已经尝试根据到每个质心的最小距离(每个都与一种颜色相关联)“手动绘制”每个像素:
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')
或者 scipy.spatial.Voronoi
,这也给了我顶点点,虽然我仍然看不出如何通过它们绘制一条具有所需可变粗细的线。
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()
编辑:
我在每个单独的区域上通过侵蚀 + 角平滑(通过评论中建议的中值滤波器)设法获得了一些令人满意的结果,然后将其绘制到黑色背景中。
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)
但是正如您所见,沿区域 boundaries/edges 的“拉伸”线并不完全存在。还有其他建议吗?
Could something like bezier polygon "approximations" help me with this?
尝试使用贝塞尔曲线:
#!/usr/bin/env python
# coding: utf-8
"""
"""
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()
这就是@JohanC 建议的样子。 IMO,它看起来比我尝试使用贝塞尔曲线要好得多。 但是,RoundedPolygon
class 似乎有一个小问题,因为有时角落会有小缺陷(例如下图中的蓝色和紫色之间)。
编辑:我修复了 RoundedPolygon class。
#!/usr/bin/env python
# coding: utf-8
"""
"""
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):
#
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()
我正在尝试创建一些艺术“情节”,如下所示:
区域的颜色并不重要,我想要实现的是沿着 Voronoi 区域的边缘的可变“厚度”(特别是,它们看起来像一个更大的圆形斑点,它们在角,中点更薄)。
我已经尝试根据到每个质心的最小距离(每个都与一种颜色相关联)“手动绘制”每个像素:
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')
或者 scipy.spatial.Voronoi
,这也给了我顶点点,虽然我仍然看不出如何通过它们绘制一条具有所需可变粗细的线。
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()
编辑:
我在每个单独的区域上通过侵蚀 + 角平滑(通过评论中建议的中值滤波器)设法获得了一些令人满意的结果,然后将其绘制到黑色背景中。
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)
Could something like bezier polygon "approximations" help me with this?
尝试使用贝塞尔曲线:
#!/usr/bin/env python
# coding: utf-8
"""
"""
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()
这就是@JohanC 建议的样子。 IMO,它看起来比我尝试使用贝塞尔曲线要好得多。 但是,RoundedPolygon
class 似乎有一个小问题,因为有时角落会有小缺陷(例如下图中的蓝色和紫色之间)。
编辑:我修复了 RoundedPolygon class。
#!/usr/bin/env python
# coding: utf-8
"""
"""
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):
#
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()