| <!DOCTYPE html> |
| <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); } } |
| |
| |
| .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 { |
| 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-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); } |
| |
| |
| .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;} |
| |
| |
| .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 { |
| 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); } } |
| |
| |
| .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-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-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); } |
| |
| |
| .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; } |
| |
| |
| .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 { 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 + ConvNeXt Fusion + 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 (Recommended)</option> |
| <option value="resnet">ResNet Only</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</div> |
| <img id="camOriginal" src="" alt="Original"> |
| </div> |
| <div class="img-card"> |
| <div style="font-weight:600; color:#e2e8f0;">ResNet Focus</div> |
| <img id="camResnet" src="" alt="ResNet"> |
| </div> |
| <div class="img-card"> |
| <div style="font-weight:600; color:#e2e8f0;">Fusion Focus</div> |
| <img id="camFusion" src="" alt="Fusion"> |
| </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; |
| |
| |
| 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'; |
| }; |
| reader.readAsDataURL(currentFile); |
| } |
| }); |
| |
| |
| function switchResultTab(tabId) { |
| |
| document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); |
| document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); |
| |
| |
| const tabButton = document.querySelector(`.tab[onclick*="${tabId}"]`); |
| if(tabButton) { |
| tabButton.classList.add('active'); |
| } |
| |
| |
| document.getElementById(tabId).classList.add('active'); |
| |
| |
| if(tabId === 'tab-pred') { |
| window.dispatchEvent(new Event('resize')); |
| } |
| } |
| |
| |
| 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; |
| |
| |
| 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 attention maps..."; |
| |
| const fileFormData = new FormData(); |
| fileFormData.append('file', currentFile); |
| |
| const camRes = await fetch(`${API_URL}/predict`, { method: 'POST', body: fileFormData }); |
| if(!camRes.ok) throw new Error("GradCAM API Failed"); |
| const camData = await camRes.json(); |
| |
| |
| loaderStatusText.innerText = "Running YOLO object detection..."; |
| const yoloRes = await fetch(`${API_URL}/predict/yolo`, { method: 'POST', body: fileFormData }); |
| if(!yoloRes.ok) throw new Error("YOLO API Failed"); |
| const yoloData = await yoloRes.json(); |
| |
| |
| |
| |
| let confVal = 0; |
| |
| let highestClass = Object.keys(predData).reduce((a, b) => predData[a] > predData[b] ? a : b); |
| confVal = (predData[highestClass] * 100).toFixed(2); |
| document.getElementById('finalPredText').innerText = highestClass; |
| document.getElementById('confText').innerText = `Confidence Score: ${confVal}%`; |
| drawChart(predData, `${engineMode === 'fusion' ? 'Fusion' : 'ResNet'} Output`); |
| |
| |
| setTimeout(() => { document.getElementById('confBar').style.width = confVal + '%'; }, 100); |
| |
| |
| document.getElementById('camOriginal').src = `${API_URL}${camData.original_image}`; |
| document.getElementById('camResnet').src = `${API_URL}${camData.resnet_viz}`; |
| document.getElementById('camFusion').src = `${API_URL}${camData.fusion_viz}`; |
| |
| |
| document.getElementById('yoloImage').src = `${API_URL}${yoloData.yolo_image}`; |
| const logContainer = document.getElementById('yoloLogContainer'); |
| if (yoloData.total_detections === 0) { |
| logContainer.innerHTML = `<div style="color: #a1a1aa; padding: 10px;">🟢 No specific damage bounding 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) => { |
| let detConf = (det.confidence * 100).toFixed(1); |
| logHTML += ` |
| <div class="detection-item"> |
| <b style="color: #e2e8f0;">Region ${idx + 1}</b><br> |
| <span style="color: #a1a1aa; font-size: 0.9em;">Confidence: ${detConf}%</span> |
| </div> |
| `; |
| }); |
| logContainer.innerHTML = logHTML; |
| } |
| |
| |
| resultsSection.style.display = 'block'; |
| switchResultTab('tab-pred'); |
| |
| } catch(e) { |
| alert(`Error connecting to AI server. It might be waking up or offline. Details: ${e.message}`); |
| console.error(e); |
| } finally { |
| loader.style.display = 'none'; |
| scanLine.style.display = 'none'; |
| analyzeBtn.disabled = false; |
| analyzeBtn.innerText = "🚀 Run AI Analysis"; |
| } |
| } |
| </script> |
|
|
| </body> |
| </html> |