Nuestro grupo especializado pasados varios días de investigación y recopilar de información, dimos con la solución, nuestro deseo es que todo este artículo sea de gran utilidad en tu plan.
Solución:
El problema es que el tipo de formas aleatorias que se muestran en la pregunta no es verdaderamente aleatorio. De alguna manera son formas suavizadas, ordenadas y aparentemente aleatorias. Si bien crear formas verdaderamente aleatorias es fácil con la computadora, crear esas formas pseudoaleatorias es mucho más fácil con un lápiz y papel.
Por tanto, una opción es crear estas formas de forma interactiva. Esto se muestra en la pregunta Ajuste BSpline interactivo en Python.
Si desea crear formas aleatorias mediante programación, podemos adaptar la solución a Cómo conectar puntos teniendo en cuenta la posición y orientación de cada uno de ellos utilizando curvas de Bezier cúbicas.
La idea es crear un conjunto de puntos aleatorios a través de get_random_points
y llamar a una función get_bezier_curve
Con ésos. Esto crea un conjunto de curvas Bézier que se conectan suavemente entre sí en los puntos de entrada. También nos aseguramos de que sean cíclicos, es decir, que la transición entre el punto de inicio y el final también sea suave.
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)
Puede utilizar esas funciones, por ejemplo, como
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()
Podemos comprobar cómo influyen los parámetros en el resultado. Básicamente, hay 3 parámetros para usar aquí:
rad
, el radio alrededor de los puntos en los que se encuentran los puntos de control de la curva Bézier. Este número es relativo a la distancia entre puntos adyacentes y, por lo tanto, debe estar entre 0 y 1. Cuanto mayor sea el radio, más nítidas serán las características de la curva.edgy
, un parámetro para determinar la suavidad de la curva. Si es 0, el ángulo de la curva a través de cada punto será la media entre la dirección y los puntos adyacentes. Cuanto más grande sea, más se determinará el ángulo solo por un punto adyacente. Por tanto, la curva se vuelve “más afilada”.n
el número de puntos aleatorios que se utilizarán. Por supuesto, el número mínimo de puntos es 3. Cuantos más puntos utilice, más características se volverán las formas; a riesgo de crear superposiciones o bucles en la curva.
ruta de matplotlib
Una forma sencilla de lograr formas aleatorias y bastante suavizadas es utilizando el módulo matplotlib.path.
Usando una curva de Bézier cúbica, la mayoría de las líneas se suavizarán y el número de bordes afilados será uno de los parámetros a ajustar.
Los pasos serían los siguientes. Primero se definen los parámetros de la forma, estos son el número de bordes afilados n
y la perturbación máxima con respecto a la posición predeterminada en el círculo unitario r
. En este ejemplo, los puntos se mueven desde el círculo unitario con una corrección radial, que modifica el radio de 1 a un número aleatorio entre 1-r
,1+r
.
Es por eso que los vértices se definen como seno o coseno del ángulo correspondiente multiplicado por el factor del radio, para colocar los puntos en el círculo y luego modificar su radio para introducir la componente aleatoria. los stack
, .T
para transponer y [:,None]
son simplemente para convertir las matrices a la entrada aceptada por matplotlib.
A continuación, se muestra un ejemplo que utiliza este tipo de corrección radial:
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()
Que para n=8
y r=0.7
produce formas como estas:
Ruta matplotlib filtrada gaussiana
También existe la opción de generar la forma con el código anterior para una sola forma, y luego usar scipy para realizar un filtrado gaussiano de la imagen generada.
La idea principal detrás de realizar un filtro gaussiano y recuperar la forma suavizada es crear una forma rellena; guardar la imagen como una matriz 2d (cuyos valores estarán entre 0 y 1 ya que será una imagen en escala de grises); luego aplique el filtro gaussiano; y, finalmente, obtenga la forma suavizada como el contorno 0.5 de la matriz filtrada.
Por lo tanto, esta segunda versión se vería así:
# 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)
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 = 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')
Para responder a su pregunta, no existe una forma sencilla de hacerlo. Generar elementos aleatorios que se vean y se sientan naturales es un problema mucho más difícil de lo que podría parecer al principio; es por eso que cosas como el ruido perlin son técnicas de importancia.
Cualquier enfoque programático tradicional (que no involucre, digamos, redes neuronales) probablemente terminaría como un proceso complicado de varios pasos de elegir puntos aleatorios, colocar formas, dibujar líneas, etc., afinado hasta que se vea como lo desea. Obtener cualquier cosa que genere de manera confiable formas tan dinámicas y orgánicas como sus ejemplos desde cero será muy difícil con este tipo de enfoque.
Si está más interesado en el resultado que en la implementación, puede intentar encontrar una biblioteca que genere texturas aleatorias suaves de apariencia convincente y cortar las líneas de contorno de ellas. Ese es el único enfoque “fácil” que me viene a la mente en este momento. Aquí hay un ejemplo de ruido perlin. Tenga en cuenta las formas formadas a partir de niveles de gris.
Comentarios y puntuaciones del artículo
Si sostienes alguna desconfianza o capacidad de enriquecer nuestro crónica te insinuamos dejar una disquisición y con mucho placer lo observaremos.