【问题标题】:Create random shape/contour using matplotlib使用 matplotlib 创建随机形状/轮廓
【发布时间】:2018-06-07 01:32:07
【问题描述】:

我正在尝试使用 python 生成随机轮廓的图像,但找不到简单的方法。

这是我想要的示例:

最初我坚持使用 matplotlib 和 gaussian 函数,但我什至无法接近我展示的图像。

有简单的方法吗?

感谢您的帮助

【问题讨论】:

    标签: python matplotlib contour shapes bezier


    【解决方案1】:

    问题是问题中显示的随机形状并不是真正随机的。它们以某种方式被平滑、有序、看似随机的形状。虽然使用计算机创建真正随机的形状很容易,但使用笔和纸创建这些伪随机形状要容易得多。

    因此,一种选择是交互式地创建这样的形状。这显示在问题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。使用的点越多,形状的特征就越丰富;有在曲线中产生重叠或循环的风险。

    【讨论】:

    • 基于这个出色的答案,我写了一个小项目,允许用贝塞尔曲线做一些事情(生成随机形状,生成随机形状的数据集,网格形状,形状为孔的网格域, 等等)。阅读此页面的人可能对此感兴趣:github.com/jviquerat/bezier_shapes
    • 我刚刚发布了一个可能感兴趣的类似问题(希望不是重复的):stackoverflow.com/questions/63107373/…
    • 这是一个非常有用且详细的答案,代码写得很好,谢谢!
    【解决方案2】:

    matplotlib 路径

    使用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=8r=0.7 会产生如下形状:


    高斯滤波 matplotlib 路径

    也可以选择使用上面的代码为单个形状生成形状,然后使用 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')
    

    【讨论】:

    • fig = plt.figure() NameError: name 'plt' is not defined @OriolAbril
    • import matplotlib.pyplot as plt
    【解决方案3】:

    要回答您的问题,没有简单的方法。生成看起来和感觉自然的随机事物是一个比最初看起来要困难得多的问题——这就是为什么像柏林噪声这样的技术很重要的原因。

    任何传统的程序化方法(例如,不涉及神经网络)都可能最终成为一个复杂的多步骤过程,包括选择随机点、放置形状、绘制线条等,并进行微调直到看起来符合您的要求它。使用这种方法,从头开始获得任何能够可靠地生成具有动态和有机外观的形状的东西都将非常困难。

    如果您对结果比实现更感兴趣,您可能会尝试找到一个库,该库可以生成令人信服的平滑随机纹理并从中切割轮廓线。这是目前唯一想到的“简单”方法。这是一个柏林噪声示例。注意由灰度级形成的形状。

    【讨论】:

    • 你的例子有一些很好的形状。是否可以从此函数中提取单个形状?
    • @klaus 如果图像可以存储为数组,那么可以。我的答案的一部分实际上是从数组中检索轮廓坐标。
    • 你能举个图书馆的例子吗?
    猜你喜欢
    • 1970-01-01
    • 2014-11-28
    • 2014-11-20
    • 1970-01-01
    • 2021-07-20
    • 2014-10-16
    • 1970-01-01
    • 1970-01-01
    • 2016-03-21
    相关资源
    最近更新 更多