Xenova HF Staff commited on
Commit
bede8f0
·
verified ·
1 Parent(s): 32fa6d7

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +536 -18
index.html CHANGED
@@ -1,19 +1,537 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>SAM3 WebGPU | Transformers.js</title>
8
+
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+
11
+ <style>
12
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
13
+
14
+ body {
15
+ font-family: 'Inter', sans-serif;
16
+ }
17
+
18
+ /* Style for the mask canvas overlay */
19
+ canvas {
20
+ position: absolute;
21
+ top: 0;
22
+ left: 0;
23
+ opacity: 0.6;
24
+ pointer-events: none;
25
+ }
26
+
27
+ /* Style for the emoji markers (star/cross) */
28
+ .icon {
29
+ position: absolute;
30
+ transform: translate(-50%, -50%);
31
+ font-size: 24px;
32
+ user-select: none;
33
+ pointer-events: none;
34
+ text-shadow: 0 0 4px white;
35
+ }
36
+
37
+ .aspect-w-3 {
38
+ position: relative;
39
+ width: 100%;
40
+ }
41
+
42
+ .aspect-w-3::before {
43
+ content: '';
44
+ display: block;
45
+ padding-bottom: calc(var(--aspect-h) / var(--aspect-w) * 100%);
46
+ }
47
+
48
+ .aspect-w-3> :first-child {
49
+ position: absolute;
50
+ top: 0;
51
+ left: 0;
52
+ width: 100%;
53
+ height: 100%;
54
+ }
55
+
56
+ .aspect-w-3 {
57
+ --aspect-w: 3;
58
+ }
59
+
60
+ .aspect-h-2 {
61
+ --aspect-h: 2;
62
+ }
63
+
64
+ .aspect-w-4 {
65
+ --aspect-w: 4;
66
+ }
67
+
68
+ .aspect-h-3 {
69
+ --aspect-h: 3;
70
+ }
71
+ </style>
72
+ </head>
73
+
74
+ <body class="bg-gray-100 text-gray-800 min-h-screen flex flex-col items-center justify-center p-4 sm:p-8">
75
+
76
+ <div class="w-full max-w-3xl bg-white rounded-xl shadow-2xl overflow-hidden">
77
+
78
+ <div class="p-6 sm:p-10">
79
+ <h1 class="text-3xl sm:text-4xl font-bold text-center text-gray-900">SAM3 WebGPU</h1>
80
+ <h3 class="text-lg sm:text-xl text-gray-500 text-center mb-6">
81
+ In-browser image segmentation w/
82
+ <a href="https://hf.co/docs/transformers.js" target="_blank" class="text-blue-600 hover:underline">🤗
83
+ Transformers.js</a>
84
+ </h3>
85
+
86
+ <div id="container"
87
+ class="relative w-full max-w-2xl mx-auto border border-gray-200 rounded-lg overflow-hidden cursor-pointer bg-gray-50 shadow-sm transition-all aspect-w-3 aspect-h-2">
88
+
89
+ <label id="upload-area" for="upload"
90
+ class="absolute inset-0 z-10 flex flex-col justify-center items-center p-10 transition-all cursor-pointer border-2 border-dashed border-gray-300 rounded-lg hover:bg-gray-50/50 hover:border-blue-500">
91
+ <div class="flex flex-col items-center justify-center p-6 transition-colors w-full max-w-sm">
92
+ <svg class="w-12 h-12 text-gray-400" fill="currentColor" viewBox="0 0 25 25"
93
+ xmlns="http://www.w3.org/2000/svg">
94
+ <path
95
+ 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">
96
+ </path>
97
+ </svg>
98
+ <span class="text-lg font-medium text-gray-500 mt-2">Click to upload image</span>
99
+ <span class="text-sm text-gray-400">or drag and drop</span>
100
+ </div>
101
+
102
+ <p class="text-gray-500 text-sm my-4">...or try an example:</p>
103
+
104
+ <div id="example-gallery" class="flex gap-4">
105
+ <img src="https://huggingface.co/datasets/hf-internal-testing/sam2-fixtures/resolve/main/truck.jpg"
106
+ class="example-image w-20 h-20 sm:w-24 sm:h-24 object-cover rounded-lg shadow-md cursor-pointer hover:opacity-80 transition-opacity"
107
+ alt="Example of a truck">
108
+ <img src="https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/corgi.jpg"
109
+ class="example-image w-20 h-20 sm:w-24 sm:h-24 object-cover rounded-lg shadow-md cursor-pointer hover:opacity-80 transition-opacity"
110
+ alt="Example of a corgi">
111
+ <img src="https://huggingface.co/datasets/hf-internal-testing/sam2-fixtures/resolve/main/groceries.jpg"
112
+ class="example-image w-20 h-20 sm:w-24 sm:h-24 object-cover rounded-lg shadow-md cursor-pointer hover:opacity-80 transition-opacity"
113
+ alt="Example of groceries">
114
+ </div>
115
+ </label>
116
+
117
+ <img id="image-display" class="absolute inset-0 w-full h-full object-contain block hidden z-0" />
118
+
119
+ <canvas id="mask-output"></canvas>
120
+ </div>
121
+
122
+ <label id="status" class="text-base text-center text-gray-600 min-h-[1.5rem] mt-6 mb-4 block w-full">Loading
123
+ model...</label>
124
+
125
+ <div id="controls" class="flex flex-col sm:flex-row justify-center gap-3">
126
+ <button id="reset-image" disabled
127
+ class="w-full sm:w-auto inline-flex items-center justify-center px-4 py-2 bg-gray-200 text-gray-800 font-medium rounded-lg shadow-sm hover:bg-gray-300 transition-colors disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed">
128
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
129
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
130
+ class="w-4 h-4 mr-2">
131
+ <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
132
+ <path d="M3 3v5h5" />
133
+ </svg>
134
+ Reset image
135
+ </button>
136
+ <button id="clear-points" disabled
137
+ class="w-full sm:w-auto inline-flex items-center justify-center px-4 py-2 bg-gray-200 text-gray-800 font-medium rounded-lg shadow-sm hover:bg-gray-300 transition-colors disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed">
138
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
139
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
140
+ class="w-4 h-4 mr-2">
141
+ <line x1="18" y1="6" x2="6" y2="18"></line>
142
+ <line x1="6" y1="6" x2="18" y2="18"></line>
143
+ </svg>
144
+ Clear points
145
+ </button>
146
+ <button id="cut-mask" disabled
147
+ class="w-full sm:w-auto inline-flex items-center justify-center px-4 py-2 bg-blue-600 text-white font-medium rounded-lg shadow-sm hover:bg-blue-700 transition-colors disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed">
148
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
149
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
150
+ class="w-4 h-4 mr-2">
151
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
152
+ <polyline points="7 10 12 15 17 10" />
153
+ <line x1="12" y1="15" x2="12" y2="3" />
154
+ </svg>
155
+ Cut & Download
156
+ </button>
157
+ </div>
158
+
159
+ <p id="information" class="text-sm text-gray-500 mt-4 text-center">
160
+ Left click = positive (⭐), Right click = negative (❌).
161
+ </p>
162
+ </div>
163
+ </div>
164
+
165
+ <input id="upload" type="file" accept="image/*" disabled class="hidden" />
166
+
167
+
168
+ <script type="module">
169
+ import {
170
+ Sam3TrackerModel,
171
+ AutoProcessor,
172
+ RawImage,
173
+ Tensor,
174
+ } from "https://cdn.jsdelivr.net/npm/@huggingface/[email protected]";
175
+
176
+ const statusLabel = document.getElementById("status");
177
+ const fileUpload = document.getElementById("upload");
178
+ const imageContainer = document.getElementById("container");
179
+ const uploadArea = document.getElementById("upload-area");
180
+ const exampleImages = document.querySelectorAll(".example-image");
181
+ const resetButton = document.getElementById("reset-image");
182
+ const clearButton = document.getElementById("clear-points");
183
+ const cutButton = document.getElementById("cut-mask");
184
+ const imageDisplay = document.getElementById("image-display");
185
+ const maskCanvas = document.getElementById("mask-output");
186
+ const maskContext = maskCanvas.getContext("2d");
187
+
188
+ let isEncoding = false;
189
+ let isDecoding = false;
190
+ let decodePending = false;
191
+ let lastPoints = null;
192
+ let isMultiMaskMode = false;
193
+ let imageInput = null;
194
+ let imageProcessed = null;
195
+ let imageEmbeddings = null;
196
+
197
+
198
+ /**
199
+ * Encodes the image and generates embeddings.
200
+ */
201
+ async function encode(url) {
202
+ if (isEncoding) return;
203
+ isEncoding = true;
204
+ statusLabel.textContent = "Extracting image embedding...";
205
+
206
+ try {
207
+ imageInput = await RawImage.fromURL(url);
208
+
209
+ imageDisplay.onload = updateCanvasGeometry;
210
+ imageDisplay.src = url;
211
+ imageDisplay.classList.remove('hidden');
212
+ uploadArea.classList.add("hidden");
213
+ cutButton.disabled = true;
214
+
215
+ imageProcessed = await processor(imageInput);
216
+ imageEmbeddings = await model.get_image_embeddings(imageProcessed);
217
+ console.log({ imageEmbeddings })
218
+
219
+ statusLabel.textContent = "Embedding extracted! Click on the image.";
220
+ resetButton.disabled = false;
221
+ clearButton.disabled = false;
222
+ } catch (error) {
223
+ console.error("Error during encoding:", error);
224
+ statusLabel.textContent = "Error loading image. Please try again.";
225
+ resetUI();
226
+ } finally {
227
+ isEncoding = false;
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Decodes the mask based on the current points.
233
+ */
234
+ async function decode() {
235
+ if (isDecoding || !imageEmbeddings || !lastPoints || lastPoints.length === 0) {
236
+ if (isDecoding) {
237
+ decodePending = true;
238
+ }
239
+ return;
240
+ }
241
+ isDecoding = true;
242
+
243
+ try {
244
+ const reshaped = imageProcessed.reshaped_input_sizes[0];
245
+ const points = lastPoints
246
+ .map((x) => [x.position[0] * reshaped[1], x.position[1] * reshaped[0]])
247
+ .flat(Infinity);
248
+ const labels = lastPoints.map((x) => BigInt(x.label)).flat(Infinity);
249
+
250
+ const num_points = lastPoints.length;
251
+ const input_points = new Tensor("float32", points, [1, 1, num_points, 2]);
252
+ const input_labels = new Tensor("int64", labels, [1, 1, num_points]);
253
+
254
+ const { pred_masks, iou_scores } = await model({
255
+ ...imageEmbeddings,
256
+ input_points,
257
+ input_labels,
258
+ });
259
+
260
+ const masks = await processor.post_process_masks(
261
+ pred_masks,
262
+ imageProcessed.original_sizes,
263
+ imageProcessed.reshaped_input_sizes,
264
+ );
265
+
266
+ updateMaskOverlay(RawImage.fromTensor(masks[0][0]), iou_scores.data);
267
+
268
+ } catch (error) {
269
+ console.error("Error during decoding:", error);
270
+ statusLabel.textContent = "Error generating mask.";
271
+ } finally {
272
+ isDecoding = false;
273
+ }
274
+
275
+ if (decodePending) {
276
+ decodePending = false;
277
+ setTimeout(decode, 0);
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Resizes and positions the canvas overlay to match the displayed image.
283
+ */
284
+ function updateCanvasGeometry() {
285
+ if (!imageDisplay.src || imageDisplay.classList.contains('hidden')) return;
286
+
287
+ const { naturalWidth, naturalHeight } = imageDisplay;
288
+ const { width: containerWidth, height: containerHeight } = imageContainer.getBoundingClientRect();
289
+
290
+ const imageAspectRatio = naturalWidth / naturalHeight;
291
+ const containerAspectRatio = containerWidth / containerHeight;
292
+
293
+ let newWidth, newHeight, newTop, newLeft;
294
+
295
+ if (imageAspectRatio > containerAspectRatio) {
296
+ newWidth = containerWidth;
297
+ newHeight = newWidth / imageAspectRatio;
298
+ newTop = (containerHeight - newHeight) / 2;
299
+ newLeft = 0;
300
+ } else {
301
+ newHeight = containerHeight;
302
+ newWidth = newHeight * imageAspectRatio;
303
+ newLeft = (containerWidth - newWidth) / 2;
304
+ newTop = 0;
305
+ }
306
+
307
+ maskCanvas.style.width = `${newWidth}px`;
308
+ maskCanvas.style.height = `${newHeight}px`;
309
+ maskCanvas.style.top = `${newTop}px`;
310
+ maskCanvas.style.left = `${newLeft}px`;
311
+ }
312
+
313
+ /**
314
+ * Draws the generated mask onto the canvas.
315
+ */
316
+ function updateMaskOverlay(mask, scores) {
317
+ if (maskCanvas.width !== mask.width || maskCanvas.height !== mask.height) {
318
+ maskCanvas.width = mask.width;
319
+ maskCanvas.height = mask.height;
320
+ }
321
+
322
+ const imageData = maskContext.createImageData(
323
+ maskCanvas.width,
324
+ maskCanvas.height,
325
+ );
326
+
327
+ const numMasks = scores.length;
328
+ let bestIndex = 0;
329
+ for (let i = 1; i < numMasks; ++i) {
330
+ if (scores[i] > scores[bestIndex]) {
331
+ bestIndex = i;
332
+ }
333
+ }
334
+ statusLabel.textContent = `Segment score: ${scores[bestIndex].toFixed(2)}`;
335
+
336
+ const pixelData = imageData.data;
337
+ for (let i = 0; i < pixelData.length; ++i) {
338
+ if (mask.data[numMasks * i + bestIndex] === 1) {
339
+ const offset = 4 * i;
340
+ pixelData[offset] = 0;
341
+ pixelData[offset + 1] = 114;
342
+ pixelData[offset + 2] = 189;
343
+ pixelData[offset + 3] = 255;
344
+ }
345
+ }
346
+
347
+ maskContext.putImageData(imageData, 0, 0);
348
+ }
349
+
350
+ function clearPointsAndMask() {
351
+ isMultiMaskMode = false;
352
+ lastPoints = null;
353
+
354
+ document.querySelectorAll(".icon").forEach((e) => e.remove());
355
+
356
+ cutButton.disabled = true;
357
+
358
+ maskContext.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
359
+ statusLabel.textContent = "Points cleared. Click to add new points.";
360
+ }
361
+
362
+ function resetUI() {
363
+ imageInput = null;
364
+ imageProcessed = null;
365
+ imageEmbeddings = null;
366
+ isEncoding = false;
367
+ isDecoding = false;
368
+ decodePending = false;
369
+
370
+ clearPointsAndMask();
371
+
372
+ cutButton.disabled = true;
373
+ resetButton.disabled = true;
374
+ clearButton.disabled = true;
375
+ imageDisplay.src = '';
376
+ imageDisplay.classList.add('hidden');
377
+ uploadArea.classList.remove("hidden");
378
+
379
+ // Reset canvas geometry
380
+ maskCanvas.style.width = '0px';
381
+ maskCanvas.style.height = '0px';
382
+
383
+ exampleImages.forEach(img => img.style.pointerEvents = "auto");
384
+
385
+ statusLabel.textContent = "Ready";
386
+ }
387
+ function clamp(x, min = 0, max = 1) {
388
+ return Math.max(Math.min(x, max), min);
389
+ }
390
+ function getPoint(e) {
391
+ const imgBB = imageDisplay.getBoundingClientRect();
392
+ const canvasBB = maskCanvas.getBoundingClientRect();
393
+
394
+ // Calculate normalized coordinates (0 to 1) relative to the image
395
+ const mouseX = clamp((e.clientX - canvasBB.left) / canvasBB.width);
396
+ const mouseY = clamp((e.clientY - canvasBB.top) / canvasBB.height);
397
+
398
+ return {
399
+ position: [mouseX, mouseY],
400
+ label: e.button === 2 ? 0 : 1,
401
+ };
402
+ }
403
+
404
+ fileUpload.addEventListener("change", function (e) {
405
+ const file = e.target.files[0];
406
+ if (!file) return;
407
+
408
+ const reader = new FileReader();
409
+ reader.onload = (e2) => encode(e2.target.result);
410
+ reader.readAsDataURL(file);
411
+ });
412
+
413
+ exampleImages.forEach((img) => {
414
+ img.addEventListener("click", (e) => {
415
+ e.preventDefault();
416
+ e.stopPropagation();
417
+ exampleImages.forEach(i => i.style.pointerEvents = "none");
418
+ encode(img.src);
419
+ });
420
+ });
421
+
422
+ window.addEventListener("resize", updateCanvasGeometry);
423
+
424
+ resetButton.addEventListener("click", resetUI);
425
+
426
+ clearButton.addEventListener("click", clearPointsAndMask);
427
+
428
+ imageContainer.addEventListener("mousedown", (e) => {
429
+ if (!imageEmbeddings || uploadArea.classList.contains('hidden') === false) {
430
+ return;
431
+ }
432
+
433
+ if (e.button !== 0 && e.button !== 2) return;
434
+
435
+ if (!isMultiMaskMode) {
436
+ lastPoints = [];
437
+ isMultiMaskMode = true;
438
+ cutButton.disabled = false;
439
+ }
440
+
441
+ const point = getPoint(e);
442
+ lastPoints.push(point);
443
+
444
+ const icon = document.createElement('span');
445
+ icon.className = 'icon';
446
+ icon.textContent = point.label === 1 ? '⭐' : '❌';
447
+
448
+ // Calculate position relative to the container, considering canvas offset and size
449
+ const canvasRect = maskCanvas.getBoundingClientRect();
450
+ const containerRect = imageContainer.getBoundingClientRect();
451
+ const left = canvasRect.left - containerRect.left + point.position[0] * canvasRect.width;
452
+ const top = canvasRect.top - containerRect.top + point.position[1] * canvasRect.height;
453
+
454
+ icon.style.left = `${left}px`;
455
+ icon.style.top = `${top}px`;
456
+ imageContainer.appendChild(icon);
457
+
458
+ decode();
459
+ });
460
+
461
+ imageContainer.addEventListener("contextmenu", (e) => e.preventDefault());
462
+
463
+ imageContainer.addEventListener("mousemove", (e) => {
464
+ if (!imageEmbeddings || isMultiMaskMode || uploadArea.classList.contains('hidden') === false) {
465
+ return;
466
+ }
467
+ lastPoints = [getPoint(e)];
468
+ decode();
469
+ });
470
+
471
+ cutButton.addEventListener("click", async () => {
472
+ if (!imageInput || !maskCanvas) return;
473
+
474
+ const [w, h] = [maskCanvas.width, maskCanvas.height];
475
+
476
+ const maskImageData = maskContext.getImageData(0, 0, w, h);
477
+ const maskPixelData = maskImageData.data;
478
+
479
+ const cutCanvas = new OffscreenCanvas(w, h);
480
+ const cutContext = cutCanvas.getContext("2d");
481
+
482
+ const cutImageData = cutContext.createImageData(w, h);
483
+ const cutPixelData = cutImageData.data;
484
+
485
+ const imagePixelData = imageInput.data;
486
+
487
+ for (let i = 0; i < w * h; ++i) {
488
+ const maskOffset = 4 * i;
489
+ const imageOffset = 3 * i;
490
+
491
+ if (maskPixelData[maskOffset + 3] > 0) {
492
+ cutPixelData[maskOffset] = imagePixelData[imageOffset];
493
+ cutPixelData[maskOffset + 1] = imagePixelData[imageOffset + 1];
494
+ cutPixelData[maskOffset + 2] = imagePixelData[imageOffset + 2];
495
+ cutPixelData[maskOffset + 3] = 255;
496
+ }
497
+ }
498
+ cutContext.putImageData(cutImageData, 0, 0);
499
+
500
+ const link = document.createElement("a");
501
+ link.download = "mask-cutout.png";
502
+ link.href = URL.createObjectURL(await cutCanvas.convertToBlob());
503
+ link.click();
504
+ link.remove();
505
+ });
506
+
507
+ async function loadModel() {
508
+ try {
509
+ const model_id = "onnx-community/sam3-tracker-ONNX";
510
+
511
+ const model = await Sam3TrackerModel.from_pretrained(model_id, {
512
+ dtype: {
513
+ vision_encoder: "q4",
514
+ prompt_encoder_mask_decoder: "fp32",
515
+ },
516
+ device: "webgpu",
517
+ });
518
+
519
+ const processor = await AutoProcessor.from_pretrained(model_id);
520
+
521
+ statusLabel.textContent = "Ready";
522
+
523
+ fileUpload.disabled = false;
524
+
525
+ return { model, processor };
526
+ } catch (error) {
527
+ console.error("Error loading model:", error);
528
+ statusLabel.textContent = "Error loading model. Please refresh the page.";
529
+ }
530
+ }
531
+
532
+ const { model, processor } = await loadModel();
533
+
534
+ </script>
535
+ </body>
536
+
537
+ </html>