""" ╔══════════════════════════════════════════════════════════════════════════════╗ ║ ║ ║ 🎓 CISC 121 - OOP Sorting & Searching Visualizer ║ ║ ║ ║ Queen's University - Introduction to Computing Science I ║ ║ ║ ║ This application demonstrates Object-Oriented Programming concepts ║ ║ through interactive visualization of sorting and searching algorithms. ║ ║ ║ ║ HOW TO RUN: python app_oop_gradio.py ║ ║ ║ ╚══════════════════════════════════════════════════════════════════════════════╝ 📚 PHASE 5: Gradio UI This is the final phase - creating a user-friendly web interface that: 1. Allows capturing/uploading gesture images 2. Displays the image list with gesture recognition 3. Lets users run sorting/searching algorithms 4. Visualizes each step of the algorithm The UI demonstrates COMPOSITION - the GradioApp class composes: - ImageList (data management) - SortingAlgorithm / SearchAlgorithm (algorithm execution) - Visualizer (step-by-step display) """ # ============================================================================== # IMPORTS # ============================================================================== import gradio as gr from PIL import Image import os from typing import List, Tuple, Optional # Import our OOP package from oop_sorting_teaching import ( # Models GestureRanking, GestureImage, ImageList, StepType, Step, # Sorting BubbleSort, MergeSort, QuickSort, PivotStrategy, PartitionScheme, # Searching LinearSearch, BinarySearch, # Visualization Visualizer, VisualizationConfig, RendererFactory, ) # Try to import transformers for gesture recognition try: from transformers import pipeline CLASSIFIER_AVAILABLE = True except ImportError: CLASSIFIER_AVAILABLE = False print("⚠️ transformers not installed. Using manual gesture selection.") # ============================================================================== # CONFIGURATION # ============================================================================== MODEL_NAME = "dima806/hand_gestures_image_detection" HF_TOKEN = os.environ.get("HF_TOKEN", None) APP_TITLE = "## 🎓 CISC 121 - OOP Sorting & Searching Visualizer" APP_DESCRIPTION = """ **Learn Object-Oriented Programming through Algorithm Visualization!** This app demonstrates key OOP concepts: - 📦 **Classes & Objects**: GestureImage, ImageList, Algorithms - 🎭 **Inheritance**: All sorting algorithms inherit from SortingAlgorithm - 🔄 **Polymorphism**: Swap between algorithms seamlessly - 🏭 **Factory Pattern**: RendererFactory creates the right visualizer **How to use:** 1. **Add images** using the buttons below (capture or manual) 2. **View your list** of gesture images 3. **Run an algorithm** to see step-by-step visualization 4. **Navigate steps** to understand how the algorithm works """ # ============================================================================== # GRADIO APP CLASS # ============================================================================== class GradioApp: """ 📚 CONCEPT: Composition The GradioApp class COMPOSES (contains) other objects: - ImageList for managing captured images - Visualizer for displaying algorithm steps - Classifier for gesture recognition (if available) This is the Controller in MVC pattern - it coordinates between user interface (View) and data/logic (Model). """ def __init__(self): """Initialize the application state.""" self.image_list = ImageList() self.visualizer = Visualizer(VisualizationConfig( show_statistics=True, show_legend=True, image_size=60 )) self._capture_count = 0 # Initialize classifier if available self.classifier = None if CLASSIFIER_AVAILABLE: try: self.classifier = pipeline( "image-classification", model=MODEL_NAME, token=HF_TOKEN ) print(f"✅ Loaded model: {MODEL_NAME}") except Exception as e: print(f"⚠️ Could not load model: {e}") # ------------------------------------------------------------------------- # Image Management Methods # ------------------------------------------------------------------------- def add_manual_gesture(self, gesture_name: str) -> Tuple[str, str]: """ Add a gesture image manually (without camera). Returns: Tuple of (image_list_html, status_message) """ if not gesture_name: return self._render_image_list(), "⚠️ Please select a gesture" self._capture_count += 1 self.image_list.add_new(gesture_name) return ( self._render_image_list(), f"✅ Added {GestureRanking.get_emoji(gesture_name)} {gesture_name} (#{self._capture_count})" ) def add_from_image(self, image: Image.Image) -> Tuple[str, str]: """ Add a gesture from an uploaded/captured image. Uses AI classification if available, otherwise prompts for manual selection. """ if image is None: return self._render_image_list(), "⚠️ No image provided" if self.classifier: try: # Classify the image results = self.classifier(image) if results: top_result = results[0] gesture_name = top_result['label'].lower() confidence = top_result['score'] self._capture_count += 1 img = GestureImage.create_from_prediction( gesture_name=gesture_name, capture_id=self._capture_count, image=image, confidence=confidence ) self.image_list._save_state() # Save before modifying self.image_list._images.append(img) return ( self._render_image_list(), f"✅ Detected: {img.emoji} {gesture_name} ({confidence:.1%} confidence)" ) except Exception as e: return self._render_image_list(), f"⚠️ Classification error: {e}" return self._render_image_list(), "⚠️ No classifier available. Use manual gesture selection." def remove_image(self, index: int) -> Tuple[str, str]: """Remove an image at the given index.""" if 0 <= index < len(self.image_list): removed = self.image_list[index] self.image_list.remove(index) return self._render_image_list(), f"✅ Removed {removed}" return self._render_image_list(), "⚠️ Invalid index" def shuffle_images(self) -> Tuple[str, str]: """Shuffle the image list.""" self.image_list.shuffle() return self._render_image_list(), "🔀 Shuffled!" def clear_images(self) -> Tuple[str, str]: """Clear all images.""" count = len(self.image_list) self.image_list.clear() self._capture_count = 0 self.visualizer.reset() return self._render_image_list(), f"🗑️ Cleared {count} images" def undo_action(self) -> Tuple[str, str]: """Undo the last action.""" if self.image_list.undo(): return self._render_image_list(), "↩️ Undone!" return self._render_image_list(), "⚠️ Nothing to undo" def add_sample_data(self) -> Tuple[str, str]: """Add sample data for testing.""" gestures = ['fist', 'peace', 'like', 'peace', 'ok', 'fist'] for g in gestures: self._capture_count += 1 self.image_list.add_new(g) return self._render_image_list(), f"✅ Added {len(gestures)} sample gestures" def add_instability_demo(self) -> Tuple[str, str]: """ Add data specifically designed to demonstrate Quick Sort instability. 📚 EDUCATIONAL PURPOSE: This creates a scenario where Quick Sort will reorder equal elements, demonstrating that it's an UNSTABLE sorting algorithm. Setup: [✌️₁] [✌️₂] [✌️₃] [✊₄] After Quick Sort: The peace signs may be reordered (e.g., ₂,₃,₁) After Bubble/Merge Sort: Order preserved (₁,₂,₃) """ self.clear_images() # Three peace signs followed by a lower-ranked fist demo_gestures = ['peace', 'peace', 'peace', 'fist'] for g in demo_gestures: self._capture_count += 1 self.image_list.add_new(g) return ( self._render_image_list(), "🎓 Instability Demo: [✌️₁][✌️₂][✌️₃][✊₄]\n" "Try Quick Sort vs Bubble Sort - watch the subscript order!" ) def add_worst_case_demo(self) -> Tuple[str, str]: """ Add already-sorted data to demonstrate worst-case for Quick Sort. 📚 EDUCATIONAL PURPOSE: When data is already sorted and we use First Pivot strategy, Quick Sort degrades to O(n²) - its worst case! """ self.clear_images() # Sorted order: fist(1) < peace(2) < like(3) < ok(4) < call(5) sorted_gestures = ['fist', 'peace', 'like', 'ok', 'call'] for g in sorted_gestures: self._capture_count += 1 self.image_list.add_new(g) return ( self._render_image_list(), "🎓 Worst-Case Demo: Already sorted data!\n" "Quick Sort with First Pivot → O(n²)\n" "Try Median-of-3 or Random pivot to see the difference." ) def add_binary_search_demo(self) -> Tuple[str, str]: """ Add sorted data for binary search demonstration. 📚 EDUCATIONAL PURPOSE: Binary search requires sorted data. This preset shows how O(log n) is much faster than O(n) linear search. """ self.clear_images() # Create larger sorted dataset for more dramatic comparison gestures = ['fist', 'fist', 'peace', 'peace', 'like', 'like', 'ok', 'ok', 'call', 'call', 'palm', 'palm'] for g in gestures: self._capture_count += 1 self.image_list.add_new(g) return ( self._render_image_list(), "🎓 Search Demo: 12 sorted elements\n" "Linear Search: up to 12 comparisons\n" "Binary Search: at most 4 comparisons (log₂12 ≈ 3.6)" ) # ------------------------------------------------------------------------- # Algorithm Execution Methods # ------------------------------------------------------------------------- def run_sort(self, algorithm_name: str, pivot_strategy: str = "first", partition_scheme: str = "2-way") -> Tuple[str, str, str]: """ Run a sorting algorithm on the image list. Returns: Tuple of (visualization_html, image_list_html, status_message) """ if len(self.image_list) < 2: return ( self.visualizer.render_current(), self._render_image_list(), "⚠️ Need at least 2 images to sort" ) # Create the algorithm instance if algorithm_name == "Bubble Sort": algo = BubbleSort() elif algorithm_name == "Merge Sort": algo = MergeSort() elif algorithm_name == "Quick Sort": # Map string to enum pivot_map = { "first": PivotStrategy.FIRST, "last": PivotStrategy.LAST, "median": PivotStrategy.MEDIAN_OF_THREE, "random": PivotStrategy.RANDOM, } partition_map = { "2-way": PartitionScheme.TWO_WAY, "3-way": PartitionScheme.THREE_WAY, } algo = QuickSort( pivot_strategy=pivot_map.get(pivot_strategy, PivotStrategy.FIRST), partition_scheme=partition_map.get(partition_scheme, PartitionScheme.TWO_WAY) ) else: return ( self.visualizer.render_current(), self._render_image_list(), f"⚠️ Unknown algorithm: {algorithm_name}" ) # Get data copy and run algorithm data = list(self.image_list) sorted_data, steps = algo.run_full(data) # Load into visualizer self.visualizer.load_steps(steps, sorted_data, algo.name) # Update the image list to sorted order self.image_list._save_state() # Save before modifying self.image_list._images = list(sorted_data) return ( self.visualizer.render_current(), self._render_image_list(), f"✅ {algo.name}: {len(steps)} steps" ) def run_search(self, algorithm_name: str, target_index: int) -> Tuple[str, str]: """ Run a search algorithm. Args: algorithm_name: "Linear Search" or "Binary Search" target_index: Index of the target element to search for Returns: Tuple of (visualization_html, status_message) """ if len(self.image_list) < 1: return self.visualizer.render_current(), "⚠️ Need at least 1 image to search" if not (0 <= target_index < len(self.image_list)): return self.visualizer.render_current(), "⚠️ Invalid target index" data = list(self.image_list) target = data[target_index] # For binary search, we need sorted data if algorithm_name == "Binary Search": if not self.image_list.is_sorted(): return ( self.visualizer.render_current(), "⚠️ Binary Search requires sorted data! Run a sort first." ) algo = BinarySearch(variant="iterative") else: algo = LinearSearch() # Run the search result_index, steps = algo.run_full(data, target) # Load into visualizer self.visualizer.load_steps(steps, data, algo.name) if result_index is not None: status = f"✅ {algo.name}: Found {target} at index {result_index}" else: status = f"❌ {algo.name}: {target} not found" return self.visualizer.render_current(), status # ------------------------------------------------------------------------- # Visualization Navigation Methods # ------------------------------------------------------------------------- def viz_next(self) -> str: """Go to next visualization step.""" return self.visualizer.next_step() def viz_prev(self) -> str: """Go to previous visualization step.""" return self.visualizer.prev_step() def viz_start(self) -> str: """Go to first step.""" return self.visualizer.go_to_start() def viz_end(self) -> str: """Go to last step.""" return self.visualizer.go_to_end() def viz_goto(self, step: int) -> str: """Go to a specific step.""" return self.visualizer.go_to_step(int(step) - 1) # Convert to 0-based # ------------------------------------------------------------------------- # Rendering Methods # ------------------------------------------------------------------------- def _render_image_list(self) -> str: """Render the current image list as HTML.""" if len(self.image_list) == 0: return """
Add gestures using the buttons above!