shreshthsaini commited on
Commit
119525a
·
verified ·
1 Parent(s): 36ce49d

Deploy the JLD full/fast interactive demo on free ZeroGPU

Browse files
.gitattributes CHANGED
@@ -33,3 +33,9 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ examples/bikes.png filter=lfs diff=lfs merge=lfs -text
37
+ examples/bikes_blur.png filter=lfs diff=lfs merge=lfs -text
38
+ examples/hats.png filter=lfs diff=lfs merge=lfs -text
39
+ examples/hats_noise.png filter=lfs diff=lfs merge=lfs -text
40
+ examples/parrots.png filter=lfs diff=lfs merge=lfs -text
41
+ examples/parrots_jpeg_q30.png filter=lfs diff=lfs merge=lfs -text
LICENSE ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shreshth Saini, Balu Adsumilli, Alan C. Bovik
4
+
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
README.md CHANGED
@@ -1,13 +1,72 @@
1
  ---
2
- title: JLD Demo
3
- emoji: 📈
4
- colorFrom: gray
5
- colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.29.1
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
 
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: JLD Perceptual Distance
3
+ emoji: 🔍
4
+ colorFrom: blue
5
+ colorTo: yellow
6
  sdk: gradio
7
  sdk_version: 6.29.1
8
+ python_version: "3.12"
9
  app_file: app.py
10
  pinned: false
11
+ license: mit
12
+ tags:
13
+ - image-quality-assessment
14
+ - perceptual-distance
15
+ - full-reference
16
+ - dinov2
17
+ - arxiv:2610.05967
18
  ---
19
 
