AMontiB commited on
Commit
09cf71e
·
1 Parent(s): 6af078d
Files changed (3) hide show
  1. app.py +7 -28
  2. requirements.txt +4 -47
  3. shadow.py +110 -139
app.py CHANGED
@@ -1,45 +1,25 @@
1
- import gradio as gr
2
-
3
- def dummy_function(image):
4
- return "Analysis complete"
5
-
6
- iface = gr.Interface(
7
- fn=dummy_function,
8
- inputs=gr.Image(type="filepath"),
9
- outputs="text",
10
- title="Test Interface"
11
- )
12
-
13
- iface.launch()
14
-
15
- '''
16
  import os
17
  os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
18
- os.environ["GRADIO_IS_E2E_TEST"] = "True"
19
-
20
- # Fix for the JSON schema issue
21
- import warnings
22
- warnings.filterwarnings("ignore", category=DeprecationWarning)
23
 
24
  import gradio as gr
25
 
26
  # Import the UI-creation functions from your tool scripts
27
  import CFA as CFA_tool
28
  import JPEG_Ghost as JPEG_Ghost_tool
29
- #import PRNU as PRNU_tool
30
  import shadow as shadows_tool
31
 
32
  # Create the tabbed interface
33
  demo = gr.TabbedInterface(
34
  interface_list=[
35
- CFA_tool.create_ui(),
36
- JPEG_Ghost_tool.create_ui(),
37
- #PRNU_tool.create_ui(),
38
- shadows_tool.build_gradio_interface()
39
  ],
40
  tab_names=[
41
  "🎨 CFA Analysis",
42
- "👻 JPEG Ghost",
43
  "📸 PRNU Analysis",
44
  "☀️ Shadow Analysis"
45
  ],
@@ -48,5 +28,4 @@ demo = gr.TabbedInterface(
48
 
49
  # Launch the app
50
  if __name__ == "__main__":
51
- demo.launch()
52
- '''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
 
 
 
 
 
3
 
4
  import gradio as gr
5
 
6
  # Import the UI-creation functions from your tool scripts
7
  import CFA as CFA_tool
8
  import JPEG_Ghost as JPEG_Ghost_tool
9
+ import PRNU as PRNU_tool
10
  import shadow as shadows_tool
11
 
12
  # Create the tabbed interface
