aboalaa147 commited on
Commit
c3f66ee
·
verified ·
1 Parent(s): 7b33306

Upload stringart.py

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