|
|
import numpy as np |
|
|
import matplotlib.pyplot as plt |
|
|
from skimage.color import rgb2gray |
|
|
from skimage.transform import resize |
|
|
from skimage.draw import line |
|
|
from skimage.filters import gaussian |
|
|
import gradio as gr |
|
|
import tempfile |
|
|
|
|
|
def adjust_brightness_contrast(img, brightness=0.0, contrast=1.0): |
|
|
img = img * contrast + brightness |
|
|
img = np.clip(img, 0, 1) |
|
|
return img |
|
|
|
|
|
def preprocess_image(image, size=(200, 200), brightness=0.0, contrast=1.0): |
|
|
if image.ndim == 3: |
|
|
img_gray = rgb2gray(image) |
|
|
else: |
|
|
img_gray = image |
|
|
img_resized = resize(img_gray, size, anti_aliasing=True) |
|
|
img_adj = adjust_brightness_contrast(img_resized, brightness, contrast) |
|
|
img_inverted = 1 - img_adj |
|
|
img_inverted = (img_inverted - img_inverted.min()) / (img_inverted.max() - img_inverted.min() + 1e-8) |
|
|
return img_inverted |
|
|
|
|
|
def generate_pins(num_pins, radius=1.0): |
|
|
angles = np.linspace(0, 2 * np.pi, num_pins, endpoint=False) |
|
|
pins = np.array([(radius * np.cos(a), radius * np.sin(a)) for a in angles]) |
|
|
return pins |
|
|
|
|
|
def draw_line(canvas, pin1, pin2, opacity=0.2): |
|
|
h, w = canvas.shape |
|
|
x1 = int((pin1[0] + 1) / 2 * (w - 1)) |
|
|
y1 = int((pin1[1] + 1) / 2 * (h - 1)) |
|
|
x2 = int((pin2[0] + 1) / 2 * (w - 1)) |
|
|
y2 = int((pin2[1] + 1) / 2 * (h - 1)) |
|
|
rr, cc = line(y1, x1, y2, x2) |
|
|
rr = np.clip(rr, 0, h-1) |
|
|
cc = np.clip(cc, 0, w-1) |
|
|
canvas[rr, cc] = np.clip(canvas[rr, cc] - opacity, 0, 1) |
|
|
return canvas |
|
|
|
|
|
def blur_and_downsample(img, sigma=1.5, size=(100, 100)): |
|
|
img_blur = gaussian(img, sigma=sigma) |
|
|
img_small = resize(img_blur, size, anti_aliasing=True) |
|
|
return img_small |
|
|
|
|
|
def generate_connections(img, pins, num_connections, opacity=0.2): |
|
|
num_pins = len(pins) |
|
|
connections = [] |
|
|
current_pin = 0 |
|
|
canvas = np.ones_like(img) |
|
|
target_blur = blur_and_downsample(img) |
|
|
for _ in range(num_connections): |
|
|
best_error = np.inf |
|
|
best_pin = None |
|
|
best_rr, best_cc = None, None |
|
|
for next_pin in range(num_pins): |
|
|
if next_pin == current_pin: |
|
|
continue |
|
|
h, w = canvas.shape |
|
|
x1 = int((pins[current_pin][0] + 1) / 2 * (w - 1)) |
|
|
y1 = int((pins[current_pin][1] + 1) / 2 * (h - 1)) |
|
|
x2 = int((pins[next_pin][0] + 1) / 2 * (w - 1)) |
|
|
y2 = int((pins[next_pin][1] + 1) / 2 * (h - 1)) |
|
|
rr, cc = line(y1, x1, y2, x2) |
|
|
rr = np.clip(rr, 0, h-1) |
|
|
cc = np.clip(cc, 0, w-1) |
|
|
temp_canvas = canvas.copy() |
|
|
temp_canvas[rr, cc] = np.clip(temp_canvas[rr, cc] - opacity, 0, 1) |
|
|
temp_blur = blur_and_downsample(temp_canvas) |
|
|
error = np.sum((temp_blur - target_blur) ** 2) |
|
|
if error < best_error: |
|
|
best_error = error |
|
|
best_pin = next_pin |
|
|
best_rr, best_cc = rr, cc |
|
|
if best_pin is None: |
|
|
break |
|
|
connections.append((current_pin, best_pin)) |
|
|
canvas[best_rr, best_cc] = np.clip(canvas[best_rr, best_cc] - opacity, 0, 1) |
|
|
current_pin = best_pin |
|
|
return connections |
|
|
|
|
|
def draw_string_art(image, num_pins, num_connections, opacity=0.2, brightness=0.0, contrast=1.0): |
|
|
img = preprocess_image(image, brightness=brightness, contrast=contrast) |
|
|
pins = generate_pins(num_pins) |
|
|
connections = generate_connections(img, pins, num_connections, opacity) |
|
|
fig, ax = plt.subplots(figsize=(8, 8)) |
|
|
ax.set_facecolor("white") |
|
|
ax.axis("off") |
|
|
for c in connections: |
|
|
p1, p2 = pins[c[0]], pins[c[1]] |
|
|
ax.plot([p1[0], p2[0]], [p1[1], p2[1]], 'k-', linewidth=0.7, alpha=opacity) |
|
|
ax.scatter(pins[:,0], pins[:,1], c='red', s=16) |
|
|
plt.tight_layout(pad=0) |
|
|
plt.close(fig) |
|
|
connections_1_based = [str(c[0]+1) for c in connections] |
|
|
connections_str = ', '.join(connections_1_based) |
|
|
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".txt", mode="w", encoding="utf-8") |
|
|
temp_file.write(connections_str) |
|
|
temp_file.close() |
|
|
return fig, temp_file.name |
|
|
|
|
|
def interface(image, num_pins, num_connections, opacity, brightness, contrast): |
|
|
fig, filename = draw_string_art(image, num_pins, num_connections, opacity, brightness, contrast) |
|
|
return fig, filename |
|
|
|
|
|
demo = gr.Interface( |
|
|
fn=interface, |
|
|
inputs=[ |
|
|
gr.Image(type="numpy", label="Upload Image"), |
|
|
gr.Slider(50, 400, 100, step=1, label="Number of Pins"), |
|
|
gr.Slider(500, 10000, 1000, step=1, label="Number of Connections"), |
|
|
gr.Slider(0.05, 0.5, 0.2, step=0.01, label="String Opacity (Lower = Lighter)"), |
|
|
gr.Slider(-0.5, 0.5, 0.0, step=0.01, label="Brightness"), |
|
|
gr.Slider(0.5, 2.0, 1.0, step=0.01, label="Contrast") |
|
|
], |
|
|
outputs=[ |
|
|
gr.Plot(label="String Art"), |
|
|
gr.File(label="Connections File") |
|
|
], |
|
|
title="String Art Generator (Wow!Strings-like)", |
|
|
description="Upload an image, set the number of pins, connections, string opacity, brightness, and contrast. The generator will create a string art image that closely resembles your input and a downloadable connection file." |
|
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch() |
|
|
|