CoolFace
Apppublic

LEGENDFTW/image-filtering-explorer

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
app.py295 linesDownload Raw Back to src
1import sys2import os3sys.path.insert(0, os.path.dirname(__file__))4import streamlit as st5import numpy as np6from PIL import Image7import io8import os9 10from processing import apply_filter, FILTER_DESCRIPTIONS11from metrics import compute_metrics, compute_noise_profile12from utils import load_sample_image, image_to_bytes, overlay_noise_heatmap13 14# ── Page config ──────────────────────────────────────────────────────────────15st.set_page_config(16    page_title="Image Filtering & Denoising Explorer",17    page_icon="🔍",18    layout="wide",19)20 21# ── Custom CSS ────────────────────────────────────────────────────────────────22st.markdown("""23<style>24    .main-title  { font-size:2.2rem; font-weight:700; color:#1a1a2e; margin-bottom:0.2rem; }25    .sub-title   { font-size:1rem; color:#555; margin-bottom:1.5rem; }26    .metric-card { background:#f0f4ff; border-radius:10px; padding:14px 18px; margin:6px 0; }27    .metric-label{ font-size:0.78rem; color:#666; font-weight:600; letter-spacing:.05em; text-transform:uppercase; }28    .metric-value{ font-size:1.6rem; font-weight:700; color:#2c3e7a; }29    .info-box    { background:#fffbe6; border-left:4px solid #f4c430;30                   border-radius:6px; padding:12px 16px; font-size:0.88rem; color:#444; }31    .filter-desc { background:#eef6ff; border-radius:8px; padding:12px 16px;32                   font-size:0.9rem; color:#333; margin-bottom:1rem; }33    section[data-testid="stSidebar"] { background:#f7f9ff; }34</style>35""", unsafe_allow_html=True)36 37# ── Header ────────────────────────────────────────────────────────────────────38st.markdown('<p class="main-title">🔍 Image Filtering & Denoising Explorer</p>', unsafe_allow_html=True)39st.markdown('<p class="sub-title">Interactively compare spatial filters — understand how kernel size, '40            'noise type, and algorithm choice affect image quality.</p>', unsafe_allow_html=True)41 42# ── Sidebar ───────────────────────────────────────────────────────────────────43with st.sidebar:44    st.header("⚙️ Controls")45 46    # ── Image source ──────────────────────────────────────────────────────────47    st.subheader("1. Image source")48    source = st.radio("Choose input", ["Built-in sample", "Upload your own"], horizontal=True)49 50    img_array = None51    if source == "Built-in sample":52        sample_name = st.selectbox(53            "Sample image",54            ["checkerboard_noisy.png", "gradient_saltpepper.png", "circles_mixed_noise.png"],55            format_func=lambda x: x.replace("_", " ").replace(".png", "").title()56        )57        img_array = load_sample_image(sample_name)58        st.caption("📌 These samples have synthetic noise added so you can see filtering effects clearly.")59    else:60        uploaded = st.file_uploader("Upload image (JPG / PNG)", type=["jpg", "jpeg", "png"])61        if uploaded:62            pil = Image.open(uploaded).convert("RGB")63            # Resize large images for performance64            if max(pil.size) > 800:65                pil.thumbnail((800, 800), Image.LANCZOS)66                st.caption("⚡ Image resized to ≤800px for performance.")67            img_array = np.array(pil)68        else:69            st.info("Upload an image to get started, or switch to a built-in sample.")70 71    # ── Filter selection ──────────────────────────────────────────────────────72    st.subheader("2. Filter")73    filter_name = st.selectbox(74        "Algorithm",75        ["Gaussian Blur", "Median Filter", "Bilateral Filter",76         "Box (Mean) Filter", "Non-local Means"],77    )78 79    # ── Filter parameters ─────────────────────────────────────────────────────80    st.subheader("3. Parameters")81 82    params = {}83    if filter_name in ("Gaussian Blur", "Box (Mean) Filter"):84        params["ksize"] = st.slider("Kernel size", 3, 31, 7, step=2,85                                    help="Larger = stronger smoothing. Must be odd.")86        if filter_name == "Gaussian Blur":87            params["sigma"] = st.slider("σ (sigma)", 0.5, 10.0, 1.5, step=0.5,88                                        help="Spread of the Gaussian. Larger = more blur.")89 90    elif filter_name == "Median Filter":91        params["ksize"] = st.slider("Kernel size", 3, 21, 5, step=2,92                                    help="Larger removes bigger noise clusters but loses fine detail.")93 94    elif filter_name == "Bilateral Filter":95        params["d"]        = st.slider("Diameter (d)", 3, 25, 9, step=2,96                                       help="Pixel neighbourhood diameter.")97        params["sigma_color"] = st.slider("σ color", 10, 200, 75, step=5,98                                          help="How much colour difference is tolerated. Higher = more colour averaging.")99        params["sigma_space"] = st.slider("σ space", 10, 200, 75, step=5,100                                          help="Spatial extent of the filter. Higher = farther pixels influence each other.")101 102    elif filter_name == "Non-local Means":103        params["h"]             = st.slider("Filter strength (h)", 3, 30, 10,104                                            help="Higher = stronger denoising but risks blurring detail.")105        params["template_size"] = st.slider("Template patch size", 3, 11, 7, step=2,106                                            help="Size of the patch used for comparison.")107        params["search_size"]   = st.slider("Search window size", 11, 35, 21, step=2,108                                            help="Area searched for similar patches. Larger = slower but better.")109 110    # ── Add synthetic noise option ─────────────────────────────────────────────111    st.subheader("4. Add extra noise (optional)")112    add_noise = st.checkbox("Add noise to input", value=False)113    noise_type, noise_level = None, 0114    if add_noise:115        noise_type  = st.selectbox("Noise type", ["Gaussian", "Salt & Pepper", "Speckle"])116        noise_level = st.slider("Noise intensity", 5, 80, 25)117 118# ── Main panel ────────────────────────────────────────────────────────────────119if img_array is None:120    st.markdown("""121    <div class="info-box">122    👈  Choose a built-in sample image or upload your own using the sidebar controls.123    </div>124    """, unsafe_allow_html=True)125    st.stop()126 127# Apply synthetic noise if requested128from processing import add_synthetic_noise129display_input = img_array.copy()130if add_noise and noise_type:131    display_input = add_synthetic_noise(display_input, noise_type, noise_level)132 133# Apply filter134try:135    filtered = apply_filter(display_input, filter_name, params)136except Exception as e:137    st.error(f"Filter error: {e}")138    st.stop()139 140# ── Tab layout ────────────────────────────────────────────────────────────────141tab1, tab2, tab3 = st.tabs(["📷 Comparison", "📊 Diagnostics", "📚 Theory"])142 143# ────────────────────── TAB 1: Side-by-side ──────────────────────────────────144with tab1:145    st.markdown(f'<div class="filter-desc">🔬 <b>{filter_name}</b>: {FILTER_DESCRIPTIONS[filter_name]}</div>',146                unsafe_allow_html=True)147 148    col1, col2 = st.columns(2)149    with col1:150        st.image(display_input, caption="Input image", use_container_width=True)151    with col2:152        st.image(filtered, caption=f"After {filter_name}", use_container_width=True)153 154    # Difference image155    diff = np.abs(display_input.astype(np.int32) - filtered.astype(np.int32))156    diff_vis = np.clip(diff * 3, 0, 255).astype(np.uint8)   # amplify for visibility157    with st.expander("🔎 Show difference image (amplified ×3)"):158        st.image(diff_vis, caption="Removed detail / noise (amplified)", use_container_width=True)159        st.caption("Bright areas = pixels that changed the most. This reveals where the filter is working hardest.")160 161# ────────────────────── TAB 2: Diagnostics ───────────────────────────────────162with tab2:163    metrics = compute_metrics(display_input, filtered)164 165    # Metric cards166    m1, m2, m3, m4 = st.columns(4)167    with m1:168        st.markdown(f"""<div class="metric-card">169            <div class="metric-label">PSNR</div>170            <div class="metric-value">{metrics['psnr']:.1f} dB</div>171            <div style="font-size:.75rem;color:#888">Higher = less signal lost</div>172        </div>""", unsafe_allow_html=True)173    with m2:174        st.markdown(f"""<div class="metric-card">175            <div class="metric-label">SSIM</div>176            <div class="metric-value">{metrics['ssim']:.3f}</div>177            <div style="font-size:.75rem;color:#888">1.0 = identical structure</div>178        </div>""", unsafe_allow_html=True)179    with m3:180        st.markdown(f"""<div class="metric-card">181            <div class="metric-label">Mean Δ</div>182            <div class="metric-value">{metrics['mean_diff']:.2f}</div>183            <div style="font-size:.75rem;color:#888">Avg pixel change (0-255)</div>184        </div>""", unsafe_allow_html=True)185    with m4:186        st.markdown(f"""<div class="metric-card">187            <div class="metric-label">Noise σ reduction</div>188            <div class="metric-value">{metrics['noise_reduction']:.1f}%</div>189            <div style="font-size:.75rem;color:#888">Estimated noise removed</div>190        </div>""", unsafe_allow_html=True)191 192    st.divider()193 194    # Histogram comparison195    col_h1, col_h2 = st.columns(2)196    noise_profile_in  = compute_noise_profile(display_input)197    noise_profile_out = compute_noise_profile(filtered)198 199    import plotly.graph_objects as go200    from plotly.subplots import make_subplots201 202    # Pixel intensity histograms203    fig_hist = make_subplots(rows=1, cols=2,204                             subplot_titles=("Input — pixel intensity", "Filtered — pixel intensity"))205    colors_rgb = ["#e74c3c", "#2ecc71", "#3498db"]206    channel_names = ["Red", "Green", "Blue"]207    for c, (col, name) in enumerate(zip(colors_rgb, channel_names)):208        for row, arr in [(1, display_input), (2, filtered)]:  # plotly col209            hist, edges = np.histogram(arr[:, :, c].ravel(), bins=64, range=(0, 255))210            fig_hist.add_trace(211                go.Bar(x=edges[:-1], y=hist, name=name,212                       marker_color=col, opacity=0.6,213                       showlegend=(row == 1)),214                row=1, col=row215            )216    fig_hist.update_layout(height=300, margin=dict(t=40, b=10), barmode="overlay",217                           legend_title="Channel")218    st.plotly_chart(fig_hist, use_container_width=True)219    st.caption("Histograms that are more concentrated / peaked after filtering indicate noise reduction. "220               "Very compressed histograms suggest over-smoothing.")221 222    # Noise heatmap223    st.subheader("Local noise heatmap")224    heatmap_img = overlay_noise_heatmap(display_input, filtered)225    col_hm1, col_hm2 = st.columns([1, 2])226    with col_hm1:227        st.image(heatmap_img, caption="Noise activity heatmap", use_container_width=True)228    with col_hm2:229        st.markdown("""230**How to read this:**231- 🔴 **Hot (red/yellow)** areas had the most noise removed — the filter worked hardest here.232- 🔵 **Cool (blue/black)** areas changed little — either already clean or the filter preserved structure.233 234Use this to check whether noise removal is **uniform** (ideal) or **selective** (suggests the filter is confused by edges or textures).235        """)236 237    # Metric interpretation238    st.divider()239    psnr = metrics['psnr']240    ssim = metrics['ssim']241    interp = []242    if psnr > 35:243        interp.append("✅ **PSNR > 35 dB** — very low signal distortion; filter is working gently.")244    elif psnr > 25:245        interp.append("⚠️ **PSNR 25–35 dB** — moderate distortion; some fine detail is being lost.")246    else:247        interp.append("🔴 **PSNR < 25 dB** — significant distortion; filter may be too aggressive.")248    if ssim > 0.90:249        interp.append("✅ **SSIM > 0.9** — structural content well preserved.")250    elif ssim > 0.75:251        interp.append("⚠️ **SSIM 0.75–0.9** — noticeable structural changes; check edges.")252    else:253        interp.append("🔴 **SSIM < 0.75** — heavy structural loss; try reducing filter strength.")254 255    st.markdown("**Interpretation:**\n\n" + "\n\n".join(interp))256 257# ────────────────────── TAB 3: Theory ────────────────────────────────────────258with tab3:259    st.subheader("📚 How spatial filters work")260    st.markdown("""261Spatial image filters operate by replacing each pixel with a function of its **neighbourhood**.262The key trade-off in denoising is:263 264> **Smoothing removes noise, but also blurs edges. The goal is to smooth noise while preserving structure.**265 266---267### Filter comparison268| Filter | Kernel type | Best noise | Preserves edges? | Speed |269|---|---|---|---|---|270| **Box (Mean)** | Uniform average | Gaussian | ❌ Poor | ⚡ Fast |271| **Gaussian Blur** | Weighted average (bell curve) | Gaussian | ⚠️ Partial | ⚡ Fast |272| **Median** | Non-linear median | Salt & Pepper | ✅ Good | 🐢 Medium |273| **Bilateral** | Gaussian × intensity-weight | Gaussian | ✅ Very good | 🐢 Medium |274| **Non-local Means (NLM)** | Patch similarity | Any | ✅ Excellent | 🐌 Slow |275 276---277### Key concepts278 279**Kernel size** controls the neighbourhood radius. Larger kernels smooth more aggressively but risk blurring fine detail and edges.280 281**Gaussian σ** controls the *shape* of the bell curve. Small σ = sharp, localised filter; large σ = wide, slow-decaying influence.282 283**Bilateral σ_color** adds an intensity gate: pixels that differ too much in colour are ignored even if they're close spatially. This is what lets bilateral filters smooth flat regions while keeping sharp edges.284 285**Non-local Means** takes this further: instead of comparing single pixels, it compares *patches* (small image regions). Similar-looking patches anywhere in the search window contribute to the estimate — making it very powerful for textured areas.286 287---288### When each filter fails289 290- **Gaussian / Box**: smears salt-and-pepper noise (single outlier pixels) into surrounding pixels instead of removing them.291- **Median**: poor at Gaussian noise unless kernel is large (then blurs edges).292- **Bilateral**: can fail on fine textures (misidentifies texture variation as an "edge" to preserve).293- **NLM**: very slow; may over-smooth unique regions with no similar patches.294    """)295