Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Car Damage AI</title> | |
| <script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap'); | |
| :root { | |
| --bg-dark: #09090b; | |
| --bg-card: #18181b; | |
| --text-primary: #e2e8f0; | |
| --text-secondary: #a1a1aa; | |
| --accent: #00c6ff; | |
| --accent-hover: #0072ff; | |
| --glass: rgba(255, 255, 255, 0.03); | |
| --card-border: #27272a; | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Inter', sans-serif; } | |
| body { | |
| background-color: var(--bg-dark); | |
| color: var(--text-primary); | |
| min-height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| align-items: flex-start; | |
| padding: 40px 20px; | |
| background-image: radial-gradient(circle at top right, rgba(0, 198, 255, 0.05) 0%, transparent 40%); | |
| } | |
| .container { | |
| width: 100%; | |
| max-width: 850px; | |
| background: var(--bg-card); | |
| border-radius: 20px; | |
| padding: 35px; | |
| box-shadow: 0 20px 40px rgba(0,0,0,0.6); | |
| animation: slideUpFade 0.6s ease-out forwards; | |
| border: 1px solid var(--card-border); | |
| } | |
| @keyframes slideUpFade { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } } | |
| /* Shimmering Main Title */ | |
| .shimmer-text { | |
| text-align: center; | |
| font-size: 2.5rem; | |
| font-weight: 800; | |
| background: linear-gradient(90deg, #e2e8f0 0%, #ffffff 25%, #00c6ff 50%, #e2e8f0 75%, #e2e8f0 100%); | |
| background-size: 200% auto; | |
| color: transparent; | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| animation: shimmer 4s linear infinite; | |
| margin-bottom: 0.2rem; | |
| } | |
| @keyframes shimmer { 0% { background-position: -200% center; } 100% { background-position: 200% center; } } | |
| .subtitle { text-align: center; color: var(--text-secondary); font-size: 1rem; margin-bottom: 25px; } | |
| /* Warning Box */ | |
| .warning-box { | |
| background: rgba(0, 198, 255, 0.1); | |
| border-left: 4px solid var(--accent); | |
| color: var(--text-primary); | |
| padding: 12px 15px; | |
| border-radius: 8px; | |
| margin-bottom: 25px; | |
| font-size: 0.9rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| /* Controls Section */ | |
| .controls-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| margin-bottom: 25px; | |
| } | |
| .file-wrapper { | |
| position: relative; height: 160px; border: 2px dashed #444; border-radius: 16px; | |
| display: flex; justify-content: center; align-items: center; cursor: pointer; | |
| transition: all 0.3s ease; background: var(--glass); overflow: hidden; | |
| } | |
| .file-wrapper:hover { border-color: var(--accent); background: rgba(0, 198, 255, 0.05); } | |
| .file-wrapper input { position: absolute; width: 100%; height: 100%; opacity: 0; cursor: pointer; z-index: 2; } | |
| .settings-card { | |
| background: rgba(0,0,0,0.2); | |
| border-radius: 16px; | |
| padding: 20px; | |
| border: 1px solid var(--card-border); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| } | |
| select { | |
| width: 100%; background: #27272a; border: 1px solid #3f3f46; padding: 14px; | |
| border-radius: 12px; color: white; outline: none; margin-top: 10px; font-size: 1rem; | |
| } | |
| select:focus { border-color: var(--accent); } | |
| /* Preview Area & Animations */ | |
| .image-area { | |
| width: 100%; height: 350px; background: #09090b; border-radius: 16px; | |
| margin-bottom: 25px; display: none; justify-content: center; align-items: center; | |
| overflow: hidden; position: relative; border: 1px solid var(--card-border); | |
| } | |
| .image-area img { max-width: 100%; max-height: 100%; object-fit: contain; z-index: 1;} | |
| /* Scanner Animation */ | |
| .scan-line { | |
| position: absolute; top: -10%; left: 0; width: 100%; height: 5px; | |
| background: var(--accent); box-shadow: 0 0 15px var(--accent), 0 0 30px var(--accent); | |
| z-index: 5; opacity: 0.8; display: none; animation: scanMove 2s ease-in-out infinite; filter: blur(1px); | |
| } | |
| @keyframes scanMove { 0% { top: -10%; opacity: 0.5; } 50% { opacity: 1; } 100% { top: 110%; opacity: 0.5; } } | |
| /* Loader Overlay */ | |
| .loader-overlay { | |
| position: absolute; top: 0; left: 0; width: 100%; height: 100%; | |
| background: rgba(0,0,0,0.65); backdrop-filter: blur(4px); | |
| display: none; flex-direction: column; justify-content: center; align-items: center; z-index: 10; | |
| } | |
| .spinner { | |
| width: 50px; height: 50px; border: 4px solid rgba(0, 198, 255, 0.2); | |
| border-top: 4px solid var(--accent); border-radius: 50%; | |
| animation: spin 1s cubic-bezier(0.68, -0.55, 0.27, 1.55) infinite; margin-bottom: 15px; | |
| } | |
| @keyframes spin { 100% { transform: rotate(360deg); } } | |
| /* Buttons */ | |
| .btn { | |
| width: 100%; padding: 16px; background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%); | |
| color: white; border: none; border-radius: 12px; cursor: pointer; font-weight: 700; font-size: 1rem; | |
| transition: all 0.3s ease; box-shadow: 0 4px 15px rgba(0, 114, 255, 0.3); | |
| } | |
| .btn:hover:not(:disabled) { transform: scale(1.02); box-shadow: 0 8px 25px rgba(0, 198, 255, 0.5); } | |
| .btn:disabled { background: #444; color: #888; box-shadow: none; transform: none; cursor: not-allowed;} | |
| /* Results Tabs */ | |
| .results-section { display: none; margin-top: 30px; animation: slideUpFade 0.5s ease-out; } | |
| .tabs { display: flex; gap: 10px; margin-bottom: 20px; border-bottom: 1px solid var(--card-border); padding-bottom: 10px; overflow-x: auto; } | |
| .tab { | |
| padding: 10px 20px; cursor: pointer; border-radius: 8px; color: var(--text-secondary); | |
| font-weight: 600; transition: all 0.3s ease; white-space: nowrap; | |
| } | |
| .tab.active { background: rgba(0, 198, 255, 0.1); color: var(--accent); } | |
| .tab-content { display: none; } | |
| .tab-content.active { display: block; animation: slideUpFade 0.4s ease-out; } | |
| /* Progress Bar */ | |
| .progress-wrapper { background: #27272a; border-radius: 20px; overflow: hidden; height: 12px; margin: 10px 0 20px 0; box-shadow: inset 0 2px 4px rgba(0,0,0,0.5); } | |
| .progress-fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent-hover)); border-radius: 20px; width: 0%; transition: width 1.5s cubic-bezier(0.22, 1, 0.36, 1); } | |
| /* Final Prediction Text */ | |
| .big-text { font-size: 2.5rem; font-weight: 800; background: -webkit-linear-gradient(45deg, #00c6ff, #0072ff); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 5px; } | |
| /* Images Grid (Attention Maps) */ | |
| .img-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; } | |
| .img-card { background: rgba(0,0,0,0.3); border: 1px solid var(--card-border); border-radius: 12px; padding: 10px; text-align: center; } | |
| .img-card img { width: 100%; border-radius: 8px; margin-top: 10px; } | |
| /* YOLO Grid */ | |
| .yolo-grid { display: grid; grid-template-columns: 1.5fr 1fr; gap: 20px; } | |
| .log-box { background: rgba(0,0,0,0.3); border: 1px solid var(--card-border); border-radius: 12px; padding: 20px; height: 100%; } | |
| .detection-item { background: #27272a; padding: 12px; border-radius: 8px; margin-bottom: 10px; border-left: 4px solid var(--accent); box-shadow: 0 2px 4px rgba(0,0,0,0.2); } | |
| @media (max-width: 768px) { | |
| .controls-grid, .img-grid, .yolo-grid { grid-template-columns: 1fr; } | |
| .shimmer-text { font-size: 2rem; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="shimmer-text">🚗 Car Damage AI</div> | |
| <div class="subtitle">Fusion Intelligence: ResNet + YOLO</div> | |
| <div class="warning-box"> | |
| <span style="font-size: 1.2rem;">⏱️</span> | |
| <span><b>Note:</b> The first analysis may take up to 3-4 mins while models warm up. Subsequent requests are faster!</span> | |
| </div> | |
| <div class="controls-grid"> | |
| <div class="file-wrapper"> | |
| <input type="file" id="fileInput" accept="image/jpeg, image/png, image/jpg"> | |
| <div style="text-align: center;"> | |
| <p style="font-size: 2.5rem; margin-bottom: 5px;">📷</p> | |
| <p style="color:#a1a1aa; font-weight: 500;">Tap or Drag & Drop Vehicle Image</p> | |
| </div> | |
| </div> | |
| <div class="settings-card"> | |
| <h3 style="font-size: 1.1rem; margin-bottom: 5px;">⚙️ Analysis Settings</h3> | |
| <p style="font-size: 0.85rem; color: var(--text-secondary);">Select the neural network pipeline.</p> | |
| <select id="engineMode"> | |
| <option value="fusion">Fusion</option> | |
| <option value="resnet">ResNet</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="image-area" id="previewBox"> | |
| <img id="displayImage" src="" alt="Car Image"> | |
| <div class="scan-line" id="scanLine"></div> | |
| <div class="loader-overlay" id="loader"> | |
| <div class="spinner"></div> | |
| <p style="color:white; font-weight:600; letter-spacing: 1px; margin-bottom: 5px;">🧠 ANALYZING...</p> | |
| <p id="loaderStatusText" style="color:#00c6ff; font-size:0.9rem;">Extracting features...</p> | |
| </div> | |
| </div> | |
| <button class="btn" id="analyzeBtn" onclick="analyze()">🚀 Run AI Analysis</button> | |
| <div class="results-section" id="resultsSection"> | |
| <div class="tabs"> | |
| <div class="tab active" onclick="switchResultTab('tab-pred')">📊 Prediction</div> | |
| <div class="tab" onclick="switchResultTab('tab-attention')">👀 Attention Maps</div> | |
| <div class="tab" onclick="switchResultTab('tab-yolo')">🎯 Localization</div> | |
| </div> | |
| <div id="tab-pred" class="tab-content active"> | |
| <div class="settings-card"> | |
| <div id="finalPredText" class="big-text">--</div> | |
| <div style="font-weight: 600; margin-top: 5px;" id="confText">Confidence Score: 0%</div> | |
| <div class="progress-wrapper"> | |
| <div class="progress-fill" id="confBar"></div> | |
| </div> | |
| <h3 style="margin: 15px 0 5px 0; font-size: 1.1rem;">Probability Distribution</h3> | |
| <div id="plotlyChart" style="width:100%; height:300px;"></div> | |
| </div> | |
| </div> | |
| <div id="tab-attention" class="tab-content"> | |
| <div class="img-grid"> | |
| <div class="img-card"> | |
| <div style="font-weight:600; color:#e2e8f0;">Original Image</div> | |
| <img id="camOriginal" src="" alt="Original Image"> | |
| </div> | |
| <div class="img-card"> | |
| <div id="camSelectedLabel" style="font-weight:600; color:#e2e8f0;">Selected Grad-CAM</div> | |
| <img id="camSelected" src="" alt="Selected Grad-CAM"> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="tab-yolo" class="tab-content"> | |
| <div class="yolo-grid"> | |
| <div class="settings-card"> | |
| <h3 style="margin-bottom: 10px;">Bounding Boxes</h3> | |
| <img id="yoloImage" src="" alt="YOLO Output" style="width: 100%; border-radius: 8px;"> | |
| </div> | |
| <div class="log-box"> | |
| <h3 style="margin-bottom: 15px;">Detection Log</h3> | |
| <div id="yoloLogContainer"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const API_URL = "http://127.0.0.1:8000"; | |
| let currentFile = null; | |
| // DOM Elements | |
| const fileInput = document.getElementById('fileInput'); | |
| const displayImage = document.getElementById('displayImage'); | |
| const previewBox = document.getElementById('previewBox'); | |
| const resultsSection = document.getElementById('resultsSection'); | |
| const loader = document.getElementById('loader'); | |
| const loaderStatusText = document.getElementById('loaderStatusText'); | |
| const scanLine = document.getElementById('scanLine'); | |
| const analyzeBtn = document.getElementById('analyzeBtn'); | |
| fileInput.addEventListener('change', e => { | |
| if(e.target.files[0]) { | |
| currentFile = e.target.files[0]; | |
| const reader = new FileReader(); | |
| reader.onload = x => { | |
| displayImage.src = x.target.result; | |
| previewBox.style.display = 'flex'; | |
| resultsSection.style.display = 'none'; // Hide old results | |
| }; | |
| reader.readAsDataURL(currentFile); | |
| } | |
| }); | |
| // --- BUG FIX IS HERE --- | |
| function switchResultTab(tabId) { | |
| // 1. Remove active state from all tabs and panels | |
| document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); | |
| document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); | |
| // 2. Find the tab button that corresponds to this panel and make it active | |
| const tabButton = document.querySelector(`.tab[onclick*="${tabId}"]`); | |
| if(tabButton) { | |
| tabButton.classList.add('active'); | |
| } | |
| // 3. Make the specific panel active | |
| document.getElementById(tabId).classList.add('active'); | |
| // 4. Resize Plotly chart if switching back to its tab to prevent layout squash | |
| if(tabId === 'tab-pred') { | |
| window.dispatchEvent(new Event('resize')); | |
| } | |
| } | |
| // Plotly Chart Helper | |
| function drawChart(dataObj, title) { | |
| const labels = Object.keys(dataObj); | |
| const values = Object.values(dataObj); | |
| const trace = { | |
| x: labels, | |
| y: values, | |
| type: 'bar', | |
| marker: { color: '#00c6ff', line: { color: '#0072ff', width: 1.5 } }, | |
| opacity: 0.85 | |
| }; | |
| const layout = { | |
| title: title || '', | |
| paper_bgcolor: 'rgba(0,0,0,0)', | |
| plot_bgcolor: 'rgba(0,0,0,0)', | |
| font: { family: 'Inter', color: '#a1a1aa' }, | |
| margin: { l: 40, r: 10, t: 30, b: 40 }, | |
| xaxis: { title: 'Classes' }, | |
| yaxis: { title: 'Probability', range: [0, 1] } | |
| }; | |
| Plotly.newPlot('plotlyChart', [trace], layout, {displayModeBar: false, responsive: true}); | |
| } | |
| async function analyze() { | |
| if(!currentFile) return alert("Please upload an image first."); | |
| const engineMode = document.getElementById('engineMode').value; // fusion or resnet | |
| // UI Prep | |
| loader.style.display = 'flex'; | |
| scanLine.style.display = 'block'; | |
| analyzeBtn.disabled = true; | |
| analyzeBtn.innerText = "Processing..."; | |
| resultsSection.style.display = 'none'; | |
| const formData = new FormData(); | |
| formData.append('image', currentFile); | |
| try { | |
| loaderStatusText.innerText = "Extracting features..."; | |
| const predRes = await fetch(`${API_URL}/predict/${engineMode}`, { method: 'POST', body: formData }); | |
| if (!predRes.ok) throw new Error("Prediction API failed"); | |
| const predData = await predRes.json(); | |
| loaderStatusText.innerText = "Generating Grad-CAM..."; | |
| const camForm = new FormData(); | |
| camForm.append('file', currentFile); | |
| const camRes = await fetch(`${API_URL}/predict?mode=${engineMode}`, { method: 'POST', body: camForm }); | |
| if (!camRes.ok) throw new Error("Grad-CAM API failed"); | |
| const camData = await camRes.json(); | |
| loaderStatusText.innerText = "Running YOLO detection..."; | |
| const yoloRes = await fetch(`${API_URL}/predict/yolo`, { method: 'POST', body: camForm }); | |
| if (!yoloRes.ok) throw new Error("YOLO API failed"); | |
| const yoloData = await yoloRes.json(); | |
| const highestClass = Object.keys(predData).reduce((a, b) => predData[a] > predData[b] ? a : b); | |
| const highestScore = predData[highestClass] || 0; | |
| document.getElementById('finalPredText').innerText = highestClass; | |
| document.getElementById('confText').innerText = `Confidence Score: ${(highestScore * 100).toFixed(2)}%`; | |
| drawChart(predData, `${engineMode.toUpperCase()} Output`); | |
| setTimeout(() => { document.getElementById('confBar').style.width = `${(highestScore * 100).toFixed(2)}%`; }, 100); | |
| document.getElementById('camOriginal').src = `${API_URL}${camData.original_image}`; | |
| document.getElementById('camSelected').src = `${API_URL}${camData.selected_viz}`; | |
| document.getElementById('camSelectedLabel').innerText = engineMode === 'fusion' ? 'Fusion Grad-CAM' : 'ResNet Grad-CAM'; | |
| document.getElementById('yoloImage').src = `${API_URL}${yoloData.yolo_image}`; | |
| const logContainer = document.getElementById('yoloLogContainer'); | |
| if (!yoloData.detections || yoloData.detections.length === 0) { | |
| logContainer.innerHTML = '<div style="color: #a1a1aa; padding: 10px;">🟢 No damage boxes detected.</div>'; | |
| } else { | |
| let logHTML = `<div style="color: #ffcc00; margin-bottom: 10px; font-weight:600;">🔴 Found ${yoloData.total_detections} damage region(s).</div>`; | |
| yoloData.detections.forEach((det, idx) => { | |
| logHTML += `<div class="detection-item"><b style="color: #e2e8f0;">Region ${idx + 1}</b><br><span style="color: #a1a1aa; font-size: 0.9em;">${det.label} · ${(det.confidence * 100).toFixed(1)}%</span></div>`; | |
| }); | |
| logContainer.innerHTML = logHTML; | |
| } | |
| resultsSection.style.display = 'block'; | |
| switchResultTab('tab-pred'); | |
| } catch (error) { | |
| alert(`Error connecting to AI server. Details: ${error.message}`); | |
| console.error(error); | |
| } finally { | |
| loader.style.display = 'none'; | |
| scanLine.style.display = 'none'; | |
| analyzeBtn.disabled = false; | |
| analyzeBtn.innerText = "🚀 Run AI Analysis"; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |