"""FlashReFocus: interactive image refocusing (Hugging Face Space, ZeroGPU). 1. Restore: Stage I (one-step diffusion with detail weight omega and the decoder refiner) restores the all-in-focus image once, and Depth Anything V2 estimates its depth once. 2. Click the restored image (or the depth map) to focus; only the 1.3M-parameter renderer runs per edit. Modes: one focal plane; two focal planes (first click A, second click B); tilt-shift (a click sets the sharp row). 3. The aperture (blur strength K), the depth curve and an optional user depth map re-render the current edit. """ import time import gradio as gr import numpy as np import spaces from PIL import Image, ImageDraw, ImageFont import engine SINGLE, DUAL, TILT = "📍 One focal plane", "👥 Two focal planes", "🏙️ Tilt-shift" EXAMPLES = ["examples/portrait_city.jpg", "examples/portrait_aquarium.jpg", "examples/flower.jpg", "examples/dock.jpg", "examples/sculpture.jpg"] FONT = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" try: ImageFont.truetype(FONT, 12) except OSError: FONT = None def fit(img, long_side): """Long side at most `long_side`, both sides multiples of 16.""" w, h = img.size s = min(1.0, long_side / max(w, h)) w2, h2 = max(16, int(w * s) // 16 * 16), max(16, int(h * s) // 16 * 16) return img.convert("RGB").resize((w2, h2), Image.LANCZOS) @spaces.GPU(duration=40) def gpu_restore(rgb, omega): t0 = time.perf_counter() aif, depth = engine.restore(rgb, omega) return aif, depth, time.perf_counter() - t0 @spaces.GPU(duration=10) def gpu_render(aif, depth, focal_depth, strength): t0 = time.perf_counter() out = engine.render(aif, depth, focal_depth, strength) return out, time.perf_counter() - t0 def depth_image(depth): return Image.fromarray((np.clip(1 - depth, 0, 1) * 255).astype(np.uint8)) # near is bright def scene_depth(state, gamma): """The depth the edits start from: the estimate (or the user's map), with the depth curve D^gamma applied.""" d = state["user_depth"] if state.get("user_depth") is not None else state["depth"] return np.power(np.clip(d, 0, 1), float(gamma)).astype(np.float32) def depth_at(depth, point): h, w = depth.shape return float(depth[min(max(point[1], 0), h - 1), min(max(point[0], 0), w - 1)]) def edit(state, mode, gamma, band): """(depth to render, focal depth D_f) for the current mode, or None when a focal point is still missing.""" d = scene_depth(state, gamma) h, w = d.shape if mode == TILT: return engine.tilt_shift(h, w, state.get("tilt_row", 0.5), float(band)), 0.5 pts = state["pts"] if not pts: return None if mode == DUAL and len(pts) == 2: da, db = depth_at(d, pts[0]), depth_at(d, pts[1]) return engine.two_focal_planes(d, da, db), da return d, depth_at(d, pts[0]) def restore(img, long_side, omega, gamma): if img is None: return None, None, None, None, "Upload an image or pick an example first." rgb = np.asarray(fit(img, int(long_side))) aif, depth, t = gpu_restore(rgb, float(omega)) state = {"aif": aif, "depth": depth, "user_depth": None, "pts": [], "tilt_row": 0.5} msg = (f"Restored {aif.shape[1]}×{aif.shape[0]} with ω = {float(omega):.2f} and estimated its depth in {t:.2f} s. " "**Click the restored image or the depth map to focus.**") return Image.fromarray(aif), depth_image(scene_depth(state, gamma)), None, state, msg def render(state, strength, mode, gamma, band, show_marker): """(refocused image, depth view, message).""" edited = edit(state, mode, gamma, band) if edited is None: return None, depth_image(scene_depth(state, gamma)), "Click the restored image or the depth map to focus." depth, focal_depth = edited out, dt = gpu_render(state["aif"], depth, focal_depth, float(strength)) out = Image.fromarray(out) if show_marker: out = draw_markers(out, state, mode) k = f"aperture K = {float(strength):.2f}" + (" (extrapolated)" if float(strength) > 1 else "") if mode == TILT: msg = f"Tilt-shift band at {state.get('tilt_row', 0.5):.0%} of the height, {k}: rendered in {dt:.2f} s." elif mode == DUAL and len(state["pts"]) == 2: msg = f"Two focal planes A {state['pts'][0]} and B {state['pts'][1]}, {k}: rendered in {dt:.2f} s." elif mode == DUAL: msg = f"Plane A {state['pts'][0]} set (rendered in {dt:.2f} s). Click a second point for plane B." else: msg = f"Focus {state['pts'][0]} (D_f = {focal_depth:.2f}), {k}: rendered in {dt:.2f} s." return out, depth_image(depth), msg def draw_markers(im, state, mode): """Focal points (A / B with two planes), or the sharp row of tilt-shift, on a 3x supersampled overlay.""" im = im.convert("RGBA"); S = 3 ov = Image.new("RGBA", (im.width * S, im.height * S), (0, 0, 0, 0)); d = ImageDraw.Draw(ov) r = max(9, round(min(im.size) * 0.022)) * S; lw = max(2, round(r / 6)) if mode == TILT: y = state.get("tilt_row", 0.5) * ov.height for x in range(0, ov.width, 12 * lw): d.line([x, y, x + 6 * lw, y], fill=(255, 255, 255, 220), width=lw) return Image.alpha_composite(im, ov.resize(im.size, Image.LANCZOS)).convert("RGB") font = ImageFont.truetype(FONT, round(r * 1.25)) if FONT else ImageFont.load_default() for i, (x, y) in enumerate(state["pts"]): cx, cy = x * S, y * S d.ellipse([cx - r - lw, cy - r - lw, cx + r + lw, cy + r + lw], outline=(0, 0, 0, 170), width=lw * 3) d.ellipse([cx - r, cy - r, cx + r, cy + r], outline=(255, 255, 255, 255), width=lw) dot = lw * 1.3 d.ellipse([cx - dot, cy - dot, cx + dot, cy + dot], fill=(255, 255, 255, 255)) if mode == DUAL: tx = cx + r + lw * 2 if cx + r * 3 < ov.width else cx - r * 2.2 d.text((tx, max(0, cy - r * 1.9)), "AB"[i], font=font, fill=(255, 255, 255, 255), stroke_width=max(2, lw), stroke_fill=(0, 0, 0, 200)) return Image.alpha_composite(im, ov.resize(im.size, Image.LANCZOS)).convert("RGB") def aif_view(state, mode): return draw_markers(Image.fromarray(state["aif"]), state, mode) def on_click(state, strength, mode, gamma, band, show_marker, evt: gr.SelectData): if not state: return None, None, None, state, "Restore an image first." p = (int(evt.index[0]), int(evt.index[1])) if mode == TILT: state = dict(state, tilt_row=p[1] / state["aif"].shape[0]) else: state = dict(state, pts=state["pts"] + [p] if mode == DUAL and len(state["pts"]) == 1 else [p]) out, depth_view, msg = render(state, strength, mode, gamma, band, show_marker) return aif_view(state, mode), out, depth_view, state, msg def on_change(state, strength, mode, gamma, band, show_marker): if not state: return gr.update(), None, gr.update(), state, "" if mode != DUAL and len(state["pts"]) > 1: state = dict(state, pts=state["pts"][:1]) out, depth_view, msg = render(state, strength, mode, gamma, band, show_marker) return aif_view(state, mode), out, depth_view, state, msg def on_user_depth(state, user_depth, strength, mode, gamma, band, show_marker): if not state: return gr.update(), None, gr.update(), state, "Restore an image first, then upload its depth map." h, w = state["aif"].shape[:2] state = dict(state, user_depth=None if user_depth is None else engine.depth_from_image(user_depth, h, w)) out, depth_view, msg = render(state, strength, mode, gamma, band, show_marker) note = "Using your depth map. " if user_depth is not None else "Using the estimated depth again. " return aif_view(state, mode), out, depth_view, state, note + msg def on_clear(state, gamma): if not state: return gr.update(), None, gr.update(), state, "" state = dict(state, pts=[], tilt_row=0.5) return (Image.fromarray(state["aif"]), None, depth_image(scene_depth(state, gamma)), state, "Focal points cleared. Click the restored image or the depth map to focus.") RECOMMENDED = {"long_side": 1024, "omega": 1.25, "gamma": 1.0, "band": 0.06, "strength": 0.5, "mode": SINGLE, "show_marker": False} def on_reset(state): """Recommended settings (the paper's defaults), the estimated depth again; focal points are kept.""" r = RECOMMENDED controls = [r["long_side"], r["omega"], r["gamma"], r["band"], r["strength"], r["mode"], r["show_marker"], None] if not state: return controls + [gr.update(), None, gr.update(), state, "Settings reset to the recommended values."] state = dict(state, user_depth=None, pts=state["pts"][:1]) out, depth_view, msg = render(state, r["strength"], r["mode"], r["gamma"], r["band"], r["show_marker"]) note = "Settings reset to the recommended values (ω applies on the next Restore). " return controls + [aif_view(state, r["mode"]), out, depth_view, state, note + msg] LOGO = """ """ HEADER = f""" """ CSS = """ #header {margin-bottom: 4px} #header .title {display: flex; align-items: center; justify-content: center; gap: 16px} #header h1 {margin: 0; font-size: 2.1em; color: #2E7D32} #header .title p {margin: 2px 0 0; color: var(--body-text-color-subdued)} #header .authors {text-align: center; margin-top: 10px; line-height: 1.6} #header .authors a {color: #2E7D32; text-decoration: none; font-weight: 600} #header .authors small {color: var(--body-text-color-subdued)} #header .links {display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin: 12px 0} #header .links a {padding: 5px 14px; border-radius: 999px; border: 1px solid #48A43F; color: #2E7D32; text-decoration: none; font-weight: 600} #header .links a:hover {background: #48A43F; color: white} #header .tip {background: #f1f8ef; border-left: 4px solid #48A43F; padding: 10px 14px; border-radius: 8px; color: #1f3d1f} .step h3 {margin: 6px 0 2px; color: #2E7D32} #status {background: #f1f8ef; border-left: 4px solid #48A43F; padding: 8px 14px; border-radius: 8px; min-height: 2.4em} #status p {margin: 0; color: #1f3d1f} """ with gr.Blocks(title="FlashReFocus", theme=gr.themes.Soft(primary_hue="green"), css=CSS) as demo: gr.HTML(HEADER) state = gr.State(None) with gr.Row(): with gr.Column(): gr.Markdown("### ① Upload a photo and restore it once", elem_classes="step") inp = gr.Image(label="Input photograph", type="pil", sources=["upload", "clipboard"], height=320) gr.Examples(EXAMPLES, inputs=inp, label="Examples (Unsplash portraits and our own photographs)") with gr.Accordion("Restoration settings", open=False): long_side = gr.Slider(512, 1536, value=1024, step=64, label="Long side (pixels)") omega = gr.Slider(0.0, 1.5, value=1.25, step=0.25, label="Detail weight ω", info="0: pixel LoRA only (most faithful); higher restores more detail.") btn = gr.Button("✨ Restore", variant="primary", size="lg") gr.Markdown("### ③ Depth map", elem_classes="step") depth = gr.Image(label="Depth used for rendering (near is bright): click to focus", type="pil", interactive=False, height=280) with gr.Accordion("Depth controls", open=False): gamma = gr.Slider(0.4, 2.5, value=1.0, step=0.05, label="Depth curve γ (D → D^γ)", info="< 1 spreads out the near range, > 1 the far range; blur follows the new spacing.") band = gr.Slider(0.02, 0.3, value=0.06, step=0.01, label="Tilt-shift: half-height of the sharp band") user_depth = gr.Image(label="Your own depth map (optional; near is bright, any size)", type="numpy", sources=["upload", "clipboard"], height=150) with gr.Column(): gr.Markdown("### ② Click the restored image to focus", elem_classes="step") aif = gr.Image(label="Restored all-in-focus image (click to focus)", type="pil", interactive=False, height=320) mode = gr.Radio([SINGLE, DUAL, TILT], value=SINGLE, label="Focus mode", info="Two planes: the first click sets A, the second B. Tilt-shift: click the row to keep sharp.") strength = gr.Slider(0.0, 1.5, value=0.5, step=0.05, label="🔆 Aperture (blur strength K)", info="0: everything sharp; 1: the widest aperture seen in training; above 1 extrapolates.") with gr.Row(): show_marker = gr.Checkbox(value=False, label="Show focal markers") clear = gr.Button("Clear focal points", size="sm") reset = gr.Button("↺ Recommended settings", size="sm") status = gr.Markdown("Upload a photo or pick an example, then press ✨ Restore.", elem_id="status") out = gr.Image(label="Refocused result", type="pil", interactive=False, height=430, format="png") controls = [state, strength, mode, gamma, band, show_marker] outputs = [aif, out, depth, state, status] btn.click(restore, [inp, long_side, omega, gamma], [aif, depth, out, state, status]) aif.select(on_click, controls, outputs) depth.select(on_click, controls, outputs) for slider in (strength, gamma, band): slider.release(on_change, controls, outputs) mode.change(on_change, controls, outputs) show_marker.change(on_change, controls, outputs) user_depth.change(on_user_depth, [state, user_depth, strength, mode, gamma, band, show_marker], outputs) clear.click(on_clear, [state, gamma], outputs) reset.click(on_reset, [state], [long_side, omega, gamma, band, strength, mode, show_marker, user_depth] + outputs) if __name__ == "__main__": demo.queue(max_size=20).launch(show_api=False)