davanstrien HF Staff Claude Opus 4.6 (1M context) commited on
Commit
867ce05
·
1 Parent(s): a3f75c5

Support segmentation mask overlays alongside detection boxes

Browse files

Auto-detects dataset format (objects column vs segmentation_map column)
and renders accordingly. Segmentation masks are colorized per-instance
and overlaid with transparency. All existing detection functionality
preserved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. index.html +235 -61
index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>SAM3 Object Detection Browser</title>
7
  <style>
8
  * {
9
  margin: 0;
@@ -79,6 +79,28 @@
79
  border-bottom-color: #222;
80
  }
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  .controls {
83
  border: 1px solid #ddd;
84
  padding: 20px;
@@ -220,12 +242,23 @@
220
  background: #000;
221
  }
222
 
223
- .image-container img {
224
  width: 100%;
225
  height: auto;
226
  display: block;
227
  }
228
 
 
 
 
 
 
 
 
 
 
 
 
229
  .image-container canvas {
230
  position: absolute;
231
  top: 0;
@@ -344,9 +377,9 @@
344
  <body>
345
  <div class="container">
346
  <header>
347
- <h1>SAM3 Object Detection Browser</h1>
348
- <p class="subtitle">Browse and explore object detection results from Meta's SAM3 model</p>
349
- <p class="header-links">Create your own detection dataset with the <a href="https://huggingface.co/datasets/uv-scripts/sam3" target="_blank">SAM3 detection script</a></p>
350
  </header>
351
 
352
  <div class="controls">
@@ -354,9 +387,11 @@
354
  <label for="dataset-id">Dataset ID:</label>
355
  <input type="text" id="dataset-id" value="davanstrien/newspapers-image-predictions" placeholder="username/dataset-name">
356
  <div class="example-datasets">
357
- Examples:
358
  <a href="#" data-dataset="davanstrien/newspapers-image-predictions">photographs</a>
359
  <a href="#" data-dataset="davanstrien/newspapers-illustration-predictions">illustrations</a>
 
 
360
  </div>
361
  </div>
362
 
@@ -401,6 +436,20 @@
401
  const itemsPerPage = 12;
402
  let confidenceThreshold = 0.5;
403
  let showOnlyDetections = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
 
405
  // DOM elements
406
  const datasetIdInput = document.getElementById('dataset-id');
@@ -414,8 +463,9 @@
414
  const errorDiv = document.getElementById('error');
415
  const imageGrid = document.getElementById('image-grid');
416
  const paginationDiv = document.getElementById('pagination');
 
417
 
418
- // Initialize state from DOM (in case browser persists checkbox state)
419
  showOnlyDetections = onlyDetectionsCheckbox.checked;
420
 
421
  // Event listeners
@@ -446,6 +496,28 @@
446
  });
447
  });
448
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  // Load dataset from HuggingFace with progressive rendering
450
  async function loadDataset() {
451
  const datasetId = datasetIdInput.value.trim();
@@ -462,14 +534,14 @@
462
  imageGrid.innerHTML = '';
463
  paginationDiv.style.display = 'none';
464
  loadButton.disabled = true;
 
465
 
466
  dataset = [];
467
  let isLoadingComplete = false;
468
 
469
  try {
470
- // Fetch rows progressively and render after each batch
471
  let offset = 0;
472
- const batchSize = 50; // Smaller batches for faster initial display
473
  let hasMore = true;
474
  let isFirstBatch = true;
475
 
@@ -491,10 +563,17 @@
491
  break;
492
  }
493
 
494
- // Convert rows to dataset format
 
 
 
 
 
 
 
495
  const newRows = data.rows.map(item => ({
496
  index: offset + item.row_idx,
497
- image: item.row.image, // Keep as object or string
498
  objects: item.row.objects || { bbox: [], category: [], score: [] },
499
  ...item.row
500
  }));
@@ -502,16 +581,13 @@
502
  dataset = dataset.concat(newRows);
503
  offset += data.rows.length;
504
 
505
- // Render immediately after first batch
506
  if (isFirstBatch) {
507
  filterAndRender();
508
  loadingDiv.textContent = `Loading more... (${dataset.length} rows loaded)`;
509
  isFirstBatch = false;
510
  } else {
511
- // Update view and stats as we load more
512
  filterDataset();
513
  renderStats();
514
- // Only re-render current page if we're on page 1 (to avoid disrupting user)
515
  if (currentPage === 0) {
516
  renderPage();
517
  renderPagination();
@@ -519,14 +595,11 @@
519
  loadingDiv.textContent = `Loading more... (${dataset.length} rows loaded)`;
520
  }
521
 
522
- // Stop if we got fewer rows than requested (end of dataset)
523
  if (data.rows.length < batchSize) {
524
  hasMore = false;
525
  }
526
 
527
- // Limit to prevent overwhelming the browser
528
  if (dataset.length >= 10000) {
529
- console.log('Loaded 10,000 rows, stopping to prevent performance issues');
530
  hasMore = false;
531
  }
532
  }
@@ -535,7 +608,6 @@
535
  throw new Error('No data found in dataset');
536
  }
