Commit ·
867ce05
1
Parent(s): a3f75c5
Support segmentation mask overlays alongside detection boxes
Browse filesAuto-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>
- 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
|
| 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
|
| 348 |
-
<p class="subtitle">Browse
|
| 349 |
-
<p class="header-links">Create your own
|
| 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 |
-
|
| 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
|
| 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;
|
| 473 |
let hasMore = true;
|
| 474 |
let isFirstBatch = true;
|
| 475 |
|
|
@@ -491,10 +563,17 @@
|
|
| 491 |
break;
|
| 492 |
}
|
| 493 |
|
| 494 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 495 |
const newRows = data.rows.map(item => ({
|
| 496 |
index: offset + item.row_idx,
|
| 497 |
-
image: item.row.image,
|
| 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
|
| 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 |
-
|
| 586 |
-
return sum + objects.score.filter(score => score >= confidenceThreshold).length;
|
| 587 |
}, 0);
|
| 588 |
|
| 589 |
const imagesWithDetections = filteredDataset.filter(item => {
|
| 590 |
-
|
| 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
|
| 606 |
</div>
|
| 607 |
<div class="stat-item">
|
| 608 |
<div class="stat-value">${totalDetections}</div>
|
| 609 |
-
<div class="stat-label">Total
|
| 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
|
| 628 |
-
const validDetections =
|
| 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"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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}
|
| 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 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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';
|
| 704 |
} else if (score >= 0.4) {
|
| 705 |
-
color = '#f39c12';
|
| 706 |
} else {
|
| 707 |
-
color = '#e74c3c';
|
| 708 |
}
|
| 709 |
|
| 710 |
-
// Draw rectangle
|
| 711 |
ctx.strokeStyle = color;
|
| 712 |
ctx.lineWidth = 3;
|
| 713 |
ctx.strokeRect(x, y, width, height);
|
| 714 |
|
| 715 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
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 |
});
|