【发布时间】:2018-06-07 01:32:07
【问题描述】:
我正在尝试使用 python 生成随机轮廓的图像,但找不到简单的方法。
这是我想要的示例:
最初我坚持使用 matplotlib 和 gaussian 函数,但我什至无法接近我展示的图像。
有简单的方法吗?
感谢您的帮助
【问题讨论】:
标签: python matplotlib contour shapes bezier
我正在尝试使用 python 生成随机轮廓的图像,但找不到简单的方法。
这是我想要的示例:
最初我坚持使用 matplotlib 和 gaussian 函数,但我什至无法接近我展示的图像。
有简单的方法吗?
感谢您的帮助
【问题讨论】:
标签: python matplotlib contour shapes bezier
问题是问题中显示的随机形状并不是真正随机的。它们以某种方式被平滑、有序、看似随机的形状。虽然使用计算机创建真正随机的形状很容易,但使用笔和纸创建这些伪随机形状要容易得多。
因此,一种选择是交互式地创建这样的形状。这显示在问题Interactive BSpline fitting in Python 中。
如果您想以编程方式创建随机形状,我们可以使用三次贝塞尔曲线将解决方案调整为How to connect points taking into consideration position and orientation of each of them。
这个想法是通过get_random_points 创建一组随机点,并用这些点调用函数get_bezier_curve。这将创建一组贝塞尔曲线,这些曲线在输入点处彼此平滑连接。我们还确保它们是循环的,即起点和终点之间的过渡也是平滑的。
import numpy as np
from scipy.special import binom
import matplotlib.pyplot as plt
bernstein = lambda n, k, t: binom(n,k)* t**k * (1.-t)**(n-k)
def bezier(points, num=200):
N = len(points)
t = np.linspace(0, 1, num=num)
curve = np.zeros((num, 2))
for i in range(N):
curve += np.outer(bernstein(N - 1, i, t), points[i])
return curve
class Segment():
def __init__(self, p1, p2, angle1, angle2, **kw):
self.p1 = p1; self.p2 = p2
self.angle1 = angle1; self.angle2 = angle2
self.numpoints = kw.get("numpoints", 100)
r = kw.get("r", 0.3)
d = np.sqrt(np.sum((self.p2-self.p1)**2))
self.r = r*d
self.p = np.zeros((4,2))
self.p[0,:] = self.p1[:]
self.p[3,:] = self.p2[:]
self.calc_intermediate_points(self.r)
def calc_intermediate_points(self,r):
self.p[1,:] = self.p1 + np.array([self.r*np.cos(self.angle1),
self.r*np.sin(self.angle1)])
self.p[2,:] = self.p2 + np.array([self.r*np.cos(self.angle2+np.pi),
self.r*np.sin(self.angle2+np.pi)])
self.curve = bezier(self.p,self.numpoints)
def get_curve(points, **kw):
segments = []
for i in range(len(points)-1):
seg = Segment(points[i,:2], points[i+1,:2], points[i,2],points[i+1,2],**kw)
segments.append(seg)
curve = np.concatenate([s.curve for s in segments])
return segments, curve
def ccw_sort(p):
d = p-np.mean(p,axis=0)
s = np.arctan2(d[:,0], d[:,1])
return p[np.argsort(s),:]
def get_bezier_curve(a, rad=0.2, edgy=0):
""" given an array of points *a*, create a curve through
those points.
*rad* is a number between 0 and 1 to steer the distance of
control points.
*edgy* is a parameter which controls how "edgy" the curve is,
edgy=0 is smoothest."""
p = np.arctan(edgy)/np.pi+.5
a = ccw_sort(a)
a = np.append(a, np.atleast_2d(a[0,:]), axis=0)
d = np.diff(a, axis=0)
ang = np.arctan2(d[:,1],d[:,0])
f = lambda ang : (ang>=0)*ang + (ang<0)*(ang+2*np.pi)
ang = f(ang)
ang1 = ang
ang2 = np.roll(ang,1)
ang = p*ang1 + (1-p)*ang2 + (np.abs(ang2-ang1) > np.pi )*np.pi
ang = np.append(ang, [ang[0]])
a = np.append(a, np.atleast_2d(ang).T, axis=1)
s, c = get_curve(a, r=rad, method="var")
x,y = c.T
return x,y, a
def get_random_points(n=5, scale=0.8, mindst=None, rec=0):
""" create n random points in the unit square, which are *mindst*
apart, then scale them."""
mindst = mindst or .7/n
a = np.random.rand(n,2)
d = np.sqrt(np.sum(np.diff(ccw_sort(a), axis=0), axis=1)**2)
if np.all(d >= mindst) or rec>=200:
return a*scale
else:
return get_random_points(n=n, scale=scale, mindst=mindst, rec=rec+1)
您可以使用这些功能,例如作为
fig, ax = plt.subplots()
ax.set_aspect("equal")
rad = 0.2
edgy = 0.05
for c in np.array([[0,0], [0,1], [1,0], [1,1]]):
a = get_random_points(n=7, scale=1) + c
x,y, _ = get_bezier_curve(a,rad=rad, edgy=edgy)
plt.plot(x,y)
plt.show()
我们可能会检查参数如何影响结果。这里主要使用 3 个参数:
rad,贝塞尔曲线控制点所在点周围的半径。该数字与相邻点之间的距离有关,因此应介于 0 和 1 之间。半径越大,曲线的特征越清晰。edgy,决定曲线平滑度的参数。如果为 0,则曲线通过每个点的角度将是到相邻点的方向之间的平均值。它越大,角度将仅由一个相邻点确定。曲线因此变得“更加前卫”。n 要使用的随机点数。当然,最小点数是 3。使用的点越多,形状的特征就越丰富;有在曲线中产生重叠或循环的风险。【讨论】:
使用matplotlib.path 模块是实现随机且非常平滑的形状的简单方法。
使用三次贝塞尔曲线,大部分线条都会被平滑,锐边的数量将是调整的参数之一。
步骤如下。首先定义形状的参数,这些是锐边的数量n 和相对于单位圆中默认位置的最大扰动r。在本例中,点从单位圆中移动,并进行径向校正,将半径从 1 修改为1-r,1+r 之间的随机数。
这就是为什么顶点被定义为相应角度乘以半径因子的正弦或余弦,将点放置在圆中,然后修改它们的半径以引入随机分量。转置的stack、.T 和[:,None] 只是将数组转换为matplotlib 接受的输入。
下面有一个使用这种径向校正的例子:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.path import Path
import matplotlib.patches as patches
n = 8 # Number of possibly sharp edges
r = .7 # magnitude of the perturbation from the unit circle,
# should be between 0 and 1
N = n*3+1 # number of points in the Path
# There is the initial point and 3 points per cubic bezier curve. Thus, the curve will only pass though n points, which will be the sharp edges, the other 2 modify the shape of the bezier curve
angles = np.linspace(0,2*np.pi,N)
codes = np.full(N,Path.CURVE4)
codes[0] = Path.MOVETO
verts = np.stack((np.cos(angles),np.sin(angles))).T*(2*r*np.random.random(N)+1-r)[:,None]
verts[-1,:] = verts[0,:] # Using this instad of Path.CLOSEPOLY avoids an innecessary straight line
path = Path(verts, codes)
fig = plt.figure()
ax = fig.add_subplot(111)
patch = patches.PathPatch(path, facecolor='none', lw=2)
ax.add_patch(patch)
ax.set_xlim(np.min(verts)*1.1, np.max(verts)*1.1)
ax.set_ylim(np.min(verts)*1.1, np.max(verts)*1.1)
ax.axis('off') # removes the axis to leave only the shape
plt.show()
n=8 和 r=0.7 会产生如下形状:
也可以选择使用上面的代码为单个形状生成形状,然后使用 scipy 对生成的图像执行gaussian filtering。
执行高斯滤波器并检索平滑形状的主要思想是创建填充形状;将图像保存为二维数组(其值将在 0 和 1 之间,因为它将是灰度图像);然后应用高斯滤波器;最终得到平滑后的形状作为滤波数组的 0.5 等高线。
因此,第二个版本看起来像:
# additional imports
from skimage import color as skolor # see the docs at scikit-image.org/
from skimage import measure
from scipy.ndimage import gaussian_filter
sigma = 7 # smoothing parameter
# ...
path = Path(verts, codes)
fig = plt.figure()
ax = fig.add_axes([0,0,1,1]) # create the subplot filling the whole figure
patch = patches.PathPatch(path, facecolor='k', lw=2) # Fill the shape in black
# ...
ax.axis('off')
fig.canvas.draw()
##### Smoothing ####
# get the image as an array of values between 0 and 1
data = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8)
data = data.reshape(fig.canvas.get_width_height()[::-1] + (3,))
gray_image = skolor.rgb2gray(data)
# filter the image
smoothed_image = gaussian_filter(gray_image,sigma)
# Retrive smoothed shape as 0.5 contour
smooth_contour = measure.find_contours(smoothed_image[::-1,:], 0.5)[0]
# Note, the values of the contour will range from 0 to smoothed_image.shape[0]
# and likewise for the second dimension, if desired,
# they should be rescaled to go between 0,1 afterwards
# compare smoothed ans original shape
fig = plt.figure(figsize=(8,4))
ax1 = fig.add_subplot(1,2,1)
patch = patches.PathPatch(path, facecolor='none', lw=2)
ax1.add_patch(patch)
ax1.set_xlim(np.min(verts)*1.1, np.max(verts)*1.1)
ax1.set_ylim(np.min(verts)*1.1, np.max(verts)*1.1)
ax1.axis('off') # removes the axis to leave only the shape
ax2 = fig.add_subplot(1,2,2)
ax2.plot(smooth_contour[:, 1], smooth_contour[:, 0], linewidth=2, c='k')
ax2.axis('off')
【讨论】:
【讨论】: