dipta007 commited on
Commit
455bb44
·
verified ·
1 Parent(s): c2d2e7f

Bring back the comparison with a scale picker; reflow the layout

Browse files
Files changed (3) hide show
  1. app.py +96 -63
  2. samples/example_256x_input.png +3 -0
  3. test_app_smoke.py +53 -4
app.py CHANGED
@@ -19,6 +19,7 @@ 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 = """
@@ -37,10 +38,6 @@ HEADER = """
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},
@@ -55,44 +52,11 @@ 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"
@@ -122,6 +86,49 @@ def pick_point(evt: gr.SelectData):
122
  return x / geometry.PROCESS_SIZE, y / geometry.PROCESS_SIZE
123
 
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  def estimate_duration(image, levels, cx, cy):
126
  # Checked against the visitor's remaining quota BEFORE the run, so a loose number locks out
127
  # low-quota visitors and a tight one gets the run killed mid-way. Measured warm on ZeroGPU:
@@ -133,11 +140,12 @@ def _stream(image, levels, cx, cy):
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)}** &nbsp; {prompt or '_(no prompt)_'}")
142
  left = levels - step
143
  if not step:
@@ -147,8 +155,11 @@ def _stream(image, levels, cx, cy):
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)
@@ -163,16 +174,23 @@ def run_example(image):
163
  yield from _stream(image, 4, 0.5, 0.5)
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, "
@@ -182,35 +200,50 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="amber"), css=CSS,
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()
 
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_COMPARE = ("samples/example_256x_input.png", "samples/example_256x.png")
23
  EXAMPLE_NOTE = "_An example run. Upload a photo above to make your own._"
24
 
25
  HEADER = """
 
38
  </div>
39
  """
40
 
 
 
 
 
41
  BIBTEX = """@misc{dipta2026oraclezoomonpolicyselfdistillationinspired,
42
  title = {OracleZoom: On-Policy Self-Distillation Inspired Reference-Constrained Recursive Image Super Resolution},
43
  author = {Shubhashis Roy Dipta and Sourajit Saha and Shaswati Saha and Nobin Sarwar},
 
52
  #hero video {border-radius:12px}
53
  .contain {max-width:1400px !important}
54
  footer {display:none !important}
55
+ #zoom-target img {cursor:crosshair}
 
56
  /* a deep run wraps to two rows; keep any scrollbar from rendering light on a dark theme */
57
  #levels, #levels * {scrollbar-color: var(--border-color-primary) transparent}
58
  """
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
  def label(factor):
62
  return "input" if factor == 1 else f"{factor}x"
 
86
  return x / geometry.PROCESS_SIZE, y / geometry.PROCESS_SIZE
87
 
88
 
