Spaces:
Running on Zero
Running on Zero
File size: 11,864 Bytes
8feebce fcc874d 8feebce fcc874d 8feebce fcc874d 8feebce fcc874d 455bb44 fcc874d 8feebce fcc874d 9007883 fcc874d 8feebce fcc874d e3e7fff fcc874d 455bb44 736e070 fcc874d 8feebce fcc874d 8feebce fcc874d 1349d21 fcc874d 455bb44 736e070 455bb44 736e070 455bb44 736e070 455bb44 736e070 455bb44 fcc874d af95dfe fcc874d 455bb44 fcc874d 8feebce fcc874d 455bb44 8feebce fcc874d 8feebce fcc874d 8feebce fcc874d 455bb44 fcc874d 455bb44 e3e7fff 8feebce fcc874d 736e070 455bb44 fcc874d 8feebce 736e070 fcc874d 8feebce 455bb44 fcc874d 1349d21 8feebce fcc874d 455bb44 736e070 fcc874d 736e070 455bb44 736e070 455bb44 fcc874d 455bb44 8feebce 455bb44 fcc874d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 | """OracleZoom demo: upload a photo, watch it zoom 4x and deeper."""
import gradio as gr
import spaces
from PIL import Image, ImageDraw
import geometry
import video
import zoom
MODELS = zoom.Models()
UPSCALE = 4
MAX_STEPS = 8
ACCENT = "#f5b942"
SAMPLES = ["0479", "0064", "0245", "0393", "0457"]
# Shipped output of a real run, so the page shows the payoff before anyone spends any quota.
EXAMPLE_INPUT = "samples/example_1x.png"
EXAMPLE_CLIP = "samples/example_zoom.mp4"
EXAMPLE_LEVELS = [(EXAMPLE_INPUT, "input")] + \
[(f"samples/example_{f}x.png", f"{f}x") for f in (4, 16, 64, 256)]
EXAMPLE_COMPARE = ("samples/example_256x_input.png", "samples/example_256x.png")
EXAMPLE_NOTE = "_An example run. Upload a photo above to make your own._"
HEADER = """
<div style="text-align:center;max-width:820px;margin:0 auto 4px">
<h1 style="margin:0;font-size:2.1em;letter-spacing:-.02em">OracleZoom 🔎</h1>
<p style="margin:.5em 0 .9em;font-size:1.08em;line-height:1.5;opacity:.85">
Zoom into any photo far past what it holds. Four steps of 4x take you to
<b>256x</b>, each one drawn from the last. Push it to eight steps if you want.
</p>
<p style="margin:0;font-size:.95em">
<a href="https://arxiv.org/abs/2609.06490">Paper</a> ·
<a href="https://github.com/dipta007/OracleZoom">Code</a> ·
<a href="https://dipta007.github.io/OracleZoom/">Project page</a> ·
<a href="https://huggingface.co/dipta007/OracleZoom">Model</a> ·
<a href="https://huggingface.co/datasets/dipta007/OracleZoom-4KLSDB-train">Data</a> ·
<a href="https://huggingface.co/collections/dipta007/oraclezoom">Collection</a>
</p>
</div>
"""
BIBTEX = """@misc{dipta2026oraclezoomonpolicyselfdistillationinspired,
title = {OracleZoom: On-Policy Self-Distillation Inspired Reference-Constrained Recursive Image Super Resolution},
author = {Shubhashis Roy Dipta and Sourajit Saha and Shaswati Saha and Nobin Sarwar},
year = {2026},
eprint = {2609.06490},
archivePrefix = {arXiv},
primaryClass = {cs.CV},
url = {https://arxiv.org/abs/2609.06490}
}"""
# Dark only. Gradio otherwise follows the visitor's system setting, and the light theme washes
# the amber accents out against the photos. One redirect on first load, then it sticks.
FORCE_DARK = """
() => {
const url = new URL(window.location);
if (url.searchParams.get('__theme') !== 'dark') {
url.searchParams.set('__theme', 'dark');
window.location.replace(url.href);
}
}
"""
CSS = """
#hero video {border-radius:12px}
.contain {max-width:1400px !important}
footer {display:none !important}
#zoom-target img {cursor:crosshair}
/* Let the level strip size itself. Any fixed height is wrong at some window width or depth:
too short and it scrolls, too tall and it leaves a dead band. .gallery-container is the one
that actually carries the height; styling only the outer block leaves the band behind. */
#levels, #levels .gallery-container, #levels .grid-wrap {
height:auto !important; max-height:none !important; min-height:0 !important;
overflow:visible !important; flex-grow:0 !important}
"""
def label(factor):
return "input" if factor == 1 else f"{factor}x"
def preview(image, levels, cx, cy):
"""Show where the zoom will go, so nobody spends a run to find out.
Falls back to the shipped example when nothing is uploaded yet. Returning None here instead
would blank the box the moment a visitor touched any slider, taking the one thing that
explains what the amber boxes mean with it.
"""
canvas = geometry.resize_and_center_crop(
image if image is not None else Image.open(EXAMPLE_INPUT)).convert("RGB")
rects = geometry.nested_rects(canvas.size, int(levels), UPSCALE, (cx, cy))
dimmed = Image.blend(canvas, Image.new("RGB", canvas.size, (0, 0, 0)), 0.45)
dimmed.paste(canvas.crop(rects[0]), rects[0][:2])
draw = ImageDraw.Draw(dimmed)
for rect in rects:
if rect[2] - rect[0] >= 3:
draw.rectangle(rect, outline=ACCENT, width=2)
return dimmed
def pick_point(evt: gr.SelectData):
x, y = evt.index
# rounded to the sliders' own step, so a click does not read 0.427 on a 0.01 slider
return (round(x / geometry.PROCESS_SIZE, 2), round(y / geometry.PROCESS_SIZE, 2))
def _gallery_paths(gallery):
"""Gradio hands a Gallery back to a handler as (media, caption) tuples, media being a path
because the component is filepath-typed. Not dicts, which is what the payload looks like."""
return [(item[0], item[1] or "") for item in gallery or []
if isinstance(item, (tuple, list)) and len(item) == 2]
def pick_scale(gallery, cx, cy, choice):
"""Rebuild the comparison at whichever scale was asked for.
The left side is the plain enlargement that fed that step, which is the previous level
cropped and scaled back up. Recomputing it from the levels already on screen keeps this
on the CPU, so switching scales costs no GPU quota.
"""
levels = _gallery_paths(gallery)
labels = [lbl for _, lbl in levels]
if choice not in labels:
return gr.skip()
i = labels.index(choice)
if i == 0:
return gr.skip()
prev = Image.open(levels[i - 1][0]).convert("RGB")
blurry = geometry.zoom_window(prev, UPSCALE, (cx, cy)).resize(prev.size, Image.BICUBIC)
return blurry, Image.open(levels[i][0]).convert("RGB")
def scale_choices(levels):
return [label(UPSCALE ** n) for n in range(1, int(levels) + 1)]
CLIP_H = 480 # tuned so the right column ends level with the Zoom in button
def gallery_shape(levels):
"""One row up to five levels, two beyond. Height is left to CSS, which hugs the content."""
items = int(levels) + 1
rows = 1 if items <= 5 else 2
return gr.update(columns=-(-items // rows), rows=rows)
def estimate_duration(image, levels, cx, cy):
# Checked against the visitor's remaining quota BEFORE the run, so a loose number locks out
# low-quota visitors and a tight one gets the run killed mid-way. Measured warm on ZeroGPU:
# 6.4s fixed, 3.2s per level. Near double that, to survive a cold worker streaming weights.
return int(12 + 6 * int(levels))
def _stream(image, levels, cx, cy):
if image is None:
raise gr.Error("Upload a photo first.")
levels = int(levels)
gallery, prompts, frames, compare = [], [], [], None
for step, factor, prompt, blurry, result in zoom.zoom(MODELS, image, levels, UPSCALE, (cx, cy)):
frames.append(result)
gallery.append((result, label(factor)))
if step:
compare = (blurry, result)
prompts.append(f"**{label(factor)}** {prompt or '_(no prompt)_'}")
left = levels - step
if not step:
status = f"Warmed up. Zooming {levels} step{'s' if levels > 1 else ''}…"
elif left:
status = f"At **{label(factor)}**, {left} step{'s' if left > 1 else ''} to go…"
else:
status = f"At **{label(factor)}**. Rendering the clip…"
# First yield clears any clip left from the previous run; later ones leave it alone.
yield (status, (None if not step else gr.skip()), gallery, compare,
"\n\n".join(prompts), gr.skip())
yield ("Done. 🔎", video.render(frames, UPSCALE, (cx, cy)), gallery, compare,
"\n\n".join(prompts),
gr.update(choices=scale_choices(levels), value=label(UPSCALE ** levels)))
@spaces.GPU(duration=estimate_duration)
def run(image, levels, cx, cy):
yield from _stream(image, levels, cx, cy)
@spaces.GPU(duration=estimate_duration(None, 4, 0, 0))
def run_example(image):
"""One input, so the examples render as thumbnails instead of a four-column table.
Its results are cached, which is how a visitor with no quota left still sees output."""
yield from _stream(image, 4, 0.5, 0.5)
def _example_scale_options():
"""Keep the picker's starting options honest about the shipped example's depth."""
return scale_choices(len(EXAMPLE_LEVELS) - 1)
with gr.Blocks(theme=gr.themes.Soft(primary_hue="amber"), css=CSS, js=FORCE_DARK,
title="OracleZoom: zoom past 256x") as demo:
gr.HTML(HEADER)
# Inputs down the left, results down the right. Both columns carry real content the whole
# way, which is what stops one of them stretching a component into an empty band.
with gr.Row(equal_height=False):
with gr.Column(scale=4):
image = gr.Image(label="Your photo", type="pil", height=300,
sources=["upload", "clipboard"])
# Inputs only, no fn: the outputs it would fill live further down the page, and
# Gradio can only wire components that already exist.
gr.Examples(examples=[f"samples/{n}.png" for n in SAMPLES], inputs=[image],
label="Or try any of these")
# Seeded with the example so the box overlay explains itself before any upload.
target = gr.Image(value=preview(None, 4, 0.5, 0.5), elem_id="zoom-target",
label="Click to move the zoom point", type="pil",
interactive=False, height=340, show_download_button=False)
with gr.Row():
cx = gr.Slider(0, 1, value=0.5, step=0.01, label="Horizontal", elem_id="zoom-x")
cy = gr.Slider(0, 1, value=0.5, step=0.01, label="Vertical", elem_id="zoom-y")
levels = gr.Slider(1, MAX_STEPS, value=4, step=1, label="Zoom steps",
info=f"Each step is {UPSCALE}x. 4 steps reach 256x, "
f"{MAX_STEPS} reach {UPSCALE ** MAX_STEPS}x. "
f"Fewer steps finish sooner.")
go = gr.Button("🔎 Zoom in", variant="primary", size="lg")
status = gr.Markdown("Showing an example. Upload a photo, then press **Zoom in**.")
gr.Markdown("##### Vision Language Model at each step:")
prompts = gr.Markdown(EXAMPLE_NOTE)
with gr.Column(scale=6):
clip = gr.Video(value=EXAMPLE_CLIP, label="The zoom", elem_id="hero", autoplay=True,
loop=True, show_share_button=True, height=CLIP_H)
gallery = gr.Gallery(value=EXAMPLE_LEVELS, label="Every level, start to finish",
elem_id="levels", object_fit="cover", show_download_button=True,
**{k: v for k, v in gallery_shape(len(EXAMPLE_LEVELS) - 1).items()
if k != "__type__"})
scale = gr.Radio(_example_scale_options(), value=EXAMPLE_LEVELS[-1][1],
label="Compare at",
info="Left is the plain enlargement that step began from, "
"right is what OracleZoom drew. Switching is free.")
compare = gr.ImageSlider(value=EXAMPLE_COMPARE, height=470, elem_id="compare",
show_label=False)
controls = [image, levels, cx, cy]
for c in controls:
c.change(preview, controls, target, show_api=False)
target.select(pick_point, None, [cx, cy], show_api=False)
# Reshape before the run, not after, so the strip does not resize under the results.
levels.change(gallery_shape, levels, gallery, show_api=False)
go.click(run, controls, [status, clip, gallery, compare, prompts, scale])
scale.change(pick_scale, [gallery, cx, cy, scale], compare, show_api=False)
gr.Markdown("### Cite our work")
gr.Code(value=BIBTEX, language=None, show_label=False, container=False)
if __name__ == "__main__":
demo.queue(max_size=20).launch()
|