| import glob |
| import io |
| import math |
| import os |
| import sqlite3 |
| import threading |
| import time |
|
|
| import numpy as np |
| from PIL import Image, ImageOps |
|
|
| from PBC3 import PBC3, preload_numba |
| from pbc3_types import PBC3Config |
|
|
| try: |
| import pillow_jxl |
| except Exception: |
| pillow_jxl = None |
|
|
| PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) |
| DATA_DIR = os.path.join(PROJECT_DIR, "hpt_data") |
| DB_PATH = os.path.join(PROJECT_DIR, "pbc3_benchmark.db") |
| IMAGE_EXTS = ("*.png", "*.jpg", "*.jpeg", "*.webp", "*.bmp") |
|
|
| PBC3_PRESETS = ("compression", "balanced", "quality", "high_quality") |
| JPEG_QUALITIES = (1, 3, 5, 10, 20, 40, 70, 95) |
| JP2_QUALITIES = (0, 1, 3, 5, 10, 20, 40, 70, 95) |
| WEBP_QUALITIES = (0, 1, 3, 5, 10, 20, 40, 70, 95) |
| JXL_QUALITIES = (0, 1, 3, 5, 10, 20, 40, 70, 95) |
| AVIF_QUALITIES = (0, 1, 3, 5, 10, 20, 40, 70, 95) |
| CODECS = ("PBC3", "JPEG", "JPEG2000", "WEBP", "JXL", "AVIF", "PNG") |
|
|
| _LOCK = threading.Lock() |
| _STOP = threading.Event() |
| _THREAD = None |
| _STATE = {"running": False, "done": 0, "total": None, "current": None, "started": None, "error": None, "log": []} |
|
|
|
|
| def _log(msg): |
| with _LOCK: |
| _STATE["log"] = (_STATE["log"] + [f"{time.strftime('%H:%M:%S')} {msg}"])[-5000:] |
| print(f"[benchmark] {msg}", flush=True) |
|
|
|
|
| def _new_bar(): |
| with _LOCK: |
| _STATE["log"] = (_STATE["log"] + [""])[-5000:] |
|
|
|
|
| def _bar_tick(): |
| with _LOCK: |
| if _STATE["log"]: |
| _STATE["log"][-1] += "|" |
|
|
|
|
| def status(): |
| with _LOCK: |
| s = dict(_STATE) |
| s["log"] = list(_STATE["log"]) |
| return s |
|
|
|
|
| def _connect(): |
| con = sqlite3.connect(DB_PATH) |
| con.row_factory = sqlite3.Row |
| con.execute(""" |
| CREATE TABLE IF NOT EXISTS results ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| ts REAL NOT NULL, |
| run_id TEXT NOT NULL, |
| trial INTEGER NOT NULL, |
| codec TEXT NOT NULL, |
| variant TEXT NOT NULL, |
| q REAL, |
| image TEXT NOT NULL, |
| mp REAL NOT NULL, |
| encode_seconds REAL NOT NULL, |
| decode_seconds REAL NOT NULL, |
| bpp REAL NOT NULL, |
| mse REAL NOT NULL, |
| psnr REAL NOT NULL |
| ) |
| """) |
| con.execute("CREATE INDEX IF NOT EXISTS idx_results_codec ON results(codec, variant, q)") |
| con.execute("CREATE INDEX IF NOT EXISTS idx_results_mp ON results(mp)") |
| con.commit() |
| return con |
|
|
|
|
| def dataset(): |
| paths = sorted(p for ext in IMAGE_EXTS for p in glob.glob(os.path.join(DATA_DIR, ext))) |
| images = [] |
| for p in paths: |
| try: |
| img = ImageOps.exif_transpose(Image.open(p)).convert("RGB") |
| arr = np.asarray(img) |
| h, w = arr.shape[:2] |
| images.append({"name": os.path.basename(p), "mp": round(w * h / 1e6, 4), "arr": arr}) |
| except Exception as e: |
| _log(f"skipped {os.path.basename(p)}: {e}") |
| return images |
|
|
|
|
| def dataset_summary(): |
| return {"count": len(dataset()), "images": [{"name": im["name"], "mp": im["mp"]} for im in dataset()]} |
|
|
|
|
| def _mse(a, b): |
| return float(np.mean((a.astype(np.float32) - b.astype(np.float32)) ** 2)) |
|
|
|
|
| def _psnr(mse): |
| if mse <= 0: |
| return 99.0 |
| return float(10.0 * math.log10((255.0 * 255.0) / mse)) |
|
|
|
|
| def _jp2_rate(q): |
| q = max(0.0, min(95.0, float(q))) / 95.0 |
| return 200.0 ** (1.0 - q) |
|
|
|
|
| def _encode_codec_bytes(img, fmt, q): |
| buf = io.BytesIO() |
| if fmt == "JPEG2000": |
| img.save(buf, format="JPEG2000", quality_mode="rates", quality_layers=[_jp2_rate(q)]) |
| elif fmt == "PNG": |
| img.save(buf, format="PNG") |
| else: |
| img.save(buf, format=fmt, quality=int(q)) |
| return buf.getvalue() |
|
|
|
|
| def _eval_pbc3(arr, preset): |
| pixels = arr.shape[0] * arr.shape[1] |
| cfg = getattr(PBC3Config, preset)() |
| t = time.perf_counter() |
| res = PBC3.compress(Image.fromarray(arr), config=cfg) |
| enc = time.perf_counter() - t |
| t = time.perf_counter() |
| dec = PBC3.decompress(res.data) |
| dec_s = time.perf_counter() - t |
| recon = np.asarray(dec.image.convert("RGB").resize((arr.shape[1], arr.shape[0]))) |
| mse = _mse(arr, recon) |
| return enc, dec_s, float(len(res.data) * 8 / pixels), mse, _psnr(mse) |
|
|
|
|
| def _eval_codec(arr, fmt, q): |
| pixels = arr.shape[0] * arr.shape[1] |
| img = Image.fromarray(arr) |
| t = time.perf_counter() |
| data = _encode_codec_bytes(img, fmt, q) |
| enc = time.perf_counter() - t |
| t = time.perf_counter() |
| rec_img = Image.open(io.BytesIO(data)).convert("RGB") |
| rec_img.load() |
| dec_s = time.perf_counter() - t |
| recon = np.asarray(rec_img.resize((arr.shape[1], arr.shape[0]))) |
| mse = _mse(arr, recon) |
| return enc, dec_s, float(len(data) * 8 / pixels), mse, _psnr(mse) |
|
|
|
|
| def _jobs(codecs): |
| jobs = [] |
| selected = set(codecs or CODECS) |
| if "PBC3" in selected: |
| jobs += [("PBC3", p, None) for p in PBC3_PRESETS] |
| for codec, fmt, qs in ( |
| ("JPEG", "JPEG", JPEG_QUALITIES), |
| ("JPEG2000", "JPEG2000", JP2_QUALITIES), |
| ("WEBP", "WEBP", WEBP_QUALITIES), |
| ("JXL", "JXL", JXL_QUALITIES), |
| ("AVIF", "AVIF", AVIF_QUALITIES), |
| ): |
| if codec in selected: |
| jobs += [(codec, f"q{q}", q) for q in qs] |
| if "PNG" in selected: |
| jobs.append(("PNG", "lossless", 0)) |
| return jobs |
|
|
|
|
| def start(codecs=None, n_trials=1): |
| global _THREAD |
| with _LOCK: |
| if _STATE["running"]: |
| return {"error": "A benchmark is already running."} |
| if not os.path.isdir(DATA_DIR): |
| return {"error": "Dataset folder missing: hpt_data/"} |
| n_trials = max(1, min(10, int(n_trials or 1))) |
| selected = [c for c in (codecs or CODECS) if c in CODECS] |
| if not selected: |
| return {"error": "Select at least one codec."} |
| _STOP.clear() |
| _THREAD = threading.Thread(target=_run, args=(selected, n_trials), daemon=True) |
| _THREAD.start() |
| return {"started": True, "codecs": selected, "n_trials": n_trials} |
|
|
|
|
| def stop(): |
| _STOP.set() |
| return {"stopping": True} |
|
|
|
|
| def _insert(con, row): |
| con.execute(""" |
| INSERT INTO results |
| (ts, run_id, trial, codec, variant, q, image, mp, encode_seconds, decode_seconds, bpp, mse, psnr) |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| """, row) |
|
|
|
|
| def _run(codecs, n_trials): |
| with _LOCK: |
| _STATE.update(running=True, done=0, total=None, current=None, started=time.time(), error=None, log=[]) |
| try: |
| images = dataset() |
| if not images: |
| raise RuntimeError("No images found in hpt_data/") |
| preload_numba(os.path.join(PROJECT_DIR, "patch_policy.npz")) |
| jobs = _jobs(codecs) |
| total = n_trials * len(jobs) * len(images) |
| run_id = time.strftime("%Y%m%d_%H%M%S") |
| with _LOCK: |
| _STATE["total"] = total |
| _log(f"loaded {len(images)} images · {len(jobs)} data points · {n_trials} trial(s)") |
| done = 0 |
| con = _connect() |
| try: |
| for trial in range(1, n_trials + 1): |
| for codec, variant, q in jobs: |
| if _STOP.is_set(): |
| return |
| with _LOCK: |
| _STATE["current"] = {"trial": trial, "codec": codec, "variant": variant} |
| _log(f"trial {trial}/{n_trials} {codec} {variant} started") |
| _new_bar() |
| for im in images: |
| if _STOP.is_set(): |
| return |
| try: |
| if codec == "PBC3": |
| enc, dec, bpp, mse, psnr = _eval_pbc3(im["arr"], variant) |
| else: |
| fmt = "PNG" if codec == "PNG" else codec |
| enc, dec, bpp, mse, psnr = _eval_codec(im["arr"], fmt, q) |
| _insert(con, (time.time(), run_id, trial, codec, variant, q, im["name"], im["mp"], enc, dec, bpp, mse, psnr)) |
| con.commit() |
| except Exception as e: |
| _log(f"{codec} {variant} failed on {im['name']}: {e}") |
| done += 1 |
| with _LOCK: |
| _STATE["done"] = done |
| _bar_tick() |
| _log(f"trial {trial}/{n_trials} {codec} {variant} done") |
| finally: |
| con.close() |
| except Exception as e: |
| with _LOCK: |
| _STATE["error"] = str(e) |
| _log(f"ERROR: {e}") |
| finally: |
| with _LOCK: |
| _STATE["running"] = False |
| _STATE["current"] = None |
| _log("benchmark stopped") |
|
|
|
|
| def reset_pbc(): |
| if status()["running"]: |
| return {"error": "Stop the benchmark before resetting PBC3 rows."} |
| if not os.path.exists(DB_PATH): |
| return {"ok": True, "deleted": 0} |
| con = _connect() |
| try: |
| cur = con.execute("DELETE FROM results WHERE codec = ?", ("PBC3",)) |
| con.commit() |
| return {"ok": True, "deleted": cur.rowcount} |
| finally: |
| con.close() |
|
|
|
|
| def results(mp_min=None, mp_max=None): |
| if not os.path.exists(DB_PATH): |
| return {"exists": False, "rows": [], "points": []} |
| con = _connect() |
| try: |
| where, args = [], [] |
| if mp_min is not None: |
| where.append("mp >= ?"); args.append(float(mp_min)) |
| if mp_max is not None: |
| where.append("mp <= ?"); args.append(float(mp_max)) |
| w = "WHERE " + " AND ".join(where) if where else "" |
| rows = [dict(r) for r in con.execute(f"SELECT * FROM results {w} ORDER BY id", args)] |
| points = [dict(r) for r in con.execute(f""" |
| SELECT codec, variant, q, COUNT(*) AS count, COUNT(DISTINCT image) AS images, |
| AVG(encode_seconds) AS encode_seconds, AVG(decode_seconds) AS decode_seconds, |
| AVG(bpp) AS bpp, AVG(mse) AS mse, AVG(psnr) AS psnr |
| FROM results {w} |
| GROUP BY codec, variant, q |
| ORDER BY codec, q, variant |
| """, args)] |
| return {"exists": True, "rows": rows, "points": points, "count": len(rows)} |
| finally: |
| con.close() |
|
|