aaurelions/quadrocopter
0
1<!DOCTYPE html>2<html lang="en">3 4<head>5 <meta charset="UTF-8">6 <meta name="viewport" content="width=device-width, initial-scale=1.0">7 <title>Quadcopter Interceptor Simulation</title>8 <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script>9 <script src="https://cdn.tailwindcss.com"></script>10 <style>11 /* --- Base & Theming --- */12 :root {13 --bg-start: #f4f6f9;14 --bg-end: #e2e8f0;15 --card-bg: rgba(255, 255, 255, 0.7);16 --text-primary: #1f2937;17 --text-secondary: #4b5563;18 --border-color: rgba(0, 0, 0, 0.1);19 --shadow: 0 10px 30px rgba(0, 0, 0, 0.07);20 --primary-gradient: linear-gradient(90deg, #3b82f6, #8b5cf6);21 --primary-hover-gradient: linear-gradient(90deg, #2563eb, #7c3aed);22 --detail-bg: rgba(0, 0, 0, 0.04);23 --propeller-color: #64748b;24 }25 26 html.dark {27 --bg-start: #111827;28 --bg-end: #1f2937;29 --card-bg: rgba(31, 41, 55, 0.7);30 --text-primary: #f9fafb;31 --text-secondary: #d1d5db;32 --border-color: rgba(255, 255, 255, 0.1);33 --shadow: 0 10px 30px rgba(0, 0, 0, 0.2);34 --detail-bg: rgba(255, 255, 255, 0.06);35 --propeller-color: #94a3b8;36 }37 38 body {39 background: linear-gradient(-45deg, var(--bg-start), var(--bg-end));40 transition: background 0.5s;41 font-family: 'Inter', sans-serif;42 }43 44 /* Helpers */45 .text-text-primary {46 color: var(--text-primary);47 }48 49 .text-text-secondary {50 color: var(--text-secondary);51 }52 53 .border-border-color {54 border-color: var(--border-color);55 }56 57 .bg-card-bg {58 background-color: var(--card-bg);59 }60 61 .bg-detail-bg {62 background-color: var(--detail-bg);63 }64 65 .backdrop-blur-xl {66 backdrop-filter: blur(16px);67 -webkit-backdrop-filter: blur(16px);68 }69 70 /* Propeller Animation */71 @keyframes spin {72 from {73 transform: rotate(0deg);74 }75 76 to {77 transform: rotate(360deg);78 }79 }80 81 .propeller-blade {82 animation: spin 1s linear infinite;83 animation-play-state: paused;84 /* Start paused */85 background-color: var(--propeller-color);86 transition: background-color 0.3s;87 }88 89 /* Custom scrollbar for results & gallery */90 .custom-scrollbar::-webkit-scrollbar {91 height: 6px;92 width: 6px;93 }94 95 .custom-scrollbar::-webkit-scrollbar-track {96 background: transparent;97 }98 99 .custom-scrollbar::-webkit-scrollbar-thumb {100 background: rgba(0, 0, 0, 0.2);101 border-radius: 3px;102 }103 104 html.dark .custom-scrollbar::-webkit-scrollbar-thumb {105 background: rgba(255, 255, 255, 0.2);106 }107 </style>108 <link rel="preconnect" href="https://fonts.googleapis.com">109 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>110 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">111 <script>112 tailwind.config = {113 darkMode: 'class',114 theme: {115 extend: {116 fontFamily: {117 sans: ['Inter', 'sans-serif'],118 },119 }120 }121 }122 </script>123</head>124 125<body class="min-h-screen flex items-center justify-center p-2 sm:p-4 antialiased">126 127 <!-- Model Loading Overlay -->128 <div id="model-loading-overlay"129 class="fixed inset-0 bg-black/70 flex flex-col items-center justify-center z-50 transition-opacity duration-300">130 <div class="flex flex-col items-center gap-4">131 <div class="animate-spin rounded-full h-16 w-16 border-b-4 border-blue-400"></div>132 <span class="text-white text-2xl font-bold tracking-wide">Loading AI Model...</span>133 <span class="text-blue-200 text-sm">This may take a few seconds on first load.</span>134 </div>135 </div>136 137 <!-- Main Application Card -->138 <div139 class="w-full max-w-7xl mx-auto bg-card-bg border border-border-color rounded-2xl sm:rounded-3xl shadow-2xl backdrop-blur-xl p-4 md:p-6 transition-all duration-500">140 <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">141 142 <!-- Left Panel: Quadcopter Simulation -->143 <div class="flex flex-col gap-4">144 <header class="flex justify-between items-center">145 <h1 class="text-lg sm:text-xl md:text-2xl font-bold text-text-primary">Quadcopter Simulation</h1>146 <button id="theme-switcher"147 class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-black/5 dark:hover:bg-white/10 transition"148 aria-label="Switch theme" title="Switch theme">149 <svg id="sun-icon" class="w-6 h-6 text-text-secondary" fill="none" stroke="currentColor"150 viewBox="0 0 24 24">151 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"152 d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z">153 </path>154 </svg>155 <svg id="moon-icon" class="w-6 h-6 text-text-secondary hidden" fill="none" stroke="currentColor"156 viewBox="0 0 24 24">157 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"158 d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z">159 </path>160 </svg>161 </button>162 </header>163 164 <div165 class="relative w-full aspect-square bg-detail-bg rounded-2xl p-4 sm:p-6 flex items-center justify-center">166 <div class="relative w-full h-full max-w-[350px] max-h-[350px]">167 <div class="absolute inset-0 flex items-center justify-center">168 <div169 class="w-[28%] h-[28%] bg-gray-400 dark:bg-gray-600 rounded-full shadow-lg flex items-center justify-center text-white font-bold text-lg">170 <svg xmlns="http://www.w3.org/2000/svg" class="w-1/2 h-1/2 opacity-50"171 viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"172 stroke-linecap="round" stroke-linejoin="round">173 <path174 d="M12 22s-8-4.5-8-11.8A8 8 0 0 1 12 2a8 8 0 0 1 8 8.2c0 7.3-8 11.8-8 11.8z" />175 <circle cx="12" cy="10" r="3" />176 </svg>177 </div>178 </div>179 <div180 class="absolute w-[90%] h-1.5 top-1/2 left-[5%] -translate-y-1 bg-gray-300 dark:bg-gray-500 rounded-full">181 </div>182 <div183 class="absolute h-[90%] w-1.5 left-1/2 top-[5%] -translate-x-1 bg-gray-300 dark:bg-gray-500 rounded-full">184 </div>185 <div id="motor-fl" class="absolute top-0 left-0"></div>186 <div id="motor-fr" class="absolute top-0 right-0"></div>187 <div id="motor-rl" class="absolute bottom-0 left-0"></div>188 <div id="motor-rr" class="absolute bottom-0 right-0"></div>189 </div>190 </div>191 192 <div>193 <h2 class="text-base sm:text-lg font-semibold text-text-primary mb-2">Flight Telemetry</h2>194 <div class="grid grid-cols-2 gap-2 text-left">195 <div class="bg-detail-bg p-2 rounded-lg flex items-center gap-2">196 <svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">197 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"198 d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1V10a1 1 0 00-1-1H7a1 1 0 00-1 1v10a1 1 0 001 1h2z">199 </path>200 </svg>201 <div>202 <p class="text-xs text-text-secondary font-medium">TARGET</p>203 <p id="telemetry-target" class="text-sm sm:text-base font-bold text-text-primary">-</p>204 </div>205 </div>206 <div class="bg-detail-bg p-2 rounded-lg flex items-center gap-2">207 <svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">208 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"209 d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>210 </svg>211 <div>212 <p class="text-xs text-text-secondary font-medium">STATUS</p>213 <p id="telemetry-status" class="text-sm sm:text-base font-bold text-text-primary">IDLE214 </p>215 </div>216 </div>217 <div class="bg-detail-bg p-2 rounded-lg flex items-center gap-2">218 <svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">219 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"220 d="M4 8h16M4 16h16"></path>221 </svg>222 <div>223 <p class="text-xs text-text-secondary font-medium">X-VECTOR</p>224 <p id="telemetry-x" class="text-sm sm:text-base font-bold text-text-primary">0.00</p>225 </div>226 </div>227 <div class="bg-detail-bg p-2 rounded-lg flex items-center gap-2">228 <svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">229 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"230 d="M8 4v16M16 4v16"></path>231 </svg>232 <div>233 <p class="text-xs text-text-secondary font-medium">Y-VECTOR</p>234 <p id="telemetry-y" class="text-sm sm:text-base font-bold text-text-primary">0.00</p>235 </div>236 </div>237 </div>238 </div>239 </div>240 241 <!-- Right Panel: AI Detector -->242 <div id="detector-card"243 class="bg-card-bg/50 dark:bg-card-bg/50 border border-border-color rounded-2xl p-4 md:p-6 flex flex-col">244 <header class="flex justify-between items-center mb-4">245 <h1 class="text-lg sm:text-xl md:text-2xl font-bold text-text-primary">AI Vision System</h1>246 </header>247 248 <div id="content-area" class="flex-grow flex flex-col min-h-0">249 <div id="uploader-view" class="flex-grow flex flex-col">250 <div id="drop-zone"251 class="flex-grow flex flex-col items-center justify-center border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-2xl p-4 text-center cursor-pointer hover:border-blue-500 dark:hover:border-blue-400 hover:scale-[1.01] transition-all duration-300"252 tabindex="0">253 <svg class="w-12 h-12 sm:w-16 sm:h-16 opacity-50 text-text-secondary" fill="none"254 stroke="currentColor" viewBox="0 0 24 24">255 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"256 d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12">257 </path>258 </svg>259 <p class="mt-3 font-semibold text-text-primary">Drag & Drop Sky Image</p>260 <p class="text-xs sm:text-sm text-text-secondary mt-1">or</p>261 <label for="image-upload"262 class="mt-3 px-5 py-2 rounded-lg font-semibold text-white cursor-pointer transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 text-sm"263 style="background: var(--primary-gradient);"264 onmouseover="this.style.background='var(--primary-hover-gradient)'"265 onmouseout="this.style.background='var(--primary-gradient)'">266 Select File267 </label>268 <input type="file" id="image-upload" class="hidden" accept="image/*">269 </div>270 <div class="mt-4">271 <h3 class="text-sm font-semibold text-text-secondary text-center mb-2">Or Select a Preset272 </h3>273 <div id="preset-gallery" class="custom-scrollbar flex gap-2 overflow-x-auto pb-2">274 <!-- Presets will be injected here -->275 </div>276 </div>277 </div>278 279 <div id="results-view" class="hidden flex-grow flex flex-col min-h-0">280 <div class="relative mb-2 group">281 <canvas id="output-canvas" class="w-full h-auto rounded-xl shadow-md"></canvas>282 <button id="reset-button"283 class="absolute top-2 right-2 w-9 h-9 bg-black/40 hover:bg-black/60 backdrop-blur-sm rounded-full flex items-center justify-center transition opacity-0 group-hover:opacity-100"284 title="Analyze another image">285 <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">286 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"287 d="M19 12H5M12 19l-7-7 7-7"></path>288 </svg>289 </button>290 </div>291 <div id="results-summary" class="flex-grow overflow-y-auto space-y-2 pr-2 custom-scrollbar">292 </div>293 </div>294 </div>295 <footer id="status-footer" class="mt-4 text-center text-sm text-text-secondary h-5"></footer>296 </div>297 </div>298 </div>299 300 <!-- Toast Notification -->301 <div id="toast"302 class="fixed bottom-6 right-6 bg-red-600 text-white px-4 py-2 rounded-lg shadow-lg opacity-0 transform translate-y-4 pointer-events-none transition-all duration-300 z-50">303 </div>304 305 306 <script>307 // --- DOM Elements ---308 const uploaderView = document.getElementById('uploader-view');309 const resultsView = document.getElementById('results-view');310 const dropZone = document.getElementById('drop-zone');311 const imageUpload = document.getElementById('image-upload');312 const outputCanvas = document.getElementById('output-canvas');313 const resultsSummary = document.getElementById('results-summary');314 const statusFooter = document.getElementById('status-footer');315 const resetButton = document.getElementById('reset-button');316 const themeSwitcher = document.getElementById('theme-switcher');317 const sunIcon = document.getElementById('sun-icon');318 const moonIcon = document.getElementById('moon-icon');319 const modelLoadingOverlay = document.getElementById('model-loading-overlay');320 const toast = document.getElementById('toast');321 const presetGallery = document.getElementById('preset-gallery');322 323 const telemetry = {324 target: document.getElementById('telemetry-target'),325 status: document.getElementById('telemetry-status'),326 x: document.getElementById('telemetry-x'),327 y: document.getElementById('telemetry-y'),328 };329 const motorElements = {330 fl: document.getElementById('motor-fl'),331 fr: document.getElementById('motor-fr'),332 rl: document.getElementById('motor-rl'),333 rr: document.getElementById('motor-rr'),334 };335 let session;336 337 // --- Constants ---338 const MODEL_URL = "https://huggingface.co/aaurelions/yolo11n.onnx/resolve/main/yolo11n.onnx";339 const COCO_CLASSES = ['person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush'];340 const TARGET_CLASS = 'airplane';341 const COLORS = ["#3b82f6", "#ef4444", "#22c55e", "#f97316", "#8b5cf6", "#fde047", "#ec4899", "#14b8a6", "#64748b"];342 343 // --- UI & Initialization Logic ---344 function showToast(message, duration = 3000) {345 toast.textContent = message;346 toast.classList.remove('opacity-0', 'translate-y-4');347 setTimeout(() => {348 toast.classList.add('opacity-0', 'translate-y-4');349 }, duration);350 }351 352 function showModelLoading(show) {353 modelLoadingOverlay.style.display = show ? 'flex' : 'none';354 modelLoadingOverlay.classList.toggle('opacity-0', !show);355 }356 357 function setDarkMode(isDark) {358 document.documentElement.classList.toggle('dark', isDark);359 sunIcon.classList.toggle('hidden', isDark);360 moonIcon.classList.toggle('hidden', !isDark);361 localStorage.setItem('theme', isDark ? 'dark' : 'light');362 }363 364 themeSwitcher.addEventListener('click', () => setDarkMode(!document.documentElement.classList.contains('dark')));365 366 resetButton.addEventListener('click', () => {367 resultsView.classList.add('hidden');368 uploaderView.classList.remove('hidden');369 statusFooter.textContent = 'Model is ready. Select an image.';370 imageUpload.value = '';371 resetSimulation();372 });373 374 // --- Drag & Drop Logic ---375 ['dragenter', 'dragover'].forEach(eventName => {376 dropZone.addEventListener(eventName, (e) => {377 e.preventDefault();378 dropZone.classList.add('ring-4', 'ring-blue-400', 'scale-105', 'border-solid');379 }, false);380 });381 ['dragleave', 'drop'].forEach(eventName => {382 dropZone.addEventListener(eventName, (e) => {383 e.preventDefault();384 dropZone.classList.remove('ring-4', 'ring-blue-400', 'scale-105', 'border-solid');385 }, false);386 });387 dropZone.addEventListener('drop', (e) => {388 const file = e.dataTransfer.files[0];389 if (file) handleFile(file);390 });391 imageUpload.addEventListener('change', (e) => { if (e.target.files[0]) handleFile(e.target.files[0]); });392 393 // --- Quadcopter Simulation Logic ---394 function createMotorComponent(container, label) {395 container.classList.add('w-14', 'h-14', 'sm:w-16', 'sm:h-16', 'flex', 'items-center', 'justify-center', 'transform');396 if (label.includes('l')) container.classList.add('-translate-x-1/2'); else container.classList.add('translate-x-1/2');397 if (label.includes('f')) container.classList.add('-translate-y-1/2'); else container.classList.add('translate-y-1/2');398 399 const mount = document.createElement('div');400 mount.className = 'w-7 h-7 sm:w-8 sm:h-8 bg-gray-300 dark:bg-gray-700 rounded-full shadow-inner';401 402 const propeller = document.createElement('div');403 propeller.className = 'absolute w-full h-full flex items-center justify-center';404 propeller.innerHTML = `<div class="propeller-blade absolute w-full h-1.5 rounded-full"></div><div class="propeller-blade absolute w-1.5 h-full rounded-full"></div>`;405 406 const speedDisplay = document.createElement('div');407 speedDisplay.className = 'absolute -bottom-4 text-xs font-mono text-text-secondary';408 speedDisplay.textContent = '0%';409 410 const labelDisplay = document.createElement('div');411 labelDisplay.className = 'absolute -top-4 text-xs font-bold text-text-secondary tracking-wider';412 labelDisplay.textContent = label.toUpperCase();413 414 container.append(labelDisplay, mount, propeller, speedDisplay);415 }416 417 function updateMotor(motorEl, speed) {418 const clampedSpeed = Math.max(0, Math.min(100, speed));419 const speedDisplay = motorEl.querySelector('.absolute.-bottom-4');420 speedDisplay.textContent = `${Math.round(clampedSpeed)}%`;421 const blades = motorEl.querySelectorAll('.propeller-blade');422 if (clampedSpeed > 5) {423 const animationDuration = 1 / (clampedSpeed / 20);424 blades.forEach(b => {425 b.style.animationPlayState = 'running';426 b.style.animationDuration = `${Math.max(0.05, animationDuration)}s`;427 });428 } else {429 blades.forEach(b => b.style.animationPlayState = 'paused');430 }431 }432 433 function resetSimulation() {434 Object.values(motorElements).forEach(el => updateMotor(el, 0));435 telemetry.target.textContent = '-';436 telemetry.status.textContent = 'IDLE';437 telemetry.x.textContent = '0.00';438 telemetry.y.textContent = '0.00';439 telemetry.status.classList.remove('text-green-500', 'text-red-500', 'text-yellow-500');440 }441 442 function calculateMotorSpeeds(target, imageWidth, imageHeight) {443 const [x1, y1, x2, y2] = target.box;444 const targetX = x1 + (x2 - x1) / 2;445 const targetY = y1 + (y2 - y1) / 2;446 const normX = (targetX / imageWidth - 0.5) * 2;447 const normY = (targetY / imageHeight - 0.5) * 2;448 const BASE_SPEED = 60;449 const MANEUVER_INTENSITY = 25;450 const pitch_adjustment = normY * MANEUVER_INTENSITY;451 const roll_adjustment = normX * MANEUVER_INTENSITY;452 const speeds = {453 fl: BASE_SPEED - pitch_adjustment - roll_adjustment,454 fr: BASE_SPEED - pitch_adjustment + roll_adjustment,455 rl: BASE_SPEED + pitch_adjustment - roll_adjustment,456 rr: BASE_SPEED + pitch_adjustment + roll_adjustment,457 };458 updateMotor(motorElements.fl, speeds.fl);459 updateMotor(motorElements.fr, speeds.fr);460 updateMotor(motorElements.rl, speeds.rl);461 updateMotor(motorElements.rr, speeds.rr);462 telemetry.target.textContent = 'AIRPLANE';463 telemetry.status.textContent = 'TRACKING';464 telemetry.status.classList.remove('text-red-500', 'text-yellow-500');465 telemetry.status.classList.add('text-green-500');466 telemetry.x.textContent = normX.toFixed(2);467 telemetry.y.textContent = normY.toFixed(2);468 }469 470 // --- Core AI & Rendering Logic ---471 async function handleFile(file) {472 if (!session) { showToast("AI Model is not loaded yet."); return; }473 if (!file || !file.type.startsWith('image/')) { showToast("Please select a valid image file."); return; }474 const image = new Image();475 image.src = URL.createObjectURL(file);476 image.onload = () => {477 runAnalysis(image);478 URL.revokeObjectURL(image.src);479 };480 image.onerror = () => {481 showToast("Could not load the selected image file.");482 statusFooter.textContent = "Error: Invalid image.";483 }484 }485 486 async function handleUrl(url) {487 if (!session) { showToast("AI Model is not loaded yet."); return; }488 const image = new Image();489 image.crossOrigin = "anonymous"; // Handle CORS for remote images490 image.src = url;491 image.onload = () => runAnalysis(image);492 image.onerror = () => {493 showToast(`Could not load image from URL: ${url}`);494 statusFooter.textContent = "Error: Invalid image URL.";495 };496 }497 498 async function runAnalysis(image) {499 resetSimulation();500 statusFooter.innerHTML = '<div class="flex items-center justify-center gap-2"><div class="animate-spin rounded-full h-4 w-4 border-b-2 border-text-primary"></div><span>Analyzing image...</span></div>';501 uploaderView.classList.add('hidden');502 resultsView.classList.remove('hidden');503 try {504 const inputTensor = preprocess(image);505 const results = await session.run({ 'images': inputTensor });506 const boxes = postprocess(results.output0, image.width, image.height);507 const finalBoxes = nonMaxSuppression(boxes, 0.5, 0.45);508 const airplanes = finalBoxes.filter(b => COCO_CLASSES[b.classId] === TARGET_CLASS);509 let primaryTarget = null;510 if (airplanes.length > 0) {511 primaryTarget = airplanes.sort((a, b) => b.confidence - a.confidence)[0];512 calculateMotorSpeeds(primaryTarget, image.naturalWidth, image.naturalHeight);513 } else {514 telemetry.target.textContent = 'NONE';515 telemetry.status.textContent = 'SCANNING';516 telemetry.status.classList.add('text-yellow-500');517 }518 drawDetections(image, finalBoxes, primaryTarget);519 displaySummary(finalBoxes);520 statusFooter.textContent = `Found ${finalBoxes.length} object(s). ${airplanes.length > 0 ? 'Target acquired.' : 'No target found.'}`;521 } catch (e) {522 showToast(`An error occurred: ${e.message}`);523 statusFooter.textContent = "Error during analysis.";524 resetButton.click();525 }526 }527 528 function preprocess(img) {529 const canvas = document.createElement('canvas');530 const modelWidth = 640, modelHeight = 640;531 canvas.width = modelWidth;532 canvas.height = modelHeight;533 const ctx = canvas.getContext('2d');534 ctx.drawImage(img, 0, 0, modelWidth, modelHeight);535 const imageData = ctx.getImageData(0, 0, modelWidth, modelHeight);536 const float32Data = new Float32Array(1 * 3 * modelHeight * modelWidth);537 for (let i = 0, j = 0; i < imageData.data.length; i += 4, j++) {538 float32Data[j] = imageData.data[i] / 255.0;539 float32Data[modelWidth * modelHeight + j] = imageData.data[i + 1] / 255.0;540 float32Data[2 * modelWidth * modelHeight + j] = imageData.data[i + 2] / 255.0;541 }542 return new ort.Tensor('float32', float32Data, [1, 3, modelHeight, modelWidth]);543 }544 545 function postprocess(tensor, originalWidth, originalHeight) {546 const data = tensor.data;547 const boxes = [];548 const modelWidth = 640, modelHeight = 640;549 const xRatio = originalWidth / modelWidth;550 const yRatio = originalHeight / modelHeight;551 for (let i = 0; i < tensor.dims[2]; i++) {552 const classScores = [];553 for (let j = 0; j < COCO_CLASSES.length; j++) {554 classScores.push(data[(4 + j) * tensor.dims[2] + i]);555 }556 const maxScore = Math.max(...classScores);557 if (maxScore > 0.25) {558 const classId = classScores.indexOf(maxScore);559 const [cx, cy, w, h] = [data[i], data[tensor.dims[2] + i], data[2 * tensor.dims[2] + i], data[3 * tensor.dims[2] + i]];560 const x1 = (cx - w / 2) * xRatio;561 const y1 = (cy - h / 2) * yRatio;562 const x2 = (cx + w / 2) * xRatio;563 const y2 = (cy + h / 2) * yRatio;564 boxes.push({ classId, confidence: maxScore, box: [x1, y1, x2, y2] });565 }566 }567 return boxes;568 }569 570 function nonMaxSuppression(boxes, iouThreshold, scoreThreshold) {571 boxes.sort((a, b) => b.confidence - a.confidence);572 const finalBoxes = [];573 while (boxes.length > 0) {574 const current = boxes.shift();575 if (current.confidence < scoreThreshold) continue;576 finalBoxes.push(current);577 boxes = boxes.filter(box => {578 if (box.classId !== current.classId) return true;579 const iou = calculateIoU(current.box, box.box);580 return iou <= iouThreshold;581 });582 }583 return finalBoxes;584 }585 586 function calculateIoU(box1, box2) {587 const [x1, y1, x2, y2] = box1;588 const [x1_b, y1_b, x2_b, y2_b] = box2;589 const interX1 = Math.max(x1, x1_b);590 const interY1 = Math.max(y1, y1_b);591 const interX2 = Math.min(x2, x2_b);592 const interY2 = Math.min(y2, y2_b);593 const interArea = Math.max(0, interX2 - interX1) * Math.max(0, interY2 - interY1);594 const box1Area = (x2 - x1) * (y2 - y1);595 const box2Area = (x2_b - x1_b) * (y2_b - y1_b);596 const unionArea = box1Area + box2Area - interArea;597 return unionArea > 0 ? interArea / unionArea : 0;598 }599 600 function drawDetections(img, boxes, primaryTarget) {601 const ctx = outputCanvas.getContext('2d');602 outputCanvas.width = img.naturalWidth;603 outputCanvas.height = img.naturalHeight;604 ctx.drawImage(img, 0, 0, outputCanvas.width, outputCanvas.height);605 boxes.forEach(det => {606 const isTarget = (det === primaryTarget);607 const [x1, y1, x2, y2] = det.box;608 const w = x2 - x1;609 const h = y2 - y1;610 const color = isTarget ? '#ef4444' : COLORS[det.classId % COLORS.length];611 ctx.strokeStyle = color;612 ctx.lineWidth = isTarget ? Math.max(4, outputCanvas.width * 0.005) : Math.max(2, outputCanvas.width * 0.003);613 ctx.strokeRect(x1, y1, w, h);614 ctx.fillStyle = color;615 const label = `${COCO_CLASSES[det.classId]} (${(det.confidence * 100).toFixed(1)}%)`;616 const fontSize = Math.max(14, outputCanvas.width * 0.015);617 ctx.font = `bold ${fontSize}px sans-serif`;618 const textWidth = ctx.measureText(label).width;619 ctx.fillRect(x1 - (ctx.lineWidth / 2), y1 - (fontSize + 8), textWidth + 10, fontSize + 8);620 ctx.fillStyle = '#ffffff';621 ctx.fillText(label, x1 + 5, y1 - 5);622 });623 if (primaryTarget) {624 const [x1, y1, x2, y2] = primaryTarget.box;625 const targetX = x1 + (x2 - x1) / 2;626 const targetY = y1 + (y2 - y1) / 2;627 const centerX = outputCanvas.width / 2;628 const centerY = outputCanvas.height / 2;629 ctx.beginPath();630 ctx.moveTo(centerX, centerY);631 ctx.lineTo(targetX, targetY);632 ctx.strokeStyle = 'rgba(239, 68, 68, 0.8)';633 ctx.lineWidth = 3;634 ctx.setLineDash([10, 5]);635 ctx.stroke();636 ctx.setLineDash([]);637 ctx.beginPath();638 ctx.arc(centerX, centerY, 10, 0, 2 * Math.PI);639 ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';640 ctx.fill();641 ctx.strokeStyle = 'rgba(239, 68, 68, 0.9)';642 ctx.stroke();643 }644 }645 646 function displaySummary(boxes) {647 resultsSummary.innerHTML = '';648 if (boxes.length === 0) {649 resultsSummary.innerHTML = '<p class="text-text-secondary text-center">No objects detected.</p>';650 return;651 }652 boxes.forEach(box => {653 const wrapper = document.createElement('div');654 wrapper.className = 'bg-gray-500/10 rounded-lg p-2 flex items-center gap-3';655 const color = COLORS[box.classId % COLORS.length];656 const className = COCO_CLASSES[box.classId];657 wrapper.innerHTML = `658 <div class="w-1 h-8 rounded" style="background-color: ${color}"></div>659 <div class="flex-grow">660 <div class="flex justify-between items-center">661 <span class="font-semibold text-sm text-text-primary">${className}</span>662 <span class="font-mono text-xs text-text-primary bg-white/50 dark:bg-black/20 px-1.5 py-0.5 rounded">${(box.confidence * 100).toFixed(1)}%</span>663 </div>664 <p class="text-xs text-text-secondary font-mono">665 Pos: ${Math.round(box.box[0])},${Math.round(box.box[1])}666 </p>667 </div>`;668 resultsSummary.appendChild(wrapper);669 });670 }671 672 function populatePresetGallery() {673 for (let i = 1; i <= 10; i++) {674 const thumbSrc = `/images/thumbnails/${i}.jpg`;675 const fullSrc = `/images/${i}.jpg`;676 677 const img = document.createElement('img');678 img.src = thumbSrc;679 img.className = "w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-transparent hover:border-blue-500 transition-all duration-200 flex-shrink-0";680 img.loading = "lazy";681 img.alt = `Preset Image ${i}`;682 img.onclick = () => handleUrl(fullSrc);683 img.onerror = () => {684 img.src = `https://placehold.co/80x80/e2e8f0/4b5563?text=Img+${i}`;685 img.onclick = null; // Disable click if placeholder686 };687 presetGallery.appendChild(img);688 }689 }690 691 // --- App Initialization ---692 async function initialize() {693 setDarkMode(localStorage.getItem('theme') === 'dark' || (window.matchMedia('(prefers-color-scheme: dark)').matches && !localStorage.getItem('theme')));694 createMotorComponent(motorElements.fl, 'fl');695 createMotorComponent(motorElements.fr, 'fr');696 createMotorComponent(motorElements.rl, 'rl');697 createMotorComponent(motorElements.rr, 'rr');698 populatePresetGallery();699 resetSimulation();700 statusFooter.textContent = 'Initializing AI engine...';701 showModelLoading(true);702 try {703 session = await ort.InferenceSession.create(MODEL_URL, { executionProviders: ['wasm'] });704 statusFooter.textContent = 'Model is ready. Select an image.';705 } catch (e) {706 statusFooter.textContent = `Error loading model: ${e.message}`;707 showToast(`Failed to load the AI model. Please refresh. ${e.message}`);708 } finally {709 showModelLoading(false);710 }711 }712 713 document.addEventListener('DOMContentLoaded', initialize);714 </script>715</body>716 717</html>