Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import numpy as np | |
| from numba import njit | |
| from PIL import Image, ImageOps | |
| import io | |
| import tempfile | |
| import os | |
| from huggingface_hub import hf_hub_download | |
| files = ["a.jpg", "b.png", "c.png", 'd.png'] | |
| paths = [ | |
| hf_hub_download( | |
| repo_id="Rothfeld/drostescher", | |
| filename=f, | |
| repo_type="space" | |
| ) | |
| for f in files | |
| ] | |
| import shutil | |
| for a,b in zip(files,paths): | |
| shutil.copy(b,a) | |
| A,B,C,D = files | |
| # ββ forward transformation ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def tl_to_yx(t, l, w, h): | |
| y = (1 - t / h) * 2 - 1 | |
| x = (l / w) * 2 - 1 | |
| return y, x | |
| def yx_to_ra(y, x): | |
| a = np.arctan2(y, x) % (2 * np.pi) | |
| r = (x * x + y * y) ** 0.5 | |
| return r, a | |
| def a_to_na(a): | |
| return a / (2 * np.pi) | |
| def r_to_logr(r): | |
| return np.log2(r) | |
| def encode_to_logrna(l, t, w, h): | |
| y, x = tl_to_yx(t, l, w, h) | |
| r, a = yx_to_ra(y, x) | |
| logr = r_to_logr(r) | |
| na = a_to_na(a) | |
| return logr, na | |
| # ββ inverse transformation ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def na_to_a(na): | |
| return na * (2 * np.pi) | |
| def logr_to_r(logr): | |
| return 2 ** logr | |
| def ra_to_yx(r, a): | |
| x = r * np.cos(a) | |
| y = r * np.sin(a) | |
| return y, x | |
| def yx_to_tl(y, x, w, h): | |
| t = (1 - (y + 1) / 2) * h | |
| l = ((x + 1) / 2) * w | |
| t = np.round(t).astype(np.int64) | |
| l = np.round(l).astype(np.int64) | |
| return t, l | |
| def decode_to_tl(logr, na, w, h): | |
| a = na_to_a(na) | |
| r = logr_to_r(logr) | |
| y, x = ra_to_yx(r, a) | |
| t, l = yx_to_tl(y, x, w, h) | |
| t %= h | |
| l %= w | |
| return t, l | |
| # ββ utilities βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def drostify(o: Image.Image): | |
| w, h = o.width, o.height | |
| scale = 1 / 2 | |
| w2, h2 = int(w * scale), int(h * scale) | |
| hb = (w - w2) // 2 | |
| vb = (h - h2) // 2 | |
| l, t, r, b = (hb, vb, w - hb, h - vb) | |
| small = o.resize((r - l, b - t)) | |
| o.paste(small, (l, t, r, b), mask=small.getchannel('A')) | |
| def pad_to_aspect(img, target_ratio, fill=0): | |
| w, h = img.size | |
| current_ratio = w / h | |
| if current_ratio > target_ratio: | |
| new_h = int(round(w / target_ratio)) | |
| pad_total = new_h - h | |
| top = pad_total // 2 | |
| bottom = pad_total - top | |
| left = right = 0 | |
| else: | |
| new_w = int(round(h * target_ratio)) | |
| pad_total = new_w - w | |
| left = pad_total // 2 | |
| right = pad_total - left | |
| top = bottom = 0 | |
| return ImageOps.expand(img, (left, top, right, bottom), fill=fill) | |
| def make_still(img_pil, output_size): | |
| """Single Droste-effect still frame.""" | |
| o = img_pil.convert("RGBA") | |
| o = pad_to_aspect(o, output_size[0] / output_size[1]) | |
| o = o.resize(output_size, Image.Resampling.NEAREST) | |
| drostify(o) | |
| w, h = o.width, o.height | |
| t, l = np.meshgrid(np.arange(h), np.arange(w), indexing="ij") | |
| logr, na = encode_to_logrna(l, t, w, h) | |
| logr -= na | |
| logr %= -1 | |
| t2, l2 = decode_to_tl(logr, na, w, h) | |
| oa = np.array(o) | |
| ia = oa[t2, l2] | |
| for i in range(1, 10): | |
| transparent = ia[:, :, 3] == 0 | |
| if not transparent.any(): | |
| break | |
| dt, dl = decode_to_tl(logr[transparent] - i, na[transparent], w, h) | |
| ia[transparent] = oa[dt, dl] | |
| return Image.fromarray(ia) | |
| def make_animation(img_pil, output_size, n_frames, n_rotations): | |
| """Animated Droste zoom loop.""" | |
| o = img_pil.convert("RGBA") | |
| o = pad_to_aspect(o, output_size[0] / output_size[1]) | |
| o = o.resize(output_size, Image.Resampling.NEAREST) | |
| drostify(o) | |
| origa = np.array(o.copy()) | |
| w, h = o.width, o.height | |
| t0, l0 = np.meshgrid(np.arange(h), np.arange(w), indexing="ij") | |
| steps = np.linspace(0.0, 1.0, n_frames, endpoint=False) | |
| frames = [] | |
| for s in steps: | |
| logr, na = encode_to_logrna(l0, t0, w, h) | |
| logr -= s | |
| logr -= na | |
| logr %= -1 | |
| nac = na + s * n_rotations | |
| t2, l2 = decode_to_tl(logr, nac, w, h) | |
| ia = origa[t2, l2] | |
| for i in range(1, 5): | |
| transparent = ia[:, :, 3] == 0 | |
| if not transparent.any(): | |
| break | |
| dt, dl = decode_to_tl(logr[transparent] - i, nac[transparent], w, h) | |
| ia[transparent] = origa[dt, dl] | |
| frames.append(Image.fromarray(ia).convert("RGB")) | |
| return frames | |
| # ββ Gradio callbacks ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def run_still(image, width, height): | |
| if image is None: | |
| return None, "β οΈ Please upload an image." | |
| try: | |
| pil = Image.fromarray(image) | |
| result = make_still(pil, (int(width), int(height))) | |
| return np.array(result.convert("RGB")), "β Done!" | |
| except Exception as e: | |
| return None, f"β Error: {e}" | |
| def run_animation(image, width, height, n_frames, n_rotations, fps): | |
| if image is None: | |
| return None, "β οΈ Please upload an image." | |
| try: | |
| pil = Image.fromarray(image) | |
| frames = make_animation(pil, (int(width), int(height)), int(n_frames), int(n_rotations)) | |
| tmp = tempfile.NamedTemporaryFile(suffix=".gif", delete=False) | |
| frames[0].save( | |
| tmp.name, | |
| save_all=True, | |
| append_images=frames[1:], | |
| duration=int(1000 / fps), | |
| loop=0, | |
| disposal=2, | |
| ) | |
| return tmp.name, f"β {len(frames)} frames @ {fps} fps" | |
| except Exception as e: | |
| return None, f"β Error: {e}" | |
| # ββ UI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| DESCRIPTION = """ | |
| # π Droste Effect | |
| Upload any image (transparency supported) and apply the **Droste effect** β | |
| an infinite self-similar zoom that maps the image into a logarithmic spiral. | |
| Two modes: | |
| - **Still** β a single warped frame | |
| - **Animation** β a seamlessly looping GIF zoom | |
| """ | |
| with gr.Blocks(title="Droste Effect") as demo: | |
| gr.Markdown(DESCRIPTION) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| image_input = gr.Image(label="Input image", type="numpy", image_mode='RGBA') | |
| with gr.Accordion("Output size", open=False): | |
| width = gr.Slider(64, 1024, value=400, step=8, label="Width") | |
| height = gr.Slider(64, 1024, value=400, step=8, label="Height") | |
| with gr.Column(scale=2): | |
| with gr.Tab("πΌοΈ Still"): | |
| still_btn = gr.Button("Generate still", variant="primary") | |
| still_out = gr.Image(label="Droste still", type="numpy", format='png') | |
| still_status = gr.Textbox(label="Status", interactive=False, max_lines=1) | |
| still_btn.click( | |
| run_still, | |
| inputs=[image_input, width, height], | |
| outputs=[still_out, still_status], | |
| ) | |
| with gr.Tab("ποΈ Animation"): | |
| with gr.Row(): | |
| n_frames = gr.Slider(12, 120, value=60, step=4, label="Frames") | |
| n_rotations = gr.Slider(0, 8, value=0, step=1, label="Rotations per loop") | |
| fps = gr.Slider(6, 60, value=30, step=2, label="FPS") | |
| anim_btn = gr.Button("Generate animation", variant="primary") | |
| anim_out = gr.Image(label="Droste animation (GIF)") | |
| anim_status = gr.Textbox(label="Status", interactive=False, max_lines=1) | |
| anim_btn.click( | |
| run_animation, | |
| inputs=[image_input, width, height, n_frames, n_rotations, fps], | |
| outputs=[anim_out, anim_status], | |
| ) | |
| # ββ Examples βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| gr.Markdown("### πΌοΈ Example images β click to load") | |
| # Still examples: [image, width, height] | |
| gr.Examples( | |
| examples=[ | |
| [A, 1024, 1024], | |
| [B, 400, 400], | |
| [C, 512, 512], | |
| [D, 400, 400], | |
| ], | |
| inputs=[image_input, width, height], | |
| outputs=[still_out, still_status], | |
| fn=run_still, | |
| cache_examples=False, | |
| label="Still examples", | |
| ) | |
| gr.Markdown(""" | |
| --- | |
| **How it works:** each pixel's Cartesian coordinates are converted to polar form, | |
| the radius is logβ-transformed, and the angle is used to modulate the radius offset, | |
| turning circles into spirals. The image is then resampled in that warped space, | |
| creating the recursive Droste zoom. Transparent pixels are resolved by stepping | |
| one ring inward until opaque colour is found. | |
| Space generated from ./_orig.py using claude. | |
| """) | |
| if __name__ == "__main__": | |
| demo.launch() |