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