Python实现随机抽人回答问题

这是"抽人回答问题"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 = currentDetectionsrandomIndex;

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)来改变人脸检测的灵敏度。数值越高,检测越严格。

相关推荐
baopixiaoz2 小时前
AI量化策略师|Web3 量化交易研究员
大数据·人工智能·python·区块链
zlinear数据采集卡2 小时前
数据采集卡从入门到精通(9):分辨率与精度——16位卡不等于1/65536的精度
开发语言·数据库·fpga开发·开源·c#
全栈技术负责人2 小时前
解密 MCP(Model Context Protocol):大模型时代的“Type-C”总线与三驾马车架构深度解析
开发语言·ai·架构
丰锋ff2 小时前
1.7在Qt窗口中添加右键菜单
开发语言·qt
Titan20242 小时前
Linux网络学习:套接字、UDP的封装与应用
linux·服务器·开发语言·网络·c++
Uncommon.2 小时前
使用Pytorch自动计算梯度
人工智能·pytorch·python
阿标的博客3 小时前
Python实训案例(二):输出植物证书
python
鸿芯微控科技3 小时前
压电点胶阀出胶量不稳定怎么排查?驱动波形、背压、点胶重量与Python分析
开发语言·python·压电点胶阀·精密点胶·点胶重量·流体控制
一位正在转型AI全栈的前端工程师3 小时前
从 0 到 1 搭建生产级 RAG 系统:切片、召回与重排序调优实录
python·前端工程化
糖炒栗子03263 小时前
learn-claude-code 简要记录
笔记·python