// Particle Animation Background class ParticleBackground { constructor(canvasId) { this.canvas = document.getElementById(canvasId); this.ctx = this.canvas.getContext('2d'); this.particles = []; this.particleCount = 50; this.init(); } init() { // Set canvas to full window size this.resizeCanvas(); window.addEventListener('resize', () => this.resizeCanvas()); // Create particles this.createParticles(); // Start animation loop this.animate(); } resizeCanvas() { this.canvas.width = window.innerWidth; this.canvas.height = window.innerHeight; } createParticles() { this.particles = []; for (let i = 0; i < this.particleCount; i++) { this.particles.push({ x: Math.random() * this.canvas.width, y: Math.random() * this.canvas.height, radius: Math.random() * 3 + 1, speed: Math.random() * 1 + 0.2, directionX: Math.random() * 2 - 1, directionY: Math.random() * 2 - 1, opacity: Math.random() * 0.5 + 0.1 }); } } drawParticles() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); for (let i = 0; i < this.particles.length; i++) { const p = this.particles[i]; // Draw particle this.ctx.beginPath(); this.ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); this.ctx.fillStyle = `rgba(255, 255, 255, ${p.opacity})`; this.ctx.fill(); // Update position p.x += p.directionX * p.speed; p.y += p.directionY * p.speed; // Bounce off edges if (p.x < 0 || p.x > this.canvas.width) p.directionX *= -1; if (p.y < 0 || p.y > this.canvas.height) p.directionY *= -1; // Draw connections for (let j = i + 1; j < this.particles.length; j++) { const p2 = this.particles[j]; const distance = Math.sqrt( Math.pow(p.x - p2.x, 2) + Math.pow(p.y - p2.y, 2) ); if (distance < 150) { this.ctx.beginPath(); this.ctx.strokeStyle = `rgba(255, 255, 255, ${0.2 * (1 - distance/150)})`; this.ctx.lineWidth = 0.5; this.ctx.moveTo(p.x, p.y); this.ctx.lineTo(p2.x, p2.y); this.ctx.stroke(); } } } } animate() { this.drawParticles(); requestAnimationFrame(() => this.animate()); } } /* ============================================== Object Detection Handler ================================================== */ class VisionAIDetector { constructor() { // DOM Elements this.modelSelect = document.getElementById('modelSelect'); this.thresholdRange = document.getElementById('thresholdRange'); this.thresholdValue = document.getElementById('thresholdValue'); this.uploadBtn = document.getElementById('uploadBtn'); this.imageUpload = document.getElementById('imageUpload'); this.liveCaptureBtn = document.getElementById('liveCaptureBtn'); this.screenshotBtn = document.getElementById('screenshotBtn'); this.liveVideo = document.getElementById('liveVideo'); this.detectedCanvas = document.getElementById('detectedCanvas'); this.loadingOverlay = document.getElementById('loadingOverlay'); this.modelLabel = document.getElementById('modelLabel'); this.objectList = document.getElementById('objectList'); this.objectCounter = document.querySelector('.object-counter'); this.totalObjects = document.getElementById('totalObjects'); this.totalCategories = document.getElementById('totalCategories'); this.avgConfidence = document.getElementById('avgConfidence'); this.objectTypeChart = document.getElementById('objectTypeChart'); this.generateAudioBtn = document.getElementById('generateAudioBtn'); this.voiceTypeSelect = document.getElementById('voiceTypeSelect'); this.speechRateSelect = document.getElementById('speechRateSelect'); // Tab panel elements this.objectsTab = document.querySelector('[data-tab="objects"]'); this.statsTab = document.querySelector('[data-tab="stats"]'); this.audioTab = document.querySelector('[data-tab="audio"]'); this.objectsTabPane = document.getElementById('objectsTab'); this.statsTabPane = document.getElementById('statsTab'); this.audioTabPane = document.getElementById('audioTab'); // Canvas context this.ctx = this.detectedCanvas.getContext('2d'); // State variables this.stream = null; this.chart = null; this.detectionResults = null; this.currentImageDataUrl = null; // Store the current image for reprocessing this.processingLock = false; // Lock to prevent multiple simultaneous processings // Backend URL - change this to match your production setup this.apiUrl = 'http://localhost:5000'; // Initialize this.init(); } init() { // Set initial values this.modelLabel.textContent = this.modelSelect.value.split('.')[0]; // Event listeners for image input this.uploadBtn.addEventListener('click', () => this.imageUpload.click()); this.imageUpload.addEventListener('change', (e) => this.handleImageUpload(e)); this.liveCaptureBtn.addEventListener('click', () => this.toggleLiveCapture()); this.screenshotBtn.addEventListener('click', () => this.captureScreenshot()); // Event listeners for settings changes with real-time processing this.modelSelect.addEventListener('change', () => { this.modelLabel.textContent = this.modelSelect.value.split('.')[0]; this.reprocessCurrentImage(); }); this.thresholdRange.addEventListener('input', () => { this.thresholdValue.textContent = `${this.thresholdRange.value}%`; // Debounce threshold changes to prevent too many API calls clearTimeout(this.thresholdTimeout); this.thresholdTimeout = setTimeout(() => { this.reprocessCurrentImage(); }, 300); }); // Tab panel handlers - Enhanced for direct tab navigation this.objectsTab.addEventListener('click', () => this.switchTab('objects')); this.statsTab.addEventListener('click', () => this.switchTab('stats')); this.audioTab.addEventListener('click', () => this.switchTab('audio')); // Initialize charts this.initChart(); } /* ============================================== Tab Switching Logic under Detection Section ================================================== */ switchTab(tabId) { // Remove active class from all tabs [this.objectsTab, this.statsTab, this.audioTab].forEach(tab => tab.classList.remove('active')); // Hide all panes first this.objectsTabPane.style.display = 'none'; this.statsTabPane.style.display = 'none'; this.audioTabPane.style.display = 'none'; // Add active class to selected tab and show only its pane if (tabId === 'objects') { this.objectsTab.classList.add('active'); this.objectsTabPane.style.display = 'block'; } else if (tabId === 'stats') { this.statsTab.classList.add('active'); this.statsTabPane.style.display = 'block'; // Refresh stats content if we have results if (this.detectionResults) { this.updateStats(this.detectionResults); } } else if (tabId === 'audio') { this.audioTab.classList.add('active'); this.objectsTabPane.style.display = 'block'; this.audioTabPane.style.display = 'block'; } } async handleImageUpload(e) { const file = e.target.files[0]; if (!file) return; try { // Show loading overlay this.loadingOverlay.style.display = 'flex'; // Read the image file const imageDataUrl = await this.readFileAsDataURL(file); this.currentImageDataUrl = imageDataUrl; // Store for later reprocessing // Load image to get dimensions const img = await this.loadImage(imageDataUrl); // Set canvas dimensions this.detectedCanvas.width = img.width; this.detectedCanvas.height = img.height; // Draw original image on canvas this.ctx.drawImage(img, 0, 0); // Get selected model and confidence threshold const model = this.modelSelect.value; const confidenceThreshold = parseInt(this.thresholdRange.value) / 100; // Process the image await this.processImage(imageDataUrl, model, confidenceThreshold); // Enable screenshot button this.screenshotBtn.disabled = false; // Hide loading overlay this.loadingOverlay.style.display = 'none'; } catch (error) { console.error('Error processing image:', error); this.showError('Failed to process image. Please try again.'); this.loadingOverlay.style.display = 'none'; } } async reprocessCurrentImage() { // If no image is loaded or processing is already happening, do nothing if (!this.currentImageDataUrl || this.processingLock) return; this.processingLock = true; try { // Show loading overlay this.loadingOverlay.style.display = 'flex'; // Get current settings const model = this.modelSelect.value; const confidenceThreshold = parseInt(this.thresholdRange.value) / 100; // Reprocess with new settings await this.processImage(this.currentImageDataUrl, model, confidenceThreshold); // Hide loading overlay this.loadingOverlay.style.display = 'none'; } catch (error) { console.error('Error reprocessing image:', error); this.showError('Failed to reprocess image. Please try again.'); this.loadingOverlay.style.display = 'none'; } finally { this.processingLock = false; } } async toggleLiveCapture() { if (!this.stream) { // Start camera try { this.stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 } } }); // Display video this.liveVideo.srcObject = this.stream; this.liveVideo.style.display = 'block'; this.detectedCanvas.style.display = 'none'; this.liveVideo.play(); // Change button text this.liveCaptureBtn.innerHTML = 'Capture'; // Enable screenshot button this.screenshotBtn.disabled = false; } catch (error) { console.error('Error accessing camera:', error); this.showError('Could not access camera. Please check permissions.'); } } else { // Take a snapshot and process this.captureScreenshot(); } } captureScreenshot() { if (!this.stream && this.liveVideo.style.display !== 'block') return; try { // Show loading overlay this.loadingOverlay.style.display = 'flex'; // Create temporary canvas to capture frame const tempCanvas = document.createElement('canvas'); tempCanvas.width = this.liveVideo.videoWidth; tempCanvas.height = this.liveVideo.videoHeight; const tempCtx = tempCanvas.getContext('2d'); tempCtx.drawImage(this.liveVideo, 0, 0); // Convert to data URL const imageDataUrl = tempCanvas.toDataURL('image/jpeg'); this.currentImageDataUrl = imageDataUrl; // Store for later reprocessing // Set canvas dimensions this.detectedCanvas.width = tempCanvas.width; this.detectedCanvas.height = tempCanvas.height; // Draw captured frame on main canvas this.ctx.drawImage(tempCanvas, 0, 0); // Stop video stream this.stopVideoStream(); // Show canvas this.detectedCanvas.style.display = 'block'; // Get selected model and confidence threshold const model = this.modelSelect.value; const confidenceThreshold = parseInt(this.thresholdRange.value) / 100; // Process the image this.processImage(imageDataUrl, model, confidenceThreshold); } catch (error) { console.error('Error capturing screenshot:', error); this.showError('Failed to capture image. Please try again.'); this.loadingOverlay.style.display = 'none'; } } stopVideoStream() { if (this.stream) { this.stream.getTracks().forEach(track => track.stop()); this.stream = null; this.liveVideo.style.display = 'none'; this.liveCaptureBtn.innerHTML = 'Live Camera'; } } async processImage(imageDataUrl, selectedModel, confidenceThreshold) { try { // Make API request to backend const response = await fetch(`${this.apiUrl}/detect`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image: imageDataUrl, model: selectedModel, confidence: confidenceThreshold }) }); if (!response.ok) { throw new Error(`Server returned ${response.status}`); } const data = await response.json(); // Store results for use in other tabs this.detectionResults = data; // Get original image from canvas (important to preserve it when reprocessing) const originalImage = new Image(); originalImage.src = imageDataUrl; // Wait for image to load await new Promise(resolve => { originalImage.onload = resolve; }); // Clear canvas and redraw original image this.ctx.clearRect(0, 0, this.detectedCanvas.width, this.detectedCanvas.height); this.ctx.drawImage(originalImage, 0, 0, this.detectedCanvas.width, this.detectedCanvas.height); // Draw detection results this.drawDetections(data.detections); // Update object list this.updateObjectList(data.grouped_objects); // Update stats this.updateStats(data); // Enable audio generation this.generateAudioBtn.disabled = false; this.generateAudioBtn.onclick = () => this.generateAudioDescription(data.grouped_objects); // Hide loading overlay this.loadingOverlay.style.display = 'none'; } catch (error) { console.error('Detection Error:', error); this.showError('Detection failed. Please try again.'); this.loadingOverlay.style.display = 'none'; } } drawDetections(detections) { // Draw each detection detections.forEach(detection => { const [x, y, width, height] = detection.bbox; // Draw bounding box this.ctx.beginPath(); this.ctx.rect(x, y, width, height); this.ctx.lineWidth = 3; this.ctx.strokeStyle = 'rgba(255, 0, 0, 0.8)'; this.ctx.stroke(); // Create label background const label = `${detection.class} (${(detection.confidence * 100).toFixed(0)}%)`; this.ctx.font = '16px Arial'; const textWidth = this.ctx.measureText(label).width + 10; this.ctx.fillStyle = 'rgba(255, 0, 0, 0.7)'; this.ctx.fillRect( x, y > 25 ? y - 25 : y, textWidth, 25 ); // Draw label text this.ctx.fillStyle = 'white'; this.ctx.fillText( label, x + 5, y > 25 ? y - 7 : y + 18 ); }); } updateObjectList(groupedObjects) { // Clear previous list this.objectList.innerHTML = ''; if (groupedObjects.length === 0) { const li = document.createElement('li'); li.className = 'no-objects'; li.textContent = 'No objects detected'; this.objectList.appendChild(li); this.objectCounter.textContent = '0'; return; } // Update counter const totalCount = groupedObjects.reduce((sum, obj) => sum + obj.count, 0); this.objectCounter.textContent = totalCount; // Add each object group to the list groupedObjects.forEach(group => { const li = document.createElement('li'); const confidence = this.detectionResults.detections .filter(d => d.class === group.class) .reduce((sum, d) => sum + d.confidence, 0) / group.count; li.innerHTML = `