537
 
538
- // Final update
539
  isLoadingComplete = true;
540
  loadingDiv.style.display = 'none';
541
  filterAndRender();
@@ -554,16 +626,11 @@
554
  // Filter dataset based on current filters
555
  function filterDataset() {
556
  filteredDataset = dataset.filter(item => {
557
- const objects = item.objects || { bbox: [], category: [], score: [] };
558
-
559
- // Filter by confidence threshold
560
- const validDetections = objects.score.filter(score => score >= confidenceThreshold);
561
-
562
- // If "only detections" is checked, filter out images without detections
563
  if (showOnlyDetections && validDetections.length === 0) {
564
  return false;
565
  }
566
-
567
  return true;
568
  });
569
  }
@@ -579,20 +646,19 @@
579
 
580
  // Render statistics
581
  function renderStats() {
582
- // Calculate stats from FILTERED dataset for consistency
583
  const totalImages = filteredDataset.length;
584
  const totalDetections = filteredDataset.reduce((sum, item) => {
585
- const objects = item.objects || { bbox: [], category: [], score: [] };
586
- return sum + objects.score.filter(score => score >= confidenceThreshold).length;
587
  }, 0);
588
 
589
  const imagesWithDetections = filteredDataset.filter(item => {
590
- const objects = item.objects || { bbox: [], category: [], score: [] };
591
- return objects.score.some(score => score >= confidenceThreshold);
592
  }).length;
593
 
594
  const avgDetections = totalImages > 0 ? (totalDetections / totalImages).toFixed(2) : 0;
595
 
 
 
596
  statsContainer.innerHTML = `
597
  <div class="stats">
598
  <div class="stats-grid">
@@ -602,11 +668,11 @@
602
  </div>
603
  <div class="stat-item">
604
  <div class="stat-value">${imagesWithDetections}</div>
605
- <div class="stat-label">Images with Detections</div>
606
  </div>
607
  <div class="stat-item">
608
  <div class="stat-value">${totalDetections}</div>
609
- <div class="stat-label">Total Detections</div>
610
  </div>
611
  <div class="stat-item">
612
  <div class="stat-value">${avgDetections}</div>
@@ -624,27 +690,37 @@
624
  const pageItems = filteredDataset.slice(start, end);
625
 
626
  imageGrid.innerHTML = pageItems.map(item => {
627
- const objects = item.objects || { bbox: [], category: [], score: [] };
628
- const validDetections = objects.score
629
  .map((score, idx) => ({ score, idx }))
630
  .filter(({ score }) => score >= confidenceThreshold);
631
 
632
- // Extract image URL properly (handle both object with src and direct string)
633
  const imageUrl = typeof item.image === 'object' ? item.image?.src : item.image;
634
 
 
 
 
 
 
 
 
 
635
  return `
636
  <div class="image-card">
637
  <div class="image-container" id="container-${item.index}">
638
- <img src="${imageUrl}" alt="Image ${item.index}" crossorigin="anonymous" onload="drawBoundingBoxes(${item.index})">
 
 
 
 
639
  <canvas id="canvas-${item.index}"></canvas>
640
  </div>
641
  <div class="image-info">
642
  <div class="image-index">Image #${item.index}</div>
643
- <div class="detections-count">${validDetections.length} detection(s)</div>
644
  <div class="detections-list">
645
  ${validDetections.map(({ score, idx }) => {
646
  const confidenceClass = score >= 0.7 ? 'confidence-high' : score >= 0.4 ? 'confidence-medium' : 'confidence-low';
647
- const category = objects.category && objects.category[idx] !== undefined ? objects.category[idx] : 0;
648
  return `
649
  <div class="detection-item">
650
  <span class="${confidenceClass}">${(score * 100).toFixed(1)}%</span>
@@ -659,62 +735,161 @@
659
  }).join('');
660
  }
661
 
662
- // Draw bounding boxes on canvas
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
663
  window.drawBoundingBoxes = function(itemIndex) {
664
  const item = dataset.find(d => d.index === itemIndex);
665
  if (!item) return;
666
 
667
  const container = document.getElementById(`container-${itemIndex}`);
668
  const canvas = document.getElementById(`canvas-${itemIndex}`);
669
- const img = container.querySelector('img');
670
 
671
  if (!canvas || !img) return;
672
 
673
- // Set canvas size to match image
674
  canvas.width = img.naturalWidth;
675
  canvas.height = img.naturalHeight;
676
 
677
  const ctx = canvas.getContext('2d');
678
-
679
- // Clear canvas before drawing
680
  ctx.clearRect(0, 0, canvas.width, canvas.height);
681
 
682
  const objects = item.objects || { bbox: [], category: [], score: [] };
683
 
684
- // Debug: log drawing info
685
- const validBoxes = objects.bbox.filter((_, idx) => objects.score[idx] >= confidenceThreshold);
686
- if (validBoxes.length > 0) {
687
- console.log(`Drawing ${validBoxes.length} boxes for image #${itemIndex}`);
688
- }
689
-
690
- // Draw each bounding box
691
  objects.bbox.forEach((bbox, idx) => {
692
  const score = objects.score[idx];
693
  if (score < confidenceThreshold) return;
694
 
695
  const [x, y, width, height] = bbox;
696
 
697
- // Debug: log bbox coordinates
698
- console.log(` Box ${idx}: [${x}, ${y}, ${width}, ${height}] score: ${score}`);
699
-
700
- // Choose color based on confidence
701
  let color;
702
  if (score >= 0.7) {
703
- color = '#27ae60'; // Green
704
  } else if (score >= 0.4) {
705
- color = '#f39c12'; // Orange
706
  } else {
707
- color = '#e74c3c'; // Red
708
  }
709
 
710
- // Draw rectangle
711
  ctx.strokeStyle = color;
712
  ctx.lineWidth = 3;
713
  ctx.strokeRect(x, y, width, height);
714
 
715
- console.log(` Drew box at [${x}, ${y}] size [${width}, ${height}] in ${color}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716
 
717
- // Draw label background
718
  const label = `${(score * 100).toFixed(1)}%`;
719
  ctx.font = 'bold 16px Arial';
720
  const textWidth = ctx.measureText(label).width;
@@ -723,7 +898,6 @@
723
  ctx.fillStyle = color;
724
  ctx.fillRect(x, y - textHeight - 4, textWidth + 10, textHeight + 4);
725
 
726
- // Draw label text
727
  ctx.fillStyle = 'white';
728
  ctx.fillText(label, x + 5, y - 8);
729
  });
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SAM3 Results Browser</title>
7
  <style>
8
  * {
9
  margin: 0;
 
79
  border-bottom-color: #222;
80
  }
81
 
82
+ .mode-badge {
83
+ display: inline-block;
84
+ padding: 2px 8px;
85
+ border-radius: 3px;
86
+ font-size: 11px;
87
+ font-weight: 600;
88
+ text-transform: uppercase;
89
+ letter-spacing: 0.5px;
90
+ margin-left: 8px;
91
+ vertical-align: middle;
92
+ }
93
+
94
+ .mode-badge.detection {
95
+ background: #e8f5e9;
96
+ color: #2e7d32;
97
+ }
98
+
99
+ .mode-badge.segmentation {
100
+ background: #e3f2fd;
101
+ color: #1565c0;
102
+ }
103
+
104
  .controls {
105
  border: 1px solid #ddd;
106
  padding: 20px;
 
242
  background: #000;
243
  }
244
 
245
+ .image-container img.source-image {
246
  width: 100%;
247
  height: auto;
248
  display: block;
249
  }
250
 
251
+ .image-container .mask-overlay {
252
+ position: absolute;
253
+ top: 0;
254
+ left: 0;
255
+ width: 100%;
256
+ height: 100%;
257
+ pointer-events: none;
258
+ z-index: 5;
259
+ mix-blend-mode: normal;
260
+ }
261
+
262
  .image-container canvas {
263
  position: absolute;
264
  top: 0;
 
377
  <body>
378
  <div class="container">
379
  <header>
380
+ <h1>SAM3 Results Browser <span id="mode-badge" class="mode-badge" style="display:none;"></span></h1>
381
+ <p class="subtitle">Browse object detection and segmentation results from Meta's SAM3 model</p>
382
+ <p class="header-links">Create your own results with the <a href="https://huggingface.co/datasets/uv-scripts/sam3" target="_blank">SAM3 scripts</a></p>
383
  </header>
384
 
385
  <div class="controls">
 
387
  <label for="dataset-id">Dataset ID:</label>
388
  <input type="text" id="dataset-id" value="davanstrien/newspapers-image-predictions" placeholder="username/dataset-name">
389
  <div class="example-datasets">
390
+ Detection:
391
  <a href="#" data-dataset="davanstrien/newspapers-image-predictions">photographs</a>
392
  <a href="#" data-dataset="davanstrien/newspapers-illustration-predictions">illustrations</a>
393
+ &nbsp;&nbsp;Segmentation:
394
+ <a href="#" data-dataset="davanstrien/sam3-segment-deer-test">deer masks</a>
395
  </div>
396
  </div>
397
 
 
436
  const itemsPerPage = 12;
437
  let confidenceThreshold = 0.5;
438
  let showOnlyDetections = false;
439
+ // 'detection' = objects column with bbox/score, 'segmentation' = segmentation_map column
440
+ let datasetMode = 'detection';
441
+
442
+ // Mask overlay colors (RGBA)
443
+ const MASK_COLORS = [
444
+ [231, 76, 60], // red
445
+ [46, 204, 113], // green
446
+ [52, 152, 219], // blue
447
+ [241, 196, 15], // yellow
448
+ [155, 89, 182], // purple
449
+ [230, 126, 34], // orange
450
+ [26, 188, 156], // teal
451
+ [236, 112, 99], // salmon
452
+ ];
453
 
454
  // DOM elements
455
  const datasetIdInput = document.getElementById('dataset-id');
 
463
  const errorDiv = document.getElementById('error');
464
  const imageGrid = document.getElementById('image-grid');
465
  const paginationDiv = document.getElementById('pagination');
466
+ const modeBadge = document.getElementById('mode-badge');
467
 
468
+ // Initialize state from DOM
469
  showOnlyDetections = onlyDetectionsCheckbox.checked;
470
 
471
  // Event listeners
 
496
  });
497
  });
498
 
499
+ // Get scores for an item regardless of dataset mode
500
+ function getScores(item) {
501
+ if (datasetMode === 'segmentation') {
502
+ return item.scores || [];
503
+ }
504
+ const objects = item.objects || { bbox: [], category: [], score: [] };
505
+ return objects.score || [];
506
+ }
507
+
508
+ // Get number of detections above threshold
509
+ function getDetectionCount(item) {
510
+ return getScores(item).filter(s => s >= confidenceThreshold).length;
511
+ }
512
+
513
+ // Detect dataset mode from first row
514
+ function detectMode(row) {
515
+ if (row.segmentation_map !== undefined) {
516
+ return 'segmentation';
517
+ }
518
+ return 'detection';
519
+ }
520
+
521
  // Load dataset from HuggingFace with progressive rendering
522
  async function loadDataset() {
523
  const datasetId = datasetIdInput.value.trim();
 
534
  imageGrid.innerHTML = '';
535
  paginationDiv.style.display = 'none';
536
  loadButton.disabled = true;
537
+ modeBadge.style.display = 'none';
538
 
539
  dataset = [];
540
  let isLoadingComplete = false;
541
 
542
  try {
 
543
  let offset = 0;
544
+ const batchSize = 50;
545
  let hasMore = true;
546
  let isFirstBatch = true;
547
 
 
563
  break;
564
  }
565
 
566
+ // Detect mode from first row
567
+ if (isFirstBatch) {
568
+ datasetMode = detectMode(data.rows[0].row);
569
+ modeBadge.textContent = datasetMode;
570
+ modeBadge.className = `mode-badge ${datasetMode}`;
571
+ modeBadge.style.display = 'inline-block';
572
+ }
573
+
574
  const newRows = data.rows.map(item => ({
575
  index: offset + item.row_idx,
576
+ image: item.row.image,
577
  objects: item.row.objects || { bbox: [], category: [], score: [] },
578
  ...item.row
579
  }));
 
581
  dataset = dataset.concat(newRows);
582
  offset += data.rows.length;
583
 
 
584
  if (isFirstBatch) {
585
  filterAndRender();
586
  loadingDiv.textContent = `Loading more... (${dataset.length} rows loaded)`;
587
  isFirstBatch = false;
588
  } else {
 
589
  filterDataset();
590
  renderStats();
 
591
  if (currentPage === 0) {
592
  renderPage();
593
  renderPagination();
 
595
  loadingDiv.textContent = `Loading more... (${dataset.length} rows loaded)`;
596
  }
597
 
 
598
  if (data.rows.length < batchSize) {
599
  hasMore = false;
600
  }
601
 
 
602
  if (dataset.length >= 10000) {
 
603
  hasMore = false;
604
  }
605
  }
 
608
  throw new Error('No data found in dataset');
609
  }
610
 
 
611
  isLoadingComplete = true;
612
  loadingDiv.style.display = 'none';
613
  filterAndRender();
 
626
  // Filter dataset based on current filters
627
  function filterDataset() {
628
  filteredDataset = dataset.filter(item => {
629
+ const scores = getScores(item);
630
+ const validDetections = scores.filter(score => score >= confidenceThreshold);
 
 
 
 
631
  if (showOnlyDetections && validDetections.length === 0) {
632
  return false;
633
  }
 
634
  return true;
635
  });
636
  }
 
646
 
647
  // Render statistics
648
  function renderStats() {
 
649
  const totalImages = filteredDataset.length;
650
  const totalDetections = filteredDataset.reduce((sum, item) => {
651
+ return sum + getDetectionCount(item);
 
652
  }, 0);
653
 
654
  const imagesWithDetections = filteredDataset.filter(item => {
655
+ return getDetectionCount(item) > 0;
 
656
  }).length;
657
 
658
  const avgDetections = totalImages > 0 ? (totalDetections / totalImages).toFixed(2) : 0;
659
 
660
+ const modeLabel = datasetMode === 'segmentation' ? 'Instances' : 'Detections';
661
+
662
  statsContainer.innerHTML = `
663
  <div class="stats">
664
  <div class="stats-grid">
 
668
  </div>
669
  <div class="stat-item">
670
  <div class="stat-value">${imagesWithDetections}</div>
671
+ <div class="stat-label">Images with ${modeLabel}</div>
672
  </div>
673
  <div class="stat-item">
674
  <div class="stat-value">${totalDetections}</div>
675
+ <div class="stat-label">Total ${modeLabel}</div>
676
  </div>
677
  <div class="stat-item">
678
  <div class="stat-value">${avgDetections}</div>
 
690
  const pageItems = filteredDataset.slice(start, end);
691
 
692
  imageGrid.innerHTML = pageItems.map(item => {
693
+ const scores = getScores(item);
694
+ const validDetections = scores
695
  .map((score, idx) => ({ score, idx }))
696
  .filter(({ score }) => score >= confidenceThreshold);
697
 
 
698
  const imageUrl = typeof item.image === 'object' ? item.image?.src : item.image;
699
 
700
+ // For segmentation, get mask image URL
701
+ let maskUrl = '';
702
+ if (datasetMode === 'segmentation' && item.segmentation_map) {
703
+ maskUrl = typeof item.segmentation_map === 'object' ? item.segmentation_map?.src : item.segmentation_map;
704
+ }
705
+
706
+ const countLabel = datasetMode === 'segmentation' ? 'instance(s)' : 'detection(s)';
707
+
708
  return `
709
  <div class="image-card">
710
  <div class="image-container" id="container-${item.index}">
711
+ <img class="source-image" src="${imageUrl}" alt="Image ${item.index}" crossorigin="anonymous"
712
+ onload="onImageLoaded(${item.index})">
713
+ ${maskUrl ? `<img class="mask-overlay" src="${maskUrl}" crossorigin="anonymous"
714
+ data-mask-url="${maskUrl}" style="display:none;"
715
+ onload="onMaskLoaded(${item.index})">` : ''}
716
  <canvas id="canvas-${item.index}"></canvas>
717
  </div>
718
  <div class="image-info">
719
  <div class="image-index">Image #${item.index}</div>
720
+ <div class="detections-count">${validDetections.length} ${countLabel}</div>
721
  <div class="detections-list">
722
  ${validDetections.map(({ score, idx }) => {
723
  const confidenceClass = score >= 0.7 ? 'confidence-high' : score >= 0.4 ? 'confidence-medium' : 'confidence-low';
 
724
  return `
725
  <div class="detection-item">
726
  <span class="${confidenceClass}">${(score * 100).toFixed(1)}%</span>
 
735
  }).join('');
736
  }
737
 
738
+ // Called when the source image loads
739
+ window.onImageLoaded = function(itemIndex) {
740
+ if (datasetMode === 'detection') {
741
+ drawBoundingBoxes(itemIndex);
742
+ } else {
743
+ // For segmentation, draw boxes if present; mask overlay handled separately
744
+ drawSegmentationBoxes(itemIndex);
745
+ }
746
+ };
747
+
748
+ // Called when the mask image loads - colorize and overlay it
749
+ window.onMaskLoaded = function(itemIndex) {
750
+ const container = document.getElementById(`container-${itemIndex}`);
751
+ const canvas = document.getElementById(`canvas-${itemIndex}`);
752
+ const sourceImg = container.querySelector('img.source-image');
753
+ const maskImg = container.querySelector('img.mask-overlay');
754
+
755
+ if (!canvas || !sourceImg || !maskImg) return;
756
+
757
+ canvas.width = sourceImg.naturalWidth;
758
+ canvas.height = sourceImg.naturalHeight;
759
+ const ctx = canvas.getContext('2d');
760
+
761
+ // Draw the mask onto a temporary canvas to read pixel data
762
+ const tempCanvas = document.createElement('canvas');
763
+ tempCanvas.width = maskImg.naturalWidth;
764
+ tempCanvas.height = maskImg.naturalHeight;
765
+ const tempCtx = tempCanvas.getContext('2d');
766
+ tempCtx.drawImage(maskImg, 0, 0);
767
+
768
+ let maskData;
769
+ try {
770
+ maskData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
771
+ } catch (e) {
772
+ console.warn('Cannot read mask pixels (CORS):', e);
773
+ return;
774
+ }
775
+
776
+ // Create colored overlay - each unique non-zero pixel value gets a color
777
+ const overlay = ctx.createImageData(canvas.width, canvas.height);
778
+
779
+ // Scale factors if mask and source image differ in size
780
+ const scaleX = tempCanvas.width / canvas.width;
781
+ const scaleY = tempCanvas.height / canvas.height;
782
+
783
+ for (let y = 0; y < canvas.height; y++) {
784
+ for (let x = 0; x < canvas.width; x++) {
785
+ const mx = Math.floor(x * scaleX);
786
+ const my = Math.floor(y * scaleY);
787
+ const mi = (my * tempCanvas.width + mx) * 4;
788
+ // Segmentation maps are grayscale: R channel has the instance ID
789
+ const instanceId = maskData.data[mi];
790
+
791
+ const oi = (y * canvas.width + x) * 4;
792
+ if (instanceId > 0) {
793
+ const color = MASK_COLORS[(instanceId - 1) % MASK_COLORS.length];
794
+ overlay.data[oi] = color[0];
795
+ overlay.data[oi + 1] = color[1];
796
+ overlay.data[oi + 2] = color[2];
797
+ overlay.data[oi + 3] = 100; // semi-transparent
798
+ }
799
+ }
800
+ }
801
+
802
+ ctx.putImageData(overlay, 0, 0);
803
+
804
+ // Now draw bounding boxes on top if present
805
+ drawSegmentationBoxes(itemIndex);
806
+ };
807
+
808
+ // Draw bounding boxes for detection datasets (objects column)
809
  window.drawBoundingBoxes = function(itemIndex) {
810
  const item = dataset.find(d => d.index === itemIndex);
811
  if (!item) return;
812
 
813
  const container = document.getElementById(`container-${itemIndex}`);
814
  const canvas = document.getElementById(`canvas-${itemIndex}`);
815
+ const img = container.querySelector('img.source-image');
816
 
817
  if (!canvas || !img) return;
818
 
 
819
  canvas.width = img.naturalWidth;
820
  canvas.height = img.naturalHeight;
821
 
822
  const ctx = canvas.getContext('2d');
 
 
823
  ctx.clearRect(0, 0, canvas.width, canvas.height);
824
 
825
  const objects = item.objects || { bbox: [], category: [], score: [] };
826
 
 
 
 
 
 
 
 
827
  objects.bbox.forEach((bbox, idx) => {
828
  const score = objects.score[idx];
829
  if (score < confidenceThreshold) return;
830
 
831
  const [x, y, width, height] = bbox;
832
 
 
 
 
 
833
  let color;
834
  if (score >= 0.7) {
835
+ color = '#27ae60';
836
  } else if (score >= 0.4) {
837
+ color = '#f39c12';
838
  } else {
839
+ color = '#e74c3c';
840
  }
841
 
 
842
  ctx.strokeStyle = color;
843
  ctx.lineWidth = 3;
844
  ctx.strokeRect(x, y, width, height);
845
 
846
+ const label = `${(score * 100).toFixed(1)}%`;
847
+ ctx.font = 'bold 16px Arial';
848
+ const textWidth = ctx.measureText(label).width;
849
+ const textHeight = 20;
850
+
851
+ ctx.fillStyle = color;
852
+ ctx.fillRect(x, y - textHeight - 4, textWidth + 10, textHeight + 4);
853
+
854
+ ctx.fillStyle = 'white';
855
+ ctx.fillText(label, x + 5, y - 8);
856
+ });
857
+ };
858
+
859
+ // Draw bounding boxes for segmentation datasets (boxes column at top level)
860
+ window.drawSegmentationBoxes = function(itemIndex) {
861
+ const item = dataset.find(d => d.index === itemIndex);
862
+ if (!item || !item.boxes) return;
863
+
864
+ const container = document.getElementById(`container-${itemIndex}`);
865
+ const canvas = document.getElementById(`canvas-${itemIndex}`);
866
+ const img = container.querySelector('img.source-image');
867
+
868
+ if (!canvas || !img) return;
869
+
870
+ // Don't clear - mask overlay may already be drawn
871
+ const ctx = canvas.getContext('2d');
872
+ const scores = item.scores || [];
873
+
874
+ item.boxes.forEach((bbox, idx) => {
875
+ const score = scores[idx] !== undefined ? scores[idx] : 1.0;
876
+ if (score < confidenceThreshold) return;
877
+
878
+ const [x, y, width, height] = bbox;
879
+
880
+ let color;
881
+ if (score >= 0.7) {
882
+ color = '#27ae60';
883
+ } else if (score >= 0.4) {
884
+ color = '#f39c12';
885
+ } else {
886
+ color = '#e74c3c';
887
+ }
888
+
889
+ ctx.strokeStyle = color;
890
+ ctx.lineWidth = 3;
891
+ ctx.strokeRect(x, y, width, height);
892
 
 
893
  const label = `${(score * 100).toFixed(1)}%`;
894
  ctx.font = 'bold 16px Arial';
895
  const textWidth = ctx.measureText(label).width;
 
898
  ctx.fillStyle = color;
899
  ctx.fillRect(x, y - textHeight - 4, textWidth + 10, textHeight + 4);
900
 
 
901
  ctx.fillStyle = 'white';
902
  ctx.fillText(label, x + 5, y - 8);
903
  });