CS5130 / testing.py
Khang Nguyen
Update mosaic app with frid preview and benchmark fix
45b95d5
Raw
History Blame Contribute Delete
11.3 kB
import os, time, glob
import numpy as np
from PIL import Image, ImageDraw
import gradio as gr
DEFAULT_TILE_DIR = "./tiles"
EXAMPLES_DIR = "./examples"
# ---- helpers ----
def norm_path(p: str) -> str:
return os.path.abspath(os.path.expanduser((p or "").rstrip("/")))
def pil_to_np(img: Image.Image) -> np.ndarray:
if img.mode != "RGB": img = img.convert("RGB")
return np.asarray(img).astype(np.float32) / 255.0
def np_to_pil(arr: np.ndarray) -> Image.Image:
arr = np.clip(arr * 255.0, 0, 255).astype(np.uint8)
return Image.fromarray(arr)
def crop_to_grid(img_np, ty, tx):
H, W, _ = img_np.shape
ch, cw = max(1, H // ty), max(1, W // tx)
new_h, new_w = ch * ty, cw * tx
cropped = img_np[:new_h, :new_w, :]
# Optional debug print:
# print(f"[crop_to_grid] Original=({H},{W}), Cropped=({new_h},{new_w}), Cell=({ch},{cw})")
return cropped, ch, cw
def mse(a, b): return float(np.mean((a - b) ** 2))
def simple_ssim(a, b):
g1, g2 = a.mean(axis=2), b.mean(axis=2)
mu1, mu2 = g1.mean(), g2.mean()
var1, var2 = g1.var(), g2.var()
cov = ((g1 - mu1) * (g2 - mu2)).mean()
C1, C2 = 0.01**2, 0.03**2
den = (mu1**2+mu2**2+C1)*(var1+var2+C2)
return float(((2*mu1*mu2+C1)*(2*cov+C2))/den) if den else 0.0
# ---- grid stats ----
def cell_stats_vec(img_np, ty, tx):
"""Vectorized: reshape and reduce without Python loops."""
cropped, ch, cw = crop_to_grid(img_np, ty, tx)
grid = cropped.reshape(ty, ch, tx, cw, 3).swapaxes(1,2) # (ty, tx, ch, cw, 3)
return grid.mean((2,3)), np.median(grid,(2,3)), cropped, ch, cw
def cell_stats_loop(img_np, ty, tx):
"""Loop-based: compute mean/median per cell with explicit loops."""
cropped, ch, cw = crop_to_grid(img_np, ty, tx)
means = np.zeros((ty, tx, 3), dtype=np.float32)
meds = np.zeros((ty, tx, 3), dtype=np.float32)
for i in range(ty):
for j in range(tx):
block = cropped[i*ch:(i+1)*ch, j*cw:(j+1)*cw, :]
means[i, j] = block.mean(axis=(0, 1))
meds[i, j] = np.median(block, axis=(0, 1))
return means, meds, cropped, ch, cw
# ---- tiles ----
def list_images(folder):
folder = norm_path(folder)
exts = ("jpg","jpeg","png","bmp","webp")
files=[]
for e in exts: files += glob.glob(os.path.join(folder, f"*.{e}"))
return sorted(set(files))
def load_tiles(tile_dir,h,w,stat="Mean"):
files = list_images(norm_path(tile_dir))
if not files: raise ValueError(f"No images in {tile_dir}")
tiles,cols=[],[]
for f in files:
try:
arr = pil_to_np(Image.open(f).resize((w,h),Image.BOX))
tiles.append(arr)
cols.append(np.median(arr,(0,1)) if stat=="Median" else arr.mean((0,1)))
except:
continue
return np.stack(tiles), np.stack(cols)
# ---- mapping ----
def nearest_indices(cell_cols,tile_cols):
ty,tx,_=cell_cols.shape
cm=cell_cols.reshape(-1,3)
d=((cm[:,None,:]-tile_cols[None,:,:])**2).sum(2)
return d.argmin(1).reshape(ty,tx)
def assemble(indices,tiles,ch,cw):
ty,tx=indices.shape
out=np.zeros((ty*ch,tx*cw,3), dtype=np.float32)
for i in range(ty):
for j in range(tx):
out[i*ch:(i+1)*ch, j*cw:(j+1)*cw, :] = tiles[indices[i,j]]
return out
# ---- preview overlay ----
def add_tile_grid_overlay(cropped_np, ch, cw, ty, tx, color=(255, 0, 0), width=2):
"""Draw grid lines over the cropped preview to visualize the ty×tx tiling."""
H, W, _ = cropped_np.shape
preview = np_to_pil(cropped_np.copy())
draw = ImageDraw.Draw(preview)
for i in range(1, ty): # horizontal
y = i * ch
draw.line([(0, y), (W - 1, y)], fill=color, width=width)
for j in range(1, tx): # vertical
x = j * cw
draw.line([(x, 0), (x, H - 1)], fill=color, width=width)
return preview
# ---- pipelines ----
def build_mosaic(pil_img, tiles_side, tile_dir, cell_stat="Mean", tile_stat="Mean"):
"""Vectorized default, returns (cropped+grid preview, mosaic, metrics, elapsed)."""
t0=time.time()
img_np=pil_to_np(pil_img)
means,meds,cropped,ch,cw=cell_stats_vec(img_np,tiles_side,tiles_side)
cell_cols=means if cell_stat=="Mean" else meds
tbank,tcols=load_tiles(tile_dir,ch,cw,stat=tile_stat)
idx=nearest_indices(cell_cols,tcols)
mosaic=assemble(idx,tbank,ch,cw)
m,s=mse(cropped,mosaic),simple_ssim(cropped,mosaic)
overlay_preview = add_tile_grid_overlay(cropped, ch, cw, tiles_side, tiles_side, color=(255,0,0), width=2)
return overlay_preview, np_to_pil(mosaic), f"MSE: {m:.6f} | SSIM*: {s:.6f}", f"{time.time()-t0:.3f}s"
def build_mosaic_mode(pil_img, tiles_side, tile_dir, cell_stat="Mean", tile_stat="Mean", use_vectorized=True):
"""Switchable vec/loop (for benchmark). Returns (mosaic, MSE, SSIM, elapsed)."""
t0=time.time()
img_np=pil_to_np(pil_img)
if use_vectorized:
means,meds,cropped,ch,cw=cell_stats_vec(img_np,tiles_side,tiles_side)
else:
means,meds,cropped,ch,cw=cell_stats_loop(img_np,tiles_side,tiles_side)
cell_cols=means if cell_stat=="Mean" else meds
tbank,tcols=load_tiles(tile_dir,ch,cw,stat=tile_stat)
idx=nearest_indices(cell_cols,tcols)
mosaic=assemble(idx,tbank,ch,cw)
m,s=mse(cropped,mosaic),simple_ssim(cropped,mosaic)
return np_to_pil(mosaic), m, s, time.time()-t0
# ---- benchmark helpers ----
def _time_run(pil_img, grid, tile_dir, cell_stat, tile_stat, vectorized: bool):
_mos, _m, _s, elapsed = build_mosaic_mode(
pil_img, tiles_side=grid, tile_dir=tile_dir,
cell_stat=cell_stat, tile_stat=tile_stat, use_vectorized=vectorized
)
return elapsed
def run_benchmark_table(pil_img, grids=(16,32,64), tile_dir=DEFAULT_TILE_DIR, cell_stat="Mean", tile_stat="Mean"):
rows=[]
for g in grids:
t_vec = _time_run(pil_img, g, tile_dir, cell_stat, tile_stat, True)
t_loop = _time_run(pil_img, g, tile_dir, cell_stat, tile_stat, False)
speed = t_loop / max(t_vec, 1e-9)
rows.append((g, t_vec, t_loop, speed))
header = "Grid | Vectorized Time (s) | Loop Time (s) | Speedup (loop/vec)"
sep = "-" * len(header)
lines = [header, sep] + [f"{g}×{g} | {tv:.3f} | {tl:.3f} | {sp:.2f}×" for g,tv,tl,sp in rows]
table = "\n".join(lines)
analysis = (
"Analysis:\n"
"- Runtime increases with grid size because cells grow as (tiles_per_side)^2.\n"
"- Matching cost ~ O(#cells × #tiles). Doubling tiles_per_side ≈ 4× more cells.\n"
"- Vectorized NumPy is consistently faster (often ~10–20×) than loops due to optimized C.\n"
"- Larger grids improve visual detail but cost more time; pick based on quality vs speed."
)
return table, analysis
# ---- gradio ui ----
with gr.Blocks(title="CS 5130 – Mosaic") as demo:
gr.Markdown("### Image Mosaic\nUpload on the left → Mosaic result on the right.")
# Remember last uploaded image so Benchmark can reuse it
last_image = gr.State()
# Build Mosaic tab
with gr.Tab("Build Mosaic"):
with gr.Row():
in_img = gr.Image(type="pil", label="Upload main image")
orig_preview = gr.Image(type="pil", label="Cropped-for-grid (preview)")
out_mosa = gr.Image(type="pil", label="Mosaic result")
tile_dir_box = gr.Textbox(value=DEFAULT_TILE_DIR, label="Tiles folder path")
grid = gr.Slider(8,128,value=32,step=1,label="Tiles per side")
cell_stat = gr.Radio(["Mean","Median"],value="Mean",label="Cell color")
tile_stat = gr.Radio(["Mean","Median"],value="Mean",label="Tile color")
btn = gr.Button("Generate Mosaic")
metrics = gr.Textbox(label="Similarity (MSE/SSIM*)", interactive=False)
perf = gr.Textbox(label="Elapsed (s)", interactive=False)
def ui_run(img, tps, cstat, tstat, tdir):
if img is None:
return None, None, "Upload an image.", ""
try:
tdir = norm_path(tdir or DEFAULT_TILE_DIR)
orig, mos, m, s = build_mosaic(img, int(tps), tdir, cstat, tstat)
return orig, mos, m, s
except Exception as e:
return None, None, f"Error: {e}", ""
btn.click(
ui_run,
inputs=[in_img, grid, cell_stat, tile_stat, tile_dir_box],
outputs=[orig_preview, out_mosa, metrics, perf]
)
# Keep track of last uploaded image for Benchmark reuse
def _remember_image(img):
return img
in_img.change(_remember_image, inputs=[in_img], outputs=[last_image])
# Clickable examples (populate only the input image)
exdir = norm_path(EXAMPLES_DIR)
if os.path.isdir(exdir):
ex_files = [os.path.join(exdir, f) for f in os.listdir(exdir)
if f.lower().endswith((".jpg",".jpeg",".png"))][:6]
if ex_files:
gr.Examples(ex_files, inputs=[in_img], label="Examples (Build)")
# Benchmark tab
with gr.Tab("Benchmark"):
gr.Markdown(
"Compare **Vectorized** vs **Loop** timings across grid sizes. "
"Use an example or click **Use uploaded image** to reuse the image from the Build tab."
)
bench_img = gr.Image(type="pil", label="Main image for benchmark (RGB)")
use_current = gr.Button("Use uploaded image") # copies from Build tab state
def _use_uploaded(img):
if img is None:
return None
return img
use_current.click(_use_uploaded, inputs=[last_image], outputs=[bench_img])
grids_box = gr.Textbox(value="16, 32, 64", label="Grid sizes (comma-separated)")
b_cell = gr.Radio(["Mean","Median"], value="Mean", label="Cell color")
b_tile = gr.Radio(["Mean","Median"], value="Mean", label="Tile color")
b_folder = gr.Textbox(value=DEFAULT_TILE_DIR, label="Tile folder path")
run_bench = gr.Button("Run Benchmark")
bench_table = gr.Code(label="Results Table")
bench_note = gr.Textbox(label="Brief Analysis", lines=6)
def ui_bench(img, grids_csv, cstat, tstat, tdir):
if img is None: return "Please upload/select an image (or click 'Use uploaded image').", ""
try:
grids = [int(x.strip()) for x in grids_csv.split(",") if x.strip()]
except:
grids = [16,32,64]
tdir = norm_path(tdir or DEFAULT_TILE_DIR)
table, analysis = run_benchmark_table(img, tuple(grids), tdir, cstat, tstat)
return table, analysis
run_bench.click(
ui_bench,
inputs=[bench_img, grids_box, b_cell, b_tile, b_folder],
outputs=[bench_table, bench_note]
)
# Benchmark examples (optional)
if os.path.isdir(exdir):
ex_files_bench = [os.path.join(exdir, f) for f in os.listdir(exdir)
if f.lower().endswith((".jpg",".jpeg",".png"))][:6]
if ex_files_bench:
gr.Examples(ex_files_bench, inputs=[bench_img], label="Examples (Benchmark)")
if __name__=="__main__":
demo.launch(allowed_paths=[norm_path(DEFAULT_TILE_DIR), norm_path(EXAMPLES_DIR)])