AkshitShubham commited on
Commit
5ddec87
·
verified ·
1 Parent(s): ea23993

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +637 -186
app.py CHANGED
@@ -1,208 +1,659 @@
1
- import gradio as gr
2
- import requests
3
- import os
4
- import base64
5
- from io import BytesIO
6
- from PIL import Image
7
  import json
 
 
 
 
8
 
9
- def generate_image(prompt, cfg_scale, width, height, seed, steps, input_image=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  """
11
- Generate image using NVIDIA's Flux.1-dev API
 
12
  """
13
- try:
14
- # Get API key from environment
15
- api_key = os.getenv("NVIDIA_API_KEY")
16
- if not api_key:
17
- return None, "Error: NVIDIA_API_KEY environment variable not set"
 
 
 
 
 
 
 
 
 
18
 
19
- # API endpoint
20
- invoke_url = "https://ai.api.nvidia.com/v1/genai/black-forest-labs/flux.1-dev"
 
21
 
22
- # Headers
23
- headers = {
24
- "Authorization": f"Bearer {api_key}",
25
- "Accept": "application/json",
26
- }
27
 
28
- # Prepare payload
29
- payload = {
30
- "prompt": prompt,
31
- "cfg_scale": cfg_scale,
32
- "width": int(width),
33
- "height": int(height),
34
- "seed": int(seed) if seed != -1 else 0,
35
- "steps": int(steps)
36
- }
37
 
38
- # Handle input image for img2img or control modes
39
- if input_image is not None:
40
- # Convert PIL image to base64
41
- buffered = BytesIO()
42
- input_image.save(buffered, format="PNG")
43
- img_str = base64.b64encode(buffered.getvalue()).decode()
44
- payload["image"] = f"data:image/png;base64,{img_str}"
45
- payload["mode"] = "canny" # You can make this configurable
46
-
47
- # Make API request
48
- response = requests.post(invoke_url, headers=headers, json=payload, timeout=120)
49
- response.raise_for_status()
50
-
51
- response_body = response.json()
52
-
53
- # Check if response contains image data
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 create_gradio_interface():
75
  """
76
- Create and configure the Gradio interface
 
77
  """
 
 
78
 
79
- with gr.Blocks(title="NVIDIA Flux.1-dev Image Generator", theme=gr.themes.Soft()) as demo:
80
- gr.Markdown(
81
- """
82
- # 🎨 NVIDIA Flux.1-dev Image Generator
83
- Generate high-quality images using NVIDIA's Flux.1-dev model via their API.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
- **Requirements:** Set your `NVIDIA_API_KEY` environment variable before running.
86
- """
87
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
- with gr.Row():
90
- with gr.Column(scale=1):
91
- # Input controls
92
- prompt = gr.Textbox(
93
- label="Prompt",
94
- placeholder="Describe the image you want to generate...",
95
- lines=3,
96
- value="a simple coffee shop interior"
97
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
- input_image = gr.Image(
100
- label="Input Image (optional - for img2img/control)",
101
- type="pil",
102
- height=300
103
- )
104
 
105
- with gr.Row():
106
- cfg_scale = gr.Slider(
107
- minimum=1.0,
108
- maximum=20.0,
109
- value=3.5,
110
- step=0.5,
111
- label="CFG Scale",
112
- info="Higher values follow prompt more closely"
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
- with gr.Row():
125
- width = gr.Dropdown(
126
- choices=[512, 768, 1024, 1280],
127
- value=1024,
128
- label="Width"
129
- )
130
-
131
- height = gr.Dropdown(
132
- choices=[512, 768, 1024, 1280],
133
- value=1024,
134
- label="Height"
135
- )
136
 
137
- seed = gr.Number(
138
- label="Seed",
139
- value=-1,
140
- info="Use -1 for random seed"
141
- )
142
 
143
- generate_btn = gr.Button(
144
- "🎨 Generate Image",
145
- variant="primary",
146
- size="lg"
147
- )
148
-
149
- with gr.Column(scale=1):
150
- # Output
151
- output_image = gr.Image(
152
- label="Generated Image",
153
- type="pil",
154
- height=500
155
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
- status_text = gr.Textbox(
158
- label="Status",
159
- lines=3,
160
- interactive=False
161
- )
162
-
163
- # Example prompts
164
- gr.Markdown("### 💡 Example Prompts")
165
- examples = gr.Examples(
166
- examples=[
167
- ["a cozy coffee shop with warm lighting and vintage furniture"],
168
- ["a futuristic cityscape at sunset with flying cars"],
169
- ["a magical forest with glowing mushrooms and fairy lights"],
170
- ["a minimalist modern kitchen with marble countertops"],
171
- ["a steampunk laboratory with brass machinery and glowing tubes"],
172
- ["a serene mountain lake reflecting snow-capped peaks"],
173
- ],
174
- inputs=[prompt]
175
- )
176
-
177
- # Event handlers
178
- generate_btn.click(
179
- fn=generate_image,
180
- inputs=[prompt, cfg_scale, width, height, seed, steps, input_image],
181
- outputs=[output_image, status_text]
182
- )
183
-
184
- # Keyboard shortcut
185
- prompt.submit(
186
- fn=generate_image,
187
- inputs=[prompt, cfg_scale, width, height, seed, steps, input_image],
188
- outputs=[output_image, status_text]
189
- )
190
-
191
- return demo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
 
193
  if __name__ == "__main__":
194
- # Check if API key is set
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()