I'm writing an experiment for cognitive psych. I need to generate scribble-like figures with arbitrary control over a few properties.
- the figure can never self-intersect
- it has to close
- it has to occupy most of the drawing surface/canvas
- I have to be able to export it as a series of points
Additionally, I need to control it's complexity to some degree.
My general approach so far has been to do this:
- divide the canvas into quadrants and generate intersection points along each axis. This guarantees that the figure occupies most of the area by ensuring it has to pass into every quadrant.
- I generate a random number of points in each quadrant.
- I randomly choose either a line or a curve and connect each dot in sequence
This works, but, it has zero control over self-intersections, and I already know the ways in which I might be able to control for this are foolish, brute-strength approaches. I've included the code but it's a bit lengthy for SO. Also I'm in no way especially attached to this general process, it's just what I was able to dream up on my own. Any advice/strategies/criticism on my logic are welcome.
import numpy as np
import aggdraw
from random import choice
from PIL import ImageDraw, Image
from PIL import ImagePath
# there are a few functions used here, like "angle_between" that I've not included for brevity; they're all just simple functions that do what they say, usually some trig
def generate_segment_positions(seg_count, x_offset, y_offset, avg_dist, dist_variance):
# this just ensures that the points I choose fall within the desired
# quadrant and are within certain distances of each other
min_pt_dist = avg_dist - dist_variance
max_pt_dist = avg_dist + dist_variance
q_pts_x = []
q_pts_y = []
while not len(q_pts_x) == seg_count:
x = from_range(x_offset, x_offset + 400)
try:
if not (x in range(q_pts_x[-1] - min_pt_dist, q_pts_x[-1] + min_pt_dist)):
q_pts_x.append(x)
except IndexError:
q_pts_x.append(x)
while not len(q_pts_y) == seg_count:
y = from_range(y_offset, y_offset + 400)
try:
if not (y in range(q_pts_y[-1] - min_pt_dist, q_pts_y[-1] + min_pt_dist)):
q_pts_y.append(y)
except IndexError:
q_pts_y.append(y)
q_points = []
for p in q_pts_x:
q_points.append((p, q_pts_y[q_pts_x.index(p)]))
return q_points
def generate_arc_controls(dest, origin, quadrant):
# this is.. my attempting to work out some control over whether or not
# the curves I create go all over hell and creation... it doesn't work
# but I'm hoping it offers some insight into what I'm trying to do
m = midpoint(dest, origin)
rotation = int(angle_between(dest, origin))
angle_1 = int(np.random.normal(90, 10)) + rotation
angle_2 = int(np.random.normal(90, 10)) + rotation
quad_offset = 0
if quadrant == 0:
quad_offset = 180
if quadrant == 1:
quad_offset = 90
if quadrant == 3:
quad_offset += 270
angle_1 += quad_offset
angle_2 += quad_offset
mp_len = int(line_segment_len(dest, m))
amplitude_1 = from_range(mp_len // 2, mp_len)
amplitude_2 = from_range(mp_len // 2, mp_len)
c1 = point_pos(dest[0], dest[1], amplitude_1, angle_1)
c2 = point_pos(origin[0], origin[1], amplitude_2, angle_2)
if any(i for i in c1 + c2) < 0:
return generate_arc_controls(dest, origin, quadrant)
d_dest_c1 = line_segment_len(dest, c1)
d_dest_c2 = line_segment_len(dest, c2)
return [c1, c2] if d_dest_c1 > d_dest_c2 else [c2, c1]
def generate_figure(self):
BOT_L = 0
TOP_L = 1
TOP_R = 2
initial_position = (400, from_range(450, 750))
segments = []
min_segments_per_q = 2
max_segments_per_q = 4
quadrant_intersects = [(random.choice(range(50, 350)), 400),
(400, random.choice(range(50, 350))),
(random.choice(range(450, 750), 400), initial_position)]
avg_dist = 150 # used to give some control
dist_variance = 50 # over the points I later join
for quad in range(0,4):
seg_count = from_range(min_segments_per_q, max_segments_per_q)
x_offset = 0 if quad in [BOT_L, TOP_L] else 400
y_offset = 0 if quad in [TOP_L, TOP_R] else 400
origin = None
for j in range(0, seg_count):
# generate start and end points for each segment in the quadrant
q_points = self.generate_segment_positions(seg_count, x_offset, y_offset, avg_dist, dist_variance)
# set origin to the destination of previous segment
o = initial_position if origin is None else origin
# assign destination point; quadrant intersect for last segment of each quadrant
d = q_points[j] if j < seg_count - 1 else quadrant_intersects[quad]
# choose a segment type
s = random.choice(['arc', 'line'])
if s == 'line':
segments.append(['line', [d, o]])
if s == 'arc':
c = self.generate_arc_controls(d, o, quad)
segments.append(['arc', [c[0], c[1], d]])
origin = d
surf = aggdraw.Draw("RGBA", (800, 800), (255,255,255,255))
p_str = "M{0} {1}".format(*initial_position)
for s in segments:
if s[0] == 'line':
p_str += " L{0} {1}".format(*s[1][0])
if s[0] == 'arc':
pts = s[1][0] + s[1][2]
p_str += " Q {0} {1} {2} {3}".format(*pts)
sym = aggdraw.Symbol(p_str)
surf.symbol((0,0), sym, aggdraw.Pen((255,0,0), 1, 255))
return surf