Spaces:
Running
Running
| """Damage simulator: degrade a FULL clean image with a sampled recipe. | |
| webapp_spec "/api/simulate": use ``sample_recipe(severity, rng)`` + ``apply_recipe`` | |
| on the FULL resized image (NOT ``make_training_pair``'s 63x63 crop). This gives | |
| the demo a clean reference, so the restoration that follows can report true PSNR. | |
| The recipe RNG is seeded so a (severity, seed) pair is reproducible — the noise | |
| realisation and the band-within-level draws are deterministic. | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| from rlrestore.data.pipeline import apply_recipe | |
| from rlrestore.data.recipes import ( | |
| BLUR_GRID, | |
| JPEG_GRID, | |
| NOISE_GRID, | |
| RecipeParams, | |
| SEVERITY_LEVELS, | |
| ) | |
| from .imaging import encode_dataurl | |
| from .quality import psnr as ref_psnr | |
| __all__ = ["simulate", "demo_recipe", "SEVERITIES"] | |
| SEVERITIES = ["mild", "moderate", "severe"] | |
| # Demo damage emphasises what the toolchain actually reverses well: heavy sensor | |
| # noise and JPEG blocking (the realistic "low-light phone photo"), with only | |
| # slight blur. High-level Gaussian blur is near-irreversible, so a blur-dominant | |
| # draw would denoise to a still-blurry result and read as "nothing happened" — | |
| # this keeps the simulator's before/after legible. Each triple stays inside its | |
| # severity band (sum(levels) - 2 in the SEVERITY_LEVELS range); the scientific | |
| # evaluation in reports/ uses the full sample_recipe distribution, not this. | |
| _DEMO_LEVELS = { | |
| "mild": [(1, 6, 4), (1, 7, 3)], | |
| "moderate": [(1, 9, 4), (2, 8, 5), (1, 8, 6)], | |
| "severe": [(2, 10, 8), (1, 10, 9), (2, 10, 9)], | |
| } | |
| def demo_recipe(severity: str, rng) -> RecipeParams: | |
| """A noise/JPEG-dominant, low-blur recipe for the demo simulator.""" | |
| choices = _DEMO_LEVELS[severity] | |
| b, n, j = choices[int(rng.integers(len(choices)))] | |
| return RecipeParams( | |
| (b, n, j), | |
| float(rng.uniform(BLUR_GRID[b - 1], BLUR_GRID[b])), | |
| float(rng.uniform(NOISE_GRID[n - 1], NOISE_GRID[n])), | |
| float(rng.uniform(JPEG_GRID[j], JPEG_GRID[j - 1])), | |
| ) | |
| def simulate( | |
| clean_img: np.ndarray, | |
| severity: str, | |
| seed: int, | |
| fmt: str = "png", | |
| ) -> dict: | |
| """Degrade ``clean_img`` (HWC float32 [0,1], already resized) at ``severity``. | |
| Returns a dict with clean/degraded data URLs, the recipe params, and the | |
| reference PSNR of degraded-vs-clean. ``degraded_img`` is included (not in the | |
| JSON) so the caller can chain a restoration with the clean reference. | |
| """ | |
| if severity not in SEVERITY_LEVELS: | |
| raise ValueError( | |
| f"severity must be one of {sorted(SEVERITY_LEVELS)}, got {severity!r}" | |
| ) | |
| rng = np.random.default_rng(seed) | |
| clean = np.ascontiguousarray(clean_img, dtype=np.float32) | |
| params = demo_recipe(severity, rng) | |
| degraded = apply_recipe(clean, params, rng) | |
| psnr_deg = ref_psnr(degraded, clean) | |
| return { | |
| "severity": severity, | |
| "seed": int(seed), | |
| "recipe": { | |
| "blur_sigma": float(params.blur_sigma), | |
| "noise_sigma": float(params.noise_sigma), | |
| "jpeg_quality": float(params.jpeg_quality), | |
| "levels": list(params.levels), | |
| }, | |
| "clean": encode_dataurl(clean, clamp=True, fmt=fmt), | |
| "degraded": encode_dataurl(degraded, clamp=True, fmt=fmt), | |
| "psnr_degraded_vs_clean": float(psnr_deg), | |
| # Not serialised directly — used by the route to optionally restore. | |
| "_clean_img": clean, | |
| "_degraded_img": degraded, | |
| } | |