CHang's picture
Update index.html
2387f9c verified
<!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>