Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>Threat Detection Comparison</title> | |
| <style> | |
| :root { | |
| --primary: #E73562; | |
| --secondary: #EA782D; | |
| --rf-accent: #E73562; | |
| --yolo-accent: #33ff99; | |
| --dark-bg: #121212; | |
| --surface-bg: #1E1E2E; | |
| --text-primary: #E0E0E0; | |
| --text-secondary: #BDB2C6; | |
| --border-color: rgba(255, 255, 255, 0.06); | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: var(--dark-bg); | |
| color: var(--text-primary); | |
| padding: 20px; | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .container { | |
| width: 100%; | |
| max-width: 1600px; | |
| margin: 0 auto; | |
| background: var(--surface-bg); | |
| border-radius: 14px; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.35); | |
| border: 1px solid var(--border-color); | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| flex: 1; | |
| } | |
| header { | |
| text-align: center; | |
| padding: 15px; | |
| background: rgba(0,0,0,0.18); | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| h1 { | |
| /* FIX: Removed background-clip text which was causing the solid orange bar */ | |
| color: var(--primary); | |
| text-shadow: 0 2px 10px rgba(231, 53, 98, 0.2); | |
| font-size: 1.8em; | |
| font-weight: 700; | |
| margin-bottom: 4px; | |
| } | |
| .header-note { | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| } | |
| .main-content { | |
| display: flex; | |
| flex-direction: column; | |
| flex: 1; | |
| padding: 20px; | |
| gap: 20px; | |
| } | |
| /* Top Controls Bar */ | |
| .controls-top { | |
| display: flex; | |
| gap: 20px; | |
| background: rgba(0,0,0,0.15); | |
| border-radius: 12px; | |
| padding: 20px; | |
| border: 1px solid var(--border-color); | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .upload-group { | |
| flex: 1; | |
| min-width: 300px; | |
| } | |
| .preview-group { | |
| width: 150px; | |
| height: 100px; | |
| background: #000; | |
| border: 1px solid var(--border-color); | |
| border-radius: 8px; | |
| display: none; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .preview-group img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .actions-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| min-width: 250px; | |
| } | |
| .upload-area { | |
| border: 2px dashed var(--primary); | |
| border-radius: 8px; | |
| padding: 25px; | |
| text-align: center; | |
| transition: all 0.2s ease; | |
| cursor: pointer; | |
| background: rgba(255,255,255,0.02); | |
| height: 100%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-direction: column; | |
| } | |
| .upload-area:hover, .upload-area.dragover { | |
| background: rgba(231, 53, 98, 0.08); | |
| border-color: var(--secondary); | |
| } | |
| .upload-area p { color: var(--text-secondary); font-size: 0.95em; margin: 0; } | |
| label { color: var(--text-secondary); font-size: 0.85em; display: flex; justify-content: space-between; margin-bottom: 4px; } | |
| .slider-val { color: var(--primary); font-weight: bold; } | |
| input[type="range"] { | |
| width: 100%; height: 6px; border-radius: 4px; | |
| background: rgba(255,255,255,0.1); outline: none; -webkit-appearance: none; | |
| cursor: pointer; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; | |
| background: var(--primary); cursor: pointer; | |
| } | |
| .action-btn { | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| color: white; border: none; padding: 12px 24px; | |
| border-radius: 8px; font-weight: 600; | |
| cursor: pointer; width: 100%; | |
| transition: transform 0.2s; | |
| } | |
| .action-btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(231, 53, 98, 0.4); } | |
| .action-btn:disabled { opacity: 0.5; cursor: not-allowed; background: #444; } | |
| /* Results Area */ | |
| .results-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| flex: 1; | |
| min-height: 0; | |
| } | |
| .result-card { | |
| background: rgba(0,0,0,0.2); | |
| border-radius: 12px; | |
| border: 1px solid var(--border-color); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| height: 100%; | |
| } | |
| .card-header { | |
| padding: 12px 15px; | |
| border-bottom: 1px solid var(--border-color); | |
| font-weight: 600; | |
| letter-spacing: 0.5px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| background: rgba(0,0,0,0.1); | |
| } | |
| .dot { width: 8px; height: 8px; border-radius: 50%; } | |
| .dot.rf { background: var(--rf-accent); box-shadow: 0 0 8px var(--rf-accent); } | |
| .dot.yolo { background: var(--yolo-accent); box-shadow: 0 0 8px var(--yolo-accent); } | |
| .image-box { | |
| flex: 1; | |
| background: #000; | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 10px; | |
| height: 100%; | |
| min-height: 500px; | |
| width: 100%; | |
| } | |
| .image-box img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: contain; | |
| display: block; | |
| } | |
| .loader { | |
| position: absolute; | |
| inset: 0; | |
| background: rgba(0,0,0,0.7); | |
| display: none; | |
| justify-content: center; | |
| align-items: center; | |
| flex-direction: column; | |
| color: var(--text-secondary); | |
| font-size: 1.1em; | |
| z-index: 10; | |
| } | |
| .spinner { | |
| width: 40px; height: 40px; | |
| border: 4px solid rgba(255,255,255,0.1); | |
| border-radius: 50%; | |
| margin-bottom: 15px; | |
| animation: spin 1s linear infinite; | |
| } | |
| .rf .spinner { border-top-color: var(--rf-accent); } | |
| .yolo .spinner { border-top-color: var(--yolo-accent); } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| @media (max-width: 900px) { | |
| .controls-top { flex-direction: column; align-items: stretch; } | |
| .preview-group { width: 100%; height: 150px; } | |
| .results-grid { grid-template-columns: 1fr; } | |
| .image-box { min-height: 400px; } | |
| } | |
| .visually-hidden { position:absolute; left:-9999px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>Comparison: RF-DETR vs YOLOv8</h1> | |
| <p class="header-note">Threat Detection on CPU • Upload an image to compare model performance</p> | |
| </header> | |
| <div class="main-content"> | |
| <!-- Top Controls (Full Width) --> | |
| <div class="controls-top"> | |
| <!-- 1. Upload --> | |
| <div class="upload-group"> | |
| <div id="upload-area" class="upload-area"> | |
| <p style="font-size: 1.1em; font-weight: 500; color: var(--text-primary);">Click or Drop Image Here</p> | |
| <p style="font-size: 0.8em; margin-top: 5px;">JPG, PNG supported</p> | |
| </div> | |
| <input id="file-input" class="visually-hidden" type="file" accept="image/*" /> | |
| </div> | |
| <!-- 2. Preview (Appears after upload) --> | |
| <div id="preview-container" class="preview-group"> | |
| <img id="preview-img" src="" alt="Input"> | |
| </div> | |
| <!-- 3. Actions --> | |
| <div class="actions-group"> | |
| <div> | |
| <label>Confidence Threshold <span id="conf-val" class="slider-val">0.25</span></label> | |
| <input id="conf-slider" type="range" min="0.05" max="0.95" step="0.05" value="0.25"> | |
| </div> | |
| <button id="predict-btn" class="action-btn" disabled>RUN COMPARISON</button> | |
| </div> | |
| </div> | |
| <!-- Bottom Results (Grid) --> | |
| <div class="results-grid"> | |
| <!-- RF-DETR --> | |
| <div class="result-card rf"> | |
| <div class="card-header"> | |
| <span class="dot rf"></span> RF-DETR Nano | |
| </div> | |
| <div class="image-box"> | |
| <div class="loader" id="loader-rf"><div class="spinner"></div>Inference Running...</div> | |
| <img id="res-rf"> | |
| </div> | |
| </div> | |
| <!-- YOLOv8 --> | |
| <div class="result-card yolo"> | |
| <div class="card-header"> | |
| <span class="dot yolo"></span> YOLOv8 Nano | |
| </div> | |
| <div class="image-box"> | |
| <div class="loader" id="loader-yolo"><div class="spinner"></div>Inference Running...</div> | |
| <img id="res-yolo"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const fileInput = document.getElementById('file-input'); | |
| const uploadArea = document.getElementById('upload-area'); | |
| const previewContainer = document.getElementById('preview-container'); | |
| const previewImg = document.getElementById('preview-img'); | |
| const predictBtn = document.getElementById('predict-btn'); | |
| const confSlider = document.getElementById('conf-slider'); | |
| const confVal = document.getElementById('conf-val'); | |
| const resRf = document.getElementById('res-rf'); | |
| const resYolo = document.getElementById('res-yolo'); | |
| const loadRf = document.getElementById('loader-rf'); | |
| const loadYolo = document.getElementById('loader-yolo'); | |
| let currentDataUrl = null; | |
| // --- Event Listeners --- | |
| confSlider.addEventListener('input', (e) => confVal.textContent = e.target.value); | |
| 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'); | |
| if(e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]); | |
| }); | |
| fileInput.addEventListener('change', (e) => { | |
| if(e.target.files[0]) handleFile(e.target.files[0]); | |
| }); | |
| function handleFile(file) { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| currentDataUrl = e.target.result; | |
| previewImg.src = currentDataUrl; | |
| previewContainer.style.display = 'block'; // Show preview | |
| predictBtn.disabled = false; | |
| // Clear previous results | |
| resRf.src = ""; | |
| resYolo.src = ""; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| predictBtn.addEventListener('click', async () => { | |
| if(!currentDataUrl) return; | |
| // UI State: Running | |
| predictBtn.disabled = true; | |
| loadRf.style.display = 'flex'; | |
| loadYolo.style.display = 'flex'; | |
| try { | |
| const response = await fetch('/predict', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| image: currentDataUrl, | |
| conf: parseFloat(confSlider.value) | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if(data.error) throw new Error(data.error); | |
| // Set Results | |
| resRf.src = data.rfdetr.image; | |
| resYolo.src = data.yolov8.image; | |
| } catch (err) { | |
| console.error(err); | |
| alert("Prediction Error: " + err.message); | |
| } finally { | |
| predictBtn.disabled = false; | |
| loadRf.style.display = 'none'; | |
| loadYolo.style.display = 'none'; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |