import streamlit as st import torch import torch.nn as nn from torchvision import models from torchvision.models import EfficientNet_B0_Weights from PIL import Image import numpy as np import albumentations as A from albumentations.pytorch import ToTensorV2 import json from pathlib import Path import os # ── Page config ───────────────────────────────────────────────────────────── st.set_page_config( page_title="WildEye", page_icon="🦌", layout="wide", initial_sidebar_state="expanded", ) # ── Custom CSS ─────────────────────────────────────────────────────────────── st.markdown(""" """, unsafe_allow_html=True) # ── Constants ───────────────────────────────────────────────────────────────── IMAGENET_MEAN = [0.485, 0.456, 0.406] IMAGENET_STD = [0.229, 0.224, 0.225] IMAGE_SIZE = 224 CLASSES = ['butterfly', 'cat', 'chicken', 'cow', 'dog', 'elephant', 'horse', 'sheep', 'spider', 'squirrel'] CLASS_EMOJI = { 'butterfly': '🦋', 'cat': '🐱', 'chicken': '🐔', 'cow': '🐄', 'dog': '🐶', 'elephant': '🐘', 'horse': '🐎', 'sheep': '🐑', 'spider': '🕷️', 'squirrel': '🐿️', } # ── Model ───────────────────────────────────────────────────────────────────── @st.cache_resource def load_model(): """Load the domain-augmented model. Tries local path first, then HF Hub.""" num_classes = len(CLASSES) model = models.efficientnet_b0(weights=EfficientNet_B0_Weights.IMAGENET1K_V1) in_features = model.classifier[1].in_features model.classifier = nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes), ) # Try local weights first (for Colab / local dev) local_paths = [ Path('models/domain_aug_best.pth'), Path('src/models/domain_aug_best.pth'), ] for p in local_paths: if p.exists(): ckpt = torch.load(p, map_location='cpu') model.load_state_dict(ckpt['model_state_dict']) model.eval() return model, str(p) return model, 'pretrained_only' # ── Augmentation builder ────────────────────────────────────────────────────── def build_transform(night_ir, motion_blur, low_light, noise, occlusion): """Build a transform from slider values (all 0-1 floats).""" ops = [A.Resize(IMAGE_SIZE, IMAGE_SIZE)] if night_ir > 0: ops.append(A.ToGray(p=1.0)) ops.append(A.RandomBrightnessContrast( brightness_limit=(-night_ir * 0.5, -night_ir * 0.1), contrast_limit=(-0.1, 0.1), p=1.0 )) if motion_blur > 0: blur_limit = max(3, int(motion_blur * 20)) ops.append(A.MotionBlur(blur_limit=(blur_limit, blur_limit + 4), p=1.0)) if low_light > 0: ops.append(A.RandomBrightnessContrast( brightness_limit=(-low_light * 0.6, -low_light * 0.2), contrast_limit=(-low_light * 0.3, 0.0), p=1.0 )) if noise > 0: ops.append(A.GaussNoise(std_range=(noise * 0.05, noise * 0.15), p=1.0)) if occlusion > 0: n_holes = max(1, int(occlusion * 10)) hole_size = max(10, int(occlusion * 50)) ops.append(A.CoarseDropout( num_holes_range=(n_holes, n_holes + 2), hole_height_range=(hole_size, hole_size + 10), hole_width_range=(hole_size, hole_size + 10), p=1.0 )) ops += [A.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD), ToTensorV2()] return A.Compose(ops) def predict(model, img_np, transform): tensor = transform(image=img_np)['image'].unsqueeze(0) with torch.no_grad(): logits = model(tensor)[0] probs = torch.softmax(logits, dim=0).numpy() top_idx = int(probs.argmax()) return CLASSES[top_idx], probs def denormalize(tensor): img = tensor.squeeze(0).permute(1, 2, 0).numpy() return np.clip(img * np.array(IMAGENET_STD) + np.array(IMAGENET_MEAN), 0, 1) # ── UI ──────────────────────────────────────────────────────────────────────── # Hero st.markdown('
🦌 WildEye
', unsafe_allow_html=True) st.markdown( '
Robust Wildlife Classifier · Transfer Learning + Domain Augmentation
', unsafe_allow_html=True ) # Sidebar with st.sidebar: st.markdown("### 🔬 Robustness Lab") st.markdown( "
Simulate real-world camera-trap conditions. " "Watch how each model handles them.
", unsafe_allow_html=True ) night_ir = st.slider("🌙 Night / IR", 0.0, 1.0, 0.0, 0.05) motion_blur = st.slider("💨 Motion Blur", 0.0, 1.0, 0.0, 0.05) low_light = st.slider("🔅 Low Light", 0.0, 1.0, 0.0, 0.05) noise = st.slider("📡 Sensor Noise", 0.0, 1.0, 0.0, 0.05) occlusion = st.slider("🌿 Occlusion", 0.0, 1.0, 0.0, 0.05) if st.button("⚡ Worst-case scenario", use_container_width=True): st.session_state['preset'] = 'worst' st.rerun() if st.button("✨ Reset to clean", use_container_width=True): st.session_state['preset'] = 'clean' st.rerun() if st.session_state.get('preset') == 'worst': night_ir = motion_blur = low_light = noise = occlusion = 0.8 elif st.session_state.get('preset') == 'clean': night_ir = motion_blur = low_light = noise = occlusion = 0.0 st.divider() st.markdown("### 📊 Project Results") st.markdown("""
+19.3%
Robustness gain
worst-case conditions
-1.1%
Cost on clean data
""", unsafe_allow_html=True) st.divider() st.markdown( "
" "Model: EfficientNet-B0
" "Training: Animals-10 (8,695 imgs)
" "Augmentations: 6 domain-specific
" "Mini Hackathon #1 — WildEye
", unsafe_allow_html=True ) # Main content col_upload, col_results = st.columns([1, 1], gap="large") with col_upload: st.markdown("#### Upload an image") uploaded = st.file_uploader( "Camera trap image", type=["jpg", "jpeg", "png", "webp"], label_visibility="collapsed" ) if not uploaded: st.markdown( "
📷 Drop an animal photo here
" "Supports: jpg · png · webp
", unsafe_allow_html=True ) else: img_pil = Image.open(uploaded).convert("RGB") img_np = np.array(img_pil) tf = build_transform(night_ir, motion_blur, low_light, noise, occlusion) import io from PIL import Image as PILImage aug_tensor = tf(image=img_np)['image'] aug_display = (denormalize(aug_tensor.unsqueeze(0)) * 255).astype(np.uint8) any_aug = any([night_ir, motion_blur, low_light, noise, occlusion]) if any_aug: c1, c2 = st.columns(2) c1.image(img_pil, caption="Original", use_container_width=True) c2.image(aug_display, caption="As model sees it", use_container_width=True) else: st.image(img_pil, caption="Original (no perturbations)", use_container_width=True) # Active perturbation tags active = [] if night_ir > 0: active.append(f"🌙 Night IR ({night_ir:.0%})") if motion_blur > 0: active.append(f"💨 Blur ({motion_blur:.0%})") if low_light > 0: active.append(f"🔅 Low Light ({low_light:.0%})") if noise > 0: active.append(f"📡 Noise ({noise:.0%})") if occlusion > 0: active.append(f"🌿 Occlusion ({occlusion:.0%})") if active: tags_html = " ".join(f"{t}" for t in active) st.markdown(tags_html, unsafe_allow_html=True) with col_results: if uploaded: st.markdown("#### Prediction") model, source = load_model() pred_class, probs = predict(model, img_np, tf) conf = probs.max() emoji = CLASS_EMOJI.get(pred_class, "🐾") # Main prediction card color = "#3fb950" if conf > 0.7 else "#d29922" if conf > 0.4 else "#f85149" st.markdown(f"""
{emoji}
{pred_class.upper()}
Confidence: {conf:.1%}
""", unsafe_allow_html=True) # Top-5 bar chart st.markdown("
**Top predictions**", unsafe_allow_html=True) top5_idx = probs.argsort()[::-1][:5] top5_cls = [CLASSES[i] for i in top5_idx] top5_prob = [probs[i] for i in top5_idx] import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(5, 2.8)) fig.patch.set_facecolor('#161b22') ax.set_facecolor('#161b22') colors_bar = ['#3fb950' if c == pred_class else '#30363d' for c in top5_cls] bars = ax.barh( [f"{CLASS_EMOJI.get(c,'🐾')} {c}" for c in top5_cls[::-1]], top5_prob[::-1], color=colors_bar[::-1], edgecolor='none', height=0.6 ) for bar, val in zip(bars, top5_prob[::-1]): ax.text(min(val + 0.01, 0.95), bar.get_y() + bar.get_height()/2, f'{val:.1%}', va='center', color='#e6edf3', fontsize=8, fontfamily='monospace') ax.set_xlim(0, 1.05) ax.tick_params(colors='#8b949e', labelsize=8) ax.spines[['top','right','bottom']].set_visible(False) ax.spines['left'].set_color('#30363d') ax.xaxis.set_visible(False) plt.tight_layout(pad=0.5) st.pyplot(fig, use_container_width=True) plt.close() # Robustness warning if any_aug: severity = np.mean([night_ir, motion_blur, low_light, noise, occlusion]) if conf < 0.5: st.markdown( "
⚠️ Low confidence under these conditions — " "this is exactly what domain augmentation trains against.
", unsafe_allow_html=True ) elif conf > 0.8: st.markdown( "
✓ High confidence maintained despite perturbations — " "domain augmentation working as intended.
", unsafe_allow_html=True ) else: st.markdown( "
" "Upload an image to see predictions.

" "Use the sidebar sliders to simulate
real camera-trap conditions.
", unsafe_allow_html=True ) # Footer st.divider() st.markdown( "
WildEye · Mini Hackathon #1 · " "EfficientNet-B0 + Domain Augmentation · " "Robustness gain: +19.3% under worst-case conditions
", unsafe_allow_html=True )