various examples

This commit is contained in:
Michael Fogleman 2018-02-10 15:51:48 -05:00
parent a9563efe84
commit 639b965b84
6 changed files with 743 additions and 5 deletions

117
examples/life.py Normal file
View File

@ -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

View File

@ -5,16 +5,16 @@ import numpy as np
import os import os
import sys import sys
NUMBER = '20' NUMBER = '?'
TITLE = 'Ten Seconds of Bubble Bobble' TITLE = 'Five Seconds of Donkey Kong'
LABEL = '#%s' % NUMBER LABEL = '#%s' % NUMBER
COLUMNS = 6 COLUMNS = 6
SECONDS = 10 SECONDS = 5
FRAME_OFFSET = 0 FRAME_OFFSET = 0
MIN_CHANGES = 1 MIN_CHANGES = 1
UNIQUE = False UNIQUE = False
SIMPLIFY = 0 # 5 SIMPLIFY = 0
def simplify_sparkline(values, n): def simplify_sparkline(values, n):
if not n: if not n:
@ -125,7 +125,7 @@ def main():
path = [] path = []
for j, value in enumerate(row): for j, value in enumerate(row):
x = (j / len(row) + c * 1.1) x = (j / len(row) + c * 1.1)
y = 1-value + r * 1.5 y = 1 - value + r * 1.5
path.append((x, y)) path.append((x, y))
paths.append(path) paths.append(path)
d = axi.Drawing(paths) d = axi.Drawing(paths)

265
examples/osm.py Normal file
View File

@ -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()

122
examples/ribbon.py Normal file
View File

@ -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()

190
examples/rule.py Normal file
View File

@ -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()

44
examples/stipple.py Normal file
View File

@ -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()