| | <!DOCTYPE html> |
| | <html lang="en"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | <title>Traffic Sign Classification</title> |
| | <style> |
| | * { |
| | margin: 0; |
| | padding: 0; |
| | box-sizing: border-box; |
| | } |
| | |
| | body { |
| | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | min-height: 100vh; |
| | padding: 20px; |
| | } |
| | |
| | .container { |
| | max-width: 1200px; |
| | margin: 0 auto; |
| | background: white; |
| | border-radius: 20px; |
| | box-shadow: 0 20px 60px rgba(0,0,0,0.3); |
| | overflow: hidden; |
| | } |
| | |
| | .header { |
| | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | color: white; |
| | padding: 40px; |
| | text-align: center; |
| | } |
| | |
| | .header h1 { |
| | font-size: 2.5em; |
| | margin-bottom: 10px; |
| | } |
| | |
| | .header p { |
| | font-size: 1.2em; |
| | opacity: 0.9; |
| | margin-bottom: 20px; |
| | } |
| | |
| | .github-link { |
| | display: inline-flex; |
| | align-items: center; |
| | gap: 8px; |
| | color: white; |
| | text-decoration: none; |
| | font-weight: 600; |
| | font-size: 1.1em; |
| | padding: 12px 24px; |
| | background: rgba(255, 255, 255, 0.2); |
| | border: 2px solid white; |
| | border-radius: 10px; |
| | transition: all 0.3s; |
| | } |
| | |
| | .github-link:hover { |
| | background: white; |
| | color: #667eea; |
| | transform: translateY(-2px); |
| | box-shadow: 0 5px 15px rgba(0,0,0,0.2); |
| | } |
| | |
| | .content { |
| | padding: 40px; |
| | } |
| | |
| | .info-section { |
| | background: #f8f9fa; |
| | border-radius: 15px; |
| | padding: 30px; |
| | margin-bottom: 30px; |
| | border-left: 5px solid #667eea; |
| | } |
| | |
| | .info-section h2 { |
| | color: #667eea; |
| | margin-bottom: 15px; |
| | font-size: 1.8em; |
| | } |
| | |
| | .info-section p { |
| | color: #495057; |
| | line-height: 1.8; |
| | font-size: 1.05em; |
| | } |
| | |
| | .upload-section { |
| | background: #f8f9fa; |
| | border-radius: 15px; |
| | padding: 40px; |
| | text-align: center; |
| | margin-bottom: 30px; |
| | border: 3px dashed #667eea; |
| | transition: all 0.3s; |
| | } |
| | |
| | .upload-section:hover { |
| | border-color: #764ba2; |
| | background: #f0f2ff; |
| | } |
| | |
| | .upload-icon { |
| | font-size: 4em; |
| | color: #667eea; |
| | margin-bottom: 20px; |
| | } |
| | |
| | .file-input { |
| | display: none; |
| | } |
| | |
| | .upload-button { |
| | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | color: white; |
| | padding: 15px 40px; |
| | border: none; |
| | border-radius: 30px; |
| | font-size: 1.2em; |
| | font-weight: bold; |
| | cursor: pointer; |
| | transition: transform 0.2s; |
| | margin: 10px; |
| | } |
| | |
| | .upload-button:hover { |
| | transform: scale(1.05); |
| | } |
| | |
| | .test-button { |
| | background: #28a745; |
| | color: white; |
| | padding: 15px 40px; |
| | border: none; |
| | border-radius: 30px; |
| | font-size: 1.2em; |
| | font-weight: bold; |
| | cursor: pointer; |
| | transition: transform 0.2s; |
| | margin: 10px; |
| | } |
| | |
| | .test-button:hover { |
| | background: #218838; |
| | transform: scale(1.05); |
| | } |
| | |
| | .results-section { |
| | display: none; |
| | } |
| | |
| | .result-card { |
| | background: white; |
| | border-radius: 15px; |
| | padding: 30px; |
| | box-shadow: 0 5px 20px rgba(0,0,0,0.1); |
| | margin-bottom: 30px; |
| | } |
| | |
| | .result-header { |
| | text-align: center; |
| | margin-bottom: 30px; |
| | } |
| | |
| | .result-image { |
| | max-width: 400px; |
| | max-height: 400px; |
| | border-radius: 15px; |
| | box-shadow: 0 10px 30px rgba(0,0,0,0.3); |
| | margin: 0 auto 30px; |
| | display: block; |
| | border: 4px solid #667eea; |
| | } |
| | |
| | .prediction-main { |
| | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | color: white; |
| | padding: 30px; |
| | border-radius: 15px; |
| | text-align: center; |
| | margin-bottom: 30px; |
| | } |
| | |
| | .prediction-main h2 { |
| | font-size: 2em; |
| | margin-bottom: 10px; |
| | } |
| | |
| | .confidence { |
| | font-size: 3em; |
| | font-weight: bold; |
| | margin: 20px 0; |
| | } |
| | |
| | .confidence-bar { |
| | background: rgba(255,255,255,0.3); |
| | height: 30px; |
| | border-radius: 15px; |
| | overflow: hidden; |
| | margin-top: 20px; |
| | } |
| | |
| | .confidence-fill { |
| | background: white; |
| | height: 100%; |
| | transition: width 0.5s; |
| | } |
| | |
| | .top5-predictions { |
| | margin-top: 30px; |
| | } |
| | |
| | .top5-predictions h3 { |
| | color: #667eea; |
| | margin-bottom: 20px; |
| | font-size: 1.5em; |
| | } |
| | |
| | .prediction-item { |
| | display: flex; |
| | justify-content: space-between; |
| | align-items: center; |
| | padding: 15px; |
| | background: #f8f9fa; |
| | border-radius: 10px; |
| | margin-bottom: 10px; |
| | transition: transform 0.2s; |
| | } |
| | |
| | .prediction-item:hover { |
| | transform: translateX(5px); |
| | background: #e9ecef; |
| | } |
| | |
| | .prediction-name { |
| | font-weight: 600; |
| | color: #495057; |
| | } |
| | |
| | .prediction-confidence { |
| | font-size: 1.2em; |
| | font-weight: bold; |
| | color: #667eea; |
| | } |
| | |
| | .loading { |
| | display: none; |
| | text-align: center; |
| | padding: 40px; |
| | } |
| | |
| | .spinner { |
| | border: 5px solid #f3f3f3; |
| | border-top: 5px solid #667eea; |
| | border-radius: 50%; |
| | width: 60px; |
| | height: 60px; |
| | animation: spin 1s linear infinite; |
| | margin: 0 auto 20px; |
| | } |
| | |
| | @keyframes spin { |
| | 0% { transform: rotate(0deg); } |
| | 100% { transform: rotate(360deg); } |
| | } |
| | |
| | .error { |
| | display: none; |
| | background: #f8d7da; |
| | color: #721c24; |
| | padding: 20px; |
| | border-radius: 10px; |
| | border-left: 5px solid #dc3545; |
| | margin-bottom: 20px; |
| | } |
| | |
| | .reset-button { |
| | background: #6c757d; |
| | color: white; |
| | padding: 12px 30px; |
| | border: none; |
| | border-radius: 25px; |
| | font-size: 1.1em; |
| | cursor: pointer; |
| | margin-top: 20px; |
| | transition: transform 0.2s; |
| | } |
| | |
| | .reset-button:hover { |
| | background: #5a6268; |
| | transform: scale(1.05); |
| | } |
| | </style> |
| | </head> |
| | <body> |
| | <div class="container"> |
| | <div class="header"> |
| | <h1>🚦 Traffic Sign Classification</h1> |
| | <p>AI-Powered Recognition of 43 Traffic Sign Categories</p> |
| | <a href="https://github.com/koesan/Traffic_Sign_Classification" target="_blank" class="github-link"> |
| | <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> |
| | <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/> |
| | </svg> |
| | View on GitHub |
| | </a> |
| | </div> |
| |
|
| | <div class="content"> |
| | |
| | <div class="info-section"> |
| | <h2>📖 About</h2> |
| | <p style="font-size: 1.1em; line-height: 1.8;"> |
| | <strong>CNN-based traffic sign recognition</strong> system with 99.27% accuracy across 43 sign categories. |
| | Upload an image and the AI model will identify the traffic sign type with confidence scores. |
| | </p> |
| | <p style="font-size: 1.05em; color: #6c757d; margin-top: 15px;"> |
| | <strong>Usage:</strong> Click "Choose Image" or "Test Example" → View classification results |
| | </p> |
| | </div> |
| |
|
| | |
| | <div class="error" id="errorMessage"></div> |
| |
|
| | |
| | <div class="upload-section" id="uploadSection"> |
| | <div class="upload-icon">📤</div> |
| | <h2 style="color: #667eea; margin-bottom: 15px;">Upload Traffic Sign Image</h2> |
| | <p style="color: #6c757d; margin-bottom: 20px;"> |
| | Supported formats: PNG, JPG, JPEG<br> |
| | Images will be automatically resized to 32×32 pixels |
| | </p> |
| | <input type="file" id="fileInput" class="file-input" accept="image/*"> |
| | <button class="upload-button" onclick="document.getElementById('fileInput').click()"> |
| | Choose Image |
| | </button> |
| | <button class="test-button" onclick="testExample()"> |
| | 🧪 Test Example |
| | </button> |
| | </div> |
| |
|
| | |
| | <div class="loading" id="loading"> |
| | <div class="spinner"></div> |
| | <p style="color: #667eea; font-size: 1.2em;">Analyzing traffic sign...</p> |
| | </div> |
| |
|
| | |
| | <div class="results-section" id="resultsSection"> |
| | <div class="result-card"> |
| | <div class="result-header"> |
| | <img id="resultImage" class="result-image" alt="Traffic Sign"> |
| | </div> |
| |
|
| | <div class="prediction-main"> |
| | <h2>🚦 Predicted Sign</h2> |
| | <h1 id="predictedClass" style="margin: 20px 0; font-size: 2.5em;"></h1> |
| | <div class="confidence" id="confidenceValue"></div> |
| | <div class="confidence-bar"> |
| | <div class="confidence-fill" id="confidenceFill"></div> |
| | </div> |
| | </div> |
| |
|
| | <div class="top5-predictions"> |
| | <h3>📊 Top 5 Predictions</h3> |
| | <div id="top5List"></div> |
| | </div> |
| |
|
| | <div style="text-align: center;"> |
| | <button class="reset-button" onclick="reset()"> |
| | 🔄 Classify Another Image |
| | </button> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | <script> |
| | document.getElementById('fileInput').addEventListener('change', function(e) { |
| | const file = e.target.files[0]; |
| | if (file) { |
| | analyzeImage(file); |
| | } |
| | }); |
| | |
| | async function analyzeImage(file) { |
| | if (!file.type.startsWith('image/')) { |
| | showError('Please upload a valid image file (PNG, JPG, JPEG)'); |
| | return; |
| | } |
| | |
| | |
| | document.getElementById('loading').style.display = 'block'; |
| | document.getElementById('uploadSection').style.display = 'none'; |
| | document.getElementById('resultsSection').style.display = 'none'; |
| | document.getElementById('errorMessage').style.display = 'none'; |
| | |
| | const formData = new FormData(); |
| | formData.append('file', file); |
| | |
| | try { |
| | const response = await fetch('/predict', { |
| | method: 'POST', |
| | body: formData |
| | }); |
| | |
| | const data = await response.json(); |
| | |
| | document.getElementById('loading').style.display = 'none'; |
| | |
| | if (data.error) { |
| | showError(data.error); |
| | } else { |
| | showResults(data); |
| | } |
| | } catch (error) { |
| | document.getElementById('loading').style.display = 'none'; |
| | showError('An error occurred: ' + error.message); |
| | } |
| | } |
| | |
| | async function testExample() { |
| | |
| | document.getElementById('loading').style.display = 'block'; |
| | document.getElementById('uploadSection').style.display = 'none'; |
| | document.getElementById('resultsSection').style.display = 'none'; |
| | document.getElementById('errorMessage').style.display = 'none'; |
| | |
| | try { |
| | const response = await fetch('/test-example', { |
| | method: 'POST' |
| | }); |
| | |
| | const data = await response.json(); |
| | |
| | document.getElementById('loading').style.display = 'none'; |
| | |
| | if (data.error) { |
| | showError(data.error); |
| | } else { |
| | showResults(data); |
| | } |
| | } catch (error) { |
| | document.getElementById('loading').style.display = 'none'; |
| | showError('An error occurred: ' + error.message); |
| | } |
| | } |
| | |
| | function showResults(data) { |
| | document.getElementById('resultImage').src = data.image; |
| | document.getElementById('predictedClass').textContent = data.predicted_class; |
| | document.getElementById('confidenceValue').textContent = (data.confidence * 100).toFixed(1) + '%'; |
| | document.getElementById('confidenceFill').style.width = (data.confidence * 100) + '%'; |
| | |
| | |
| | const top5List = document.getElementById('top5List'); |
| | top5List.innerHTML = ''; |
| | |
| | data.top5_predictions.forEach((pred, index) => { |
| | const item = document.createElement('div'); |
| | item.className = 'prediction-item'; |
| | item.innerHTML = ` |
| | <span class="prediction-name"> |
| | <strong>${index + 1}.</strong> ${pred.class_name} |
| | </span> |
| | <span class="prediction-confidence"> |
| | ${(pred.confidence * 100).toFixed(1)}% |
| | </span> |
| | `; |
| | top5List.appendChild(item); |
| | }); |
| | |
| | document.getElementById('resultsSection').style.display = 'block'; |
| | } |
| | |
| | function showError(message) { |
| | const errorElement = document.getElementById('errorMessage'); |
| | errorElement.textContent = '❌ Error: ' + message; |
| | errorElement.style.display = 'block'; |
| | document.getElementById('uploadSection').style.display = 'block'; |
| | } |
| | |
| | function reset() { |
| | document.getElementById('fileInput').value = ''; |
| | document.getElementById('uploadSection').style.display = 'block'; |
| | document.getElementById('resultsSection').style.display = 'none'; |
| | document.getElementById('errorMessage').style.display = 'none'; |
| | document.getElementById('loading').style.display = 'none'; |
| | } |
| | </script> |
| | </body> |
| | </html> |
| |
|