| <!DOCTYPE html> |
| <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> |