diff --git a/examples/life.py b/examples/life.py new file mode 100644 index 0000000..81d616a --- /dev/null +++ b/examples/life.py @@ -0,0 +1,117 @@ +import axi +import math +import random + +RULE = '23/3' + +class Generation(object): + def __init__(self, grid=None): + self.grid = grid or set() + def randomize(self, w, h, p, seed=None): + random.seed(seed) + self.grid.clear() + for y in range(-h, h+1): + for x in range(-w, w+1): + d = math.hypot(x, y) + p = 1 - d / 24 + p = p * 0.8 + p = max(0, p) + p = p ** 1.5 + if random.random() < p: + self.set(x, y) + def set(self, x, y): + self.grid.add((x, y)) + def unset(self, x, y): + self.grid.discard((x, y)) + def get(self, x, y): + return (x, y) in self.grid + def count_neighbors(self, x, y): + count = 0 + for dy in [-1, 0, 1]: + for dx in [-1, 0, 1]: + if (dx or dy) and (x + dx, y + dy) in self.grid: + count += 1 + return count + def next(self): + xs = [x for x, y in self.grid] + ys = [y for x, y in self.grid] + minx = min(xs) - 1 + maxx = max(xs) + 1 + miny = min(ys) - 1 + maxy = max(ys) + 1 + grid = set() + keep, spawn = RULE.split('/') + keep = map(int, keep) + spawn = map(int, spawn) + for y in range(miny, maxy + 1): + for x in range(minx, maxx + 1): + n = self.count_neighbors(x, y) + if (x, y) in self.grid: + if n in keep: + grid.add((x, y)) + else: + if n in spawn: + grid.add((x, y)) + return Generation(grid) + +def circle(cx, cy, r, n): + points = [] + a0 = random.random() * 2 * math.pi + for i in range(n + 1): + a = 4.5 * math.pi * i / n + a0 + x = cx + math.cos(a) * r + y = cy + math.sin(a) * r + points.append((x, y)) + return points + +def circles(generations): + paths = [] + n = len(generations) + for i, g in enumerate(generations): + p = float(i) / (n - 1) + # print i, p, len(g.grid) + # r = 1 - p * 0.85 - 0.1 + r = p * 0.85 + 0.05 + r = r * 0.5 + for x, y in g.grid: + if x < 36 or y < 40 or x >= 64 or y >= 60: + continue + paths.append(circle(x, y, r, 200)) + return axi.Drawing(paths) + +def lines(generations): + paths = [] + n = len(generations) + for i, g in enumerate(generations): + p = float(i) / n + a = p * 2 * math.pi + for x, y in g.grid: + x2 = x + math.cos(a) * 0.45 + y2 = y + math.sin(a) * 0.45 + dx1 = random.gauss(0, 0.015) + dy1 = random.gauss(0, 0.015) + dx2 = random.gauss(0, 0.09) + dy2 = random.gauss(0, 0.09) + paths.append([(x + dx1, y + dy1), (x2 + dx2, y2 + dy2)]) + return axi.Drawing(paths) + +def main(seed): + n = 90/2 + gs = [] + g = Generation() + g.randomize(24, 24, 0.3, seed) + for i in range(n + 10): + gs.append(g) + g = g.next() + d = lines(gs[-n:]) + d = d.rotate_and_scale_to_fit(12, 8.5, step=90) + d = d.sort_paths().join_paths(0.02) + im = d.render() + im.write_to_png('%06d.png' % seed) + axi.draw(d) + +if __name__ == '__main__': + main(16) + # for i in range(100): + # main(i) + # print i diff --git a/examples/nes.py b/examples/nes.py index c7620d3..1c27a7b 100644 --- a/examples/nes.py +++ b/examples/nes.py @@ -5,16 +5,16 @@ import numpy as np import os import sys -NUMBER = '20' -TITLE = 'Ten Seconds of Bubble Bobble' +NUMBER = '?' +TITLE = 'Five Seconds of Donkey Kong' LABEL = '#%s' % NUMBER COLUMNS = 6 -SECONDS = 10 +SECONDS = 5 FRAME_OFFSET = 0 MIN_CHANGES = 1 UNIQUE = False -SIMPLIFY = 0 # 5 +SIMPLIFY = 0 def simplify_sparkline(values, n): if not n: @@ -125,7 +125,7 @@ def main(): path = [] for j, value in enumerate(row): x = (j / len(row) + c * 1.1) - y = 1-value + r * 1.5 + y = 1 - value + r * 1.5 path.append((x, y)) paths.append(path) d = axi.Drawing(paths) diff --git a/examples/osm.py b/examples/osm.py new file mode 100644 index 0000000..c52c9bc --- /dev/null +++ b/examples/osm.py @@ -0,0 +1,265 @@ +from shapely import geometry, ops, affinity + +import axi +import math +import osm2shapely +import sys + +import logging +logging.basicConfig() + +HOME = 35.768616, -78.844005 +RALEIGH = 35.777486, -78.635794 +DURHAM = 35.999392, -78.919217 +PARIS = 48.856744, 2.351248 +HOLBROOK = 32.422131, -80.713289 +FORMLABS = 42.374414, -71.087908 +PINE_RIDGE_ROAD = 42.427164, -71.143553 +TIMES_SQUARE = 40.758582, -73.985066 + +LAT, LNG = TIMES_SQUARE +LANDSCAPE = False +PAGE_WIDTH_IN = 12 +PAGE_HEIGHT_IN = 8.5 +MAP_WIDTH_KM = 1.61 +LANE_WIDTH_M = 3.7 +EARTH_RADIUS_KM = 6371 + +WEIGHTS = { + 'motorway': 2, + 'motorway_link': 2, + 'trunk_link': 2, + 'trunk': 2, + 'primary_link': 1.75, + 'primary': 1.75, + 'secondary': 1.5, + 'secondary_link': 1.5, + 'tertiary_link': 1.25, + 'tertiary': 1.25, + 'living_street': 1, + 'unclassified': 1, + 'residential': 1, + # 'service': 0, + # 'railway': 0, + # 'pedestrian': 0, + # 'footway': 0, + # 'natural': 0, +} + +def paths_to_shapely(paths): + return geometry.MultiLineString(paths) + +def shapely_to_paths(g): + if isinstance(g, geometry.Point): + return [] + elif isinstance(g, geometry.LineString): + return [list(g.coords)] + elif isinstance(g, (geometry.MultiPoint, geometry.MultiLineString, geometry.MultiPolygon, geometry.collection.GeometryCollection)): + paths = [] + for x in g: + paths.extend(shapely_to_paths(x)) + return paths + elif isinstance(g, geometry.Polygon): + paths = [] + paths.append(list(g.exterior.coords)) + for interior in g.interiors: + paths.extend(shapely_to_paths(interior)) + return paths + else: + raise Exception('unhandled shapely geometry: %s' % type(g)) + +def follow(g, step): + result = [] + d = step + e = 1e-3 + l = g.length + while d < l: + p = g.interpolate(d) + a = g.interpolate(d - e) + b = g.interpolate(d + e) + angle = math.atan2(b.y - a.y, b.x - a.x) + result.append((p.x, p.y, angle)) + d += step + return result + +def hatch(g, angle, step): + print g.area + x0, y0, x1, y1 = g.bounds + d = max(x1 - x0, y1 - y0) * 2 + lines = [] + x = 0 + while x < d: + lines.append(geometry.LineString([(x - d / 2, -d / 2), (x - d / 2, d / 2)])) + x += step + if not lines: + return None + lines = geometry.collection.GeometryCollection(lines) + lines = affinity.rotate(lines, angle) + lines = affinity.translate(lines, (x0 + x1) / 2, (y0 + y1) / 2) + return g.intersection(lines) + +def crop(g, w, h): + return g.intersection(geometry.Polygon(box(w, h))) + +def box(w, h): + w *= 0.5 + h *= 0.5 + return [(-w, -h), (w, -h), (w, h), (-w, h), (-w, -h)] + +def haversine(lat1, lng1, lat2, lng2): + lng1, lat1, lng2, lat2 = map(math.radians, [lng1, lat1, lng2, lat2]) + dlng = lng2 - lng1 + dlat = lat2 - lat1 + a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlng/2)**2 + return math.asin(math.sqrt(a)) * 2 * EARTH_RADIUS_KM + +class LambertAzimuthalEqualAreaProjection(object): + def __init__(self, lat, lng): + self.lat = lat + self.lng = lng + self.angle = math.radians(29) + self.scale = 1 + self.scale = self.kilometer_scale() + def project(self, lng, lat): + lng, lat = math.radians(lng), math.radians(lat) + clng, clat = math.radians(self.lng), math.radians(self.lat) + k = math.sqrt(2 / (1 + math.sin(clat)*math.sin(lat) + math.cos(clat)*math.cos(lat)*math.cos(lng-clng))) + x = k * math.cos(lat) * math.sin(lng-clng) + y = k * (math.cos(clat)*math.sin(lat) - math.sin(clat)*math.cos(lat)*math.cos(lng-clng)) + rx = x * math.cos(self.angle) - y * math.sin(self.angle) + ry = y * math.cos(self.angle) + x * math.sin(self.angle) + x = rx + y = ry + s = self.scale + return (x * s, -y * s) + def kilometer_scale(self): + e = 1e-3 + lat, lng = self.lat, self.lng + km_per_lat = haversine(lat - e, lng, lat + e, lng) / (2 * e) + km_per_lng = haversine(lat, lng - e, lat, lng + e) / (2 * e) + x1, y1 = self.project(lng - 1 / km_per_lng, lat - 1 / km_per_lat) + x2, y2 = self.project(lng + 1 / km_per_lng, lat + 1 / km_per_lat) + sx = 2 / (x2 - x1) + sy = 2 / (y1 - y2) + return (sx + sy) / 2 + def transform(self, g): + result = ops.transform(self.project, g) + result.tags = g.tags + return result + +def circle(cx, cy, r, n, revs=5): + points = [] + n *= revs + for i in range(n + 1): + a = revs * math.pi * i / n + x = cx + math.cos(a) * r + y = cy + math.sin(a) * r + points.append((x, y)) + return points + +def main(): + # parse osm file into shapely geometries + geometries = osm2shapely.parse(sys.argv[1]) + + # setup map projection + projection = LambertAzimuthalEqualAreaProjection(LAT, LNG) + geometries = [projection.transform(g) for g in geometries] + + # determine width and height of map + w = MAP_WIDTH_KM + if LANDSCAPE: + h = float(w) * PAGE_HEIGHT_IN / PAGE_WIDTH_IN + else: + h = float(w) * PAGE_WIDTH_IN / PAGE_HEIGHT_IN + + # process geometries + gs = [] + roads = [] + for g in geometries: + highway = g.tags.get('highway') + if highway and highway in WEIGHTS: + weight = WEIGHTS[highway] + if weight: + g = g.buffer(LANE_WIDTH_M / 1000.0 * weight) + g = crop(g, w * 1.1, h * 1.1) + roads.append(g) + # elif 'natural' in g.tags: + # gs.append(g) + elif 'building' in g.tags: + gs.append(g) + + print 'crop' + gs = [crop(g, w * 1.1, h * 1.1) for g in gs] + roads = [crop(g, w * 1.1, h * 1.1) for g in roads] + + # gs = [] + # for key, ways in handler.ways.items(): + # if key not in WEIGHTS: + # print 'skip', key, len(ways) + # continue + # print 'layer', key, len(ways) + # ggs = [] + # for way in ways: + # coords = [projection.project(*handler.coords[x]) for x in way] + # if key == 'natural' and coords[0] == coords[-1]: + # ggs.append(geometry.Polygon(coords)) + # else: + # ggs.append(geometry.LineString(coords)) + # # g = paths_to_shapely(paths) + # g = geometry.collection.GeometryCollection(ggs) + # if key == 'railway': + # paths = shapely_to_paths(g) + # g = paths_to_shapely(paths) + # points = follow(g, 20 / 1000.0) + # s = 4 / 1000.0 + # for x, y, a in points: + # x1 = x + math.cos(a + math.pi / 2) * s + # y1 = y + math.sin(a + math.pi / 2) * s + # x2 = x + math.cos(a - math.pi / 2) * s + # y2 = y + math.sin(a - math.pi / 2) * s + # paths.append([(x1, y1), (x2, y2)]) + # g = paths_to_shapely(paths) + # if key == 'natural': + # gs.append(crop(paths_to_shapely(shapely_to_paths(g)), w * 1.1, h * 1.1)) + # paths = hatch(g, 45, 10 / 1000.0) + # g = paths_to_shapely(paths) + # weight = WEIGHTS[key] + # if weight: + # g = g.buffer(LANE_WIDTH_M / 1000.0 * weight) + # g = crop(g, w * 1.1, h * 1.1) + # gs.append(g) + + print 'union' + roads = ops.cascaded_union(roads) + all_roads = [] + while not roads.is_empty: + all_roads.append(roads) + roads = roads.buffer(-LANE_WIDTH_M / 1000.0 / 3) + g = geometry.collection.GeometryCollection(gs + all_roads) + g = paths_to_shapely(shapely_to_paths(g)) + + print 'crop' + g = crop(g, w, h) + + paths = shapely_to_paths(g) + + # dot at origin + # s = 3 / 1000.0 / 3 + # for i in range(4): + # paths.append(circle(0, 0, i * s, 360)) + + # border around map + s = 6 / 1000.0 / 2 + for m in range(1): + paths.append(box(w - s * m, h - s * m)) + + print 'axi' + d = axi.Drawing(paths) + d = d.rotate_and_scale_to_fit(PAGE_WIDTH_IN, PAGE_HEIGHT_IN, step=90) + d = d.sort_paths().join_paths(0.002).simplify_paths(0.002) + im = d.render() + im.write_to_png('out.png') + axi.draw(d) + +if __name__ == '__main__': + main() diff --git a/examples/ribbon.py b/examples/ribbon.py new file mode 100644 index 0000000..25789e2 --- /dev/null +++ b/examples/ribbon.py @@ -0,0 +1,122 @@ +import axi +import sys +import textwrap + +NUMBER = 'XX' +LABEL = '#%s' % NUMBER + +TITLE = textwrap.wrap( + "X-ray structure of human beta3beta3 alcohol dehydrogenase" +, 40) + +ABSTRACT = textwrap.wrap( + "The three-dimensional structure of the human beta3beta3 dimeric alcohol dehydrogenase (beta3) was determined to 2.4-A resolution. beta3 was crystallized as a ternary complex with the coenzyme NAD+ and the competitive inhibitor 4-iodopyrazole. beta3 is a polymorphic variant at ADH2 that differs from beta1 by a single amino acid substitution of Arg-369 --> Cys. The available x-ray structures of mammalian alcohol dehydrogenases show that the side chain of Arg-369 forms an ion pair with the NAD(H) pyrophosphate to stabilize the E.NAD(H) complex. The Cys-369 side chain of beta3 cannot form this interaction. The three-dimensional structures of beta3 and beta1 are virtually identical, with the exception that Cys-369 and two water molecules in beta3 occupy the position of Arg-369 in beta1. The two waters occupy the same positions as two guanidino nitrogens of Arg-369. Hence, the number of hydrogen bonding interactions between the enzyme and NAD(H) are the same for both isoenzymes. However, beta3 differs from beta1 by the loss of the electrostatic interaction between the NAD(H) pyrophosphate and the Arg-369 side chain. The equilibrium dissociation constants of beta3 for NAD+ and NADH are 350-fold and 4000-fold higher, respectively, than those for beta1. These changes correspond to binding free energy differences of 3.5 kcal/mol for NAD+ and 4.9 kcal/mol for NADH. Thus, the Arg-369 --> Cys substitution of beta3 isoenzyme destabilizes the interaction between coenzyme and beta3 alcohol dehydrogenase." +, 120) + +def concat(ds): + result = axi.Drawing() + for d in ds: + result.add(d) + return result + +def stack_drawings(ds, spacing=0): + result = axi.Drawing() + y = 0 + for d in ds: + d = d.origin().translate(-d.width / 2, y) + result.add(d) + y += d.height + spacing + return result + +def grid_drawings(ds, columns, spacing=0): + result = axi.Drawing() + w = max(d.width for d in ds) + spacing + h = max(d.height for d in ds) + spacing + for i, d in enumerate(ds): + r = i / columns + c = i % columns + x = c * w + (w - d.width) / 2 + y = r * h + (h - d.height) / 2 + d = d.origin().translate(x, y) + result.add(d) + return result + +def title(): + ds = [axi.Drawing(axi.text(line, axi.TIMESIB)) for line in TITLE] + spacing = max(d.height for d in ds) * 1.5 + ds = [d.translate(-d.width / 2, i * spacing) for i, d in enumerate(ds)] + d = concat(ds) + d = d.scale_to_fit_width(8.5) + d = d.join_paths(0.01) + return d + +def abstract(): + ds = [axi.Drawing(p) for p in axi.justify_text(ABSTRACT, axi.TIMESR)] + spacing = max(d.height for d in ds) * 1.5 + ds = [d.translate(0, i * spacing) for i, d in enumerate(ds)] + d = concat(ds) + d = d.scale_to_fit_width(8.5) + d = d.join_paths(0.01) + return d + +def label(): + d = axi.Drawing(axi.text(LABEL, axi.FUTURAL)) + d = d.scale_to_fit_height(0.125) + d = d.rotate(-90) + d = d.move(12, 8.5, 1, 1) + d = d.join_paths(0.01) + return d + +def main(): + # text = stack_drawings([title(), abstract()], 0.25) + text = title() + + filenames = [ + sys.argv[1], + # 'ribbon/1j1c.txt', + # 'ribbon/amyloid-beta/1mwp.txt', + # 'ribbon/amyloid-beta/1owt.txt', + # 'ribbon/amyloid-beta/1rw6.txt', + # 'ribbon/amyloid-beta/1iyt.txt', + ] + angles = [90, 90, 75, 60] + print 'loading paths' + ds = [] + for filename, angle in zip(filenames, angles): + ds.append(axi.Drawing(axi.load_paths(filename)).scale_to_fit(8.5, 12).scale(1, -1)) + d = grid_drawings(ds, 2, 1) + print len(d.paths) + print 'joining paths' + d = d.join_paths(0.01) + print len(d.paths) + print 'transforming paths' + # d = d.scale(1, -1) + d = d.rotate_and_scale_to_fit(8.5, 12 - text.height - 0.75, step=5) + d = d.origin() + print 'sorting paths' + d = d.sort_paths() + print 'joining paths' + d = d.join_paths(0.01) + print len(d.paths) + print 'simplifying paths' + d = d.simplify_paths(0.001) + + # add title and label and fit to page + d = stack_drawings([d, text], 0.75) + d = d.rotate(-90) + # d = d.center(12, 8.5) + d = d.scale_to_fit(12, 8.5) + # d.add(title()) + # d.add(label()) + + print 'rendering paths' + d.render(line_width=0.25/25.4).write_to_png('out.png') + # axi.draw(d) + + print d.bounds + + d.dump('out.axi') + d.dump_svg('out.svg') + +if __name__ == '__main__': + main() diff --git a/examples/rule.py b/examples/rule.py new file mode 100644 index 0000000..7e37ba7 --- /dev/null +++ b/examples/rule.py @@ -0,0 +1,190 @@ +from collections import defaultdict +from shapely import geometry + +import axi +import math +import random + +def fill_circle(cx, cy, r1, r2, revs, points_per_rev): + points = [] + a0 = random.random() * 2 * math.pi + n = revs * points_per_rev + for i in range(n + 1): + a = a0 + revs * 2 * math.pi * i / n + r = r1 + (r2 - r1) * min(1, float(i) / (n - points_per_rev)) + x = cx + math.cos(a) * r + y = cy + math.sin(a) * r + points.append((x, y)) + return points + +def random_row(n): + return bin(random.getrandbits(n) | (1 << n))[3:] + +def compute_row(rule, previous): + row = [] + previous = '00' + previous + '00' + for i in range(len(previous) - 2): + x = int(previous[i:i+3], 2) + y = '1' if rule & (1 << x) else '0' + row.append(y) + return ''.join(row) + +def compute_rows(rule, n): + # rows = ['1'] + rows = [random_row(256)] + for _ in range(n - 1): + rows.append(compute_row(rule, rows[-1])) + return rows + +def pad(rows): + result = [] + n = len(max(rows, key=len)) + for row in rows: + p = (n - len(row)) / 2 + 1 + row = '.' * p + row + '.' * p + result.append(row) + return result + +def trim(rows): + return [row.strip('.') for row in rows] + +def crop(rows): + w = len(rows[0]) + h = len(rows) + n = int(h * 0.165) + i = w / 2 - n / 2 + j = i + n + return [row[i:j] for row in rows]#[-int(n*1.375*3):]] + +def crop_diagonal(rows): + rows = trim(rows) + result = [] + for i, row in enumerate(rows): + if i < len(rows) / 2: + result.append(row) + else: + j = 2 * (i - len(rows) / 2 + 1) + result.append(row[j:-j]) + return result + +def trim_pair(pair, d): + line = geometry.LineString(pair) + p1 = line.interpolate(d) + p2 = line.interpolate(line.length - d) + return ((p1.x, p1.y), (p2.x, p2.y)) + +def form_pairs(rows): + pairs = [] + rows = pad(rows) + for y, row in enumerate(rows): + if y == 0: + continue + for x, value in enumerate(row): + if value != '1': + continue + i = x - len(rows[-1]) / 2 + j = y + if rows[y - 1][x - 1] == '1': + pairs.append(((i - 1, j - 1), (i, j))) + if rows[y - 1][x] == '1': + pairs.append(((i, j - 1), (i, j))) + if rows[y - 1][x + 1] == '1': + pairs.append(((i + 1, j - 1), (i, j))) + points = set() + for (x1, y1), (x2, y2) in pairs: + points.add((x1, y1)) + points.add((x2, y2)) + return pairs, points + +def create_drawing(rule, h): + # print rule + rows = compute_rows(rule, h) + rows = pad(rows) + # rows = crop_diagonal(rows) + rows = crop(rows) + rows = pad(rows) + pairs, points = form_pairs(rows) + counts = defaultdict(int) + for a, b in pairs: + counts[a] += 1 + counts[b] += 1 + # paths = [trim_pair(x, 0.25) for x in pairs] + paths = pairs + circle = axi.Drawing([fill_circle(0, 0, 0, 0.3, 3, 100)]) + # paths = [] + # paths = random.sample(pairs, len(pairs) / 2) + for x, y in points: + if counts[(x, y)] != 1: + continue + paths.extend(circle.translate(x, y).paths) + continue + r = 0.3 + # r = random.gauss(0.125, 0.075) + # if r < 0: + # continue + revs = int(math.ceil(r / 0.125)) + 1 + paths.append(fill_circle(x, y, 0, r, revs, 100)) + d = axi.Drawing(paths) + return d + +def stack_drawings(ds, spacing=0): + result = axi.Drawing() + x = 0 + for d in ds: + d = d.origin().translate(x, -d.height / 2) + result.add(d) + x += d.width + spacing + return result + +def title(text): + d = axi.Drawing(axi.text(text, axi.FUTURAM)) + d = d.scale_to_fit_height(0.2) + d = d.move(6, 8.5, 0.5, 1) + d = d.join_paths(0.01) + d = d.rotate(-90) + return d + +def main(): + rule = 150 + seed = 37 + random.seed(seed) + h = 128 + + rules = [30, 60, 90, 106, 150, 105, 122, 154] + ds = [] + bs = [] + sizer = axi.HorizontalSizer() + for rule in rules: + d = create_drawing(rule, h) + ds.append(d) + b = axi.Box(d.width, d.height) + bs.append(b) + sizer.add(b) + sizer.add_spacer(2) + sizer.fit() + drawing = axi.Drawing() + for d, b in zip(ds, bs): + x, y, w, h = b.dimensions + d = d.move(x + w / 2, y + h / 2, 0.5, 0.5) + drawing.add(d) + d = drawing + + # d = create_drawing(rule, h) + # d = d.rotate(-90) + d = d.rotate_and_scale_to_fit(12, 8.5, step=90) + # d = stack_drawings([d, title('Rule %d' % rule)], 0.2) + # d = d.scale_to_fit(12, 8.5) + print 'sorting paths' + d = d.sort_paths() + print 'joining paths' + d = d.join_paths(0.01) + print 'simplifying paths' + d = d.simplify_paths(0.001) + print d.bounds + d.dump('out%04d.axi' % seed) + im = d.render(scale=109 * 1, line_width=0.3/25.4) + im.write_to_png('out%04d.png' % seed) + # axi.draw(d) + +if __name__ == '__main__': + main() diff --git a/examples/stipple.py b/examples/stipple.py new file mode 100644 index 0000000..863cdb1 --- /dev/null +++ b/examples/stipple.py @@ -0,0 +1,44 @@ +from PIL import Image + +import axi +import random +import sys + +def main(): + filename = sys.argv[1] + print 'loading image' + im = Image.open(filename) + im = im.convert('L') + w, h = im.size + data = list(im.getdata()) + paths = [] + for y in xrange(h): + for x in xrange(w): + if data[y*w+x] == 0: + paths.append([(x, y), (x, y)]) + random.shuffle(paths) + print len(paths) + d = axi.Drawing(paths) + print 'transforming paths' + # d = d.scale(1, -1) + d = d.rotate_and_scale_to_fit(12, 8.5, step=90) + # print 'sorting paths' + # d = d.sort_paths() + # print 'joining paths' + # d = d.join_paths(0.05) + # print len(d.paths) + print 'sorting paths' + d = d.sort_paths() + print 'joining paths' + d = d.join_paths(0.03) + print len(d.paths) + d.paths = [x for x in d.paths if len(x) > 2] + print 'simplifying paths' + d = d.simplify_paths(0.002) + print len(d.paths) + print 'rendering paths' + d.render(line_width=0.3/25.4).write_to_png('out.png') + axi.draw(d) + +if __name__ == '__main__': + main()