aboalaa147 commited on
Commit
12757ed
Β·
verified Β·
1 Parent(s): 7ed181a

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +531 -0
app.py ADDED
@@ -0,0 +1,531 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import numpy as np
3
+ import cv2
4
+ from PIL import Image, ImageDraw
5
+ import io
6
+ import base64
7
+ import math
8
+ import time
9
+ from typing import Tuple, List, Optional
10
+
11
+ class StringArtGenerator:
12
+ def __init__(self, num_nails=400, canvas_size=800):
13
+ self.num_nails = num_nails
14
+ self.canvas_size = canvas_size
15
+ self.nail_positions = []
16
+ self.connections = []
17
+
18
+ def generate_circle_nails(self):
19
+ """Generate nail positions around a circle"""
20
+ center = self.canvas_size // 2
21
+ radius = center - 50
22
+ angles = np.linspace(0, 2 * np.pi, self.num_nails, endpoint=False)
23
+
24
+ self.nail_positions = []
25
+ for angle in angles:
26
+ x = int(center + radius * np.cos(angle))
27
+ y = int(center + radius * np.sin(angle))
28
+ self.nail_positions.append((x, y))
29
+
30
+ def generate_heart_nails(self):
31
+ """Generate nail positions around a heart shape"""
32
+ center_x, center_y = self.canvas_size // 2, self.canvas_size // 2
33
+ scale = 80
34
+
35
+ self.nail_positions = []
36
+ t_values = np.linspace(0, 2 * np.pi, self.num_nails)
37
+
38
+ for t in t_values:
39
+ # Heart parametric equations
40
+ x = 16 * np.sin(t)**3
41
+ y = 13 * np.cos(t) - 5 * np.cos(2*t) - 2 * np.cos(3*t) - np.cos(4*t)
42
+
43
+ # Scale and center
44
+ x = int(center_x + scale * x)
45
+ y = int(center_y - scale * y) # Negative to flip vertically
46
+
47
+ self.nail_positions.append((x, y))
48
+
49
+ def preprocess_image(self, image, threshold=128, blur_kernel=5):
50
+ """Convert image to binary format suitable for string art"""
51
+ # Convert PIL Image to numpy array if needed
52
+ if isinstance(image, Image.Image):
53
+ image = np.array(image)
54
+
55
+ # Convert to grayscale
56
+ if len(image.shape) == 3:
57
+ gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
58
+ else:
59
+ gray = image
60
+
61
+ # Apply Gaussian blur
62
+ if blur_kernel > 1:
63
+ gray = cv2.GaussianBlur(gray, (blur_kernel, blur_kernel), 0)
64
+
65
+ # Apply threshold
66
+ _, binary = cv2.threshold(gray, threshold, 255, cv2.THRESH_BINARY_INV)
67
+
68
+ # Resize to canvas size
69
+ binary = cv2.resize(binary, (self.canvas_size, self.canvas_size))
70
+
71
+ return binary
72
+
73
+ def calculate_line_score(self, binary_image, nail1_idx, nail2_idx):
74
+ """Calculate how much darkness a line covers"""
75
+ x1, y1 = self.nail_positions[nail1_idx]
76
+ x2, y2 = self.nail_positions[nail2_idx]
77
+
78
+ # Get line pixels using Bresenham's algorithm
79
+ line_pixels = self.get_line_pixels(x1, y1, x2, y2)
80
+
81
+ # Calculate score based on darkness covered
82
+ score = 0
83
+ for x, y in line_pixels:
84
+ if 0 <= x < self.canvas_size and 0 <= y < self.canvas_size:
85
+ score += binary_image[y, x]
86
+
87
+ return score / 255.0 # Normalize
88
+
89
+ def get_line_pixels(self, x1, y1, x2, y2):
90
+ """Get all pixels on a line using Bresenham's algorithm"""
91
+ pixels = []
92
+
93
+ dx = abs(x2 - x1)
94
+ dy = abs(y2 - y1)
95
+ sx = 1 if x1 < x2 else -1
96
+ sy = 1 if y1 < y2 else -1
97
+ err = dx - dy
98
+
99
+ x, y = x1, y1
100
+
101
+ while True:
102
+ pixels.append((x, y))
103
+
104
+ if x == x2 and y == y2:
105
+ break
106
+
107
+ e2 = 2 * err
108
+ if e2 > -dy:
109
+ err -= dy
110
+ x += sx
111
+ if e2 < dx:
112
+ err += dx
113
+ y += sy
114
+
115
+ return pixels
116
+
117
+ def generate_string_art(self, binary_image, max_lines=3000):
118
+ """Generate string art using greedy algorithm"""
119
+ self.connections = []
120
+ used_image = np.zeros_like(binary_image, dtype=np.float32)
121
+
122
+ # Start from a random nail
123
+ current_nail = 0
124
+
125
+ for line_num in range(max_lines):
126
+ best_score = -1
127
+ best_nail = -1
128
+
129
+ # Find the best next nail
130
+ for next_nail in range(self.num_nails):
131
+ if next_nail == current_nail:
132
+ continue
133
+
134
+ # Calculate score for this line
135
+ score = self.calculate_line_score(binary_image - used_image, current_nail, next_nail)
136
+
137
+ if score > best_score:
138
+ best_score = score
139
+ best_nail = next_nail
140
+
141
+ # If no good line found, break
142
+ if best_score <= 0 or best_nail == -1:
143
+ break
144
+
145
+ # Add the line
146
+ self.connections.append((current_nail, best_nail))
147
+
148
+ # Update used image (darken the line area)
149
+ line_pixels = self.get_line_pixels(
150
+ self.nail_positions[current_nail][0], self.nail_positions[current_nail][1],
151
+ self.nail_positions[best_nail][0], self.nail_positions[best_nail][1]
152
+ )
153
+
154
+ for x, y in line_pixels:
155
+ if 0 <= x < self.canvas_size and 0 <= y < self.canvas_size:
156
+ used_image[y, x] = min(255, used_image[y, x] + 50)
157
+
158
+ current_nail = best_nail
159
+
160
+ return len(self.connections)
161
+
162
+ def render_string_art(self, thread_color=(0, 0, 0), line_opacity=0.8):
163
+ """Render the string art as an image"""
164
+ # Create white canvas
165
+ canvas = np.ones((self.canvas_size, self.canvas_size, 3), dtype=np.uint8) * 255
166
+
167
+ # Convert thread color to RGB
168
+ thread_rgb = thread_color
169
+
170
+ # Draw lines
171
+ for nail1_idx, nail2_idx in self.connections:
172
+ x1, y1 = self.nail_positions[nail1_idx]
173
+ x2, y2 = self.nail_positions[nail2_idx]
174
+
175
+ # Create line with opacity
176
+ overlay = canvas.copy()
177
+ cv2.line(overlay, (x1, y1), (x2, y2), thread_rgb, 1)
178
+ canvas = cv2.addWeighted(canvas, 1 - line_opacity, overlay, line_opacity, 0)
179
+
180
+ # Draw nails
181
+ for x, y in self.nail_positions:
182
+ cv2.circle(canvas, (x, y), 2, (100, 100, 100), -1)
183
+
184
+ return canvas
185
+
186
+ def generate_instructions(self):
187
+ """Generate step-by-step threading instructions"""
188
+ instructions = []
189
+ for i, (nail1, nail2) in enumerate(self.connections):
190
+ instructions.append(f"Step {i+1}: Nail {nail1} β†’ Nail {nail2}")
191
+ return "\n".join(instructions)
192
+
193
+ def create_predefined_shape(shape_type, size=800):
194
+ """Create predefined shapes"""
195
+ canvas = np.zeros((size, size), dtype=np.uint8)
196
+ center = size // 2
197
+
198
+ if shape_type == "Circle":
199
+ cv2.circle(canvas, (center, center), center - 100, 255, -1)
200
+ elif shape_type == "Heart":
201
+ # Create heart shape
202
+ points = []
203
+ scale = 100
204
+ for t in np.linspace(0, 2 * np.pi, 1000):
205
+ x = 16 * np.sin(t)**3
206
+ y = 13 * np.cos(t) - 5 * np.cos(2*t) - 2 * np.cos(3*t) - np.cos(4*t)
207
+ points.append([int(center + scale * x), int(center - scale * y)])
208
+
209
+ points = np.array(points, dtype=np.int32)
210
+ cv2.fillPoly(canvas, [points], 255)
211
+
212
+ return Image.fromarray(canvas)
213
+
214
+ def hex_to_rgb(hex_color):
215
+ """Convert hex color to RGB tuple"""
216
+ hex_color = hex_color.lstrip('#')
217
+ return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
218
+
219
+ def generate_string_art_interface(
220
+ input_image: Optional[Image.Image],
221
+ shape_type: str,
222
+ use_predefined: bool,
223
+ num_nails: int,
224
+ max_lines: int,
225
+ threshold: int,
226
+ blur_kernel: int,
227
+ thread_color: str,
228
+ line_opacity: float
229
+ ):
230
+ """Main function for Gradio interface"""
231
+
232
+ try:
233
+ # Determine input image
234
+ if use_predefined:
235
+ if shape_type in ["Circle", "Heart"]:
236
+ working_image = create_predefined_shape(shape_type)
237
+ else:
238
+ return None, None, "Please select a valid predefined shape.", ""
239
+ else:
240
+ if input_image is None:
241
+ return None, None, "Please upload an image or use a predefined shape.", ""
242
+ working_image = input_image
243
+
244
+ # Initialize generator
245
+ generator = StringArtGenerator(num_nails=num_nails, canvas_size=800)
246
+
247
+ # Generate nails based on shape
248
+ if use_predefined and shape_type == "Heart":
249
+ generator.generate_heart_nails()
250
+ else:
251
+ generator.generate_circle_nails()
252
+
253
+ # Preprocess image
254
+ processed_image = generator.preprocess_image(working_image, threshold, blur_kernel)
255
+ processed_pil = Image.fromarray(processed_image)
256
+
257
+ # Generate string art
258
+ start_time = time.time()
259
+ total_lines = generator.generate_string_art(processed_image, max_lines=max_lines)
260
+ end_time = time.time()
261
+
262
+ # Render result
263
+ thread_rgb = hex_to_rgb(thread_color)
264
+ result_image = generator.render_string_art(thread_rgb, line_opacity)
265
+ result_pil = Image.fromarray(result_image)
266
+
267
+ # Generate instructions
268
+ instructions = generator.generate_instructions()
269
+
270
+ # Create info text
271
+ info_text = f"""
272
+ 🎯 **Generation Complete!**
273
+ - **Total Lines**: {total_lines}
274
+ - **Processing Time**: {end_time - start_time:.2f} seconds
275
+ - **Nails Used**: {num_nails}
276
+ - **Thread Color**: {thread_color}
277
+
278
+ πŸ“‹ **Instructions Preview** (showing first 10 steps):
279
+ {chr(10).join(instructions.split(chr(10))[:10])}
280
+ ...
281
+
282
+ πŸ’‘ **Tips for Physical Crafting**:
283
+ 1. Print the full instructions from the downloadable text
284
+ 2. Mark nail positions evenly around your board perimeter
285
+ 3. Follow the step-by-step nail connections
286
+ 4. Maintain consistent string tension
287
+ """
288
+
289
+ return result_pil, processed_pil, info_text, instructions
290
+
291
+ except Exception as e:
292
+ return None, None, f"Error: {str(e)}", ""
293
+
294
+ # Create Gradio interface
295
+ def create_gradio_app():
296
+ with gr.Blocks(
297
+ title="🎨 String Art Generator",
298
+ theme=gr.themes.Soft(),
299
+ css="""
300
+ .gradio-container {
301
+ max-width: 1200px !important;
302
+ }
303
+ .main-header {
304
+ text-align: center;
305
+ margin-bottom: 2rem;
306
+ }
307
+ .info-box {
308
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
309
+ color: white;
310
+ padding: 1rem;
311
+ border-radius: 10px;
312
+ margin: 1rem 0;
313
+ }
314
+ """
315
+ ) as app:
316
+
317
+ # Header
318
+ gr.HTML("""
319
+ <div class="main-header">
320
+ <h1>🎨 String Art Generator</h1>
321
+ <p style="font-size: 1.2em; color: #666;">Transform images into beautiful string art patterns for physical crafting</p>
322
+ </div>
323
+ """)
324
+
325
+ with gr.Row():
326
+ # Left Column - Controls
327
+ with gr.Column(scale=1):
328
+ gr.HTML("<h3>πŸ–ΌοΈ Image Input</h3>")
329
+
330
+ use_predefined = gr.Checkbox(
331
+ label="Use Predefined Shape",
332
+ value=True,
333
+ info="Check to use predefined shapes, uncheck to upload your own image"
334
+ )
335
+
336
+ with gr.Group(visible=True) as predefined_group:
337
+ shape_type = gr.Radio(
338
+ choices=["Circle", "Heart"],
339
+ value="Circle",
340
+ label="Select Shape"
341
+ )
342
+
343
+ with gr.Group(visible=False) as upload_group:
344
+ input_image = gr.Image(
345
+ label="Upload Image",
346
+ type="pil",
347
+ info="Upload PNG, JPG, or other image formats"
348
+ )
349
+
350
+ gr.HTML("<h3>βš™οΈ Configuration</h3>")
351
+
352
+ num_nails = gr.Slider(
353
+ minimum=300,
354
+ maximum=1000,
355
+ value=400,
356
+ step=50,
357
+ label="Number of Nails",
358
+ info="More nails = more detail but slower processing"
359
+ )
360
+
361
+ max_lines = gr.Slider(
362
+ minimum=1000,
363
+ maximum=5000,
364
+ value=3000,
365
+ step=250,
366
+ label="Maximum Lines",
367
+ info="More lines = denser result"
368
+ )
369
+
370
+ gr.HTML("<h3>🎨 Appearance</h3>")
371
+
372
+ thread_color = gr.ColorPicker(
373
+ value="#000000",
374
+ label="Thread Color"
375
+ )
376
+
377
+ line_opacity = gr.Slider(
378
+ minimum=0.1,
379
+ maximum=1.0,
380
+ value=0.8,
381
+ step=0.1,
382
+ label="Line Opacity"
383
+ )
384
+
385
+ gr.HTML("<h3>πŸ”§ Processing</h3>")
386
+
387
+ threshold = gr.Slider(
388
+ minimum=50,
389
+ maximum=200,
390
+ value=128,
391
+ step=10,
392
+ label="Threshold",
393
+ info="Lower = more detail, Higher = simpler shapes"
394
+ )
395
+
396
+ blur_kernel = gr.Slider(
397
+ minimum=1,
398
+ maximum=15,
399
+ value=5,
400
+ step=2,
401
+ label="Blur Amount",
402
+ info="Smooths the image before processing"
403
+ )
404
+
405
+ generate_btn = gr.Button(
406
+ "πŸš€ Generate String Art",
407
+ variant="primary",
408
+ size="lg"
409
+ )
410
+
411
+ # Right Column - Results
412
+ with gr.Column(scale=2):
413
+ gr.HTML("<h3>πŸ“Š Results</h3>")
414
+
415
+ with gr.Row():
416
+ with gr.Column():
417
+ processed_output = gr.Image(
418
+ label="Processed Image",
419
+ type="pil"
420
+ )
421
+
422
+ with gr.Column():
423
+ result_output = gr.Image(
424
+ label="String Art Result",
425
+ type="pil"
426
+ )
427
+
428
+ info_output = gr.Markdown(
429
+ label="Generation Info",
430
+ value="Click 'Generate String Art' to start!"
431
+ )
432
+
433
+ gr.HTML("<h3>πŸ“₯ Downloads</h3>")
434
+
435
+ with gr.Row():
436
+ instructions_download = gr.File(
437
+ label="Threading Instructions (.txt)",
438
+ visible=False
439
+ )
440
+
441
+ instructions_text = gr.Textbox(
442
+ label="Threading Instructions",
443
+ lines=10,
444
+ max_lines=20,
445
+ placeholder="Instructions will appear here after generation...",
446
+ info="Copy these step-by-step instructions for physical crafting"
447
+ )
448
+
449
+ # Toggle visibility based on predefined checkbox
450
+ def toggle_input_method(use_pred):
451
+ return {
452
+ predefined_group: gr.Group(visible=use_pred),
453
+ upload_group: gr.Group(visible=not use_pred)
454
+ }
455
+
456
+ use_predefined.change(
457
+ toggle_input_method,
458
+ inputs=[use_predefined],
459
+ outputs=[predefined_group, upload_group]
460
+ )
461
+
462
+ # Generate button click handler
463
+ def generate_and_download(image, shape, use_pred, nails, lines, thresh, blur, color, opacity):
464
+ result_img, processed_img, info, instructions = generate_string_art_interface(
465
+ image, shape, use_pred, nails, lines, thresh, blur, color, opacity
466
+ )
467
+
468
+ # Create downloadable file
469
+ if instructions:
470
+ instructions_file = io.StringIO(instructions)
471
+ instructions_bytes = io.BytesIO(instructions.encode('utf-8'))
472
+ return result_img, processed_img, info, instructions, instructions_bytes
473
+
474
+ return result_img, processed_img, info, instructions, None
475
+
476
+ generate_btn.click(
477
+ generate_and_download,
478
+ inputs=[
479
+ input_image, shape_type, use_predefined, num_nails, max_lines,
480
+ threshold, blur_kernel, thread_color, line_opacity
481
+ ],
482
+ outputs=[
483
+ result_output, processed_output, info_output,
484
+ instructions_text, instructions_download
485
+ ]
486
+ )
487
+
488
+ # Add examples
489
+ gr.HTML("<h3>πŸ’‘ Quick Start Examples</h3>")
490
+
491
+ gr.Examples(
492
+ examples=[
493
+ [None, "Circle", True, 400, 3000, 128, 5, "#000000", 0.8],
494
+ [None, "Heart", True, 500, 3500, 120, 3, "#FF0000", 0.7],
495
+ [None, "Circle", True, 600, 4000, 140, 7, "#0000FF", 0.9],
496
+ ],
497
+ inputs=[
498
+ input_image, shape_type, use_predefined, num_nails, max_lines,
499
+ threshold, blur_kernel, thread_color, line_opacity
500
+ ],
501
+ outputs=[
502
+ result_output, processed_output, info_output,
503
+ instructions_text, instructions_download
504
+ ],
505
+ fn=generate_and_download,
506
+ cache_examples=False
507
+ )
508
+
509
+ # Footer
510
+ gr.HTML("""
511
+ <div style="text-align: center; margin-top: 2rem; padding: 1rem; background: #f0f0f0; border-radius: 10px;">
512
+ <h4>πŸ”¨ Physical Crafting Tips</h4>
513
+ <p>
514
+ <strong>Materials:</strong> Wooden board, small nails, string/thread, hammer<br>
515
+ <strong>Process:</strong> Mark nail positions β†’ Hammer nails β†’ Follow step-by-step instructions<br>
516
+ <strong>Pro Tip:</strong> Keep consistent string tension for best results!
517
+ </p>
518
+ </div>
519
+ """)
520
+
521
+ return app
522
+
523
+ # Launch the app
524
+ if __name__ == "__main__":
525
+ app = create_gradio_app()
526
+ app.launch(
527
+ server_name="0.0.0.0",
528
+ server_port=7860,
529
+ share=True,
530
+ show_error=True
531
+ )