|
|
<!DOCTYPE html>
|
|
|
<html lang="en">
|
|
|
<head>
|
|
|
<meta charset="UTF-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>Expression Recognition</title>
|
|
|
<style>
|
|
|
body {
|
|
|
margin: 0;
|
|
|
padding: 0;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
height: 100vh;
|
|
|
background-color: #1a1a1a;
|
|
|
font-family: 'Segoe UI', sans-serif;
|
|
|
color: white;
|
|
|
}
|
|
|
|
|
|
.video-container {
|
|
|
position: relative;
|
|
|
width: 640px;
|
|
|
height: 480px;
|
|
|
background: #000;
|
|
|
border-radius: 12px;
|
|
|
overflow: hidden;
|
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
|
|
}
|
|
|
|
|
|
video {
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
object-fit: cover;
|
|
|
|
|
|
transform: scaleX(-1);
|
|
|
}
|
|
|
|
|
|
canvas {
|
|
|
position: absolute;
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
#loader {
|
|
|
position: absolute;
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
background: rgba(0,0,0,0.9);
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
z-index: 10;
|
|
|
}
|
|
|
|
|
|
.spinner {
|
|
|
width: 40px;
|
|
|
height: 40px;
|
|
|
border: 4px solid #f3f3f3;
|
|
|
border-top: 4px solid #9b59b6;
|
|
|
border-radius: 50%;
|
|
|
animation: spin 1s linear infinite;
|
|
|
margin-bottom: 15px;
|
|
|
}
|
|
|
|
|
|
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
|
|
|
|
.controls { margin-top: 20px; display: flex; gap: 15px; }
|
|
|
button { padding: 12px 30px; font-size: 16px; border: none; border-radius: 50px; cursor: pointer; font-weight: 600; transition: transform 0.1s; }
|
|
|
button:active { transform: scale(0.95); }
|
|
|
|
|
|
#btnCapture { background: linear-gradient(135deg, #28a745, #218838); color: white; }
|
|
|
#btnCapture:disabled { background: #555; cursor: not-allowed; }
|
|
|
#btnRetake { background: linear-gradient(135deg, #dc3545, #c82333); color: white; display: none; }
|
|
|
#status { margin-top: 15px; color: #ccc; font-size: 14px; }
|
|
|
</style>
|
|
|
<script src="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js"></script>
|
|
|
</head>
|
|
|
<body>
|
|
|
|
|
|
<div class="video-container">
|
|
|
<video id="video" autoplay muted playsinline></video>
|
|
|
<canvas id="canvas"></canvas>
|
|
|
<div id="loader">
|
|
|
<div class="spinner"></div>
|
|
|
<div id="loadingText">Loading Expression Models...</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="controls">
|
|
|
<button id="btnCapture" disabled>Wait...</button>
|
|
|
<button id="btnRetake">Retake</button>
|
|
|
</div>
|
|
|
|
|
|
<div id="status">Initializing system...</div>
|
|
|
|
|
|
<script>
|
|
|
const video = document.getElementById('video');
|
|
|
const canvas = document.getElementById('canvas');
|
|
|
const btnCapture = document.getElementById('btnCapture');
|
|
|
const btnRetake = document.getElementById('btnRetake');
|
|
|
const statusText = document.getElementById('status');
|
|
|
const loader = document.getElementById('loader');
|
|
|
|
|
|
|
|
|
const MODEL_URL = 'https://cdn.jsdelivr.net/gh/cgarciagl/face-api.js@0.22.2/weights/';
|
|
|
|
|
|
async function init() {
|
|
|
try {
|
|
|
|
|
|
await faceapi.nets.ssdMobilenetv1.loadFromUri(MODEL_URL);
|
|
|
|
|
|
|
|
|
await faceapi.nets.faceExpressionNet.loadFromUri(MODEL_URL);
|
|
|
|
|
|
startCamera();
|
|
|
} catch (error) {
|
|
|
alert("Error loading models: " + error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function startCamera() {
|
|
|
navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 } })
|
|
|
.then(stream => { video.srcObject = stream; })
|
|
|
.catch(err => { console.error(err); });
|
|
|
}
|
|
|
|
|
|
video.addEventListener('play', () => {
|
|
|
const displaySize = { width: video.videoWidth, height: video.videoHeight };
|
|
|
faceapi.matchDimensions(canvas, displaySize);
|
|
|
loader.style.display = 'none';
|
|
|
btnCapture.disabled = false;
|
|
|
btnCapture.innerText = "Capture Expression";
|
|
|
statusText.innerText = "Ready. Show me an emotion!";
|
|
|
});
|
|
|
|
|
|
btnCapture.addEventListener('click', async () => {
|
|
|
if (video.paused) return;
|
|
|
|
|
|
video.pause();
|
|
|
btnCapture.style.display = 'none';
|
|
|
btnRetake.style.display = 'inline-block';
|
|
|
statusText.innerText = "Analyzing Expressions...";
|
|
|
|
|
|
const displaySize = { width: video.videoWidth, height: video.videoHeight };
|
|
|
faceapi.matchDimensions(canvas, displaySize);
|
|
|
|
|
|
|
|
|
const detections = await faceapi.detectAllFaces(video, new faceapi.SsdMobilenetv1Options({ minConfidence: 0.5 }))
|
|
|
.withFaceExpressions();
|
|
|
|
|
|
const resizedDetections = faceapi.resizeResults(detections, displaySize);
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
|
|
|
|
|
ctx.save();
|
|
|
ctx.scale(-1, 1);
|
|
|
ctx.translate(-canvas.width, 0);
|
|
|
faceapi.draw.drawDetections(canvas, resizedDetections);
|
|
|
ctx.restore();
|
|
|
|
|
|
|
|
|
resizedDetections.forEach(result => {
|
|
|
const expressions = result.expressions;
|
|
|
|
|
|
|
|
|
const sorted = Object.entries(expressions).sort((a, b) => b[1] - a[1]);
|
|
|
const topEmotion = sorted[0];
|
|
|
|
|
|
|
|
|
const box = result.detection.box;
|
|
|
const mirroredX = canvas.width - box.x - box.width;
|
|
|
const mirroredPos = { x: mirroredX, y: box.bottomLeft.y };
|
|
|
|
|
|
|
|
|
new faceapi.draw.DrawTextField(
|
|
|
[`${topEmotion[0]} (${Math.round(topEmotion[1] * 100)}%)`],
|
|
|
mirroredPos
|
|
|
).draw(canvas);
|
|
|
|
|
|
|
|
|
|
|
|
const minConfidence = 0.1;
|
|
|
faceapi.draw.drawFaceExpressions(canvas, resizedDetections, minConfidence, mirroredPos);
|
|
|
});
|
|
|
|
|
|
if (detections.length === 0) statusText.innerText = "No face detected.";
|
|
|
else statusText.innerText = `Analysis Done. Found ${detections.length} face(s).`;
|
|
|
});
|
|
|
|
|
|
btnRetake.addEventListener('click', () => {
|
|
|
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
|
|
|
video.play();
|
|
|
btnCapture.style.display = 'inline-block';
|
|
|
btnRetake.style.display = 'none';
|
|
|
statusText.innerText = "Ready.";
|
|
|
});
|
|
|
|
|
|
init();
|
|
|
</script>
|
|
|
</body>
|
|
|
</html>
|
|
|
|