这是“抽人回答问题”HTML代码。它利用摄像头实时检测人脸,并随机抽取一张人脸截图显示在屏幕上。

 

```html

<!DOCTYPE html>

<html lang="zh-CN">

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">

    <title>抽人回答问题 · 人脸捕获</title>

    <!-- 使用 Tailwind 精简样式,并自定义一些动效 -->

    <script src="https://cdn.tailwindcss.com"></script>

    <!-- 人脸检测库 face-api.js 核心与模型 -->

    <script defer src="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js"></script>

    <style>

        /* 摄像头容器与画布叠加层 */

        #video-wrapper {

            position: relative;

            width: 100%;

            max-width: 640px;

            margin: 0 auto;

            aspect-ratio: 4 / 3;

            background: #1e1e2f;

            border-radius: 24px;

            overflow: hidden;

            box-shadow: 0 20px 40px -12px rgba(0, 0, 0, 0.6);

        }

        #video-element {

            position: absolute;

            top: 0;

            left: 0;

            width: 100%;

            height: 100%;

            object-fit: cover;

            transform: scaleX(-1); /* 镜像,更自然 */

        }

        #overlay-canvas {

            position: absolute;

            top: 0;

            left: 0;

            width: 100%;

            height: 100%;

            pointer-events: none; /* 让按钮可以穿透点击 */

            transform: scaleX(-1); /* 与视频保持镜像一致 */

        }

        /* 截图展示区 */

        #snapshot-container {

            position: relative;

            width: 100%;

            max-width: 320px;

            margin: 0 auto;

            aspect-ratio: 1 / 1;

            background: #2d2d44;

            border-radius: 32px;

            overflow: hidden;

            box-shadow: 0 10px 30px -8px rgba(0, 0, 0, 0.5);

            border: 2px solid rgba(255, 255, 255, 0.08);

            transition: all 0.2s ease;

        }

        #snapshot-container img {

            width: 100%;

            height: 100%;

            object-fit: cover;

            display: block;

        }

        .glow-text {

            text-shadow: 0 0 12px rgba(120, 180, 255, 0.5);

        }

        /* 按钮动效 */

        .btn-primary {

            background: linear-gradient(135deg, #6366f1, #8b5cf6);

            transition: transform 0.15s, box-shadow 0.2s;

            box-shadow: 0 8px 20px -6px #6366f180;

        }

        .btn-primary:hover:not(:disabled) {

            transform: scale(1.02);

            box-shadow: 0 12px 28px -6px #6366f1b0;

        }

        .btn-primary:active:not(:disabled) {

            transform: scale(0.96);

        }

        .btn-primary:disabled {

            opacity: 0.5;

            cursor: not-allowed;

            filter: grayscale(0.4);

        }

        .btn-secondary {

            background: rgba(255, 255, 255, 0.08);

            backdrop-filter: blur(6px);

            border: 1px solid rgba(255, 255, 255, 0.15);

            transition: 0.15s;

        }

        .btn-secondary:hover:not(:disabled) {

            background: rgba(255, 255, 255, 0.16);

        }

        .badge {

            background: rgba(0, 0, 0, 0.5);

            backdrop-filter: blur(4px);

            padding: 4px 14px;

            border-radius: 40px;

            font-size: 0.8rem;

            border: 1px solid rgba(255, 255, 255, 0.1);

        }

        /* 加载模型占位 */

        .loader {

            border: 3px solid rgba(255, 255, 255, 0.1);

            border-top-color: #818cf8;

            border-radius: 50%;

            width: 36px;

            height: 36px;

            animation: spin 0.9s linear infinite;

        }

        @keyframes spin {

            to { transform: rotate(360deg); }

        }

    </style>

</head>

<body class="min-h-screen bg-gradient-to-br from-[#0f0f1a] via-[#1a1a2e] to-[#16213e] text-white font-sans antialiased flex items-center justify-center p-4">

 

    <main class="w-full max-w-3xl mx-auto flex flex-col items-center gap-6">

        <!-- 标题 & 状态 -->

        <div class="flex items-center justify-between w-full max-w-md px-2">

            <h1 class="text-3xl font-bold tracking-tight flex items-center gap-2">

                <span class="bg-gradient-to-r from-indigo-300 to-purple-300 bg-clip-text text-transparent">📸 抽人</span>

                <span class="text-sm font-normal text-white/40">回答问题</span>

            </h1>

            <span id="status-badge" class="badge text-white/70 text-xs flex items-center gap-1">

                <span class="w-2 h-2 rounded-full bg-green-400 inline-block animate-pulse" id="cam-indicator"></span>

                等待启动

            </span>

        </div>

 

        <!-- 摄像头 + 人脸框叠加 -->

        <div id="video-wrapper">

            <video id="video-element" autoplay muted playsinline></video>

            <canvas id="overlay-canvas"></canvas>

            <!-- 轻提示:检测到人脸数量 -->

            <div class="absolute bottom-3 left-3 bg-black/50 backdrop-blur-sm px-4 py-1.5 rounded-full text-xs text-white/80 border border-white/10 flex items-center gap-2">

                <span>👤 检测到</span>

                <span id="face-count" class="font-bold text-indigo-300 min-w-[20px] text-center">0</span>

                <span>张脸</span>

            </div>

        </div>

 

        <!-- 操作栏 -->

        <div class="flex flex-wrap items-center justify-center gap-3 w-full max-w-md mt-1">

            <button id="capture-btn" class="btn-primary px-8 py-3 rounded-full font-medium text-white flex items-center gap-2 disabled:opacity-50" disabled>

                <span>🎲</span> 抽人

            </button>

            <button id="reset-btn" class="btn-secondary px-6 py-3 rounded-full font-medium text-white/80 flex items-center gap-2 disabled:opacity-40" disabled>

                <span>↺</span> 返回重抽

            </button>

        </div>

 

        <!-- 截图展示 (抽中的人) -->

        <div id="snapshot-container" class="relative">

            <img id="snapshot-img" src="" alt="抽中人脸截图" class="w-full h-full object-cover opacity-0 transition-opacity duration-300">

            <div id="placeholder-snapshot" class="absolute inset-0 flex flex-col items-center justify-center text-white/30 text-sm">

                <span class="text-4xl mb-2">👤</span>

                <span>等待抽取</span>

            </div>

            <!-- 若抽中,显示“中奖”角标 -->

            <div id="winner-badge" class="absolute top-2 right-2 bg-indigo-500/80 backdrop-blur-sm text-white text-xs font-bold px-3 py-1 rounded-full border border-white/20 hidden">

                ⭐ 被抽中

            </div>

        </div>

 

        <!-- 调试/状态信息(可选) -->

        <p class="text-xs text-white/30 max-w-sm text-center mt-2 leading-relaxed">

            <span id="info-text">模型加载中,请授权摄像头...</span>

        </p>

    </main>

 

    <script>

        (function() {

            "use strict";

 

            // ---------- DOM 元素 ----------

            const video = document.getElementById('video-element');

            const canvas = document.getElementById('overlay-canvas');

            const ctx = canvas.getContext('2d');

            const snapshotImg = document.getElementById('snapshot-img');

            const placeholder = document.getElementById('placeholder-snapshot');

            const winnerBadge = document.getElementById('winner-badge');

            const faceCountSpan = document.getElementById('face-count');

            const statusBadge = document.getElementById('status-badge');

            const camIndicator = document.getElementById('cam-indicator');

            const infoText = document.getElementById('info-text');

 

            const captureBtn = document.getElementById('capture-btn');

            const resetBtn = document.getElementById('reset-btn');

 

            // ---------- 状态变量 ----------

            let isModelLoaded = false;

            let isCameraReady = false;

            let isCapturing = false; // 正在抽取中 (锁定按钮)

            let hasSnapshot = false; // 是否已有截图 (用于显示reset)

            let currentDetections = []; // 存储当前帧人脸检测结果

            let animationFrameId = null;

            let lastFaceCount = 0;

 

            // ---------- 配置 ----------

            const MODEL_URL = 'https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/models/'; // 轻量级模型

            const DETECT_OPTIONS = new faceapi.SsdMobilenetv1Options({

                minConfidence: 0.5, // 置信度阈值

                maxResults: 10

            });

 

            // ---------- 辅助函数 ----------

            function updateUIForReady() {

                if (isModelLoaded && isCameraReady) {

                    captureBtn.disabled = false;

                    resetBtn.disabled = false;

                    statusBadge.innerHTML = `<span class="w-2 h-2 rounded-full bg-green-400 inline-block"></span> 就绪`;

                    infoText.textContent = '✅ 已就绪,点击“抽人”随机截取一张人脸';

                } else {

                    captureBtn.disabled = true;

                    resetBtn.disabled = true;

                }

            }

 

            // 更新检测到的人脸数量

            function updateFaceCount(detections) {

                const count = detections ? detections.length : 0;

                faceCountSpan.textContent = count;

                // 如果没有人脸,给个提示颜色

                if (count === 0 && isCameraReady) {

                    faceCountSpan.className = 'font-bold text-rose-300 min-w-[20px] text-center';

                } else {

                    faceCountSpan.className = 'font-bold text-indigo-300 min-w-[20px] text-center';

                }

                // 当人脸数量变化时,轻微更新状态 (但不干扰capture)

            }

 

            // 绘制人脸框 (镜像处理,与视频一致)

            function drawDetections(detections) {

                const videoWidth = video.videoWidth;

                const videoHeight = video.videoHeight;

                if (!videoWidth || !videoHeight) return;

 

                // canvas 尺寸与视频比例同步

                canvas.width = videoWidth;

                canvas.height = videoHeight;

                ctx.clearRect(0, 0, canvas.width, canvas.height);

 

                if (!detections || detections.length === 0) return;

 

                // face-api 的检测框坐标是基于原始图像 (未镜像)

                // 但 canvas 我们设置了 scaleX(-1) 所以绘制时需要镜像翻转

                // 因为 canvas 整体镜像,所以我们直接绘制矩形即可,坐标会自动镜像

                // 不过绘制文字时需要额外注意方向,但这里仅绘制框,无需文字

                ctx.strokeStyle = '#60a5fa';

                ctx.lineWidth = 2.5;

                ctx.shadowColor = '#60a5fa60';

                ctx.shadowBlur = 8;

 

                detections.forEach(det => {

                    const box = det.detection.box;

                    const x = box.x;

                    const y = box.y;

                    const w = box.width;

                    const h = box.height;

                    // 由于 canvas 镜像,x 会自动翻转,直接绘制即可

                    ctx.strokeRect(x, y, w, h);

                    // 加一个小圆角装饰

                    ctx.fillStyle = '#60a5fa30';

                    ctx.fillRect(x, y-1, w, 2);

                });

                // 重置阴影

                ctx.shadowBlur = 0;

            }

 

            // ---------- 核心: 随机抽取一张人脸截图 ----------

            function captureRandomFace() {

                if (!isModelLoaded || !isCameraReady) {

                    alert('请等待模型和摄像头就绪');

                    return;

                }

                if (isCapturing) return;

                if (!currentDetections || currentDetections.length === 0) {

                    alert('未检测到人脸,请面对摄像头 😅');

                    return;

                }

 

                isCapturing = true;

                captureBtn.disabled = true;

                resetBtn.disabled = true;

                // 隐藏之前的截图状态

                snapshotImg.classList.add('opacity-0');

                placeholder.classList.remove('hidden');

                winnerBadge.classList.add('hidden');

 

                // 随机选择一个检测结果

                const randomIndex = Math.floor(Math.random() * currentDetections.length);

                const selected = currentDetections[randomIndex];

                const box = selected.detection.box;

 

                // 从视频中截取人脸区域 (注意视频是镜像的,但坐标基于原始图像,canvas 也是镜像,一致)

                const videoWidth = video.videoWidth;

                const videoHeight = video.videoHeight;

                if (!videoWidth || !videoHeight) {

                    isCapturing = false;

                    captureBtn.disabled = false;

                    resetBtn.disabled = false;

                    return;

                }

 

                // 扩充边距 10%,让截图更自然

                const margin = 0.12;

                let x = Math.max(0, box.x - box.width * margin);

                let y = Math.max(0, box.y - box.height * margin);

                let w = Math.min(videoWidth - x, box.width * (1 + margin * 2));

                let h = Math.min(videoHeight - y, box.height * (1 + margin * 2));

                // 确保至少 30x30

                if (w < 30) w = 30;

                if (h < 30) h = 30;

 

                // 利用 canvas 截取 (由于视频是镜像的,直接截取)

                const tempCanvas = document.createElement('canvas');

                tempCanvas.width = w;

                tempCanvas.height = h;

                const tempCtx = tempCanvas.getContext('2d');

                // 绘制视频帧的对应区域 (注意视频本身是镜像,直接draw即可,因为坐标已对应)

                tempCtx.drawImage(video, x, y, w, h, 0, 0, w, h);

 

                // 将截图显示到 snapshot-img

                const dataUrl = tempCanvas.toDataURL('image/png');

                snapshotImg.src = dataUrl;

                // 显示截图,隐藏占位

                snapshotImg.onload = () => {

                    snapshotImg.classList.remove('opacity-0');

                    placeholder.classList.add('hidden');

                    winnerBadge.classList.remove('hidden');

                    hasSnapshot = true;

                    isCapturing = false;

                    captureBtn.disabled = false;

                    resetBtn.disabled = false;

                    // 更新信息

                    infoText.textContent = `🎯 已抽中 #${randomIndex+1} 号脸,点击“返回重抽”可重新抽取`;

                };

                snapshotImg.onerror = () => {

                    // fallback

                    placeholder.classList.remove('hidden');

                    winnerBadge.classList.add('hidden');

                    isCapturing = false;

                    captureBtn.disabled = false;

                    resetBtn.disabled = false;

                    infoText.textContent = '⚠️ 截图失败,请重试';

                };

            }

 

            // ---------- 重置/重新开始 ----------

            function resetSnapshot() {

                if (isCapturing) return;

                // 清空截图,回到初始状态

                snapshotImg.src = '';

                snapshotImg.classList.add('opacity-0');

                placeholder.classList.remove('hidden');

                winnerBadge.classList.add('hidden');

                hasSnapshot = false;

                infoText.textContent = '🔄 已重置,点击“抽人”再次抽取';

                // 按钮状态恢复 (但若模型未就绪则禁用)

                updateUIForReady();

                // 重新开始新一轮 (清除任何临时状态)

                // 并且如果当前有人脸框,继续显示

            }

 

            // ---------- 视频帧循环 (检测人脸 + 绘制) ----------

            async function detectLoop() {

                if (!isCameraReady || !isModelLoaded) {

                    animationFrameId = requestAnimationFrame(detectLoop);

                    return;

                }

 

                try {

                    // 检测人脸

                    const detections = await faceapi.detectAllFaces(video, DETECT_OPTIONS);

                    currentDetections = detections || [];

                    // 更新计数

                    updateFaceCount(currentDetections);

                    // 绘制框

                    drawDetections(currentDetections);

                } catch (err) {

                    // 偶尔出错忽略

                    console.warn('检测异常:', err);

                }

 

                animationFrameId = requestAnimationFrame(detectLoop);

            }

 

            // ---------- 初始化摄像头 ----------

            async function initCamera() {

                try {

                    const stream = await navigator.mediaDevices.getUserMedia({

                        video: { facingMode: 'user', width: { ideal: 640 }, height: { ideal: 480 } },

                        audio: false,

                    });

                    video.srcObject = stream;

                    await video.play();

                    isCameraReady = true;

                    camIndicator.className = 'w-2 h-2 rounded-full bg-green-400 inline-block animate-pulse';

                    statusBadge.innerHTML = `<span class="w-2 h-2 rounded-full bg-green-400 inline-block"></span> 摄像头已开`;

                    infoText.textContent = '📷 摄像头已启动,加载模型中...';

                    // 设置canvas尺寸 (在第一次视频帧加载后设置)

                    video.addEventListener('loadedmetadata', () => {

                        canvas.width = video.videoWidth;

                        canvas.height = video.videoHeight;

                    });

                    // 更新UI

                    updateUIForReady();

                    // 开始检测循环

                    if (animationFrameId) cancelAnimationFrame(animationFrameId);

                    detectLoop();

                } catch (err) {

                    console.error('摄像头启动失败:', err);

                    camIndicator.className = 'w-2 h-2 rounded-full bg-rose-400 inline-block';

                    statusBadge.innerHTML = `<span class="w-2 h-2 rounded-full bg-rose-400 inline-block"></span> 摄像头不可用`;

                    infoText.textContent = '❌ 无法访问摄像头,请检查权限或连接';

                    alert('无法访问摄像头,请允许浏览器访问摄像头后刷新页面。');

                }

            }

 

            // ---------- 加载 face-api 模型 ----------

            async function loadModels() {

                try {

                    infoText.textContent = '⏳ 加载人脸检测模型...';

                    await faceapi.nets.ssdMobilenetv1.loadFromUri(MODEL_URL);

                    // 也可以加载 tinyFaceDetector 但 ssd 更准

                    isModelLoaded = true;

                    infoText.textContent = '🧠 模型加载完成,等待摄像头...';

                    updateUIForReady();

                    // 如果摄像头已就绪,更新状态

                    if (isCameraReady) {

                        statusBadge.innerHTML = `<span class="w-2 h-2 rounded-full bg-green-400 inline-block"></span> 完全就绪`;

                        infoText.textContent = '✅ 一切就绪,点击“抽人”开始!';

                    }

                } catch (err) {

                    console.error('模型加载失败:', err);

                    infoText.textContent = '❌ 模型加载失败,请刷新重试';

                    alert('人脸检测模型加载失败,请检查网络后刷新。');

                }

            }

 

            // ---------- 事件绑定 ----------

            captureBtn.addEventListener('click', captureRandomFace);

            resetBtn.addEventListener('click', resetSnapshot);

 

            // ---------- 启动 ----------

            async function init() {

                // 先加载模型,再开摄像头 (并行也行,但顺序保证ui)

                await loadModels();

                await initCamera();

                // 若模型加载失败,摄像头也可能已经开启,但按钮禁用

                updateUIForReady();

            }

 

            // 页面关闭前释放资源

            window.addEventListener('beforeunload', () => {

                if (video.srcObject) {

                    const tracks = video.srcObject.getTracks();

                    tracks.forEach(track => track.stop());

                }

                if (animationFrameId) cancelAnimationFrame(animationFrameId);

            });

 

            // 启动所有

            init();

 

        })();

    </script>

</body>

</html>

```

 

备注

 

 

核心功能与操作流程

 

您可以通过“抽人”按钮随机选择当前画面中的一张人脸,并查看其截图。整个流程清晰且反馈即时。

 

· 人脸检测与显示:页面会实时显示摄像头画面,并用蓝色框标记出检测到的所有人脸。左上角会同步显示当前检测到的人脸数量。

· 随机抽取与截图:点击 “抽人” 按钮,系统会从当前检测到的人脸中随机选取一个,并将其面部区域放大截图,显示在下方区域,同时会有一个“被抽中”的角标提示。

· 重置与重抽:点击 “返回重抽” 按钮,可以清除当前截图,恢复初始状态,以便您进行新一轮的抽取。

 

 

可以根据需要调整 DETECT_OPTIONS 中的 minConfidence(置信度阈值,0-1)来改变人脸检测的灵敏度。数值越高,检测越严格。

 

更多推荐