20
+ An author-maintained interactive demo of **JLD: Perceptual Distance Through a Jacobian Lens**.
21
+
22
+ [Paper](https://arxiv.org/abs/2610.05967) · [Code](https://github.com/shreshthsaini/jld) · [Research website](https://shreshthsaini.github.io/jld/)
23
+
24
+ Upload an aligned reference/distorted pair, select JLD or JLD-fast, and obtain
25
+ a scalar distance plus a local lens response overlay. Lower means more similar.
26
+ The overlay excludes the global CLS term and is normalized separately for each pair.
27
+
28
+ Both images must have the same native size. The demo converts to RGB and
29
+ center-crops to multiples of 14 pixels without resizing. Public demo resource
30
+ limits are 262,144 pixels, a longest side of 1024 pixels, and 10 MB per image.
31
+ Use the local package for larger images. The pretrained encoder loads on hosted
32
+ startup, while the fitted lens is included in the package. The hosted Space uses
33
+ free shared ZeroGPU access; queueing and daily usage limits may apply.
34
+
35
+ ## Local use with uv
36
+
37
+ ```bash
38
+ uv add "jacobian-lens-distance @ https://github.com/shreshthsaini/jld/releases/download/v1.1.0/jacobian_lens_distance-1.1.0-py3-none-any.whl"
39
+ ```
40
+
41
+ ```python
42
+ from jld import JLD
43
+
44
+ metric = JLD.pretrained("full")
45
+ distance = metric("reference.png", "distorted.png")
46
+ ```
47
+
48
+ To run this Space checkout locally:
49
+
50
+ ```bash
51
+ uv run --python 3.12 --with-requirements requirements.txt app.py
52
+ ```
53
+
54
+ The app runs on CPU and listens on localhost for local runs. Hugging Face manages
55
+ external access and dynamic GPU allocation when deployed as a ZeroGPU Space.
56
+
57
+ ## Citation
58
+
59
+ ```bibtex
60
+ @article{saini2026jld,
61
+ title={JLD: Perceptual Distance Through A Jacobian Lens},
62
+ author={Saini, Shreshth and Adsumilli, Balu and Bovik, Alan C.},
63
+ journal={arXiv preprint arXiv:2610.05967},
64
+ year={2026}
65
+ }
66
+ ```
67
+
68
+ Code is MIT licensed. Bundled images have their own provenance and source reuse
69
+ statement in [examples/PROVENANCE.md](examples/PROVENANCE.md). Encoder weights
70
+ retain their upstream license. No uploaded images or scores are deliberately
71
+ archived by the app; Gradio and Hugging Face may keep temporary upload files
72
+ under their platform policies.
app.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """An author-maintained JLD Space, using the public package without score surrogates."""
2
+ from __future__ import annotations
3
+
4
+ from functools import lru_cache
5
+ import os
6
+ from pathlib import Path
7
+ import threading
8
+ import warnings
9
+
10
+ # Import before torch so the hosted ZeroGPU runtime can enable CUDA emulation.
11
+ import spaces
12
+ import gradio as gr
13
+ import numpy as np
14
+ from PIL import Image
15
+ import torch
16
+
17
+ from jld import JLD
18
+ from jld.cli import RAMP
19
+ from jld.lens import Lens
20
+ from jld.metric import PRETRAINED_LENS
21
+
22
+ HERE = Path(__file__).resolve().parent
23
+ EXAMPLES = HERE / "examples"
24
+ MAX_PIXELS = 262_144
25
+ MAX_SIDE = 1024
26
+ MAX_FILE_BYTES = 10 * 1024 * 1024
27
+ PATCH = 14
28
+ Image.MAX_IMAGE_PIXELS = MAX_PIXELS
29
+ warnings.filterwarnings("error", category=Image.DecompressionBombWarning)
30
+ torch.set_num_threads(min(4, max(1, int(os.environ.get("JLD_CPU_THREADS", "2")))))
31
+ MODEL_LOCK = threading.Lock()
32
+ HOSTED = bool(os.environ.get("SPACE_ID"))
33
+
34
+ PAPER = "https://arxiv.org/abs/2610.05967"
35
+ CODE = "https://github.com/shreshthsaini/jld"
36
+ WEBSITE = "https://shreshthsaini.github.io/jld/"
37
+ INSTALL = (
38
+ 'uv add "jacobian-lens-distance @ '
39
+ 'https://github.com/shreshthsaini/jld/releases/download/v1.1.0/'
40
+ 'jacobian_lens_distance-1.1.0-py3-none-any.whl"'
41
+ )
42
+ BIBTEX = """@article{saini2026jld,
43
+ title={JLD: Perceptual Distance Through A Jacobian Lens},
44
+ author={Saini, Shreshth and Adsumilli, Balu and Bovik, Alan C.},
45
+ journal={arXiv preprint arXiv:2610.05967},
46
+ year={2026}
47
+ }"""
48
+
49
+
50
+ def load_image(path: str | None) -> Image.Image:
51
+ """Check resource limits before pixel decoding, then retain native RGB pixels."""
52
+ if not path:
53
+ raise gr.Error("Choose both a reference image and a distorted image.")
54
+ try:
55
+ if Path(path).stat().st_size > MAX_FILE_BYTES:
56
+ raise gr.Error("Each image must be at most 10 MB. Use the local package for larger files.")
57
+ with Image.open(path) as image:
58
+ width, height = image.size
59
+ if width < PATCH or height < PATCH:
60
+ raise gr.Error("Each image must be at least 14 pixels wide and high.")
61
+ if width * height > MAX_PIXELS or max(width, height) > MAX_SIDE:
62
+ raise gr.Error(
63
+ "This demo accepts at most 262,144 pixels per image and a longest side of 1024 pixels. "
64
+ "Use the local package to score larger images at their native resolution."
65
+ )
66
+ if getattr(image, "n_frames", 1) > 1:
67
+ raise gr.Error("Please upload a single still image.")
68
+ return image.convert("RGB")
69
+ except gr.Error:
70
+ raise
71
+ except (Image.DecompressionBombWarning, Image.DecompressionBombError):
72
+ raise gr.Error("Image dimensions exceed this demo's memory limit.") from None
73
+ except (OSError, ValueError):
74
+ raise gr.Error("Could not read the image. Please use a PNG, JPEG, or WebP image.") from None
75
+
76
+
77
+ @lru_cache(maxsize=1)
78
+ def metrics() -> dict[str, JLD]:
79
+ """Share one frozen encoder between variants to bound resident memory."""
80
+ device = "cuda" if HOSTED else os.environ.get("JLD_DEVICE", "cpu")
81
+ full = JLD.pretrained("full", device=device)
82
+ # Read the shipped lens on CPU without copying a fake-CUDA tensor back to CPU.
83
+ fast = JLD(full.encoder, Lens.load(PRETRAINED_LENS).U, full.chroma_sigma, 0.0)
84
+ return {"full": full, "fast": fast}
85
+
86
+
87
+ if HOSTED:
88
+ # ZeroGPU optimizes models placed on emulated CUDA at module startup.
89
+ metrics()
90
+
91
+
92
+ @spaces.GPU(duration=15)
93
+ def compute_distance_and_map(reference: Image.Image, distorted: Image.Image, variant: str):
94
+ models = metrics()
95
+ value = float(models[variant](reference, distorted)[0])
96
+ # Maps use the same lens and chroma prefilter; they never include the CLS term.
97
+ patch_map = models["fast"].map(reference, distorted)[0].numpy()
98
+ return value, patch_map
99
+
100
+
101
+ def map_overlay(reference: Image.Image, patch_map: np.ndarray) -> Image.Image:
102
+ """Use the public CLI's colour ramp and per-image maximum normalization."""
103
+ height, width = patch_map.shape[0] * PATCH, patch_map.shape[1] * PATCH
104
+ left, top = (reference.width - width) // 2, (reference.height - height) // 2
105
+ cropped = reference.crop((left, top, left + width, top + height))
106
+ scaled = patch_map / max(float(patch_map.max()), 1e-12)
107
+ positions = np.linspace(0, 1, len(RAMP))
108
+ colors = np.stack([np.interp(scaled, positions, RAMP[:, c]) for c in range(3)], axis=-1)
109
+ heat = Image.fromarray(colors.astype(np.uint8)).resize((width, height), Image.Resampling.NEAREST)
110
+ gray = Image.fromarray(np.asarray(cropped).mean(axis=-1).round().astype(np.uint8)).convert("RGB")
111
+ return Image.blend(gray, heat, 0.6)
112
+
113
+
114
+ def score_pair(reference_path: str | None, distorted_path: str | None, variant: str):
115
+ if variant not in ("full", "fast"):
116
+ raise gr.Error("Choose full or fast.")
117
+ reference, distorted = load_image(reference_path), load_image(distorted_path)
118
+ if reference.size != distorted.size:
119
+ raise gr.Error(
120
+ f"Images must have exactly the same native size: reference {reference.size}, "
121
+ f"distorted {distorted.size}. This demo does not resize or align your images."
122
+ )
123
+ # Serialize calls even through direct API usage, including first model initialization.
124
+ with MODEL_LOCK:
125
+ value, patch_map = compute_distance_and_map(reference, distorted, variant)
126
+ height, width = patch_map.shape[0] * PATCH, patch_map.shape[1] * PATCH
127
+ name = "JLD" if variant == "full" else "JLD-fast"
128
+ details = (
129
+ f"**{name}: {value:.6f}**. Lower means more similar to the reference. "
130
+ f"Scored RGB crop: **{width} × {height} pixels**, from native "
131
+ f"{reference.width} × {reference.height}; no resizing.\n\n"
132
+ "The overlay shows **local projected patch displacement**, after the chroma prefilter. "
133
+ "It excludes the global CLS term used by full JLD. Brighter colours mark larger local "
134
+ "displacements. Colours are normalized to this pair's maximum, so do not compare colour "
135
+ "intensity across different pairs. An identical pair has zero displacement."
136
+ )
137
+ return value, map_overlay(reference, patch_map), details
138
+
139
+
140
+ def build_demo() -> gr.Blocks:
141
+ with gr.Blocks(title="JLD: Jacobian Lens Distance", analytics_enabled=False) as demo:
142
+ gr.Markdown(
143
+ "# JLD: Perceptual Distance Through a Jacobian Lens\n"
144
+ "Compare a reference image with its distorted version using our frozen DINOv2 lens. "
145
+ "Try a release example below, or upload your own pair.\n\n"
146
+ f"[Read the paper]({PAPER}) · [Code and installation]({CODE}) · [Research website]({WEBSITE})"
147
+ )
148
+ with gr.Row():
149
+ reference = gr.Image(label="Reference image", type="filepath", sources=["upload"], height=280)
150
+ distorted = gr.Image(label="Distorted image", type="filepath", sources=["upload"], height=280)
151
+ variant = gr.Radio(
152
+ choices=[("JLD: lens + global CLS term", "full"), ("JLD-fast: lens term only", "fast")],
153
+ value="fast", label="Variant",
154
+ )
155
+ gr.Markdown(
156
+ "Use **aligned images of the same native size**. Both are converted to RGB and center-cropped "
157
+ "to multiples of 14 pixels. The demo keeps image resolution and accepts up to 262,144 pixels "
158
+ "per image, a longest side of 1024 pixels, and files up to 10 MB. "
159
+ "The hosted demo uses free shared GPU access; queueing and daily usage limits may apply."
160
+ )
161
+ run = gr.Button("Compute distance and local map", variant="primary")
162
+ with gr.Row():
163
+ distance = gr.Number(label="Distance (lower means more similar)", precision=6)
164
+ response = gr.Image(label="Local lens response overlay (excludes CLS)", type="pil", height=300)
165
+ explanation = gr.Markdown()
166
+ run.click(score_pair, [reference, distorted, variant], [distance, response, explanation],
167
+ api_name="score_pair", concurrency_limit=1, concurrency_id="jld")
168
+ gr.Examples(
169
+ examples=[
170
+ [str(EXAMPLES / "parrots.png"), str(EXAMPLES / "parrots_jpeg_q8.png"), "fast"],
171
+ [str(EXAMPLES / "parrots.png"), str(EXAMPLES / "parrots_jpeg_q30.png"), "full"],
172
+ [str(EXAMPLES / "bikes.png"), str(EXAMPLES / "bikes_blur.png"), "fast"],
173
+ [str(EXAMPLES / "hats.png"), str(EXAMPLES / "hats_noise.png"), "full"],
174
+ [str(EXAMPLES / "parrots.png"), str(EXAMPLES / "parrots.png"), "fast"],
175
+ ], inputs=[reference, distorted, variant], label="Authentic release examples",
176
+ )
177
+ gr.Markdown(
178
+ "Examples come from the public JLD release: Kodak reference photographs hosted by "
179
+ "[Rich Franzen](https://r0k.us/graphics/kodak/), with fixed JPEG, blur, and noise transformations. "
180
+ "They are demonstrations, not a benchmark evaluation. See [example provenance](https://github.com/"
181
+ "shreshthsaini/jld/blob/main/huggingface/space/examples/PROVENANCE.md)."
182
+ )
183
+ with gr.Accordion("Use JLD in your research", open=False):
184
+ gr.Markdown(
185
+ "Install the versioned release in your project with uv:\n\n"
186
+ f"```bash\n{INSTALL}\n```\n\n"
187
+ "```python\nfrom jld import JLD\nmetric = JLD.pretrained('full')\n"
188
+ "distance = metric('reference.png', 'distorted.png')\n```\n\n"
189
+ "If you use JLD, please cite the paper."
190
+ )
191
+ gr.Code(BIBTEX, language=None, label="BibTeX citation")
192
+ return demo.queue(max_size=8, default_concurrency_limit=1)
193
+
194
+
195
+ demo = build_demo()
196
+
197
+ if __name__ == "__main__":
198
+ if os.environ.get("SPACE_ID"):
199
+ # Hugging Face supplies its managed runtime launch configuration.
200
+ demo.launch(max_file_size="10mb", show_error=False)
201
+ else:
202
+ demo.launch(server_name="127.0.0.1", max_file_size="10mb", show_error=False)
examples/PROVENANCE.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Example image provenance
2
+
3
+ These PNGs are copied unchanged from the public JLD repository's `examples/`
4
+ directory. They use reference photographs from the Kodak Lossless True Color
5
+ Image Suite hosted by Rich Franzen at https://r0k.us/graphics/kodak/:
6
+
7
+ | Reference | Kodak source | Release preprocessing |
8
+ |---|---|---|
9
+ | `parrots.png` | `kodim23.png` | Resize to 294-pixel height, center-crop to 392 × 294 |
10
+ | `hats.png` | `kodim03.png` | Resize to 294-pixel height, center-crop to 392 × 294 |
11
+ | `bikes.png` | `kodim05.png` | Resize to 294-pixel height, center-crop to 392 × 294 |
12
+
13
+ The source site's reuse statement says Rich Franzen understands the Eastman
14
+ Kodak Company released the photographs for unrestricted usage. This is the
15
+ source's statement, not a claim that JLD authors own these photographs or
16
+ license them under MIT. Checked 2026-10-07.
17
+
18
+ Distortions follow `examples/make_examples.py` in the JLD release:
19
+
20
+ - `parrots_jpeg_q8.png` and `parrots_jpeg_q30.png`: Pillow JPEG round trip at quality 8 and 30.
21
+ - `bikes_blur.png`: Pillow Gaussian blur, radius 2.
22
+ - `hats_noise.png`: Gaussian noise with standard deviation 12 on 8-bit RGB, NumPy RNG seed 0.
23
+
24
+ These are reproducible demonstration pairs. The Space computes their scores
25
+ live with the released model and does not present them as a benchmark result.
examples/bikes.png ADDED

Git LFS Details

  • SHA256: 76835c26a47ce347ad7c767edf55760bef1621355f04c9605f9b9323dfb0bcd8
  • Pointer size: 131 Bytes
  • Size of remote file: 269 kB
examples/bikes_blur.png ADDED

Git LFS Details

  • SHA256: 553be6bb8777c7143524974b2bdec7974b03e715116b4ff9a259aaac1fb3afd7
  • Pointer size: 131 Bytes
  • Size of remote file: 143 kB
examples/hats.png ADDED

Git LFS Details

  • SHA256: 51ce756cf9d2bd37c66da8b59491121468375537e763da0117dd9940c2cc54f4
  • Pointer size: 131 Bytes
  • Size of remote file: 175 kB
examples/hats_noise.png ADDED

Git LFS Details

  • SHA256: a6764243f71822fe4ad962b38d878b9aba14313ddb9d59e68d51a30db1bf460a
  • Pointer size: 131 Bytes
  • Size of remote file: 274 kB
examples/parrots.png ADDED

Git LFS Details

  • SHA256: 8fba89a41b5d1ec5fb368e6a7ae367859ac6d4cb474bb5a0939731b0fe50295c
  • Pointer size: 131 Bytes
  • Size of remote file: 179 kB
examples/parrots_jpeg_q30.png ADDED

Git LFS Details

  • SHA256: e176c366ea669ba2ba53cbf53341b2da28a9a39d5b45d64f0a38d8b22c65d845
  • Pointer size: 131 Bytes
  • Size of remote file: 129 kB
examples/parrots_jpeg_q8.png ADDED
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio==6.29.1
2
+ spaces
3
+ torch==2.13.0
4
+ torchvision==0.28.0
5
+ timm==1.0.30
6
+ jacobian-lens-distance @ https://github.com/shreshthsaini/jld/releases/download/v1.1.0/jacobian_lens_distance-1.1.0-py3-none-any.whl