davanstrien HF Staff
Claude Opus 4.6 (1M context)
Support segmentation mask overlays alongside detection boxes
867ce05 Download index.html from uv-scripts/sam3-detection-browser: direct link, hf CLI and curl.
- Browser
- Download file 32.2 kB
-
https://huggingface.co/spaces/uv-scripts/sam3-detection-browser/resolve/main/index.html
- Command line
-
hf download hf://spaces/uv-scripts/sam3-detection-browser/index.html
-
curl -L -o index.html https://huggingface.co/spaces/uv-scripts/sam3-detection-browser/resolve/main/index.html
32.2 kB
| <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> | |
| 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> | |