depth-anything-v2 / index.html
josrano's picture
Upload folder using huggingface_hub
e763cf1 verified
Raw
History Blame Contribute Delete
19.9 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Depth Anything V2 — In-Browser</title>
<style>
:root {
--bg: #0f0f1a;
--card: #1a1a2e;
--accent: #6c63ff;
--accent-hover: #5a52d5;
--text: #e0e0e0;
--text-dim: #888;
--border: #2a2a4a;
--radius: 12px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
padding: 24px;
}
.container { max-width: 960px; margin: 0 auto; }
h1 { font-size: 1.8rem; margin-bottom: 4px; }
.subtitle { color: var(--text-dim); font-size: 0.95rem; margin-bottom: 24px; }
.subtitle a { color: var(--accent); text-decoration: none; }
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 24px;
margin-bottom: 20px;
}
.controls {
display: flex;
gap: 16px;
align-items: flex-end;
flex-wrap: wrap;
margin-bottom: 20px;
}
.field { display: flex; flex-direction: column; gap: 6px; }
.field label { font-size: 0.85rem; color: var(--text-dim); }
select, button {
background: var(--bg);
color: var(--text);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 16px;
font-size: 0.95rem;
cursor: pointer;
transition: border-color 0.2s;
}
select:hover, button:hover { border-color: var(--accent); }
button.primary {
background: var(--accent);
border-color: var(--accent);
font-weight: 600;
}
button.primary:hover { background: var(--accent-hover); }
button:disabled { opacity: 0.5; cursor: not-allowed; }
.upload-area {
border: 2px dashed var(--border);
border-radius: var(--radius);
padding: 40px;
text-align: center;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
margin-bottom: 16px;
}
.upload-area:hover, .upload-area.dragover {
border-color: var(--accent);
background: rgba(108, 99, 255, 0.05);
}
.upload-area p { color: var(--text-dim); font-size: 0.9rem; }
.upload-area .icon { font-size: 2rem; margin-bottom: 8px; }
#file-input { display: none; }
.examples { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 16px; }
.examples span { font-size: 0.85rem; color: var(--text-dim); width: 100%; margin-bottom: 4px; }
.example-img {
width: 80px; height: 60px;
object-fit: cover;
border-radius: 6px;
border: 2px solid var(--border);
cursor: pointer;
transition: border-color 0.2s;
}
.example-img:hover { border-color: var(--accent); }
.progress-wrap { margin-bottom: 16px; }
.progress-bar {
height: 6px;
background: var(--bg);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--accent);
width: 0%;
transition: width 0.3s;
}
.progress-label { font-size: 0.85rem; color: var(--text-dim); margin-top: 6px; }
.result-container { position: relative; border-radius: var(--radius); overflow: hidden; }
.slider-container {
position: relative;
display: none;
user-select: none;
}
.slider-container.active { display: block; }
.slider-container img {
width: 100%;
display: block;
}
.slider-overlay {
position: absolute;
top: 0; left: 0;
width: 50%;
height: 100%;
overflow: hidden;
}
.slider-overlay img {
width: 100vw;
max-width: 912px;
position: absolute;
top: 0; left: 0;
}
.slider-handle {
position: absolute;
top: 0; bottom: 0;
left: 50%;
width: 4px;
background: white;
cursor: ew-resize;
transform: translateX(-50%);
}
.slider-handle::after {
content: '⇄';
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
background: white;
color: #333;
border-radius: 50%;
width: 32px; height: 32px;
display: flex; align-items: center; justify-content: center;
font-size: 1rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.status {
padding: 12px 16px;
border-radius: 8px;
font-size: 0.9rem;
margin-bottom: 16px;
display: none;
}
.status.active { display: block; }
.status.info { background: rgba(108, 99, 255, 0.15); border: 1px solid var(--accent); }
.status.error { background: rgba(255, 80, 80, 0.15); border: 1px solid #ff5050; }
.status.success { background: rgba(80, 255, 120, 0.1); border: 1px solid #50ff78; }
.badge {
display: inline-block;
font-size: 0.75rem;
padding: 2px 8px;
border-radius: 4px;
background: rgba(108, 99, 255, 0.2);
color: var(--accent);
margin-left: 8px;
}
.download-row { display: flex; gap: 10px; margin-top: 12px; }
.download-row a {
font-size: 0.85rem;
color: var(--accent);
text-decoration: none;
padding: 6px 12px;
border: 1px solid var(--border);
border-radius: 6px;
}
.download-row a:hover { border-color: var(--accent); }
</style>
</head>
<body>
<div class="container">
<h1>Depth Anything V2 <span class="badge">runs in your browser</span></h1>
<p class="subtitle">
Monocular depth estimation via
<a href="https://arxiv.org/abs/2406.09463" target="_blank">Depth Anything V2</a>
(Yang et al., 2024) · powered by
<a href="https://huggingface.co/docs/transformers.js" target="_blank">Transformers.js</a>
+ ONNX Runtime · no server, no GPU needed
</p>
<div class="card">
<div class="controls">
<div class="field">
<label for="model-select">Model size</label>
<select id="model-select">
<option value="onnx-community/depth-anything-v2-small-ONNX" selected>Small (25M — fastest)</option>
<option value="onnx-community/depth-anything-v2-base-ONNX">Base (98M — balanced)</option>
<option value="onnx-community/depth-anything-v2-large-ONNX">Large (335M — best quality)</option>
</select>
</div>
<div class="field">
<label for="device-select">Device</label>
<select id="device-select">
<option value="webgpu" selected>WebGPU (if available)</option>
<option value="wasm">WASM (universal)</option>
</select>
</div>
<div class="field">
<label>&nbsp;</label>
<button class="primary" id="compute-btn" disabled>Compute Depth</button>
</div>
</div>
<div class="upload-area" id="upload-area">
<div class="icon">🖼️</div>
<p>Click to upload or drag & drop an image</p>
<p style="font-size:0.8rem; margin-top:4px;">JPG, PNG, WebP — max ~4K recommended</p>
</div>
<input type="file" id="file-input" accept="image/*" />
<div class="examples">
<span>Or try an example:</span>
<img class="example-img" src="https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/cats.jpg" data-src="https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/cats.jpg" alt="cats" />
<img class="example-img" src="https://images.unsplash.com/photo-1518791841217-8f7f0d9c3864?w=160" data-src="https://images.unsplash.com/photo-1518791841217-8f7f0d9c3864?w=800" alt="cat portrait" />
<img class="example-img" src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=160" data-src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800" alt="landscape" />
<img class="example-img" src="https://images.unsplash.com/photo-1543349689-9a4fe4c87b7a?w=160" data-src="https://images.unsplash.com/photo-1543349689-9a4fe4c87b7a?w=800" alt="city" />
<img class="example-img" src="https://images.unsplash.com/photo-1554629947-334ff61d85dc?w=160" data-src="https://images.unsplash.com/photo-1554629947-334ff61d85dc?w=800" alt="mountain" />
</div>
<div class="progress-wrap" id="progress-wrap" style="display:none;">
<div class="progress-bar"><div class="progress-fill" id="progress-fill"></div></div>
<div class="progress-label" id="progress-label">Initializing…</div>
</div>
<div class="status" id="status"></div>
<div class="slider-container" id="slider-container">
<img id="depth-img" alt="Depth map" />
<div class="slider-overlay" id="slider-overlay">
<img id="original-img" alt="Original" />
</div>
<div class="slider-handle" id="slider-handle"></div>
</div>
<div class="download-row" id="download-row" style="display:none;">
<a href="#" id="download-depth" download="depth.png">⬇ Download depth map (PNG)</a>
</div>
</div>
<p class="subtitle" style="text-align:center;">
First run downloads the model (~25–335 MB depending on size) and caches it.
Subsequent runs are instant to load.
</p>
</div>
<script type="module">
import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.7.5';
// Allow remote models from the Hub
env.allowLocalModels = false;
const uploadArea = document.getElementById('upload-area');
const fileInput = document.getElementById('file-input');
const computeBtn = document.getElementById('compute-btn');
const modelSelect = document.getElementById('model-select');
const deviceSelect = document.getElementById('device-select');
const progressWrap = document.getElementById('progress-wrap');
const progressFill = document.getElementById('progress-fill');
const progressLabel = document.getElementById('progress-label');
const statusEl = document.getElementById('status');
const sliderContainer = document.getElementById('slider-container');
const depthImg = document.getElementById('depth-img');
const originalImg = document.getElementById('original-img');
const sliderOverlay = document.getElementById('slider-overlay');
const sliderHandle = document.getElementById('slider-handle');
const downloadRow = document.getElementById('download-row');
const downloadDepth = document.getElementById('download-depth');
let currentImageURL = null;
let currentImageBitmap = null;
let depthCanvas = null;
let isComputing = false;
// --- Status helpers ---
function showStatus(msg, type = 'info') {
statusEl.textContent = msg;
statusEl.className = `status active ${type}`;
}
function hideStatus() {
statusEl.className = 'status';
}
function showProgress(pct, label) {
progressWrap.style.display = 'block';
progressFill.style.width = `${pct}%`;
if (label) progressLabel.textContent = label;
}
function hideProgress() {
progressWrap.style.display = 'none';
progressFill.style.width = '0%';
}
// --- Image loading ---
async function loadImage(url) {
const res = await fetch(url);
const blob = await res.blob();
currentImageURL = URL.createObjectURL(blob);
currentImageBitmap = await createImageBitmap(blob);
computeBtn.disabled = false;
hideStatus();
sliderContainer.classList.remove('active');
downloadRow.style.display = 'none';
}
// --- Upload interactions ---
uploadArea.addEventListener('click', () => fileInput.click());
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => uploadArea.classList.remove('dragover'));
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith('image/')) handleFile(file);
});
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) handleFile(file);
});
function handleFile(file) {
if (currentImageURL) URL.revokeObjectURL(currentImageURL);
currentImageURL = URL.createObjectURL(file);
currentImageBitmap = null; // will be loaded on compute
computeBtn.disabled = false;
hideStatus();
sliderContainer.classList.remove('active');
downloadRow.style.display = 'none';
}
// --- Example images ---
document.querySelectorAll('.example-img').forEach((img) => {
img.addEventListener('click', async () => {
showStatus('Loading example image…', 'info');
try {
await loadImage(img.dataset.src);
hideStatus();
} catch (err) {
showStatus(`Failed to load example: ${err.message}`, 'error');
}
});
});
// --- Depth colormap (Spectral_r) ---
function spectralColormap(value) {
// value: 0..1 → [r, g, b]
const v = Math.max(0, Math.min(1, value));
// Approximate Spectral_r: red → orange → yellow → green → blue
const stops = [
[0.0, [50, 0, 130]],
[0.1, [100, 0, 200]],
[0.2, [180, 30, 220]],
[0.3, [220, 80, 180]],
[0.4, [240, 120, 120]],
[0.5, [250, 170, 60]],
[0.6, [240, 200, 40]],
[0.7, [200, 230, 30]],
[0.8, [120, 240, 40]],
[0.9, [40, 220, 80]],
[1.0, [0, 180, 120]],
];
for (let i = 0; i < stops.length - 1; i++) {
if (v <= stops[i + 1][0]) {
const t = (v - stops[i][0]) / (stops[i + 1][0] - stops[i][0]);
return [
Math.round(stops[i][1][0] + t * (stops[i + 1][1][0] - stops[i][1][0])),
Math.round(stops[i][1][1] + t * (stops[i + 1][1][1] - stops[i][1][1])),
Math.round(stops[i][1][2] + t * (stops[i + 1][1][2] - stops[i][1][2])),
];
}
}
return stops[stops.length - 1][1];
}
// --- Compute depth ---
computeBtn.addEventListener('click', async () => {
if (isComputing || !currentImageURL) return;
isComputing = true;
computeBtn.disabled = true;
computeBtn.textContent = 'Computing…';
hideStatus();
sliderContainer.classList.remove('active');
downloadRow.style.display = 'none';
const modelId = modelSelect.value;
const device = deviceSelect.value;
try {
showProgress(0, 'Loading model (first run downloads weights)…');
let pipe;
try {
pipe = await pipeline('depth-estimation', modelId, {
device: device,
progress_callback: (data) => {
if (data.status === 'progress') {
const pct = data.progress ? Math.round(data.progress * 100) : 0;
showProgress(pct, `Downloading ${data.file}${pct}%`);
} else if (data.status === 'ready') {
showProgress(100, 'Model loaded');
}
},
});
} catch (gpuErr) {
// Fallback to WASM if WebGPU fails
if (device === 'webgpu') {
showStatus('WebGPU unavailable — falling back to WASM…', 'info');
pipe = await pipeline('depth-estimation', modelId, {
device: 'wasm',
progress_callback: (data) => {
if (data.status === 'progress') {
const pct = data.progress ? Math.round(data.progress * 100) : 0;
showProgress(pct, `Downloading ${data.file}${pct}%`);
}
},
});
} else {
throw gpuErr;
}
}
showProgress(100, 'Running inference…');
// Load image
let imageInput = currentImageURL;
const output = await pipe(imageInput);
showProgress(100, 'Rendering depth map…');
// output.depth is a RawImage; output.prediction is also available
const depth = output.depth || output.prediction;
const { width, height, data } = depth;
// Normalize depth values
let min = Infinity, max = -Infinity;
for (let i = 0; i < data.length; i++) {
if (data[i] < min) min = data[i];
if (data[i] > max) max = data[i];
}
const range = max - min || 1;
// Build colored depth map
depthCanvas = document.createElement('canvas');
depthCanvas.width = width;
depthCanvas.height = height;
const ctx = depthCanvas.getContext('2d');
const imageData = ctx.createImageData(width, height);
for (let i = 0; i < data.length; i++) {
const norm = (data[i] - min) / range;
const [r, g, b] = spectralColormap(norm);
imageData.data[i * 4] = r;
imageData.data[i * 4 + 1] = g;
imageData.data[i * 4 + 2] = b;
imageData.data[i * 4 + 3] = 255;
}
ctx.putImageData(imageData, 0, 0);
const depthDataURL = depthCanvas.toDataURL('image/png');
depthImg.src = depthDataURL;
// Load original for slider overlay
originalImg.src = currentImageURL;
// Wait for images to load
await Promise.all([
new Promise((res) => { depthImg.onload = res; }),
new Promise((res) => { originalImg.onload = res; }),
]);
// Set overlay image width to match container
const containerWidth = sliderContainer.offsetWidth;
originalImg.style.width = `${containerWidth}px`;
sliderContainer.classList.add('active');
downloadDepth.href = depthDataURL;
downloadRow.style.display = 'flex';
hideProgress();
showStatus('Depth estimation complete! Drag the slider to compare.', 'success');
setTimeout(hideStatus, 4000);
} catch (err) {
hideProgress();
showStatus(`Error: ${err.message}`, 'error');
console.error(err);
} finally {
isComputing = false;
computeBtn.disabled = false;
computeBtn.textContent = 'Compute Depth';
}
});
// --- Slider interaction ---
let isDragging = false;
sliderHandle.addEventListener('mousedown', (e) => { isDragging = true; e.preventDefault(); });
document.addEventListener('mouseup', () => { isDragging = false; });
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
updateSliderPosition(e.clientX);
});
// Touch support
sliderHandle.addEventListener('touchstart', (e) => { isDragging = true; e.preventDefault(); }, { passive: false });
document.addEventListener('touchend', () => { isDragging = false; });
document.addEventListener('touchmove', (e) => {
if (!isDragging) return;
updateSliderPosition(e.touches[0].clientX);
}, { passive: false });
// Click on container to move slider
sliderContainer.addEventListener('click', (e) => {
if (e.target === sliderHandle) return;
updateSliderPosition(e.clientX);
});
function updateSliderPosition(clientX) {
const rect = sliderContainer.getBoundingClientRect();
let pct = ((clientX - rect.left) / rect.width) * 100;
pct = Math.max(0, Math.min(100, pct));
sliderOverlay.style.width = `${pct}%`;
sliderHandle.style.left = `${pct}%`;
}
// Check WebGPU support and adjust default
if (!navigator.gpu) {
deviceSelect.value = 'wasm';
deviceSelect.querySelector('option[value="webgpu"]').textContent = 'WebGPU (unavailable)';
}
</script>
</body>
</html>