davanstrien's picture
davanstrien HF Staff
Support segmentation mask overlays alongside detection boxes
867ce05
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SAM3 Results Browser</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
line-height: 1.5;
color: #222;
background: #fff;
font-size: 15px;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 40px 20px;
}
header {
border-bottom: 1px solid #ddd;
padding-bottom: 20px;
margin-bottom: 40px;
}
h1 {
color: #222;
font-weight: 400;
font-size: 28px;
margin-bottom: 8px;
}
.subtitle {
color: #666;
font-size: 15px;
font-weight: 300;
margin-bottom: 12px;
}
.header-links {
font-size: 13px;
color: #666;
}
.header-links a {
color: #222;
text-decoration: none;
border-bottom: 1px solid #222;
}
.header-links a:hover {
border-bottom-color: #666;
}
.example-datasets {
margin-top: 8px;
font-size: 12px;
color: #666;
}
.example-datasets a {
color: #666;
text-decoration: none;
border-bottom: 1px dotted #999;
cursor: pointer;
margin-right: 12px;
}
.example-datasets a:hover {
color: #222;
border-bottom-color: #222;
}
.mode-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-left: 8px;
vertical-align: middle;
}
.mode-badge.detection {
background: #e8f5e9;
color: #2e7d32;
}
.mode-badge.segmentation {
background: #e3f2fd;
color: #1565c0;
}
.controls {
border: 1px solid #ddd;
padding: 20px;
margin-bottom: 40px;
background: #fafafa;
}
.control-group {
margin-bottom: 18px;
}
.control-group:last-child {
margin-bottom: 0;
}
label {
display: block;
font-weight: 400;
margin-bottom: 6px;
color: #444;
font-size: 14px;
}
input[type="text"],
select {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 2px;
font-size: 14px;
background: white;
}
input[type="text"]:focus,
select:focus {
outline: none;
border-color: #888;
}
input[type="range"] {
width: 100%;
margin-right: 10px;
}
.slider-container {
display: flex;
align-items: center;
gap: 15px;
}
.slider-value {
min-width: 50px;
font-weight: 400;
color: #222;
}
.checkbox-container {
display: flex;
align-items: center;
gap: 10px;
}
input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.stats {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
padding: 25px 0;
margin-bottom: 40px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 30px;
}
.stat-item {
text-align: center;
padding: 0;
}
.stat-value {
font-size: 36px;
font-weight: 300;
color: #222;
margin-bottom: 4px;
letter-spacing: -0.5px;
}
.stat-label {
font-size: 13px;
color: #666;
font-weight: 400;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.loading {
text-align: center;
padding: 40px;
font-size: 14px;
color: #666;
}
.error {
background: #fff;
border: 1px solid #d00;
padding: 15px;
margin: 20px 0;
color: #d00;
font-size: 14px;
}
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 40px;
margin-bottom: 40px;
}
.image-card {
background: white;
border: 1px solid #ddd;
overflow: hidden;
}
.image-card:hover {
border-color: #999;
}
.image-container {
position: relative;
width: 100%;
background: #000;
}
.image-container img.source-image {
width: 100%;
height: auto;
display: block;
}
.image-container .mask-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 5;
mix-blend-mode: normal;
}
.image-container canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
}
.image-info {
padding: 12px;
border-top: 1px solid #eee;
}
.image-index {
font-size: 11px;
color: #999;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.detections-count {
font-weight: 400;
color: #222;
margin-bottom: 8px;
font-size: 14px;
}
.detections-list {
font-size: 13px;
color: #666;
}
.detection-item {
padding: 3px 0;
}
.confidence-high {
color: #27ae60;
font-weight: 600;
}
.confidence-medium {
color: #f39c12;
font-weight: 600;
}
.confidence-low {
color: #e74c3c;
font-weight: 600;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
margin: 40px 0;
padding-top: 20px;
border-top: 1px solid #ddd;
}
.pagination button {
padding: 8px 16px;
border: 1px solid #666;
background: white;
color: #222;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.pagination button:hover:not(:disabled) {
background: #222;
color: white;
}
.pagination button:disabled {
border-color: #ddd;
color: #ccc;
cursor: not-allowed;
}
.pagination span {
color: #666;
font-size: 13px;
}
.load-button {
padding: 10px 20px;
border: 1px solid #666;
background: white;
color: #222;
cursor: pointer;
font-size: 14px;
font-weight: 400;
transition: all 0.2s;
width: 100%;
margin-top: 12px;
}
.load-button:hover {
background: #222;
color: white;
}
.load-button:disabled {
border-color: #ddd;
color: #ccc;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>SAM3 Results Browser <span id="mode-badge" class="mode-badge" style="display:none;"></span></h1>
<p class="subtitle">Browse object detection and segmentation results from Meta's SAM3 model</p>
<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>
</header>
<div class="controls">
<div class="control-group">
<label for="dataset-id">Dataset ID:</label>
<input type="text" id="dataset-id" value="davanstrien/newspapers-image-predictions" placeholder="username/dataset-name">
<div class="example-datasets">
Detection:
<a href="#" data-dataset="davanstrien/newspapers-image-predictions">photographs</a>
<a href="#" data-dataset="davanstrien/newspapers-illustration-predictions">illustrations</a>
&nbsp;&nbsp;Segmentation:
<a href="#" data-dataset="davanstrien/sam3-segment-deer-test">deer masks</a>
</div>
</div>
<div class="control-group">
<label for="split">Split:</label>
<select id="split">
<option value="train">train</option>
<option value="validation">validation</option>
<option value="test">test</option>
</select>
</div>
<div class="control-group">
<label for="confidence-threshold">Confidence Threshold: <span id="confidence-value" class="slider-value">0.50</span></label>
<div class="slider-container">
<input type="range" id="confidence-threshold" min="0" max="1" step="0.05" value="0.5">
</div>
</div>
<div class="control-group">
<div class="checkbox-container">
<input type="checkbox" id="only-detections">
<label for="only-detections" style="margin-bottom: 0;">Show only images with detections</label>
</div>
</div>
<button class="load-button" id="load-dataset">Load Dataset</button>
</div>
<div id="stats-container"></div>
<div id="loading" class="loading" style="display: none;">Loading dataset...</div>
<div id="error" class="error" style="display: none;"></div>
<div id="image-grid" class="image-grid"></div>
<div id="pagination" class="pagination" style="display: none;"></div>
</div>
<script>
// Global state
let dataset = [];
let filteredDataset = [];
let currentPage = 0;
const itemsPerPage = 12;
let confidenceThreshold = 0.5;
let showOnlyDetections = false;
// 'detection' = objects column with bbox/score, 'segmentation' = segmentation_map column
let datasetMode = 'detection';
// Mask overlay colors (RGBA)
const MASK_COLORS = [
[231, 76, 60], // red
[46, 204, 113], // green
[52, 152, 219], // blue
[241, 196, 15], // yellow
[155, 89, 182], // purple
[230, 126, 34], // orange
[26, 188, 156], // teal
[236, 112, 99], // salmon
];
// DOM elements
const datasetIdInput = document.getElementById('dataset-id');
const splitSelect = document.getElementById('split');
const confidenceSlider = document.getElementById('confidence-threshold');
const confidenceValue = document.getElementById('confidence-value');
const onlyDetectionsCheckbox = document.getElementById('only-detections');
const loadButton = document.getElementById('load-dataset');
const statsContainer = document.getElementById('stats-container');
const loadingDiv = document.getElementById('loading');
const errorDiv = document.getElementById('error');
const imageGrid = document.getElementById('image-grid');
const paginationDiv = document.getElementById('pagination');
const modeBadge = document.getElementById('mode-badge');
// Initialize state from DOM
showOnlyDetections = onlyDetectionsCheckbox.checked;
// Event listeners
confidenceSlider.addEventListener('input', (e) => {
confidenceThreshold = parseFloat(e.target.value);
confidenceValue.textContent = confidenceThreshold.toFixed(2);
if (dataset.length > 0) {
filterAndRender();
}
});
onlyDetectionsCheckbox.addEventListener('change', (e) => {
showOnlyDetections = e.target.checked;
if (dataset.length > 0) {
filterAndRender();
}
});
loadButton.addEventListener('click', loadDataset);
// Handle example dataset links
document.querySelectorAll('.example-datasets a').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const datasetId = e.target.getAttribute('data-dataset');
datasetIdInput.value = datasetId;
loadDataset();
});
});
// Get scores for an item regardless of dataset mode
function getScores(item) {
if (datasetMode === 'segmentation') {
return item.scores || [];
}
const objects = item.objects || { bbox: [], category: [], score: [] };
return objects.score || [];
}
// Get number of detections above threshold
function getDetectionCount(item) {
return getScores(item).filter(s => s >= confidenceThreshold).length;
}
// Detect dataset mode from first row
function detectMode(row) {
if (row.segmentation_map !== undefined) {
return 'segmentation';
}
return 'detection';
}
// Load dataset from HuggingFace with progressive rendering
async function loadDataset() {
const datasetId = datasetIdInput.value.trim();
const split = splitSelect.value;
if (!datasetId) {
showError('Please enter a dataset ID');
return;
}
loadingDiv.style.display = 'block';
errorDiv.style.display = 'none';
statsContainer.innerHTML = '';
imageGrid.innerHTML = '';
paginationDiv.style.display = 'none';
loadButton.disabled = true;
modeBadge.style.display = 'none';
dataset = [];
let isLoadingComplete = false;
try {
let offset = 0;
const batchSize = 50;
let hasMore = true;
let isFirstBatch = true;
while (hasMore) {
const url = `https://datasets-server.huggingface.co/rows?dataset=${encodeURIComponent(datasetId)}&config=default&split=${split}&offset=${offset}&length=${batchSize}`;
const response = await fetch(url);
if (!response.ok) {
if (offset === 0) {
throw new Error(`Failed to load dataset. Check dataset ID and split name.`);
}
break;
}
const data = await response.json();
if (!data.rows || data.rows.length === 0) {
hasMore = false;
break;
}
// Detect mode from first row
if (isFirstBatch) {
datasetMode = detectMode(data.rows[0].row);
modeBadge.textContent = datasetMode;
modeBadge.className = `mode-badge ${datasetMode}`;
modeBadge.style.display = 'inline-block';
}
const newRows = data.rows.map(item => ({
index: offset + item.row_idx,
image: item.row.image,
objects: item.row.objects || { bbox: [], category: [], score: [] },
...item.row
}));
dataset = dataset.concat(newRows);
offset += data.rows.length;
if (isFirstBatch) {
filterAndRender();
loadingDiv.textContent = `Loading more... (${dataset.length} rows loaded)`;
isFirstBatch = false;
} else {
filterDataset();
renderStats();
if (currentPage === 0) {
renderPage();
renderPagination();
}
loadingDiv.textContent = `Loading more... (${dataset.length} rows loaded)`;
}
if (data.rows.length < batchSize) {
hasMore = false;
}
if (dataset.length >= 10000) {
hasMore = false;
}
}
if (dataset.length === 0) {
throw new Error('No data found in dataset');
}
isLoadingComplete = true;
loadingDiv.style.display = 'none';
filterAndRender();
} catch (error) {
loadingDiv.style.display = 'none';
showError(`Failed to load dataset: ${error.message}`);
} finally {
loadButton.disabled = false;
if (isLoadingComplete) {
loadingDiv.textContent = 'Loading dataset...';
}
}
}
// Filter dataset based on current filters
function filterDataset() {
filteredDataset = dataset.filter(item => {
const scores = getScores(item);
const validDetections = scores.filter(score => score >= confidenceThreshold);
if (showOnlyDetections && validDetections.length === 0) {
return false;
}
return true;
});
}
// Filter and render
function filterAndRender() {
filterDataset();
currentPage = 0;
renderStats();
renderPage();
renderPagination();
}
// Render statistics
function renderStats() {
const totalImages = filteredDataset.length;
const totalDetections = filteredDataset.reduce((sum, item) => {
return sum + getDetectionCount(item);
}, 0);
const imagesWithDetections = filteredDataset.filter(item => {
return getDetectionCount(item) > 0;
}).length;
const avgDetections = totalImages > 0 ? (totalDetections / totalImages).toFixed(2) : 0;
const modeLabel = datasetMode === 'segmentation' ? 'Instances' : 'Detections';
statsContainer.innerHTML = `
<div class="stats">
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">${filteredDataset.length}</div>
<div class="stat-label">Filtered Images</div>
</div>
<div class="stat-item">
<div class="stat-value">${imagesWithDetections}</div>
<div class="stat-label">Images with ${modeLabel}</div>
</div>
<div class="stat-item">
<div class="stat-value">${totalDetections}</div>
<div class="stat-label">Total ${modeLabel}</div>
</div>
<div class="stat-item">
<div class="stat-value">${avgDetections}</div>
<div class="stat-label">Avg per Image</div>
</div>
</div>
</div>
`;
}
// Render current page
function renderPage() {
const start = currentPage * itemsPerPage;
const end = start + itemsPerPage;
const pageItems = filteredDataset.slice(start, end);
imageGrid.innerHTML = pageItems.map(item => {
const scores = getScores(item);
const validDetections = scores
.map((score, idx) => ({ score, idx }))
.filter(({ score }) => score >= confidenceThreshold);
const imageUrl = typeof item.image === 'object' ? item.image?.src : item.image;
// For segmentation, get mask image URL
let maskUrl = '';
if (datasetMode === 'segmentation' && item.segmentation_map) {
maskUrl = typeof item.segmentation_map === 'object' ? item.segmentation_map?.src : item.segmentation_map;
}
const countLabel = datasetMode === 'segmentation' ? 'instance(s)' : 'detection(s)';
return `
<div class="image-card">
<div class="image-container" id="container-${item.index}">
<img class="source-image" src="${imageUrl}" alt="Image ${item.index}" crossorigin="anonymous"
onload="onImageLoaded(${item.index})">
${maskUrl ? `<img class="mask-overlay" src="${maskUrl}" crossorigin="anonymous"
data-mask-url="${maskUrl}" style="display:none;"
onload="onMaskLoaded(${item.index})">` : ''}
<canvas id="canvas-${item.index}"></canvas>
</div>
<div class="image-info">
<div class="image-index">Image #${item.index}</div>
<div class="detections-count">${validDetections.length} ${countLabel}</div>
<div class="detections-list">
${validDetections.map(({ score, idx }) => {
const confidenceClass = score >= 0.7 ? 'confidence-high' : score >= 0.4 ? 'confidence-medium' : 'confidence-low';
return `
<div class="detection-item">
<span class="${confidenceClass}">${(score * 100).toFixed(1)}%</span>
confidence
</div>
`;
}).join('')}
</div>
</div>
</div>
`;
}).join('');
}
// Called when the source image loads
window.onImageLoaded = function(itemIndex) {
if (datasetMode === 'detection') {
drawBoundingBoxes(itemIndex);
} else {
// For segmentation, draw boxes if present; mask overlay handled separately
drawSegmentationBoxes(itemIndex);
}
};
// Called when the mask image loads - colorize and overlay it
window.onMaskLoaded = function(itemIndex) {
const container = document.getElementById(`container-${itemIndex}`);
const canvas = document.getElementById(`canvas-${itemIndex}`);
const sourceImg = container.querySelector('img.source-image');
const maskImg = container.querySelector('img.mask-overlay');
if (!canvas || !sourceImg || !maskImg) return;
canvas.width = sourceImg.naturalWidth;
canvas.height = sourceImg.naturalHeight;
const ctx = canvas.getContext('2d');
// Draw the mask onto a temporary canvas to read pixel data
const tempCanvas = document.createElement('canvas');
tempCanvas.width = maskImg.naturalWidth;
tempCanvas.height = maskImg.naturalHeight;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(maskImg, 0, 0);
let maskData;
try {
maskData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
} catch (e) {
console.warn('Cannot read mask pixels (CORS):', e);
return;
}
// Create colored overlay - each unique non-zero pixel value gets a color
const overlay = ctx.createImageData(canvas.width, canvas.height);
// Scale factors if mask and source image differ in size
const scaleX = tempCanvas.width / canvas.width;
const scaleY = tempCanvas.height / canvas.height;
for (let y = 0; y < canvas.height; y++) {
for (let x = 0; x < canvas.width; x++) {
const mx = Math.floor(x * scaleX);
const my = Math.floor(y * scaleY);
const mi = (my * tempCanvas.width + mx) * 4;
// Segmentation maps are grayscale: R channel has the instance ID
const instanceId = maskData.data[mi];
const oi = (y * canvas.width + x) * 4;
if (instanceId > 0) {
const color = MASK_COLORS[(instanceId - 1) % MASK_COLORS.length];
overlay.data[oi] = color[0];
overlay.data[oi + 1] = color[1];
overlay.data[oi + 2] = color[2];
overlay.data[oi + 3] = 100; // semi-transparent
}
}
}
ctx.putImageData(overlay, 0, 0);
// Now draw bounding boxes on top if present
drawSegmentationBoxes(itemIndex);
};
// Draw bounding boxes for detection datasets (objects column)
window.drawBoundingBoxes = function(itemIndex) {
const item = dataset.find(d => d.index === itemIndex);
if (!item) return;
const container = document.getElementById(`container-${itemIndex}`);
const canvas = document.getElementById(`canvas-${itemIndex}`);
const img = container.querySelector('img.source-image');
if (!canvas || !img) return;
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
const objects = item.objects || { bbox: [], category: [], score: [] };
objects.bbox.forEach((bbox, idx) => {
const score = objects.score[idx];
if (score < confidenceThreshold) return;
const [x, y, width, height] = bbox;
let color;
if (score >= 0.7) {
color = '#27ae60';
} else if (score >= 0.4) {
color = '#f39c12';
} else {
color = '#e74c3c';
}
ctx.strokeStyle = color;
ctx.lineWidth = 3;
ctx.strokeRect(x, y, width, height);
const label = `${(score * 100).toFixed(1)}%`;
ctx.font = 'bold 16px Arial';
const textWidth = ctx.measureText(label).width;
const textHeight = 20;
ctx.fillStyle = color;
ctx.fillRect(x, y - textHeight - 4, textWidth + 10, textHeight + 4);
ctx.fillStyle = 'white';
ctx.fillText(label, x + 5, y - 8);
});
};
// Draw bounding boxes for segmentation datasets (boxes column at top level)
window.drawSegmentationBoxes = function(itemIndex) {
const item = dataset.find(d => d.index === itemIndex);
if (!item || !item.boxes) return;
const container = document.getElementById(`container-${itemIndex}`);
const canvas = document.getElementById(`canvas-${itemIndex}`);
const img = container.querySelector('img.source-image');
if (!canvas || !img) return;
// Don't clear - mask overlay may already be drawn
const ctx = canvas.getContext('2d');
const scores = item.scores || [];
item.boxes.forEach((bbox, idx) => {
const score = scores[idx] !== undefined ? scores[idx] : 1.0;
if (score < confidenceThreshold) return;
const [x, y, width, height] = bbox;
let color;
if (score >= 0.7) {
color = '#27ae60';
} else if (score >= 0.4) {
color = '#f39c12';
} else {
color = '#e74c3c';
}
ctx.strokeStyle = color;
ctx.lineWidth = 3;
ctx.strokeRect(x, y, width, height);
const label = `${(score * 100).toFixed(1)}%`;
ctx.font = 'bold 16px Arial';
const textWidth = ctx.measureText(label).width;
const textHeight = 20;
ctx.fillStyle = color;
ctx.fillRect(x, y - textHeight - 4, textWidth + 10, textHeight + 4);
ctx.fillStyle = 'white';
ctx.fillText(label, x + 5, y - 8);
});
};
// Render pagination
function renderPagination() {
const totalPages = Math.ceil(filteredDataset.length / itemsPerPage);
if (totalPages <= 1) {
paginationDiv.style.display = 'none';
return;
}
paginationDiv.style.display = 'flex';
paginationDiv.innerHTML = `
<button id="prev-btn" ${currentPage === 0 ? 'disabled' : ''}>Previous</button>
<span>Page ${currentPage + 1} of ${totalPages}</span>
<button id="next-btn" ${currentPage >= totalPages - 1 ? 'disabled' : ''}>Next</button>
`;
document.getElementById('prev-btn').addEventListener('click', () => {
if (currentPage > 0) {
currentPage--;
renderPage();
renderPagination();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
});
document.getElementById('next-btn').addEventListener('click', () => {
if (currentPage < totalPages - 1) {
currentPage++;
renderPage();
renderPagination();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
});
}
// Show error message
function showError(message) {
errorDiv.textContent = message;
errorDiv.style.display = 'block';
}
// Auto-load default dataset on page load
window.addEventListener('load', () => {
loadDataset();
});
</script>
</body>
</html>