Subham9126 commited on
Commit
21dcea0
·
verified ·
1 Parent(s): 397f031

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +684 -21
index.html CHANGED
@@ -1,29 +1,692 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
3
-
4
  <head>
5
- <meta charset="UTF-8" />
6
- <link rel="stylesheet" href="style.css" />
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
9
- <title>Transformers.js - Object Detection</title>
 
 
 
 
 
 
 
 
 
 
 
 
10
  </head>
 
11
 
12
- <body>
13
- <h1>Object Detection w/ 🤗 Transformers.js</h1>
14
- <label id="container" for="upload">
15
- <svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
16
- <path fill="#000"
17
- d="M3.5 24.3a3 3 0 0 1-1.9-.8c-.5-.5-.8-1.2-.8-1.9V2.9c0-.7.3-1.3.8-1.9.6-.5 1.2-.7 2-.7h18.6c.7 0 1.3.2 1.9.7.5.6.7 1.2.7 2v18.6c0 .7-.2 1.4-.7 1.9a3 3 0 0 1-2 .8H3.6Zm0-2.7h18.7V2.9H3.5v18.7Zm2.7-2.7h13.3c.3 0 .5 0 .6-.3v-.7l-3.7-5a.6.6 0 0 0-.6-.2c-.2 0-.4 0-.5.3l-3.5 4.6-2.4-3.3a.6.6 0 0 0-.6-.3c-.2 0-.4.1-.5.3l-2.7 3.6c-.1.2-.2.4 0 .7.1.2.3.3.6.3Z">
18
- </path>
19
- </svg>
20
- Click to upload image
21
- <label id="example">(or try example)</label>
22
- </label>
23
- <label id="status">Loading model...</label>
24
- <input id="upload" type="file" accept="image/*" />
25
-
26
- <script src="index.js" type="module"></script>
27
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="en">
 
3
  <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Image Space Analyzer with TF.js</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <!-- TensorFlow.js and COCO-SSD model -->
9
+ <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest/dist/tf.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/coco-ssd@latest/dist/coco-ssd.min.js"></script>
11
+ <style>
12
+ body { display: flex; flex-direction: column; min-height: 100vh; font-family: sans-serif; }
13
+ main { flex-grow: 1; display: flex; flex-direction: column; /* Mobile first */ }
14
+ @media (min-width: 768px) { /* md breakpoint */
15
+ main { flex-direction: row; }
16
+ }
17
+ #imageSection { /* On md and up, takes up ~70% */ }
18
+ #analysisSection { /* On md and up, takes up ~30% */ }
19
 
20
+ #imageDisplayContainer { position: relative; margin: auto; line-height: 0; }
21
+ #imageCanvas, #selectionCanvas { position: absolute; top: 0; left: 0; display: block; }
22
+ #selectionCanvas { z-index: 10; cursor: crosshair; }
23
+ #imageWrapper {
24
+ max-height: 80vh; display: flex; justify-content: center; align-items: center;
25
+ overflow: hidden; background-color: #e2e8f0; /* bg-gray-300 */
26
+ }
27
+ .loader {
28
+ border: 5px solid #f3f3f3; border-top: 5px solid #3498db;
29
+ border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite;
30
+ margin: 20px auto;
31
+ }
32
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
33
+ </style>
34
  </head>
35
+ <body class="bg-gray-100">
36
 
