Spaces:
Running on Zero
Running on Zero
Bring back the comparison with a scale picker; reflow the layout
Browse files- app.py +96 -63
- samples/example_256x_input.png +3 -0
- 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:
|
| 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,
|
| 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:
|
|
@@ -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,
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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="
|
| 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=
|
| 189 |
-
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 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 |
-
|
|
|
|
| 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)}** {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
|
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) ==
|
| 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 |
-
|
| 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 |
|