Spaces:
Sleeping
Sleeping
Drop the comparison slider, drag the zoom point, allow 8 steps
Browse files
app.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""OracleZoom demo: upload a photo, watch it zoom 4x
|
| 2 |
import gradio as gr
|
| 3 |
import spaces
|
| 4 |
from PIL import Image, ImageDraw
|
|
@@ -10,15 +10,15 @@ import zoom
|
|
| 10 |
MODELS = zoom.Models()
|
| 11 |
|
| 12 |
UPSCALE = 4
|
|
|
|
| 13 |
ACCENT = "#f5b942"
|
| 14 |
-
LABELS = {1: "input", 4: "4x", 16: "16x", 64: "64x", 256: "256x"}
|
| 15 |
SAMPLES = ["0479", "0064", "0245", "0393", "0457"]
|
| 16 |
|
| 17 |
# Shipped output of a real run, so the page shows the payoff before anyone spends any quota.
|
|
|
|
| 18 |
EXAMPLE_CLIP = "samples/example_zoom.mp4"
|
| 19 |
-
EXAMPLE_LEVELS = [(
|
| 20 |
[(f"samples/example_{f}x.png", f"{f}x") for f in (4, 16, 64, 256)]
|
| 21 |
-
EXAMPLE_COMPARE = ("samples/example_256x_input.png", "samples/example_256x.png")
|
| 22 |
EXAMPLE_NOTE = "_An example run. Upload a photo above to make your own._"
|
| 23 |
|
| 24 |
HEADER = """
|
|
@@ -26,7 +26,7 @@ HEADER = """
|
|
| 26 |
<h1 style="margin:0;font-size:2.1em;letter-spacing:-.02em">OracleZoom 🔎</h1>
|
| 27 |
<p style="margin:.5em 0 .9em;font-size:1.08em;line-height:1.5;opacity:.85">
|
| 28 |
Zoom into any photo far past what it holds. Four steps of 4x take you to
|
| 29 |
-
<b>256x</b>, each one drawn from the last.
|
| 30 |
</p>
|
| 31 |
<p style="margin:0;font-size:.95em">
|
| 32 |
<a href="https://arxiv.org/abs/2609.06490">Paper</a> ·
|
|
@@ -37,50 +37,76 @@ HEADER = """
|
|
| 37 |
</div>
|
| 38 |
"""
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
|
|
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
### What you are looking at
|
| 53 |
-
|
| 54 |
-
Standard super-resolution models break down well before 16x. OracleZoom gets to 256x by
|
| 55 |
-
zooming one 4x step at a time and feeding each result into the next step. At every step a
|
| 56 |
-
vision language model writes a short description of the crop, and that description guides
|
| 57 |
-
the detail the super-resolution model draws.
|
| 58 |
-
|
| 59 |
-
Past the first step or two there is no ground truth to recover, so the deep levels are
|
| 60 |
-
**plausible detail, not measured detail**. The zoom point is a crop of your photo, not a
|
| 61 |
-
real camera lens moving closer. Read the paper for what we do and do not claim.
|
| 62 |
-
|
| 63 |
-
```bibtex
|
| 64 |
-
@inproceedings{dipta2027oraclezoom,
|
| 65 |
-
title = {OracleZoom: Reference-Constrained Recursive Super-Resolution},
|
| 66 |
-
author = {Roy Dipta, Shubhashis and Saha, Sourajit and Saha, Shaswati and Sarwar, Nobin},
|
| 67 |
-
year = {2027}
|
| 68 |
-
}
|
| 69 |
-
```
|
| 70 |
-
"""
|
| 71 |
|
| 72 |
CSS = """
|
| 73 |
#hero video {border-radius:12px}
|
| 74 |
.contain {max-width:1400px !important}
|
| 75 |
footer {display:none !important}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
"""
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
def preview(image, levels, cx, cy):
|
| 80 |
-
"""Show where the zoom will go, so nobody spends a run to find out.
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
rects = geometry.nested_rects(canvas.size, int(levels), UPSCALE, (cx, cy))
|
| 85 |
dimmed = Image.blend(canvas, Image.new("RGB", canvas.size, (0, 0, 0)), 0.45)
|
| 86 |
dimmed.paste(canvas.crop(rects[0]), rects[0][:2])
|
|
@@ -107,24 +133,22 @@ def _stream(image, levels, cx, cy):
|
|
| 107 |
if image is None:
|
| 108 |
raise gr.Error("Upload a photo first.")
|
| 109 |
levels = int(levels)
|
| 110 |
-
gallery, prompts, frames
|
| 111 |
-
for step, factor, prompt,
|
| 112 |
frames.append(result)
|
| 113 |
-
gallery.append((result,
|
| 114 |
if step:
|
| 115 |
-
|
| 116 |
-
prompts.append(f"**{LABELS[factor]}** {prompt or '_(no prompt)_'}")
|
| 117 |
left = levels - step
|
| 118 |
if not step:
|
| 119 |
status = f"Warmed up. Zooming {levels} step{'s' if levels > 1 else ''}…"
|
| 120 |
elif left:
|
| 121 |
-
status = f"At **{
|
| 122 |
else:
|
| 123 |
-
status = f"At **{
|
| 124 |
# First yield clears any clip left from the previous run; later ones leave it alone.
|
| 125 |
-
yield status, (None if not step else gr.skip()), gallery,
|
| 126 |
-
yield
|
| 127 |
-
"\n\n".join(prompts))
|
| 128 |
|
| 129 |
|
| 130 |
@spaces.GPU(duration=estimate_duration)
|
|
@@ -140,48 +164,53 @@ def run_example(image):
|
|
| 140 |
|
| 141 |
|
| 142 |
with gr.Blocks(theme=gr.themes.Soft(primary_hue="amber"), css=CSS,
|
| 143 |
-
title="OracleZoom: zoom
|
| 144 |
gr.HTML(HEADER)
|
| 145 |
with gr.Row():
|
| 146 |
with gr.Column(scale=4):
|
| 147 |
-
image = gr.Image(label="Your photo", type="pil", height=300,
|
|
|
|
| 148 |
# Seeded with the example so the box overlay explains itself before any upload.
|
| 149 |
-
target = gr.Image(value=preview(
|
| 150 |
-
label="
|
| 151 |
interactive=False, height=340, show_download_button=False)
|
| 152 |
-
levels = gr.Slider(1,
|
| 153 |
-
info="
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
| 157 |
go = gr.Button("🔎 Zoom in", variant="primary", size="lg")
|
| 158 |
-
gr.Markdown(HOW)
|
| 159 |
with gr.Column(scale=6):
|
| 160 |
status = gr.Markdown("Upload a photo, then press **Zoom in**.")
|
| 161 |
clip = gr.Video(value=EXAMPLE_CLIP, label="The zoom", elem_id="hero", autoplay=True,
|
| 162 |
loop=True, show_share_button=True, height=400)
|
| 163 |
-
gallery = gr.Gallery(value=EXAMPLE_LEVELS, label="Every level", columns=5, height=
|
| 164 |
-
object_fit="cover", show_download_button=True)
|
| 165 |
-
|
| 166 |
-
label="256x: plain enlargement (left) vs OracleZoom (right)")
|
| 167 |
-
gr.Markdown("##### What the model said it saw, step by step")
|
| 168 |
prompts = gr.Markdown(EXAMPLE_NOTE)
|
| 169 |
|
| 170 |
controls = [image, levels, cx, cy]
|
|
|
|
| 171 |
for c in controls:
|
| 172 |
c.change(preview, controls, target, show_api=False)
|
| 173 |
target.select(pick_point, None, [cx, cy], show_api=False)
|
| 174 |
-
go.click(run, controls,
|
| 175 |
|
| 176 |
gr.Examples(
|
| 177 |
examples=[f"samples/{n}.png" for n in SAMPLES],
|
| 178 |
inputs=[image],
|
| 179 |
-
outputs=
|
| 180 |
fn=run_example,
|
| 181 |
cache_examples=True,
|
| 182 |
label="Or try one of these (already computed, costs you nothing)",
|
| 183 |
)
|
| 184 |
-
gr.Markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
if __name__ == "__main__":
|
| 187 |
demo.queue(max_size=20).launch()
|
|
|
|
| 1 |
+
"""OracleZoom demo: upload a photo, watch it zoom 4x and deeper."""
|
| 2 |
import gradio as gr
|
| 3 |
import spaces
|
| 4 |
from PIL import Image, ImageDraw
|
|
|
|
| 10 |
MODELS = zoom.Models()
|
| 11 |
|
| 12 |
UPSCALE = 4
|
| 13 |
+
MAX_STEPS = 8
|
| 14 |
ACCENT = "#f5b942"
|
|
|
|
| 15 |
SAMPLES = ["0479", "0064", "0245", "0393", "0457"]
|
| 16 |
|
| 17 |
# Shipped output of a real run, so the page shows the payoff before anyone spends any quota.
|
| 18 |
+
EXAMPLE_INPUT = "samples/example_1x.png"
|
| 19 |
EXAMPLE_CLIP = "samples/example_zoom.mp4"
|
| 20 |
+
EXAMPLE_LEVELS = [(EXAMPLE_INPUT, "input")] + \
|
| 21 |
[(f"samples/example_{f}x.png", f"{f}x") for f in (4, 16, 64, 256)]
|
|
|
|
| 22 |
EXAMPLE_NOTE = "_An example run. Upload a photo above to make your own._"
|
| 23 |
|
| 24 |
HEADER = """
|
|
|
|
| 26 |
<h1 style="margin:0;font-size:2.1em;letter-spacing:-.02em">OracleZoom 🔎</h1>
|
| 27 |
<p style="margin:.5em 0 .9em;font-size:1.08em;line-height:1.5;opacity:.85">
|
| 28 |
Zoom into any photo far past what it holds. Four steps of 4x take you to
|
| 29 |
+
<b>256x</b>, each one drawn from the last. Push it to eight steps if you want.
|
| 30 |
</p>
|
| 31 |
<p style="margin:0;font-size:.95em">
|
| 32 |
<a href="https://arxiv.org/abs/2609.06490">Paper</a> ·
|
|
|
|
| 37 |
</div>
|
| 38 |
"""
|
| 39 |
|
| 40 |
+
LIMITS = ("Past the first step or two there is no ground truth left to recover, so the deep "
|
| 41 |
+
"levels are **plausible detail, not measured detail**. The zoom is a crop of your "
|
| 42 |
+
"photo, not a real lens moving closer.")
|
| 43 |
|
| 44 |
+
BIBTEX = """@misc{dipta2026oraclezoomonpolicyselfdistillationinspired,
|
| 45 |
+
title = {OracleZoom: On-Policy Self-Distillation Inspired Reference-Constrained Recursive Image Super Resolution},
|
| 46 |
+
author = {Shubhashis Roy Dipta and Sourajit Saha and Shaswati Saha and Nobin Sarwar},
|
| 47 |
+
year = {2026},
|
| 48 |
+
eprint = {2609.06490},
|
| 49 |
+
archivePrefix = {arXiv},
|
| 50 |
+
primaryClass = {cs.CV},
|
| 51 |
+
url = {https://arxiv.org/abs/2609.06490}
|
| 52 |
+
}"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
CSS = """
|
| 55 |
#hero video {border-radius:12px}
|
| 56 |
.contain {max-width:1400px !important}
|
| 57 |
footer {display:none !important}
|
| 58 |
+
#zoom-target img {cursor:grab}
|
| 59 |
+
#zoom-target img:active {cursor:grabbing}
|
| 60 |
+
/* a deep run wraps to two rows; keep any scrollbar from rendering light on a dark theme */
|
| 61 |
+
#levels, #levels * {scrollbar-color: var(--border-color-primary) transparent}
|
| 62 |
"""
|
| 63 |
|
| 64 |
+
# Drag the zoom point straight on the preview. Throttled, because every move asks the server to
|
| 65 |
+
# redraw. Clicking and the two sliders both still work if this ever stops matching the DOM.
|
| 66 |
+
DRAG_JS = """
|
| 67 |
+
() => {
|
| 68 |
+
if (window.__ozDrag) return; // load can fire more than once; keep one state
|
| 69 |
+
const S = window.__ozDrag = {on: false, last: 0};
|
| 70 |
+
const send = (e, force) => {
|
| 71 |
+
const img = document.querySelector('#zoom-target img');
|
| 72 |
+
if (!img) return;
|
| 73 |
+
const now = Date.now();
|
| 74 |
+
if (!force && now - S.last < 120) return;
|
| 75 |
+
S.last = now;
|
| 76 |
+
const r = img.getBoundingClientRect();
|
| 77 |
+
const at = (v, lo, span) => Math.min(Math.max((v - lo) / span, 0), 1);
|
| 78 |
+
for (const [id, v] of [['zoom-x', at(e.clientX, r.left, r.width)],
|
| 79 |
+
['zoom-y', at(e.clientY, r.top, r.height)]]) {
|
| 80 |
+
const el = document.querySelector('#' + id + ' input[type=range]');
|
| 81 |
+
if (!el) continue;
|
| 82 |
+
el.value = v.toFixed(2);
|
| 83 |
+
el.dispatchEvent(new Event('input', {bubbles: true}));
|
| 84 |
+
}
|
| 85 |
+
};
|
| 86 |
+
// delegated off document, so Gradio swapping the img or the sliders mid-drag is harmless
|
| 87 |
+
document.addEventListener('mousedown', e => {
|
| 88 |
+
if (!e.target.closest('#zoom-target img')) return;
|
| 89 |
+
S.on = true; e.preventDefault(); send(e, true);
|
| 90 |
+
}, true);
|
| 91 |
+
document.addEventListener('mousemove', e => { if (S.on) send(e, false); });
|
| 92 |
+
document.addEventListener('mouseup', e => { if (S.on) { S.on = false; send(e, true); } });
|
| 93 |
+
}
|
| 94 |
+
"""
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def label(factor):
|
| 98 |
+
return "input" if factor == 1 else f"{factor}x"
|
| 99 |
+
|
| 100 |
|
| 101 |
def preview(image, levels, cx, cy):
|
| 102 |
+
"""Show where the zoom will go, so nobody spends a run to find out.
|
| 103 |
+
|
| 104 |
+
Falls back to the shipped example when nothing is uploaded yet. Returning None here instead
|
| 105 |
+
would blank the box the moment a visitor touched any slider, taking the one thing that
|
| 106 |
+
explains what the amber boxes mean with it.
|
| 107 |
+
"""
|
| 108 |
+
canvas = geometry.resize_and_center_crop(
|
| 109 |
+
image if image is not None else Image.open(EXAMPLE_INPUT)).convert("RGB")
|
| 110 |
rects = geometry.nested_rects(canvas.size, int(levels), UPSCALE, (cx, cy))
|
| 111 |
dimmed = Image.blend(canvas, Image.new("RGB", canvas.size, (0, 0, 0)), 0.45)
|
| 112 |
dimmed.paste(canvas.crop(rects[0]), rects[0][:2])
|
|
|
|
| 133 |
if image is None:
|
| 134 |
raise gr.Error("Upload a photo first.")
|
| 135 |
levels = int(levels)
|
| 136 |
+
gallery, prompts, frames = [], [], []
|
| 137 |
+
for step, factor, prompt, _blurry, result in zoom.zoom(MODELS, image, levels, UPSCALE, (cx, cy)):
|
| 138 |
frames.append(result)
|
| 139 |
+
gallery.append((result, label(factor)))
|
| 140 |
if step:
|
| 141 |
+
prompts.append(f"**{label(factor)}** {prompt or '_(no prompt)_'}")
|
|
|
|
| 142 |
left = levels - step
|
| 143 |
if not step:
|
| 144 |
status = f"Warmed up. Zooming {levels} step{'s' if levels > 1 else ''}…"
|
| 145 |
elif left:
|
| 146 |
+
status = f"At **{label(factor)}**, {left} step{'s' if left > 1 else ''} to go…"
|
| 147 |
else:
|
| 148 |
+
status = f"At **{label(factor)}**. Rendering the clip…"
|
| 149 |
# First yield clears any clip left from the previous run; later ones leave it alone.
|
| 150 |
+
yield status, (None if not step else gr.skip()), gallery, "\n\n".join(prompts)
|
| 151 |
+
yield "Done. 🔎", video.render(frames, UPSCALE, (cx, cy)), gallery, "\n\n".join(prompts)
|
|
|
|
| 152 |
|
| 153 |
|
| 154 |
@spaces.GPU(duration=estimate_duration)
|
|
|
|
| 164 |
|
| 165 |
|
| 166 |
with gr.Blocks(theme=gr.themes.Soft(primary_hue="amber"), css=CSS,
|
| 167 |
+
title="OracleZoom: zoom past 256x") as demo:
|
| 168 |
gr.HTML(HEADER)
|
| 169 |
with gr.Row():
|
| 170 |
with gr.Column(scale=4):
|
| 171 |
+
image = gr.Image(label="Your photo", type="pil", height=300,
|
| 172 |
+
sources=["upload", "clipboard"])
|
| 173 |
# Seeded with the example so the box overlay explains itself before any upload.
|
| 174 |
+
target = gr.Image(value=preview(None, 4, 0.5, 0.5), elem_id="zoom-target",
|
| 175 |
+
label="Drag to move the zoom point", type="pil",
|
| 176 |
interactive=False, height=340, show_download_button=False)
|
| 177 |
+
levels = gr.Slider(1, MAX_STEPS, value=4, step=1, label="Zoom steps",
|
| 178 |
+
info=f"Each step is {UPSCALE}x. 4 steps reach 256x, "
|
| 179 |
+
f"{MAX_STEPS} reach {UPSCALE ** MAX_STEPS}x. "
|
| 180 |
+
f"Fewer steps finish sooner.")
|
| 181 |
+
with gr.Row():
|
| 182 |
+
cx = gr.Slider(0, 1, value=0.5, step=0.01, label="Horizontal", elem_id="zoom-x")
|
| 183 |
+
cy = gr.Slider(0, 1, value=0.5, step=0.01, label="Vertical", elem_id="zoom-y")
|
| 184 |
go = gr.Button("🔎 Zoom in", variant="primary", size="lg")
|
|
|
|
| 185 |
with gr.Column(scale=6):
|
| 186 |
status = gr.Markdown("Upload a photo, then press **Zoom in**.")
|
| 187 |
clip = gr.Video(value=EXAMPLE_CLIP, label="The zoom", elem_id="hero", autoplay=True,
|
| 188 |
loop=True, show_share_button=True, height=400)
|
| 189 |
+
gallery = gr.Gallery(value=EXAMPLE_LEVELS, label="Every level", columns=5, height=205,
|
| 190 |
+
elem_id="levels", object_fit="cover", show_download_button=True)
|
| 191 |
+
gr.Markdown("##### Vision Language Model at each step:")
|
|
|
|
|
|
|
| 192 |
prompts = gr.Markdown(EXAMPLE_NOTE)
|
| 193 |
|
| 194 |
controls = [image, levels, cx, cy]
|
| 195 |
+
outputs = [status, clip, gallery, prompts]
|
| 196 |
for c in controls:
|
| 197 |
c.change(preview, controls, target, show_api=False)
|
| 198 |
target.select(pick_point, None, [cx, cy], show_api=False)
|
| 199 |
+
go.click(run, controls, outputs)
|
| 200 |
|
| 201 |
gr.Examples(
|
| 202 |
examples=[f"samples/{n}.png" for n in SAMPLES],
|
| 203 |
inputs=[image],
|
| 204 |
+
outputs=outputs,
|
| 205 |
fn=run_example,
|
| 206 |
cache_examples=True,
|
| 207 |
label="Or try one of these (already computed, costs you nothing)",
|
| 208 |
)
|
| 209 |
+
gr.Markdown(f"{LIMITS} See the [paper](https://arxiv.org/abs/2609.06490) for the claims we "
|
| 210 |
+
f"do and do not make.\n\n### Cite our work")
|
| 211 |
+
gr.Code(value=BIBTEX, language=None, show_label=False, container=False)
|
| 212 |
+
|
| 213 |
+
demo.load(None, None, None, js=DRAG_JS, show_api=False)
|
| 214 |
|
| 215 |
if __name__ == "__main__":
|
| 216 |
demo.queue(max_size=20).launch()
|