Spaces:
Sleeping
Sleeping
File size: 6,377 Bytes
c3f66ee |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
#!/bin/python
# -*- coding: utf-8 -*-
"""stringart.py - A program to calculate and visualize string art
Some more information will follow
"""
import math
import copy
import numpy as np
from PIL import Image, ImageOps, ImageFilter, ImageEnhance
class StringArtGenerator:
def __init__(self):
self.iterations = 1000
self.shape = 'circle'
self.image = None
self.data = None
self.residual = None
self.seed = 0
self.nails = 100
self.weight = 20
self.nodes = []
self.paths = []
def set_seed(self, seed):
self.seed = seed
def set_weight(self, weight):
self.weight = weight
def set_shape(self, shape):
self.shape = shape
def set_nails(self, nails):
self.nails = nails
if self.shape == 'circle':
self.set_nodes_circle()
elif self.shape == 'rectangle':
self.set_nodes_rectangle()
def set_iterations(self, iterations):
self.iterations = iterations
def set_nodes_rectangle(self):
"""Set's nails evenly (equidistance) along a rectangle of given dimensions"""
perimeter = self.get_perimeter()
spacing = perimeter/self.nails
width, height = np.shape(self.data)
pnails = [ t*spacing for t in range(self.nails) ]
xarr = []; yarr = []
for p in pnails:
if (p < width): # top edge
x = p; y = 0;
elif (p < width + height): # right edge
x = width; y = p - width;
elif (p < 2*width + height): # bottom edge}
x = width - (p - width - height); # this can obviously be simplified.
y = height;
else: # left edge
x = 0; y = height - (p - 2*width - height);
xarr.append(x)
yarr.append(y)
self.nodes = list(zip(xarr, yarr))
def get_perimeter(self):
return 2.0*np.sum(np.shape(self.data))
def set_nodes_circle(self):
"""Set's nails evenly along a circle of given diameter"""
spacing = (2*math.pi)/self.nails
steps = range(self.nails)
radius = self.get_radius()
x = [radius + radius*math.cos(t*spacing) for t in steps]
y = [radius + radius*math.sin(t*spacing) for t in steps]
self.nodes = list(zip(x, y))
def get_radius(self):
return 0.5*np.amax(np.shape(self.data))
def load_image(self, path):
img = Image.open(path)
self.image = img
np_img = np.array(self.image)
self.data = np.flipud(np_img).transpose()
def preprocess(self):
# Convert image to grayscale
self.image = ImageOps.grayscale(self.image)
self.image = ImageOps.invert(self.image)
self.image = self.image.filter(ImageFilter.EDGE_ENHANCE_MORE)
self.image = ImageEnhance.Contrast(self.image).enhance(1)
np_img = np.array(self.image)
self.data = np.flipud(np_img).transpose()
def generate(self):
self.calculate_paths()
delta = 0.0
pattern = []
nail = self.seed
datacopy = copy.deepcopy(self.data)
for i in range(self.iterations):
# calculate straight line to all other nodes and calculate
# 'darkness' from start node
# choose max darkness path
darkest_nail, darkest_path = self.choose_darkest_path(nail)
# add chosen node to pattern
pattern.append(self.nodes[darkest_nail])
# substract chosen path from image
self.data = self.data - self.weight*darkest_path
self.data[self.data < 0.0] = 0.0
if (np.sum(self.data) <= 0.0):
print("Stopping iterations. No more data or residual unchanged.")
break
# store current residual as delta for next iteration
delta = np.sum(self.data)
# continue from destination node as new start
nail = darkest_nail
self.residual = copy.deepcopy(self.data)
self.data = datacopy
return pattern
def choose_darkest_path(self, nail):
max_darkness = -1.0
for index, rowcol in enumerate(self.paths[nail]):
rows = [i[0] for i in rowcol]
cols = [i[1] for i in rowcol]
darkness = float(np.sum(self.data[rows, cols]))
if darkness > max_darkness:
darkest_path = np.zeros(np.shape(self.data))
darkest_path[rows,cols] = 1.0
darkest_nail = index
max_darkness = darkness
return darkest_nail, darkest_path
def calculate_paths(self):
for nail, anode in enumerate(self.nodes):
self.paths.append([])
for node in self.nodes:
path = self.bresenham_path(anode, node)
self.paths[nail].append(path)
def bresenham_path(self, start, end):
"""Bresenham's Line Algorithm
Produces an numpy array
"""
# Setup initial conditions
x1, y1 = start
x2, y2 = end
x1 = max(0, min(round(x1), self.data.shape[0]-1))
y1 = max(0, min(round(y1), self.data.shape[1]-1))
x2 = max(0, min(round(x2), self.data.shape[0]-1))
y2 = max(0, min(round(y2), self.data.shape[1]-1))
dx = x2 - x1
dy = y2 - y1
# Prepare output array
path = []
if (start == end):
return path
# Determine how steep the line is
is_steep = abs(dy) > abs(dx)
# Rotate line
if is_steep:
x1, y1 = y1, x1
x2, y2 = y2, x2
# Swap start and end points if necessary and store swap state
if x1 > x2:
x1, x2 = x2, x1
y1, y2 = y2, y1
# Recalculate differentials
dx = x2 - x1
dy = y2 - y1
# Calculate error
error = int(dx / 2.0)
ystep = 1 if y1 < y2 else -1
# Iterate over bounding box generating points between start and end
y = y1
for x in range(x1, x2 + 1):
if is_steep:
path.append([y, x])
else:
path.append([x, y])
error -= abs(dy)
if error < 0:
y += ystep
error += dx
return path
|