imageanlysisv2 / index.html
Subham9126's picture
Update index.html
21dcea0 verified
Raw
History Blame Contribute Delete
35.2 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Space Analyzer with TF.js</title>
<script src="https://cdn.tailwindcss.com"></script>
<!-- TensorFlow.js and COCO-SSD model -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest/dist/tf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/coco-ssd@latest/dist/coco-ssd.min.js"></script>
<style>
body { display: flex; flex-direction: column; min-height: 100vh; font-family: sans-serif; }
main { flex-grow: 1; display: flex; flex-direction: column; /* Mobile first */ }
@media (min-width: 768px) { /* md breakpoint */
main { flex-direction: row; }
}
#imageSection { /* On md and up, takes up ~70% */ }
#analysisSection { /* On md and up, takes up ~30% */ }
#imageDisplayContainer { position: relative; margin: auto; line-height: 0; }
#imageCanvas, #selectionCanvas { position: absolute; top: 0; left: 0; display: block; }
#selectionCanvas { z-index: 10; cursor: crosshair; }
#imageWrapper {
max-height: 80vh; display: flex; justify-content: center; align-items: center;
overflow: hidden; background-color: #e2e8f0; /* bg-gray-300 */
}
.loader {
border: 5px solid #f3f3f3; border-top: 5px solid #3498db;
border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
</head>
<body class="bg-gray-100">
<header class="bg-indigo-700 text-white p-4 shadow-md">
<h1 class="text-2xl sm:text-3xl font-bold text-center">Image Space Analyzer with TF.js</h1>
</header>
<main class="container mx-auto mt-4 p-0 sm:p-2 md:p-4 flex-grow w-full max-w-7xl">
<!-- Left Section: Image Input and Display -->
<section id="imageSection" class="w-full md:w-7/12 lg:w-8/12 p-2 md:pr-4 flex flex-col">
<div class="bg-white shadow-lg rounded-lg p-4 sm:p-6 mb-4">
<label for="imageUrlInput" class="block text-sm font-medium text-gray-700 mb-1">Image URL:</label>
<div class="flex">
<input type="url" id="imageUrlInput" name="imageUrlInput"
class="flex-grow p-2 border border-gray-300 rounded-l-md focus:ring-indigo-500 focus:border-indigo-500 text-sm"
placeholder="Enter image URL">
<button id="loadImageBtn"
class="bg-indigo-600 text-white px-3 sm:px-4 py-2 rounded-r-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 text-sm">
Load URL
</button>
</div>
<div class="mt-2 text-sm">
<label for="imageUploadInput" class="text-indigo-600 hover:text-indigo-500 cursor-pointer font-medium">
Or upload an image from your device
</label>
<input type="file" id="imageUploadInput" accept="image/*" class="hidden">
</div>
<p id="imageLoadError" class="text-red-600 text-sm mt-2 font-medium"></p>
</div>
<div id="imageWrapper" class="bg-gray-200 rounded-lg flex justify-center items-center flex-grow" style="min-height: 300px;">
<div id="imageDisplayContainer" class="hidden">
<canvas id="imageCanvas"></canvas>
<canvas id="selectionCanvas"></canvas>
</div>
<p id="imagePlaceholder" class="text-gray-500 p-4 text-center">Load an image to start selection.</p>
</div>
</section>
<!-- Right Section: Controls and Analysis Results -->
<section id="analysisSection" class="w-full md:w-5/12 lg:w-4/12 p-2 md:pl-0">
<div class="bg-white shadow-lg rounded-lg p-4 sm:p-6 sticky top-4">
<h2 class="text-xl font-semibold text-gray-700 mb-4">Controls & Analysis:</h2>
<div id="tfControls" class="mb-4 hidden">
<h3 class="text-md font-semibold text-gray-600 mb-2">Object Detection (TF.js)</h3>
<button id="detectObjectsBtn" class="w-full bg-teal-500 text-white px-4 py-2 rounded-md hover:bg-teal-600 focus:outline-none text-sm mb-2">
Detect Objects
</button>
<div id="tfModelStatus" class="text-xs text-gray-500 text-center">Loading TF.js model... <div class="loader inline-block w-4 h-4 border-2 border-t-teal-500"></div></div>
<label class="flex items-center mt-1 text-xs text-gray-600">
<input type="checkbox" id="mergeDetectionsCheckbox" class="form-checkbox h-4 w-4 text-teal-600 mr-2" checked>
Merge with existing manual selections
</label>
<label class="flex items-center mt-1 text-xs text-gray-600">
Min. Confidence: <input type="number" id="minConfidenceInput" value="0.5" min="0.1" max="1.0" step="0.1" class="ml-1 p-0.5 border rounded w-16 text-xs">
</label>
</div>
<div class="mb-4">
<h3 class="text-md font-semibold text-gray-600 mb-2">Selection Management</h3>
<button id="clearAllSelectionsBtn" class="w-full bg-red-500 text-white px-4 py-2 rounded-md hover:bg-red-600 focus:outline-none text-sm mb-2 hidden">Clear All Selections</button>
<button id="deleteActiveSelectionBtn" class="w-full bg-yellow-500 text-black px-4 py-2 rounded-md hover:bg-yellow-600 focus:outline-none text-sm hidden">Delete Active Selection</button>
</div>
<div id="resultsPanel" class="space-y-3">
<h3 class="text-md font-semibold text-gray-600 mb-2">Space Analysis</h3>
<div>
<p class="text-gray-600 text-sm">Total Image Area:</p>
<p id="totalAreaDisp" class="text-lg font-medium text-gray-800">- px²</p>
</div>
<div>
<p class="text-gray-600 text-sm">Total Selected Area:</p>
<p id="selectedAreaDisp" class="text-lg font-medium text-gray-800">- px²</p>
</div>
<div>
<p class="text-gray-600 text-sm">Selected Region (% of Total):</p>
<p id="percentageSelectedDisp" class="text-lg font-medium text-indigo-600">- %</p>
</div>
<div>
<p class="text-gray-600 text-sm">Extra Space (% of Total):</p>
<p id="percentageExtraDisp" class="text-lg font-medium text-green-600">- %</p>
</div>
<div>
<p class="text-gray-600 text-sm">Number of Selections:</p>
<p id="numSelectionsDisp" class="text-lg font-medium text-gray-800">0</p>
</div>
</div>
</div>
</section>
</main>
<script>
// --- Global State & Config ---
let cocoSsdModel = null;
let currentImage = null; // HTMLImageElement
let originalImgWidth = 0;
let originalImgHeight = 0;
let displayWidth = 0; // Actual width of canvas on screen
let displayHeight = 0; // Actual height of canvas on screen
let canvasScaleFactor = 1; // displayWidth / originalImgWidth
let selections = []; // Array of selection objects: { id, x, y, width, height, originalAspectRatio, type, label }
let nextSelectionId = 0;
let activeSelectionId = null;
let isDrawingNewSelection = false;
let currentDrawingRect = null; // {startX, startY, endX, endY} for new selection
let dragMode = null; // 'move', 'resize-tl', etc.
let dragStartPoint = null; // {x, y} on canvas
let dragSelectionInitialState = null;
const HANDLE_SIZE = 8;
const HANDLE_INTERACTION_RADIUS = HANDLE_SIZE + 3; // Slightly larger for easier click/hover
// --- DOM Elements ---
const imageUrlInputEl = document.getElementById('imageUrlInput');
const loadImageBtnEl = document.getElementById('loadImageBtn');
const imageUploadInputEl = document.getElementById('imageUploadInput');
const imageLoadErrorEl = document.getElementById('imageLoadError');
const imageWrapperEl = document.getElementById('imageWrapper');
const imageDisplayContainerEl = document.getElementById('imageDisplayContainer');
const imageCanvasEl = document.getElementById('imageCanvas');
const selectionCanvasEl = document.getElementById('selectionCanvas');
const imagePlaceholderEl = document.getElementById('imagePlaceholder');
const imageCtx = imageCanvasEl.getContext('2d');
const selectionCtx = selectionCanvasEl.getContext('2d');
const tfControlsEl = document.getElementById('tfControls');
const detectObjectsBtnEl = document.getElementById('detectObjectsBtn');
const tfModelStatusEl = document.getElementById('tfModelStatus');
const mergeDetectionsCheckboxEl = document.getElementById('mergeDetectionsCheckbox');
const minConfidenceInputEl = document.getElementById('minConfidenceInput');
const clearAllSelectionsBtnEl = document.getElementById('clearAllSelectionsBtn');
const deleteActiveSelectionBtnEl = document.getElementById('deleteActiveSelectionBtn');
const totalAreaDispEl = document.getElementById('totalAreaDisp');
const selectedAreaDispEl = document.getElementById('selectedAreaDisp');
const percentageSelectedDispEl = document.getElementById('percentageSelectedDisp');
const percentageExtraDispEl = document.getElementById('percentageExtraDisp');
const numSelectionsDispEl = document.getElementById('numSelectionsDisp');
// --- TF.js Model Loading ---
async function loadCocoSsdModel() {
try {
tfModelStatusEl.innerHTML = 'Loading TF.js COCO-SSD model... <div class="loader inline-block w-4 h-4 border-2 border-t-teal-500"></div>';
detectObjectsBtnEl.disabled = true;
cocoSsdModel = await cocoSsd.load();
tfModelStatusEl.textContent = 'TF.js Model Ready.';
detectObjectsBtnEl.disabled = false;
console.log('COCO-SSD model loaded.');
} catch (error) {
console.error('Error loading COCO-SSD model:', error);
tfModelStatusEl.textContent = 'Error loading TF.js model.';
tfModelStatusEl.classList.add('text-red-500');
}
}
// --- Image Handling ---
function displayImageLoadError(message) {
imageLoadErrorEl.textContent = message;
}
function clearImageLoadError() {
imageLoadErrorEl.textContent = '';
}
function resetImageArea() {
currentImage = null;
originalImgWidth = 0; originalImgHeight = 0;
displayWidth = 0; displayHeight = 0;
imageDisplayContainerEl.classList.add('hidden');
imagePlaceholderEl.classList.remove('hidden');
imagePlaceholderEl.textContent = 'Load an image to start selection.';
tfControlsEl.classList.add('hidden');
clearAllSelections(); // Also updates analysis
}
function loadImageFromUrl(url) {
clearImageLoadError();
const img = new Image();
img.crossOrigin = "Anonymous"; // Crucial for drawing external images to canvas for TF.js
img.onload = () => {
currentImage = img;
setupImageAndCanvases();
};
img.onerror = () => {
displayImageLoadError('Failed to load image from URL. Check URL and CORS policy.');
resetImageArea();
};
img.src = url;
}
function loadImageFromFile(file) {
clearImageLoadError();
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
currentImage = img;
setupImageAndCanvases();
};
img.onerror = () => {
displayImageLoadError('Failed to process uploaded image file.');
resetImageArea();
};
img.src = e.target.result;
};
reader.onerror = () => {
displayImageLoadError('Failed to read file.');
resetImageArea();
};
reader.readAsDataURL(file);
}
function setupImageAndCanvases() {
if (!currentImage) return;
originalImgWidth = currentImage.naturalWidth;
originalImgHeight = currentImage.naturalHeight;
const wrapperStyles = getComputedStyle(imageWrapperEl);
const maxDisplayWidth = imageWrapperEl.clientWidth - parseFloat(wrapperStyles.paddingLeft) - parseFloat(wrapperStyles.paddingRight);
const maxAllowedDisplayHeight = window.innerHeight * 0.80;
let scaleX = maxDisplayWidth / originalImgWidth;
let scaleY = maxAllowedDisplayHeight / originalImgHeight;
canvasScaleFactor = Math.min(1, scaleX, scaleY); // Don't scale up
displayWidth = originalImgWidth * canvasScaleFactor;
displayHeight = originalImgHeight * canvasScaleFactor;
imageCanvasEl.width = displayWidth;
imageCanvasEl.height = displayHeight;
selectionCanvasEl.width = displayWidth;
selectionCanvasEl.height = displayHeight;
imageDisplayContainerEl.style.width = `${displayWidth}px`;
imageDisplayContainerEl.style.height = `${displayHeight}px`;
imageCtx.drawImage(currentImage, 0, 0, displayWidth, displayHeight);
imagePlaceholderEl.classList.add('hidden');
imageDisplayContainerEl.classList.remove('hidden');
tfControlsEl.classList.remove('hidden');
clearAllSelections(); // Clear selections from any previous image and update analysis
updateAnalysisDisplay(); // Ensure total area is shown
}
// --- Selection Logic (similar to previous JS, adapted) ---
function getMousePos(canvas, evt) {
const rect = canvas.getBoundingClientRect();
return { x: evt.clientX - rect.left, y: evt.clientY - rect.top };
}
selectionCanvasEl.addEventListener('mousedown', (e) => {
if (!currentImage) return;
const pos = getMousePos(selectionCanvasEl, e);
dragStartPoint = pos;
for (let i = selections.length - 1; i >= 0; i--) { // Check active first, then others
const sel = selections[i];
const handleType = (activeSelectionId === sel.id) ? getHandleAtPos(pos, sel) : null; // Only check handles of active
if (handleType) {
// activeSelectionId is already sel.id
dragMode = handleType;
dragSelectionInitialState = { ...sel };
isDrawingNewSelection = false; currentDrawingRect = null;
drawSelections(); return;
}
if (isPointInRect(pos, sel)) {
activeSelectionId = sel.id; // Make this selection active
dragMode = 'move';
dragSelectionInitialState = { ...sel };
isDrawingNewSelection = false; currentDrawingRect = null;
// Optional: Bring to front (visual only, if needed for overlap)
// selections.push(selections.splice(selections.findIndex(s => s.id === sel.id), 1)[0]);
drawSelections(); return;
}
}
isDrawingNewSelection = true;
currentDrawingRect = { startX: pos.x, startY: pos.y, endX: pos.x, endY: pos.y };
activeSelectionId = null; // New drawing deactivates others
dragMode = null;
drawSelections();
});
selectionCanvasEl.addEventListener('mousemove', (e) => {
if (!currentImage) return;
const pos = getMousePos(selectionCanvasEl, e);
if (isDrawingNewSelection && currentDrawingRect) {
currentDrawingRect.endX = pos.x;
currentDrawingRect.endY = pos.y;
} else if (dragMode && activeSelectionId !== null && dragSelectionInitialState) {
const activeSel = selections.find(s => s.id === activeSelectionId);
if (!activeSel) return;
const dx = pos.x - dragStartPoint.x;
const dy = pos.y - dragStartPoint.y;
let { x: newX, y: newY, width: newWidth, height: newHeight } = dragSelectionInitialState;
if (dragMode === 'move') {
newX += dx; newY += dy;
} else { // Resizing
const orig = dragSelectionInitialState;
const aspect = orig.originalAspectRatio;
if (dragMode.includes('l')) { newWidth = orig.width - dx; newX = orig.x + dx; }
if (dragMode.includes('r')) { newWidth = orig.width + dx; }
if (dragMode.includes('t')) { newHeight = orig.height - dy; newY = orig.y + dy; }
if (dragMode.includes('b')) { newHeight = orig.height + dy; }
if (dragMode === 'resize-tl' || dragMode === 'resize-tr' || dragMode === 'resize-bl' || dragMode === 'resize-br') {
if (newWidth / (newHeight + 1e-6) > aspect) newWidth = newHeight * aspect;
else newHeight = newWidth / (aspect + 1e-6);
if (dragMode.includes('l')) newX = orig.x + (orig.width - newWidth);
if (dragMode.includes('t')) newY = orig.y + (orig.height - newHeight);
}
}
if (newWidth < 0) { newX += newWidth; newWidth = -newWidth; }
if (newHeight < 0) { newY += newHeight; newHeight = -newHeight; }
// Constrain to canvas bounds
if (dragMode === 'move') {
newX = Math.max(0, Math.min(newX, displayWidth - newWidth));
newY = Math.max(0, Math.min(newY, displayHeight - newHeight));
} else { // Resizing boundary check
if (newX < 0) { newWidth += newX; newX = 0; }
if (newY < 0) { newHeight += newY; newY = 0; }
if (newX + newWidth > displayWidth) newWidth = displayWidth - newX;
if (newY + newHeight > displayHeight) newHeight = displayHeight - newY;
// Simplified aspect re-apply after boundary constraint for corners
if ((dragMode === 'resize-tl' || dragMode === 'resize-tr' || dragMode === 'resize-bl' || dragMode === 'resize-br') && newWidth > 0 && newHeight > 0) {
const currentAspect = newWidth / (newHeight + 1e-6);
const originalAspect = dragSelectionInitialState.originalAspectRatio;
if (Math.abs(currentAspect - originalAspect) > 0.01) {
if (dragMode.includes('l') || dragMode.includes('r')) newHeight = newWidth / (originalAspect + 1e-6);
else newWidth = newHeight * originalAspect;
if (dragMode.includes('l')) newX = dragSelectionInitialState.x + dragSelectionInitialState.width - newWidth;
if (dragMode.includes('t')) newY = dragSelectionInitialState.y + dragSelectionInitialState.height - newHeight;
// Final boundary re-check
if (newX < 0) { newWidth += newX; newX = 0; }
if (newY < 0) { newHeight += newY; newY = 0; }
if (newX + newWidth > displayWidth) newWidth = displayWidth - newX;
if (newY + newHeight > displayHeight) newHeight = displayHeight - newY;
}
}
}
activeSel.x = newX; activeSel.y = newY;
activeSel.width = Math.max(0, newWidth); activeSel.height = Math.max(0, newHeight);
} else {
updateCursorStyle(pos);
}
drawSelections();
if (dragMode || isDrawingNewSelection) updateAnalysisDisplay();
});
selectionCanvasEl.addEventListener('mouseup', (e) => {
if (!currentImage) return;
if (isDrawingNewSelection && currentDrawingRect) {
let x = Math.min(currentDrawingRect.startX, currentDrawingRect.endX);
let y = Math.min(currentDrawingRect.startY, currentDrawingRect.endY);
let width = Math.abs(currentDrawingRect.startX - currentDrawingRect.endX);
let height = Math.abs(currentDrawingRect.startY - currentDrawingRect.endY);
if (width > HANDLE_SIZE / 2 && height > HANDLE_SIZE / 2) {
const newId = nextSelectionId++;
selections.push({
id: newId, x, y, width, height,
originalAspectRatio: width / (height + 1e-6),
type: 'manual', label: `Manual ${newId}`
});
activeSelectionId = newId;
}
} else if (dragMode && activeSelectionId !== null) {
const activeSel = selections.find(s => s.id === activeSelectionId);
if (activeSel) {
// Update aspect ratio if edge drag changed it
if (!dragMode.startsWith('resize-t') && !dragMode.startsWith('resize-b') && !dragMode.startsWith('resize-l') && !dragMode.startsWith('resize-r')) {
// This is for corner drags or move. For edge drags:
if (dragMode === 'resize-t' || dragMode === 'resize-b' || dragMode === 'resize-l' || dragMode === 'resize-r') {
if (activeSel.width > 0 && activeSel.height > 0) activeSel.originalAspectRatio = activeSel.width / (activeSel.height + 1e-6);
}
}
if (activeSel.width < HANDLE_SIZE / 2 || activeSel.height < HANDLE_SIZE / 2) { // Delete if too small
deleteSelectionById(activeSelectionId); // This will nullify activeSelectionId
}
}
}
isDrawingNewSelection = false; currentDrawingRect = null; dragMode = null;
updateCursorStyle(getMousePos(selectionCanvasEl, e));
drawSelections();
updateAnalysisDisplay();
updateActionButtonsVisibility();
});
selectionCanvasEl.addEventListener('mouseleave', (e) => {
if (isDrawingNewSelection || dragMode) {
selectionCanvasEl.dispatchEvent(new MouseEvent('mouseup', e)); // Finalize action
}
selectionCanvasEl.style.cursor = 'crosshair';
});
function getHandlePositions(sel) { /* ... same as previous full JS ... */
return [
{ type: 'resize-tl', x: sel.x, y: sel.y }, { type: 'resize-t', x: sel.x + sel.width / 2, y: sel.y },
{ type: 'resize-tr', x: sel.x + sel.width, y: sel.y }, { type: 'resize-l', x: sel.x, y: sel.y + sel.height / 2 },
{ type: 'resize-r', x: sel.x + sel.width, y: sel.y + sel.height / 2 }, { type: 'resize-bl', x: sel.x, y: sel.y + sel.height },
{ type: 'resize-b', x: sel.x + sel.width / 2, y: sel.y + sel.height }, { type: 'resize-br', x: sel.x + sel.width, y: sel.y + sel.height },
];
}
function getHandleAtPos(pos, sel) { /* ... same as previous full JS ... */
const handles = getHandlePositions(sel);
for (const handle of handles) {
if (pos.x >= handle.x - HANDLE_INTERACTION_RADIUS && pos.x <= handle.x + HANDLE_INTERACTION_RADIUS &&
pos.y >= handle.y - HANDLE_INTERACTION_RADIUS && pos.y <= handle.y + HANDLE_INTERACTION_RADIUS) {
return handle.type;
}
}
return null;
}
function isPointInRect(point, rect) { /* ... same as previous full JS ... */
return point.x >= rect.x && point.x <= rect.x + rect.width &&
point.y >= rect.y && point.y <= rect.y + rect.height;
}
function updateCursorStyle(pos) { /* ... same as previous full JS ... */
if (!currentImage) { selectionCanvasEl.style.cursor = 'default'; return; }
if (dragMode) {
if (dragMode === 'move') selectionCanvasEl.style.cursor = 'grabbing';
else if (dragMode === 'resize-tl' || dragMode === 'resize-br') selectionCanvasEl.style.cursor = 'nwse-resize';
else if (dragMode === 'resize-tr' || dragMode === 'resize-bl') selectionCanvasEl.style.cursor = 'nesw-resize';
else if (dragMode === 'resize-t' || dragMode === 'resize-b') selectionCanvasEl.style.cursor = 'ns-resize';
else if (dragMode === 'resize-l' || dragMode === 'resize-r') selectionCanvasEl.style.cursor = 'ew-resize';
return;
}
const activeSel = selections.find(s => s.id === activeSelectionId);
if (activeSel) {
const handleType = getHandleAtPos(pos, activeSel);
if (handleType) {
if (handleType.startsWith('resize-t') || handleType.startsWith('resize-b')) selectionCanvasEl.style.cursor = 'ns-resize';
else if (handleType.startsWith('resize-l') || handleType.startsWith('resize-r')) selectionCanvasEl.style.cursor = 'ew-resize';
else if (handleType === 'resize-tl' || handleType === 'resize-br') selectionCanvasEl.style.cursor = 'nwse-resize';
else if (handleType === 'resize-tr' || handleType === 'resize-bl') selectionCanvasEl.style.cursor = 'nesw-resize';
return;
}
if (isPointInRect(pos, activeSel)) { selectionCanvasEl.style.cursor = 'move'; return; }
}
for (const sel of selections) { // Check hover over non-active selections
if (sel.id !== activeSelectionId && isPointInRect(pos, sel)) { selectionCanvasEl.style.cursor = 'pointer'; return; }
}
selectionCanvasEl.style.cursor = 'crosshair';
}
// --- Drawing ---
function drawSelections() {
selectionCtx.clearRect(0, 0, displayWidth, displayHeight);
selections.forEach(sel => {
selectionCtx.strokeStyle = (sel.id === activeSelectionId) ? 'blue' : (sel.type === 'tfjs' ? 'green' : 'rgba(0, 123, 255, 0.9)');
selectionCtx.fillStyle = (sel.id === activeSelectionId) ? 'rgba(0, 123, 255, 0.3)' : (sel.type === 'tfjs' ? 'rgba(0,255,0,0.2)' : 'rgba(0, 123, 255, 0.2)');
selectionCtx.lineWidth = (sel.id === activeSelectionId) ? 2 : 1.5;
selectionCtx.beginPath();
selectionCtx.rect(sel.x, sel.y, sel.width, sel.height);
selectionCtx.fill();
selectionCtx.stroke();
if (sel.label) {
selectionCtx.fillStyle = (sel.id === activeSelectionId) ? 'blue' : (sel.type === 'tfjs' ? 'darkgreen' : 'black');
selectionCtx.font = "10px Arial";
const labelX = sel.x + 2;
const labelY = sel.y > 12 ? sel.y + 10 : sel.y + sel.height + 10; // Position label inside or below
selectionCtx.fillText(sel.label, labelX, labelY);
}
if (sel.id === activeSelectionId) drawHandles(sel);
});
if (isDrawingNewSelection && currentDrawingRect) {
selectionCtx.strokeStyle = 'rgba(0, 200, 0, 0.9)';
selectionCtx.fillStyle = 'rgba(0, 200, 0, 0.2)';
selectionCtx.lineWidth = 1.5;
let x = Math.min(currentDrawingRect.startX, currentDrawingRect.endX);
let y = Math.min(currentDrawingRect.startY, currentDrawingRect.endY);
let width = Math.abs(currentDrawingRect.startX - currentDrawingRect.endX);
let height = Math.abs(currentDrawingRect.startY - currentDrawingRect.endY);
selectionCtx.beginPath();
selectionCtx.rect(x, y, width, height);
selectionCtx.fill();
selectionCtx.stroke();
}
}
function drawHandles(sel) { /* ... same as previous full JS ... */
const handles = getHandlePositions(sel);
selectionCtx.fillStyle = 'blue'; // Solid blue for handles
selectionCtx.strokeStyle = 'white';
selectionCtx.lineWidth = 1;
handles.forEach(h => {
selectionCtx.beginPath();
selectionCtx.rect(h.x - HANDLE_SIZE / 2, h.y - HANDLE_SIZE / 2, HANDLE_SIZE, HANDLE_SIZE);
selectionCtx.fill();
selectionCtx.stroke();
});
}
// --- TF.js Object Detection ---
detectObjectsBtnEl.addEventListener('click', async () => {
if (!currentImage || !cocoSsdModel) {
alert('Please load an image and wait for the TF.js model to be ready.');
return;
}
detectObjectsBtnEl.disabled = true;
detectObjectsBtnEl.innerHTML = 'Detecting... <div class="loader inline-block w-4 h-4 border-2 border-t-white"></div>';
try {
// COCO-SSD expects an HTMLImageElement, HTMLVideoElement, HTMLCanvasElement, or ImageData
// We can pass the original HTMLImageElement `currentImage` or the `imageCanvasEl`
// Passing `currentImage` is usually better as it's the original data.
const predictions = await cocoSsdModel.detect(currentImage);
if (!mergeDetectionsCheckboxEl.checked) { // Remove previous TF.js detections
selections = selections.filter(s => s.type !== 'tfjs');
}
const minConfidence = parseFloat(minConfidenceInputEl.value) || 0.5;
predictions.forEach(p => {
if (p.score >= minConfidence) {
const [xOrig, yOrig, widthOrig, heightOrig] = p.bbox;
// Convert original image coordinates to display canvas coordinates
const dispX = xOrig * canvasScaleFactor;
const dispY = yOrig * canvasScaleFactor;
const dispWidth = widthOrig * canvasScaleFactor;
const dispHeight = heightOrig * canvasScaleFactor;
selections.push({
id: nextSelectionId++,
x: dispX, y: dispY, width: dispWidth, height: dispHeight,
originalAspectRatio: dispWidth / (dispHeight + 1e-6),
type: 'tfjs',
label: `${p.class} (${Math.round(p.score * 100)}%)`
});
}
});
activeSelectionId = null; // Deactivate any current selection after adding new ones
} catch (error) {
console.error('Error during TF.js detection:', error);
alert('An error occurred during object detection.');
} finally {
detectObjectsBtnEl.disabled = false;
detectObjectsBtnEl.textContent = 'Detect Objects';
drawSelections();
updateAnalysisDisplay();
updateActionButtonsVisibility();
}
});
// --- Selection Management & Analysis ---
function deleteSelectionById(idToDelete) {
selections = selections.filter(sel => sel.id !== idToDelete);
if (activeSelectionId === idToDelete) {
activeSelectionId = null;
}
// No need to redraw/update here, caller will do it
}
deleteActiveSelectionBtnEl.addEventListener('click', () => {
if (activeSelectionId !== null) {
deleteSelectionById(activeSelectionId);
// activeSelectionId is now null
drawSelections();
updateAnalysisDisplay();
updateActionButtonsVisibility();
}
});
clearAllSelectionsBtnEl.addEventListener('click', clearAllSelections);
function clearAllSelections() {
selections = [];
activeSelectionId = null;
// nextSelectionId can be reset if desired, or keep incrementing for true uniqueness over session
if (selectionCtx && displayWidth > 0) drawSelections(); // Clears canvas
updateAnalysisDisplay();
updateActionButtonsVisibility();
}
function updateActionButtonsVisibility() {
clearAllSelectionsBtnEl.classList.toggle('hidden', selections.length === 0);
deleteActiveSelectionBtnEl.classList.toggle('hidden', activeSelectionId === null || selections.length === 0);
}
function updateAnalysisDisplay() {
if (!currentImage) {
totalAreaDispEl.textContent = '- px²';
selectedAreaDispEl.textContent = '- px²';
percentageSelectedDispEl.textContent = '- %';
percentageExtraDispEl.textContent = '- %';
numSelectionsDispEl.textContent = '0';
return;
}
const totalImgAreaPx = originalImgWidth * originalImgHeight;
totalAreaDispEl.textContent = `${Math.round(totalImgAreaPx).toLocaleString()} px²`;
let totalSelectedOriginalPxArea = 0;
selections.forEach(sel => {
// Convert selection display dimensions back to original image pixel dimensions
const originalSelWidth = sel.width / canvasScaleFactor;
const originalSelHeight = sel.height / canvasScaleFactor;
totalSelectedOriginalPxArea += originalSelWidth * originalSelHeight;
});
selectedAreaDispEl.textContent = `${Math.round(totalSelectedOriginalPxArea).toLocaleString()} px²`;
numSelectionsDispEl.textContent = selections.length.toString();
if (totalImgAreaPx > 0 && totalSelectedOriginalPxArea >= 0) {
const percSelected = (totalSelectedOriginalPxArea / totalImgAreaPx) * 100;
const percExtra = 100 - percSelected;
percentageSelectedDispEl.textContent = `${percSelected.toFixed(1)} %`;
percentageExtraDispEl.textContent = `${percExtra.toFixed(1)} %`;
} else {
percentageSelectedDispEl.textContent = '0.0 %';
percentageExtraDispEl.textContent = '100.0 %';
}
}
// --- Event Listeners for Image Load ---
loadImageBtnEl.addEventListener('click', () => {
const url = imageUrlInputEl.value.trim();
if (url) {
imagePlaceholderEl.textContent = 'Loading image from URL...';
loadImageFromUrl(url);
} else {
displayImageLoadError('Please enter an image URL.');
}
});
imageUploadInputEl.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
imagePlaceholderEl.textContent = 'Loading image from file...';
loadImageFromFile(file);
}
event.target.value = null; // Reset file input
});
// --- Initial Setup ---
document.addEventListener('DOMContentLoaded', () => {
resetImageArea(); // Set initial placeholder state
loadCocoSsdModel(); // Start loading TF.js model immediately
updateActionButtonsVisibility(); // Initial button states
});
</script>
</body>
</html>