"""Image denoising runnable example. Upload a noisy (microscopy) image; restore it. Several engines behind one contract: * nlm / tv / wavelet — classical denoisers (scikit-image), always runnable * deep — a DnCNN trained on synthetic data at build time (Dockerfile.deep) Contract surfaces (do not edit): /process via gr.api (also the MCP tool), ?file_url=... loader, temp-file cleanup daemon. """ from __future__ import annotations import json import os import tempfile import urllib.request import gradio as gr import numpy as np from PIL import Image from core import cleanup from core.io import APP_TMP_DIR, new_tmp_path, register_protected from core.processing import ENGINES, process, simulate_full APP_TITLE = "Image denoising" APP_DESCRIPTION = ( "Upload a noisy image (PNG/JPG/TIFF). Restore it with a classical denoiser " "(**nlm**, **tv**, **wavelet**) or a **deep** DnCNN trained on synthetic data. " "A synthetic noisy microscopy image (with the clean ground truth) is baked in, so " "PSNR/SSIM are reported for the noisy input and the denoised result." ) INPUT_FILE_TYPES = [".png", ".jpg", ".jpeg", ".tif", ".tiff"] cleanup.start() def _baked_example() -> str | None: p = APP_TMP_DIR / "example.tif" return register_protected(p) if p.exists() else None EXAMPLE_PATH = _baked_example() def _save_png(arr: np.ndarray, basename: str) -> str: out_path = new_tmp_path(basename) Image.fromarray(arr.astype(np.uint8)).save(out_path) return out_path # ----------------- Public API (also the MCP tool) ----------------- def process_api(file_url: str, engine: str = "nlm", strength: float = 1.0) -> gr.FileData: """Denoise an image and return a noisy/denoised/clean summary PNG. Parameters ---------- file_url : noisy image (PNG/JPG/TIFF) as a URL or local path engine : 'nlm' | 'tv' | 'wavelet' (classical) or 'deep' (DnCNN) strength : denoising strength (classical engines) """ img, report = process(file_url, engine=engine, strength=float(strength)) out_path = _save_png(np.asarray(img), basename="denoise.png") print("[process_api] " + json.dumps(report)) return gr.FileData(path=out_path, orig_name=os.path.basename(out_path), mime_type="image/png") def analyze_api(file_url: str, engine: str = "nlm", strength: float = 1.0) -> dict: """Return the denoising report (engine, PSNR/SSIM noisy vs denoised) as JSON.""" _img, report = process(file_url, engine=engine, strength=float(strength)) return report # ----------------- UI ----------------- def _run_ui(file_obj, engine, strength, progress=gr.Progress(track_tqdm=True)): if file_obj is None: return None, "Upload a noisy image first." progress(0.3, desc=f"Denoising ({engine})…") r = simulate_full(file_obj, engine=engine, strength=float(strength)) progress(1.0, desc="Done") return r["summary"], json.dumps(r["report"], indent=2) with gr.Blocks(title=APP_TITLE, delete_cache=(1800, 21600)) as demo: gr.api( process_api, api_name="process", api_description=( "Input: a single noisy 2D IMAGE (PNG/JPG/TIFF), read as grayscale. " "Denoise an image (URL, file, or bytes) and return a noisy/denoised/clean " "summary PNG. engine: 'nlm' | 'tv' | 'wavelet' | 'deep'." ), ) gr.api( analyze_api, api_name="analyze", api_description="Same input as `process` (a noisy 2D image). Return the denoising report (engine, PSNR/SSIM noisy vs denoised) as JSON.", ) gr.Markdown(f"# {APP_TITLE}") gr.Markdown(APP_DESCRIPTION) last_url_state = gr.State("") with gr.Row(): file_input = gr.File(file_types=INPUT_FILE_TYPES, file_count="single", label="Upload a noisy image") with gr.Column(): engine_input = gr.Dropdown(choices=ENGINES, value="nlm", label="Engine") strength_input = gr.Slider(0.2, 3.0, value=1.0, step=0.1, label="Strength (classical)") if EXAMPLE_PATH: gr.Examples( examples=[[EXAMPLE_PATH, "nlm", 1.0]], inputs=[file_input, engine_input, strength_input], label="Try the baked-in noisy microscopy image!", ) run_btn = gr.Button("Run", variant="primary") with gr.Row(): summary_out = gr.Image(label="Noisy · denoised · clean", type="numpy") report_box = gr.Code(label="Report", language="json") run_btn.click( fn=_run_ui, inputs=[file_input, engine_input, strength_input], outputs=[summary_out, report_box], show_api=False, ) @demo.load(inputs=[last_url_state], outputs=[last_url_state, file_input], show_api=False) def _load_from_query(prev_url, request: gr.Request): url = (request.query_params or {}).get("file_url") or "" if not url or url == prev_url: return [gr.skip(), gr.skip()] suffix = os.path.splitext(url)[1] or ".png" fd, tmp_path = tempfile.mkstemp(suffix=suffix, dir=str(APP_TMP_DIR)) os.close(fd) try: urllib.request.urlretrieve(url, tmp_path) except Exception as e: # noqa: BLE001 try: os.remove(tmp_path) except Exception: # noqa: BLE001 pass raise gr.Error(f"Failed to download file_url: {e}") return [url, gr.update(value=tmp_path)] if __name__ == "__main__": demo.queue(default_concurrency_limit=1, max_size=16).launch( server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)), mcp_server=True, )