anycoder-8b9e8ef9 / index.html
Mousco's picture
Upload folder using huggingface_hub
100d48c verified
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Suivi d'Objet en Temps Réel - Vision IA</title>
<!-- Importation de polices et d'icônes pour le style -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary: #00f2ff;
--primary-dim: rgba(0, 242, 255, 0.1);
--secondary: #ff0055;
--bg-dark: #0a0e17;
--bg-panel: #111827;
--text-main: #e2e8f0;
--text-muted: #94a3b8;
--border: #1e293b;
--success: #10b981;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-dark);
color: var(--text-main);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* En-tête */
header {
background: var(--bg-panel);
border-bottom: 1px solid var(--border);
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 10;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
font-size: 1.2rem;
color: var(--primary);
text-transform: uppercase;
letter-spacing: 1px;
}
.brand i {
font-size: 1.4rem;
}
.anycoder-link {
font-size: 0.85rem;
color: var(--text-muted);
text-decoration: none;
border: 1px solid var(--border);
padding: 0.4rem 0.8rem;
border-radius: 4px;
transition: all 0.3s ease;
}
.anycoder-link:hover {
color: var(--primary);
border-color: var(--primary);
box-shadow: 0 0 10px var(--primary-dim);
}
/* Layout Principal */
main {
flex: 1;
display: grid;
grid-template-columns: 1fr 320px;
gap: 0;
height: calc(100vh - 70px);
}
/* Zone de la Caméra / Canvas */
.viewport {
position: relative;
background: #000;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
video {
display: none; /* Vidéo cachée, on affiche le canvas traité */
}
canvas {
max-width: 100%;
max-height: 100%;
cursor: crosshair;
}
.overlay-msg {
position: absolute;
color: var(--text-muted);
text-align: center;
pointer-events: none;
}
.overlay-msg i {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
/* Panneau de Contrôle */
.controls {
background: var(--bg-panel);
border-left: 1px solid var(--border);
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
overflow-y: auto;
}
.panel-section {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem;
}
.panel-title {
font-size: 0.85rem;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 1rem;
font-weight: 600;
letter-spacing: 0.5px;
display: flex;
justify-content: space-between;
}
/* Boutons */
.btn {
width: 100%;
padding: 0.8rem;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
font-family: inherit;
}
.btn-primary {
background: var(--primary);
color: #000;
}
.btn-primary:hover {
background: #6ff9ff;
box-shadow: 0 0 15px var(--primary-dim);
}
.btn-danger {
background: rgba(255, 0, 85, 0.1);
color: var(--secondary);
border: 1px solid var(--secondary);
}
.btn-danger:hover {
background: var(--secondary);
color: #fff;
}
/* Sliders */
.control-group {
margin-bottom: 1rem;
}
.control-group label {
display: flex;
justify-content: space-between;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
input[type="range"] {
width: 100%;
height: 6px;
background: var(--border);
border-radius: 3px;
outline: none;
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: var(--primary);
border-radius: 50%;
cursor: pointer;
transition: transform 0.1s;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
/* Indicateurs de données */
.data-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
.data-item {
background: rgba(0, 0, 0, 0.2);
padding: 0.5rem;
border-radius: 4px;
}
.data-label {
font-size: 0.7rem;
color: var(--text-muted);
}
.data-value {
font-family: 'JetBrains Mono', monospace;
font-size: 1rem;
color: var(--primary);
}
/* Indicateur de couleur cible */
.target-color-preview {
width: 100%;
height: 40px;
border-radius: 4px;
border: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
font-weight: bold;
font-size: 0.9rem;
margin-bottom: 1rem;
transition: background 0.2s;
}
/* Toast de notification */
.toast-container {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 100;
display: flex;
flex-direction: column;
gap: 10px;
}
.toast {
background: rgba(17, 24, 39, 0.9);
border: 1px solid var(--primary);
color: var(--text-main);
padding: 10px 20px;
border-radius: 4px;
font-size: 0.9rem;
backdrop-filter: blur(5px);
animation: slideUp 0.3s ease-out forwards;
display: flex;
align-items: center;
gap: 10px;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* Responsive Design */
@media (max-width: 900px) {
main {
grid-template-columns: 1fr;
grid-template-rows: 1fr auto;
overflow-y: auto;
}
.controls {
border-left: none;
border-top: 1px solid var(--border);
height: auto;
}
}
</style>
</head>
<body>
<header>
<div class="brand">
<i class="fa-solid fa-crosshairs"></i>
<span>Vision Tracker</span>
</div>
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">
Built with anycoder <i class="fa-solid fa-external-link-alt" style="font-size: 0.7em; margin-left:5px;"></i>
</a>
</header>
<main>
<!-- Zone d'affichage -->
<section class="viewport" id="viewport">
<div class="overlay-msg" id="placeholder">
<i class="fa-solid fa-video-slash"></i>
<p>Caméra arrêtée</p>
</div>
<video id="video" playsinline></video>
<canvas id="canvas"></canvas>
</section>
<!-- Panneau de contrôle -->
<aside class="controls">
<div class="panel-section">
<div class="panel-title">Contrôle Caméra</div>
<button id="btn-toggle-cam" class="btn btn-primary">
<i class="fa-solid fa-power-off"></i> Démarrer Caméra
</button>
</div>
<div class="panel-section">
<div class="panel-title">
Configuration Tracking
<i class="fa-solid fa-sliders"></i>
</div>
<div class="target-color-preview" id="color-preview" style="background-color: #333;">
Aucune couleur sélectionnée
</div>
<p style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 1rem;">
<i class="fa-solid fa-circle-info"></i> Cliquez sur l'image pour choisir la cible.
</p>
<div class="control-group">
<label>
Tolérance (Sensibilité)
<span id="tolerance-val">40</span>
</label>
<input type="range" id="tolerance" min="5" max="150" value="40">
</div>
<div class="control-group">
<label>
Taille Min. Objet (px)
<span id="min-size-val">500</span>
</label>
<input type="range" id="min-size" min="100" max="5000" value="500">
</div>
</div>
<div class="panel-section">
<div class="panel-title">
Télémétrie
<i class="fa-solid fa-satellite-dish"></i>
</div>
<div class="data-grid">
<div class="data-item">
<div class="data-label">Position X</div>
<div class="data-value" id="pos-x">0</div>
</div>
<div class="data-item">
<div class="data-label">Position Y</div>
<div class="data-value" id="pos-y">0</div>
</div>
<div class="data-item">
<div class="data-label">Surface (px²)</div>
<div class="data-value" id="surface">0</div>
</div>
<div class="data-item">
<div class="data-label">FPS</div>
<div class="data-value" id="fps">0</div>
</div>
</div>
</div>
<div class="panel-section">
<div class="panel-title">Debug</div>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; font-size: 0.9rem;">
<input type="checkbox" id="show-mask"> Afficher le masque de couleur
</label>
</div>
</aside>
</main>
<div class="toast-container" id="toast-container"></div>
<script>
/**
* Logique de l'application de suivi d'objet
* Utilise l'API Canvas pour analyser les pixels de la vidéo en temps réel.
*/
// Éléments du DOM
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const viewport = document.getElementById('viewport');
const placeholder = document.getElementById('placeholder');
// Contrôles
const btnToggleCam = document.getElementById('btn-toggle-cam');
const toleranceInput = document.getElementById('tolerance');
const toleranceVal = document.getElementById('tolerance-val');
const minSizeInput = document.getElementById('min-size');
const minSizeVal = document.getElementById('min-size-val');
const colorPreview = document.getElementById('color-preview');
const showMaskCheckbox = document.getElementById('show-mask');
// Affichage données
const posXDisplay = document.getElementById('pos-x');
const posYDisplay = document.getElementById('pos-y');
const surfaceDisplay = document.getElementById('surface');
const fpsDisplay = document.getElementById('fps');
// État de l'application
let isStreaming = false;
let animationId = null;
let targetColor = null; // {r, g, b}
let lastFrameTime = 0;
// Configuration
let config = {
tolerance: 40,
minObjectSize: 500,
scanStep: 4 // Performance : vérifier 1 pixel sur 4
};
// Gestion des notifications (Toast)
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = 'toast';
let icon = '<i class="fa-solid fa-info-circle"></i>';
if(type === 'success') icon = '<i class="fa-solid fa-check-circle"></i>';
if(type === 'error') icon = '<i class="fa-solid fa-exclamation-triangle"></i>';
toast.innerHTML = `${icon} <span>${message}</span>`;
container.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateY(20px)';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Démarrage / Arrêt de la caméra
btnToggleCam.addEventListener('click', async () => {
if (isStreaming) {
stopCamera();
} else {
await startCamera();
}
});
async function startCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 640 },
height: { ideal: 480 },
facingMode: 'user'
},
audio: false
});
video.srcObject = stream;
video.onloadedmetadata = () => {
video.play();
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
isStreaming = true;
btnToggleCam.innerHTML = '<i class="fa-solid fa-stop"></i> Arrêter Caméra';
btnToggleCam.classList.replace('btn-primary', 'btn-danger');
placeholder.style.display = 'none';
showToast("Caméra active. Cliquez sur un objet pour le suivre.", "success");
// Boucle de traitement
requestAnimationFrame(processFrame);
};
} catch (err) {
console.error("Erreur caméra:", err);
showToast("Impossible d'accéder à la caméra.", "error");
}
}
function stopCamera() {
if (video.srcObject) {
video.srcObject.getTracks().forEach(track => track.stop());
video.srcObject = null;
}
isStreaming = false;
cancelAnimationFrame(animationId);
// Nettoyer canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
btnToggleCam.innerHTML = '<i class="fa-solid fa-power-off"></i> Démarrer Caméra';
btnToggleCam.classList.replace('btn-danger', 'btn-primary');
placeholder.style.display = 'block';
// Reset données
posXDisplay.innerText = '0';
posYDisplay.innerText = '0';
surfaceDisplay.innerText = '0';
fpsDisplay.innerText = '0';
}
// Sélection de la couleur par clic
canvas.addEventListener('click', (e) => {
if (!isStreaming) return;
const rect = canvas.getBoundingClientRect();
// Calculer l'échelle car le canvas CSS peut être redimensionné
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
// Lire le pixel actuel
const pixel = ctx.getImageData(x, y, 1, 1).data;
targetColor = { r: pixel[0], g: pixel[1], b: pixel[2] };
// Mettre à jour l'UI
const rgbString = `rgb(${targetColor.r}, ${targetColor.g}, ${targetColor.b})`;
colorPreview.style.backgroundColor = rgbString;
colorPreview.innerText = `Cible: ${rgbString}`;
showToast("Couleur cible verrouillée !", "success");
});
// Mise à jour de la configuration
toleranceInput.addEventListener('input', (e) => {
config.tolerance = parseInt(e.target.value);
toleranceVal.innerText = config.tolerance;
});
minSizeInput.addEventListener('input', (e) => {
config.minObjectSize = parseInt(e.target.value);
minSizeVal.innerText = config.minObjectSize;
});
// Fonction principale de traitement d'image
function processFrame(timestamp) {
if (!isStreaming) return;
// Calcul FPS
const deltaTime = timestamp - lastFrameTime;
lastFrameTime = timestamp;
if (timestamp % 10 < 1) { // Mise à jour occasionnelle
fpsDisplay.innerText = Math.round(1000 / deltaTime) || 0;
}
// 1. Dessiner l'image vidéo sur le canvas
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// Si une couleur cible est définie, on lance le tracking
if (targetColor) {
trackObject();
}
animationId = requestAnimationFrame(processFrame);
}
function trackObject() {
const width = canvas.width;
const height = canvas.height;
// Obtenir les données brutes de l'image
const frameData = ctx.getImageData(0, 0, width, height);
const data = frameData.data;
const length = data.length;
let minX = width, minY = height, maxX = 0, maxY = 0;
let pixelCount = 0;
const rT = targetColor.r;
const gT = targetColor.g;
const bT = targetColor.b;
const tol = config.tolerance;
const tolSq = tol * tol * 3; // Optimisation : comparer au carré
// Scan des pixels (avec step pour perf)
for (let i = 0; i < length; i += 4 * config.scanStep) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// Calcul de distance de couleur (Euclidienne simplifiée)
// On utilise une approximation rapide pour la performance
const dist = (r - rT) ** 2 + (g - gT) ** 2 + (b - bT) ** 2;
if (dist < tolSq) {
// Pixel correspondant trouvé
const pixelIndex = i / 4;
const x = pixelIndex % width;
const y = Math.floor(pixelIndex / width);
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
pixelCount++;
// Optionnel : dessiner le masque (pour debug)
if (showMaskCheckbox.checked) {
data[i] = 0; // R
data[i + 1] = 255; // G
data[i + 2] = 0; // B
}
}
}
// Si on a activé le masque, on remet les pixels modifiés sur le canvas
if (showMaskCheckbox.checked) {
ctx.putImageData(frameData, 0, 0);
}
// Calcul des dimensions de l'objet détecté
const boxWidth = maxX - minX;
const boxHeight = maxY - minY;
const area = boxWidth * boxHeight;
// Filtrer le bruit (surface minimale)
if (area > config.minObjectSize) {
// Dessiner l'interface utilisateur (HUD)
drawHUD(minX, minY, boxWidth, boxHeight);
// Mettre à jour les données textuelles
posXDisplay.innerText = Math.round(minX + boxWidth / 2);
posYDisplay.innerText = Math.round(minY + boxHeight / 2);
surfaceDisplay.innerText = area;
} else {
// Si perdu
surfaceDisplay.innerText = "Recherche...";
posXDisplay.innerText = "-";
posYDisplay.innerText = "-";
}
}
function drawHUD(x, y, w, h) {
const centerX = x + w / 2;
const centerY = y + h / 2;
ctx.strokeStyle = '#00f2ff';
ctx.lineWidth = 2;
ctx.shadowBlur = 10;
ctx.shadowColor = '#00f2ff';
// Dessiner les coins (Style futuriste)
const lineLen = Math.min(w, h) * 0.2;
ctx.beginPath();
// Coin haut gauche
ctx.moveTo(x, y + lineLen);
ctx.lineTo(x, y);
ctx.lineTo(x + lineLen, y);
// Coin haut droite
ctx.moveTo(x + w - lineLen, y);
ctx.lineTo(x + w, y);
ctx.lineTo(x + w, y + lineLen);
// Coin bas droite
ctx.moveTo(x + w, y + h - lineLen);
ctx.lineTo(x + w, y + h);
ctx.lineTo(x + w - lineLen, y + h);
// Coin bas gauche
ctx.moveTo(x + lineLen, y + h);
ctx.lineTo(x, y + h);
ctx.lineTo(x, y + h - lineLen);
ctx.stroke();
// Lignes de guidage vers le centre
ctx.strokeStyle = 'rgba(0, 242, 255, 0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(centerX, 0);
ctx.lineTo(centerX, canvas.height);
ctx.moveTo(0, centerY);
ctx.lineTo(canvas.width, centerY);
ctx.stroke();
// Cercle central
ctx.beginPath();
ctx.arc(centerX, centerY, 5, 0, Math.PI * 2);
ctx.fillStyle = '#ff0055';
ctx.fill();
// Texte coordonnées
ctx.fillStyle = '#fff';
ctx.font = '12px JetBrains Mono';
ctx.fillText(`X:${Math.round(centerX)} Y:${Math.round(centerY)}`, x, y - 10);
// Reset shadow
ctx.shadowBlur = 0;
}
// Gestion du redimensionnement de la fenêtre
window.addEventListener('resize', () => {
if(isStreaming) {
// Le canvas garde sa résolution interne, CSS gère l'affichage
// Mais on peut vouloir ajuster si nécessaire
}
});
</script>
</body>
</html>