Jonline
Jonline

Reputation: 1747

Writing algorithm to draw semi-random 'scribbles' in Python with aggdraw without self-intersections

I'm writing an experiment for cognitive psych. I need to generate scribble-like figures with arbitrary control over a few properties.

Additionally, I need to control it's complexity to some degree.

My general approach so far has been to do this:

  1. 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.
  2. I generate a random number of points in each quadrant.
  3. 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

Upvotes: 1

Views: 605

Answers (0)

Related Questions