89
+ def _gallery_paths(gallery):
90
+ """The (path, label) pairs Gradio hands back for a Gallery value."""
91
+ out = []
92
+ for item in gallery or []:
93
+ img = item.get("image", item) if isinstance(item, dict) else item
94
+ path = img.get("path") if isinstance(img, dict) else img
95
+ out.append((path, (item.get("caption") if isinstance(item, dict) else None) or ""))
96
+ return out
97
+
98
+
99
+ def pick_scale(gallery, cx, cy, choice):
100
+ """Rebuild the comparison at whichever scale was asked for.
101
+
102
+ The left side is the plain enlargement that fed that step, which is the previous level
103
+ cropped and scaled back up. Recomputing it from the levels already on screen keeps this
104
+ on the CPU, so switching scales costs no GPU quota.
105
+ """
106
+ levels = _gallery_paths(gallery)
107
+ labels = [lbl for _, lbl in levels]
108
+ if choice not in labels:
109
+ return gr.skip()
110
+ i = labels.index(choice)
111
+ if i == 0:
112
+ return gr.skip()
113
+ prev = Image.open(levels[i - 1][0]).convert("RGB")
114
+ blurry = geometry.zoom_window(prev, UPSCALE, (cx, cy)).resize(prev.size, Image.BICUBIC)
115
+ return blurry, Image.open(levels[i][0]).convert("RGB")
116
+
117
+
118
+ def scale_choices(levels):
119
+ return [label(UPSCALE ** n) for n in range(1, int(levels) + 1)]
120
+
121
+
122
+ GALLERY_ROW_H = 245
123
+
124
+
125
+ def gallery_shape(levels):
126
+ """One row up to five levels, two beyond. Tall enough that neither row ever scrolls."""
127
+ items = int(levels) + 1
128
+ rows = 1 if items <= 5 else 2
129
+ return gr.update(columns=-(-items // rows), rows=rows, height=GALLERY_ROW_H * rows)
130
+
131
+
132
  def estimate_duration(image, levels, cx, cy):
133
  # Checked against the visitor's remaining quota BEFORE the run, so a loose number locks out
134
  # low-quota visitors and a tight one gets the run killed mid-way. Measured warm on ZeroGPU:
 
140
  if image is None:
141
  raise gr.Error("Upload a photo first.")
142
  levels = int(levels)
143
+ gallery, prompts, frames, compare = [], [], [], None
144
+ for step, factor, prompt, blurry, result in zoom.zoom(MODELS, image, levels, UPSCALE, (cx, cy)):
145
  frames.append(result)
146
  gallery.append((result, label(factor)))
147
  if step:
148
+ compare = (blurry, result)
149
  prompts.append(f"**{label(factor)}** &nbsp; {prompt or '_(no prompt)_'}")
150
  left = levels - step
151
  if not step:
 
155
  else:
156
  status = f"At **{label(factor)}**. Rendering the clip…"
157
  # First yield clears any clip left from the previous run; later ones leave it alone.
158
+ yield (status, (None if not step else gr.skip()), gallery, compare,
159
+ "\n\n".join(prompts), gr.skip())
160
+ yield ("Done. 🔎", video.render(frames, UPSCALE, (cx, cy)), gallery, compare,
161
+ "\n\n".join(prompts),
162
+ gr.update(choices=scale_choices(levels), value=label(UPSCALE ** levels)))
163
 
164
 
165
  @spaces.GPU(duration=estimate_duration)
 
174
  yield from _stream(image, 4, 0.5, 0.5)
175
 
176
 
177
+ def _example_scale_options():
178
+ """Keep the picker's starting options honest about the shipped example's depth."""
179
+ return scale_choices(len(EXAMPLE_LEVELS) - 1)
180
+
181
+
182
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="amber"), css=CSS,
183
  title="OracleZoom: zoom past 256x") as demo:
184
  gr.HTML(HEADER)
185
+ # Three wide rows, not one tall stack: set it up, watch it, then inspect it. Each row uses
186
+ # the full width, which is what keeps four large visuals from queueing up vertically.
187
+ with gr.Row(equal_height=False):
188
  with gr.Column(scale=4):
189
  image = gr.Image(label="Your photo", type="pil", height=300,
190
  sources=["upload", "clipboard"])
191
  # Seeded with the example so the box overlay explains itself before any upload.
192
  target = gr.Image(value=preview(None, 4, 0.5, 0.5), elem_id="zoom-target",
193
+ label="Click to move the zoom point", type="pil",
194
  interactive=False, height=340, show_download_button=False)
