fixes and sweep improvements.
Browse files- PBC3_animation.py +25 -12
- pbc3_sweep.py +36 -13
- server.py +4 -3
- static/app.js +17 -6
- static/index.html +4 -2
- static/sweep.js +6 -6
PBC3_animation.py
CHANGED
|
@@ -98,38 +98,51 @@ def _fit(img, max_w, max_h):
|
|
| 98 |
|
| 99 |
def _make_frame(canvas, color_space, patch_info, separated_channels, title, target=None, show_errors=False, output_size=(3840, 2160)):
|
| 100 |
arr = np.clip(canvas, 0, 255).astype(np.uint8)
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
target = np.dstack([target, np.full(target.shape[:2], 255, target.dtype)])
|
| 105 |
else:
|
| 106 |
-
target = target[:, :, :
|
| 107 |
error = None if target is None else target.astype(np.int32) - arr.astype(np.int32)
|
| 108 |
|
| 109 |
if not separated_channels:
|
| 110 |
panels = [(rgb, False)]
|
|
|
|
|
|
|
| 111 |
if show_errors:
|
| 112 |
if error is None:
|
| 113 |
raise ValueError("show_errors=True requires original_image")
|
| 114 |
panels.append((_error_image(error, color_space), True))
|
| 115 |
-
cols, rows =
|
| 116 |
else:
|
| 117 |
panels = [
|
| 118 |
(_colorize_channel(arr[:, :, 0], 0, color_space), False),
|
| 119 |
(_colorize_channel(arr[:, :, 1], 1, color_space), False),
|
| 120 |
(_colorize_channel(arr[:, :, 2], 2, color_space), False),
|
| 121 |
-
(rgb, False),
|
| 122 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
if show_errors:
|
| 124 |
if error is None:
|
| 125 |
raise ValueError("show_errors=True requires original_image")
|
| 126 |
-
|
| 127 |
(_error_image(error[:, :, 0], color_space, 0), True),
|
| 128 |
(_error_image(error[:, :, 1], color_space, 1), True),
|
| 129 |
(_error_image(error[:, :, 2], color_space, 2), True),
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
frame_w, frame_h = output_size
|
| 135 |
frame = Image.new("RGB", output_size, "black")
|
|
@@ -156,7 +169,7 @@ def _make_frame(canvas, color_space, patch_info, separated_channels, title, targ
|
|
| 156 |
py = y0 + (cell_h - fitted.height) // 2
|
| 157 |
frame.paste(fitted, (px, py))
|
| 158 |
|
| 159 |
-
is_rgb_panel = (separated_channels and col ==
|
| 160 |
is_active_channel_panel = separated_channels and col == active_channel
|
| 161 |
if patch_box is not None and not is_error and (is_rgb_panel or is_active_channel_panel):
|
| 162 |
_draw_patch(draw, patch_box, scale, (px, py))
|
|
@@ -249,7 +262,7 @@ def animate_pbc3(
|
|
| 249 |
(downsampled, original_w, original_h, w, h, color_space, channels,
|
| 250 |
channel_bits, positive_bias, has_alpha, patch_count, base_values,
|
| 251 |
warmup_on, warm_w, warm_h, warmup_split) = PBC3._read_header(br)
|
| 252 |
-
if channels
|
| 253 |
separated_channels = False
|
| 254 |
if show_errors and original_image is None:
|
| 255 |
raise ValueError("show_errors=True requires original_image=...")
|
|
|
|
| 98 |
|
| 99 |
def _make_frame(canvas, color_space, patch_info, separated_channels, title, target=None, show_errors=False, output_size=(3840, 2160)):
|
| 100 |
arr = np.clip(canvas, 0, 255).astype(np.uint8)
|
| 101 |
+
n_ch = arr.shape[2]
|
| 102 |
+
has_alpha = n_ch == 4
|
| 103 |
+
rgb = Image.fromarray(np.ascontiguousarray(arr[:, :, :3]), color_space).convert("RGB")
|
| 104 |
+
alpha_img = Image.fromarray(arr[:, :, 3], "L").convert("RGB") if has_alpha else None
|
| 105 |
+
|
| 106 |
+
if target is not None and target.shape[2] != n_ch:
|
| 107 |
+
if n_ch == 4 and target.shape[2] == 3:
|
| 108 |
target = np.dstack([target, np.full(target.shape[:2], 255, target.dtype)])
|
| 109 |
else:
|
| 110 |
+
target = target[:, :, :n_ch]
|
| 111 |
error = None if target is None else target.astype(np.int32) - arr.astype(np.int32)
|
| 112 |
|
| 113 |
if not separated_channels:
|
| 114 |
panels = [(rgb, False)]
|
| 115 |
+
if has_alpha:
|
| 116 |
+
panels.append((alpha_img, False))
|
| 117 |
if show_errors:
|
| 118 |
if error is None:
|
| 119 |
raise ValueError("show_errors=True requires original_image")
|
| 120 |
panels.append((_error_image(error, color_space), True))
|
| 121 |
+
cols, rows = len(panels), 1
|
| 122 |
else:
|
| 123 |
panels = [
|
| 124 |
(_colorize_channel(arr[:, :, 0], 0, color_space), False),
|
| 125 |
(_colorize_channel(arr[:, :, 1], 1, color_space), False),
|
| 126 |
(_colorize_channel(arr[:, :, 2], 2, color_space), False),
|
|
|
|
| 127 |
]
|
| 128 |
+
if has_alpha:
|
| 129 |
+
panels.append((alpha_img, False))
|
| 130 |
+
panels.append((rgb, False))
|
| 131 |
+
cols, rows = len(panels), 1
|
| 132 |
if show_errors:
|
| 133 |
if error is None:
|
| 134 |
raise ValueError("show_errors=True requires original_image")
|
| 135 |
+
err_panels = [
|
| 136 |
(_error_image(error[:, :, 0], color_space, 0), True),
|
| 137 |
(_error_image(error[:, :, 1], color_space, 1), True),
|
| 138 |
(_error_image(error[:, :, 2], color_space, 2), True),
|
| 139 |
+
]
|
| 140 |
+
if has_alpha:
|
| 141 |
+
a_err = np.clip(np.abs(error[:, :, 3]), 0, 255).astype(np.uint8)
|
| 142 |
+
err_panels.append((Image.fromarray(np.stack([a_err] * 3, axis=2), "RGB"), True))
|
| 143 |
+
err_panels.append((_error_image(error, color_space), True))
|
| 144 |
+
panels.extend(err_panels)
|
| 145 |
+
rows = 2
|
| 146 |
|
| 147 |
frame_w, frame_h = output_size
|
| 148 |
frame = Image.new("RGB", output_size, "black")
|
|
|
|
| 169 |
py = y0 + (cell_h - fitted.height) // 2
|
| 170 |
frame.paste(fitted, (px, py))
|
| 171 |
|
| 172 |
+
is_rgb_panel = (separated_channels and col == cols - 1) or (not separated_channels and i == 0)
|
| 173 |
is_active_channel_panel = separated_channels and col == active_channel
|
| 174 |
if patch_box is not None and not is_error and (is_rgb_panel or is_active_channel_panel):
|
| 175 |
_draw_patch(draw, patch_box, scale, (px, py))
|
|
|
|
| 262 |
(downsampled, original_w, original_h, w, h, color_space, channels,
|
| 263 |
channel_bits, positive_bias, has_alpha, patch_count, base_values,
|
| 264 |
warmup_on, warm_w, warm_h, warmup_split) = PBC3._read_header(br)
|
| 265 |
+
if channels not in (3, 4) and separated_channels:
|
| 266 |
separated_channels = False
|
| 267 |
if show_errors and original_image is None:
|
| 268 |
raise ValueError("show_errors=True requires original_image=...")
|
pbc3_sweep.py
CHANGED
|
@@ -3,8 +3,8 @@
|
|
| 3 |
One mixed Optuna study ("pbc3") in pbc3_sweep.db, three MINIMIZED objectives
|
| 4 |
(seconds, bpp, mse) averaged over every image in hpt_data/. Per-image rows and the
|
| 5 |
resolved config live in trial user_attrs so the analyzer can re-aggregate by
|
| 6 |
-
megapixel range. JPEG/AVIF baselines are logged once as tagged trials.
|
| 7 |
-
controlled from the web Sweep Runner."""
|
| 8 |
|
| 9 |
import glob, io, itertools, os, threading, time
|
| 10 |
|
|
@@ -25,8 +25,10 @@ DIRECTIONS = ("minimize", "minimize", "minimize")
|
|
| 25 |
METRIC_NAMES = ("Seconds", "Compression (bpp)", "MSE")
|
| 26 |
IMAGE_EXTS = ("*.png", "*.jpg", "*.jpeg", "*.webp", "*.bmp")
|
| 27 |
|
|
|
|
| 28 |
JPEG_QUALITIES = (1, 2, 3, 5, 8, 10, 15, 20, 30, 40, 60, 80, 95)
|
| 29 |
AVIF_QUALITIES = (0, 1, 2, 3, 5, 8, 10, 15, 20, 30, 40, 60, 80, 95)
|
|
|
|
| 30 |
|
| 31 |
# Optimizer search space. (name, kind, *args). Ranges are a starting guess — tweak freely.
|
| 32 |
SEARCH_SPACE = [
|
|
@@ -59,10 +61,11 @@ def ensure_pbc_baselines(study, images):
|
|
| 59 |
tag = f"preset_{name}"
|
| 60 |
if tag in have:
|
| 61 |
continue
|
| 62 |
-
_log(f"PBC preset baseline: {name}")
|
| 63 |
cfg = dict(vars(getattr(PBC3Config, name)()))
|
| 64 |
try:
|
| 65 |
-
|
|
|
|
| 66 |
except Exception as e:
|
| 67 |
_log(f"preset {name} failed: {e}")
|
| 68 |
continue
|
|
@@ -117,7 +120,7 @@ def _avg(rows):
|
|
| 117 |
return [float(np.mean([r[k] for r in rows])) for k in ("seconds", "bpp", "mse")]
|
| 118 |
|
| 119 |
|
| 120 |
-
def _eval_pbc3(cfg, images):
|
| 121 |
rows = []
|
| 122 |
for im in images:
|
| 123 |
arr = im["arr"]
|
|
@@ -126,10 +129,12 @@ def _eval_pbc3(cfg, images):
|
|
| 126 |
recon = np.asarray(res.image.convert("RGB").resize((arr.shape[1], arr.shape[0])))
|
| 127 |
rows.append({"name": im["name"], "mp": im["mp"], "seconds": float(res.encode_seconds),
|
| 128 |
"bpp": float(res.total_bits / pixels), "mse": _mse(arr, recon)})
|
|
|
|
|
|
|
| 129 |
return rows
|
| 130 |
|
| 131 |
|
| 132 |
-
def _eval_codec(fmt, q, images):
|
| 133 |
rows = []
|
| 134 |
for im in images:
|
| 135 |
arr = im["arr"]
|
|
@@ -141,6 +146,8 @@ def _eval_codec(fmt, q, images):
|
|
| 141 |
recon = np.asarray(Image.open(io.BytesIO(buf.getvalue())).convert("RGB"))
|
| 142 |
rows.append({"name": im["name"], "mp": im["mp"], "seconds": secs,
|
| 143 |
"bpp": len(buf.getvalue()) * 8 / pixels, "mse": _mse(arr, recon)})
|
|
|
|
|
|
|
| 144 |
return rows
|
| 145 |
|
| 146 |
|
|
@@ -162,14 +169,15 @@ def _add_completed(study, values, attrs):
|
|
| 162 |
|
| 163 |
def ensure_baselines(study, images):
|
| 164 |
have = {t.user_attrs.get("baseline") for t in study.trials}
|
| 165 |
-
for fmt, qs in (("JPEG", JPEG_QUALITIES), ("AVIF", AVIF_QUALITIES)):
|
| 166 |
for q in qs:
|
| 167 |
tag = f"{fmt.lower()}_q{q}"
|
| 168 |
if tag in have:
|
| 169 |
continue
|
| 170 |
-
_log(f"baseline {tag}")
|
| 171 |
try:
|
| 172 |
-
|
|
|
|
| 173 |
except Exception as e:
|
| 174 |
_log(f"baseline {tag} failed: {e}")
|
| 175 |
continue
|
|
@@ -192,6 +200,19 @@ def _log(msg):
|
|
| 192 |
print(f"[sweep] {msg}", flush=True)
|
| 193 |
|
| 194 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
def status():
|
| 196 |
with _LOCK:
|
| 197 |
s = dict(_STATE)
|
|
@@ -248,18 +269,21 @@ def _run(mode, spec):
|
|
| 248 |
cfg = _coerce(params)
|
| 249 |
with _LOCK:
|
| 250 |
_STATE["current"] = cfg
|
| 251 |
-
|
|
|
|
|
|
|
| 252 |
_add_completed(study, _avg(rows),
|
| 253 |
{"kind": "pbc3", "grid": True, "config": cfg, "per_image": rows})
|
| 254 |
with _LOCK:
|
| 255 |
_STATE["done"] = i + 1
|
| 256 |
-
_log(f"grid trial {i + 1}/{len(trials)} done")
|
| 257 |
else:
|
| 258 |
def objective(trial):
|
| 259 |
cfg = suggest_config(trial)
|
| 260 |
with _LOCK:
|
| 261 |
_STATE["current"] = cfg
|
| 262 |
-
|
|
|
|
|
|
|
| 263 |
trial.set_user_attr("kind", "pbc3")
|
| 264 |
trial.set_user_attr("config", cfg)
|
| 265 |
trial.set_user_attr("per_image", rows)
|
|
@@ -270,7 +294,6 @@ def _run(mode, spec):
|
|
| 270 |
n += 1
|
| 271 |
with _LOCK:
|
| 272 |
_STATE["done"] = n
|
| 273 |
-
_log(f"optimizer trial {n} done")
|
| 274 |
except Exception as e:
|
| 275 |
with _LOCK:
|
| 276 |
_STATE["error"] = str(e)
|
|
|
|
| 3 |
One mixed Optuna study ("pbc3") in pbc3_sweep.db, three MINIMIZED objectives
|
| 4 |
(seconds, bpp, mse) averaged over every image in hpt_data/. Per-image rows and the
|
| 5 |
resolved config live in trial user_attrs so the analyzer can re-aggregate by
|
| 6 |
+
megapixel range. JPEG/AVIF/WEBP baselines are logged once as tagged trials.
|
| 7 |
+
Start/stop controlled from the web Sweep Runner."""
|
| 8 |
|
| 9 |
import glob, io, itertools, os, threading, time
|
| 10 |
|
|
|
|
| 25 |
METRIC_NAMES = ("Seconds", "Compression (bpp)", "MSE")
|
| 26 |
IMAGE_EXTS = ("*.png", "*.jpg", "*.jpeg", "*.webp", "*.bmp")
|
| 27 |
|
| 28 |
+
# JPEG is a 1-95 scale (q=0 clamps to 1); AVIF/WEBP are true 0-100.
|
| 29 |
JPEG_QUALITIES = (1, 2, 3, 5, 8, 10, 15, 20, 30, 40, 60, 80, 95)
|
| 30 |
AVIF_QUALITIES = (0, 1, 2, 3, 5, 8, 10, 15, 20, 30, 40, 60, 80, 95)
|
| 31 |
+
WEBP_QUALITIES = (0, 1, 2, 3, 5, 8, 10, 15, 20, 30, 40, 60, 80, 95)
|
| 32 |
|
| 33 |
# Optimizer search space. (name, kind, *args). Ranges are a starting guess — tweak freely.
|
| 34 |
SEARCH_SPACE = [
|
|
|
|
| 61 |
tag = f"preset_{name}"
|
| 62 |
if tag in have:
|
| 63 |
continue
|
| 64 |
+
_log(f"PBC preset baseline: {name} started")
|
| 65 |
cfg = dict(vars(getattr(PBC3Config, name)()))
|
| 66 |
try:
|
| 67 |
+
_new_bar()
|
| 68 |
+
rows = _eval_pbc3(cfg, images, on_image=_bar_tick)
|
| 69 |
except Exception as e:
|
| 70 |
_log(f"preset {name} failed: {e}")
|
| 71 |
continue
|
|
|
|
| 120 |
return [float(np.mean([r[k] for r in rows])) for k in ("seconds", "bpp", "mse")]
|
| 121 |
|
| 122 |
|
| 123 |
+
def _eval_pbc3(cfg, images, on_image=None):
|
| 124 |
rows = []
|
| 125 |
for im in images:
|
| 126 |
arr = im["arr"]
|
|
|
|
| 129 |
recon = np.asarray(res.image.convert("RGB").resize((arr.shape[1], arr.shape[0])))
|
| 130 |
rows.append({"name": im["name"], "mp": im["mp"], "seconds": float(res.encode_seconds),
|
| 131 |
"bpp": float(res.total_bits / pixels), "mse": _mse(arr, recon)})
|
| 132 |
+
if on_image:
|
| 133 |
+
on_image()
|
| 134 |
return rows
|
| 135 |
|
| 136 |
|
| 137 |
+
def _eval_codec(fmt, q, images, on_image=None):
|
| 138 |
rows = []
|
| 139 |
for im in images:
|
| 140 |
arr = im["arr"]
|
|
|
|
| 146 |
recon = np.asarray(Image.open(io.BytesIO(buf.getvalue())).convert("RGB"))
|
| 147 |
rows.append({"name": im["name"], "mp": im["mp"], "seconds": secs,
|
| 148 |
"bpp": len(buf.getvalue()) * 8 / pixels, "mse": _mse(arr, recon)})
|
| 149 |
+
if on_image:
|
| 150 |
+
on_image()
|
| 151 |
return rows
|
| 152 |
|
| 153 |
|
|
|
|
| 169 |
|
| 170 |
def ensure_baselines(study, images):
|
| 171 |
have = {t.user_attrs.get("baseline") for t in study.trials}
|
| 172 |
+
for fmt, qs in (("JPEG", JPEG_QUALITIES), ("AVIF", AVIF_QUALITIES), ("WEBP", WEBP_QUALITIES)):
|
| 173 |
for q in qs:
|
| 174 |
tag = f"{fmt.lower()}_q{q}"
|
| 175 |
if tag in have:
|
| 176 |
continue
|
| 177 |
+
_log(f"baseline {tag} started")
|
| 178 |
try:
|
| 179 |
+
_new_bar()
|
| 180 |
+
rows = _eval_codec(fmt, q, images, on_image=_bar_tick)
|
| 181 |
except Exception as e:
|
| 182 |
_log(f"baseline {tag} failed: {e}")
|
| 183 |
continue
|
|
|
|
| 200 |
print(f"[sweep] {msg}", flush=True)
|
| 201 |
|
| 202 |
|
| 203 |
+
def _new_bar():
|
| 204 |
+
"""Append a fresh, empty progress line that _bar_tick grows in place."""
|
| 205 |
+
with _LOCK:
|
| 206 |
+
_STATE["log"] = (_STATE["log"] + [""])[-60:]
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def _bar_tick():
|
| 210 |
+
"""Add one '|' to the current progress line (called once per image)."""
|
| 211 |
+
with _LOCK:
|
| 212 |
+
if _STATE["log"]:
|
| 213 |
+
_STATE["log"][-1] += "|"
|
| 214 |
+
|
| 215 |
+
|
| 216 |
def status():
|
| 217 |
with _LOCK:
|
| 218 |
s = dict(_STATE)
|
|
|
|
| 269 |
cfg = _coerce(params)
|
| 270 |
with _LOCK:
|
| 271 |
_STATE["current"] = cfg
|
| 272 |
+
_log(f"grid trial {i + 1}/{len(trials)} started")
|
| 273 |
+
_new_bar()
|
| 274 |
+
rows = _eval_pbc3(cfg, images, on_image=_bar_tick)
|
| 275 |
_add_completed(study, _avg(rows),
|
| 276 |
{"kind": "pbc3", "grid": True, "config": cfg, "per_image": rows})
|
| 277 |
with _LOCK:
|
| 278 |
_STATE["done"] = i + 1
|
|
|
|
| 279 |
else:
|
| 280 |
def objective(trial):
|
| 281 |
cfg = suggest_config(trial)
|
| 282 |
with _LOCK:
|
| 283 |
_STATE["current"] = cfg
|
| 284 |
+
_log(f"optimizer trial {trial.number} started")
|
| 285 |
+
_new_bar()
|
| 286 |
+
rows = _eval_pbc3(cfg, images, on_image=_bar_tick)
|
| 287 |
trial.set_user_attr("kind", "pbc3")
|
| 288 |
trial.set_user_attr("config", cfg)
|
| 289 |
trial.set_user_attr("per_image", rows)
|
|
|
|
| 294 |
n += 1
|
| 295 |
with _LOCK:
|
| 296 |
_STATE["done"] = n
|
|
|
|
| 297 |
except Exception as e:
|
| 298 |
with _LOCK:
|
| 299 |
_STATE["error"] = str(e)
|
server.py
CHANGED
|
@@ -356,11 +356,12 @@ async def decode(file: UploadFile = File(...)):
|
|
| 356 |
BPP_GUIDE = {
|
| 357 |
"JPEG": [(1, 0.17), (5, 0.21), (10, 0.29), (20, 0.44), (40, 0.65), (60, 0.85), (80, 1.26), (95, 2.77)],
|
| 358 |
"AVIF": [(0, 0.01), (2, 0.02), (5, 0.04), (10, 0.06), (20, 0.12), (40, 0.30), (60, 0.68), (80, 1.16), (95, 2.69)],
|
|
|
|
| 359 |
}
|
| 360 |
# Quality samples for the tradeoff baselines (AVIF goes down to 0, JPEG stops at 1).
|
| 361 |
-
JPEG_QUALITIES = (1, 2, 3, 5, 8, 10,
|
| 362 |
-
AVIF_QUALITIES = (0, 1, 2, 3, 5, 8, 10,
|
| 363 |
-
|
| 364 |
|
| 365 |
def _q_bounds(fmt):
|
| 366 |
return (1, 95) if fmt == "JPEG" else (0, 95)
|
|
|
|
| 356 |
BPP_GUIDE = {
|
| 357 |
"JPEG": [(1, 0.17), (5, 0.21), (10, 0.29), (20, 0.44), (40, 0.65), (60, 0.85), (80, 1.26), (95, 2.77)],
|
| 358 |
"AVIF": [(0, 0.01), (2, 0.02), (5, 0.04), (10, 0.06), (20, 0.12), (40, 0.30), (60, 0.68), (80, 1.16), (95, 2.69)],
|
| 359 |
+
"WEBP": [(0, 0.06), (5, 0.13), (10, 0.17), (20, 0.25), (40, 0.40), (60, 0.55), (80, 0.85), (95, 1.80)],
|
| 360 |
}
|
| 361 |
# Quality samples for the tradeoff baselines (AVIF goes down to 0, JPEG stops at 1).
|
| 362 |
+
JPEG_QUALITIES = (1, 2, 3, 5, 8, 10, 20, 40, 60, 80, 95)
|
| 363 |
+
AVIF_QUALITIES = (0, 1, 2, 3, 5, 8, 10, 20, 40, 60, 80, 95)
|
| 364 |
+
WEBP_QUALITIES = (0, 1, 2, 3, 5, 8, 10, 20, 40, 60, 80, 95)
|
| 365 |
|
| 366 |
def _q_bounds(fmt):
|
| 367 |
return (1, 95) if fmt == "JPEG" else (0, 95)
|
static/app.js
CHANGED
|
@@ -317,6 +317,7 @@ function enterApp() {
|
|
| 317 |
document.getElementById("app").hidden = false;
|
| 318 |
landingActive = false; // stop the teaser — no wasted compute
|
| 319 |
if (streamAbort) streamAbort.abort();
|
|
|
|
| 320 |
}
|
| 321 |
function exitToLanding() {
|
| 322 |
document.getElementById("app").hidden = true;
|
|
@@ -750,7 +751,7 @@ function renderRegistry() {
|
|
| 750 |
// raw (3 bytes RGB) / bpp.
|
| 751 |
const bppToRate = (bpp) => 24 / bpp;
|
| 752 |
|
| 753 |
-
const CODEC_COLOR = { jpeg: "#7d8cff", avif: "#34d39a" };
|
| 754 |
const PBC_COLOR = "#ff3b41";
|
| 755 |
|
| 756 |
function card(d) {
|
|
@@ -774,12 +775,13 @@ function card(d) {
|
|
| 774 |
: "";
|
| 775 |
|
| 776 |
// Only show codecs that haven't been generated yet; drop the whole row once both exist.
|
| 777 |
-
const haveJ = !!d.jpeg_image, haveA = !!d.avif_image;
|
| 778 |
-
const compareBlock = (!d.decoded && d.original_image && !(haveJ && haveA))
|
| 779 |
? `<div class="foot-row" style="align-items:center;gap:8px">
|
| 780 |
<span class="k">Compare to:</span>
|
| 781 |
${haveJ ? "" : `<button class="chip" data-act="cmp-jpeg" style="color:#7d8cff;border-color:#7d8cff">JPEG</button>`}
|
| 782 |
${haveA ? "" : `<button class="chip" data-act="cmp-avif" style="color:#34d39a;border-color:#34d39a">AVIF</button>`}
|
|
|
|
| 783 |
</div>`
|
| 784 |
: "";
|
| 785 |
const haveAnim = !!d.animation_url;
|
|
@@ -815,8 +817,10 @@ function card(d) {
|
|
| 815 |
el.querySelector('[data-act="pbc"]').onclick = () => savePbc(d);
|
| 816 |
const cmpJ = el.querySelector('[data-act="cmp-jpeg"]');
|
| 817 |
const cmpA = el.querySelector('[data-act="cmp-avif"]');
|
|
|
|
| 818 |
if (cmpJ) cmpJ.onclick = () => compareCodec(d, "jpeg", cmpJ);
|
| 819 |
if (cmpA) cmpA.onclick = () => compareCodec(d, "avif", cmpA);
|
|
|
|
| 820 |
const animBtn = el.querySelector('[data-act="anim"]');
|
| 821 |
if (animBtn) animBtn.onclick = () => createAnimation(d, animBtn);
|
| 822 |
el.querySelector('[data-act="delete"]').onclick = () => {
|
|
@@ -992,6 +996,7 @@ const viewerStage = document.getElementById("viewer-stage");
|
|
| 992 |
const holdBtn = document.getElementById("hold-original");
|
| 993 |
const holdJpeg = document.getElementById("hold-jpeg");
|
| 994 |
const holdAvif = document.getElementById("hold-avif");
|
|
|
|
| 995 |
const sbsToggle = document.getElementById("viewer-sbs");
|
| 996 |
const prevBtn = document.getElementById("viewer-prev");
|
| 997 |
const nextBtn = document.getElementById("viewer-next");
|
|
@@ -1025,7 +1030,10 @@ function step(dir) {
|
|
| 1025 |
}
|
| 1026 |
|
| 1027 |
function compareSrcOf(d, kind) {
|
| 1028 |
-
return kind === "original" ? d.original_image
|
|
|
|
|
|
|
|
|
|
| 1029 |
}
|
| 1030 |
|
| 1031 |
function viewerCaption(d) {
|
|
@@ -1062,7 +1070,7 @@ function codecCompareCaptionHTML(d, codec) {
|
|
| 1062 |
function updateCaption() {
|
| 1063 |
const d = registry[viewIndex];
|
| 1064 |
if (!d) return;
|
| 1065 |
-
if (sideBySide && (
|
| 1066 |
capEl.innerHTML = `<div>${codecCompareParts(d, activeCompare).sentence}</div>`; // stats moved on top
|
| 1067 |
else
|
| 1068 |
capEl.textContent = viewerCaption(d);
|
|
@@ -1099,12 +1107,15 @@ function renderViewer() {
|
|
| 1099 |
holdBtn.hidden = !d.original_image;
|
| 1100 |
holdJpeg.hidden = !d.jpeg_image;
|
| 1101 |
holdAvif.hidden = !d.avif_image;
|
|
|
|
| 1102 |
holdBtn.textContent = sideBySide ? "Original" : "Hold for original";
|
| 1103 |
holdJpeg.textContent = sideBySide ? "JPEG" : "Hold for JPEG";
|
| 1104 |
holdAvif.textContent = sideBySide ? "AVIF" : "Hold for AVIF";
|
|
|
|
| 1105 |
holdBtn.classList.toggle("holding", sideBySide && activeCompare === "original");
|
| 1106 |
holdJpeg.classList.toggle("holding", sideBySide && activeCompare === "jpeg");
|
| 1107 |
holdAvif.classList.toggle("holding", sideBySide && activeCompare === "avif");
|
|
|
|
| 1108 |
|
| 1109 |
updateCaption();
|
| 1110 |
prevBtn.disabled = viewIndex <= 0;
|
|
@@ -1144,7 +1155,7 @@ function wireHold(btn, kind, getCaptionHTML) {
|
|
| 1144 |
wireHold(holdBtn, "original");
|
| 1145 |
wireHold(holdJpeg, "jpeg", d => codecCompareCaptionHTML(d, "jpeg"));
|
| 1146 |
wireHold(holdAvif, "avif", d => codecCompareCaptionHTML(d, "avif"));
|
| 1147 |
-
|
| 1148 |
/* ============================================================
|
| 1149 |
DOWNLOAD
|
| 1150 |
============================================================ */
|
|
|
|
| 317 |
document.getElementById("app").hidden = false;
|
| 318 |
landingActive = false; // stop the teaser — no wasted compute
|
| 319 |
if (streamAbort) streamAbort.abort();
|
| 320 |
+
gotoView("compress");
|
| 321 |
}
|
| 322 |
function exitToLanding() {
|
| 323 |
document.getElementById("app").hidden = true;
|
|
|
|
| 751 |
// raw (3 bytes RGB) / bpp.
|
| 752 |
const bppToRate = (bpp) => 24 / bpp;
|
| 753 |
|
| 754 |
+
const CODEC_COLOR = { jpeg: "#7d8cff", avif: "#34d39a", webp: "#ffb454"};
|
| 755 |
const PBC_COLOR = "#ff3b41";
|
| 756 |
|
| 757 |
function card(d) {
|
|
|
|
| 775 |
: "";
|
| 776 |
|
| 777 |
// Only show codecs that haven't been generated yet; drop the whole row once both exist.
|
| 778 |
+
const haveJ = !!d.jpeg_image, haveA = !!d.avif_image, haveW = !!d.webp_image;
|
| 779 |
+
const compareBlock = (!d.decoded && d.original_image && !(haveJ && haveA && haveW))
|
| 780 |
? `<div class="foot-row" style="align-items:center;gap:8px">
|
| 781 |
<span class="k">Compare to:</span>
|
| 782 |
${haveJ ? "" : `<button class="chip" data-act="cmp-jpeg" style="color:#7d8cff;border-color:#7d8cff">JPEG</button>`}
|
| 783 |
${haveA ? "" : `<button class="chip" data-act="cmp-avif" style="color:#34d39a;border-color:#34d39a">AVIF</button>`}
|
| 784 |
+
${haveW ? "" : `<button class="chip" data-act="cmp-webp" style="color:#ffb454;border-color:#ffb454">WebP</button>`}
|
| 785 |
</div>`
|
| 786 |
: "";
|
| 787 |
const haveAnim = !!d.animation_url;
|
|
|
|
| 817 |
el.querySelector('[data-act="pbc"]').onclick = () => savePbc(d);
|
| 818 |
const cmpJ = el.querySelector('[data-act="cmp-jpeg"]');
|
| 819 |
const cmpA = el.querySelector('[data-act="cmp-avif"]');
|
| 820 |
+
const cmpW = el.querySelector('[data-act="cmp-webp"]');
|
| 821 |
if (cmpJ) cmpJ.onclick = () => compareCodec(d, "jpeg", cmpJ);
|
| 822 |
if (cmpA) cmpA.onclick = () => compareCodec(d, "avif", cmpA);
|
| 823 |
+
if (cmpW) cmpW.onclick = () => compareCodec(d, "webp", cmpW);
|
| 824 |
const animBtn = el.querySelector('[data-act="anim"]');
|
| 825 |
if (animBtn) animBtn.onclick = () => createAnimation(d, animBtn);
|
| 826 |
el.querySelector('[data-act="delete"]').onclick = () => {
|
|
|
|
| 996 |
const holdBtn = document.getElementById("hold-original");
|
| 997 |
const holdJpeg = document.getElementById("hold-jpeg");
|
| 998 |
const holdAvif = document.getElementById("hold-avif");
|
| 999 |
+
const holdWebp = document.getElementById("hold-webp");
|
| 1000 |
const sbsToggle = document.getElementById("viewer-sbs");
|
| 1001 |
const prevBtn = document.getElementById("viewer-prev");
|
| 1002 |
const nextBtn = document.getElementById("viewer-next");
|
|
|
|
| 1030 |
}
|
| 1031 |
|
| 1032 |
function compareSrcOf(d, kind) {
|
| 1033 |
+
return kind === "original" ? d.original_image
|
| 1034 |
+
: kind === "jpeg" ? d.jpeg_image
|
| 1035 |
+
: kind === "avif" ? d.avif_image
|
| 1036 |
+
: kind === "webp" ? d.webp_image : null;
|
| 1037 |
}
|
| 1038 |
|
| 1039 |
function viewerCaption(d) {
|
|
|
|
| 1070 |
function updateCaption() {
|
| 1071 |
const d = registry[viewIndex];
|
| 1072 |
if (!d) return;
|
| 1073 |
+
if (sideBySide && (["jpeg", "avif", "webp"].includes(activeCompare)))
|
| 1074 |
capEl.innerHTML = `<div>${codecCompareParts(d, activeCompare).sentence}</div>`; // stats moved on top
|
| 1075 |
else
|
| 1076 |
capEl.textContent = viewerCaption(d);
|
|
|
|
| 1107 |
holdBtn.hidden = !d.original_image;
|
| 1108 |
holdJpeg.hidden = !d.jpeg_image;
|
| 1109 |
holdAvif.hidden = !d.avif_image;
|
| 1110 |
+
holdWebp.hidden = !d.webp_image;
|
| 1111 |
holdBtn.textContent = sideBySide ? "Original" : "Hold for original";
|
| 1112 |
holdJpeg.textContent = sideBySide ? "JPEG" : "Hold for JPEG";
|
| 1113 |
holdAvif.textContent = sideBySide ? "AVIF" : "Hold for AVIF";
|
| 1114 |
+
holdWebp.textContent = sideBySide ? "WebP" : "Hold for WebP";
|
| 1115 |
holdBtn.classList.toggle("holding", sideBySide && activeCompare === "original");
|
| 1116 |
holdJpeg.classList.toggle("holding", sideBySide && activeCompare === "jpeg");
|
| 1117 |
holdAvif.classList.toggle("holding", sideBySide && activeCompare === "avif");
|
| 1118 |
+
holdWebp.classList.toggle("holding", sideBySide && activeCompare === "webp");
|
| 1119 |
|
| 1120 |
updateCaption();
|
| 1121 |
prevBtn.disabled = viewIndex <= 0;
|
|
|
|
| 1155 |
wireHold(holdBtn, "original");
|
| 1156 |
wireHold(holdJpeg, "jpeg", d => codecCompareCaptionHTML(d, "jpeg"));
|
| 1157 |
wireHold(holdAvif, "avif", d => codecCompareCaptionHTML(d, "avif"));
|
| 1158 |
+
wireHold(holdWebp, "webp", d => codecCompareCaptionHTML(d, "webp"));
|
| 1159 |
/* ============================================================
|
| 1160 |
DOWNLOAD
|
| 1161 |
============================================================ */
|
static/index.html
CHANGED
|
@@ -13,8 +13,9 @@
|
|
| 13 |
/* JPEG/AVIF hold buttons reuse the codec colors from the Sweep Analyzer. */
|
| 14 |
#hold-jpeg { --hc:#7d8cff; }
|
| 15 |
#hold-avif { --hc:#34d39a; }
|
| 16 |
-
#hold-
|
| 17 |
-
#hold-jpeg
|
|
|
|
| 18 |
</style>
|
| 19 |
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>👁️</text></svg>">
|
| 20 |
</head>
|
|
@@ -177,6 +178,7 @@
|
|
| 177 |
<button id="hold-original" class="hold-btn" hidden>Hold for original</button>
|
| 178 |
<button id="hold-jpeg" class="hold-btn" hidden>Hold for JPEG</button>
|
| 179 |
<button id="hold-avif" class="hold-btn" hidden>Hold for AVIF</button>
|
|
|
|
| 180 |
</div>
|
| 181 |
</div>
|
| 182 |
|
|
|
|
| 13 |
/* JPEG/AVIF hold buttons reuse the codec colors from the Sweep Analyzer. */
|
| 14 |
#hold-jpeg { --hc:#7d8cff; }
|
| 15 |
#hold-avif { --hc:#34d39a; }
|
| 16 |
+
#hold-webp { --hc:#ffb454; }
|
| 17 |
+
#hold-jpeg, #hold-avif, #hold-webp { border-color: var(--hc); color: var(--hc); }
|
| 18 |
+
#hold-jpeg.holding, #hold-avif.holding, #hold-webp.holding { background: var(--hc); border-color: var(--hc); color: #fff; }
|
| 19 |
</style>
|
| 20 |
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>👁️</text></svg>">
|
| 21 |
</head>
|
|
|
|
| 178 |
<button id="hold-original" class="hold-btn" hidden>Hold for original</button>
|
| 179 |
<button id="hold-jpeg" class="hold-btn" hidden>Hold for JPEG</button>
|
| 180 |
<button id="hold-avif" class="hold-btn" hidden>Hold for AVIF</button>
|
| 181 |
+
<button id="hold-webp" class="hold-btn" hidden>Hold for WEBP</button>
|
| 182 |
</div>
|
| 183 |
</div>
|
| 184 |
|
static/sweep.js
CHANGED
|
@@ -16,15 +16,15 @@
|
|
| 16 |
study: null, trials: [], filtered: [],
|
| 17 |
selected: null, codec: null, built: false, fsKind: null,
|
| 18 |
sort: { key: "mse", dir: 1 }, // MSE ascending = best first
|
| 19 |
-
show: { pbcAll: true, pbcPareto: true, jpeg: false, avif: false },
|
| 20 |
dyn: [], mpMin: null, mpMax: null,
|
| 21 |
};
|
| 22 |
|
| 23 |
const COLORS = {
|
| 24 |
gray: "#56565d", pareto: "#ff9b2e", sel: "#ff3b41", base: "#34c6ff",
|
| 25 |
-
jpeg: "#7d8cff", avif: "#34d39a",
|
| 26 |
};
|
| 27 |
-
const TOGGLES = [["pbcAll", "PBC All"], ["pbcPareto", "PBC Pareto"], ["jpeg", "JPEG"], ["avif", "AVIF"]];
|
| 28 |
// Recommendation weight presets: [speed, compression, mse].
|
| 29 |
const PRESETS = { "Fast": [80, 15, 5], "Tiny File": [10, 80, 10], "Balanced": [33, 33, 34], "Best Quality": [5, 15, 80] };
|
| 30 |
|
|
@@ -242,8 +242,8 @@
|
|
| 242 |
function buildCodec(all) {
|
| 243 |
const pick = (c) => all.filter((t) => t.codec === c)
|
| 244 |
.map((t) => ({ q: t.q, bpp: t.bpp, mse: t.mse, speed: t.speed, eff: t.eff })).sort((a, b) => a.q - b.q);
|
| 245 |
-
const jpeg = pick("JPEG"), avif = pick("AVIF");
|
| 246 |
-
S.codec = (jpeg.length || avif.length) ? { jpeg, avif } : null;
|
| 247 |
}
|
| 248 |
|
| 249 |
function renderSummary() {
|
|
@@ -407,7 +407,7 @@
|
|
| 407 |
if (S.show.pbcAll) traces.push(ptTrace(all, xKey, yKey, { name: "PBC all", marker: { size: 5, color: COLORS.gray, opacity: .55 } }));
|
| 408 |
if (S.show.pbcPareto) traces.push(ptTrace(all.filter((t) => t.pareto), xKey, yKey, { name: "PBC Pareto", marker: { size: 8, color: COLORS.pareto } }));
|
| 409 |
|
| 410 |
-
[["jpeg", COLORS.jpeg], ["avif", COLORS.avif]].forEach(([k, c]) => {
|
| 411 |
if (!S.show[k]) return;
|
| 412 |
const pts = codecPoints(k);
|
| 413 |
if (pts && pts.length) traces.push(codecTrace([...pts].sort((a, b) => a[xKey] - b[xKey]), xKey, yKey, k.toUpperCase(), c));
|
|
|
|
| 16 |
study: null, trials: [], filtered: [],
|
| 17 |
selected: null, codec: null, built: false, fsKind: null,
|
| 18 |
sort: { key: "mse", dir: 1 }, // MSE ascending = best first
|
| 19 |
+
show: { pbcAll: true, pbcPareto: true, jpeg: false, avif: false, webp: false },
|
| 20 |
dyn: [], mpMin: null, mpMax: null,
|
| 21 |
};
|
| 22 |
|
| 23 |
const COLORS = {
|
| 24 |
gray: "#56565d", pareto: "#ff9b2e", sel: "#ff3b41", base: "#34c6ff",
|
| 25 |
+
jpeg: "#7d8cff", avif: "#34d39a", webp: "#ffb454",
|
| 26 |
};
|
| 27 |
+
const TOGGLES = [["pbcAll", "PBC All"], ["pbcPareto", "PBC Pareto"], ["jpeg", "JPEG"], ["avif", "AVIF"], ["webp", "WebP"]];
|
| 28 |
// Recommendation weight presets: [speed, compression, mse].
|
| 29 |
const PRESETS = { "Fast": [80, 15, 5], "Tiny File": [10, 80, 10], "Balanced": [33, 33, 34], "Best Quality": [5, 15, 80] };
|
| 30 |
|
|
|
|
| 242 |
function buildCodec(all) {
|
| 243 |
const pick = (c) => all.filter((t) => t.codec === c)
|
| 244 |
.map((t) => ({ q: t.q, bpp: t.bpp, mse: t.mse, speed: t.speed, eff: t.eff })).sort((a, b) => a.q - b.q);
|
| 245 |
+
const jpeg = pick("JPEG"), avif = pick("AVIF"), webp = pick("WEBP");
|
| 246 |
+
S.codec = (jpeg.length || avif.length || webp.length) ? { jpeg, avif, webp } : null;
|
| 247 |
}
|
| 248 |
|
| 249 |
function renderSummary() {
|
|
|
|
| 407 |
if (S.show.pbcAll) traces.push(ptTrace(all, xKey, yKey, { name: "PBC all", marker: { size: 5, color: COLORS.gray, opacity: .55 } }));
|
| 408 |
if (S.show.pbcPareto) traces.push(ptTrace(all.filter((t) => t.pareto), xKey, yKey, { name: "PBC Pareto", marker: { size: 8, color: COLORS.pareto } }));
|
| 409 |
|
| 410 |
+
[["jpeg", COLORS.jpeg], ["avif", COLORS.avif], ["webp", COLORS.webp]].forEach(([k, c]) => {
|
| 411 |
if (!S.show[k]) return;
|
| 412 |
const pts = codecPoints(k);
|
| 413 |
if (pts && pts.length) traces.push(codecTrace([...pts].sort((a, b) => a[xKey] - b[xKey]), xKey, yKey, k.toUpperCase(), c));
|