Khang Nguyen commited on
Commit ·
45b95d5
1
Parent(s): 3c989a3
Update mosaic app with frid preview and benchmark fix
Browse files- app.py +91 -36
- requirements.txt +4 -0
- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
# ----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
# ----
|
| 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 |
-
|
|
|
|
| 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:
|
|
|
|
| 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(
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
-
#
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
if os.path.isdir(exdir):
|
| 181 |
-
ex_files=[os.path.join(exdir,f) for f in os.listdir(exdir)
|
| 182 |
-
|
| 183 |
if ex_files:
|
| 184 |
-
gr.Examples(ex_files,inputs=[in_img],label="
|
| 185 |
|
| 186 |
-
#
|
| 187 |
with gr.Tab("Benchmark"):
|
| 188 |
-
gr.Markdown(
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)])
|