Spaces:
Runtime error
Runtime error
| """ | |
| Interactive Edge Detection Demo | |
| An educational tool for graduate-level image analysis courses | |
| This demo explores computational approaches to edge detection, focusing on: | |
| - Classical gradient-based filters (Sobel, Prewitt, Roberts, Laplacian) | |
| - Advanced Canny edge detection | |
| - Comparative analysis of different methods | |
| """ | |
| import io | |
| import numpy as np | |
| import cv2 as cv | |
| import streamlit as st | |
| from PIL import Image | |
| from huggingface_hub import hf_hub_download | |
| def load_sample_image(): | |
| """Load a sample image for the demo. Falls back to generated image if download fails.""" | |
| try: | |
| # Try to download from HuggingFace | |
| image_path = hf_hub_download( | |
| repo_id="amithjkamath/exampleimages", | |
| filename="sample-image.jpg", | |
| repo_type="dataset", | |
| ) | |
| img = cv.imread(image_path) | |
| img = cv.cvtColor(img, cv.COLOR_BGR2RGB) | |
| except Exception: | |
| # Generate a sample image with edges | |
| img = generate_sample_image() | |
| return img | |
| def generate_sample_image(size=512): | |
| """Generate a sample image with interesting edge features for demonstration.""" | |
| img = np.zeros((size, size, 3), dtype=np.uint8) | |
| # Create a gradient background (soft edges) | |
| for i in range(size): | |
| img[i, :, :] = int(100 * i / size) | |
| # Add geometric shapes with different edge characteristics | |
| # Circle with smooth edges | |
| cv.circle(img, (size//4, size//4), size//8, (255, 255, 255), -1) | |
| # Rectangle with sharp corners | |
| cv.rectangle(img, (size//2, size//2), (3*size//4, 3*size//4), (200, 50, 50), -1) | |
| # Triangle (sharp edges at different orientations) | |
| pts = np.array([[3*size//4, size//4], [size-50, size//4 + size//8], | |
| [3*size//4 + size//16, 50]], np.int32) | |
| cv.fillPoly(img, [pts], (50, 200, 200)) | |
| # Add some thin lines (challenging for edge detection) | |
| cv.line(img, (50, 3*size//4), (size//3, 3*size//4), (255, 255, 255), 2) | |
| # Add text (multiple edge orientations) | |
| cv.putText(img, "EDGES", (size//8, size-80), | |
| cv.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 3) | |
| return img | |
| def apply_sobel_filter(img, ksize=3): | |
| """ | |
| Apply Sobel edge detection filter. | |
| Sobel computes the gradient using separable kernels: | |
| - Gx: horizontal gradient (vertical edges) | |
| - Gy: vertical gradient (horizontal edges) | |
| - Magnitude: sqrt(Gx² + Gy²) | |
| Args: | |
| img: Input grayscale image | |
| ksize: Kernel size (1, 3, 5, 7) | |
| Returns: | |
| edges: Edge magnitude image | |
| gx, gy: Gradient components | |
| """ | |
| # Compute gradients in both directions | |
| gx = cv.Sobel(img, cv.CV_64F, 1, 0, ksize=ksize) | |
| gy = cv.Sobel(img, cv.CV_64F, 0, 1, ksize=ksize) | |
| # Compute magnitude | |
| magnitude = np.sqrt(gx**2 + gy**2) | |
| # Normalize to 0-255 range | |
| magnitude = np.uint8(255 * magnitude / np.max(magnitude)) if np.max(magnitude) > 0 else magnitude.astype(np.uint8) | |
| return magnitude, gx, gy | |
| def apply_prewitt_filter(img): | |
| """ | |
| Apply Prewitt edge detection filter. | |
| Prewitt is similar to Sobel but uses simpler kernels: | |
| Gx = [[-1, 0, 1], Gy = [[-1, -1, -1], | |
| [-1, 0, 1], [ 0, 0, 0], | |
| [-1, 0, 1]] [ 1, 1, 1]] | |
| Args: | |
| img: Input grayscale image | |
| Returns: | |
| edges: Edge magnitude image | |
| gx, gy: Gradient components | |
| """ | |
| # Define Prewitt kernels | |
| kernel_x = np.array([[-1, 0, 1], | |
| [-1, 0, 1], | |
| [-1, 0, 1]], dtype=np.float32) | |
| kernel_y = np.array([[-1, -1, -1], | |
| [ 0, 0, 0], | |
| [ 1, 1, 1]], dtype=np.float32) | |
| # Apply convolution | |
| gx = cv.filter2D(img, cv.CV_64F, kernel_x) | |
| gy = cv.filter2D(img, cv.CV_64F, kernel_y) | |
| # Compute magnitude | |
| magnitude = np.sqrt(gx**2 + gy**2) | |
| # Normalize to 0-255 range | |
| magnitude = np.uint8(255 * magnitude / np.max(magnitude)) if np.max(magnitude) > 0 else magnitude.astype(np.uint8) | |
| return magnitude, gx, gy | |
| def apply_roberts_filter(img): | |
| """ | |
| Apply Roberts Cross edge detection filter. | |
| Roberts uses 2x2 diagonal gradient kernels: | |
| Gx = [[ 1, 0], Gy = [[ 0, 1], | |
| [ 0, -1]] [-1, 0]] | |
| This is the simplest gradient operator, sensitive to diagonal edges. | |
| Args: | |
| img: Input grayscale image | |
| Returns: | |
| edges: Edge magnitude image | |
| gx, gy: Gradient components | |
| """ | |
| # Define Roberts Cross kernels | |
| kernel_x = np.array([[ 1, 0], | |
| [ 0, -1]], dtype=np.float32) | |
| kernel_y = np.array([[ 0, 1], | |
| [-1, 0]], dtype=np.float32) | |
| # Apply convolution | |
| gx = cv.filter2D(img, cv.CV_64F, kernel_x) | |
| gy = cv.filter2D(img, cv.CV_64F, kernel_y) | |
| # Compute magnitude | |
| magnitude = np.sqrt(gx**2 + gy**2) | |
| # Normalize to 0-255 range | |
| magnitude = np.uint8(255 * magnitude / np.max(magnitude)) if np.max(magnitude) > 0 else magnitude.astype(np.uint8) | |
| return magnitude, gx, gy | |
| def apply_laplacian_filter(img, ksize=3): | |
| """ | |
| Apply Laplacian edge detection filter. | |
| The Laplacian is a second-order derivative operator that detects | |
| regions of rapid intensity change. Unlike gradient-based methods, | |
| it's isotropic (rotation-invariant) but more sensitive to noise. | |
| Common kernel (ksize=3): | |
| [[ 0, 1, 0], | |
| [ 1, -4, 1], | |
| [ 0, 1, 0]] | |
| Args: | |
| img: Input grayscale image | |
| ksize: Kernel size (1, 3, 5, 7) | |
| Returns: | |
| edges: Edge response image | |
| """ | |
| # Apply Laplacian | |
| laplacian = cv.Laplacian(img, cv.CV_64F, ksize=ksize) | |
| # Take absolute value (edges can be positive or negative) | |
| laplacian = np.abs(laplacian) | |
| # Normalize to 0-255 range | |
| laplacian = np.uint8(255 * laplacian / np.max(laplacian)) if np.max(laplacian) > 0 else laplacian.astype(np.uint8) | |
| return laplacian | |
| def apply_canny_edge_detector(img, low_threshold=50, high_threshold=150, aperture_size=3, use_l2=True): | |
| """ | |
| Apply Canny edge detection algorithm. | |
| The Canny edge detector is a multi-stage algorithm: | |
| 1. Noise reduction (Gaussian blur) | |
| 2. Gradient calculation (Sobel) | |
| 3. Non-maximum suppression (thin edges) | |
| 4. Double thresholding (strong and weak edges) | |
| 5. Edge tracking by hysteresis (connect weak edges to strong ones) | |
| Args: | |
| img: Input grayscale image | |
| low_threshold: Lower threshold for hysteresis | |
| high_threshold: Upper threshold for hysteresis | |
| aperture_size: Sobel kernel size (3, 5, 7) | |
| use_l2: Use L2 norm for gradient magnitude (more accurate but slower) | |
| Returns: | |
| edges: Binary edge map | |
| """ | |
| edges = cv.Canny(img, low_threshold, high_threshold, | |
| apertureSize=aperture_size, L2gradient=use_l2) | |
| return edges | |
| def compute_gradient_direction(gx, gy): | |
| """ | |
| Compute gradient direction from gradient components. | |
| Args: | |
| gx, gy: Gradient components in x and y directions | |
| Returns: | |
| direction: Gradient direction in degrees (0-360) | |
| """ | |
| direction = np.arctan2(gy, gx) * 180 / np.pi | |
| direction = (direction + 360) % 360 # Ensure positive angles | |
| return direction | |
| def create_gradient_visualization(gx, gy): | |
| """ | |
| Create a color-coded visualization of gradient direction. | |
| Uses HSV color space where: | |
| - Hue represents direction | |
| - Saturation is constant | |
| - Value represents magnitude | |
| Args: | |
| gx, gy: Gradient components | |
| Returns: | |
| RGB image showing gradient direction and magnitude | |
| """ | |
| # Compute magnitude and direction | |
| magnitude = np.sqrt(gx**2 + gy**2) | |
| direction = np.arctan2(gy, gx) | |
| # Normalize magnitude | |
| magnitude_norm = magnitude / np.max(magnitude) if np.max(magnitude) > 0 else magnitude | |
| # Create HSV image | |
| hsv = np.zeros((*gx.shape, 3), dtype=np.uint8) | |
| hsv[..., 0] = ((direction + np.pi) / (2 * np.pi) * 180).astype(np.uint8) # Hue: 0-180 | |
| hsv[..., 1] = 255 # Saturation: full | |
| hsv[..., 2] = (magnitude_norm * 255).astype(np.uint8) # Value: magnitude | |
| # Convert to RGB | |
| rgb = cv.cvtColor(hsv, cv.COLOR_HSV2RGB) | |
| return rgb | |
| def main_loop(): | |
| """Main application loop.""" | |
| st.set_page_config(layout="wide", page_title="Edge Detection Demo") | |
| st.title("🔍 Interactive Edge Detection Demo") | |
| st.markdown(""" | |
| Welcome to an educational exploration of edge detection in digital image processing. | |
| This demo demonstrates how computational methods identify boundaries and discontinuities in images— | |
| a fundamental task in computer vision with applications from medical imaging to autonomous systems. | |
| """) | |
| # Load sample image | |
| sample_img = load_sample_image() | |
| # Sidebar: Image input | |
| st.sidebar.header("Image Input") | |
| uploaded_file = st.sidebar.file_uploader("Upload your own image (optional)", | |
| type=['png', 'jpg', 'jpeg']) | |
| if uploaded_file is not None: | |
| # Use uploaded image | |
| file_bytes = np.asarray(bytearray(uploaded_file.read()), dtype=np.uint8) | |
| img = cv.imdecode(file_bytes, cv.IMREAD_COLOR) | |
| img = cv.cvtColor(img, cv.COLOR_BGR2RGB) | |
| # Resize if too large | |
| max_size = 512 | |
| h, w = img.shape[:2] | |
| if max(h, w) > max_size: | |
| scale = max_size / max(h, w) | |
| new_w, new_h = int(w * scale), int(h * scale) | |
| img = cv.resize(img, (new_w, new_h), interpolation=cv.INTER_AREA) | |
| else: | |
| img = sample_img | |
| # Convert to grayscale for edge detection | |
| gray = cv.cvtColor(img, cv.COLOR_RGB2GRAY) | |
| # Sidebar: Pre-processing options | |
| st.sidebar.header("Pre-processing") | |
| apply_blur = st.sidebar.checkbox("Apply Gaussian Blur", value=False, | |
| help="Reduces noise before edge detection") | |
| if apply_blur: | |
| blur_ksize = st.sidebar.slider("Blur Kernel Size", | |
| min_value=3, max_value=15, value=5, step=2, | |
| help="Larger kernels = more smoothing") | |
| gray = cv.GaussianBlur(gray, (blur_ksize, blur_ksize), 0) | |
| # Sidebar: Select edge detection method | |
| st.sidebar.header("Edge Detection Method") | |
| method = st.sidebar.selectbox( | |
| "Select Method", | |
| ["Sobel", "Prewitt", "Roberts", "Laplacian", "Canny"], | |
| help="Choose which edge detection algorithm to apply" | |
| ) | |
| # Method-specific parameters | |
| st.sidebar.subheader("Method Parameters") | |
| if method == "Sobel": | |
| sobel_ksize = st.sidebar.select_slider( | |
| "Sobel Kernel Size", | |
| options=[1, 3, 5, 7], | |
| value=3, | |
| help="Larger kernels detect coarser edges" | |
| ) | |
| edges, gx, gy = apply_sobel_filter(gray, ksize=sobel_ksize) | |
| show_gradients = st.sidebar.checkbox("Show Gradient Components", value=False) | |
| show_direction = st.sidebar.checkbox("Show Gradient Direction", value=False) | |
| elif method == "Prewitt": | |
| edges, gx, gy = apply_prewitt_filter(gray) | |
| show_gradients = st.sidebar.checkbox("Show Gradient Components", value=False) | |
| show_direction = st.sidebar.checkbox("Show Gradient Direction", value=False) | |
| elif method == "Roberts": | |
| edges, gx, gy = apply_roberts_filter(gray) | |
| show_gradients = st.sidebar.checkbox("Show Gradient Components", value=False) | |
| show_direction = st.sidebar.checkbox("Show Gradient Direction", value=False) | |
| elif method == "Laplacian": | |
| laplacian_ksize = st.sidebar.select_slider( | |
| "Laplacian Kernel Size", | |
| options=[1, 3, 5, 7], | |
| value=3, | |
| help="Larger kernels = smoother response" | |
| ) | |
| edges = apply_laplacian_filter(gray, ksize=laplacian_ksize) | |
| show_gradients = False | |
| show_direction = False | |
| elif method == "Canny": | |
| st.sidebar.markdown("**Threshold Values**") | |
| low_threshold = st.sidebar.slider( | |
| "Low Threshold", | |
| min_value=0, max_value=255, value=50, | |
| help="Pixels below this are definitely not edges" | |
| ) | |
| high_threshold = st.sidebar.slider( | |
| "High Threshold", | |
| min_value=0, max_value=255, value=150, | |
| help="Pixels above this are definitely edges" | |
| ) | |
| aperture_size = st.sidebar.select_slider( | |
| "Sobel Kernel Size", | |
| options=[3, 5, 7], | |
| value=3, | |
| help="Kernel size for internal gradient computation" | |
| ) | |
| use_l2 = st.sidebar.checkbox("Use L2 Gradient", value=True, | |
| help="More accurate but slower magnitude calculation") | |
| edges = apply_canny_edge_detector(gray, low_threshold, high_threshold, | |
| aperture_size, use_l2) | |
| show_gradients = False | |
| show_direction = False | |
| # Display: Original and detected edges | |
| st.markdown("---") | |
| st.markdown("## Visual Results") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown("### Original Image") | |
| st.image(img, use_column_width=True, caption="Input image") | |
| st.caption(f"Size: {img.shape[1]}×{img.shape[0]} pixels") | |
| with col2: | |
| st.markdown(f"### {method} Edge Detection") | |
| if method == "Canny": | |
| # Canny produces binary edges, display with inverted colormap for visibility | |
| st.image(edges, use_column_width=True, caption=f"{method} edges", clamp=True) | |
| else: | |
| st.image(edges, use_column_width=True, caption=f"{method} edge magnitude", clamp=True) | |
| # Show edge statistics | |
| edge_pixels = np.sum(edges > 0) | |
| total_pixels = edges.shape[0] * edges.shape[1] | |
| edge_percentage = (edge_pixels / total_pixels) * 100 | |
| st.caption(f"Edge pixels: {edge_pixels:,} ({edge_percentage:.2f}%)") | |
| # Display gradient components if requested | |
| if show_gradients and method in ["Sobel", "Prewitt", "Roberts"]: | |
| st.markdown("---") | |
| st.markdown("### Gradient Components") | |
| st.markdown(""" | |
| The gradient has two components: | |
| - **Gx** (horizontal gradient): responds to vertical edges | |
| - **Gy** (vertical gradient): responds to horizontal edges | |
| """) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown("**Gx: Horizontal Gradient**") | |
| gx_display = np.abs(gx) | |
| gx_display = np.uint8(255 * gx_display / np.max(gx_display)) if np.max(gx_display) > 0 else gx_display.astype(np.uint8) | |
| st.image(gx_display, use_column_width=True, caption="Vertical edges", clamp=True) | |
| with col2: | |
| st.markdown("**Gy: Vertical Gradient**") | |
| gy_display = np.abs(gy) | |
| gy_display = np.uint8(255 * gy_display / np.max(gy_display)) if np.max(gy_display) > 0 else gy_display.astype(np.uint8) | |
| st.image(gy_display, use_column_width=True, caption="Horizontal edges", clamp=True) | |
| # Display gradient direction if requested | |
| if show_direction and method in ["Sobel", "Prewitt", "Roberts"]: | |
| st.markdown("---") | |
| st.markdown("### Gradient Direction Visualization") | |
| st.markdown(""" | |
| This color-coded visualization shows: | |
| - **Hue (color)**: Direction of the gradient (edge orientation) | |
| - **Brightness**: Magnitude of the gradient (edge strength) | |
| """) | |
| gradient_viz = create_gradient_visualization(gx, gy) | |
| st.image(gradient_viz, use_column_width=True, caption="Gradient direction and magnitude") | |
| # Add color legend | |
| st.markdown(""" | |
| **Color Legend:** | |
| - 🔴 Red: Rightward gradient | |
| - 🟡 Yellow: Upward-right gradient | |
| - 🟢 Green: Upward gradient | |
| - 🔵 Cyan: Upward-left gradient | |
| - 🟣 Blue: Leftward gradient | |
| - 🟣 Magenta: Downward-left gradient | |
| """) | |
| # Comparison with Canny | |
| if method != "Canny": | |
| st.sidebar.markdown("---") | |
| st.sidebar.header("Compare with Canny") | |
| show_comparison = st.sidebar.checkbox("Show Canny Comparison", value=False) | |
| if show_comparison: | |
| st.markdown("---") | |
| st.markdown(f"## Comparison: {method} vs. Canny") | |
| col1, col2 = st.columns(2) | |
| # Default Canny parameters for comparison | |
| canny_low = st.sidebar.slider("Canny Low Threshold (Comparison)", | |
| min_value=0, max_value=255, value=50) | |
| canny_high = st.sidebar.slider("Canny High Threshold (Comparison)", | |
| min_value=0, max_value=255, value=150) | |
| canny_edges = apply_canny_edge_detector(gray, canny_low, canny_high) | |
| with col1: | |
| st.markdown(f"### {method}") | |
| st.image(edges, use_column_width=True, clamp=True) | |
| edge_pixels_method = np.sum(edges > 0) | |
| st.caption(f"Edge pixels: {edge_pixels_method:,}") | |
| with col2: | |
| st.markdown("### Canny") | |
| st.image(canny_edges, use_column_width=True, clamp=True) | |
| edge_pixels_canny = np.sum(canny_edges > 0) | |
| st.caption(f"Edge pixels: {edge_pixels_canny:,}") | |
| # Educational content | |
| st.markdown("---") | |
| st.markdown("## 📚 Educational Insights") | |
| tab1, tab2, tab3, tab4 = st.tabs([ | |
| "What Are Edges?", | |
| "Gradient-Based Methods", | |
| "Canny Algorithm", | |
| "Practical Considerations" | |
| ]) | |
| with tab1: | |
| st.markdown(""" | |
| ### What Are Edges in Digital Images? | |
| **Definition:** | |
| Edges correspond to significant local changes in image intensity. They typically occur at: | |
| - **Object boundaries**: Where one object ends and another begins | |
| - **Surface orientation changes**: Corners, creases, and ridges | |
| - **Material property changes**: Reflectance, color, or texture discontinuities | |
| - **Illumination boundaries**: Shadows and highlights | |
| **Why Edge Detection Matters:** | |
| Edge detection is foundational to computer vision because edges: | |
| - Reduce data dimensionality while preserving structural information | |
| - Are relatively invariant to illumination changes | |
| - Enable higher-level tasks: object recognition, segmentation, tracking | |
| - Form the basis for feature extraction in many applications | |
| **The Computational Challenge:** | |
| Distinguishing true edges from noise requires balancing: | |
| - **Sensitivity**: Detecting all significant edges | |
| - **Specificity**: Avoiding false positives from noise | |
| - **Localization**: Accurately positioning detected edges | |
| **The "Aha!" Moment:** | |
| Edges are fundamentally about **derivatives**. Just as derivatives in calculus identify rates of change, | |
| image gradients identify spatial rates of intensity change. This mathematical insight transforms | |
| a perceptual concept (edges) into a computational operation (differentiation). | |
| """) | |
| with tab2: | |
| st.markdown(""" | |
| ### Gradient-Based Edge Detection Methods | |
| **Fundamental Principle:** | |
| All gradient-based methods approximate the first derivative of image intensity. | |
| The gradient is a vector pointing in the direction of greatest intensity increase: | |
| $$\\nabla I = \\begin{bmatrix} \\frac{\\partial I}{\\partial x} \\\\ \\frac{\\partial I}{\\partial y} \\end{bmatrix} = \\begin{bmatrix} G_x \\\\ G_y \\end{bmatrix}$$ | |
| The **edge magnitude** is: $|\\nabla I| = \\sqrt{G_x^2 + G_y^2}$ | |
| The **edge direction** is: $\\theta = \\arctan\\left(\\frac{G_y}{G_x}\\right)$ | |
| --- | |
| #### Roberts Cross Operator (1963) | |
| **Kernels (2×2):** | |
| $$G_x = \\begin{bmatrix} +1 & 0 \\\\ 0 & -1 \\end{bmatrix}, \\quad | |
| G_y = \\begin{bmatrix} 0 & +1 \\\\ -1 & 0 \\end{bmatrix}$$ | |
| **Characteristics:** | |
| - Simplest gradient operator, computationally efficient | |
| - Computes diagonal differences (45° rotated gradients) | |
| - Sensitive to noise due to small kernel | |
| - Good for images with sharp, diagonal features | |
| - Historical significance: one of the earliest edge detectors | |
| **The "Aha!" Moment:** | |
| Roberts showed that a simple 2×2 difference operation could capture edges— | |
| proving that edge detection doesn't require complex computation. | |
| --- | |
| #### Prewitt Operator (1970) | |
| **Kernels (3×3):** | |
| $$G_x = \\begin{bmatrix} -1 & 0 & +1 \\\\ -1 & 0 & +1 \\\\ -1 & 0 & +1 \\end{bmatrix}, \\quad | |
| G_y = \\begin{bmatrix} -1 & -1 & -1 \\\\ 0 & 0 & 0 \\\\ +1 & +1 & +1 \\end{bmatrix}$$ | |
| **Characteristics:** | |
| - Uses 3×3 neighborhood for gradient estimation | |
| - Incorporates implicit averaging (smoothing) perpendicular to gradient direction | |
| - More robust to noise than Roberts | |
| - Equal weighting of all pixels in each row/column | |
| - Separable: can be computed as two 1D convolutions | |
| **Key Insight:** | |
| Prewitt demonstrates the value of **spatial averaging**—smoothing in one direction | |
| while differentiating in the other reduces noise sensitivity. | |
| --- | |
| #### Sobel Operator (1968) | |
| **Kernels (3×3):** | |
| $$G_x = \\begin{bmatrix} -1 & 0 & +1 \\\\ -2 & 0 & +2 \\\\ -1 & 0 & +1 \\end{bmatrix}, \\quad | |
| G_y = \\begin{bmatrix} -1 & -2 & -1 \\\\ 0 & 0 & 0 \\\\ +1 & +2 & +1 \\end{bmatrix}$$ | |
| **Characteristics:** | |
| - Similar to Prewitt but with **weighted averaging** (2:1 center weight) | |
| - Better approximation of the true gradient | |
| - More isotropic response (similar sensitivity to all edge orientations) | |
| - Most widely used first-order edge detector | |
| - Can extend to larger kernel sizes (5×5, 7×7) for coarser features | |
| **The "Aha!" Moment:** | |
| Sobel's weighting scheme approximates a Gaussian smoothing perpendicular to the gradient. | |
| This is an early recognition that optimal edge detection combines **smoothing and differentiation**— | |
| a principle later formalized by Canny. | |
| --- | |
| #### Laplacian Operator (Second-Order) | |
| **Mathematical Form:** | |
| $$\\nabla^2 I = \\frac{\\partial^2 I}{\\partial x^2} + \\frac{\\partial^2 I}{\\partial y^2}$$ | |
| **Common Kernel (3×3):** | |
| $$\\nabla^2 = \\begin{bmatrix} 0 & 1 & 0 \\\\ 1 & -4 & 1 \\\\ 0 & 1 & 0 \\end{bmatrix}$$ | |
| **Characteristics:** | |
| - Second-order derivative: detects zero-crossings (rapid intensity changes) | |
| - **Isotropic**: rotationally invariant, no directional bias | |
| - Produces double edges (both sides of intensity transitions) | |
| - Very sensitive to noise (second derivative amplifies high frequencies) | |
| - Often combined with Gaussian smoothing → **Laplacian of Gaussian (LoG)** | |
| **Key Insight:** | |
| The Laplacian identifies edge locations as zero-crossings—where the second derivative | |
| changes sign. This gives **precise localization** but at the cost of noise sensitivity. | |
| --- | |
| #### Comparison Summary | |
| | Method | Kernel Size | Order | Noise Sensitivity | Directionality | Computational Cost | | |
| |--------|-------------|-------|-------------------|----------------|-------------------| | |
| | Roberts | 2×2 | 1st | High | Diagonal | Very Low | | |
| | Prewitt | 3×3 | 1st | Medium | Separable (H/V) | Low | | |
| | Sobel | 3×3+ | 1st | Medium-Low | Separable (H/V) | Low | | |
| | Laplacian | 3×3+ | 2nd | Very High | Isotropic | Low | | |
| **Practical Recommendation:** | |
| - **Sobel**: Best general-purpose gradient operator | |
| - **Roberts**: When speed is critical and images are low-noise | |
| - **Prewitt**: Similar to Sobel, historical interest | |
| - **Laplacian**: When precise localization matters, always pre-smooth | |
| """) | |
| if method in ["Sobel", "Prewitt", "Roberts"]: | |
| st.info(f""" | |
| **Current Method: {method}** | |
| You're currently using the {method} operator. Notice how it responds to different edge orientations | |
| in your image. Try toggling "Show Gradient Components" to see how Gx and Gy separately capture | |
| vertical and horizontal edges. | |
| """) | |
| with tab3: | |
| st.markdown(""" | |
| ### The Canny Edge Detector: Optimal Edge Detection | |
| **Historical Context:** | |
| In 1986, John Canny published a landmark paper deriving an "optimal" edge detector | |
| from first principles. He defined three criteria for good edge detection: | |
| 1. **Good Detection**: Minimize false positives and false negatives | |
| 2. **Good Localization**: Detected edges should be close to true edges | |
| 3. **Single Response**: One detector response per edge (no double edges) | |
| **The Canny Algorithm: A Multi-Stage Pipeline** | |
| --- | |
| #### Stage 1: Noise Reduction (Gaussian Smoothing) | |
| $$G(x, y) = \\frac{1}{2\\pi\\sigma^2} e^{-\\frac{x^2 + y^2}{2\\sigma^2}}$$ | |
| - Convolve image with Gaussian filter to reduce noise | |
| - $\\sigma$ controls smoothing scale | |
| - Trade-off: larger $\\sigma$ removes more noise but also blurs edges | |
| **Insight:** Canny recognized that edge detection and noise suppression are inherently coupled. | |
| The Gaussian is optimal for smoothing while preserving edge localization. | |
| --- | |
| #### Stage 2: Gradient Calculation | |
| - Apply Sobel operator (or similar) to compute $G_x$ and $G_y$ | |
| - Calculate magnitude: $M = \\sqrt{G_x^2 + G_y^2}$ | |
| - Calculate direction: $\\theta = \\arctan(G_y / G_x)$ | |
| --- | |
| #### Stage 3: Non-Maximum Suppression (Edge Thinning) | |
| **The Problem:** Gradient operators produce thick edges (multiple pixels respond to a single edge). | |
| **The Solution:** | |
| - For each pixel, examine magnitude along gradient direction | |
| - Suppress pixel if either neighbor along gradient direction has higher magnitude | |
| - Result: edges thinned to single-pixel width | |
| **The "Aha!" Moment:** | |
| This stage enforces the "single response" criterion. By suppressing non-maxima perpendicular | |
| to the edge direction, Canny achieves sub-pixel localization accuracy. | |
| --- | |
| #### Stage 4: Double Thresholding | |
| Use two thresholds to classify edge pixels: | |
| - **Strong edges**: $M > T_{high}$ — definitely edges | |
| - **Weak edges**: $T_{low} < M < T_{high}$ — potential edges | |
| - **Non-edges**: $M < T_{low}$ — definitely not edges | |
| **Rationale:** | |
| - Single threshold is too binary: either misses edges or includes noise | |
| - Double threshold creates a "confidence gradient" | |
| --- | |
| #### Stage 5: Edge Tracking by Hysteresis | |
| **The Final Step:** | |
| - Keep all strong edges | |
| - Keep weak edges only if connected to strong edges | |
| - Discard isolated weak edges | |
| **The "Aha!" Moment:** | |
| Hysteresis exploits **edge continuity**. Real edges are extended structures; noise is isolated. | |
| By requiring weak edges to connect to strong edges, Canny achieves robustness without | |
| sacrificing sensitivity. | |
| --- | |
| ### Why Canny Is "Optimal" | |
| Canny proved that, under certain assumptions (Gaussian noise, linear filters), his algorithm | |
| achieves optimal trade-offs among his three criteria. Specifically: | |
| - The **optimal filter** for edge detection is approximately the derivative of a Gaussian | |
| - The multi-scale nature (controlled by $\\sigma$) allows detection of edges at different scales | |
| - Non-maximum suppression and hysteresis address fundamental ambiguities in edge detection | |
| --- | |
| ### Limitations and Practical Considerations | |
| **When Canny Struggles:** | |
| - **Texture**: Dense texture can trigger many weak edges | |
| - **Low contrast**: Subtle edges may fall below threshold | |
| - **Computational cost**: More expensive than simple gradient operators | |
| **Parameter Tuning:** | |
| - **$T_{low}$ and $T_{high}$**: Typical ratio is 1:2 or 1:3 | |
| - Too high: miss edges | |
| - Too low: include noise | |
| - **$\\sigma$ (implicit in aperture size)**: Larger for coarse edges, smaller for fine details | |
| **Modern Context:** | |
| Despite being nearly 40 years old, Canny remains widely used. Its principles influenced: | |
| - Multi-scale edge detection (e.g., Canny-Deriche) | |
| - Learning-based edge detection (e.g., structured forests, CNNs) | |
| - Active contours and level sets | |
| --- | |
| ### Interactive Exploration | |
| **Try This:** | |
| 1. Set low_threshold = 50, high_threshold = 150 (default) | |
| 2. Gradually increase both thresholds → edges disappear | |
| 3. Gradually decrease both → more edges, including noise | |
| 4. Compare Canny to Sobel with the comparison feature | |
| **Question to Ponder:** | |
| Can you find threshold values where Canny detects edges that Sobel misses, | |
| and vice versa? What does this reveal about their different approaches? | |
| """) | |
| if method == "Canny": | |
| st.info(f""" | |
| **Current Parameters:** | |
| - Low Threshold: {low_threshold} | |
| - High Threshold: {high_threshold} | |
| - Ratio: 1:{high_threshold/low_threshold:.2f} | |
| **Interpretation:** | |
| - Pixels with magnitude > {high_threshold} are **strong edges** (white) | |
| - Pixels with magnitude between {low_threshold} and {high_threshold} are kept only if connected to strong edges | |
| - All other pixels are **non-edges** (black) | |
| """) | |
| with tab4: | |
| st.markdown(""" | |
| ### Practical Considerations in Edge Detection | |
| --- | |
| #### 1. Pre-processing: The Critical First Step | |
| **Gaussian Smoothing:** | |
| - Almost always beneficial, especially for Laplacian | |
| - Suppresses noise while preserving edge structure | |
| - $\\sigma$ controls scale: larger $\\sigma$ for coarser edges | |
| **Histogram Equalization:** | |
| - Enhances contrast in low-contrast images | |
| - Can make subtle edges detectable | |
| - May amplify noise in uniform regions | |
| **Morphological Operations:** | |
| - Closing: Connect nearby edge segments | |
| - Opening: Remove isolated noise pixels | |
| - Applied after edge detection, not before | |
| --- | |
| #### 2. Choosing the Right Method | |
| **Sobel** when: | |
| - You need directional gradient information | |
| - Computational efficiency matters | |
| - Image is reasonably clean | |
| - You're building a pipeline for further processing (e.g., Hough transform) | |
| **Canny** when: | |
| - You need thin, connected edges | |
| - You can afford the computational cost | |
| - You need robustness to noise | |
| - Output will be used for segmentation or shape analysis | |
| **Laplacian** when: | |
| - You need isotropic response | |
| - You're detecting blob-like features (LoG) | |
| - Combined with Gaussian (LoG): excellent for multi-scale analysis | |
| **Roberts** when: | |
| - Extreme computational constraints | |
| - Diagonal edges are prominent | |
| - Historical comparison or educational purposes | |
| --- | |
| #### 3. Common Pitfalls and Solutions | |
| **Problem: Too many edges detected** | |
| - **Cause:** Low threshold, high noise, texture | |
| - **Solution:** | |
| - Increase threshold (Canny) | |
| - Pre-smooth more aggressively | |
| - Use morphological opening to remove small responses | |
| **Problem: Missing important edges** | |
| - **Cause:** High threshold, low contrast, blurred image | |
| - **Solution:** | |
| - Decrease threshold | |
| - Apply histogram equalization | |
| - Try multi-scale detection (multiple $\\sigma$ values) | |
| **Problem: Thick, messy edges** | |
| - **Cause:** Not using non-maximum suppression | |
| - **Solution:** | |
| - Use Canny instead of raw gradient magnitude | |
| - Implement custom NMS if using Sobel/Prewitt | |
| **Problem: Disconnected edge segments** | |
| - **Cause:** Threshold too high, gaps in actual edges | |
| - **Solution:** | |
| - Lower threshold (especially Canny's low threshold) | |
| - Apply morphological closing | |
| - Use probabilistic Hough transform to connect segments | |
| --- | |
| #### 4. Domain-Specific Considerations | |
| **Medical Imaging:** | |
| - Often low contrast, high noise | |
| - Canny with careful threshold tuning | |
| - Multi-scale approaches (LoG pyramids) | |
| - Consider anisotropic diffusion for pre-processing | |
| **Natural Images (Photography):** | |
| - Complex scenes, varied lighting | |
| - Canny generally works well | |
| - May need semantic segmentation to identify "important" edges | |
| **Document Analysis:** | |
| - High contrast, sharp edges | |
| - Simple methods (Sobel, Roberts) often sufficient | |
| - Adaptive thresholding for varying illumination | |
| **Industrial Inspection:** | |
| - Controlled environment, consistent lighting | |
| - Fast methods preferred (Sobel) | |
| - Template matching often combined with edges | |
| **Autonomous Driving:** | |
| - Real-time requirements | |
| - Lane detection: focused edge detection in ROI | |
| - Modern systems use CNN-based edge detection | |
| --- | |
| #### 5. Computational Performance | |
| **Runtime Complexity (for N×N image):** | |
| - Roberts, Prewitt, Sobel: O(N²) — single-pass convolution | |
| - Laplacian: O(N²) — single-pass convolution | |
| - Canny: O(N²) but with higher constant factor due to multi-stage pipeline | |
| **Memory Considerations:** | |
| - Gradient methods: 2-3× image memory (for Gx, Gy, magnitude) | |
| - Canny: 3-4× image memory (gradients + intermediate stages) | |
| **Optimization Strategies:** | |
| - Use separable filters when possible (Sobel, Prewitt) | |
| - Implement on GPU for real-time applications | |
| - Consider approximate methods for very large images | |
| - Use integral images for multi-scale LoG | |
| --- | |
| #### 6. Beyond Classical Methods | |
| **Learning-Based Edge Detection:** | |
| - Structured Edge Detection (SED): Random forests on local patches | |
| - Holistically-Nested Edge Detection (HED): Deep CNN | |
| - Advantages: Context-aware, learns from data | |
| - Disadvantages: Requires training data, computationally intensive | |
| **Multi-Scale Approaches:** | |
| - Canny at multiple scales, combine results | |
| - Laplacian of Gaussian pyramid | |
| - Scale-space theory (Lindeberg) | |
| **Oriented Edge Detection:** | |
| - Steerable filters: efficient computation of gradients at multiple orientations | |
| - Gabor filters: detect edges at specific scales and orientations | |
| - Useful for texture analysis and orientation-dependent tasks | |
| --- | |
| #### 7. Evaluation Metrics | |
| **Quantitative Evaluation (when ground truth available):** | |
| - **Precision**: fraction of detected edges that are true edges | |
| - **Recall**: fraction of true edges that are detected | |
| - **F-measure**: harmonic mean of precision and recall | |
| - **Localization error**: distance between detected and true edge pixels | |
| **Qualitative Evaluation:** | |
| - Visual inspection: Are important edges detected? | |
| - Downstream task performance: Does edge quality improve final result? | |
| --- | |
| ### Take-Home Messages | |
| 1. **No Universal Best Method**: Choice depends on application requirements | |
| 2. **Pre-processing Matters**: Clean input → clean edges | |
| 3. **Thresholds Are Critical**: Tune based on your specific images | |
| 4. **Computational Complexity**: Simple doesn't mean inferior | |
| 5. **Edge Detection Is a Means, Not an End**: Always consider the downstream task | |
| **The Ultimate "Aha!" Moment:** | |
| Edge detection transforms high-dimensional pixel data into sparse, interpretable structures. | |
| This dimensionality reduction—from millions of pixels to thousands of edge points—is what makes | |
| computer vision computationally tractable. Understanding edges is understanding how machines | |
| "see" structure in the visual world. | |
| """) | |
| # Footer | |
| st.markdown("---") | |
| st.markdown(""" | |
| <small> | |
| Educational Demo for Image Analysis Courses | | |
| Built with Streamlit and OpenCV | | |
| <a href="https://github.com/ubern-image-analysis/edge-detection" target="_blank">View Source</a> | |
| </small> | |
| """, unsafe_allow_html=True) | |
| if __name__ == "__main__": | |
| main_loop() | |