195
  levels = gr.Slider(1, MAX_STEPS, value=4, step=1, label="Zoom steps",
196
  info=f"Each step is {UPSCALE}x. 4 steps reach 256x, "
 
200
  cx = gr.Slider(0, 1, value=0.5, step=0.01, label="Horizontal", elem_id="zoom-x")
201
  cy = gr.Slider(0, 1, value=0.5, step=0.01, label="Vertical", elem_id="zoom-y")
202
  go = gr.Button("🔎 Zoom in", variant="primary", size="lg")
203
+ status = gr.Markdown("Showing an example. Upload a photo, then press **Zoom in**.")
204
  with gr.Column(scale=6):
 
205
  clip = gr.Video(value=EXAMPLE_CLIP, label="The zoom", elem_id="hero", autoplay=True,
206
+ loop=True, show_share_button=True, height=470)
207
+
208
+ with gr.Row():
209
+ gallery = gr.Gallery(value=EXAMPLE_LEVELS, label="Every level, start to finish",
210
+ elem_id="levels", object_fit="cover", show_download_button=True,
211
+ **{k: v for k, v in gallery_shape(len(EXAMPLE_LEVELS) - 1).items()
212
+ if k != "__type__"})
213
+
214
+ with gr.Row(equal_height=False):
215
+ with gr.Column(scale=5):
216
+ scale = gr.Radio(_example_scale_options(), value=EXAMPLE_LEVELS[-1][1],
217
+ label="Compare at",
218
+ info="Left is the plain enlargement that step began from, "
219
+ "right is what OracleZoom drew. Switching is free.")
220
+ compare = gr.ImageSlider(value=EXAMPLE_COMPARE, height=480, elem_id="compare",
221
+ show_label=False)
222
+ with gr.Column(scale=5):
223
  gr.Markdown("##### Vision Language Model at each step:")
224
  prompts = gr.Markdown(EXAMPLE_NOTE)
225
 
226
+ with gr.Row():
227
+ gr.Examples(
228
+ examples=[f"samples/{n}.png" for n in SAMPLES],
229
+ inputs=[image],
230
+ outputs=[status, clip, gallery, compare, prompts, scale],
231
+ fn=run_example,
232
+ cache_examples=True,
233
+ label="Or try one of these (already computed)",
234
+ )
235
+
236
  controls = [image, levels, cx, cy]
 
237
  for c in controls:
238
  c.change(preview, controls, target, show_api=False)
239
  target.select(pick_point, None, [cx, cy], show_api=False)
240
+ # Reshape before the run, not after, so the strip does not resize under the results.
241
+ levels.change(gallery_shape, levels, gallery, show_api=False)
242
+ go.click(run, controls, [status, clip, gallery, compare, prompts, scale])
243
+ scale.change(pick_scale, [gallery, cx, cy, scale], compare, show_api=False)
 
 
 
 
 
 
 
 
 
244
 
245
+ gr.Markdown("### Cite our work")
246
+ gr.Code(value=BIBTEX, language=None, show_label=False, container=False)
247
 
248
  if __name__ == "__main__":
249
  demo.queue(max_size=20).launch()
samples/example_256x_input.png ADDED

Git LFS Details

  • SHA256: c4180ea285ecf61b1618029e8373f5b2c5efe40403e71baa796d58ba982f435d
  • Pointer size: 131 Bytes
  • Size of remote file: 205 kB
test_app_smoke.py CHANGED
@@ -61,7 +61,7 @@ def test_declared_duration_grows_with_depth(app, photo):
61
 
62
 
63
  def test_the_shipped_example_assets_exist(app):
64
- for path in [app.EXAMPLE_CLIP, *(p for p, _ in app.EXAMPLE_LEVELS)]:
65
  assert os.path.getsize(path) > 1000, path
66
  assert [label for _, label in app.EXAMPLE_LEVELS] == ["input", "4x", "16x", "64x", "256x"]
67
 
@@ -87,10 +87,11 @@ def test_every_sample_named_for_the_examples_is_present(app):
87
  def test_run_streams_one_update_per_level_then_the_clip(app, photo):
88
  yields = list(app.run(photo, 3, 0.5, 0.5))
89
  assert len(yields) == 5 # entry level, 3 zoom steps, then the clip
90
- assert all(len(y) == 4 for y in yields)
91
  assert yields[0][1] is None # first yield clears any stale clip
92
- status, clip, gallery, prompts = yields[-1]
93
  assert [label for _, label in gallery] == ["input", "4x", "16x", "64x"]
 
94
  assert prompts.count("tag") == 3
95
  try:
96
  assert os.path.getsize(clip) > 10_000
@@ -98,11 +99,59 @@ def test_run_streams_one_update_per_level_then_the_clip(app, photo):
98
  os.remove(clip)
99
 
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  def test_a_deep_run_labels_every_level(app, photo):
102
- status, clip, gallery, prompts = list(app.run(photo, app.MAX_STEPS, 0.5, 0.5))[-1]
103
  try:
104
  assert [label for _, label in gallery] == [
105
  "input", "4x", "16x", "64x", "256x", "1024x", "4096x", "16384x", "65536x"]
 
106
  finally:
107
  os.remove(clip)
108
 
 
61
 
62
 
63
  def test_the_shipped_example_assets_exist(app):
64
+ for path in [app.EXAMPLE_CLIP, *app.EXAMPLE_COMPARE, *(p for p, _ in app.EXAMPLE_LEVELS)]:
65
  assert os.path.getsize(path) > 1000, path
66
  assert [label for _, label in app.EXAMPLE_LEVELS] == ["input", "4x", "16x", "64x", "256x"]
67
 
 
87
  def test_run_streams_one_update_per_level_then_the_clip(app, photo):
88
  yields = list(app.run(photo, 3, 0.5, 0.5))
89
  assert len(yields) == 5 # entry level, 3 zoom steps, then the clip
90
+ assert all(len(y) == 6 for y in yields)
91
  assert yields[0][1] is None # first yield clears any stale clip
92
+ status, clip, gallery, compare, prompts, _scale = yields[-1]
93
  assert [label for _, label in gallery] == ["input", "4x", "16x", "64x"]
94
+ assert compare is not None and len(compare) == 2
95
  assert prompts.count("tag") == 3
96
  try:
97
  assert os.path.getsize(clip) > 10_000
 
99
  os.remove(clip)
100
 
101
 
102
+ def test_the_comparison_shows_the_deepest_level(app, photo):
103
+ """Left is what the model started that step from, right is what it produced."""
104
+ *_, (_, _, gallery, compare, _, scale) = list(app.run(photo, 2, 0.5, 0.5))
105
+ assert compare[1] is gallery[-1][0]
106
+ assert scale["choices"] == ["4x", "16x"] and scale["value"] == "16x"
107
+
108
+
109
+ def test_the_scale_picker_offers_one_option_per_step(app):
110
+ assert app.scale_choices(1) == ["4x"]
111
+ assert app.scale_choices(4) == ["4x", "16x", "64x", "256x"]
112
+ assert app.scale_choices(app.MAX_STEPS)[-1] == "65536x"
113
+ assert "input" not in app.scale_choices(app.MAX_STEPS)
114
+
115
+
116
+ def test_switching_scale_rebuilds_the_pair_without_the_gpu(app):
117
+ """Runs off the level images already on screen, so it must not touch zoom.zoom."""
118
+ gallery = [{"image": {"path": p}, "caption": c} for p, c in app.EXAMPLE_LEVELS]
119
+ left, right = app.pick_scale(gallery, 0.5, 0.5, "16x")
120
+ assert left.size == right.size == (512, 512)
121
+ from PIL import Image
122
+ import numpy as np
123
+ expected = np.asarray(Image.open("samples/example_16x.png").convert("RGB"))
124
+ assert np.array_equal(np.asarray(right), expected)
125
+
126
+
127
+ def test_switching_to_a_scale_that_was_not_run_changes_nothing(app):
128
+ import gradio as gr
129
+ gallery = [{"image": {"path": p}, "caption": c} for p, c in app.EXAMPLE_LEVELS]
130
+ assert app.pick_scale(gallery, 0.5, 0.5, "65536x") == gr.skip()
131
+ assert app.pick_scale(gallery, 0.5, 0.5, "input") == gr.skip()
132
+
133
+
134
+ def test_the_level_strip_wraps_to_two_rows_instead_of_scrolling(app):
135
+ """Six levels or more will not fit one row at a readable size."""
136
+ for steps in (1, 2, 3, 4):
137
+ shape = app.gallery_shape(steps)
138
+ assert shape["rows"] == 1 and shape["columns"] == steps + 1
139
+ assert shape["height"] == app.GALLERY_ROW_H
140
+ for steps in (5, 6, 7, 8):
141
+ shape = app.gallery_shape(steps)
142
+ items = steps + 1
143
+ assert shape["rows"] == 2
144
+ assert shape["height"] == 2 * app.GALLERY_ROW_H
145
+ # every item has a cell, and no more than one cell goes spare
146
+ assert shape["columns"] * 2 >= items > shape["columns"] * 2 - 2
147
+
148
+
149
  def test_a_deep_run_labels_every_level(app, photo):
150
+ _, clip, gallery, _, _, scale = list(app.run(photo, app.MAX_STEPS, 0.5, 0.5))[-1]
151
  try:
152
  assert [label for _, label in gallery] == [
153
  "input", "4x", "16x", "64x", "256x", "1024x", "4096x", "16384x", "65536x"]
154
+ assert scale["value"] == "65536x" and len(scale["choices"]) == app.MAX_STEPS
155
  finally:
156
  os.remove(clip)
157