Download index.html from CHang/ROVI-Dataset-Example-Viewer: direct link, hf CLI and curl.
- Browser
- Download file 40.6 kB
-
https://huggingface.co/spaces/CHang/ROVI-Dataset-Example-Viewer/resolve/main/index.html
- Command line
-
hf download hf://spaces/CHang/ROVI-Dataset-Example-Viewer/index.html
-
curl -L -o index.html https://huggingface.co/spaces/CHang/ROVI-Dataset-Example-Viewer/resolve/main/index.html
40.6 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Dataset Image Viewer</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| padding: 10px; | |
| } | |
| .viewer-container { | |
| background: rgba(255, 255, 255, 0.95); | |
| backdrop-filter: blur(10px); | |
| border-radius: 15px; | |
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2); | |
| padding: 20px; | |
| width: calc(100vw - 20px); | |
| max-height: calc(100vh - 20px); | |
| overflow-y: auto; | |
| } | |
| .header { | |
| text-align: center; | |
| margin-bottom: 20px; | |
| } | |
| .header h1 { | |
| color: #333; | |
| font-size: 1.8rem; | |
| font-weight: 700; | |
| margin-bottom: 8px; | |
| background: linear-gradient(45deg, #667eea, #764ba2); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .loading-screen { | |
| text-align: center; | |
| padding: 60px 20px; | |
| color: #333; | |
| } | |
| .loading-title { | |
| font-size: 1.2rem; | |
| font-weight: 600; | |
| margin-bottom: 20px; | |
| color: #555; | |
| } | |
| .loading-progress { | |
| background: rgba(102, 126, 234, 0.1); | |
| border-radius: 25px; | |
| padding: 20px; | |
| margin: 20px auto; | |
| max-width: 500px; | |
| border: 2px solid rgba(102, 126, 234, 0.2); | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| height: 8px; | |
| background: #e9ecef; | |
| border-radius: 10px; | |
| overflow: hidden; | |
| margin: 15px 0; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(45deg, #667eea, #764ba2); | |
| border-radius: 10px; | |
| transition: width 0.3s ease; | |
| width: 0%; | |
| } | |
| .progress-text { | |
| font-size: 1rem; | |
| font-weight: 600; | |
| color: #667eea; | |
| margin-bottom: 10px; | |
| } | |
| .progress-details { | |
| font-size: 0.9rem; | |
| color: #666; | |
| line-height: 1.5; | |
| } | |
| .spinner-large { | |
| border: 4px solid #f3f3f3; | |
| border-top: 4px solid #667eea; | |
| border-radius: 50%; | |
| width: 50px; | |
| height: 50px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 20px; | |
| } | |
| .max-demo-notice { | |
| background: rgba(255, 193, 7, 0.1); | |
| border: 2px solid #ffc107; | |
| border-radius: 15px; | |
| padding: 15px; | |
| margin: 15px 0; | |
| text-align: center; | |
| color: #856404; | |
| } | |
| .navigation { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 20px; | |
| margin-bottom: 20px; | |
| flex-wrap: wrap; | |
| } | |
| .nav-btn, .toggle-btn { | |
| background: linear-gradient(45deg, #667eea, #764ba2); | |
| color: white; | |
| border: none; | |
| padding: 10px 20px; | |
| border-radius: 25px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| font-weight: 600; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); | |
| } | |
| .nav-btn:hover, .toggle-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 25px rgba(102, 126, 234, 0.4); | |
| } | |
| .nav-btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .toggle-btn.active { | |
| background: linear-gradient(45deg, #28a745, #20c997); | |
| } | |
| .toggle-btn.inactive { | |
| background: linear-gradient(45deg, #dc3545, #fd7e14); | |
| } | |
| .image-counter { | |
| background: rgba(102, 126, 234, 0.1); | |
| padding: 8px 16px; | |
| border-radius: 20px; | |
| font-weight: 600; | |
| color: #333; | |
| } | |
| .loading-indicator { | |
| background: rgba(255, 193, 7, 0.1); | |
| padding: 5px 12px; | |
| border-radius: 15px; | |
| font-size: 12px; | |
| color: #856404; | |
| font-weight: 500; | |
| } | |
| .main-content { | |
| display: grid; | |
| grid-template-columns: 1fr 350px; | |
| gap: 20px; | |
| margin-bottom: 20px; | |
| } | |
| .image-section { | |
| background: #f8f9fa; | |
| border-radius: 15px; | |
| overflow: hidden; | |
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); | |
| position: relative; | |
| } | |
| .image-container { | |
| position: relative; | |
| width: 100%; | |
| height: 70vh; | |
| overflow: hidden; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: #f0f0f0; | |
| } | |
| .main-image { | |
| max-width: 100%; | |
| max-height: 100%; | |
| width: auto; | |
| height: auto; | |
| display: block; | |
| object-fit: contain; | |
| } | |
| .image-overlay { | |
| position: absolute; | |
| top: 10px; | |
| right: 10px; | |
| background: rgba(0, 0, 0, 0.7); | |
| color: white; | |
| padding: 5px 10px; | |
| border-radius: 5px; | |
| font-size: 12px; | |
| z-index: 10; | |
| } | |
| .bounding-box { | |
| position: absolute; | |
| border: 3px solid; | |
| background: transparent; | |
| pointer-events: none; | |
| transition: all 0.3s ease; | |
| z-index: 5; | |
| } | |
| .bounding-box.active { | |
| opacity: 1; | |
| } | |
| .bounding-box.inactive { | |
| opacity: 0; | |
| } | |
| .box-label { | |
| position: absolute; | |
| color: white; | |
| padding: 4px 8px; | |
| font-size: 12px; | |
| font-weight: 700; | |
| border-radius: 4px; | |
| max-width: 200px; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| line-height: 1.4; | |
| text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8); | |
| min-height: 20px; | |
| display: flex; | |
| align-items: center; | |
| z-index: 15; | |
| } | |
| .box-label.active { | |
| opacity: 1; | |
| } | |
| .box-label.inactive { | |
| opacity: 0; | |
| } | |
| .metadata-panel { | |
| background: #f8f9fa; | |
| border-radius: 15px; | |
| padding: 15px; | |
| overflow-y: auto; | |
| max-height: 70vh; | |
| } | |
| .metadata-section { | |
| margin-bottom: 15px; | |
| } | |
| .metadata-title { | |
| font-weight: 700; | |
| color: #333; | |
| margin-bottom: 8px; | |
| font-size: 1rem; | |
| border-bottom: 2px solid #667eea; | |
| padding-bottom: 3px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .metadata-content { | |
| color: #666; | |
| line-height: 1.4; | |
| font-size: 13px; | |
| } | |
| .labels-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); | |
| gap: 6px; | |
| max-height: 300px; | |
| overflow-y: auto; | |
| align-items: stretch; | |
| } | |
| .label-tag { | |
| padding: 10px 14px; | |
| border-radius: 15px; | |
| font-size: 15px; | |
| text-align: center; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| border: 2px solid transparent; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| min-height: 40px; | |
| word-wrap: break-word; | |
| hyphens: auto; | |
| line-height: 1.2; | |
| } | |
| .label-tag.active { | |
| background: linear-gradient(45deg, #198754, #20c997); | |
| color: white; | |
| border-color: #198754; | |
| box-shadow: 0 2px 8px rgba(25, 135, 84, 0.3); | |
| } | |
| .label-tag.inactive { | |
| background: #e9ecef; | |
| color: #6c757d; | |
| border-color: #dee2e6; | |
| } | |
| .label-tag:hover { | |
| transform: translateY(-1px); | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); | |
| } | |
| .source-meta-section { | |
| margin-top: 10px; | |
| padding-top: 10px; | |
| border-top: 1px solid #dee2e6; | |
| } | |
| .source-meta-content { | |
| max-height: 200px; | |
| overflow-y: auto; | |
| font-size: 13px; | |
| line-height: 1.4; | |
| color: #666; | |
| } | |
| .captions-section { | |
| background: white; | |
| border-radius: 15px; | |
| padding: 20px; | |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); | |
| grid-column: 1 / -1; | |
| } | |
| .caption-item { | |
| margin-bottom: 15px; | |
| padding: 15px; | |
| background: rgba(102, 126, 234, 0.05); | |
| border-left: 4px solid #667eea; | |
| border-radius: 0 10px 10px 0; | |
| } | |
| .caption-label { | |
| font-weight: 600; | |
| color: #333; | |
| margin-bottom: 5px; | |
| } | |
| .caption-text { | |
| color: #555; | |
| line-height: 1.6; | |
| font-size: 14px; | |
| } | |
| .error-message { | |
| background: #ff4757; | |
| color: white; | |
| padding: 15px; | |
| border-radius: 10px; | |
| text-align: center; | |
| margin: 20px 0; | |
| } | |
| .loading { | |
| text-align: center; | |
| padding: 40px; | |
| color: #666; | |
| font-size: 16px; | |
| } | |
| .image-loading { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| background: rgba(255, 255, 255, 0.9); | |
| padding: 20px; | |
| border-radius: 10px; | |
| text-align: center; | |
| z-index: 20; | |
| } | |
| .spinner { | |
| border: 3px solid #f3f3f3; | |
| border-top: 3px solid #667eea; | |
| border-radius: 50%; | |
| width: 30px; | |
| height: 30px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 10px; | |
| } | |
| .header-links { | |
| display: inline-flex; | |
| gap: 12px; | |
| margin-left: 15px; | |
| align-items: center; | |
| } | |
| .header-link { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 32px; | |
| height: 32px; | |
| background: rgba(102, 126, 234, 0.1); | |
| border-radius: 8px; | |
| text-decoration: none; | |
| transition: all 0.3s ease; | |
| font-size: 16px; | |
| } | |
| .header-link:hover { | |
| background: rgba(102, 126, 234, 0.2); | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); | |
| } | |
| .header-link svg { | |
| color: #667eea; | |
| transition: color 0.3s ease; | |
| } | |
| .header-link:hover svg { | |
| color: #5a6fd8; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| @media (max-width: 1024px) { | |
| .main-content { | |
| grid-template-columns: 1fr; | |
| } | |
| .metadata-panel { | |
| max-height: none; | |
| } | |
| .image-container { | |
| height: 50vh; | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .navigation { | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .header h1 { | |
| font-size: 1.5rem; | |
| } | |
| .viewer-container { | |
| padding: 15px; | |
| } | |
| .loading-progress { | |
| margin: 20px 10px; | |
| padding: 15px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="viewer-container"> | |
| <div class="header"> | |
| <h1>📊 ROVI Example Viewer | |
| <span class="header-links"> | |
| <a href="https://github.com/CihangPeng/ROVI" target="_blank" class="header-link" title="GitHub Repository"> | |
| <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> | |
| <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/> | |
| </svg> | |
| </a> | |
| <a href="https://huggingface.co/datasets/CHang/ROVI" target="_blank" class="header-link" title="Hugging Face Dataset"> | |
| <img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" width="16" height="16" alt="Hugging Face" style="display: block;"> | |
| </a> | |
| </span> | |
| </h1> | |
| </div> | |
| <div id="loadingScreen" class="loading-screen"> | |
| <div class="spinner-large"></div> | |
| <div class="loading-title">Loading Examples</div> | |
| <div class="loading-progress"> | |
| <div class="progress-text" id="progressText">Loading json annotation file...</div> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" id="progressFill"></div> | |
| </div> | |
| <div class="progress-details" id="progressDetails"> | |
| Please wait while we load and validate images | |
| </div> | |
| </div> | |
| </div> | |
| <div id="errorMessage" class="error-message" style="display: none;"> | |
| ❌ Failed to load annotation file. Please ensure the JSON path is accessible. | |
| </div> | |
| <div id="mainViewer" style="display: none;"> | |
| <div id="maxDemoNotice" class="max-demo-notice" style="display: none;"> | |
| 🎯 <strong>Demo Limit Reached:</strong> Displaying maximum of 100 images for optimal performance | |
| </div> | |
| <div class="navigation"> | |
| <button class="nav-btn" id="prevBtn" onclick="navigatePrevious()">← Previous</button> | |
| <div class="image-counter"> | |
| <span id="currentIndex">1</span> / <span id="totalImages">0</span> | |
| </div> | |
| <button class="nav-btn" id="nextBtn" onclick="navigateNext()">Next →</button> | |
| <button class="toggle-btn active" id="globalToggle" onclick="toggleAllBoxes()"> | |
| Hide All Boxes | |
| </button> | |
| <div id="cacheStatus" class="loading-indicator" style="display: none;"> | |
| 📥 Caching images... | |
| </div> | |
| </div> | |
| <div class="main-content"> | |
| <div class="image-section"> | |
| <div class="image-container" id="imageContainer"> | |
| <div id="imageLoading" class="image-loading" style="display: none;"> | |
| <div class="spinner"></div> | |
| <div>Loading image...</div> | |
| </div> | |
| <img id="mainImage" class="main-image" alt="Dataset image" /> | |
| <div class="image-overlay"> | |
| <span id="imageId"></span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="metadata-panel"> | |
| <div class="metadata-section"> | |
| <div class="metadata-title">📏 Dimensions</div> | |
| <div class="metadata-content" id="dimensions"></div> | |
| </div> | |
| <div class="metadata-section"> | |
| <div class="metadata-title"> | |
| 🏷️ Labels | |
| <small style="font-size: 10px; color: #999;">Click to toggle boxes</small> | |
| </div> | |
| <div class="labels-grid" id="labelsContainer"></div> | |
| </div> | |
| <div class="metadata-section"> | |
| <div class="metadata-title">📊 Details</div> | |
| <div class="metadata-content" id="sourceInfo"></div> | |
| </div> | |
| <div class="metadata-section source-meta-section"> | |
| <div class="metadata-title">🔍 Source Meta</div> | |
| <div class="source-meta-content" id="sourceMeta"></div> | |
| </div> | |
| </div> | |
| <div class="captions-section"> | |
| <div class="metadata-title">💬 Captions</div> | |
| <div class="caption-item"> | |
| <div class="caption-label">VLM Description</div> | |
| <div class="caption-text" id="vlmCaption"></div> | |
| </div> | |
| <div class="caption-item"> | |
| <div class="caption-label">Web Caption</div> | |
| <div class="caption-text" id="webCaption"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| class DatasetViewer { | |
| constructor() { | |
| this.dataset = {}; | |
| this.validImages = []; | |
| this.imageCache = new Map(); | |
| this.allImageIds = []; | |
| this.currentIndex = 0; | |
| this.currentImageData = null; | |
| this.boxStates = {}; | |
| this.MAX_IMAGES = 100; | |
| this.INITIAL_CACHE_SIZE = 5; | |
| this.LOOKAHEAD_CACHE_SIZE = 10; | |
| this.boxColors = [ | |
| '#FF0066', '#00FF66', '#6600FF', '#FF6600', '#00FFFF', | |
| '#FF0099', '#99FF00', '#0099FF', '#FF9900', '#9900FF', | |
| '#00FF99', '#FF3300', '#3300FF', '#FFFF00', '#FF00FF', | |
| '#00CCFF', '#FF6699', '#66FF99', '#9966FF', '#FFCC00' | |
| ]; | |
| this.isBackgroundLoading = false; | |
| this.nextImageIndex = 0; | |
| this.maxReached = false; | |
| } | |
| shuffleArray(array) { | |
| const shuffled = [...array]; | |
| for (let i = shuffled.length - 1; i > 0; i--) { | |
| const j = Math.floor(Math.random() * (i + 1)); | |
| [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; | |
| } | |
| return shuffled; | |
| } | |
| updateProgress(current, total, message, details) { | |
| const progressText = document.getElementById('progressText'); | |
| const progressFill = document.getElementById('progressFill'); | |
| const progressDetails = document.getElementById('progressDetails'); | |
| const percentage = total > 0 ? (current / total) * 100 : 0; | |
| progressText.textContent = message; | |
| progressFill.style.width = `${percentage}%`; | |
| progressDetails.textContent = details; | |
| } | |
| async loadDataset() { | |
| try { | |
| this.updateProgress(0, 100, 'Loading dataset file...', 'Fetching JSON data from server'); | |
| const response = await fetch('https://huggingface.co/datasets/CHang/ROVI/raw/main/sampled_ROVI_val_1000.json'); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}: ${response.statusText}`); | |
| } | |
| this.updateProgress(30, 100, 'Parsing example meta...', 'Processing JSON data (downloading 3.14MB from CHang/ROVI)'); | |
| this.dataset = await response.json(); | |
| this.allImageIds = this.shuffleArray(Object.keys(this.dataset)); | |
| if (this.allImageIds.length === 0) { | |
| throw new Error('Dataset is empty'); | |
| } | |
| this.updateProgress(60, 100, 'Caching initial images...', 'Loading first batch for display'); | |
| await this.loadInitialImages(); | |
| if (this.validImages.length === 0) { | |
| throw new Error('No valid images found'); | |
| } | |
| this.updateProgress(100, 100, 'Ready!', `Loaded ${this.validImages.length} images`); | |
| setTimeout(() => this.initializeViewer(), 800); | |
| } catch (error) { | |
| console.error('Error loading dataset:', error); | |
| this.showError(`Failed to load dataset: ${error.message}`); | |
| } | |
| } | |
| async loadInitialImages() { | |
| this.validImages = []; | |
| this.nextImageIndex = 0; | |
| let attempts = 0; | |
| const maxAttempts = Math.min(50, this.allImageIds.length); | |
| while (this.validImages.length < this.INITIAL_CACHE_SIZE && attempts < maxAttempts) { | |
| const imageId = this.allImageIds[this.nextImageIndex]; | |
| const imageData = this.dataset[imageId]; | |
| this.updateProgress( | |
| this.validImages.length, | |
| this.INITIAL_CACHE_SIZE, | |
| `Validating images (with short timeout)... (${this.validImages.length}/${this.INITIAL_CACHE_SIZE})`, | |
| `Testing: ${imageId.substring(0, 30)}... | Valid: ${this.validImages.length} | Skipped: ${attempts - this.validImages.length}` | |
| ); | |
| try { | |
| await this.preloadImage(imageData.url); | |
| this.validImages.push(imageId); | |
| } catch (error) { | |
| console.warn(`Image validation failed for ${imageId}:`, error.message); | |
| } | |
| this.nextImageIndex++; | |
| attempts++; | |
| if (this.nextImageIndex >= this.allImageIds.length) { | |
| console.warn('Reached end of dataset during initial load'); | |
| break; | |
| } | |
| } | |
| if (this.validImages.length === 0) { | |
| throw new Error('No valid images found in dataset'); | |
| } | |
| } | |
| async startBackgroundCaching() { | |
| if (this.isBackgroundLoading || this.validImages.length >= this.MAX_IMAGES) return; | |
| this.isBackgroundLoading = true; | |
| const cacheStatus = document.getElementById('cacheStatus'); | |
| while (this.validImages.length < this.MAX_IMAGES && this.nextImageIndex < this.allImageIds.length) { | |
| const currentCacheAhead = Math.min(this.LOOKAHEAD_CACHE_SIZE, this.MAX_IMAGES - this.validImages.length); | |
| const targetSize = this.validImages.length + currentCacheAhead; | |
| cacheStatus.style.display = 'block'; | |
| let loaded = 0; | |
| let attempts = 0; | |
| const maxBackgroundAttempts = Math.min(30, this.allImageIds.length - this.nextImageIndex); | |
| while (this.validImages.length < targetSize && attempts < maxBackgroundAttempts && this.nextImageIndex < this.allImageIds.length) { | |
| const imageId = this.allImageIds[this.nextImageIndex]; | |
| const imageData = this.dataset[imageId]; | |
| try { | |
| await this.preloadImage(imageData.url); | |
| this.validImages.push(imageId); | |
| loaded++; | |
| cacheStatus.textContent = `📥 Cached ${loaded} images (${this.validImages.length} total)`; | |
| this.updateNavigationButtons(); | |
| } catch (error) { | |
| console.warn(`Background validation failed for ${imageId}:`, error.message); | |
| } | |
| this.nextImageIndex++; | |
| attempts++; | |
| if (loaded % 2 === 0) { | |
| await new Promise(resolve => setTimeout(resolve, 10)); | |
| } | |
| } | |
| if (this.validImages.length >= this.MAX_IMAGES && !this.maxReached) { | |
| this.maxReached = true; | |
| document.getElementById('maxDemoNotice').style.display = 'block'; | |
| cacheStatus.textContent = '🎯 Maximum demo images reached'; | |
| setTimeout(() => cacheStatus.style.display = 'none', 3000); | |
| break; | |
| } | |
| if (this.nextImageIndex >= this.allImageIds.length) { | |
| cacheStatus.textContent = '✅ All available images processed'; | |
| setTimeout(() => cacheStatus.style.display = 'none', 2000); | |
| break; | |
| } | |
| cacheStatus.style.display = 'none'; | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| } | |
| this.isBackgroundLoading = false; | |
| } | |
| async preloadImage(url) { | |
| if (this.imageCache.has(url)) { | |
| return this.imageCache.get(url); | |
| } | |
| return new Promise((resolve, reject) => { | |
| const img = new Image(); | |
| let isResolved = false; | |
| img.onload = () => { | |
| if (isResolved) return; | |
| isResolved = true; | |
| if (img.naturalWidth === 0 || img.naturalHeight === 0) { | |
| reject(new Error(`Invalid image dimensions: ${url}`)); | |
| return; | |
| } | |
| this.imageCache.set(url, img); | |
| resolve(img); | |
| }; | |
| img.onerror = () => { | |
| if (isResolved) return; | |
| isResolved = true; | |
| reject(new Error(`Failed to load: ${url}`)); | |
| }; | |
| img.onabort = () => { | |
| if (isResolved) return; | |
| isResolved = true; | |
| reject(new Error(`Load aborted: ${url}`)); | |
| }; | |
| const timeoutId = setTimeout(() => { | |
| if (isResolved) return; | |
| isResolved = true; | |
| img.src = ''; | |
| reject(new Error(`Timeout: ${url}`)); | |
| }, 3000); | |
| img.src = url; | |
| img.onload = () => { | |
| clearTimeout(timeoutId); | |
| if (isResolved) return; | |
| isResolved = true; | |
| if (img.naturalWidth === 0 || img.naturalHeight === 0) { | |
| reject(new Error(`Invalid dimensions: ${url}`)); | |
| return; | |
| } | |
| this.imageCache.set(url, img); | |
| resolve(img); | |
| }; | |
| }); | |
| } | |
| initializeViewer() { | |
| document.getElementById('loadingScreen').style.display = 'none'; | |
| document.getElementById('mainViewer').style.display = 'block'; | |
| document.getElementById('totalImages').textContent = this.validImages.length; | |
| this.currentIndex = 0; | |
| this.displayImage(this.currentIndex); | |
| setTimeout(() => this.startBackgroundCaching(), 1000); | |
| } | |
| displayImage(index) { | |
| if (index < 0 || index >= this.validImages.length) { | |
| console.error(`Invalid index ${index}, valid range: 0-${this.validImages.length - 1}`); | |
| return; | |
| } | |
| const imageId = this.validImages[index]; | |
| this.currentImageData = this.dataset[imageId]; | |
| this.currentIndex = index; | |
| this.boxStates = {}; | |
| this.currentImageData.labels.forEach((_, i) => { | |
| this.boxStates[i] = true; | |
| }); | |
| this.updateNavigationButtons(); | |
| const img = document.getElementById('mainImage'); | |
| const cachedImage = this.imageCache.get(this.currentImageData.url); | |
| img.src = cachedImage.src; | |
| this.updateMetadata(imageId); | |
| this.updateLabels(); | |
| this.drawBoundingBoxes(); | |
| this.updateGlobalToggleButton(); | |
| if (index >= this.validImages.length - 5 && !this.isBackgroundLoading && this.validImages.length < this.MAX_IMAGES) { | |
| this.startBackgroundCaching(); | |
| } | |
| } | |
| updateNavigationButtons() { | |
| document.getElementById('currentIndex').textContent = this.currentIndex + 1; | |
| document.getElementById('totalImages').textContent = this.validImages.length; | |
| document.getElementById('prevBtn').disabled = this.currentIndex === 0; | |
| document.getElementById('nextBtn').disabled = this.currentIndex >= this.validImages.length - 1; | |
| } | |
| updateMetadata(imageId) { | |
| document.getElementById('imageId').textContent = imageId; | |
| document.getElementById('dimensions').textContent = | |
| `${this.currentImageData.width} × ${this.currentImageData.height}px`; | |
| const sourceInfo = document.getElementById('sourceInfo'); | |
| sourceInfo.innerHTML = ` | |
| <strong>Source:</strong> ${this.currentImageData.source}<br> | |
| <strong>PHash:</strong> ${this.currentImageData.phash}<br> | |
| <strong>Bounding Boxes:</strong> ${this.currentImageData.box_num}<br> | |
| <strong>Categories:</strong> ${this.currentImageData.category_num}<br> | |
| <strong>VLM caption tokens (CLIP):</strong> ${this.currentImageData.vlm_clip_tok_num}<br> | |
| <strong>Web caption tokens (CLIP):</strong> ${this.currentImageData.web_clip_tok_num} | |
| `; | |
| const sourceMeta = document.getElementById('sourceMeta'); | |
| if (this.currentImageData.source_meta && typeof this.currentImageData.source_meta === 'object') { | |
| let metaHTML = ''; | |
| Object.entries(this.currentImageData.source_meta).forEach(([key, value]) => { | |
| let displayValue = value; | |
| if (typeof value === 'number') { | |
| displayValue = Number.isInteger(value) ? value : value.toFixed(3); | |
| } else if (typeof value === 'object') { | |
| displayValue = JSON.stringify(value, null, 2); | |
| } | |
| metaHTML += `<strong>${key}:</strong> ${displayValue}<br>`; | |
| }); | |
| sourceMeta.innerHTML = metaHTML; | |
| } else { | |
| sourceMeta.innerHTML = '<em>No source meta available</em>'; | |
| } | |
| document.getElementById('vlmCaption').textContent = this.currentImageData.vlm_description; | |
| document.getElementById('webCaption').textContent = this.currentImageData.web_caption; | |
| } | |
| updateLabels() { | |
| const labelsContainer = document.getElementById('labelsContainer'); | |
| labelsContainer.innerHTML = ''; | |
| this.currentImageData.labels.forEach((label, index) => { | |
| const labelTag = document.createElement('div'); | |
| labelTag.className = `label-tag ${this.boxStates[index] ? 'active' : 'inactive'}`; | |
| labelTag.textContent = label; | |
| labelTag.onclick = () => this.toggleBox(index); | |
| labelTag.style.borderColor = this.boxColors[index % this.boxColors.length]; | |
| labelsContainer.appendChild(labelTag); | |
| }); | |
| } | |
| drawBoundingBoxes() { | |
| const existingBoxes = document.querySelectorAll('.bounding-box, .box-label'); | |
| existingBoxes.forEach(element => element.remove()); | |
| const container = document.getElementById('imageContainer'); | |
| const img = document.getElementById('mainImage'); | |
| setTimeout(() => { | |
| const containerRect = container.getBoundingClientRect(); | |
| const imgRect = img.getBoundingClientRect(); | |
| const displayedWidth = imgRect.width; | |
| const displayedHeight = imgRect.height; | |
| const scaleX = displayedWidth / this.currentImageData.width; | |
| const scaleY = displayedHeight / this.currentImageData.height; | |
| const offsetX = imgRect.left - containerRect.left; | |
| const offsetY = imgRect.top - containerRect.top; | |
| this.currentImageData.bboxes.forEach((bbox, index) => { | |
| const [x1, y1, x2, y2] = bbox; | |
| const color = this.boxColors[index % this.boxColors.length]; | |
| const boxDiv = document.createElement('div'); | |
| boxDiv.className = `bounding-box ${this.boxStates[index] ? 'active' : 'inactive'}`; | |
| boxDiv.style.left = `${offsetX + (x1 * scaleX)}px`; | |
| boxDiv.style.top = `${offsetY + (y1 * scaleY)}px`; | |
| boxDiv.style.width = `${(x2 - x1) * scaleX}px`; | |
| boxDiv.style.height = `${(y2 - y1) * scaleY}px`; | |
| boxDiv.style.borderColor = color; | |
| container.appendChild(boxDiv); | |
| const labelDiv = document.createElement('div'); | |
| labelDiv.className = `box-label ${this.boxStates[index] ? 'active' : 'inactive'}`; | |
| labelDiv.textContent = this.currentImageData.labels[index]; | |
| labelDiv.style.backgroundColor = color; | |
| labelDiv.style.left = `${offsetX + (x1 * scaleX) + 4}px`; | |
| labelDiv.style.top = `${offsetY + (y1 * scaleY) + 4}px`; | |
| container.appendChild(labelDiv); | |
| }); | |
| }, 50); | |
| } | |
| toggleBox(index) { | |
| this.boxStates[index] = !this.boxStates[index]; | |
| this.updateLabels(); | |
| this.drawBoundingBoxes(); | |
| this.updateGlobalToggleButton(); | |
| } | |
| updateGlobalToggleButton() { | |
| const toggleBtn = document.getElementById('globalToggle'); | |
| const visibleBoxes = Object.values(this.boxStates).filter(state => state).length; | |
| if (visibleBoxes === 0) { | |
| toggleBtn.className = 'toggle-btn inactive'; | |
| toggleBtn.textContent = 'Show All Boxes'; | |
| } else { | |
| toggleBtn.className = 'toggle-btn active'; | |
| toggleBtn.textContent = 'Hide All Boxes'; | |
| } | |
| } | |
| toggleAllBoxes() { | |
| const visibleBoxes = Object.values(this.boxStates).filter(state => state).length; | |
| const shouldShowAll = visibleBoxes === 0; | |
| Object.keys(this.boxStates).forEach(key => { | |
| this.boxStates[key] = shouldShowAll; | |
| }); | |
| this.updateLabels(); | |
| this.drawBoundingBoxes(); | |
| this.updateGlobalToggleButton(); | |
| } | |
| navigatePrevious() { | |
| if (this.currentIndex > 0) { | |
| this.displayImage(this.currentIndex - 1); | |
| } | |
| } | |
| navigateNext() { | |
| if (this.currentIndex < this.validImages.length - 1) { | |
| this.displayImage(this.currentIndex + 1); | |
| } | |
| } | |
| showError(message) { | |
| document.getElementById('loadingScreen').style.display = 'none'; | |
| document.getElementById('errorMessage').style.display = 'block'; | |
| document.getElementById('errorMessage').innerHTML = ` | |
| ❌ ${message}<br> | |
| <small>Please check the console for more details</small> | |
| `; | |
| } | |
| } | |
| let viewer = null; | |
| function navigatePrevious() { | |
| if (viewer) viewer.navigatePrevious(); | |
| } | |
| function navigateNext() { | |
| if (viewer) viewer.navigateNext(); | |
| } | |
| function toggleAllBoxes() { | |
| if (viewer) viewer.toggleAllBoxes(); | |
| } | |
| document.addEventListener('keydown', function(e) { | |
| if (!viewer) return; | |
| if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') { | |
| e.preventDefault(); | |
| viewer.navigatePrevious(); | |
| } else if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') { | |
| e.preventDefault(); | |
| viewer.navigateNext(); | |
| } else if (e.key === ' ') { | |
| e.preventDefault(); | |
| viewer.toggleAllBoxes(); | |
| } | |
| }); | |
| window.addEventListener('resize', () => { | |
| if (viewer && viewer.currentImageData) { | |
| setTimeout(() => viewer.drawBoundingBoxes(), 100); | |
| } | |
| }); | |
| document.addEventListener('DOMContentLoaded', () => { | |
| viewer = new DatasetViewer(); | |
| viewer.loadDataset(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |