rl-restore / backend /simulate.py
parhamkhoshsolat's picture
Noise/JPEG-dominant demo damage so restoration is visibly real
5329ca4 verified
Raw
History Blame Contribute Delete
3.5 kB
"""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,
}