13
  demo = gr.TabbedInterface(
14
  interface_list=[
15
+ CFA_tool.demo, # Use the Blocks object directly
16
+ JPEG_Ghost_tool.demo, # Use the Blocks object directly
17
+ PRNU_tool.iface, # Use the Interface object directly
18
+ shadows_tool.build_gradio_interface() # Call the function that returns the interface
19
  ],
20
  tab_names=[
21
  "🎨 CFA Analysis",
22
+ "👻 JPEG Ghost",
23
  "📸 PRNU Analysis",
24
  "☀️ Shadow Analysis"
25
  ],
 
28
 
29
  # Launch the app
30
  if __name__ == "__main__":
31
+ demo.launch()
 
requirements.txt CHANGED
@@ -1,53 +1,10 @@
1
- asttokens==2.4.1
2
- backcall==0.2.0
3
- colorama==0.4.6
4
- comm==0.2.2
5
- contourpy==1.1.1
6
- cycler==0.12.1
7
- debugpy==1.8.8
8
- decorator==5.1.1
9
- executing==2.1.0
10
- fonttools==4.55.0
11
- imagecodecs==2023.3.16
12
- imageio==2.35.1
13
- importlib-metadata==8.5.0
14
- importlib-resources==6.4.5
15
- ipykernel==6.29.5
16
- ipython==8.12.3
17
- jedi==0.19.2
18
- jupyter-client==8.6.3
19
- jupyter-core==5.7.2
20
- kiwisolver==1.4.7
21
- lazy-loader==0.4
22
- matplotlib==3.7.5
23
- matplotlib-inline==0.1.7
24
- nest-asyncio==1.6.0
25
- networkx==3.1
26
- numpy==1.24.4
27
- opencv-python==4.10.0.84
28
- packaging==24.2
29
- parso==0.8.4
30
- pickleshare==0.7.5
31
- pillow==10.4.0
32
- platformdirs==4.3.6
33
- prompt-toolkit==3.0.48
34
- psutil==6.1.0
35
- pure-eval==0.2.3
36
- pygments==2.18.0
37
- pyparsing==3.1.4
38
- python-dateutil==2.9.0.post0
39
- PyWavelets==1.4.1
40
- scikit-image==0.21.0
41
- scipy==1.10.1
42
- six==1.16.0
43
- tornado==6.4.1
44
- gradio==4.44.1
45
- huggingface_hub==0.23.4
46
  numpy==1.24.4
47
  opencv-python==4.10.0.84
48
  pillow==10.4.0
49
  scikit-image==0.21.0
50
  scipy==1.10.1
51
  matplotlib==3.7.5
52
- fastapi==0.104.1
53
- uvicorn==0.24.0
 
1
+ gradio==4.32.4
2
+ fastapi==0.104.1
3
+ uvicorn==0.24.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  numpy==1.24.4
5
  opencv-python==4.10.0.84
6
  pillow==10.4.0
7
  scikit-image==0.21.0
8
  scipy==1.10.1
9
  matplotlib==3.7.5
10
+ imageio==2.35.1
 
shadow.py CHANGED
@@ -1,12 +1,11 @@
1
  """
2
- Gradio app module for interactive vanishing-point selection.
3
- This file is intended to be imported by app.py.
4
  """
5
 
6
- import io
7
  import math
8
  import numpy as np
9
- from PIL import Image, ImageDraw, ImageFont
10
  import gradio as gr
11
  from scipy.optimize import minimize
12
 
@@ -21,13 +20,11 @@ def build_line_from_points(p1, p2):
21
  c = x1 * y2 - y1 * x2
22
  return np.array([a, b, c], dtype=float)
23
 
24
-
25
  def distance_point_to_line(pt, line):
26
  x, y = pt
27
  a, b, c = line
28
  return abs(a * x + b * y + c) / math.hypot(a, b)
29
 
30
-
31
  def total_distances(x, lines, noise_lines):
32
  """Sum of distances from candidate point x to all lines and noise lines."""
33
  pt = x
@@ -38,7 +35,6 @@ def total_distances(x, lines, noise_lines):
38
  s += distance_point_to_line(pt, Ln)
39
  return s
40
 
41
-
42
  def add_noise_lines_for_line(p1, p2, n=4, sigma=1.0):
43
  """Create a list of "noise" lines by jittering the endpoints slightly."""
44
  noise_lines = []
@@ -51,45 +47,19 @@ def add_noise_lines_for_line(p1, p2, n=4, sigma=1.0):
51
  # ------------------------- Drawing utilities ------------------------------
52
 
53
  def draw_overlay(base_pil, yellow_lines, red_lines, yellow_points, red_points, vps=None):
54
- """Return a new PIL image with overlays drawn: lines, points and vanishing points.
55
-
56
- - yellow_lines, red_lines: lists of line coefficients
57
- - yellow_points, red_points: lists of tuples (p1, p2) for each line
58
- - vps: dict with keys 'yellow' and 'red' for vanishing points (x,y)
59
- """
60
- if isinstance(base_pil, np.ndarray):
61
- img = Image.fromarray(base_pil).convert("RGBA")
62
- else:
63
- img = base_pil.copy().convert("RGBA")
64
-
65
  draw = ImageDraw.Draw(img)
66
 
67
- # helpers
68
  def draw_point(pt, color, r=4):
69
  x, y = pt
70
  draw.ellipse((x - r, y - r, x + r, y + r), fill=color, outline=color)
71
 
72
- def draw_line_by_points(p1, p2, color, width=2, dash=False):
73
- # we just draw a straight segment connecting endpoints
74
- if dash:
75
- # dashed line: draw small segments
76
- x1, y1 = p1
77
- x2, y2 = p2
78
- segs = 40
79
- for i in range(segs):
80
- t0 = i / segs
81
- t1 = (i + 0.5) / segs
82
- xa = x1 * (1 - t0) + x2 * t0
83
- ya = y1 * (1 - t0) + y2 * t0
84
- xb = x1 * (1 - t1) + x2 * t1
85
- yb = y1 * (1 - t1) + y2 * t1
86
- draw.line((xa, ya, xb, yb), fill=color, width=width)
87
- else:
88
- draw.line((p1[0], p1[1], p2[0], p2[1]), fill=color, width=width)
89
 
90
  # Draw yellow lines
91
  for idx, ((p1, p2), L) in enumerate(zip(yellow_points, yellow_lines)):
92
- # draw long extents of line by projecting to image bounds
93
  draw_line_segment_from_line(L, img.size, color=(255, 215, 0, 200), draw=draw)
94
  draw_point(p1, (255, 215, 0, 255))
95
  draw_point(p2, (255, 215, 0, 255))
@@ -109,36 +79,26 @@ def draw_overlay(base_pil, yellow_lines, red_lines, yellow_points, red_points, v
109
 
110
  return img.convert("RGB")
111
 
112
-
113
  def draw_line_segment_from_line(line, image_size, draw=None, color=(255, 255, 0, 255)):
114
- """Given line coefficients and image size, draw a segment across the image bounds.
115
- This draws directly using ImageDraw if 'draw' is provided.
116
- """
117
  W, H = image_size
118
  a, b, c = line
119
  points = []
120
- # intersection with left edge x=0
121
  if abs(b) > 1e-9:
122
- y = -(a * 0 + c) / b
123
  points.append((0, y))
124
- # right edge x=W
125
- if abs(b) > 1e-9:
126
  y = -(a * W + c) / b
127
  points.append((W, y))
128
- # top edge y=0 --> a x + c = 0
129
  if abs(a) > 1e-9:
130
- x = -(b * 0 + c) / a
131
  points.append((x, 0))
132
- # bottom edge y=H
133
- if abs(a) > 1e-9:
134
  x = -(b * H + c) / a
135
  points.append((x, H))
136
 
137
  # keep only points within the image bounds
138
  pts_in = [(x, y) for (x, y) in points if -W * 0.1 <= x <= W * 1.1 and -H * 0.1 <= y <= H * 1.1]
139
  if len(pts_in) >= 2 and draw is not None:
140
- # pick two extreme points
141
- # sort by x coordinate
142
  pts_in = sorted(pts_in, key=lambda p: (p[0], p[1]))
143
  pA = pts_in[0]
144
  pB = pts_in[-1]
@@ -146,41 +106,22 @@ def draw_line_segment_from_line(line, image_size, draw=None, color=(255, 255, 0,
146
 
147
  # ------------------------- Gradio app callbacks ---------------------------
148
 
149
- def init_states():
150
- return None, [], [], [], [], []
151
-
152
-
153
  def on_mode_change(mode, image, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs):
154
- """Switch drawing mode between 'yellow', 'red' or None.
155
- Returns image (unchanged) and updated states.
156
- """
157
- # Just update the mode state. Clear any pending single point.
158
  return (image, mode, [], y_lines, r_lines, y_pairs, r_pairs)
159
 
160
-
161
- def on_image_select(sel: gr.SelectData, image, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs):
162
- """Called when user clicks on the image. sel.index gives (x, y) in pixels.
163
- """
164
  if image is None:
165
- gr.Warning("Please upload an image first.")
166
- return image, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs
167
-
168
- if sel is None:
169
- return image, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs
170
-
171
- idx = getattr(sel, "index", None)
172
- if idx is None:
173
- idx = getattr(sel, "data", None) or getattr(sel, "value", None)
174
- if not idx:
175
  return image, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs
176
 
177
- x, y = int(idx[0]), int(idx[1])
178
-
179
- # append to current_points
180
  current_points = list(current_points) if current_points is not None else []
181
  current_points.append((x, y))
182
 
183
- # if we have two points, create a line
184
  if len(current_points) >= 2 and current_mode in ("yellow", "red"):
185
  p1 = current_points[-2]
186
  p2 = current_points[-1]
@@ -195,38 +136,43 @@ def on_image_select(sel: gr.SelectData, image, current_mode, current_points, y_l
195
  r_pairs = list(r_pairs) if r_pairs is not None else []
196
  r_lines.append(L)
197
  r_pairs.append((p1, p2))
198
-
199
- # Clear current points after forming a line
200
  current_points = []
201
 
202
-
203
- # redraw overlay image
204
- base_pil = Image.fromarray(image) if isinstance(image, np.ndarray) else image
 
 
 
205
  out = draw_overlay(base_pil, y_lines or [], r_lines or [], y_pairs or [], r_pairs or [], vps=None)
 
206
 
207
- return out, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs
208
-
209
 
210
  def compute_vanishing_points(image, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs):
211
- """Compute vanishing points for both color groups, draw them and return annotated image.
212
- """
213
  if image is None:
214
- gr.Warning("Please upload an image and draw lines first.")
215
  return image, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs
216
 
217
- img_pil = Image.fromarray(image) if isinstance(image, np.ndarray) else image
 
 
 
218
 
219
  vps = {"yellow": None, "red": None}
220
 
221
- # process yellow group
222
  if y_lines and len(y_lines) > 1:
223
  lines_arr = np.array(y_lines)
224
  inters = []
225
  for i in range(len(lines_arr) - 1):
226
  for j in range(i + 1, len(lines_arr)):
227
  try:
228
- ip = np.linalg.solve(np.array([[lines_arr[i][0], lines_arr[i][1]],[lines_arr[j][0], lines_arr[j][1]]]),
229
- -np.array([lines_arr[i][2], lines_arr[j][2]]))
 
 
230
  inters.append(ip)
231
  except Exception:
232
  pass
@@ -242,15 +188,17 @@ def compute_vanishing_points(image, current_mode, current_points, y_lines, r_lin
242
  res = minimize(lambda x: total_distances(x, lines_arr, noise), p0, method='Powell')
243
  vps['yellow'] = (float(res.x[0]), float(res.x[1]))
244
 
245
- # process red group
246
  if r_lines and len(r_lines) > 1:
247
  lines_arr = np.array(r_lines)
248
  inters = []
249
  for i in range(len(lines_arr) - 1):
250
  for j in range(i + 1, len(lines_arr)):
251
  try:
252
- ip = np.linalg.solve(np.array([[lines_arr[i][0], lines_arr[i][1]],[lines_arr[j][0], lines_arr[j][1]]]),
253
- -np.array([lines_arr[i][2], lines_arr[j][2]]))
 
 
254
  inters.append(ip)
255
  except Exception:
256
  pass
@@ -267,73 +215,96 @@ def compute_vanishing_points(image, current_mode, current_points, y_lines, r_lin
267
  vps['red'] = (float(res.x[0]), float(res.x[1]))
268
 
269
  out = draw_overlay(img_pil, y_lines or [], r_lines or [], y_pairs or [], r_pairs or [], vps=vps)
270
- # Return state, clearing current_points
271
- return out, current_mode, [], y_lines, r_lines, y_pairs, r_pairs
272
 
 
273
 
274
- def on_upload(image):
275
- # When a new image is uploaded, reset all states
 
 
 
 
 
276
  return image, None, [], [], [], [], []
277
 
278
  # ------------------------------ Build Blocks ------------------------------
279
 
280
  def build_gradio_interface():
281
- with gr.Blocks(theme=gr.themes.Soft()) as demo:
282
- gr.Markdown("# ☀️ Shadow Vanishing-Point Picker")
 
283
  with gr.Row():
284
- img_in = gr.Image(label="Upload image and then click to add points", type="numpy", interactive=True, height=600)
285
- with gr.Column(scale=1):
286
- start_y = gr.Button("Start Yellow Line")
287
- start_r = gr.Button("Start Red Line")
 
 
 
 
 
288
  none_btn = gr.Button("Stop Drawing")
289
- compute_btn = gr.Button("Compute Vanishing Points", variant="primary")
290
  reset_btn = gr.Button("Reset All")
291
- gr.Markdown("Click the image to add points. Two points make one line. Add at least 2 lines per color group to compute a vanishing point.")
292
-
293
- # states
 
 
 
 
 
 
 
 
294
  current_mode = gr.State(None)
295
  current_points = gr.State([])
296
  y_lines = gr.State([])
297
  r_lines = gr.State([])
298
  y_pairs = gr.State([])
299
  r_pairs = gr.State([])
 
 
 
 
 
 
 
300
 
301
- # Original image state to allow reseting
302
- original_image = gr.State()
303
-
304
- # link buttons to mode change
305
- start_y.click(on_mode_change, inputs=[gr.State("yellow"), img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs],
306
- outputs=[img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs])
307
- start_r.click(on_mode_change, inputs=[gr.State("red"), img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs],
308
- outputs=[img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs])
309
- none_btn.click(on_mode_change, inputs=[gr.State(None), img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs],
310
- outputs=[img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs])
311
-
312
- # image select event
313
- img_in.select(on_image_select, inputs=[img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs],
314
- outputs=[img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs])
315
-
316
- compute_btn.click(compute_vanishing_points, inputs=[img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs],
317
- outputs=[img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs])
318
-
319
- # Store original image on upload
320
- img_in.upload(
321
- lambda img: (img, img, None, [], [], [], [], []), # Reset all states on new upload
322
- inputs=[img_in],
323
- outputs=[img_in, original_image, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs]
324
  )
325
 
326
- # Reset button restores the original image and clears states
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  reset_btn.click(
328
- lambda img: (img, None, [], [], [], [], []), # Reset all states
329
- inputs=[original_image],
330
  outputs=[img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs]
331
  )
332
 
333
  return demo
334
 
335
- # --- Remove the launch() call ---
336
- # if __name__ == '__main__':
337
- # demo = build_gradio_interface()
338
- # demo.queue()
339
- # demo.launch()
 
1
  """
2
+ Gradio app for shadow analysis with vanishing-point selection tool
3
+ Updated for Gradio 4.x compatibility
4
  """
5
 
 
6
  import math
7
  import numpy as np
8
+ from PIL import Image, ImageDraw
9
  import gradio as gr
10
  from scipy.optimize import minimize
11
 
 
20
  c = x1 * y2 - y1 * x2
21
  return np.array([a, b, c], dtype=float)
22
 
 
23
  def distance_point_to_line(pt, line):
24
  x, y = pt
25
  a, b, c = line
26
  return abs(a * x + b * y + c) / math.hypot(a, b)
27
 
 
28
  def total_distances(x, lines, noise_lines):
29
  """Sum of distances from candidate point x to all lines and noise lines."""
30
  pt = x
 
35
  s += distance_point_to_line(pt, Ln)
36
  return s
37
 
 
38
  def add_noise_lines_for_line(p1, p2, n=4, sigma=1.0):
39
  """Create a list of "noise" lines by jittering the endpoints slightly."""
40
  noise_lines = []
 
47
  # ------------------------- Drawing utilities ------------------------------
48
 
49
  def draw_overlay(base_pil, yellow_lines, red_lines, yellow_points, red_points, vps=None):
50
+ """Return a new PIL image with overlays drawn: lines, points and vanishing points."""
51
+ img = base_pil.copy().convert("RGBA")
 
 
 
 
 
 
 
 
 
52
  draw = ImageDraw.Draw(img)
53
 
 
54
  def draw_point(pt, color, r=4):
55
  x, y = pt
56
  draw.ellipse((x - r, y - r, x + r, y + r), fill=color, outline=color)
57
 
58
+ def draw_line_by_points(p1, p2, color, width=2):
59
+ draw.line((p1[0], p1[1], p2[0], p2[1]), fill=color, width=width)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
  # Draw yellow lines
62
  for idx, ((p1, p2), L) in enumerate(zip(yellow_points, yellow_lines)):
 
63
  draw_line_segment_from_line(L, img.size, color=(255, 215, 0, 200), draw=draw)
64
  draw_point(p1, (255, 215, 0, 255))
65
  draw_point(p2, (255, 215, 0, 255))
 
79
 
80
  return img.convert("RGB")
81
 
 
82
  def draw_line_segment_from_line(line, image_size, draw=None, color=(255, 255, 0, 255)):
83
+ """Given line coefficients and image size, draw a segment across the image bounds."""
 
 
84
  W, H = image_size
85
  a, b, c = line
86
  points = []
87
+ # intersection with edges
88
  if abs(b) > 1e-9:
89
+ y = -c / b
90
  points.append((0, y))
 
 
91
  y = -(a * W + c) / b
92
  points.append((W, y))
 
93
  if abs(a) > 1e-9:
94
+ x = -c / a
95
  points.append((x, 0))
 
 
96
  x = -(b * H + c) / a
97
  points.append((x, H))
98
 
99
  # keep only points within the image bounds
100
  pts_in = [(x, y) for (x, y) in points if -W * 0.1 <= x <= W * 1.1 and -H * 0.1 <= y <= H * 1.1]
101
  if len(pts_in) >= 2 and draw is not None:
 
 
102
  pts_in = sorted(pts_in, key=lambda p: (p[0], p[1]))
103
  pA = pts_in[0]
104
  pB = pts_in[-1]
 
106
 
107
  # ------------------------- Gradio app callbacks ---------------------------
108
 
 
 
 
 
109
  def on_mode_change(mode, image, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs):
110
+ """Switch drawing mode between 'yellow', 'red' or None."""
 
 
 
111
  return (image, mode, [], y_lines, r_lines, y_pairs, r_pairs)
112
 
113
+ def on_image_click(evt: gr.SelectData, image, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs):
114
+ """Called when user clicks on the image in Gradio 4.x"""
 
 
115
  if image is None:
 
 
 
 
 
 
 
 
 
 
116
  return image, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs
117
 
118
+ x, y = evt.index
119
+
120
+ # Convert to list if needed
121
  current_points = list(current_points) if current_points is not None else []
122
  current_points.append((x, y))
123
 
124
+ # If we have two points, create a line
125
  if len(current_points) >= 2 and current_mode in ("yellow", "red"):
126
  p1 = current_points[-2]
127
  p2 = current_points[-1]
 
136
  r_pairs = list(r_pairs) if r_pairs is not None else []
137
  r_lines.append(L)
138
  r_pairs.append((p1, p2))
139
+ # Reset current points for next line
 
140
  current_points = []
141
 
142
+ # Redraw overlay
143
+ if isinstance(image, np.ndarray):
144
+ base_pil = Image.fromarray(image)
145
+ else:
146
+ base_pil = image
147
+
148
  out = draw_overlay(base_pil, y_lines or [], r_lines or [], y_pairs or [], r_pairs or [], vps=None)
149
+ out_np = np.array(out)
150
 
151
+ return out_np, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs
 
152
 
153
  def compute_vanishing_points(image, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs):
154
+ """Compute vanishing points for both color groups."""
 
155
  if image is None:
 
156
  return image, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs
157
 
158
+ if isinstance(image, np.ndarray):
159
+ img_pil = Image.fromarray(image)
160
+ else:
161
+ img_pil = image
162
 
163
  vps = {"yellow": None, "red": None}
164
 
165
+ # Process yellow group
166
  if y_lines and len(y_lines) > 1:
167
  lines_arr = np.array(y_lines)
168
  inters = []
169
  for i in range(len(lines_arr) - 1):
170
  for j in range(i + 1, len(lines_arr)):
171
  try:
172
+ ip = np.linalg.solve(
173
+ np.array([[lines_arr[i][0], lines_arr[i][1]], [lines_arr[j][0], lines_arr[j][1]]]),
174
+ -np.array([lines_arr[i][2], lines_arr[j][2]])
175
+ )
176
  inters.append(ip)
177
  except Exception:
178
  pass
 
188
  res = minimize(lambda x: total_distances(x, lines_arr, noise), p0, method='Powell')
189
  vps['yellow'] = (float(res.x[0]), float(res.x[1]))
190
 
191
+ # Process red group
192
  if r_lines and len(r_lines) > 1:
193
  lines_arr = np.array(r_lines)
194
  inters = []
195
  for i in range(len(lines_arr) - 1):
196
  for j in range(i + 1, len(lines_arr)):
197
  try:
198
+ ip = np.linalg.solve(
199
+ np.array([[lines_arr[i][0], lines_arr[i][1]], [lines_arr[j][0], lines_arr[j][1]]]),
200
+ -np.array([lines_arr[i][2], lines_arr[j][2]])
201
+ )
202
  inters.append(ip)
203
  except Exception:
204
  pass
 
215
  vps['red'] = (float(res.x[0]), float(res.x[1]))
216
 
217
  out = draw_overlay(img_pil, y_lines or [], r_lines or [], y_pairs or [], r_pairs or [], vps=vps)
218
+ out_np = np.array(out)
 
219
 
220
+ return out_np, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs
221
 
222
+ def reset_all(image, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs):
223
+ """Reset all states."""
224
+ if image is not None:
225
+ if isinstance(image, np.ndarray):
226
+ return image, None, [], [], [], [], []
227
+ else:
228
+ return np.array(image), None, [], [], [], [], []
229
  return image, None, [], [], [], [], []
230
 
231
  # ------------------------------ Build Blocks ------------------------------
232
 
233
  def build_gradio_interface():
234
+ with gr.Blocks() as demo:
235
+ gr.Markdown("# Shadow Analysis - Vanishing Point Detection")
236
+
237
  with gr.Row():
238
+ img_in = gr.Image(
239
+ label="Upload image and click to add points",
240
+ type="numpy",
241
+ interactive=True,
242
+ height=600
243
+ )
244
+ with gr.Column():
245
+ start_y = gr.Button("Start Yellow Lines")
246
+ start_r = gr.Button("Start Red Lines")
247
  none_btn = gr.Button("Stop Drawing")
248
+ compute_btn = gr.Button("Compute Vanishing Points")
249
  reset_btn = gr.Button("Reset All")
250
+
251
+ gr.Markdown("""
252
+ **Instructions:**
253
+ 1. Upload an image
254
+ 2. Click 'Start Yellow' or 'Start Red' to choose line color
255
+ 3. Click on the image to add points (2 points = 1 line)
256
+ 4. Add at least 2 lines per color group
257
+ 5. Click 'Compute Vanishing Points' to analyze
258
+ """)
259
+
260
+ # State variables
261
  current_mode = gr.State(None)
262
  current_points = gr.State([])
263
  y_lines = gr.State([])
264
  r_lines = gr.State([])
265
  y_pairs = gr.State([])
266
  r_pairs = gr.State([])
267
+
268
+ # Event handlers
269
+ start_y.click(
270
+ fn=on_mode_change,
271
+ inputs=[gr.State("yellow"), img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs],
272
+ outputs=[img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs]
273
+ )
274
 
275
+ start_r.click(
276
+ fn=on_mode_change,
277
+ inputs=[gr.State("red"), img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs],
278
+ outputs=[img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  )
280
 
281
+ none_btn.click(
282
+ fn=on_mode_change,
283
+ inputs=[gr.State(None), img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs],
284
+ outputs=[img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs]
285
+ )
286
+
287
+ # Image click event - updated for Gradio 4.x
288
+ img_in.select(
289
+ fn=on_image_click,
290
+ inputs=[img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs],
291
+ outputs=[img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs]
292
+ )
293
+
294
+ compute_btn.click(
295
+ fn=compute_vanishing_points,
296
+ inputs=[img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs],
297
+ outputs=[img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs]
298
+ )
299
+
300
  reset_btn.click(
301
+ fn=reset_all,
302
+ inputs=[img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs],
303
  outputs=[img_in, current_mode, current_points, y_lines, r_lines, y_pairs, r_pairs]
304
  )
305
 
306
  return demo
307
 
308
+ if __name__ == '__main__':
309
+ demo = build_gradio_interface()
310
+ demo.launch()