37
+ <header class="bg-indigo-700 text-white p-4 shadow-md">
38
+ <h1 class="text-2xl sm:text-3xl font-bold text-center">Image Space Analyzer with TF.js</h1>
39
+ </header>
40
+
41
+ <main class="container mx-auto mt-4 p-0 sm:p-2 md:p-4 flex-grow w-full max-w-7xl">
42
+ <!-- Left Section: Image Input and Display -->
43
+ <section id="imageSection" class="w-full md:w-7/12 lg:w-8/12 p-2 md:pr-4 flex flex-col">
44
+ <div class="bg-white shadow-lg rounded-lg p-4 sm:p-6 mb-4">
45
+ <label for="imageUrlInput" class="block text-sm font-medium text-gray-700 mb-1">Image URL:</label>
46
+ <div class="flex">
47
+ <input type="url" id="imageUrlInput" name="imageUrlInput"
48
+ class="flex-grow p-2 border border-gray-300 rounded-l-md focus:ring-indigo-500 focus:border-indigo-500 text-sm"
49
+ placeholder="Enter image URL">
50
+ <button id="loadImageBtn"
51
+ 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">
52
+ Load URL
53
+ </button>
54
+ </div>
55
+ <div class="mt-2 text-sm">
56
+ <label for="imageUploadInput" class="text-indigo-600 hover:text-indigo-500 cursor-pointer font-medium">
57
+ Or upload an image from your device
58
+ </label>
59
+ <input type="file" id="imageUploadInput" accept="image/*" class="hidden">
60
+ </div>
61
+ <p id="imageLoadError" class="text-red-600 text-sm mt-2 font-medium"></p>
62
+ </div>
63
+
64
+ <div id="imageWrapper" class="bg-gray-200 rounded-lg flex justify-center items-center flex-grow" style="min-height: 300px;">
65
+ <div id="imageDisplayContainer" class="hidden">
66
+ <canvas id="imageCanvas"></canvas>
67
+ <canvas id="selectionCanvas"></canvas>
68
+ </div>
69
+ <p id="imagePlaceholder" class="text-gray-500 p-4 text-center">Load an image to start selection.</p>
70
+ </div>
71
+ </section>
72
+
73
+ <!-- Right Section: Controls and Analysis Results -->
74
+ <section id="analysisSection" class="w-full md:w-5/12 lg:w-4/12 p-2 md:pl-0">
75
+ <div class="bg-white shadow-lg rounded-lg p-4 sm:p-6 sticky top-4">
76
+ <h2 class="text-xl font-semibold text-gray-700 mb-4">Controls & Analysis:</h2>
77
+
78
+ <div id="tfControls" class="mb-4 hidden">
79
+ <h3 class="text-md font-semibold text-gray-600 mb-2">Object Detection (TF.js)</h3>
80
+ <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">
81
+ Detect Objects
82
+ </button>
83
+ <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>
84
+ <label class="flex items-center mt-1 text-xs text-gray-600">
85
+ <input type="checkbox" id="mergeDetectionsCheckbox" class="form-checkbox h-4 w-4 text-teal-600 mr-2" checked>
86
+ Merge with existing manual selections
87
+ </label>
88
+ <label class="flex items-center mt-1 text-xs text-gray-600">
89
+ 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">
90
+ </label>
91
+ </div>
92
+
93
+ <div class="mb-4">
94
+ <h3 class="text-md font-semibold text-gray-600 mb-2">Selection Management</h3>
95
+ <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>
96
+ <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>
97
+ </div>
98
+
99
+ <div id="resultsPanel" class="space-y-3">
100
+ <h3 class="text-md font-semibold text-gray-600 mb-2">Space Analysis</h3>
101
+ <div>
102
+ <p class="text-gray-600 text-sm">Total Image Area:</p>
103
+ <p id="totalAreaDisp" class="text-lg font-medium text-gray-800">- px²</p>
104
+ </div>
105
+ <div>
106
+ <p class="text-gray-600 text-sm">Total Selected Area:</p>
107
+ <p id="selectedAreaDisp" class="text-lg font-medium text-gray-800">- px²</p>
108
+ </div>
109
+ <div>
110
+ <p class="text-gray-600 text-sm">Selected Region (% of Total):</p>
111
+ <p id="percentageSelectedDisp" class="text-lg font-medium text-indigo-600">- %</p>
112
+ </div>
113
+ <div>
114
+ <p class="text-gray-600 text-sm">Extra Space (% of Total):</p>
115
+ <p id="percentageExtraDisp" class="text-lg font-medium text-green-600">- %</p>
116
+ </div>
117
+ <div>
118
+ <p class="text-gray-600 text-sm">Number of Selections:</p>
119
+ <p id="numSelectionsDisp" class="text-lg font-medium text-gray-800">0</p>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </section>
124
+ </main>
125
+
126
+ <script>
127
+ // --- Global State & Config ---
128
+ let cocoSsdModel = null;
129
+ let currentImage = null; // HTMLImageElement
130
+ let originalImgWidth = 0;
131
+ let originalImgHeight = 0;
132
+ let displayWidth = 0; // Actual width of canvas on screen
133
+ let displayHeight = 0; // Actual height of canvas on screen
134
+ let canvasScaleFactor = 1; // displayWidth / originalImgWidth
135
+
136
+ let selections = []; // Array of selection objects: { id, x, y, width, height, originalAspectRatio, type, label }
137
+ let nextSelectionId = 0;
138
+ let activeSelectionId = null;
139
+
140
+ let isDrawingNewSelection = false;
141
+ let currentDrawingRect = null; // {startX, startY, endX, endY} for new selection
142
+ let dragMode = null; // 'move', 'resize-tl', etc.
143
+ let dragStartPoint = null; // {x, y} on canvas
144
+ let dragSelectionInitialState = null;
145
+
146
+ const HANDLE_SIZE = 8;
147
+ const HANDLE_INTERACTION_RADIUS = HANDLE_SIZE + 3; // Slightly larger for easier click/hover
148
+
149
+ // --- DOM Elements ---
150
+ const imageUrlInputEl = document.getElementById('imageUrlInput');
151
+ const loadImageBtnEl = document.getElementById('loadImageBtn');
152
+ const imageUploadInputEl = document.getElementById('imageUploadInput');
153
+ const imageLoadErrorEl = document.getElementById('imageLoadError');
154
+
155
+ const imageWrapperEl = document.getElementById('imageWrapper');
156
+ const imageDisplayContainerEl = document.getElementById('imageDisplayContainer');
157
+ const imageCanvasEl = document.getElementById('imageCanvas');
158
+ const selectionCanvasEl = document.getElementById('selectionCanvas');
159
+ const imagePlaceholderEl = document.getElementById('imagePlaceholder');
160
+ const imageCtx = imageCanvasEl.getContext('2d');
161
+ const selectionCtx = selectionCanvasEl.getContext('2d');
162
+
163
+ const tfControlsEl = document.getElementById('tfControls');
164
+ const detectObjectsBtnEl = document.getElementById('detectObjectsBtn');
165
+ const tfModelStatusEl = document.getElementById('tfModelStatus');
166
+ const mergeDetectionsCheckboxEl = document.getElementById('mergeDetectionsCheckbox');
167
+ const minConfidenceInputEl = document.getElementById('minConfidenceInput');
168
+
169
+ const clearAllSelectionsBtnEl = document.getElementById('clearAllSelectionsBtn');
170
+ const deleteActiveSelectionBtnEl = document.getElementById('deleteActiveSelectionBtn');
171
+
172
+ const totalAreaDispEl = document.getElementById('totalAreaDisp');
173
+ const selectedAreaDispEl = document.getElementById('selectedAreaDisp');
174
+ const percentageSelectedDispEl = document.getElementById('percentageSelectedDisp');
175
+ const percentageExtraDispEl = document.getElementById('percentageExtraDisp');
176
+ const numSelectionsDispEl = document.getElementById('numSelectionsDisp');
177
+
178
+
179
+ // --- TF.js Model Loading ---
180
+ async function loadCocoSsdModel() {
181
+ try {
182
+ tfModelStatusEl.innerHTML = 'Loading TF.js COCO-SSD model... <div class="loader inline-block w-4 h-4 border-2 border-t-teal-500"></div>';
183
+ detectObjectsBtnEl.disabled = true;
184
+ cocoSsdModel = await cocoSsd.load();
185
+ tfModelStatusEl.textContent = 'TF.js Model Ready.';
186
+ detectObjectsBtnEl.disabled = false;
187
+ console.log('COCO-SSD model loaded.');
188
+ } catch (error) {
189
+ console.error('Error loading COCO-SSD model:', error);
190
+ tfModelStatusEl.textContent = 'Error loading TF.js model.';
191
+ tfModelStatusEl.classList.add('text-red-500');
192
+ }
193
+ }
194
+
195
+ // --- Image Handling ---
196
+ function displayImageLoadError(message) {
197
+ imageLoadErrorEl.textContent = message;
198
+ }
199
+ function clearImageLoadError() {
200
+ imageLoadErrorEl.textContent = '';
201
+ }
202
+
203
+ function resetImageArea() {
204
+ currentImage = null;
205
+ originalImgWidth = 0; originalImgHeight = 0;
206
+ displayWidth = 0; displayHeight = 0;
207
+ imageDisplayContainerEl.classList.add('hidden');
208
+ imagePlaceholderEl.classList.remove('hidden');
209
+ imagePlaceholderEl.textContent = 'Load an image to start selection.';
210
+ tfControlsEl.classList.add('hidden');
211
+ clearAllSelections(); // Also updates analysis
212
+ }
213
+
214
+ function loadImageFromUrl(url) {
215
+ clearImageLoadError();
216
+ const img = new Image();
217
+ img.crossOrigin = "Anonymous"; // Crucial for drawing external images to canvas for TF.js
218
+ img.onload = () => {
219
+ currentImage = img;
220
+ setupImageAndCanvases();
221
+ };
222
+ img.onerror = () => {
223
+ displayImageLoadError('Failed to load image from URL. Check URL and CORS policy.');
224
+ resetImageArea();
225
+ };
226
+ img.src = url;
227
+ }
228
+
229
+ function loadImageFromFile(file) {
230
+ clearImageLoadError();
231
+ const reader = new FileReader();
232
+ reader.onload = (e) => {
233
+ const img = new Image();
234
+ img.onload = () => {
235
+ currentImage = img;
236
+ setupImageAndCanvases();
237
+ };
238
+ img.onerror = () => {
239
+ displayImageLoadError('Failed to process uploaded image file.');
240
+ resetImageArea();
241
+ };
242
+ img.src = e.target.result;
243
+ };
244
+ reader.onerror = () => {
245
+ displayImageLoadError('Failed to read file.');
246
+ resetImageArea();
247
+ };
248
+ reader.readAsDataURL(file);
249
+ }
250
+
251
+ function setupImageAndCanvases() {
252
+ if (!currentImage) return;
253
+ originalImgWidth = currentImage.naturalWidth;
254
+ originalImgHeight = currentImage.naturalHeight;
255
+
256
+ const wrapperStyles = getComputedStyle(imageWrapperEl);
257
+ const maxDisplayWidth = imageWrapperEl.clientWidth - parseFloat(wrapperStyles.paddingLeft) - parseFloat(wrapperStyles.paddingRight);
258
+ const maxAllowedDisplayHeight = window.innerHeight * 0.80;
259
+
260
+ let scaleX = maxDisplayWidth / originalImgWidth;
261
+ let scaleY = maxAllowedDisplayHeight / originalImgHeight;
262
+ canvasScaleFactor = Math.min(1, scaleX, scaleY); // Don't scale up
263
+
264
+ displayWidth = originalImgWidth * canvasScaleFactor;
265
+ displayHeight = originalImgHeight * canvasScaleFactor;
266
+
267
+ imageCanvasEl.width = displayWidth;
268
+ imageCanvasEl.height = displayHeight;
269
+ selectionCanvasEl.width = displayWidth;
270
+ selectionCanvasEl.height = displayHeight;
271
+
272
+ imageDisplayContainerEl.style.width = `${displayWidth}px`;
273
+ imageDisplayContainerEl.style.height = `${displayHeight}px`;
274
+
275
+ imageCtx.drawImage(currentImage, 0, 0, displayWidth, displayHeight);
276
+
277
+ imagePlaceholderEl.classList.add('hidden');
278
+ imageDisplayContainerEl.classList.remove('hidden');
279
+ tfControlsEl.classList.remove('hidden');
280
+
281
+ clearAllSelections(); // Clear selections from any previous image and update analysis
282
+ updateAnalysisDisplay(); // Ensure total area is shown
283
+ }
284
+
285
+ // --- Selection Logic (similar to previous JS, adapted) ---
286
+ function getMousePos(canvas, evt) {
287
+ const rect = canvas.getBoundingClientRect();
288
+ return { x: evt.clientX - rect.left, y: evt.clientY - rect.top };
289
+ }
290
+
291
+ selectionCanvasEl.addEventListener('mousedown', (e) => {
292
+ if (!currentImage) return;
293
+ const pos = getMousePos(selectionCanvasEl, e);
294
+ dragStartPoint = pos;
295
+
296
+ for (let i = selections.length - 1; i >= 0; i--) { // Check active first, then others
297
+ const sel = selections[i];
298
+ const handleType = (activeSelectionId === sel.id) ? getHandleAtPos(pos, sel) : null; // Only check handles of active
299
+ if (handleType) {
300
+ // activeSelectionId is already sel.id
301
+ dragMode = handleType;
302
+ dragSelectionInitialState = { ...sel };
303
+ isDrawingNewSelection = false; currentDrawingRect = null;
304
+ drawSelections(); return;
305
+ }
306
+ if (isPointInRect(pos, sel)) {
307
+ activeSelectionId = sel.id; // Make this selection active
308
+ dragMode = 'move';
309
+ dragSelectionInitialState = { ...sel };
310
+ isDrawingNewSelection = false; currentDrawingRect = null;
311
+ // Optional: Bring to front (visual only, if needed for overlap)
312
+ // selections.push(selections.splice(selections.findIndex(s => s.id === sel.id), 1)[0]);
313
+ drawSelections(); return;
314
+ }
315
+ }
316
+ isDrawingNewSelection = true;
317
+ currentDrawingRect = { startX: pos.x, startY: pos.y, endX: pos.x, endY: pos.y };
318
+ activeSelectionId = null; // New drawing deactivates others
319
+ dragMode = null;
320
+ drawSelections();
321
+ });
322
+
323
+ selectionCanvasEl.addEventListener('mousemove', (e) => {
324
+ if (!currentImage) return;
325
+ const pos = getMousePos(selectionCanvasEl, e);
326
 
327
+ if (isDrawingNewSelection && currentDrawingRect) {
328
+ currentDrawingRect.endX = pos.x;
329
+ currentDrawingRect.endY = pos.y;
330
+ } else if (dragMode && activeSelectionId !== null && dragSelectionInitialState) {
331
+ const activeSel = selections.find(s => s.id === activeSelectionId);
332
+ if (!activeSel) return;
333
+
334
+ const dx = pos.x - dragStartPoint.x;
335
+ const dy = pos.y - dragStartPoint.y;
336
+ let { x: newX, y: newY, width: newWidth, height: newHeight } = dragSelectionInitialState;
337
+
338
+ if (dragMode === 'move') {
339
+ newX += dx; newY += dy;
340
+ } else { // Resizing
341
+ const orig = dragSelectionInitialState;
342
+ const aspect = orig.originalAspectRatio;
343
+
344
+ if (dragMode.includes('l')) { newWidth = orig.width - dx; newX = orig.x + dx; }
345
+ if (dragMode.includes('r')) { newWidth = orig.width + dx; }
346
+ if (dragMode.includes('t')) { newHeight = orig.height - dy; newY = orig.y + dy; }
347
+ if (dragMode.includes('b')) { newHeight = orig.height + dy; }
348
+
349
+ if (dragMode === 'resize-tl' || dragMode === 'resize-tr' || dragMode === 'resize-bl' || dragMode === 'resize-br') {
350
+ if (newWidth / (newHeight + 1e-6) > aspect) newWidth = newHeight * aspect;
351
+ else newHeight = newWidth / (aspect + 1e-6);
352
+ if (dragMode.includes('l')) newX = orig.x + (orig.width - newWidth);
353
+ if (dragMode.includes('t')) newY = orig.y + (orig.height - newHeight);
354
+ }
355
+ }
356
+
357
+ if (newWidth < 0) { newX += newWidth; newWidth = -newWidth; }
358
+ if (newHeight < 0) { newY += newHeight; newHeight = -newHeight; }
359
+
360
+ // Constrain to canvas bounds
361
+ if (dragMode === 'move') {
362
+ newX = Math.max(0, Math.min(newX, displayWidth - newWidth));
363
+ newY = Math.max(0, Math.min(newY, displayHeight - newHeight));
364
+ } else { // Resizing boundary check
365
+ if (newX < 0) { newWidth += newX; newX = 0; }
366
+ if (newY < 0) { newHeight += newY; newY = 0; }
367
+ if (newX + newWidth > displayWidth) newWidth = displayWidth - newX;
368
+ if (newY + newHeight > displayHeight) newHeight = displayHeight - newY;
369
+ // Simplified aspect re-apply after boundary constraint for corners
370
+ if ((dragMode === 'resize-tl' || dragMode === 'resize-tr' || dragMode === 'resize-bl' || dragMode === 'resize-br') && newWidth > 0 && newHeight > 0) {
371
+ const currentAspect = newWidth / (newHeight + 1e-6);
372
+ const originalAspect = dragSelectionInitialState.originalAspectRatio;
373
+ if (Math.abs(currentAspect - originalAspect) > 0.01) {
374
+ if (dragMode.includes('l') || dragMode.includes('r')) newHeight = newWidth / (originalAspect + 1e-6);
375
+ else newWidth = newHeight * originalAspect;
376
+ if (dragMode.includes('l')) newX = dragSelectionInitialState.x + dragSelectionInitialState.width - newWidth;
377
+ if (dragMode.includes('t')) newY = dragSelectionInitialState.y + dragSelectionInitialState.height - newHeight;
378
+ // Final boundary re-check
379
+ if (newX < 0) { newWidth += newX; newX = 0; }
380
+ if (newY < 0) { newHeight += newY; newY = 0; }
381
+ if (newX + newWidth > displayWidth) newWidth = displayWidth - newX;
382
+ if (newY + newHeight > displayHeight) newHeight = displayHeight - newY;
383
+ }
384
+ }
385
+ }
386
+ activeSel.x = newX; activeSel.y = newY;
387
+ activeSel.width = Math.max(0, newWidth); activeSel.height = Math.max(0, newHeight);
388
+ } else {
389
+ updateCursorStyle(pos);
390
+ }
391
+ drawSelections();
392
+ if (dragMode || isDrawingNewSelection) updateAnalysisDisplay();
393
+ });
394
+
395
+ selectionCanvasEl.addEventListener('mouseup', (e) => {
396
+ if (!currentImage) return;
397
+ if (isDrawingNewSelection && currentDrawingRect) {
398
+ let x = Math.min(currentDrawingRect.startX, currentDrawingRect.endX);
399
+ let y = Math.min(currentDrawingRect.startY, currentDrawingRect.endY);
400
+ let width = Math.abs(currentDrawingRect.startX - currentDrawingRect.endX);
401
+ let height = Math.abs(currentDrawingRect.startY - currentDrawingRect.endY);
402
+
403
+ if (width > HANDLE_SIZE / 2 && height > HANDLE_SIZE / 2) {
404
+ const newId = nextSelectionId++;
405
+ selections.push({
406
+ id: newId, x, y, width, height,
407
+ originalAspectRatio: width / (height + 1e-6),
408
+ type: 'manual', label: `Manual ${newId}`
409
+ });
410
+ activeSelectionId = newId;
411
+ }
412
+ } else if (dragMode && activeSelectionId !== null) {
413
+ const activeSel = selections.find(s => s.id === activeSelectionId);
414
+ if (activeSel) {
415
+ // Update aspect ratio if edge drag changed it
416
+ if (!dragMode.startsWith('resize-t') && !dragMode.startsWith('resize-b') && !dragMode.startsWith('resize-l') && !dragMode.startsWith('resize-r')) {
417
+ // This is for corner drags or move. For edge drags:
418
+ if (dragMode === 'resize-t' || dragMode === 'resize-b' || dragMode === 'resize-l' || dragMode === 'resize-r') {
419
+ if (activeSel.width > 0 && activeSel.height > 0) activeSel.originalAspectRatio = activeSel.width / (activeSel.height + 1e-6);
420
+ }
421
+ }
422
+ if (activeSel.width < HANDLE_SIZE / 2 || activeSel.height < HANDLE_SIZE / 2) { // Delete if too small
423
+ deleteSelectionById(activeSelectionId); // This will nullify activeSelectionId
424
+ }
425
+ }
426
+ }
427
+ isDrawingNewSelection = false; currentDrawingRect = null; dragMode = null;
428
+ updateCursorStyle(getMousePos(selectionCanvasEl, e));
429
+ drawSelections();
430
+ updateAnalysisDisplay();
431
+ updateActionButtonsVisibility();
432
+ });
433
+
434
+ selectionCanvasEl.addEventListener('mouseleave', (e) => {
435
+ if (isDrawingNewSelection || dragMode) {
436
+ selectionCanvasEl.dispatchEvent(new MouseEvent('mouseup', e)); // Finalize action
437
+ }
438
+ selectionCanvasEl.style.cursor = 'crosshair';
439
+ });
440
+
441
+ function getHandlePositions(sel) { /* ... same as previous full JS ... */
442
+ return [
443
+ { type: 'resize-tl', x: sel.x, y: sel.y }, { type: 'resize-t', x: sel.x + sel.width / 2, y: sel.y },
444
+ { type: 'resize-tr', x: sel.x + sel.width, y: sel.y }, { type: 'resize-l', x: sel.x, y: sel.y + sel.height / 2 },
445
+ { 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 },
446
+ { 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 },
447
+ ];
448
+ }
449
+ function getHandleAtPos(pos, sel) { /* ... same as previous full JS ... */
450
+ const handles = getHandlePositions(sel);
451
+ for (const handle of handles) {
452
+ if (pos.x >= handle.x - HANDLE_INTERACTION_RADIUS && pos.x <= handle.x + HANDLE_INTERACTION_RADIUS &&
453
+ pos.y >= handle.y - HANDLE_INTERACTION_RADIUS && pos.y <= handle.y + HANDLE_INTERACTION_RADIUS) {
454
+ return handle.type;
455
+ }
456
+ }
457
+ return null;
458
+ }
459
+ function isPointInRect(point, rect) { /* ... same as previous full JS ... */
460
+ return point.x >= rect.x && point.x <= rect.x + rect.width &&
461
+ point.y >= rect.y && point.y <= rect.y + rect.height;
462
+ }
463
+ function updateCursorStyle(pos) { /* ... same as previous full JS ... */
464
+ if (!currentImage) { selectionCanvasEl.style.cursor = 'default'; return; }
465
+ if (dragMode) {
466
+ if (dragMode === 'move') selectionCanvasEl.style.cursor = 'grabbing';
467
+ else if (dragMode === 'resize-tl' || dragMode === 'resize-br') selectionCanvasEl.style.cursor = 'nwse-resize';
468
+ else if (dragMode === 'resize-tr' || dragMode === 'resize-bl') selectionCanvasEl.style.cursor = 'nesw-resize';
469
+ else if (dragMode === 'resize-t' || dragMode === 'resize-b') selectionCanvasEl.style.cursor = 'ns-resize';
470
+ else if (dragMode === 'resize-l' || dragMode === 'resize-r') selectionCanvasEl.style.cursor = 'ew-resize';
471
+ return;
472
+ }
473
+ const activeSel = selections.find(s => s.id === activeSelectionId);
474
+ if (activeSel) {
475
+ const handleType = getHandleAtPos(pos, activeSel);
476
+ if (handleType) {
477
+ if (handleType.startsWith('resize-t') || handleType.startsWith('resize-b')) selectionCanvasEl.style.cursor = 'ns-resize';
478
+ else if (handleType.startsWith('resize-l') || handleType.startsWith('resize-r')) selectionCanvasEl.style.cursor = 'ew-resize';
479
+ else if (handleType === 'resize-tl' || handleType === 'resize-br') selectionCanvasEl.style.cursor = 'nwse-resize';
480
+ else if (handleType === 'resize-tr' || handleType === 'resize-bl') selectionCanvasEl.style.cursor = 'nesw-resize';
481
+ return;
482
+ }
483
+ if (isPointInRect(pos, activeSel)) { selectionCanvasEl.style.cursor = 'move'; return; }
484
+ }
485
+ for (const sel of selections) { // Check hover over non-active selections
486
+ if (sel.id !== activeSelectionId && isPointInRect(pos, sel)) { selectionCanvasEl.style.cursor = 'pointer'; return; }
487
+ }
488
+ selectionCanvasEl.style.cursor = 'crosshair';
489
+ }
490
+
491
+ // --- Drawing ---
492
+ function drawSelections() {
493
+ selectionCtx.clearRect(0, 0, displayWidth, displayHeight);
494
+ selections.forEach(sel => {
495
+ selectionCtx.strokeStyle = (sel.id === activeSelectionId) ? 'blue' : (sel.type === 'tfjs' ? 'green' : 'rgba(0, 123, 255, 0.9)');
496
+ 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)');
497
+ selectionCtx.lineWidth = (sel.id === activeSelectionId) ? 2 : 1.5;
498
+
499
+ selectionCtx.beginPath();
500
+ selectionCtx.rect(sel.x, sel.y, sel.width, sel.height);
501
+ selectionCtx.fill();
502
+ selectionCtx.stroke();
503
+
504
+ if (sel.label) {
505
+ selectionCtx.fillStyle = (sel.id === activeSelectionId) ? 'blue' : (sel.type === 'tfjs' ? 'darkgreen' : 'black');
506
+ selectionCtx.font = "10px Arial";
507
+ const labelX = sel.x + 2;
508
+ const labelY = sel.y > 12 ? sel.y + 10 : sel.y + sel.height + 10; // Position label inside or below
509
+ selectionCtx.fillText(sel.label, labelX, labelY);
510
+ }
511
+ if (sel.id === activeSelectionId) drawHandles(sel);
512
+ });
513
+
514
+ if (isDrawingNewSelection && currentDrawingRect) {
515
+ selectionCtx.strokeStyle = 'rgba(0, 200, 0, 0.9)';
516
+ selectionCtx.fillStyle = 'rgba(0, 200, 0, 0.2)';
517
+ selectionCtx.lineWidth = 1.5;
518
+ let x = Math.min(currentDrawingRect.startX, currentDrawingRect.endX);
519
+ let y = Math.min(currentDrawingRect.startY, currentDrawingRect.endY);
520
+ let width = Math.abs(currentDrawingRect.startX - currentDrawingRect.endX);
521
+ let height = Math.abs(currentDrawingRect.startY - currentDrawingRect.endY);
522
+ selectionCtx.beginPath();
523
+ selectionCtx.rect(x, y, width, height);
524
+ selectionCtx.fill();
525
+ selectionCtx.stroke();
526
+ }
527
+ }
528
+ function drawHandles(sel) { /* ... same as previous full JS ... */
529
+ const handles = getHandlePositions(sel);
530
+ selectionCtx.fillStyle = 'blue'; // Solid blue for handles
531
+ selectionCtx.strokeStyle = 'white';
532
+ selectionCtx.lineWidth = 1;
533
+ handles.forEach(h => {
534
+ selectionCtx.beginPath();
535
+ selectionCtx.rect(h.x - HANDLE_SIZE / 2, h.y - HANDLE_SIZE / 2, HANDLE_SIZE, HANDLE_SIZE);
536
+ selectionCtx.fill();
537
+ selectionCtx.stroke();
538
+ });
539
+ }
540
+
541
+ // --- TF.js Object Detection ---
542
+ detectObjectsBtnEl.addEventListener('click', async () => {
543
+ if (!currentImage || !cocoSsdModel) {
544
+ alert('Please load an image and wait for the TF.js model to be ready.');
545
+ return;
546
+ }
547
+ detectObjectsBtnEl.disabled = true;
548
+ detectObjectsBtnEl.innerHTML = 'Detecting... <div class="loader inline-block w-4 h-4 border-2 border-t-white"></div>';
549
+
550
+ try {
551
+ // COCO-SSD expects an HTMLImageElement, HTMLVideoElement, HTMLCanvasElement, or ImageData
552
+ // We can pass the original HTMLImageElement `currentImage` or the `imageCanvasEl`
553
+ // Passing `currentImage` is usually better as it's the original data.
554
+ const predictions = await cocoSsdModel.detect(currentImage);
555
+
556
+ if (!mergeDetectionsCheckboxEl.checked) { // Remove previous TF.js detections
557
+ selections = selections.filter(s => s.type !== 'tfjs');
558
+ }
559
+
560
+ const minConfidence = parseFloat(minConfidenceInputEl.value) || 0.5;
561
+
562
+ predictions.forEach(p => {
563
+ if (p.score >= minConfidence) {
564
+ const [xOrig, yOrig, widthOrig, heightOrig] = p.bbox;
565
+ // Convert original image coordinates to display canvas coordinates
566
+ const dispX = xOrig * canvasScaleFactor;
567
+ const dispY = yOrig * canvasScaleFactor;
568
+ const dispWidth = widthOrig * canvasScaleFactor;
569
+ const dispHeight = heightOrig * canvasScaleFactor;
570
+
571
+ selections.push({
572
+ id: nextSelectionId++,
573
+ x: dispX, y: dispY, width: dispWidth, height: dispHeight,
574
+ originalAspectRatio: dispWidth / (dispHeight + 1e-6),
575
+ type: 'tfjs',
576
+ label: `${p.class} (${Math.round(p.score * 100)}%)`
577
+ });
578
+ }
579
+ });
580
+ activeSelectionId = null; // Deactivate any current selection after adding new ones
581
+ } catch (error) {
582
+ console.error('Error during TF.js detection:', error);
583
+ alert('An error occurred during object detection.');
584
+ } finally {
585
+ detectObjectsBtnEl.disabled = false;
586
+ detectObjectsBtnEl.textContent = 'Detect Objects';
587
+ drawSelections();
588
+ updateAnalysisDisplay();
589
+ updateActionButtonsVisibility();
590
+ }
591
+ });
592
+
593
+ // --- Selection Management & Analysis ---
594
+ function deleteSelectionById(idToDelete) {
595
+ selections = selections.filter(sel => sel.id !== idToDelete);
596
+ if (activeSelectionId === idToDelete) {
597
+ activeSelectionId = null;
598
+ }
599
+ // No need to redraw/update here, caller will do it
600
+ }
601
+
602
+ deleteActiveSelectionBtnEl.addEventListener('click', () => {
603
+ if (activeSelectionId !== null) {
604
+ deleteSelectionById(activeSelectionId);
605
+ // activeSelectionId is now null
606
+ drawSelections();
607
+ updateAnalysisDisplay();
608
+ updateActionButtonsVisibility();
609
+ }
610
+ });
611
+
612
+ clearAllSelectionsBtnEl.addEventListener('click', clearAllSelections);
613
+
614
+ function clearAllSelections() {
615
+ selections = [];
616
+ activeSelectionId = null;
617
+ // nextSelectionId can be reset if desired, or keep incrementing for true uniqueness over session
618
+ if (selectionCtx && displayWidth > 0) drawSelections(); // Clears canvas
619
+ updateAnalysisDisplay();
620
+ updateActionButtonsVisibility();
621
+ }
622
+
623
+ function updateActionButtonsVisibility() {
624
+ clearAllSelectionsBtnEl.classList.toggle('hidden', selections.length === 0);
625
+ deleteActiveSelectionBtnEl.classList.toggle('hidden', activeSelectionId === null || selections.length === 0);
626
+ }
627
+
628
+ function updateAnalysisDisplay() {
629
+ if (!currentImage) {
630
+ totalAreaDispEl.textContent = '- px²';
631
+ selectedAreaDispEl.textContent = '- px²';
632
+ percentageSelectedDispEl.textContent = '- %';
633
+ percentageExtraDispEl.textContent = '- %';
634
+ numSelectionsDispEl.textContent = '0';
635
+ return;
636
+ }
637
+
638
+ const totalImgAreaPx = originalImgWidth * originalImgHeight;
639
+ totalAreaDispEl.textContent = `${Math.round(totalImgAreaPx).toLocaleString()} px²`;
640
+
641
+ let totalSelectedOriginalPxArea = 0;
642
+ selections.forEach(sel => {
643
+ // Convert selection display dimensions back to original image pixel dimensions
644
+ const originalSelWidth = sel.width / canvasScaleFactor;
645
+ const originalSelHeight = sel.height / canvasScaleFactor;
646
+ totalSelectedOriginalPxArea += originalSelWidth * originalSelHeight;
647
+ });
648
+
649
+ selectedAreaDispEl.textContent = `${Math.round(totalSelectedOriginalPxArea).toLocaleString()} px²`;
650
+ numSelectionsDispEl.textContent = selections.length.toString();
651
+
652
+ if (totalImgAreaPx > 0 && totalSelectedOriginalPxArea >= 0) {
653
+ const percSelected = (totalSelectedOriginalPxArea / totalImgAreaPx) * 100;
654
+ const percExtra = 100 - percSelected;
655
+ percentageSelectedDispEl.textContent = `${percSelected.toFixed(1)} %`;
656
+ percentageExtraDispEl.textContent = `${percExtra.toFixed(1)} %`;
657
+ } else {
658
+ percentageSelectedDispEl.textContent = '0.0 %';
659
+ percentageExtraDispEl.textContent = '100.0 %';
660
+ }
661
+ }
662
+
663
+ // --- Event Listeners for Image Load ---
664
+ loadImageBtnEl.addEventListener('click', () => {
665
+ const url = imageUrlInputEl.value.trim();
666
+ if (url) {
667
+ imagePlaceholderEl.textContent = 'Loading image from URL...';
668
+ loadImageFromUrl(url);
669
+ } else {
670
+ displayImageLoadError('Please enter an image URL.');
671
+ }
672
+ });
673
+
674
+ imageUploadInputEl.addEventListener('change', (event) => {
675
+ const file = event.target.files[0];
676
+ if (file) {
677
+ imagePlaceholderEl.textContent = 'Loading image from file...';
678
+ loadImageFromFile(file);
679
+ }
680
+ event.target.value = null; // Reset file input
681
+ });
682
+
683
+ // --- Initial Setup ---
684
+ document.addEventListener('DOMContentLoaded', () => {
685
+ resetImageArea(); // Set initial placeholder state
686
+ loadCocoSsdModel(); // Start loading TF.js model immediately
687
+ updateActionButtonsVisibility(); // Initial button states
688
+ });
689
+
690
+ </script>
691
+ </body>
692
  </html>