Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,208 +1,659 @@
|
|
| 1 |
-
import
|
| 2 |
-
import
|
| 3 |
-
import os
|
| 4 |
-
import base64
|
| 5 |
-
from io import BytesIO
|
| 6 |
-
from PIL import Image
|
| 7 |
import json
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
"""
|
| 11 |
-
|
|
|
|
| 12 |
"""
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
#
|
| 20 |
-
|
|
|
|
| 21 |
|
| 22 |
-
#
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
|
| 28 |
-
#
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
"width": int(width),
|
| 33 |
-
"height": int(height),
|
| 34 |
-
"seed": int(seed) if seed != -1 else 0,
|
| 35 |
-
"steps": int(steps)
|
| 36 |
-
}
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
if "image" in response_body:
|
| 55 |
-
# Decode base64 image
|
| 56 |
-
image_data = response_body["image"]
|
| 57 |
-
if image_data.startswith("data:image"):
|
| 58 |
-
# Remove data URL prefix
|
| 59 |
-
image_data = image_data.split(",")[1]
|
| 60 |
-
|
| 61 |
-
# Decode and create PIL Image
|
| 62 |
-
image_bytes = base64.b64decode(image_data)
|
| 63 |
-
image = Image.open(BytesIO(image_bytes))
|
| 64 |
-
|
| 65 |
-
return image, f"✅ Image generated successfully!\nSeed used: {response_body.get('seed', 'unknown')}"
|
| 66 |
-
else:
|
| 67 |
-
return None, f"❌ Error: No image in response\n{json.dumps(response_body, indent=2)}"
|
| 68 |
-
|
| 69 |
-
except requests.exceptions.RequestException as e:
|
| 70 |
-
return None, f"❌ API Request Error: {str(e)}"
|
| 71 |
-
except Exception as e:
|
| 72 |
-
return None, f"❌ Error: {str(e)}"
|
| 73 |
|
| 74 |
-
def
|
| 75 |
"""
|
| 76 |
-
|
|
|
|
| 77 |
"""
|
|
|
|
|
|
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
)
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
)
|
| 114 |
-
|
| 115 |
-
steps = gr.Slider(
|
| 116 |
-
minimum=10,
|
| 117 |
-
maximum=100,
|
| 118 |
-
value=50,
|
| 119 |
-
step=5,
|
| 120 |
-
label="Steps",
|
| 121 |
-
info="More steps = higher quality but slower"
|
| 122 |
-
)
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
)
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
value=1024,
|
| 134 |
-
label="Height"
|
| 135 |
-
)
|
| 136 |
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
info="Use -1 for random seed"
|
| 141 |
-
)
|
| 142 |
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
)
|
| 190 |
-
|
| 191 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
if __name__ == "__main__":
|
| 194 |
-
|
| 195 |
-
if not os.getenv("NVIDIA_API_KEY"):
|
| 196 |
-
print("⚠️ Warning: NVIDIA_API_KEY environment variable not set!")
|
| 197 |
-
print("Please set it before running the app:")
|
| 198 |
-
print("export NVIDIA_API_KEY=your_api_key_here")
|
| 199 |
-
print()
|
| 200 |
-
|
| 201 |
-
# Create and launch the app
|
| 202 |
-
demo = create_gradio_interface()
|
| 203 |
-
demo.launch(
|
| 204 |
-
share=False, # Set to True if you want a public link
|
| 205 |
-
server_name="0.0.0.0", # Allow external connections
|
| 206 |
-
server_port=7860,
|
| 207 |
-
show_error=True
|
| 208 |
-
)
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import json
|
| 4 |
+
import math
|
| 5 |
+
import random
|
| 6 |
+
from scipy.spatial.distance import cdist
|
| 7 |
+
import gradio as gr # Import gradio
|
| 8 |
|
| 9 |
+
# Set random seed for reproducibility (remove if you want different results each time)
|
| 10 |
+
random.seed(42)
|
| 11 |
+
np.random.seed(42)
|
| 12 |
+
|
| 13 |
+
# --- Image Processing and Contour Extraction ---
|
| 14 |
+
|
| 15 |
+
def extract_curves_from_image_np(img_bgr_np):
|
| 16 |
+
"""
|
| 17 |
+
Extract blue curves from a BGR numpy image array.
|
| 18 |
+
This is the core image processing function for contour extraction.
|
| 19 |
+
"""
|
| 20 |
+
if img_bgr_np is None:
|
| 21 |
+
print("Input image numpy array is None.")
|
| 22 |
+
return []
|
| 23 |
+
|
| 24 |
+
# Convert BGR to HSV for better color segmentation
|
| 25 |
+
hsv = cv2.cvtColor(img_bgr_np, cv2.COLOR_BGR2HSV)
|
| 26 |
+
|
| 27 |
+
# Define range for blue color in HSV
|
| 28 |
+
lower_blue = np.array([90, 50, 50])
|
| 29 |
+
upper_blue = np.array([130, 255, 255])
|
| 30 |
+
mask = cv2.inRange(hsv, lower_blue, upper_blue)
|
| 31 |
+
|
| 32 |
+
# Find contours from the mask
|
| 33 |
+
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 34 |
+
|
| 35 |
+
# Sort contours by their x-coordinate to maintain a consistent order
|
| 36 |
+
contours = sorted(contours, key=lambda c: cv2.boundingRect(c)[0])
|
| 37 |
+
|
| 38 |
+
# Filter out small contours that might be noise
|
| 39 |
+
return [c for c in contours if cv2.contourArea(c) > 20]
|
| 40 |
+
|
| 41 |
+
def simplify_contour_high_fidelity(contour):
|
| 42 |
+
"""
|
| 43 |
+
Simplify contour using Ramer-Douglas-Peucker algorithm while keeping high fidelity.
|
| 44 |
+
This reduces the number of points in the contour without losing significant shape details.
|
| 45 |
+
"""
|
| 46 |
+
epsilon = 0.001 * cv2.arcLength(contour, True) # Smaller epsilon for higher fidelity
|
| 47 |
+
approx = cv2.approxPolyDP(contour, epsilon, False)
|
| 48 |
+
return [[int(p[0][0]), int(p[0][1])] for p in approx]
|
| 49 |
+
|
| 50 |
+
def calculate_path_normals(curve_points):
|
| 51 |
"""
|
| 52 |
+
Calculate normal vectors for each point on the path.
|
| 53 |
+
These normals are initially perpendicular to the tangent at each point.
|
| 54 |
"""
|
| 55 |
+
normals = []
|
| 56 |
+
|
| 57 |
+
for i in range(len(curve_points)):
|
| 58 |
+
# Get previous and next points to calculate the tangent vector
|
| 59 |
+
# Handle wrap-around for closed curves
|
| 60 |
+
if i == 0:
|
| 61 |
+
prev_point = curve_points[-1] if len(curve_points) > 2 else curve_points[i]
|
| 62 |
+
next_point = curve_points[i + 1]
|
| 63 |
+
elif i == len(curve_points) - 1:
|
| 64 |
+
prev_point = curve_points[i - 1]
|
| 65 |
+
next_point = curve_points[0] if len(curve_points) > 2 else curve_points[i]
|
| 66 |
+
else:
|
| 67 |
+
prev_point = curve_points[i - 1]
|
| 68 |
+
next_point = curve_points[i + 1]
|
| 69 |
|
| 70 |
+
# Calculate tangent vector (vector from previous to next point)
|
| 71 |
+
tx = next_point[0] - prev_point[0]
|
| 72 |
+
ty = next_point[1] - prev_point[1]
|
| 73 |
|
| 74 |
+
# Normalize tangent vector
|
| 75 |
+
length = math.sqrt(tx*tx + ty*ty)
|
| 76 |
+
if length > 0:
|
| 77 |
+
tx /= length
|
| 78 |
+
ty /= length
|
| 79 |
|
| 80 |
+
# Calculate normal vector (perpendicular to tangent)
|
| 81 |
+
# (nx, ny) is (-ty, tx) or (ty, -tx)
|
| 82 |
+
nx = -ty
|
| 83 |
+
ny = tx
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
+
normals.append((nx, ny))
|
| 86 |
+
|
| 87 |
+
return normals
|
| 88 |
+
|
| 89 |
+
def determine_shape_center(curve_points):
|
| 90 |
+
"""
|
| 91 |
+
Calculate the approximate center of the shape using the centroid of its points.
|
| 92 |
+
This is used to determine the "inward" direction for normals.
|
| 93 |
+
"""
|
| 94 |
+
if not curve_points:
|
| 95 |
+
return (0, 0)
|
| 96 |
+
|
| 97 |
+
# Calculate centroid
|
| 98 |
+
cx = sum(p[0] for p in curve_points) / len(curve_points)
|
| 99 |
+
cy = sum(p[1] for p in curve_points) / len(curve_points)
|
| 100 |
+
return (cx, cy)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
+
def adjust_normals_for_inward_direction(curve_points, normals):
|
| 103 |
"""
|
| 104 |
+
Adjust normal vectors to point inward toward the shape center.
|
| 105 |
+
This is crucial for positioning elements inside the curve.
|
| 106 |
"""
|
| 107 |
+
center = determine_shape_center(curve_points)
|
| 108 |
+
adjusted_normals = []
|
| 109 |
|
| 110 |
+
for i, (nx, ny) in enumerate(normals):
|
| 111 |
+
px, py = curve_points[i]
|
| 112 |
+
|
| 113 |
+
# Vector from the current point on the curve to the shape's center
|
| 114 |
+
to_center_x = center[0] - px
|
| 115 |
+
to_center_y = center[1] - py
|
| 116 |
+
|
| 117 |
+
# Check if the normal points towards the center using the dot product
|
| 118 |
+
# If dot product is negative, the normal points away from the center, so flip it
|
| 119 |
+
dot_product = nx * to_center_x + ny * to_center_y
|
| 120 |
+
|
| 121 |
+
if dot_product < 0:
|
| 122 |
+
nx = -nx
|
| 123 |
+
ny = -ny
|
| 124 |
+
|
| 125 |
+
adjusted_normals.append((nx, ny))
|
| 126 |
+
|
| 127 |
+
return adjusted_normals
|
| 128 |
+
|
| 129 |
+
# --- Ultra-Dense Point Generation ---
|
| 130 |
+
|
| 131 |
+
def ultra_dense_poisson_sampling(width, height, radius, mask=None, max_attempts=100):
|
| 132 |
+
"""
|
| 133 |
+
Ultra-dense Poisson disk sampling for distributing points evenly with a minimum distance.
|
| 134 |
+
This version is modified to generate fewer, more spread-out points by increasing the radius.
|
| 135 |
+
"""
|
| 136 |
+
|
| 137 |
+
cell_size = radius / math.sqrt(2) # Size of grid cells for efficient neighbor lookup
|
| 138 |
+
grid = {} # Stores points by their grid cell
|
| 139 |
+
points = [] # List of accepted points
|
| 140 |
+
active_list = [] # Points from which new candidates are generated
|
| 141 |
+
|
| 142 |
+
if mask is not None:
|
| 143 |
+
valid_coords = np.where(mask > 0) # Get all coordinates where the mask is non-zero
|
| 144 |
+
if len(valid_coords[0]) == 0:
|
| 145 |
+
return points
|
| 146 |
+
|
| 147 |
+
# Strategy: Place a few initial seed points strategically within the mask
|
| 148 |
+
num_seeds = min(20, len(valid_coords[0]) // 50 + 1)
|
| 149 |
+
|
| 150 |
+
# Randomly select indices for potential seed points
|
| 151 |
+
indices = np.random.choice(len(valid_coords[0]), min(num_seeds * 5, len(valid_coords[0])), replace=False)
|
| 152 |
+
|
| 153 |
+
for idx in indices:
|
| 154 |
+
if len(points) >= num_seeds:
|
| 155 |
+
break
|
| 156 |
+
|
| 157 |
+
start_point = (float(valid_coords[1][idx]), float(valid_coords[0][idx]))
|
| 158 |
+
|
| 159 |
+
# Ensure seed points are not too close to each other
|
| 160 |
+
too_close = False
|
| 161 |
+
for existing_point in points:
|
| 162 |
+
dist = math.sqrt((start_point[0] - existing_point[0])**2 + (start_point[1] - existing_point[1])**2)
|
| 163 |
+
if dist < radius * 0.8: # Slightly closer for seeds to ensure initial spread
|
| 164 |
+
too_close = True
|
| 165 |
+
break
|
| 166 |
|
| 167 |
+
if not too_close:
|
| 168 |
+
points.append(start_point)
|
| 169 |
+
active_list.append(start_point)
|
| 170 |
+
|
| 171 |
+
grid_x = int(start_point[0] // cell_size)
|
| 172 |
+
grid_y = int(start_point[1] // cell_size)
|
| 173 |
+
grid[(grid_y, grid_x)] = start_point
|
| 174 |
+
|
| 175 |
+
# Aggressive expansion: try to find new points from active points
|
| 176 |
+
attempts = 0
|
| 177 |
+
max_total_attempts = 200000 # Increased limit for thoroughness
|
| 178 |
+
successful_placements = 0
|
| 179 |
+
|
| 180 |
+
while active_list and attempts < max_total_attempts:
|
| 181 |
+
attempts += 1
|
| 182 |
+
|
| 183 |
+
# Prefer newer points (at the end of the list) as they are more likely to have space around them
|
| 184 |
+
random_index = random.randint(0, len(active_list) - 1)
|
| 185 |
+
current_point = active_list[random_index]
|
| 186 |
+
found_valid = False
|
| 187 |
|
| 188 |
+
for attempt in range(max_attempts):
|
| 189 |
+
# Generate a candidate point in an annulus around the current point
|
| 190 |
+
angle = random.uniform(0, 2 * math.pi)
|
| 191 |
+
distance = random.uniform(radius, 2 * radius) # Annulus between r and 2r
|
| 192 |
+
|
| 193 |
+
new_x = current_point[0] + distance * math.cos(angle)
|
| 194 |
+
new_y = current_point[1] + distance * math.sin(angle)
|
| 195 |
+
|
| 196 |
+
# Check if the new point is within image bounds
|
| 197 |
+
if new_x < 0 or new_x >= width or new_y < 0 or new_y >= height:
|
| 198 |
+
continue
|
| 199 |
+
|
| 200 |
+
# Check if the new point is within the mask
|
| 201 |
+
if mask is not None:
|
| 202 |
+
mask_x = int(round(new_x))
|
| 203 |
+
mask_y = int(round(new_y))
|
| 204 |
+
if (mask_y >= mask.shape[0] or mask_x >= mask.shape[1] or
|
| 205 |
+
mask[mask_y, mask_x] == 0): # Check if pixel is part of the masked area
|
| 206 |
+
continue
|
| 207 |
+
|
| 208 |
+
# Check minimum distance to existing points using the grid
|
| 209 |
+
grid_x = int(new_x // cell_size)
|
| 210 |
+
grid_y = int(new_y // cell_size)
|
| 211 |
+
|
| 212 |
+
too_close = False
|
| 213 |
+
# Check neighboring cells (2 cells in each direction)
|
| 214 |
+
for dy in range(-2, 3):
|
| 215 |
+
for dx in range(-2, 3):
|
| 216 |
+
check_key = (grid_y + dy, grid_x + dx)
|
| 217 |
+
if check_key in grid:
|
| 218 |
+
existing_point = grid[check_key]
|
| 219 |
+
dist = math.sqrt((new_x - existing_point[0])**2 + (new_y - existing_point[1])**2)
|
| 220 |
+
if dist < radius: # If any existing point is within 'radius' distance, it's too close
|
| 221 |
+
too_close = True
|
| 222 |
+
break
|
| 223 |
+
if too_close:
|
| 224 |
+
break
|
| 225 |
+
|
| 226 |
+
if not too_close:
|
| 227 |
+
new_point = (new_x, new_y)
|
| 228 |
+
points.append(new_point)
|
| 229 |
+
active_list.append(new_point)
|
| 230 |
+
grid[(grid_y, grid_x)] = new_point
|
| 231 |
+
found_valid = True
|
| 232 |
+
successful_placements += 1
|
| 233 |
+
break # Found a valid point, move to the next active point
|
| 234 |
+
|
| 235 |
+
if not found_valid:
|
| 236 |
+
# If no valid point was found after max_attempts, remove current_point from active_list
|
| 237 |
+
active_list.pop(random_index)
|
| 238 |
+
|
| 239 |
+
return points
|
| 240 |
+
|
| 241 |
+
def multi_scale_grid_sampling(width, height, mask=None):
|
| 242 |
+
"""
|
| 243 |
+
Multi-scale grid sampling for maximum coverage, used as a fallback.
|
| 244 |
+
This version uses larger grid spacings to generate fewer, more spread-out points.
|
| 245 |
+
"""
|
| 246 |
+
|
| 247 |
+
all_points = []
|
| 248 |
+
|
| 249 |
+
# Multiple grid scales with larger spacings
|
| 250 |
+
spacings = [25, 35, 45] # Different, larger grid spacings
|
| 251 |
+
|
| 252 |
+
for spacing in spacings:
|
| 253 |
+
points_this_scale = []
|
| 254 |
+
jitter = spacing * 0.3 # Less jitter for more structured placement
|
| 255 |
+
|
| 256 |
+
y = spacing // 2
|
| 257 |
+
while y < height - spacing // 2:
|
| 258 |
+
x = spacing // 2
|
| 259 |
+
while x < width - spacing // 2:
|
| 260 |
+
# Add random jitter to the grid point for a more organic look
|
| 261 |
+
jx = x + random.uniform(-jitter, jitter)
|
| 262 |
+
jy = y + random.uniform(-jitter, jitter)
|
| 263 |
|
| 264 |
+
# Bounds check
|
| 265 |
+
if jx < 0 or jx >= width or jy < 0 or jy >= height:
|
| 266 |
+
x += spacing
|
| 267 |
+
continue
|
|
|
|
| 268 |
|
| 269 |
+
# Mask check: ensure point is within the masked area
|
| 270 |
+
if mask is not None:
|
| 271 |
+
mask_x = int(round(jx))
|
| 272 |
+
mask_y = int(round(jy))
|
| 273 |
+
if (mask_y >= mask.shape[0] or mask_x >= mask.shape[1] or
|
| 274 |
+
mask[mask_y, mask_x] == 0):
|
| 275 |
+
x += spacing
|
| 276 |
+
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
|
| 278 |
+
# Check distance to existing points from all scales to avoid overlaps
|
| 279 |
+
too_close = False
|
| 280 |
+
min_dist_for_this_scale = spacing * 0.8 # Minimum distance for this scale
|
| 281 |
+
|
| 282 |
+
for existing_point in all_points:
|
| 283 |
+
dist = math.sqrt((jx - existing_point[0])**2 + (jy - existing_point[1])**2)
|
| 284 |
+
if dist < min_dist_for_this_scale:
|
| 285 |
+
too_close = True
|
| 286 |
+
break
|
|
|
|
|
|
|
|
|
|
| 287 |
|
| 288 |
+
if not too_close:
|
| 289 |
+
points_this_scale.append((jx, jy))
|
| 290 |
+
all_points.append((jx, jy)) # Add to the global list of points
|
|
|
|
|
|
|
| 291 |
|
| 292 |
+
x += spacing
|
| 293 |
+
y += spacing
|
| 294 |
+
|
| 295 |
+
return all_points
|
| 296 |
+
|
| 297 |
+
def create_varied_rectangles(points, mask, curve_points, base_size=15, offset_distance=15):
|
| 298 |
+
"""
|
| 299 |
+
Create highly varied organic rectangles positioned on the inner side of the path.
|
| 300 |
+
Rectangles are larger and fewer, aiming for coverage with fewer elements.
|
| 301 |
+
"""
|
| 302 |
+
rectangles = []
|
| 303 |
+
|
| 304 |
+
if len(points) < 1:
|
| 305 |
+
return rectangles
|
| 306 |
+
|
| 307 |
+
# Calculate path normals for inward positioning if curve points are available
|
| 308 |
+
if curve_points:
|
| 309 |
+
normals = calculate_path_normals(curve_points)
|
| 310 |
+
adjusted_normals = adjust_normals_for_inward_direction(curve_points, normals)
|
| 311 |
+
else:
|
| 312 |
+
normals = []
|
| 313 |
+
adjusted_normals = []
|
| 314 |
+
|
| 315 |
+
# Calculate local density (optional, for more nuanced sizing)
|
| 316 |
+
if len(points) > 10:
|
| 317 |
+
point_array = np.array(points)
|
| 318 |
+
distances = cdist(point_array, point_array)
|
| 319 |
+
else:
|
| 320 |
+
distances = None
|
| 321 |
+
|
| 322 |
+
for i, (px, py) in enumerate(points):
|
| 323 |
+
# Size variation based on local density or random factor
|
| 324 |
+
if distances is not None and i < len(distances):
|
| 325 |
+
distances_to_point = distances[i]
|
| 326 |
+
# Find average distance to a few nearest neighbors
|
| 327 |
+
nearest_distances = sorted(distances_to_point[distances_to_point > 0])[:5]
|
| 328 |
+
|
| 329 |
+
if nearest_distances:
|
| 330 |
+
avg_neighbor_dist = sum(nearest_distances) / len(nearest_distances)
|
| 331 |
+
# Adjust size factor based on neighbor distance relative to a reference (e.g., 20)
|
| 332 |
+
size_factor = max(0.5, min(1.5, avg_neighbor_dist / 20))
|
| 333 |
+
else:
|
| 334 |
+
size_factor = random.uniform(0.8, 1.2) # Default random factor
|
| 335 |
+
else:
|
| 336 |
+
size_factor = random.uniform(0.8, 1.2) # Default random factor
|
| 337 |
+
|
| 338 |
+
# More varied rectangle dimensions based on base_size and size_factor
|
| 339 |
+
# Adjusted ranges to make rectangles taller and narrower
|
| 340 |
+
rect_width = base_size * size_factor * random.uniform(0.3, 0.6) # Slightly wider min width
|
| 341 |
+
rect_height = base_size * size_factor * random.uniform(1.5, 3.0) # Slightly shorter max height
|
| 342 |
+
|
| 343 |
+
# Find closest path point for normal direction to position the rectangle
|
| 344 |
+
current_offset_x, current_offset_y = 0, 0
|
| 345 |
+
if curve_points and adjusted_normals:
|
| 346 |
+
min_dist_to_curve = float('inf')
|
| 347 |
+
closest_normal = (0, 0) # Default if no close point found
|
| 348 |
+
|
| 349 |
+
for j, (cpx, cpy) in enumerate(curve_points):
|
| 350 |
+
dist = math.sqrt((px - cpx)**2 + (py - cpy)**2)
|
| 351 |
+
if dist < min_dist_to_curve:
|
| 352 |
+
min_dist_to_curve = dist
|
| 353 |
+
closest_normal = adjusted_normals[j]
|
| 354 |
+
|
| 355 |
+
# Position rectangle on the inner side using the normal vector and offset distance
|
| 356 |
+
current_offset_x = closest_normal[0] * offset_distance
|
| 357 |
+
current_offset_y = closest_normal[1] * offset_distance
|
| 358 |
+
|
| 359 |
+
# Calculate rectangle angle based on path tangent or local flow
|
| 360 |
+
angle = 0
|
| 361 |
+
if curve_points:
|
| 362 |
+
# Find closest path segment for tangent calculation
|
| 363 |
+
min_dist_for_angle = float('inf')
|
| 364 |
+
|
| 365 |
+
for j in range(len(curve_points)):
|
| 366 |
+
cpx, cpy = curve_points[j]
|
| 367 |
+
dist = math.sqrt((px - cpx)**2 + (py - cpy)**2)
|
| 368 |
|
| 369 |
+
if dist < min_dist_for_angle:
|
| 370 |
+
min_dist_for_angle = dist
|
| 371 |
+
|
| 372 |
+
# Calculate tangent angle at this point
|
| 373 |
+
if j < len(curve_points) - 1:
|
| 374 |
+
next_point = curve_points[j + 1]
|
| 375 |
+
dx = next_point[0] - cpx
|
| 376 |
+
dy = next_point[1] - cpy
|
| 377 |
+
elif j > 0:
|
| 378 |
+
prev_point = curve_points[j - 1]
|
| 379 |
+
dx = cpx - prev_point[0]
|
| 380 |
+
dy = cpy - prev_point[1]
|
| 381 |
+
else:
|
| 382 |
+
dx, dy = 1, 0 # Default for single point or edge case
|
| 383 |
+
|
| 384 |
+
# Angle aligned with path tangent (height of rectangle along tangent)
|
| 385 |
+
# Add some random jitter to the angle for more organic feel
|
| 386 |
+
angle = math.atan2(dy, dx) + random.uniform(-0.1, 0.1)
|
| 387 |
+
else:
|
| 388 |
+
# Fallback: complex orientation patterns if no curve points
|
| 389 |
+
angle = (math.sin(px * 0.01) * 0.5 +
|
| 390 |
+
math.cos(py * 0.012) * 0.4 +
|
| 391 |
+
math.sin((px + py) * 0.008) * 0.3 +
|
| 392 |
+
random.uniform(-0.5, 0.5))
|
| 393 |
+
|
| 394 |
+
rectangles.append({
|
| 395 |
+
'x': px + current_offset_x,
|
| 396 |
+
'y': py + current_offset_y,
|
| 397 |
+
'width': rect_width,
|
| 398 |
+
'height': rect_height,
|
| 399 |
+
'angle': angle,
|
| 400 |
+
'size_factor': size_factor
|
| 401 |
+
})
|
| 402 |
+
|
| 403 |
+
return rectangles
|
| 404 |
+
|
| 405 |
+
def generate_id(prefix="el"):
|
| 406 |
+
"""Generate a random ID for Excalidraw elements."""
|
| 407 |
+
return f"{prefix}_{''.join(random.choices('abcdefghijklmnopqrstuvwxyz013456789', k=9))}"
|
| 408 |
+
|
| 409 |
+
# --- Gradio UI Function ---
|
| 410 |
+
|
| 411 |
+
def generate_excalidraw_json_from_image(image_np):
|
| 412 |
+
"""
|
| 413 |
+
Main function to generate Excalidraw JSON from an uploaded image.
|
| 414 |
+
This function is called by the Gradio interface.
|
| 415 |
+
"""
|
| 416 |
+
if image_np is None:
|
| 417 |
+
return "Please upload an image to generate Excalidraw elements."
|
| 418 |
+
|
| 419 |
+
# Gradio's image input is typically RGB, OpenCV expects BGR
|
| 420 |
+
img_bgr = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR)
|
| 421 |
+
|
| 422 |
+
# Extract contours from the image
|
| 423 |
+
contours = extract_curves_from_image_np(img_bgr)
|
| 424 |
+
all_curves_points = [simplify_contour_high_fidelity(c) for c in contours]
|
| 425 |
+
|
| 426 |
+
final_elements = []
|
| 427 |
+
BASE_X, BASE_Y = 100, 100 # Base offset for all elements in Excalidraw
|
| 428 |
+
|
| 429 |
+
# Generate outline elements
|
| 430 |
+
for i, curve_points in enumerate(all_curves_points):
|
| 431 |
+
if not curve_points: # Skip empty curves
|
| 432 |
+
continue
|
| 433 |
+
start_x, start_y = curve_points[0][0], curve_points[0][1]
|
| 434 |
+
relative_points = [[p[0] - start_x, p[1] - start_y] for p in curve_points]
|
| 435 |
+
|
| 436 |
+
outline_element = {
|
| 437 |
+
"id": generate_id("outline"), "type": "line",
|
| 438 |
+
"x": float(BASE_X + start_x), "y": float(BASE_Y + start_y), "angle": 0,
|
| 439 |
+
"strokeColor": "#00aaff", "backgroundColor": "transparent", "fillStyle": "solid",
|
| 440 |
+
"strokeWidth": 4, "strokeStyle": "solid", "roughness": 1, "opacity": 100,
|
| 441 |
+
"roundness": {"type": 2}, "points": relative_points,
|
| 442 |
+
"seed": random.randint(1000, 1000000), "version": 1, "versionNonce": random.randint(1000, 1000000)
|
| 443 |
+
}
|
| 444 |
+
final_elements.append(outline_element)
|
| 445 |
+
|
| 446 |
+
# Generate ultra-dense interior fill with rectangles if contours exist
|
| 447 |
+
if contours:
|
| 448 |
+
|
| 449 |
+
# Create a bounding box that encompasses all contours
|
| 450 |
+
x_min = min([cv2.boundingRect(c)[0] for c in contours])
|
| 451 |
+
y_min = min([cv2.boundingRect(c)[1] for c in contours])
|
| 452 |
+
x_max = max([cv2.boundingRect(c)[0] + cv2.boundingRect(c)[2] for c in contours])
|
| 453 |
+
y_max = max([cv2.boundingRect(c)[1] + cv2.boundingRect(c)[3] for c in contours])
|
| 454 |
+
|
| 455 |
+
# Create a mask image to define the valid area for point sampling
|
| 456 |
+
mask_width = x_max + 40
|
| 457 |
+
mask_height = y_max + 40
|
| 458 |
+
mask = np.zeros((mask_height, mask_width), dtype=np.uint8)
|
| 459 |
+
|
| 460 |
+
cv2.drawContours(mask, contours, -1, 255, thickness=cv2.FILLED)
|
| 461 |
+
|
| 462 |
+
kernel = np.ones((3,3), np.uint8)
|
| 463 |
+
mask = cv2.erode(mask, kernel, iterations=1)
|
| 464 |
+
|
| 465 |
+
min_distance_between_points = 25
|
| 466 |
+
sample_points = ultra_dense_poisson_sampling(mask.shape[1], mask.shape[0], min_distance_between_points, mask)
|
| 467 |
+
|
| 468 |
+
if len(sample_points) < 70:
|
| 469 |
+
grid_points = multi_scale_grid_sampling(mask.shape[1], mask.shape[0], mask)
|
| 470 |
+
|
| 471 |
+
for gp in grid_points:
|
| 472 |
+
too_close = False
|
| 473 |
+
for sp in sample_points:
|
| 474 |
+
if math.sqrt((gp[0] - sp[0])**2 + (gp[1] - sp[0])**2) < min_distance_between_points * 0.8:
|
| 475 |
+
too_close = True
|
| 476 |
+
break
|
| 477 |
+
if not too_close:
|
| 478 |
+
sample_points.append(gp)
|
| 479 |
+
|
| 480 |
+
all_path_points = []
|
| 481 |
+
for curve in all_curves_points:
|
| 482 |
+
all_path_points.extend(curve)
|
| 483 |
+
|
| 484 |
+
rectangles = create_varied_rectangles(sample_points, mask, all_path_points,
|
| 485 |
+
base_size=15, offset_distance=15)
|
| 486 |
+
|
| 487 |
+
# Expanded color schemes for more variety
|
| 488 |
+
color_schemes = [
|
| 489 |
+
# Warm tones
|
| 490 |
+
('#ff6b6b', '#feca57', '#ff9ff3', '#ffbe76', '#f0932b'),
|
| 491 |
+
# Cool tones
|
| 492 |
+
('#74b9ff', '#00cec9', '#6c5ce7', '#a29bfe', '#81ecec'),
|
| 493 |
+
# Sunset/Earthy tones
|
| 494 |
+
('#fd79a8', '#fdcb6e', '#e17055', '#d63031', '#ffeaa7'),
|
| 495 |
+
# Green/Nature tones
|
| 496 |
+
('#55efc4', '#00b894', '#00d2d3', '#00b894', '#006266'),
|
| 497 |
+
# Muted/Pastel tones
|
| 498 |
+
('#a4b0be', '#dfe6e9', '#b2bec3', '#636e72', '#2d3436'),
|
| 499 |
+
# Vibrant tones
|
| 500 |
+
('#ff7675', '#fdcb6e', '#a29bfe', '#ffeaa7', '#55efc4'),
|
| 501 |
+
# Darker, richer tones
|
| 502 |
+
('#2c3e50', '#34495e', '#7f8c8d', '#95a5a6', '#bdc3c7')
|
| 503 |
+
]
|
| 504 |
+
|
| 505 |
+
scheme = random.choice(color_schemes)
|
| 506 |
+
|
| 507 |
+
for i, rect in enumerate(rectangles):
|
| 508 |
+
base_color_hex = random.choice(scheme)
|
| 509 |
+
r = int(base_color_hex[1:3], 16)
|
| 510 |
+
g = int(base_color_hex[3:5], 16)
|
| 511 |
+
b = int(base_color_hex[5:7], 16)
|
| 512 |
+
|
| 513 |
+
r = max(0, min(255, r + random.randint(-30, 30)))
|
| 514 |
+
g = max(0, min(255, g + random.randint(-30, 30)))
|
| 515 |
+
b = max(0, min(255, b + random.randint(-30, 30)))
|
| 516 |
+
|
| 517 |
+
bg_color = f"#{r:02x}{g:02x}{b:02x}"
|
| 518 |
+
|
| 519 |
+
base_opacity = 60 + (rect['size_factor'] - 0.8) * 20
|
| 520 |
+
opacity = max(40, min(90, base_opacity + random.randint(-15, 15)))
|
| 521 |
+
|
| 522 |
+
fill_styles = ['solid', 'cross-hatch', 'dots', 'hachure']
|
| 523 |
+
weights = [0.6, 0.2, 0.1, 0.1]
|
| 524 |
+
fill_style = random.choices(fill_styles, weights=weights)[0]
|
| 525 |
+
|
| 526 |
+
# Adjusted stroke weights: more likely to have a stroke, with varied thickness
|
| 527 |
+
stroke_width = random.uniform(0.5, 2.0) if random.random() < 0.95 else 0
|
| 528 |
+
|
| 529 |
+
rect_x = BASE_X + rect['x'] - rect['width'] / 2
|
| 530 |
+
rect_y = BASE_Y + rect['y'] - rect['height'] / 2
|
| 531 |
+
|
| 532 |
+
rect_element = {
|
| 533 |
+
"id": generate_id("rect_dense"),
|
| 534 |
+
"type": "rectangle",
|
| 535 |
+
"x": float(rect_x),
|
| 536 |
+
"y": float(rect_y),
|
| 537 |
+
"width": rect['width'],
|
| 538 |
+
"height": rect['height'],
|
| 539 |
+
"angle": rect['angle'],
|
| 540 |
+
"strokeColor": "#8e9094",
|
| 541 |
+
"backgroundColor": bg_color,
|
| 542 |
+
"fillStyle": fill_style,
|
| 543 |
+
"strokeWidth": stroke_width,
|
| 544 |
+
"strokeStyle": "solid",
|
| 545 |
+
"roughness": random.uniform(0.7, 1.5),
|
| 546 |
+
"opacity": opacity,
|
| 547 |
+
"roundness": {"type": 3},
|
| 548 |
+
"seed": random.randint(1000, 1000000),
|
| 549 |
+
"version": 1,
|
| 550 |
+
"versionNonce": random.randint(1000, 1000000)
|
| 551 |
+
}
|
| 552 |
+
final_elements.append(rect_element)
|
| 553 |
+
else:
|
| 554 |
+
# If no contours found from image, use fallback data for rectangles too
|
| 555 |
+
# This part ensures that even if the image processing fails, something is generated.
|
| 556 |
+
print("⚠️ No contours found from uploaded image for rectangle generation. Using fallback data.")
|
| 557 |
+
fallback_all_curves_points = [
|
| 558 |
+
[[194, 53],[192, 81],[191, 108],[191, 137],[192, 166],[192, 195],[194, 224],[197, 252],[200, 281],[205, 309],[210, 336],[214, 362],[219, 390],[225, 423]],
|
| 559 |
+
[[230, 429],[239, 452],[249, 475],[258, 497],[271, 519],[287, 539],[304, 553],[320, 560],[341, 569],[365, 576],[394, 580],[425, 580],[455, 574],[484, 564],[513, 552],[538, 546],[565, 523],[590, 499],[620, 465],[639, 432],[656, 396],[656, 363],[657, 329],[657, 294],[657, 260],[649, 222],[637, 185],[623, 147],[603, 110],[578, 83],[558, 71],[540, 68],[517, 59],[492, 51],[461, 46],[443, 45],[426, 46]],
|
| 560 |
+
[[426, 44], [430, 20], [434, 0]]
|
| 561 |
+
]
|
| 562 |
+
fallback_contours = [np.array(c).reshape((-1, 1, 2)).astype(np.int32) for c in fallback_all_curves_points]
|
| 563 |
+
|
| 564 |
+
# Create mask for fallback data
|
| 565 |
+
x_min_f = min([cv2.boundingRect(c)[0] for c in fallback_contours])
|
| 566 |
+
y_min_f = min([cv2.boundingRect(c)[1] for c in fallback_contours])
|
| 567 |
+
x_max_f = max([cv2.boundingRect(c)[0] + cv2.boundingRect(c)[2] for c in fallback_contours])
|
| 568 |
+
y_max_f = max([cv2.boundingRect(c)[1] + cv2.boundingRect(c)[3] for c in fallback_contours])
|
| 569 |
+
mask_width_f = x_max_f + 40
|
| 570 |
+
mask_height_f = y_max_f + 40
|
| 571 |
+
mask_f = np.zeros((mask_height_f, mask_width_f), dtype=np.uint8)
|
| 572 |
+
cv2.drawContours(mask_f, fallback_contours, -1, 255, thickness=cv2.FILLED)
|
| 573 |
+
kernel_f = np.ones((3,3), np.uint8)
|
| 574 |
+
mask_f = cv2.erode(mask_f, kernel_f, iterations=1)
|
| 575 |
+
|
| 576 |
+
min_distance_between_points = 25
|
| 577 |
+
sample_points_f = ultra_dense_poisson_sampling(mask_f.shape[1], mask_f.shape[0], min_distance_between_points, mask_f)
|
| 578 |
+
if len(sample_points_f) < 70:
|
| 579 |
+
grid_points_f = multi_scale_grid_sampling(mask_f.shape[1], mask_f.shape[0], mask_f)
|
| 580 |
+
for gp_f in grid_points_f:
|
| 581 |
+
too_close = False
|
| 582 |
+
for sp_f in sample_points_f:
|
| 583 |
+
if math.sqrt((gp_f[0] - sp_f[0])**2 + (gp_f[1] - sp_f[1])**2) < min_distance_between_points * 0.8:
|
| 584 |
+
too_close = True
|
| 585 |
+
break
|
| 586 |
+
if not too_close:
|
| 587 |
+
sample_points_f.append(gp_f)
|
| 588 |
+
|
| 589 |
+
rectangles_f = create_varied_rectangles(sample_points_f, mask_f, fallback_all_curves_points,
|
| 590 |
+
base_size=15, offset_distance=15)
|
| 591 |
+
|
| 592 |
+
# Generate elements for fallback rectangles
|
| 593 |
+
# Use a random color scheme for fallback as well
|
| 594 |
+
fallback_scheme = random.choice(color_schemes)
|
| 595 |
+
for i, rect in enumerate(rectangles_f):
|
| 596 |
+
base_color_hex = random.choice(fallback_scheme)
|
| 597 |
+
r = int(base_color_hex[1:3], 16)
|
| 598 |
+
g = int(base_color_hex[3:5], 16)
|
| 599 |
+
b = int(base_color_hex[5:7], 16)
|
| 600 |
+
r = max(0, min(255, r + random.randint(-30, 30)))
|
| 601 |
+
g = max(0, min(255, g + random.randint(-30, 30)))
|
| 602 |
+
b = max(0, min(255, b + random.randint(-30, 30)))
|
| 603 |
+
bg_color = f"#{r:02x}{g:02x}{b:02x}"
|
| 604 |
+
|
| 605 |
+
base_opacity = 60 + (rect['size_factor'] - 0.8) * 20
|
| 606 |
+
opacity = max(40, min(90, base_opacity + random.randint(-15, 15)))
|
| 607 |
+
|
| 608 |
+
fill_styles = ['solid', 'cross-hatch', 'dots', 'hachure']
|
| 609 |
+
weights = [0.6, 0.2, 0.1, 0.1]
|
| 610 |
+
fill_style = random.choices(fill_styles, weights=weights)[0]
|
| 611 |
+
|
| 612 |
+
stroke_width = random.uniform(0.5, 2.0) if random.random() < 0.95 else 0
|
| 613 |
+
|
| 614 |
+
rect_x = BASE_X + rect['x'] - rect['width'] / 2
|
| 615 |
+
rect_y = BASE_Y + rect['y'] - rect['height'] / 2
|
| 616 |
+
|
| 617 |
+
rect_element = {
|
| 618 |
+
"id": generate_id("rect_dense_fallback"), # Different ID prefix for fallback
|
| 619 |
+
"type": "rectangle",
|
| 620 |
+
"x": float(rect_x),
|
| 621 |
+
"y": float(rect_y),
|
| 622 |
+
"width": rect['width'],
|
| 623 |
+
"height": rect['height'],
|
| 624 |
+
"angle": rect['angle'],
|
| 625 |
+
"strokeColor": "#8e9094",
|
| 626 |
+
"backgroundColor": bg_color,
|
| 627 |
+
"fillStyle": fill_style,
|
| 628 |
+
"strokeWidth": stroke_width,
|
| 629 |
+
"strokeStyle": "solid",
|
| 630 |
+
"roughness": random.uniform(0.7, 1.5),
|
| 631 |
+
"opacity": opacity,
|
| 632 |
+
"roundness": {"type": 3},
|
| 633 |
+
"seed": random.randint(1000, 1000000),
|
| 634 |
+
"version": 1,
|
| 635 |
+
"versionNonce": random.randint(1000, 1000000)
|
| 636 |
+
}
|
| 637 |
+
final_elements.append(rect_element)
|
| 638 |
+
|
| 639 |
+
|
| 640 |
+
final_excalidraw_structure = {
|
| 641 |
+
"type": "excalidraw/clipboard",
|
| 642 |
+
"elements": final_elements,
|
| 643 |
+
"files": {}
|
| 644 |
+
}
|
| 645 |
+
return json.dumps(final_excalidraw_structure, indent=2)
|
| 646 |
+
|
| 647 |
+
# Gradio interface definition
|
| 648 |
+
iface = gr.Interface(
|
| 649 |
+
fn=generate_excalidraw_json_from_image,
|
| 650 |
+
inputs=gr.Image(type="numpy", label="Upload Image (Blue Curves)"),
|
| 651 |
+
outputs=gr.Textbox(label="Excalidraw JSON Output (Copy & Paste into Excalidraw)"),
|
| 652 |
+
title="Excalidraw Organic Rectangle Generator",
|
| 653 |
+
description="Upload an image containing blue curves. The tool will generate Excalidraw JSON data "
|
| 654 |
+
"with organic, aligned rectangles filling the interior of the curves. "
|
| 655 |
+
"The rectangles are designed to trace the path and avoid significant overlap."
|
| 656 |
+
)
|
| 657 |
|
| 658 |
if __name__ == "__main__":
|
| 659 |
+
iface.launch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|