FLUX-DEV / app.py
AkshitShubham's picture
Update app.py
b4b3681 verified
import cv2
import numpy as np
import json
import math
import random
import gradio as gr
# Set random seed for reproducibility
random.seed(42)
np.random.seed(42)
# --- Utility Functions ---
def generate_id(prefix="el"):
"""Generate a random ID for Excalidraw elements."""
return f"{prefix}_{''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=9))}"
def bgr_to_hex(bgr_color):
"""Convert BGR color tuple to a hex string."""
b, g, r = [int(c) for c in bgr_color]
return f"#{r:02x}{g:02x}{b:02x}"
def simplify_contour(contour, epsilon_factor=0.002):
"""
Simplifies a contour to create smooth, crisp lines using a small epsilon factor.
Args:
contour: OpenCV contour (numpy array of points).
epsilon_factor: Factor to determine the approximation accuracy. A smaller
value results in more points and a line closer to the original shape.
Returns:
List of [x, y] coordinate pairs representing the simplified line.
"""
if len(contour) < 2:
return []
# Calculate epsilon based on the contour's perimeter for responsive simplification
epsilon = epsilon_factor * cv2.arcLength(contour, True)
# Apply the Douglas-Peucker algorithm to simplify the contour
simplified = cv2.approxPolyDP(contour, epsilon, True)
# Convert from OpenCV's format (n, 1, 2) to a simple list of [x, y] points
return [[float(point[0][0]), float(point[0][1])] for point in simplified]
def image_to_excalidraw_json(image_np, min_shape_size):
"""
Main function to convert an image with a black background into Excalidraw JSON.
Args:
image_np: The input image as a NumPy array (from Gradio).
min_shape_size: The minimum area for a contour to be considered a shape.
Returns:
A string containing the Excalidraw JSON, or an error message.
"""
if image_np is None:
return "Please upload an image to begin."
# Convert the input image from RGB (Gradio) to BGR (OpenCV)
img_bgr = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR)
# --- Shape Detection ---
# Convert the image to grayscale to easily find non-black areas
img_gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
# Threshold the image to create a binary mask. All pixels that are not black (value > 5)
# will be turned white (255). This isolates the shapes from the background.
_, binary_mask = cv2.threshold(img_gray, 5, 255, cv2.THRESH_BINARY)
# Find contours (outlines of the shapes) in the binary mask
# RETR_EXTERNAL gets only the outer contours, which is what we want.
# CHAIN_APPROX_SIMPLE compresses horizontal, vertical, and diagonal segments, saving memory.
contours, _ = cv2.findContours(binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
excalidraw_elements = []
# Process each detected contour
for contour in contours:
# Filter out very small contours that are likely just noise
if cv2.contourArea(contour) < min_shape_size:
continue
# --- Color Extraction ---
# Create a mask for the current contour to isolate it
mask = np.zeros_like(img_gray)
cv2.drawContours(mask, [contour], -1, (255), thickness=cv2.FILLED)
# Calculate the average color of the shape from the original color image
# The mask ensures we only consider pixels belonging to the current shape.
mean_color_bgr = cv2.mean(img_bgr, mask=mask)[:3]
stroke_color_hex = bgr_to_hex(mean_color_bgr)
# --- Point Simplification ---
# Simplify the contour to get smooth points for the Excalidraw line
points = simplify_contour(contour)
if len(points) < 2:
continue
# --- Element Creation ---
# The first point is the anchor (x, y), subsequent points are relative to it.
start_x, start_y = points[0]
relative_points = [[p[0] - start_x, p[1] - start_y] for p in points]
# Define the style for the Excalidraw element
element_style = {
"strokeColor": stroke_color_hex,
"backgroundColor": "transparent",
"fillStyle": "hachure",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"roundness": {"type": 2}, # Creates smooth, rounded corners
"seed": random.randint(1_000_000, 9_999_999),
"version": 1,
"versionNonce": random.randint(1_000_000, 9_999_999),
}
# Create the Excalidraw line element dictionary
line_element = {
"id": generate_id("line"),
"type": "line",
"x": float(start_x),
"y": float(start_y),
"angle": 0,
"points": relative_points,
**element_style,
}
excalidraw_elements.append(line_element)
# Create the final Excalidraw clipboard structure
final_excalidraw_structure = {
"type": "excalidraw/clipboard",
"elements": excalidraw_elements,
"files": {}
}
# Convert the Python dictionary to a nicely formatted JSON string
return json.dumps(final_excalidraw_structure, indent=2)
# --- Gradio User Interface ---
def create_interface():
"""Creates and configures the Gradio web interface."""
with gr.Blocks(title="Image to Excalidraw", theme=gr.themes.Soft()) as iface:
gr.Markdown("# ✏️ Image to Excalidraw Converter")
gr.Markdown("Upload an image with colored shapes on a **black background**. This tool will trace the shapes and generate Excalidraw JSON for you to copy and paste.")
with gr.Row(variant="panel"):
with gr.Column(scale=1):
image_input = gr.Image(type="numpy", label="Upload Your Image")
min_shape_size = gr.Slider(
minimum=10,
maximum=1000,
value=50,
step=10,
label="Minimum Shape Size",
info="Filters out small noise. Increase if you see unwanted tiny specks."
)
process_btn = gr.Button("✅ Generate Excalidraw Code", variant="primary")
with gr.Column(scale=2):
output_json = gr.Textbox(
label="Excalidraw JSON Output",
info="Click to copy the JSON, then paste it directly into Excalidraw (Ctrl+V or Cmd+V).",
lines=20,
max_lines=30,
show_copy_button=True
)
# Connect the button click event to the processing function
process_btn.click(
fn=image_to_excalidraw_json,
inputs=[image_input, min_shape_size],
outputs=output_json
)
# For better user experience, also trigger processing when the image or slider changes
image_input.change(
fn=image_to_excalidraw_json,
inputs=[image_input, min_shape_size],
outputs=output_json
)
min_shape_size.release(
fn=image_to_excalidraw_json,
inputs=[image_input, min_shape_size],
outputs=output_json
)
return iface
# --- Launch the App ---
if __name__ == "__main__":
app = create_interface()
app.launch()