CoolFace
Apppublic

aaurelions/quadrocopter

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
index.html717 linesDownload Raw Back to root
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>