CoolFace
Apppublic

LEGENDFTW/image-filtering-explorer

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
app.py317 linesDownload Raw Back to root
1import sys2import os3sys.path.insert(0, os.path.dirname(__file__))4 5import streamlit as st6import numpy as np7from PIL import Image8import io9import json10import matplotlib11matplotlib.use("Agg")12import matplotlib.pyplot as plt13 14from processing import apply_filter, FILTER_DESCRIPTIONS, add_synthetic_noise15from metrics import compute_metrics16from utils import load_sample_image, overlay_noise_heatmap17 18# ── Page config ───────────────────────────────────────────────────────────────19st.set_page_config(20    page_title="Image Filtering & Denoising Explorer",21    layout="wide",22)23 24# ── Cached helpers ────────────────────────────────────────────────────────────25@st.cache_data(show_spinner=False)26def cached_apply_filter(img_bytes: bytes, filter_name: str, params_json: str):27    img = np.array(Image.open(io.BytesIO(img_bytes)))28    return apply_filter(img, filter_name, json.loads(params_json))29 30@st.cache_data(show_spinner=False)31def cached_add_noise(img_bytes: bytes, noise_type: str, level: int):32    img = np.array(Image.open(io.BytesIO(img_bytes)))33    return add_synthetic_noise(img, noise_type, level)34 35@st.cache_data(show_spinner=False)36def cached_metrics(input_bytes: bytes, filtered_bytes: bytes):37    inp = np.array(Image.open(io.BytesIO(input_bytes)))38    flt = np.array(Image.open(io.BytesIO(filtered_bytes)))39    return compute_metrics(inp, flt)40 41@st.cache_data(show_spinner=False)42def cached_heatmap(input_bytes: bytes, filtered_bytes: bytes):43    inp = np.array(Image.open(io.BytesIO(input_bytes)))44    flt = np.array(Image.open(io.BytesIO(filtered_bytes)))45    return overlay_noise_heatmap(inp, flt)46 47@st.cache_data(show_spinner=False)48def cached_histogram_image(input_bytes: bytes, filtered_bytes: bytes) -> bytes:49    inp = np.array(Image.open(io.BytesIO(input_bytes)))50    flt = np.array(Image.open(io.BytesIO(filtered_bytes)))51    fig, axes = plt.subplots(1, 2, figsize=(10, 3), facecolor="#1a1a1a")52    titles = ["Input pixel intensity", "Filtered pixel intensity"]53    colors = ["#e74c3c", "#2ecc71", "#3498db"]54    names  = ["Red", "Green", "Blue"]55    for ax, arr, title in zip(axes, [inp, flt], titles):56        ax.set_facecolor("#1a1a1a")57        for c, (color, name) in enumerate(zip(colors, names)):58            hist, edges = np.histogram(arr[:, :, c].ravel(), bins=64, range=(0, 255))59            ax.bar(edges[:-1], hist, width=4, color=color, alpha=0.6, label=name)60        ax.set_title(title, color="white", fontsize=10)61        ax.tick_params(colors="gray")62        for spine in ax.spines.values():63            spine.set_color("#444")64    axes[0].legend(facecolor="#222", labelcolor="white", fontsize=8)65    fig.tight_layout()66    buf = io.BytesIO()67    fig.savefig(buf, format="png", facecolor="#1a1a1a", dpi=100)68    plt.close(fig)69    buf.seek(0)70    return buf.read()71 72def arr_to_bytes(arr: np.ndarray) -> bytes:73    buf = io.BytesIO()74    Image.fromarray(arr.astype(np.uint8)).save(buf, format="PNG")75    return buf.getvalue()76 77# ── CSS ───────────────────────────────────────────────────────────────────────78st.markdown("""79<style>80    .main-title  { font-size:2.2rem; font-weight:700; margin-bottom:0.2rem; }81    .sub-title   { font-size:1rem; color:#888; margin-bottom:1.5rem; }82    .metric-card { background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.1);83                   border-radius:10px; padding:14px 18px; margin:6px 0; }84    .metric-label{ font-size:0.78rem; color:#aaa; font-weight:600;85                   letter-spacing:.05em; text-transform:uppercase; }86    .metric-value{ font-size:1.6rem; font-weight:700; }87    .filter-desc { background:rgba(255,255,255,0.06); border-radius:8px;88                   padding:12px 16px; font-size:0.9rem; margin-bottom:1rem; }89    .info-box    { border-left:4px solid #f4c430; border-radius:6px;90                   padding:12px 16px; font-size:0.88rem; }91</style>92""", unsafe_allow_html=True)93 94# ── Header ────────────────────────────────────────────────────────────────────95st.markdown('<p class="main-title">Image Filtering & Denoising Explorer</p>', unsafe_allow_html=True)96st.markdown('<p class="sub-title">Interactively compare spatial filters — understand how kernel size, '97            'noise type, and algorithm choice affect image quality.</p>', unsafe_allow_html=True)98 99# ── Sidebar ───────────────────────────────────────────────────────────────────100with st.sidebar:101    st.header("Controls")102 103    st.subheader("1. Image source")104    source = st.radio("Choose input", ["Built-in sample", "Upload your own"], horizontal=True)105 106    img_array = None107    if source == "Built-in sample":108        sample_name = st.selectbox(109            "Sample image",110            ["checkerboard_noisy.png", "gradient_saltpepper.png", "circles_mixed_noise.png"],111            format_func=lambda x: x.replace("_", " ").replace(".png", "").title()112        )113        img_array = load_sample_image(sample_name)114        st.caption("These samples have synthetic noise added so you can see filtering effects clearly.")115    else:116        uploaded = st.file_uploader("Upload image (JPG / PNG)", type=["jpg", "jpeg", "png"])117        if uploaded:118            pil = Image.open(uploaded).convert("RGB")119            if max(pil.size) > 800:120                pil.thumbnail((800, 800), Image.LANCZOS)121                st.caption("Image resized to 800px max for performance.")122            img_array = np.array(pil)123        else:124            st.info("Upload an image to get started, or switch to a built-in sample.")125 126    st.subheader("2. Filter")127    filter_name = st.selectbox(128        "Algorithm",129        ["Gaussian Blur", "Median Filter", "Bilateral Filter",130         "Box (Mean) Filter", "Non-local Means"],131    )132 133    st.subheader("3. Parameters")134    params = {}135    if filter_name in ("Gaussian Blur", "Box (Mean) Filter"):136        params["ksize"] = st.slider("Kernel size", 3, 31, 7, step=2,137                                    help="Larger = stronger smoothing. Must be odd.")138        if filter_name == "Gaussian Blur":139            params["sigma"] = st.slider("Sigma", 0.5, 10.0, 1.5, step=0.5,140                                        help="Spread of the Gaussian. Larger = more blur.")141    elif filter_name == "Median Filter":142        params["ksize"] = st.slider("Kernel size", 3, 21, 5, step=2,143                                    help="Larger removes bigger noise clusters but loses fine detail.")144    elif filter_name == "Bilateral Filter":145        params["d"]           = st.slider("Diameter (d)", 3, 25, 9, step=2)146        params["sigma_color"] = st.slider("Sigma color", 10, 200, 75, step=5)147        params["sigma_space"] = st.slider("Sigma space", 10, 200, 75, step=5)148    elif filter_name == "Non-local Means":149        params["h"]             = st.slider("Filter strength (h)", 3, 30, 10)150        params["template_size"] = st.slider("Template patch size", 3, 11, 7, step=2)151        params["search_size"]   = st.slider("Search window size", 11, 35, 21, step=2)152 153    st.subheader("4. Add extra noise (optional)")154    add_noise = st.checkbox("Add noise to input", value=False)155    noise_type, noise_level = None, 0156    if add_noise:157        noise_type  = st.selectbox("Noise type", ["Gaussian", "Salt & Pepper", "Speckle"])158        noise_level = st.slider("Noise intensity", 5, 80, 25)159 160# ── Guard ─────────────────────────────────────────────────────────────────────161if img_array is None:162    st.markdown('<div class="info-box">Choose a built-in sample image or upload your own using the sidebar controls.</div>',163                unsafe_allow_html=True)164    st.stop()165 166# ── Compute everything once (all cached) ──────────────────────────────────────167base_bytes = arr_to_bytes(img_array)168 169if add_noise and noise_type:170    display_input = cached_add_noise(base_bytes, noise_type, noise_level)171else:172    display_input = img_array.copy()173 174input_bytes = arr_to_bytes(display_input)175params_json = json.dumps(params, sort_keys=True)176 177with st.spinner("Applying Non-local Means — this may take a few seconds..." if filter_name == "Non-local Means" else ""):178    try:179        filtered = cached_apply_filter(input_bytes, filter_name, params_json)180    except Exception as e:181        st.error(f"Filter error: {e}")182        st.stop()183 184filtered_bytes = arr_to_bytes(filtered)185metrics        = cached_metrics(input_bytes, filtered_bytes)186heatmap        = cached_heatmap(input_bytes, filtered_bytes)187histogram_png  = cached_histogram_image(input_bytes, filtered_bytes)188 189diff     = np.abs(display_input.astype(np.int32) - filtered.astype(np.int32))190diff_vis = np.clip(diff * 3, 0, 255).astype(np.uint8)191 192# ── Render results in a fragment so only this section reruns ──────────────────193@st.fragment194def render_results():195    tab1, tab2, tab3 = st.tabs(["Comparison", "Diagnostics", "Theory"])196 197    # ── Tab 1: Comparison ─────────────────────────────────────────────────────198    with tab1:199        st.markdown(f'<div class="filter-desc"><b>{filter_name}</b>: {FILTER_DESCRIPTIONS[filter_name]}</div>',200                    unsafe_allow_html=True)201        col1, col2 = st.columns(2)202        with col1:203            st.image(display_input, caption="Input image", use_container_width=True)204        with col2:205            st.image(filtered, caption=f"After {filter_name}", use_container_width=True)206        with st.expander("Show difference image (amplified x3)"):207            st.image(diff_vis, caption="Removed detail / noise (amplified)", use_container_width=True)208            st.caption("Bright areas = pixels that changed the most.")209 210    # ── Tab 2: Diagnostics ────────────────────────────────────────────────────211    with tab2:212        m1, m2, m3, m4 = st.columns(4)213        with m1:214            st.markdown(f"""<div class="metric-card">215                <div class="metric-label">PSNR</div>216                <div class="metric-value">{metrics['psnr']:.1f} dB</div>217                <div style="font-size:.75rem;color:#aaa">Higher = less signal lost</div>218            </div>""", unsafe_allow_html=True)219        with m2:220            st.markdown(f"""<div class="metric-card">221                <div class="metric-label">SSIM</div>222                <div class="metric-value">{metrics['ssim']:.3f}</div>223                <div style="font-size:.75rem;color:#aaa">1.0 = identical structure</div>224            </div>""", unsafe_allow_html=True)225        with m3:226            st.markdown(f"""<div class="metric-card">227                <div class="metric-label">Mean change</div>228                <div class="metric-value">{metrics['mean_diff']:.2f}</div>229                <div style="font-size:.75rem;color:#aaa">Avg pixel change (0-255)</div>230            </div>""", unsafe_allow_html=True)231        with m4:232            st.markdown(f"""<div class="metric-card">233                <div class="metric-label">Noise reduction</div>234                <div class="metric-value">{metrics['noise_reduction']:.1f}%</div>235                <div style="font-size:.75rem;color:#aaa">Estimated noise removed</div>236            </div>""", unsafe_allow_html=True)237 238        st.divider()239 240        st.image(histogram_png,241                 caption="Pixel intensity histograms (left: input, right: filtered)",242                 use_container_width=True)243        st.caption("Histograms that are more concentrated after filtering indicate noise reduction.")244 245        st.subheader("Local noise heatmap")246        col_hm1, col_hm2 = st.columns([1, 2])247        with col_hm1:248            st.image(heatmap, caption="Noise activity heatmap", use_container_width=True)249        with col_hm2:250            st.markdown("""251**How to read this:**252 253- **Hot (red/yellow)** areas had the most noise removed — the filter worked hardest here.254- **Cool (blue/black)** areas changed little — either already clean or the filter preserved structure.255 256Use this to check whether noise removal is uniform (ideal) or selective.257            """)258 259        st.divider()260        psnr = metrics['psnr']261        ssim = metrics['ssim']262        interp = []263        if psnr > 35:264            interp.append("**PSNR > 35 dB** — very low signal distortion; filter is working gently.")265        elif psnr > 25:266            interp.append("**PSNR 25-35 dB** — moderate distortion; some fine detail is being lost.")267        else:268            interp.append("**PSNR < 25 dB** — significant distortion; filter may be too aggressive.")269        if ssim > 0.90:270            interp.append("**SSIM > 0.9** — structural content well preserved.")271        elif ssim > 0.75:272            interp.append("**SSIM 0.75-0.9** — noticeable structural changes; check edges.")273        else:274            interp.append("**SSIM < 0.75** — heavy structural loss; try reducing filter strength.")275        st.markdown("**Interpretation:**\n\n" + "\n\n".join(interp))276 277    # ── Tab 3: Theory ─────────────────────────────────────────────────────────278    with tab3:279        st.subheader("How spatial filters work")280        st.markdown("""281Spatial image filters operate by replacing each pixel with a function of its **neighbourhood**.282The key trade-off in denoising is:283 284> **Smoothing removes noise, but also blurs edges. The goal is to smooth noise while preserving structure.**285 286---287### Filter comparison288 289| Filter | Kernel type | Best noise | Preserves edges? | Speed |290|---|---|---|---|---|291| **Box (Mean)** | Uniform average | Gaussian | Poor | Fast |292| **Gaussian Blur** | Weighted average (bell curve) | Gaussian | Partial | Fast |293| **Median** | Non-linear median | Salt & Pepper | Good | Medium |294| **Bilateral** | Gaussian x intensity-weight | Gaussian | Very good | Medium |295| **Non-local Means** | Patch similarity | Any | Excellent | Slow |296 297---298### Key concepts299 300**Kernel size** controls the neighbourhood radius. Larger kernels smooth more aggressively but risk blurring fine detail and edges.301 302**Gaussian sigma** controls the shape of the bell curve. Small sigma = sharp, localised filter; large sigma = wide, slow-decaying influence.303 304**Bilateral sigma_color** adds an intensity gate: pixels that differ too much in colour are ignored even if spatially close. This lets bilateral filters smooth flat regions while keeping sharp edges.305 306**Non-local Means** compares small image patches. Similar-looking patches anywhere in the search window contribute to the estimate — very powerful for textured areas.307 308---309### When each filter fails310 311- **Gaussian / Box**: smears salt-and-pepper noise into surrounding pixels instead of removing them.312- **Median**: poor at Gaussian noise unless kernel is large (then blurs edges).313- **Bilateral**: can fail on fine textures (misidentifies texture variation as an edge to preserve).314- **Non-local Means**: very slow; may over-smooth unique regions with no similar patches.315        """)316 317render_results()