citoreh commited on
Commit
3481333
·
verified ·
1 Parent(s): fe2e05e

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +654 -0
app.py ADDED
@@ -0,0 +1,654 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # String Art Generator for Hugging Face Spaces
2
+ # Upload an image and generate downloadable string art instructions
3
+
4
+ import numpy as np
5
+ import matplotlib.pyplot as plt
6
+ import cv2
7
+ from PIL import Image, ImageDraw, ImageFont
8
+ import math
9
+ import io
10
+ import zipfile
11
+ import os
12
+ from reportlab.lib.pagesizes import letter, A4
13
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak
14
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
15
+ from reportlab.lib.units import inch
16
+ from reportlab.lib import colors
17
+ import gradio as gr
18
+ import tempfile
19
+ import json
20
+
21
+ class StringArtGenerator:
22
+ def __init__(self, num_pins=200, canvas_size=800):
23
+ self.num_pins = num_pins
24
+ self.canvas_size = canvas_size
25
+ self.pins = []
26
+ self.string_paths = []
27
+ self.image_processed = None
28
+ self.original_image = None
29
+
30
+ def process_image(self, image_path):
31
+ """Process the uploaded image"""
32
+ # Load and process image
33
+ image = Image.open(image_path)
34
+ self.original_image = image.copy()
35
+
36
+ # Convert to grayscale and resize
37
+ image = image.convert('L')
38
+ image = image.resize((self.canvas_size, self.canvas_size), Image.Resampling.LANCZOS)
39
+
40
+ # Convert to numpy array
41
+ img_array = np.array(image)
42
+
43
+ # Apply edge detection and processing
44
+ self.image_processed = self.preprocess_image(img_array)
45
+
46
+ return self.image_processed
47
+
48
+ def preprocess_image(self, img_array):
49
+ """Preprocess image for string art conversion"""
50
+ # Apply Gaussian blur to smooth the image
51
+ blurred = cv2.GaussianBlur(img_array, (3, 3), 0)
52
+
53
+ # Apply edge detection
54
+ edges = cv2.Canny(blurred, 50, 150)
55
+
56
+ # Combine original image with edges for better string art effect
57
+ # Invert so dark areas need more strings
58
+ processed = 255 - blurred
59
+
60
+ # Enhance contrast
61
+ processed = cv2.equalizeHist(processed)
62
+
63
+ # Combine with edges
64
+ combined = cv2.addWeighted(processed, 0.7, edges, 0.3, 0)
65
+
66
+ return combined
67
+
68
+ def generate_pins(self, shape='circle'):
69
+ """Generate pin positions around the perimeter"""
70
+ pins = []
71
+ center = self.canvas_size // 2
72
+
73
+ if shape == 'circle':
74
+ radius = center - 50 # Leave some margin
75
+ for i in range(self.num_pins):
76
+ angle = 2 * math.pi * i / self.num_pins
77
+ x = center + radius * math.cos(angle)
78
+ y = center + radius * math.sin(angle)
79
+ pins.append((int(x), int(y)))
80
+
81
+ elif shape == 'square':
82
+ margin = 50
83
+ side_pins = self.num_pins // 4
84
+
85
+ # Top side
86
+ for i in range(side_pins):
87
+ x = margin + i * (self.canvas_size - 2 * margin) / side_pins
88
+ pins.append((int(x), margin))
89
+
90
+ # Right side
91
+ for i in range(side_pins):
92
+ y = margin + i * (self.canvas_size - 2 * margin) / side_pins
93
+ pins.append((self.canvas_size - margin, int(y)))
94
+
95
+ # Bottom side
96
+ for i in range(side_pins):
97
+ x = self.canvas_size - margin - i * (self.canvas_size - 2 * margin) / side_pins
98
+ pins.append((int(x), self.canvas_size - margin))
99
+
100
+ # Left side
101
+ for i in range(side_pins):
102
+ y = self.canvas_size - margin - i * (self.canvas_size - 2 * margin) / side_pins
103
+ pins.append((margin, int(y)))
104
+
105
+ self.pins = pins
106
+ return pins
107
+
108
+ def calculate_string_score(self, pin1_idx, pin2_idx, current_canvas):
109
+ """Calculate score for adding a string between two pins"""
110
+ pin1 = self.pins[pin1_idx]
111
+ pin2 = self.pins[pin2_idx]
112
+
113
+ x1, y1 = pin1
114
+ x2, y2 = pin2
115
+
116
+ length = int(math.sqrt((x2-x1)**2 + (y2-y1)**2))
117
+ if length == 0:
118
+ return 0
119
+
120
+ score = 0
121
+ for i in range(length):
122
+ t = i / length
123
+ x = int(x1 + t * (x2 - x1))
124
+ y = int(y1 + t * (y2 - y1))
125
+
126
+ if 0 <= x < self.canvas_size and 0 <= y < self.canvas_size:
127
+ target_darkness = self.image_processed[y, x]
128
+ current_coverage = current_canvas[y, x]
129
+ contribution = target_darkness * (1 - current_coverage / 255)
130
+ score += max(0, contribution)
131
+
132
+ return score / length
133
+
134
+ def draw_string_on_canvas(self, pin1_idx, pin2_idx, canvas, intensity=30):
135
+ """Draw a string on the canvas"""
136
+ pin1 = self.pins[pin1_idx]
137
+ pin2 = self.pins[pin2_idx]
138
+
139
+ x1, y1 = pin1
140
+ x2, y2 = pin2
141
+
142
+ # Draw line using Bresenham's algorithm
143
+ dx = abs(x2 - x1)
144
+ dy = abs(y2 - y1)
145
+ x, y = x1, y1
146
+
147
+ x_inc = 1 if x1 < x2 else -1
148
+ y_inc = 1 if y1 < y2 else -1
149
+ error = dx - dy
150
+
151
+ while True:
152
+ if 0 <= x < self.canvas_size and 0 <= y < self.canvas_size:
153
+ canvas[y, x] = min(255, canvas[y, x] + intensity)
154
+
155
+ if x == x2 and y == y2:
156
+ break
157
+
158
+ e2 = 2 * error
159
+ if e2 > -dy:
160
+ error -= dy
161
+ x += x_inc
162
+ if e2 < dx:
163
+ error += dx
164
+ y += y_inc
165
+
166
+ def greedy_string_art(self, max_strings=2000, min_darkness_threshold=10):
167
+ """Generate string art using greedy algorithm"""
168
+ string_canvas = np.zeros((self.canvas_size, self.canvas_size))
169
+ string_paths = []
170
+
171
+ current_pin = 0 # Start from first pin
172
+
173
+ for string_num in range(max_strings):
174
+ best_pin = -1
175
+ best_score = -1
176
+
177
+ # Try all possible next pins
178
+ for next_pin in range(self.num_pins):
179
+ if next_pin == current_pin:
180
+ continue
181
+
182
+ score = self.calculate_string_score(current_pin, next_pin, string_canvas)
183
+
184
+ if score > best_score and score > min_darkness_threshold:
185
+ best_score = score
186
+ best_pin = next_pin
187
+
188
+ if best_pin == -1:
189
+ break
190
+
191
+ string_paths.append((current_pin, best_pin))
192
+ self.draw_string_on_canvas(current_pin, best_pin, string_canvas)
193
+ current_pin = best_pin
194
+
195
+ self.string_paths = string_paths
196
+ return string_paths
197
+
198
+ def create_visualizations(self):
199
+ """Create all visualization images"""
200
+ visualizations = {}
201
+
202
+ # 1. Original vs Processed
203
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))
204
+
205
+ # Show original image
206
+ if self.original_image:
207
+ ax1.imshow(self.original_image, cmap='gray' if self.original_image.mode == 'L' else None)
208
+ ax1.set_title('Original Image')
209
+ ax1.axis('off')
210
+
211
+ # Show processed image
212
+ ax2.imshow(self.image_processed, cmap='gray')
213
+ ax2.set_title('Processed for String Art')
214
+ ax2.axis('off')
215
+
216
+ plt.tight_layout()
217
+
218
+ # Save original vs processed
219
+ buf = io.BytesIO()
220
+ plt.savefig(buf, format='png', dpi=150, bbox_inches='tight')
221
+ buf.seek(0)
222
+ visualizations['original_vs_processed'] = buf.getvalue()
223
+ plt.close()
224
+
225
+ # 2. Pin Template
226
+ fig, ax = plt.subplots(1, 1, figsize=(10, 10))
227
+
228
+ # Draw frame
229
+ if len(self.pins) > 0:
230
+ if abs(self.pins[0][0] - self.canvas_size//2) > abs(self.pins[0][1] - self.canvas_size//2):
231
+ # Square frame
232
+ rect = plt.Rectangle((50, 50), self.canvas_size-100, self.canvas_size-100,
233
+ fill=False, color='black', linewidth=2)
234
+ ax.add_patch(rect)
235
+ else:
236
+ # Circular frame
237
+ circle = plt.Circle((self.canvas_size//2, self.canvas_size//2),
238
+ self.canvas_size//2 - 50, fill=False, color='black', linewidth=2)
239
+ ax.add_patch(circle)
240
+
241
+ # Add pin positions and numbers
242
+ for i, (x, y) in enumerate(self.pins):
243
+ ax.plot(x, y, 'ro', markersize=4)
244
+ if i % (max(1, len(self.pins)//20)) == 0: # Label every nth pin
245
+ ax.text(x+15, y+15, str(i), fontsize=8, ha='left', va='bottom',
246
+ bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8))
247
+
248
+ ax.set_xlim(0, self.canvas_size)
249
+ ax.set_ylim(0, self.canvas_size)
250
+ ax.set_aspect('equal')
251
+ ax.invert_yaxis()
252
+ ax.set_title(f'Pin Template - {self.num_pins} pins\n(Print this template to mark pin positions)',
253
+ fontsize=14, fontweight='bold')
254
+ ax.grid(True, alpha=0.3)
255
+
256
+ plt.tight_layout()
257
+
258
+ # Save pin template
259
+ buf = io.BytesIO()
260
+ plt.savefig(buf, format='png', dpi=300, bbox_inches='tight')
261
+ buf.seek(0)
262
+ visualizations['pin_template'] = buf.getvalue()
263
+ plt.close()
264
+
265
+ # 3. String Art Result
266
+ string_canvas = np.zeros((self.canvas_size, self.canvas_size))
267
+ for pin1_idx, pin2_idx in self.string_paths:
268
+ self.draw_string_on_canvas(pin1_idx, pin2_idx, string_canvas)
269
+
270
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))
271
+
272
+ # Original processed image
273
+ ax1.imshow(self.image_processed, cmap='gray')
274
+ ax1.set_title('Target Image')
275
+ ax1.axis('off')
276
+
277
+ # String art result
278
+ ax2.imshow(255 - string_canvas, cmap='gray')
279
+ ax2.set_title(f'String Art Result\n({len(self.string_paths)} strings)')
280
+ ax2.axis('off')
281
+
282
+ plt.tight_layout()
283
+
284
+ # Save string art result
285
+ buf = io.BytesIO()
286
+ plt.savefig(buf, format='png', dpi=150, bbox_inches='tight')
287
+ buf.seek(0)
288
+ visualizations['string_art_result'] = buf.getvalue()
289
+ plt.close()
290
+
291
+ return visualizations
292
+
293
+ def estimate_string_length(self):
294
+ """Estimate total string length needed"""
295
+ total_length = 0
296
+ for pin1_idx, pin2_idx in self.string_paths:
297
+ pin1 = self.pins[pin1_idx]
298
+ pin2 = self.pins[pin2_idx]
299
+ distance = math.sqrt((pin1[0] - pin2[0])**2 + (pin1[1] - pin2[1])**2)
300
+ total_length += distance / 10 # Convert pixels to cm
301
+
302
+ return total_length * 1.2 # Add 20% buffer
303
+
304
+ def create_instruction_pdf(self):
305
+ """Create a comprehensive PDF instruction manual"""
306
+ buffer = io.BytesIO()
307
+ doc = SimpleDocTemplate(buffer, pagesize=A4)
308
+ styles = getSampleStyleSheet()
309
+ story = []
310
+
311
+ # Title
312
+ title_style = ParagraphStyle(
313
+ 'CustomTitle',
314
+ parent=styles['Heading1'],
315
+ fontSize=24,
316
+ spaceAfter=30,
317
+ alignment=1 # Center alignment
318
+ )
319
+ story.append(Paragraph("STRING ART CONSTRUCTION MANUAL", title_style))
320
+ story.append(Spacer(1, 20))
321
+
322
+ # Materials section
323
+ story.append(Paragraph("MATERIALS NEEDED", styles['Heading2']))
324
+ materials = [
325
+ f"• Circular or square frame ({self.canvas_size//10}cm recommended)",
326
+ f"• {self.num_pins} small nails or pins",
327
+ f"• Black thread or string (approximately {self.estimate_string_length():.1f} meters)",
328
+ "• Hammer",
329
+ "• Ruler or measuring tape",
330
+ "• Pencil for marking",
331
+ "• Printed pin template (included)"
332
+ ]
333
+
334
+ for material in materials:
335
+ story.append(Paragraph(material, styles['Normal']))
336
+
337
+ story.append(Spacer(1, 20))
338
+
339
+ # Setup instructions
340
+ story.append(Paragraph("SETUP INSTRUCTIONS", styles['Heading2']))
341
+ setup_steps = [
342
+ f"1. Print the pin template at actual size",
343
+ f"2. Attach template to your frame/board",
344
+ f"3. Mark all {self.num_pins} pin positions",
345
+ f"4. Number each pin from 0 to {self.num_pins-1}",
346
+ f"5. Hammer nails at each marked point",
347
+ f"6. Leave about 5mm of nail protruding for string wrapping"
348
+ ]
349
+
350
+ for step in setup_steps:
351
+ story.append(Paragraph(step, styles['Normal']))
352
+
353
+ story.append(Spacer(1, 20))
354
+
355
+ # Construction info
356
+ story.append(Paragraph("CONSTRUCTION OVERVIEW", styles['Heading2']))
357
+ overview = [
358
+ f"Total strings to connect: {len(self.string_paths)}",
359
+ f"Estimated completion time: {len(self.string_paths)//20}-{len(self.string_paths)//10} minutes",
360
+ f"Starting pin: {self.string_paths[0][0] if self.string_paths else 0}",
361
+ f"Estimated string length: {self.estimate_string_length():.1f} meters"
362
+ ]
363
+
364
+ for info in overview:
365
+ story.append(Paragraph(info, styles['Normal']))
366
+
367
+ story.append(PageBreak())
368
+
369
+ # String connections table
370
+ story.append(Paragraph("STRING CONNECTION SEQUENCE", styles['Heading2']))
371
+ story.append(Paragraph("Follow this sequence exactly, connecting each numbered string from the first pin to the second pin:", styles['Normal']))
372
+ story.append(Spacer(1, 10))
373
+
374
+ # Create table with string connections
375
+ strings_per_page = 40
376
+ total_pages = (len(self.string_paths) + strings_per_page - 1) // strings_per_page
377
+
378
+ for page in range(total_pages):
379
+ start_idx = page * strings_per_page
380
+ end_idx = min((page + 1) * strings_per_page, len(self.string_paths))
381
+
382
+ # Table data
383
+ table_data = [['String #', 'From Pin', 'To Pin', 'String #', 'From Pin', 'To Pin']]
384
+
385
+ for i in range(start_idx, end_idx, 2):
386
+ row = []
387
+ # First string in row
388
+ pin1, pin2 = self.string_paths[i]
389
+ row.extend([str(i+1), str(pin1), str(pin2)])
390
+
391
+ # Second string in row (if exists)
392
+ if i+1 < end_idx:
393
+ pin1_2, pin2_2 = self.string_paths[i+1]
394
+ row.extend([str(i+2), str(pin1_2), str(pin2_2)])
395
+ else:
396
+ row.extend(['', '', ''])
397
+
398
+ table_data.append(row)
399
+
400
+ # Create table
401
+ table = Table(table_data, colWidths=[0.8*inch, 0.8*inch, 0.8*inch, 0.8*inch, 0.8*inch, 0.8*inch])
402
+ table.setStyle(TableStyle([
403
+ ('BACKGROUND', (0, 0), (-1, 0), colors.grey),
404
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
405
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
406
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
407
+ ('FONTSIZE', (0, 0), (-1, 0), 10),
408
+ ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
409
+ ('BACKGROUND', (0, 1), (-1, -1), colors.beige),
410
+ ('FONTSIZE', (0, 1), (-1, -1), 8),
411
+ ('GRID', (0, 0), (-1, -1), 1, colors.black)
412
+ ]))
413
+
414
+ story.append(table)
415
+
416
+ if page < total_pages - 1:
417
+ story.append(PageBreak())
418
+
419
+ story.append(PageBreak())
420
+
421
+ # Tips section
422
+ story.append(Paragraph("CONSTRUCTION TIPS", styles['Heading2']))
423
+ tips = [
424
+ f"• Start with string tied to pin {self.string_paths[0][0] if self.string_paths else 0}",
425
+ "• Maintain consistent tension throughout",
426
+ "• Don't pull too tight - the string should have slight slack",
427
+ "• If you make a mistake, carefully backtrack to the error",
428
+ "• Take breaks every 100-200 strings to avoid fatigue",
429
+ "• The image will become clearer as you add more strings",
430
+ "• Mark your progress every 50 strings to track completion",
431
+ "• Use good lighting to see pin numbers clearly"
432
+ ]
433
+
434
+ for tip in tips:
435
+ story.append(Paragraph(tip, styles['Normal']))
436
+
437
+ # Build PDF
438
+ doc.build(story)
439
+ buffer.seek(0)
440
+ return buffer.getvalue()
441
+
442
+ def generate_string_art(image, num_pins, max_strings, shape, progress=gr.Progress()):
443
+ """Main function to generate string art from uploaded image"""
444
+ if image is None:
445
+ return None, None, None, "Please upload an image first."
446
+
447
+ progress(0.1, desc="Initializing...")
448
+
449
+ # Initialize generator
450
+ generator = StringArtGenerator(num_pins=num_pins)
451
+
452
+ progress(0.2, desc="Processing image...")
453
+
454
+ # Process image
455
+ generator.process_image(image)
456
+
457
+ progress(0.3, desc="Generating pin layout...")
458
+
459
+ # Generate pins
460
+ generator.generate_pins(shape=shape)
461
+
462
+ progress(0.5, desc="Calculating optimal string paths...")
463
+
464
+ # Generate string art
465
+ string_paths = generator.greedy_string_art(max_strings=max_strings)
466
+
467
+ progress(0.7, desc="Creating visualizations...")
468
+
469
+ # Create visualizations
470
+ visualizations = generator.create_visualizations()
471
+
472
+ progress(0.9, desc="Generating instruction manual...")
473
+
474
+ # Create instruction PDF
475
+ pdf_content = generator.create_instruction_pdf()
476
+
477
+ progress(1.0, desc="Complete!")
478
+
479
+ # Create temporary files for downloads
480
+ with tempfile.NamedTemporaryFile(mode='wb', suffix='.pdf', delete=False) as f:
481
+ f.write(pdf_content)
482
+ pdf_path = f.name
483
+
484
+ with tempfile.NamedTemporaryFile(mode='wb', suffix='.png', delete=False) as f:
485
+ f.write(visualizations['pin_template'])
486
+ template_path = f.name
487
+
488
+ with tempfile.NamedTemporaryFile(mode='wb', suffix='.png', delete=False) as f:
489
+ f.write(visualizations['string_art_result'])
490
+ result_path = f.name
491
+
492
+ # Create zip file with all outputs
493
+ zip_buffer = io.BytesIO()
494
+ with zipfile.ZipFile(zip_buffer, 'w') as zip_file:
495
+ zip_file.writestr('instruction_manual.pdf', pdf_content)
496
+ zip_file.writestr('pin_template.png', visualizations['pin_template'])
497
+ zip_file.writestr('string_art_result.png', visualizations['string_art_result'])
498
+ zip_file.writestr('original_vs_processed.png', visualizations['original_vs_processed'])
499
+
500
+ # Add JSON with string paths for advanced users
501
+ string_data = {
502
+ 'num_pins': num_pins,
503
+ 'num_strings': len(string_paths),
504
+ 'string_paths': string_paths,
505
+ 'estimated_length_meters': generator.estimate_string_length()
506
+ }
507
+ zip_file.writestr('string_data.json', json.dumps(string_data, indent=2))
508
+
509
+ zip_buffer.seek(0)
510
+
511
+ with tempfile.NamedTemporaryFile(mode='wb', suffix='.zip', delete=False) as f:
512
+ f.write(zip_buffer.getvalue())
513
+ zip_path = f.name
514
+
515
+ # Create summary text
516
+ summary = f"""
517
+ ## String Art Generation Complete! 🎨
518
+
519
+ **Statistics:**
520
+ - **Pins:** {num_pins}
521
+ - **Strings:** {len(string_paths)}
522
+ - **Estimated String Length:** {generator.estimate_string_length():.1f} meters
523
+ - **Estimated Construction Time:** {len(string_paths)//20}-{len(string_paths)//10} minutes
524
+ - **Frame Shape:** {shape.title()}
525
+
526
+ **Downloads Available:**
527
+ 1. **Complete Package (ZIP)** - Contains all files
528
+ 2. **Instruction Manual (PDF)** - Step-by-step construction guide
529
+ 3. **Pin Template (PNG)** - Print this to mark pin positions
530
+
531
+ **Next Steps:**
532
+ 1. Download the complete package
533
+ 2. Print the pin template at actual size
534
+ 3. Follow the instruction manual
535
+ 4. Create your string art masterpiece!
536
+ """
537
+
538
+ return pdf_path, template_path, zip_path, summary
539
+
540
+ # Create Gradio interface
541
+ def create_interface():
542
+ with gr.Blocks(title="String Art Generator", theme=gr.themes.Soft()) as app:
543
+ gr.Markdown("""
544
+ # 🎨 String Art Generator
545
+
546
+ Transform any image into detailed string art instructions! Upload an image and get:
547
+ - Step-by-step construction manual
548
+ - Printable pin template
549
+ - Complete material list
550
+ - Downloadable instruction package
551
+ """)
552
+
553
+ with gr.Row():
554
+ with gr.Column(scale=1):
555
+ image_input = gr.Image(
556
+ label="Upload Image",
557
+ type="filepath",
558
+ height=300
559
+ )
560
+
561
+ with gr.Row():
562
+ num_pins = gr.Slider(
563
+ minimum=100,
564
+ maximum=400,
565
+ value=200,
566
+ step=10,
567
+ label="Number of Pins",
568
+ info="More pins = higher detail, longer construction"
569
+ )
570
+
571
+ with gr.Row():
572
+ max_strings = gr.Slider(
573
+ minimum=500,
574
+ maximum=5000,
575
+ value=2000,
576
+ step=100,
577
+ label="Maximum Strings",
578
+ info="More strings = better quality, longer time"
579
+ )
580
+
581
+ shape = gr.Radio(
582
+ choices=["circle", "square"],
583
+ value="circle",
584
+ label="Frame Shape",
585
+ info="Choose the shape of your frame"
586
+ )
587
+
588
+ generate_btn = gr.Button(
589
+ "Generate String Art Instructions",
590
+ variant="primary",
591
+ size="lg"
592
+ )
593
+
594
+ with gr.Column(scale=2):
595
+ summary_output = gr.Markdown(label="Generation Summary")
596
+
597
+ with gr.Row():
598
+ pdf_download = gr.File(
599
+ label="📋 Instruction Manual (PDF)",
600
+ visible=True
601
+ )
602
+ template_download = gr.File(
603
+ label="📍 Pin Template (PNG)",
604
+ visible=True
605
+ )
606
+
607
+ zip_download = gr.File(
608
+ label="📦 Complete Package (ZIP)",
609
+ visible=True
610
+ )
611
+
612
+ # Event handler
613
+ generate_btn.click(
614
+ fn=generate_string_art,
615
+ inputs=[image_input, num_pins, max_strings, shape],
616
+ outputs=[pdf_download, template_download, zip_download, summary_output]
617
+ )
618
+
619
+ gr.Markdown("""
620
+ ## How to Use:
621
+ 1. **Upload** your image (photos, artwork, logos work well)
622
+ 2. **Adjust** settings based on desired complexity
623
+ 3. **Generate** your string art instructions
624
+ 4. **Download** the complete package
625
+ 5. **Print** the pin template and follow the manual
626
+
627
+ ## Tips for Best Results:
628
+ - Use high-contrast images
629
+ - Simple compositions work better than complex scenes
630
+ - Black and white or monochrome images are ideal
631
+ - Portraits and geometric designs are excellent choices
632
+
633
+ ---
634
+ *Created with ❤️ for the maker community*
635
+ """)
636
+
637
+ return app
638
+
639
+ # Install required packages and run
640
+ if __name__ == "__main__":
641
+ # Create and launch the interface
642
+ app = create_interface()
643
+ app.launch()
644
+
645
+ # For Hugging Face Spaces deployment, also include requirements.txt:
646
+ """
647
+ gradio
648
+ opencv-python
649
+ pillow
650
+ numpy
651
+ matplotlib
652
+ reportlab
653
+ scipy
654
+ """