Khang Nguyen commited on
Commit
45b95d5
·
1 Parent(s): 3c989a3

Update mosaic app with frid preview and benchmark fix

Browse files
Files changed (3) hide show
  1. app.py +91 -36
  2. requirements.txt +4 -0
  3. testing.py +268 -0
app.py CHANGED
@@ -1,6 +1,6 @@
1
  import os, time, glob
2
  import numpy as np
3
- from PIL import Image
4
  import gradio as gr
5
 
6
  DEFAULT_TILE_DIR = "./tiles"
@@ -21,7 +21,11 @@ def np_to_pil(arr: np.ndarray) -> Image.Image:
21
  def crop_to_grid(img_np, ty, tx):
22
  H, W, _ = img_np.shape
23
  ch, cw = max(1, H // ty), max(1, W // tx)
24
- return img_np[:ch*ty, :cw*tx, :], ch, cw
 
 
 
 
25
 
26
  def mse(a, b): return float(np.mean((a - b) ** 2))
27
 
@@ -41,7 +45,6 @@ def cell_stats_vec(img_np, ty, tx):
41
  grid = cropped.reshape(ty, ch, tx, cw, 3).swapaxes(1,2) # (ty, tx, ch, cw, 3)
42
  return grid.mean((2,3)), np.median(grid,(2,3)), cropped, ch, cw
43
 
44
- # ---- NEW: loop-based version (explicit Python loops) ----
45
  def cell_stats_loop(img_np, ty, tx):
46
  """Loop-based: compute mean/median per cell with explicit loops."""
47
  cropped, ch, cw = crop_to_grid(img_np, ty, tx)
@@ -90,8 +93,23 @@ def assemble(indices,tiles,ch,cw):
90
  out[i*ch:(i+1)*ch, j*cw:(j+1)*cw, :] = tiles[indices[i,j]]
91
  return out
92
 
93
- # ---- pipeline (vectorized by default for UI) ----
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  def build_mosaic(pil_img, tiles_side, tile_dir, cell_stat="Mean", tile_stat="Mean"):
 
95
  t0=time.time()
96
  img_np=pil_to_np(pil_img)
97
  means,meds,cropped,ch,cw=cell_stats_vec(img_np,tiles_side,tiles_side)
@@ -100,10 +118,11 @@ def build_mosaic(pil_img, tiles_side, tile_dir, cell_stat="Mean", tile_stat="Mea
100
  idx=nearest_indices(cell_cols,tcols)
101
  mosaic=assemble(idx,tbank,ch,cw)
102
  m,s=mse(cropped,mosaic),simple_ssim(cropped,mosaic)
103
- return np_to_pil(cropped),np_to_pil(mosaic),f"MSE: {m:.6f} | SSIM*: {s:.6f}",f"{time.time()-t0:.3f}s"
 
104
 
105
- # ---- NEW: pipeline that can switch vec/loop (used by benchmark) ----
106
  def build_mosaic_mode(pil_img, tiles_side, tile_dir, cell_stat="Mean", tile_stat="Mean", use_vectorized=True):
 
107
  t0=time.time()
108
  img_np=pil_to_np(pil_img)
109
  if use_vectorized:
@@ -117,7 +136,7 @@ def build_mosaic_mode(pil_img, tiles_side, tile_dir, cell_stat="Mean", tile_stat
117
  m,s=mse(cropped,mosaic),simple_ssim(cropped,mosaic)
118
  return np_to_pil(mosaic), m, s, time.time()-t0
119
 
120
- # ---- NEW: benchmarking helpers ----
121
  def _time_run(pil_img, grid, tile_dir, cell_stat, tile_stat, vectorized: bool):
122
  _mos, _m, _s, elapsed = build_mosaic_mode(
123
  pil_img, tiles_side=grid, tile_dir=tile_dir,
@@ -149,44 +168,70 @@ def run_benchmark_table(pil_img, grids=(16,32,64), tile_dir=DEFAULT_TILE_DIR, ce
149
  with gr.Blocks(title="CS 5130 – Mosaic") as demo:
150
  gr.Markdown("### Image Mosaic\nUpload on the left → Mosaic result on the right.")
151
 
 
 
 
 
152
  with gr.Tab("Build Mosaic"):
153
  with gr.Row():
154
- in_img=gr.Image(type="pil",label="Upload main image")
155
- out_mosa=gr.Image(type="pil",label="Mosaic result")
 
156
 
157
- tile_dir_box=gr.Textbox(value=DEFAULT_TILE_DIR,label="Tiles folder path")
158
- grid=gr.Slider(8,128,value=32,step=1,label="Tiles per side")
159
- cell_stat=gr.Radio(["Mean","Median"],value="Mean",label="Cell color")
160
- tile_stat=gr.Radio(["Mean","Median"],value="Mean",label="Tile color")
161
- btn=gr.Button("Generate Mosaic")
162
 
163
- metrics=gr.Textbox(label="Similarity (MSE/SSIM*)",interactive=False)
164
- perf=gr.Textbox(label="Elapsed (s)",interactive=False)
165
 
166
- def ui_run(img,tps,cstat,tstat,tdir):
167
- if img is None: return None,None,"Upload an image.",""
 
168
  try:
169
- tdir=norm_path(tdir or DEFAULT_TILE_DIR)
170
- orig,mos,m,s=build_mosaic(img,int(tps),tdir,cstat,tstat)
171
- return orig,mos,m,s
172
  except Exception as e:
173
- return None,None,f"Error: {e}",""
174
 
175
- btn.click(ui_run,[in_img,grid,cell_stat,tile_stat,tile_dir_box],
176
- [in_img,out_mosa,metrics,perf])
 
 
 
177
 
178
- # 3 clickable examples
179
- exdir=norm_path(EXAMPLES_DIR)
 
 
 
 
 
180
  if os.path.isdir(exdir):
181
- ex_files=[os.path.join(exdir,f) for f in os.listdir(exdir)
182
- if f.lower().endswith((".jpg",".jpeg",".png"))][:3]
183
  if ex_files:
184
- gr.Examples(ex_files,inputs=[in_img],label="Click an example")
185
 
186
- # ---- NEW: Benchmark tab ----
187
  with gr.Tab("Benchmark"):
188
- gr.Markdown("Compare **Vectorized** vs **Loop** timings across grid sizes.")
189
- bench_img = gr.Image(type="pil", label="Main image for benchmark (RGB)")
 
 
 
 
 
 
 
 
 
 
 
 
190
  grids_box = gr.Textbox(value="16, 32, 64", label="Grid sizes (comma-separated)")
191
  b_cell = gr.Radio(["Mean","Median"], value="Mean", label="Cell color")
192
  b_tile = gr.Radio(["Mean","Median"], value="Mean", label="Tile color")
@@ -197,7 +242,7 @@ with gr.Blocks(title="CS 5130 – Mosaic") as demo:
197
  bench_note = gr.Textbox(label="Brief Analysis", lines=6)
198
 
199
  def ui_bench(img, grids_csv, cstat, tstat, tdir):
200
- if img is None: return "Please upload/select an image.", ""
201
  try:
202
  grids = [int(x.strip()) for x in grids_csv.split(",") if x.strip()]
203
  except:
@@ -206,8 +251,18 @@ with gr.Blocks(title="CS 5130 – Mosaic") as demo:
206
  table, analysis = run_benchmark_table(img, tuple(grids), tdir, cstat, tstat)
207
  return table, analysis
208
 
209
- run_bench.click(ui_bench, [bench_img, grids_box, b_cell, b_tile, b_folder],
210
- [bench_table, bench_note])
 
 
 
 
 
 
 
 
 
 
211
 
212
  if __name__=="__main__":
213
- demo.launch(allowed_paths=[norm_path(DEFAULT_TILE_DIR),norm_path(EXAMPLES_DIR)])
 
1
  import os, time, glob
2
  import numpy as np
3
+ from PIL import Image, ImageDraw
4
  import gradio as gr
5
 
6
  DEFAULT_TILE_DIR = "./tiles"
 
21
  def crop_to_grid(img_np, ty, tx):
22
  H, W, _ = img_np.shape
23
  ch, cw = max(1, H // ty), max(1, W // tx)
24
+ new_h, new_w = ch * ty, cw * tx
25
+ cropped = img_np[:new_h, :new_w, :]
26
+ # Optional debug print:
27
+ # print(f"[crop_to_grid] Original=({H},{W}), Cropped=({new_h},{new_w}), Cell=({ch},{cw})")
28
+ return cropped, ch, cw
29
 
30
  def mse(a, b): return float(np.mean((a - b) ** 2))
31
 
 
45
  grid = cropped.reshape(ty, ch, tx, cw, 3).swapaxes(1,2) # (ty, tx, ch, cw, 3)
46
  return grid.mean((2,3)), np.median(grid,(2,3)), cropped, ch, cw
47
 
 
48
  def cell_stats_loop(img_np, ty, tx):
49
  """Loop-based: compute mean/median per cell with explicit loops."""
50
  cropped, ch, cw = crop_to_grid(img_np, ty, tx)
 
93
  out[i*ch:(i+1)*ch, j*cw:(j+1)*cw, :] = tiles[indices[i,j]]
94
  return out
95
 
96
+ # ---- preview overlay ----
97
+ def add_tile_grid_overlay(cropped_np, ch, cw, ty, tx, color=(255, 0, 0), width=2):
98
+ """Draw grid lines over the cropped preview to visualize the ty×tx tiling."""
99
+ H, W, _ = cropped_np.shape
100
+ preview = np_to_pil(cropped_np.copy())
101
+ draw = ImageDraw.Draw(preview)
102
+ for i in range(1, ty): # horizontal
103
+ y = i * ch
104
+ draw.line([(0, y), (W - 1, y)], fill=color, width=width)
105
+ for j in range(1, tx): # vertical
106
+ x = j * cw
107
+ draw.line([(x, 0), (x, H - 1)], fill=color, width=width)
108
+ return preview
109
+
110
+ # ---- pipelines ----
111
  def build_mosaic(pil_img, tiles_side, tile_dir, cell_stat="Mean", tile_stat="Mean"):
112
+ """Vectorized default, returns (cropped+grid preview, mosaic, metrics, elapsed)."""
113
  t0=time.time()
114
  img_np=pil_to_np(pil_img)
115
  means,meds,cropped,ch,cw=cell_stats_vec(img_np,tiles_side,tiles_side)
 
118
  idx=nearest_indices(cell_cols,tcols)
119
  mosaic=assemble(idx,tbank,ch,cw)
120
  m,s=mse(cropped,mosaic),simple_ssim(cropped,mosaic)
121
+ overlay_preview = add_tile_grid_overlay(cropped, ch, cw, tiles_side, tiles_side, color=(255,0,0), width=2)
122
+ return overlay_preview, np_to_pil(mosaic), f"MSE: {m:.6f} | SSIM*: {s:.6f}", f"{time.time()-t0:.3f}s"
123
 
 
124
  def build_mosaic_mode(pil_img, tiles_side, tile_dir, cell_stat="Mean", tile_stat="Mean", use_vectorized=True):
125
+ """Switchable vec/loop (for benchmark). Returns (mosaic, MSE, SSIM, elapsed)."""
126
  t0=time.time()
127
  img_np=pil_to_np(pil_img)
128
  if use_vectorized:
 
136
  m,s=mse(cropped,mosaic),simple_ssim(cropped,mosaic)
137
  return np_to_pil(mosaic), m, s, time.time()-t0
138
 
139
+ # ---- benchmark helpers ----
140
  def _time_run(pil_img, grid, tile_dir, cell_stat, tile_stat, vectorized: bool):
141
  _mos, _m, _s, elapsed = build_mosaic_mode(
142
  pil_img, tiles_side=grid, tile_dir=tile_dir,
 
168
  with gr.Blocks(title="CS 5130 – Mosaic") as demo:
169
  gr.Markdown("### Image Mosaic\nUpload on the left → Mosaic result on the right.")
170
 
171
+ # Remember last uploaded image so Benchmark can reuse it
172
+ last_image = gr.State()
173
+
174
+ # Build Mosaic tab
175
  with gr.Tab("Build Mosaic"):
176
  with gr.Row():
177
+ in_img = gr.Image(type="pil", label="Upload main image")
178
+ orig_preview = gr.Image(type="pil", label="Cropped-for-grid (preview)")
179
+ out_mosa = gr.Image(type="pil", label="Mosaic result")
180
 
181
+ tile_dir_box = gr.Textbox(value=DEFAULT_TILE_DIR, label="Tiles folder path")
182
+ grid = gr.Slider(8,128,value=32,step=1,label="Tiles per side")
183
+ cell_stat = gr.Radio(["Mean","Median"],value="Mean",label="Cell color")
184
+ tile_stat = gr.Radio(["Mean","Median"],value="Mean",label="Tile color")
185
+ btn = gr.Button("Generate Mosaic")
186
 
187
+ metrics = gr.Textbox(label="Similarity (MSE/SSIM*)", interactive=False)
188
+ perf = gr.Textbox(label="Elapsed (s)", interactive=False)
189
 
190
+ def ui_run(img, tps, cstat, tstat, tdir):
191
+ if img is None:
192
+ return None, None, "Upload an image.", ""
193
  try:
194
+ tdir = norm_path(tdir or DEFAULT_TILE_DIR)
195
+ orig, mos, m, s = build_mosaic(img, int(tps), tdir, cstat, tstat)
196
+ return orig, mos, m, s
197
  except Exception as e:
198
+ return None, None, f"Error: {e}", ""
199
 
200
+ btn.click(
201
+ ui_run,
202
+ inputs=[in_img, grid, cell_stat, tile_stat, tile_dir_box],
203
+ outputs=[orig_preview, out_mosa, metrics, perf]
204
+ )
205
 
206
+ # Keep track of last uploaded image for Benchmark reuse
207
+ def _remember_image(img):
208
+ return img
209
+ in_img.change(_remember_image, inputs=[in_img], outputs=[last_image])
210
+
211
+ # Clickable examples (populate only the input image)
212
+ exdir = norm_path(EXAMPLES_DIR)
213
  if os.path.isdir(exdir):
214
+ ex_files = [os.path.join(exdir, f) for f in os.listdir(exdir)
215
+ if f.lower().endswith((".jpg",".jpeg",".png"))][:6]
216
  if ex_files:
217
+ gr.Examples(ex_files, inputs=[in_img], label="Examples (Build)")
218
 
219
+ # Benchmark tab
220
  with gr.Tab("Benchmark"):
221
+ gr.Markdown(
222
+ "Compare **Vectorized** vs **Loop** timings across grid sizes. "
223
+ "Use an example or click **Use uploaded image** to reuse the image from the Build tab."
224
+ )
225
+ bench_img = gr.Image(type="pil", label="Main image for benchmark (RGB)")
226
+ use_current = gr.Button("Use uploaded image") # copies from Build tab state
227
+
228
+ def _use_uploaded(img):
229
+ if img is None:
230
+ return None
231
+ return img
232
+
233
+ use_current.click(_use_uploaded, inputs=[last_image], outputs=[bench_img])
234
+
235
  grids_box = gr.Textbox(value="16, 32, 64", label="Grid sizes (comma-separated)")
236
  b_cell = gr.Radio(["Mean","Median"], value="Mean", label="Cell color")
237
  b_tile = gr.Radio(["Mean","Median"], value="Mean", label="Tile color")
 
242
  bench_note = gr.Textbox(label="Brief Analysis", lines=6)
243
 
244
  def ui_bench(img, grids_csv, cstat, tstat, tdir):
245
+ if img is None: return "Please upload/select an image (or click 'Use uploaded image').", ""
246
  try:
247
  grids = [int(x.strip()) for x in grids_csv.split(",") if x.strip()]
248
  except:
 
251
  table, analysis = run_benchmark_table(img, tuple(grids), tdir, cstat, tstat)
252
  return table, analysis
253
 
254
+ run_bench.click(
255
+ ui_bench,
256
+ inputs=[bench_img, grids_box, b_cell, b_tile, b_folder],
257
+ outputs=[bench_table, bench_note]
258
+ )
259
+
260
+ # Benchmark examples (optional)
261
+ if os.path.isdir(exdir):
262
+ ex_files_bench = [os.path.join(exdir, f) for f in os.listdir(exdir)
263
+ if f.lower().endswith((".jpg",".jpeg",".png"))][:6]
264
+ if ex_files_bench:
265
+ gr.Examples(ex_files_bench, inputs=[bench_img], label="Examples (Benchmark)")
266
 
267
  if __name__=="__main__":
268
+ demo.launch(allowed_paths=[norm_path(DEFAULT_TILE_DIR), norm_path(EXAMPLES_DIR)])
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio==4.41.0
2
+ numpy==1.26.4
3
+ Pillow==10.2.0
4
+ opencv-python==4.9.0.80
testing.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, time, glob
2
+ import numpy as np
3
+ from PIL import Image, ImageDraw
4
+ import gradio as gr
5
+
6
+ DEFAULT_TILE_DIR = "./tiles"
7
+ EXAMPLES_DIR = "./examples"
8
+
9
+ # ---- helpers ----
10
+ def norm_path(p: str) -> str:
11
+ return os.path.abspath(os.path.expanduser((p or "").rstrip("/")))
12
+
13
+ def pil_to_np(img: Image.Image) -> np.ndarray:
14
+ if img.mode != "RGB": img = img.convert("RGB")
15
+ return np.asarray(img).astype(np.float32) / 255.0
16
+
17
+ def np_to_pil(arr: np.ndarray) -> Image.Image:
18
+ arr = np.clip(arr * 255.0, 0, 255).astype(np.uint8)
19
+ return Image.fromarray(arr)
20
+
21
+ def crop_to_grid(img_np, ty, tx):
22
+ H, W, _ = img_np.shape
23
+ ch, cw = max(1, H // ty), max(1, W // tx)
24
+ new_h, new_w = ch * ty, cw * tx
25
+ cropped = img_np[:new_h, :new_w, :]
26
+ # Optional debug print:
27
+ # print(f"[crop_to_grid] Original=({H},{W}), Cropped=({new_h},{new_w}), Cell=({ch},{cw})")
28
+ return cropped, ch, cw
29
+
30
+ def mse(a, b): return float(np.mean((a - b) ** 2))
31
+
32
+ def simple_ssim(a, b):
33
+ g1, g2 = a.mean(axis=2), b.mean(axis=2)
34
+ mu1, mu2 = g1.mean(), g2.mean()
35
+ var1, var2 = g1.var(), g2.var()
36
+ cov = ((g1 - mu1) * (g2 - mu2)).mean()
37
+ C1, C2 = 0.01**2, 0.03**2
38
+ den = (mu1**2+mu2**2+C1)*(var1+var2+C2)
39
+ return float(((2*mu1*mu2+C1)*(2*cov+C2))/den) if den else 0.0
40
+
41
+ # ---- grid stats ----
42
+ def cell_stats_vec(img_np, ty, tx):
43
+ """Vectorized: reshape and reduce without Python loops."""
44
+ cropped, ch, cw = crop_to_grid(img_np, ty, tx)
45
+ grid = cropped.reshape(ty, ch, tx, cw, 3).swapaxes(1,2) # (ty, tx, ch, cw, 3)
46
+ return grid.mean((2,3)), np.median(grid,(2,3)), cropped, ch, cw
47
+
48
+ def cell_stats_loop(img_np, ty, tx):
49
+ """Loop-based: compute mean/median per cell with explicit loops."""
50
+ cropped, ch, cw = crop_to_grid(img_np, ty, tx)
51
+ means = np.zeros((ty, tx, 3), dtype=np.float32)
52
+ meds = np.zeros((ty, tx, 3), dtype=np.float32)
53
+ for i in range(ty):
54
+ for j in range(tx):
55
+ block = cropped[i*ch:(i+1)*ch, j*cw:(j+1)*cw, :]
56
+ means[i, j] = block.mean(axis=(0, 1))
57
+ meds[i, j] = np.median(block, axis=(0, 1))
58
+ return means, meds, cropped, ch, cw
59
+
60
+ # ---- tiles ----
61
+ def list_images(folder):
62
+ folder = norm_path(folder)
63
+ exts = ("jpg","jpeg","png","bmp","webp")
64
+ files=[]
65
+ for e in exts: files += glob.glob(os.path.join(folder, f"*.{e}"))
66
+ return sorted(set(files))
67
+
68
+ def load_tiles(tile_dir,h,w,stat="Mean"):
69
+ files = list_images(norm_path(tile_dir))
70
+ if not files: raise ValueError(f"No images in {tile_dir}")
71
+ tiles,cols=[],[]
72
+ for f in files:
73
+ try:
74
+ arr = pil_to_np(Image.open(f).resize((w,h),Image.BOX))
75
+ tiles.append(arr)
76
+ cols.append(np.median(arr,(0,1)) if stat=="Median" else arr.mean((0,1)))
77
+ except:
78
+ continue
79
+ return np.stack(tiles), np.stack(cols)
80
+
81
+ # ---- mapping ----
82
+ def nearest_indices(cell_cols,tile_cols):
83
+ ty,tx,_=cell_cols.shape
84
+ cm=cell_cols.reshape(-1,3)
85
+ d=((cm[:,None,:]-tile_cols[None,:,:])**2).sum(2)
86
+ return d.argmin(1).reshape(ty,tx)
87
+
88
+ def assemble(indices,tiles,ch,cw):
89
+ ty,tx=indices.shape
90
+ out=np.zeros((ty*ch,tx*cw,3), dtype=np.float32)
91
+ for i in range(ty):
92
+ for j in range(tx):
93
+ out[i*ch:(i+1)*ch, j*cw:(j+1)*cw, :] = tiles[indices[i,j]]
94
+ return out
95
+
96
+ # ---- preview overlay ----
97
+ def add_tile_grid_overlay(cropped_np, ch, cw, ty, tx, color=(255, 0, 0), width=2):
98
+ """Draw grid lines over the cropped preview to visualize the ty×tx tiling."""
99
+ H, W, _ = cropped_np.shape
100
+ preview = np_to_pil(cropped_np.copy())
101
+ draw = ImageDraw.Draw(preview)
102
+ for i in range(1, ty): # horizontal
103
+ y = i * ch
104
+ draw.line([(0, y), (W - 1, y)], fill=color, width=width)
105
+ for j in range(1, tx): # vertical
106
+ x = j * cw
107
+ draw.line([(x, 0), (x, H - 1)], fill=color, width=width)
108
+ return preview
109
+
110
+ # ---- pipelines ----
111
+ def build_mosaic(pil_img, tiles_side, tile_dir, cell_stat="Mean", tile_stat="Mean"):
112
+ """Vectorized default, returns (cropped+grid preview, mosaic, metrics, elapsed)."""
113
+ t0=time.time()
114
+ img_np=pil_to_np(pil_img)
115
+ means,meds,cropped,ch,cw=cell_stats_vec(img_np,tiles_side,tiles_side)
116
+ cell_cols=means if cell_stat=="Mean" else meds
117
+ tbank,tcols=load_tiles(tile_dir,ch,cw,stat=tile_stat)
118
+ idx=nearest_indices(cell_cols,tcols)
119
+ mosaic=assemble(idx,tbank,ch,cw)
120
+ m,s=mse(cropped,mosaic),simple_ssim(cropped,mosaic)
121
+ overlay_preview = add_tile_grid_overlay(cropped, ch, cw, tiles_side, tiles_side, color=(255,0,0), width=2)
122
+ return overlay_preview, np_to_pil(mosaic), f"MSE: {m:.6f} | SSIM*: {s:.6f}", f"{time.time()-t0:.3f}s"
123
+
124
+ def build_mosaic_mode(pil_img, tiles_side, tile_dir, cell_stat="Mean", tile_stat="Mean", use_vectorized=True):
125
+ """Switchable vec/loop (for benchmark). Returns (mosaic, MSE, SSIM, elapsed)."""
126
+ t0=time.time()
127
+ img_np=pil_to_np(pil_img)
128
+ if use_vectorized:
129
+ means,meds,cropped,ch,cw=cell_stats_vec(img_np,tiles_side,tiles_side)
130
+ else:
131
+ means,meds,cropped,ch,cw=cell_stats_loop(img_np,tiles_side,tiles_side)
132
+ cell_cols=means if cell_stat=="Mean" else meds
133
+ tbank,tcols=load_tiles(tile_dir,ch,cw,stat=tile_stat)
134
+ idx=nearest_indices(cell_cols,tcols)
135
+ mosaic=assemble(idx,tbank,ch,cw)
136
+ m,s=mse(cropped,mosaic),simple_ssim(cropped,mosaic)
137
+ return np_to_pil(mosaic), m, s, time.time()-t0
138
+
139
+ # ---- benchmark helpers ----
140
+ def _time_run(pil_img, grid, tile_dir, cell_stat, tile_stat, vectorized: bool):
141
+ _mos, _m, _s, elapsed = build_mosaic_mode(
142
+ pil_img, tiles_side=grid, tile_dir=tile_dir,
143
+ cell_stat=cell_stat, tile_stat=tile_stat, use_vectorized=vectorized
144
+ )
145
+ return elapsed
146
+
147
+ def run_benchmark_table(pil_img, grids=(16,32,64), tile_dir=DEFAULT_TILE_DIR, cell_stat="Mean", tile_stat="Mean"):
148
+ rows=[]
149
+ for g in grids:
150
+ t_vec = _time_run(pil_img, g, tile_dir, cell_stat, tile_stat, True)
151
+ t_loop = _time_run(pil_img, g, tile_dir, cell_stat, tile_stat, False)
152
+ speed = t_loop / max(t_vec, 1e-9)
153
+ rows.append((g, t_vec, t_loop, speed))
154
+ header = "Grid | Vectorized Time (s) | Loop Time (s) | Speedup (loop/vec)"
155
+ sep = "-" * len(header)
156
+ lines = [header, sep] + [f"{g}×{g} | {tv:.3f} | {tl:.3f} | {sp:.2f}×" for g,tv,tl,sp in rows]
157
+ table = "\n".join(lines)
158
+ analysis = (
159
+ "Analysis:\n"
160
+ "- Runtime increases with grid size because cells grow as (tiles_per_side)^2.\n"
161
+ "- Matching cost ~ O(#cells × #tiles). Doubling tiles_per_side ≈ 4× more cells.\n"
162
+ "- Vectorized NumPy is consistently faster (often ~10–20×) than loops due to optimized C.\n"
163
+ "- Larger grids improve visual detail but cost more time; pick based on quality vs speed."
164
+ )
165
+ return table, analysis
166
+
167
+ # ---- gradio ui ----
168
+ with gr.Blocks(title="CS 5130 – Mosaic") as demo:
169
+ gr.Markdown("### Image Mosaic\nUpload on the left → Mosaic result on the right.")
170
+
171
+ # Remember last uploaded image so Benchmark can reuse it
172
+ last_image = gr.State()
173
+
174
+ # Build Mosaic tab
175
+ with gr.Tab("Build Mosaic"):
176
+ with gr.Row():
177
+ in_img = gr.Image(type="pil", label="Upload main image")
178
+ orig_preview = gr.Image(type="pil", label="Cropped-for-grid (preview)")
179
+ out_mosa = gr.Image(type="pil", label="Mosaic result")
180
+
181
+ tile_dir_box = gr.Textbox(value=DEFAULT_TILE_DIR, label="Tiles folder path")
182
+ grid = gr.Slider(8,128,value=32,step=1,label="Tiles per side")
183
+ cell_stat = gr.Radio(["Mean","Median"],value="Mean",label="Cell color")
184
+ tile_stat = gr.Radio(["Mean","Median"],value="Mean",label="Tile color")
185
+ btn = gr.Button("Generate Mosaic")
186
+
187
+ metrics = gr.Textbox(label="Similarity (MSE/SSIM*)", interactive=False)
188
+ perf = gr.Textbox(label="Elapsed (s)", interactive=False)
189
+
190
+ def ui_run(img, tps, cstat, tstat, tdir):
191
+ if img is None:
192
+ return None, None, "Upload an image.", ""
193
+ try:
194
+ tdir = norm_path(tdir or DEFAULT_TILE_DIR)
195
+ orig, mos, m, s = build_mosaic(img, int(tps), tdir, cstat, tstat)
196
+ return orig, mos, m, s
197
+ except Exception as e:
198
+ return None, None, f"Error: {e}", ""
199
+
200
+ btn.click(
201
+ ui_run,
202
+ inputs=[in_img, grid, cell_stat, tile_stat, tile_dir_box],
203
+ outputs=[orig_preview, out_mosa, metrics, perf]
204
+ )
205
+
206
+ # Keep track of last uploaded image for Benchmark reuse
207
+ def _remember_image(img):
208
+ return img
209
+ in_img.change(_remember_image, inputs=[in_img], outputs=[last_image])
210
+
211
+ # Clickable examples (populate only the input image)
212
+ exdir = norm_path(EXAMPLES_DIR)
213
+ if os.path.isdir(exdir):
214
+ ex_files = [os.path.join(exdir, f) for f in os.listdir(exdir)
215
+ if f.lower().endswith((".jpg",".jpeg",".png"))][:6]
216
+ if ex_files:
217
+ gr.Examples(ex_files, inputs=[in_img], label="Examples (Build)")
218
+
219
+ # Benchmark tab
220
+ with gr.Tab("Benchmark"):
221
+ gr.Markdown(
222
+ "Compare **Vectorized** vs **Loop** timings across grid sizes. "
223
+ "Use an example or click **Use uploaded image** to reuse the image from the Build tab."
224
+ )
225
+ bench_img = gr.Image(type="pil", label="Main image for benchmark (RGB)")
226
+ use_current = gr.Button("Use uploaded image") # copies from Build tab state
227
+
228
+ def _use_uploaded(img):
229
+ if img is None:
230
+ return None
231
+ return img
232
+
233
+ use_current.click(_use_uploaded, inputs=[last_image], outputs=[bench_img])
234
+
235
+ grids_box = gr.Textbox(value="16, 32, 64", label="Grid sizes (comma-separated)")
236
+ b_cell = gr.Radio(["Mean","Median"], value="Mean", label="Cell color")
237
+ b_tile = gr.Radio(["Mean","Median"], value="Mean", label="Tile color")
238
+ b_folder = gr.Textbox(value=DEFAULT_TILE_DIR, label="Tile folder path")
239
+ run_bench = gr.Button("Run Benchmark")
240
+
241
+ bench_table = gr.Code(label="Results Table")
242
+ bench_note = gr.Textbox(label="Brief Analysis", lines=6)
243
+
244
+ def ui_bench(img, grids_csv, cstat, tstat, tdir):
245
+ if img is None: return "Please upload/select an image (or click 'Use uploaded image').", ""
246
+ try:
247
+ grids = [int(x.strip()) for x in grids_csv.split(",") if x.strip()]
248
+ except:
249
+ grids = [16,32,64]
250
+ tdir = norm_path(tdir or DEFAULT_TILE_DIR)
251
+ table, analysis = run_benchmark_table(img, tuple(grids), tdir, cstat, tstat)
252
+ return table, analysis
253
+
254
+ run_bench.click(
255
+ ui_bench,
256
+ inputs=[bench_img, grids_box, b_cell, b_tile, b_folder],
257
+ outputs=[bench_table, bench_note]
258
+ )
259
+
260
+ # Benchmark examples (optional)
261
+ if os.path.isdir(exdir):
262
+ ex_files_bench = [os.path.join(exdir, f) for f in os.listdir(exdir)
263
+ if f.lower().endswith((".jpg",".jpeg",".png"))][:6]
264
+ if ex_files_bench:
265
+ gr.Examples(ex_files_bench, inputs=[bench_img], label="Examples (Benchmark)")
266
+
267
+ if __name__=="__main__":
268
+ demo.launch(allowed_paths=[norm_path(DEFAULT_TILE_DIR), norm_path(EXAMPLES_DIR)])