// Global variables let currentImage = null; let isDrawing = false; let drawingPath = []; let bboxPath = {}; let canvas, drawCanvas, bboxCanvas, extractedCanvas; let ctx, drawCtx, bboxCtx, extractedCtx; let imageLoaded = false; let currentSearchId = null; let searchPollInterval = null; document.addEventListener('DOMContentLoaded', () => { setupEventListeners(); setupCanvas(); // Auto-load Breaking Bad.jpg autoLoadDefaultImage(); }); function autoLoadDefaultImage() { const defaultImagePath = '/static/Breaking Bad.jpg'; fetch(defaultImagePath) .then(response => { if (response.ok) { return response.blob(); } throw new Error('Default image not found'); }) .then(blob => { const file = new File([blob], 'Breaking Bad.jpg', { type: 'image/jpeg' }); handleFile(file); }) .catch(error => { console.log('Default image not available:', error); }); } function setupEventListeners() { const dropZone = document.getElementById('dropZone'); const fileInput = document.getElementById('fileInput'); const processBtn = document.getElementById('processBtn'); dropZone.addEventListener('click', () => fileInput.click()); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); }); dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('drag-over'); }); dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); if (e.dataTransfer.files.length > 0) { handleFile(e.dataTransfer.files[0]); } }); fileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) { handleFile(e.target.files[0]); } }); // Process button - select entire image and start search processBtn.addEventListener('click', () => { if (!imageLoaded) return; // Select entire image bboxPath = { x: 0, y: 0, width: canvas.width, height: canvas.height }; drawBoundingBox(bboxPath); extractBboxRegion(bboxPath); }); } function setupCanvas() { canvas = document.getElementById('imageCanvas'); drawCanvas = document.getElementById('drawCanvas'); bboxCanvas = document.getElementById('bboxCanvas'); extractedCanvas = document.getElementById('extractedCanvas'); ctx = canvas.getContext('2d'); drawCtx = drawCanvas.getContext('2d'); bboxCtx = bboxCanvas.getContext('2d'); extractedCtx = extractedCanvas.getContext('2d'); // Attach events to bboxCanvas (top layer) bboxCanvas.addEventListener('mousedown', startDrawing); bboxCanvas.addEventListener('mousemove', draw); bboxCanvas.addEventListener('mouseup', stopDrawing); bboxCanvas.addEventListener('mouseleave', stopDrawing); bboxCanvas.style.cursor = 'crosshair'; } function loadDemoImage(filename) { fetch(`/static/${filename}`) .then(response => response.blob()) .then(blob => { const file = new File([blob], filename, { type: 'image/jpeg' }); handleFile(file); }) .catch(error => { console.error('Error loading demo image:', error); alert('Error loading demo image'); }); } function handleFile(file) { if (!file.type.startsWith('image/')) { alert('Please select an image file'); return; } clearDrawing(); imageLoaded = false; const formData = new FormData(); formData.append('file', file); fetch('/upload', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { loadImage(data.url); document.getElementById('fileName').textContent = file.name; } else { alert('Error: ' + data.error); } }) .catch(error => { console.error('Error:', error); alert('Error uploading image'); }); } function loadImage(url) { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => { currentImage = img; const maxWidth = window.innerWidth * 0.8; const maxHeight = window.innerHeight * 0.5; let width = img.width; let height = img.height; const scale = Math.min(maxWidth / width, maxHeight / height, 1); width = width * scale; height = height * scale; canvas.width = width; canvas.height = height; drawCanvas.width = width; drawCanvas.height = height; bboxCanvas.width = width; bboxCanvas.height = height; ctx.drawImage(img, 0, 0, width, height); // Set container height to match canvas const canvasContainer = document.getElementById('canvasContainer'); canvasContainer.style.height = height + 'px'; canvasContainer.style.minHeight = height + 'px'; document.getElementById('canvasContainer').style.display = 'block'; document.getElementById('optionsSection').style.display = 'flex'; document.getElementById('hintText').style.display = 'block'; imageLoaded = true; }; img.onerror = () => alert('Error loading image'); img.src = url; } function startDrawing(e) { if (!imageLoaded) return; drawCtx.clearRect(0, 0, drawCanvas.width, drawCanvas.height); bboxCtx.clearRect(0, 0, bboxCanvas.width, bboxCanvas.height); drawingPath = []; bboxPath = {}; isDrawing = true; const rect = bboxCanvas.getBoundingClientRect(); // Account for zoom (65%) const zoom = 0.65; const x = (e.clientX - rect.left) / zoom; const y = (e.clientY - rect.top) / zoom; drawingPath = [{ x, y }]; drawCtx.beginPath(); drawCtx.moveTo(x, y); } function draw(e) { if (!isDrawing) return; const rect = bboxCanvas.getBoundingClientRect(); // Account for zoom (65%) const zoom = 0.65; const x = (e.clientX - rect.left) / zoom; const y = (e.clientY - rect.top) / zoom; drawingPath.push({ x, y }); drawCtx.lineTo(x, y); drawCtx.strokeStyle = '#ff00ff'; drawCtx.lineWidth = 3; drawCtx.lineCap = 'round'; drawCtx.lineJoin = 'round'; drawCtx.stroke(); } function stopDrawing() { if (!isDrawing) return; isDrawing = false; // Check if it's just a click (select whole image) if (drawingPath.length === 1 || (drawingPath.length === 2 && Math.abs(drawingPath[0].x - drawingPath[drawingPath.length - 1].x) < 5 && Math.abs(drawingPath[0].y - drawingPath[drawingPath.length - 1].y) < 5)) { bboxPath = { x: 0, y: 0, width: canvas.width, height: canvas.height }; drawBoundingBox(bboxPath); extractBboxRegion(bboxPath); drawCtx.clearRect(0, 0, drawCanvas.width, drawCanvas.height); } else if (drawingPath.length > 2) { createBoundingBox(); } } function createBoundingBox() { const xs = drawingPath.map(p => p.x); const ys = drawingPath.map(p => p.y); const minX = Math.min(...xs); const maxX = Math.max(...xs); const minY = Math.min(...ys); const maxY = Math.max(...ys); const padding = 10; bboxPath = { x: Math.max(0, minX - padding), y: Math.max(0, minY - padding), width: Math.min(canvas.width - Math.max(0, minX - padding), maxX - minX + padding * 2), height: Math.min(canvas.height - Math.max(0, minY - padding), maxY - minY + padding * 2) }; drawBoundingBox(bboxPath); extractBboxRegion(bboxPath); } function drawBoundingBox(bbox) { bboxCtx.clearRect(0, 0, bboxCanvas.width, bboxCanvas.height); // Darken outside bbox with rounded corners const cornerRadius = Math.min(bbox.width, bbox.height) * 0.08; bboxCtx.fillStyle = 'rgba(0, 0, 0, 0.5)'; bboxCtx.fillRect(0, 0, bboxCanvas.width, bboxCanvas.height); // Clear bbox area with rounded corners bboxCtx.globalCompositeOperation = 'destination-out'; bboxCtx.beginPath(); bboxCtx.moveTo(bbox.x + cornerRadius, bbox.y); bboxCtx.lineTo(bbox.x + bbox.width - cornerRadius, bbox.y); bboxCtx.quadraticCurveTo(bbox.x + bbox.width, bbox.y, bbox.x + bbox.width, bbox.y + cornerRadius); bboxCtx.lineTo(bbox.x + bbox.width, bbox.y + bbox.height - cornerRadius); bboxCtx.quadraticCurveTo(bbox.x + bbox.width, bbox.y + bbox.height, bbox.x + bbox.width - cornerRadius, bbox.y + bbox.height); bboxCtx.lineTo(bbox.x + cornerRadius, bbox.y + bbox.height); bboxCtx.quadraticCurveTo(bbox.x, bbox.y + bbox.height, bbox.x, bbox.y + bbox.height - cornerRadius); bboxCtx.lineTo(bbox.x, bbox.y + cornerRadius); bboxCtx.quadraticCurveTo(bbox.x, bbox.y, bbox.x + cornerRadius, bbox.y); bboxCtx.closePath(); bboxCtx.fill(); bboxCtx.globalCompositeOperation = 'source-over'; // Draw long corner indicators (extending inward) const cornerLength = Math.min(bbox.width, bbox.height) * 0.25; // Longer corners const cornerThickness = Math.max(4, Math.min(bbox.width, bbox.height) * 0.015); // Thicker bboxCtx.strokeStyle = '#ffffff'; bboxCtx.lineWidth = cornerThickness; bboxCtx.lineCap = 'round'; bboxCtx.lineJoin = 'round'; // Top-left corner with rounded edge bboxCtx.beginPath(); bboxCtx.moveTo(bbox.x, bbox.y + cornerLength); bboxCtx.lineTo(bbox.x, bbox.y + cornerRadius); bboxCtx.quadraticCurveTo(bbox.x, bbox.y, bbox.x + cornerRadius, bbox.y); bboxCtx.lineTo(bbox.x + cornerLength, bbox.y); bboxCtx.stroke(); // Top-right corner with rounded edge bboxCtx.beginPath(); bboxCtx.moveTo(bbox.x + bbox.width - cornerLength, bbox.y); bboxCtx.lineTo(bbox.x + bbox.width - cornerRadius, bbox.y); bboxCtx.quadraticCurveTo(bbox.x + bbox.width, bbox.y, bbox.x + bbox.width, bbox.y + cornerRadius); bboxCtx.lineTo(bbox.x + bbox.width, bbox.y + cornerLength); bboxCtx.stroke(); // Bottom-right corner with rounded edge bboxCtx.beginPath(); bboxCtx.moveTo(bbox.x + bbox.width, bbox.y + bbox.height - cornerLength); bboxCtx.lineTo(bbox.x + bbox.width, bbox.y + bbox.height - cornerRadius); bboxCtx.quadraticCurveTo(bbox.x + bbox.width, bbox.y + bbox.height, bbox.x + bbox.width - cornerRadius, bbox.y + bbox.height); bboxCtx.lineTo(bbox.x + bbox.width - cornerLength, bbox.y + bbox.height); bboxCtx.stroke(); // Bottom-left corner with rounded edge bboxCtx.beginPath(); bboxCtx.moveTo(bbox.x + cornerLength, bbox.y + bbox.height); bboxCtx.lineTo(bbox.x + cornerRadius, bbox.y + bbox.height); bboxCtx.quadraticCurveTo(bbox.x, bbox.y + bbox.height, bbox.x, bbox.y + bbox.height - cornerRadius); bboxCtx.lineTo(bbox.x, bbox.y + bbox.height - cornerLength); bboxCtx.stroke(); } function extractBboxRegion(bbox) { if (!bbox.width || !bbox.height) return; // Show extracted region document.getElementById('extractedCard').style.display = 'block'; const maxWidth = 300; const aspectRatio = bbox.height / bbox.width; let extractedWidth = Math.min(maxWidth, bbox.width); let extractedHeight = extractedWidth * aspectRatio; extractedCanvas.width = extractedWidth; extractedCanvas.height = extractedHeight; const imageData = ctx.getImageData(bbox.x, bbox.y, bbox.width, bbox.height); const tempCanvas = document.createElement('canvas'); tempCanvas.width = bbox.width; tempCanvas.height = bbox.height; const tempCtx = tempCanvas.getContext('2d'); tempCtx.putImageData(imageData, 0, 0); extractedCtx.drawImage(tempCanvas, 0, 0, extractedWidth, extractedHeight); // Start feature search startFeatureSearch(tempCanvas); } function clearDrawing() { if (drawCtx) drawCtx.clearRect(0, 0, drawCanvas.width, drawCanvas.height); if (bboxCtx) bboxCtx.clearRect(0, 0, bboxCanvas.width, bboxCanvas.height); drawingPath = []; bboxPath = {}; document.getElementById('extractedCard').style.display = 'none'; document.getElementById('resultsSection').style.display = 'none'; stopFeatureSearch(); } function startFeatureSearch(extractedCanvas) { stopFeatureSearch(); const imageData = extractedCanvas.toDataURL('image/jpeg', 0.9); const useSift = document.getElementById('useSift').checked; const disableViz = document.getElementById('disableViz').checked; // Show loading document.getElementById('loading').style.display = 'block'; // Show video feed if visualization is enabled if (!disableViz) { document.getElementById('video-container').style.display = 'block'; document.getElementById('video-stream').src = `/video_feed?t=${new Date().getTime()}`; } fetch('/search_features', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image: imageData, use_sift: useSift }) }) .then(response => response.json()) .then(data => { if (data.search_id) { currentSearchId = data.search_id; pollSearchResults(); } else { console.error('Failed to start search:', data.error); document.getElementById('loading').style.display = 'none'; } }) .catch(error => { console.error('Error starting search:', error); document.getElementById('loading').style.display = 'none'; }); } function stopFeatureSearch() { if (searchPollInterval) { clearInterval(searchPollInterval); searchPollInterval = null; } currentSearchId = null; } function pollSearchResults() { if (!currentSearchId) return; searchPollInterval = setInterval(() => { fetch(`/search_status/${currentSearchId}`) .then(response => response.json()) .then(data => { if (data.status === 'completed') { stopFeatureSearch(); displayFinalResults(data); document.getElementById('loading').style.display = 'none'; document.getElementById('video-container').style.display = 'none'; } else if (data.status === 'error') { stopFeatureSearch(); alert(data.message); document.getElementById('loading').style.display = 'none'; document.getElementById('video-container').style.display = 'none'; } }) .catch(error => { console.error('Error polling search:', error); stopFeatureSearch(); document.getElementById('loading').style.display = 'none'; }); }, 200); } function displayFinalResults(data) { if (!data.best_match) { const message = data.message || 'No matches found'; alert(message); return; } document.getElementById('resultsSection').style.display = 'block'; document.getElementById('bestMatchImage').src = data.best_match.match_image; document.getElementById('bestMatchName').textContent = data.best_match.filename; document.getElementById('bestMatchScore').textContent = `${data.best_match.score} matches`; // Display top matches as image grid (4 per row, 3 by default) const topMatchesGrid = document.getElementById('topMatchesGrid'); topMatchesGrid.innerHTML = ''; if (data.top_matches && data.top_matches.length > 0) { // Show first 3 by default const matchesToShow = data.top_matches.slice(0, 3); matchesToShow.forEach((match, index) => { const matchDiv = document.createElement('div'); matchDiv.className = 'match-grid-item'; matchDiv.innerHTML = ` ${match.filename}
#${index + 1} ${match.filename} ${match.score} matches
`; topMatchesGrid.appendChild(matchDiv); }); } } // Fullscreen Image Viewer function openFullscreen(imageSrc) { const modal = document.getElementById('fullscreenModal'); const modalImg = document.getElementById('fullscreenImage'); modal.style.display = 'block'; modalImg.src = imageSrc; document.body.style.overflow = 'hidden'; } function closeFullscreen() { const modal = document.getElementById('fullscreenModal'); modal.style.display = 'none'; document.body.style.overflow = 'auto'; } // Function to add fullscreen to dynamically created top match images function addFullscreenToTopMatches() { const topMatchImages = document.querySelectorAll('.top-matches-grid img'); topMatchImages.forEach(img => { img.style.cursor = 'pointer'; img.onclick = function () { openFullscreen(this.src); }; }); } // Setup fullscreen modal and observers document.addEventListener('DOMContentLoaded', function () { const modal = document.getElementById('fullscreenModal'); const closeBtn = document.querySelector('.fullscreen-close'); // Close on X button if (closeBtn) { closeBtn.onclick = closeFullscreen; } // Close on background click if (modal) { modal.onclick = function (event) { if (event.target === modal) { closeFullscreen(); } }; } // Close on ESC key document.addEventListener('keydown', function (event) { if (event.key === 'Escape') { closeFullscreen(); } }); // Add click handlers to extracted canvas const extractedCanvas = document.getElementById('extractedCanvas'); if (extractedCanvas) { extractedCanvas.addEventListener('click', function () { openFullscreen(this.toDataURL()); }); } // Add click handler to best match image const bestMatchImage = document.getElementById('bestMatchImage'); if (bestMatchImage) { bestMatchImage.addEventListener('click', function () { if (this.src) { openFullscreen(this.src); } }); } // Watch for top matches being added dynamically const topMatchesGrid = document.getElementById('topMatchesGrid'); if (topMatchesGrid) { const observer = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { if (mutation.addedNodes.length) { addFullscreenToTopMatches(); } }); }); observer.observe(topMatchesGrid, { childList: true, subtree: true }); } });