edusight-scanner / index.html
Avocado3's picture
generate me quantity scanner that can generate the number of equipments, item present from the "file uploaded, or "photo taken", a scanner that can recognize school equipments like "monitors, chairs, tables, keyboards, printers, etc", with working camera as the scanner
e09776b verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AssetSnap | School Equipment Scanner</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/feather-icons"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/coco-ssd@latest"></script>
<style>
#cameraFeed {
transform: scaleX(-1);
}
.object-tag {
position: absolute;
background: rgba(59, 130, 246, 0.7);
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
pointer-events: none;
}
.progress-ring__circle {
transition: stroke-dashoffset 0.35s;
transform: rotate(-90deg);
transform-origin: 50% 50%;
}
</style>
</head>
<body class="bg-gray-100 min-h-screen">
<div class="container mx-auto px-4 py-8">
<!-- Scanner Mode Selection -->
<div class="flex justify-center mb-8">
<div class="inline-flex rounded-md shadow-sm">
<button id="liveScannerBtn" class="px-4 py-2 text-sm font-medium rounded-l-lg bg-blue-600 text-white">
<i data-feather="video"></i> Live Scanner
</button>
<button id="uploadScannerBtn" class="px-4 py-2 text-sm font-medium rounded-r-lg bg-gray-100 text-gray-700 hover:bg-gray-200">
<i data-feather="upload"></i> Upload Scanner
</button>
</div>
</div>
<!-- Header -->
<header class="mb-12 text-center">
<h1 class="text-4xl font-bold text-blue-600 mb-2">EduSight Scanner</h1>
<p class="text-lg text-gray-600">Count school equipment from photos or live camera</p>
</header>
<!-- Main Content -->
<div class="flex flex-col lg:flex-row gap-8">
<!-- Scanner Section -->
<div id="scannerSection" class="lg:w-2/3 bg-white rounded-xl shadow-lg overflow-hidden">
<div class="p-4 bg-blue-600 text-white">
<h2 class="text-xl font-semibold flex items-center gap-2">
<i data-feather="camera"></i> Live Detection
</h2>
</div>
<div class="relative bg-black aspect-video flex items-center justify-center">
<video id="cameraFeed" autoplay playsinline class="w-full h-full object-cover"></video>
<div id="detectionOverlay" class="absolute inset-0"></div>
<div id="noCamera" class="text-white text-center p-4 hidden">
<i data-feather="video-off" class="w-12 h-12 mx-auto mb-4"></i>
<p>Camera access required for live scanning</p>
<button id="enableCameraBtn" class="mt-4 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">
Enable Camera
</button>
</div>
</div>
<div class="p-4 flex flex-col gap-4">
<div class="flex justify-between items-center">
<button id="captureBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg flex items-center gap-2">
<i data-feather="scan"></i> Scan Now
</button>
<button id="uploadBtn" class="bg-purple-600 hover:bg-purple-700 text-white px-6 py-2 rounded-lg flex items-center gap-2">
<i data-feather="upload"></i> Upload Photo
</button>
<input type="file" id="fileInput" accept="image/*" class="hidden">
</div>
<div class="flex items-center gap-2 text-gray-600">
<i data-feather="info"></i>
<span>Point camera or upload photo of school equipment</span>
</div>
</div>
</div>
<!-- Results Section -->
<div class="lg:w-1/3 bg-white rounded-xl shadow-lg">
<div class="p-4 bg-blue-600 text-white flex justify-between items-center">
<h2 class="text-xl font-semibold flex items-center gap-2">
<i data-feather="clipboard"></i> Scan Results
</h2>
<button id="clearResultsBtn" class="text-sm bg-white text-blue-600 px-2 py-1 rounded flex items-center gap-1">
<i data-feather="trash-2" class="w-4 h-4"></i> Clear
</button>
</div>
<div class="p-4 bg-blue-600 text-white">
<h2 class="text-xl font-semibold flex items-center gap-2">
<i data-feather="clipboard"></i> Scan Results
</h2>
</div>
<div class="p-4 h-96 overflow-y-auto">
<div id="loadingSection" class="flex flex-col items-center justify-center h-full">
<div class="relative w-20 h-20 mb-4">
<svg class="progress-ring w-full h-full" viewBox="0 0 100 100">
<circle class="progress-ring__circle stroke-blue-200" stroke-width="6" fill="transparent" r="40" cx="50" cy="50"></circle>
<circle class="progress-ring__circle stroke-blue-600" stroke-width="6" fill="transparent" r="40" cx="50" cy="50" stroke-dasharray="251.2" stroke-dashoffset="251.2"></circle>
</svg>
</div>
<p class="text-gray-600">Waiting for scan...</p>
</div>
<div id="resultsSection" class="hidden">
<div class="mb-4">
<div class="flex justify-between items-center mb-2">
<h3 class="font-semibold text-gray-700">Detected Items</h3>
<span id="totalCount" class="bg-blue-100 text-blue-800 px-2 py-1 rounded-full text-sm font-medium">0</span>
</div>
<div id="detectedItems" class="space-y-2"></div>
</div>
<div class="border-t pt-4">
<h3 class="font-semibold text-gray-700 mb-2">Summary</h3>
<div id="summaryChart" class="h-40"></div>
</div>
</div>
</div>
<div class="p-4 border-t flex justify-end">
<button id="exportBtn" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 hidden">
<i data-feather="download"></i> Export Report
</button>
</div>
</div>
</div>
<!-- Supported Equipment Section -->
<div class="mt-12 bg-white rounded-xl shadow-lg overflow-hidden">
<div class="p-4 bg-blue-600 text-white">
<h2 class="text-xl font-semibold flex items-center gap-2">
<i data-feather="check-circle"></i> Supported School Equipment
</h2>
</div>
<div class="p-6 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
<div class="bg-blue-50 p-4 rounded-lg text-center">
<i data-feather="monitor" class="w-8 h-8 mx-auto text-blue-600 mb-2"></i>
<span class="text-gray-700">Monitors</span>
</div>
<div class="bg-blue-50 p-4 rounded-lg text-center">
<i data-feather="printer" class="w-8 h-8 mx-auto text-blue-600 mb-2"></i>
<span class="text-gray-700">Printers</span>
</div>
<div class="bg-blue-50 p-4 rounded-lg text-center">
<i data-feather="chair" class="w-8 h-8 mx-auto text-blue-600 mb-2"></i>
<span class="text-gray-700">Chairs</span>
</div>
<div class="bg-blue-50 p-4 rounded-lg text-center">
<i data-feather="table" class="w-8 h-8 mx-auto text-blue-600 mb-2"></i>
<span class="text-gray-700">Tables</span>
</div>
<div class="bg-blue-50 p-4 rounded-lg text-center">
<i data-feather="book" class="w-8 h-8 mx-auto text-blue-600 mb-2"></i>
<span class="text-gray-700">Books</span>
</div>
<div class="bg-blue-50 p-4 rounded-lg text-center">
<i data-feather="cpu" class="w-8 h-8 mx-auto text-blue-600 mb-2"></i>
<span class="text-gray-700">Computers</span>
</div>
<div class="bg-blue-50 p-4 rounded-lg text-center">
<i data-feather="keyboard" class="w-8 h-8 mx-auto text-blue-600 mb-2"></i>
<span class="text-gray-700">Keyboards</span>
</div>
<div class="bg-blue-50 p-4 rounded-lg text-center">
<i data-feather="mouse-pointer" class="w-8 h-8 mx-auto text-blue-600 mb-2"></i>
<span class="text-gray-700">Mice</span>
</div>
<div class="bg-blue-50 p-4 rounded-lg text-center">
<i data-feather="box" class="w-8 h-8 mx-auto text-blue-600 mb-2"></i>
<span class="text-gray-700">Desktops</span>
</div>
<div class="bg-blue-50 p-4 rounded-lg text-center">
<i data-feather="clock" class="w-8 h-8 mx-auto text-blue-600 mb-2"></i>
<span class="text-gray-700">Clocks</span>
</div>
<div class="bg-blue-50 p-4 rounded-lg text-center">
<i data-feather="battery" class="w-8 h-8 mx-auto text-blue-600 mb-2"></i>
<span class="text-gray-700">Projectors</span>
</div>
<div class="bg-blue-50 p-4 rounded-lg text-center">
<i data-feather="speaker" class="w-8 h-8 mx-auto text-blue-600 mb-2"></i>
<span class="text-gray-700">Speakers</span>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
feather.replace();
// DOM Elements
const cameraFeed = document.getElementById('cameraFeed');
const detectionOverlay = document.getElementById('detectionOverlay');
const noCameraSection = document.getElementById('noCamera');
const enableCameraBtn = document.getElementById('enableCameraBtn');
const captureBtn = document.getElementById('captureBtn');
const uploadBtn = document.getElementById('uploadBtn');
const fileInput = document.getElementById('fileInput');
const loadingSection = document.getElementById('loadingSection');
const resultsSection = document.getElementById('resultsSection');
const detectedItems = document.getElementById('detectedItems');
const totalCount = document.getElementById('totalCount');
const exportBtn = document.getElementById('exportBtn');
// Model and state
let model;
let stream;
let isScanning = false;
const detectedObjects = {};
const targetClasses = [
'chair', 'dining table', 'tv', 'laptop', 'book',
'mouse', 'keyboard', 'cell phone', 'monitor', 'printer',
'backpack', 'scissors', 'bottle', 'cup', 'clock',
'vase', 'microwave', 'refrigerator', 'desk', 'computer',
'projector', 'speaker', 'calculator', 'whiteboard', 'bookshelf'
];
// Initialize COCO-SSD model
async function initModel() {
loadingSection.querySelector('p').textContent = 'Loading AI model...';
try {
model = await cocoSsd.load();
loadingSection.querySelector('p').textContent = 'Model loaded successfully!';
setTimeout(() => {
loadingSection.querySelector('p').textContent = 'Ready to scan!';
}, 2000);
} catch (error) {
loadingSection.querySelector('p').textContent = 'Failed to load model';
console.error('Model loading error:', error);
}
}
// Scanner mode toggle
function toggleScannerMode(isLive) {
if (isLive) {
document.getElementById('liveScannerBtn').classList.add('bg-blue-600', 'text-white');
document.getElementById('liveScannerBtn').classList.remove('bg-gray-100', 'text-gray-700');
document.getElementById('uploadScannerBtn').classList.add('bg-gray-100', 'text-gray-700');
document.getElementById('uploadScannerBtn').classList.remove('bg-blue-600', 'text-white');
startCamera();
} else {
document.getElementById('uploadScannerBtn').classList.add('bg-blue-600', 'text-white');
document.getElementById('uploadScannerBtn').classList.remove('bg-gray-100', 'text-gray-700');
document.getElementById('liveScannerBtn').classList.add('bg-gray-100', 'text-gray-700');
document.getElementById('liveScannerBtn').classList.remove('bg-blue-600', 'text-white');
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
cameraFeed.srcObject = null;
detectionOverlay.innerHTML = '';
noCameraSection.classList.add('hidden');
}
}
// Start camera
async function startCamera() {
try {
stream = await navigator.mediaDevices.getUserMedia({ video: true });
cameraFeed.srcObject = stream;
noCameraSection.classList.add('hidden');
// Start object detection when camera is ready
cameraFeed.onloadedmetadata = () => {
detectObjects();
};
} catch (error) {
console.error('Error accessing camera:', error);
noCameraSection.classList.remove('hidden');
}
}
// Detect objects in video feed
async function detectObjects() {
if (!model || isScanning) return;
const predictions = await model.detect(cameraFeed);
renderDetections(predictions);
requestAnimationFrame(detectObjects);
}
// Render detection boxes
function renderDetections(predictions) {
detectionOverlay.innerHTML = '';
const videoWidth = cameraFeed.videoWidth;
const videoHeight = cameraFeed.videoHeight;
const overlayWidth = detectionOverlay.offsetWidth;
const overlayHeight = detectionOverlay.offsetHeight;
predictions.forEach(prediction => {
if (!targetClasses.includes(prediction.class)) return;
// Calculate scaled coordinates
const x = (prediction.bbox[0] / videoWidth) * overlayWidth;
const y = (prediction.bbox[1] / videoHeight) * overlayHeight;
const width = (prediction.bbox[2] / videoWidth) * overlayWidth;
const height = (prediction.bbox[3] / videoHeight) * overlayHeight;
// Create detection box
const box = document.createElement('div');
box.style.position = 'absolute';
box.style.left = `${x}px`;
box.style.top = `${y}px`;
box.style.width = `${width}px`;
box.style.height = `${height}px`;
box.style.border = '2px solid rgba(59, 130, 246, 0.8)';
box.style.borderRadius = '4px';
box.style.pointerEvents = 'none';
// Create label
const label = document.createElement('div');
label.className = 'object-tag';
label.style.left = `${x}px`;
label.style.top = `${y - 24}px`;
label.textContent = `${prediction.class} (${Math.round(prediction.score * 100)}%)`;
detectionOverlay.appendChild(box);
detectionOverlay.appendChild(label);
});
}
// Capture and analyze scene
async function captureAndAnalyze() {
if (isScanning) return;
isScanning = true;
// Show loading state
loadingSection.classList.remove('hidden');
resultsSection.classList.add('hidden');
exportBtn.classList.add('hidden');
// Clear previous results
detectedItems.innerHTML = '';
Object.keys(detectedObjects).forEach(key => delete detectedObjects[key]);
// Run detection on current frame
const predictions = await model.detect(cameraFeed);
// Count objects
predictions.forEach(prediction => {
if (targetClasses.includes(prediction.class)) {
const className = formatClassName(prediction.class);
detectedObjects[className] = (detectedObjects[className] || 0) + 1;
}
});
// Display results
displayResults();
isScanning = false;
}
// Format class names for display
function formatClassName(className) {
return className
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
// Display detection results
function displayResults() {
// Update total count
const total = Object.values(detectedObjects).reduce((sum, count) => sum + count, 0);
totalCount.textContent = total;
// Populate detected items
Object.entries(detectedObjects).forEach(([item, count]) => {
const itemElement = document.createElement('div');
itemElement.className = 'flex justify-between items-center bg-gray-50 p-3 rounded-lg';
itemElement.innerHTML = `
<span class="text-gray-700">${item}</span>
<span class="bg-blue-100 text-blue-800 px-2 py-1 rounded-full text-sm font-medium">${count}</span>
`;
detectedItems.appendChild(itemElement);
});
// Show results and export button
loadingSection.classList.add('hidden');
resultsSection.classList.remove('hidden');
if (total > 0) {
exportBtn.classList.remove('hidden');
}
// Simple chart animation
animateChart();
}
// Simple chart animation
function animateChart() {
const chart = document.getElementById('summaryChart');
chart.innerHTML = '';
if (Object.keys(detectedObjects).length === 0) {
chart.innerHTML = '<p class="text-gray-500 text-center mt-12">No items detected</p>';
return;
}
const maxCount = Math.max(...Object.values(detectedObjects));
const itemHeight = 20;
const gap = 10;
const width = chart.offsetWidth;
const height = chart.offsetHeight;
const availableHeight = height - (Object.keys(detectedObjects).length * (itemHeight + gap));
let yPos = 0;
Object.entries(detectedObjects).forEach(([item, count]) => {
const barGroup = document.createElement('div');
barGroup.className = 'mb-2';
const label = document.createElement('div');
label.className = 'flex justify-between text-xs text-gray-600 mb-1';
label.innerHTML = `
<span>${item}</span>
<span>${count}</span>
`;
const barContainer = document.createElement('div');
barContainer.className = 'h-2 bg-gray-200 rounded-full overflow-hidden';
const bar = document.createElement('div');
bar.className = 'h-full bg-blue-600 rounded-full';
bar.style.width = '0';
setTimeout(() => {
bar.style.width = `${(count / maxCount) * 100}%`;
bar.style.transition = 'width 0.6s ease-out';
}, 100);
barContainer.appendChild(bar);
barGroup.appendChild(label);
barGroup.appendChild(barContainer);
chart.appendChild(barGroup);
yPos += itemHeight + gap;
});
}
// Export report
function exportReport() {
// In a real app, this would generate a PDF or CSV
alert('Report exported (this would generate a PDF/CSV in a real application)');
}
// Analyze uploaded image
async function analyzeUploadedImage(file) {
if (!model || isScanning) return;
isScanning = true;
// Show loading state
loadingSection.classList.remove('hidden');
resultsSection.classList.add('hidden');
exportBtn.classList.add('hidden');
detectedItems.innerHTML = '';
Object.keys(detectedObjects).forEach(key => delete detectedObjects[key]);
// Create image element
const img = new Image();
img.src = URL.createObjectURL(file);
img.onload = async () => {
// Display the uploaded image
cameraFeed.srcObject = null;
detectionOverlay.innerHTML = '';
const imgElement = document.createElement('img');
imgElement.src = img.src;
imgElement.style.width = '100%';
imgElement.style.height = '100%';
imgElement.style.objectFit = 'contain';
detectionOverlay.appendChild(imgElement);
// Run detection
const predictions = await model.detect(img);
// Count objects
predictions.forEach(prediction => {
if (targetClasses.includes(prediction.class)) {
const className = formatClassName(prediction.class);
detectedObjects[className] = (detectedObjects[className] || 0) + 1;
// Draw detection boxes on image
const box = document.createElement('div');
box.style.position = 'absolute';
box.style.left = `${prediction.bbox[0]}px`;
box.style.top = `${prediction.bbox[1]}px`;
box.style.width = `${prediction.bbox[2]}px`;
box.style.height = `${prediction.bbox[3]}px`;
box.style.border = '2px solid rgba(59, 130, 246, 0.8)';
box.style.pointerEvents = 'none';
const label = document.createElement('div');
label.className = 'object-tag';
label.style.left = `${prediction.bbox[0]}px`;
label.style.top = `${prediction.bbox[1] - 24}px`;
label.textContent = `${prediction.class} (${Math.round(prediction.score * 100)}%)`;
detectionOverlay.appendChild(box);
detectionOverlay.appendChild(label);
}
});
// Display results
displayResults();
isScanning = false;
};
}
// Event listeners
enableCameraBtn.addEventListener('click', startCamera);
captureBtn.addEventListener('click', captureAndAnalyze);
uploadBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
analyzeUploadedImage(e.target.files[0]);
}
});
exportBtn.addEventListener('click', exportReport);
// Clear results
function clearResults() {
detectedItems.innerHTML = '';
totalCount.textContent = '0';
Object.keys(detectedObjects).forEach(key => delete detectedObjects[key]);
loadingSection.classList.remove('hidden');
resultsSection.classList.add('hidden');
exportBtn.classList.add('hidden');
document.getElementById('summaryChart').innerHTML = '';
loadingSection.querySelector('p').textContent = 'Ready to scan!';
}
// Initialize
initModel();
// Event listeners for mode toggle
document.getElementById('liveScannerBtn').addEventListener('click', () => toggleScannerMode(true));
document.getElementById('uploadScannerBtn').addEventListener('click', () => toggleScannerMode(false));
document.getElementById('clearResultsBtn').addEventListener('click', clearResults);
// Start with camera disabled to respect privacy
toggleScannerMode(true);
});
</script>
<script>
feather.replace();
</script>
</body>
</html>