feature-search / static /bbox-drawing.js
Mateen Ahmed
App v1
b831214
// 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 = `
<img src="${match.image}" alt="${match.filename}" class="match-thumbnail">
<div class="match-info-overlay">
<span class="match-rank">#${index + 1}</span>
<span class="match-name">${match.filename}</span>
<span class="match-score">${match.score} matches</span>
</div>
`;
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
});
}
});