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