网站首页 > 技术文章 正文
宇哥闲着没事做了一个人脸识别网页,练练手。(懒人看过来:完整代码贴在最后,想要的拿走直接用不必客气,反正都是开源的)
这个人脸识别模型的语言是JavaScript,从网上找的开源模型,基于TensorFlow的Face-Api,模型下载链接:
https://cdn.jsdelivr.net/npm/face-api.js@
0.22.2/dist/face-api.min.js 开源的随便用、放心没事。
模型库也是GitHub上乱搜的一个7年前的版本,除了年龄不太准,表情、性别还都挺准的。
1.主要实现的功能
网页版人脸识别,打开网页自动弹出图像,用笔记本电脑或者台式机安装的摄像头对图像中的人脸进行识别。可以完成以下三个功能:
- 能够自动识别人脸,识别后用方框圈上;
- 能自动跟踪人脸移动;
- 能识别性别、年龄、情绪三个人脸要素。
网页做出来的效果如下:
使用体验
年龄识别压根不准,主要是年龄范围太大了 这个模型可以识别7种情绪(fearful我好像还没识别出来。。别的都识别出来了)
英文名称 中文含义 描述
neutral 中性 / 无表情 面部放松,无明显情绪反应
happy 高兴 / 快乐 微笑、眼角上扬,表示愉快
sad 伤心 / 悲伤 嘴角下垂、眼神低落,表示悲痛
angry 生气 / 愤怒 紧皱眉头、咬牙、目光愤怒 f
earful 害怕 / 惊恐 睁大眼睛、紧张表情,表示惊惧
disgusted 厌恶 / 恶心 皱鼻、撇嘴,面部反感表情
surprised 惊讶 / 吃惊 张大嘴巴、睁大眼睛,表示意外或震惊
模拟完整输出示例(年龄 + 性别 + 表情)
{
"age": 24.9,
"gender": "female",
"genderProbability": 0.96,
"expressions": {
"neutral": 0.05,
"happy": 0.88,
"sad": 0.01,
"angry": 0.00,
"fearful": 0.00,
"disgusted": 0.00,
"surprised": 0.06
}
}
这表示:
- 预测为 女性,置信度96%
- 年龄约为 25 岁
- 情绪状态为 高兴(88%)
2.设计思路
本案例需要一个主页index、一个装学习模型的文件夹model(跟index放在同一个文件夹内) Face-API人脸识别模型可以下载下来放到文件夹里,如下图所示文件名为face-api.min.js。
也可以直接调用网页版(前提是你电脑能连的上网),本案例是把模型下载下来了,断网也能随意调用。
(1)html设计
注意模型调用在这部分。
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>实时人脸属性识别 Demo</title>
<style>
body {
margin: 0;
font-family: system-ui, sans-serif;
background: #111;
color: #eee;
display: flex;
flex-direction: column;
align-items: center;
}
#videoWrapper {
position: relative;
width: 100%; /* 宽度自适应 */
max-width: 720px; /* 最大宽度 */
height: auto; /* 高度根据宽度比例自适应 */
cursor: pointer; /* 给整个容器添加点击手势 */
}
video {
width: 100%;
height: auto;
display: block;
border-radius: 8px;
}
canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 8px;
}
#info {
margin-top: 1rem;
font-size: 1.1rem;
line-height: 1.5;
}
.tag {
display: inline-block;
padding: 2px 6px;
background: #0af;
border-radius: 4px;
margin-right: 4px;
}
</style>
<!-- face-api.js CDN -->
<script src="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js"></script>
</head>
下面是页面元素,非常的简单,只有一个标题再加上一个视频(注意外面套了一层容器)
<h2>Web开发小练习-实时人脸属性识别</h2>
<div id="videoWrapper">
<video id="video" autoplay muted playsinline></video>
<canvas id="overlay"></canvas>
</div>
<div id="info"></div>
(2)script设计
核心是一个主循环函数runDetection。
<script>
const video = document.getElementById('video');
const canvas = document.getElementById('overlay');
const infoBox = document.getElementById('info');
const videoWrapper = document.getElementById('videoWrapper'); // 获取视频容器
// 初始化:加载模型 → 开摄像头 → 等视频就绪
async function init() {
const MODEL_URL = './models';
await Promise.all([
faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL),
faceapi.nets.faceExpressionNet.loadFromUri(MODEL_URL),
faceapi.nets.ageGenderNet.loadFromUri(MODEL_URL)
]);
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = stream;
} catch (err) {
alert('无法访问摄像头:' + err.message);
return;
}
// 确保拿到视频原始分辨率后再设置 canvas 尺寸
video.addEventListener('loadedmetadata', () => {
canvas.width = video.videoWidth; // 使用视频的实际宽度
canvas.height = video.videoHeight; // 使用视频的实际高度
runDetection();
});
}
// 主循环:每 100ms 推断一次
function runDetection() {
const ctx = canvas.getContext('2d');
const options = new faceapi.TinyFaceDetectorOptions({ inputSize: 224, scoreThreshold: 0.5 });
setInterval(async () => {
// 侦测人脸和属性
const detections = await faceapi
.detectAllFaces(video, options)
.withAgeAndGender()
.withFaceExpressions();
const dims = { width: canvas.width, height: canvas.height };
const resized = faceapi.resizeResults(detections, dims);
// 清空画布并绘制
ctx.clearRect(0, 0, canvas.width, canvas.height);
resized.forEach(det => {
const { age, gender, genderProbability, expressions, detection } = det;
const { x, y, width, height } = detection.box;
// 计算最大置信度表情
const { expression: emotion, probability: emoProb } =
Object.entries(expressions).reduce((best, cur) =>
cur[1] > best.probability ? { expression: cur[0], probability: cur[1] } : best,
{ expression: 'neutral', probability: 0 });
// (1) 画人脸框
ctx.strokeStyle = '#0af';
ctx.lineWidth = 2;
ctx.strokeRect(x, y, width, height);
// (2) 画文字标签
const txt = `${gender} (${(genderProbability * 100).toFixed(0)}%) | ` +
`${Math.round(age)} 岁 | ${emotion} ${(emoProb * 100).toFixed(0)}%`;
ctx.font = '16px sans-serif';
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(x, y - 22, ctx.measureText(txt).width + 8, 20);
ctx.fillStyle = '#fff';
ctx.fillText(txt, x + 4, y - 6);
});
// 页面底部信息(首张脸示例)
if (resized[0]) {
const { age, gender, expressions } = resized[0];
const topEmotion = Object.keys(expressions).reduce((a, b) => expressions[a] > expressions[b] ? a : b);
infoBox.innerHTML = `
<span class="tag">性别</span>${gender}
<span class="tag">年龄</span>${Math.round(age)}
<span class="tag">情绪</span>${topEmotion}
`;
} else {
infoBox.textContent = '暂无人脸';
}
}, 100); // 10 FPS
}
// 添加点击事件使视频容器进入全屏
videoWrapper.addEventListener('click', () => {
if (videoWrapper.requestFullscreen) {
videoWrapper.requestFullscreen();
} else if (videoWrapper.mozRequestFullScreen) { // Firefox
videoWrapper.mozRequestFullScreen();
} else if (videoWrapper.webkitRequestFullscreen) { // Chrome, Safari and Opera
videoWrapper.webkitRequestFullscreen();
} else if (videoWrapper.msRequestFullscreen) { // IE/Edge
videoWrapper.msRequestFullscreen();
}
});
// 监听全屏状态改变,确保界面适应
document.addEventListener('fullscreenchange', () => {
if (document.fullscreenElement) {
// 在全屏时调整样式
video.style.width = '100%';
video.style.height = '100%';
} else {
// 退出全屏时恢复样式
video.style.width = '100%';
video.style.height = 'auto';
}
});
init();
</script>
(3)完整代码
实现原理优点复杂,就不说了,有问题评论区交流。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>实时人脸属性识别 Demo</title>
<style>
body {
margin: 0;
font-family: system-ui, sans-serif;
background: #111;
color: #eee;
display: flex;
flex-direction: column;
align-items: center;
}
#videoWrapper {
position: relative;
width: 100%; /* 宽度自适应 */
max-width: 720px; /* 最大宽度 */
height: auto; /* 高度根据宽度比例自适应 */
cursor: pointer; /* 给整个容器添加点击手势 */
}
video {
width: 100%;
height: auto;
display: block;
border-radius: 8px;
}
canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 8px;
}
#info {
margin-top: 1rem;
font-size: 1.1rem;
line-height: 1.5;
}
.tag {
display: inline-block;
padding: 2px 6px;
background: #0af;
border-radius: 4px;
margin-right: 4px;
}
</style>
<!-- face-api.js CDN -->
<script src="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js"></script>
</head>
<body>
<h2>Web开发小练习-实时人脸属性识别</h2>
<div id="videoWrapper">
<video id="video" autoplay muted playsinline></video>
<canvas id="overlay"></canvas>
</div>
<div id="info"></div>
<script>
const video = document.getElementById('video');
const canvas = document.getElementById('overlay');
const infoBox = document.getElementById('info');
const videoWrapper = document.getElementById('videoWrapper'); // 获取视频容器
// 初始化:加载模型 → 开摄像头 → 等视频就绪
async function init() {
const MODEL_URL = './models';
await Promise.all([
faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL),
faceapi.nets.faceExpressionNet.loadFromUri(MODEL_URL),
faceapi.nets.ageGenderNet.loadFromUri(MODEL_URL)
]);
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = stream;
} catch (err) {
alert('无法访问摄像头:' + err.message);
return;
}
// 确保拿到视频原始分辨率后再设置 canvas 尺寸
video.addEventListener('loadedmetadata', () => {
canvas.width = video.videoWidth; // 使用视频的实际宽度
canvas.height = video.videoHeight; // 使用视频的实际高度
runDetection();
});
}
// 主循环:每 100ms 推断一次
function runDetection() {
const ctx = canvas.getContext('2d');
const options = new faceapi.TinyFaceDetectorOptions({ inputSize: 224, scoreThreshold: 0.5 });
setInterval(async () => {
// 侦测人脸和属性
const detections = await faceapi
.detectAllFaces(video, options)
.withAgeAndGender()
.withFaceExpressions();
const dims = { width: canvas.width, height: canvas.height };
const resized = faceapi.resizeResults(detections, dims);
// 清空画布并绘制
ctx.clearRect(0, 0, canvas.width, canvas.height);
resized.forEach(det => {
const { age, gender, genderProbability, expressions, detection } = det;
const { x, y, width, height } = detection.box;
// 计算最大置信度表情
const { expression: emotion, probability: emoProb } =
Object.entries(expressions).reduce((best, cur) =>
cur[1] > best.probability ? { expression: cur[0], probability: cur[1] } : best,
{ expression: 'neutral', probability: 0 });
// (1) 画人脸框
ctx.strokeStyle = '#0af';
ctx.lineWidth = 2;
ctx.strokeRect(x, y, width, height);
// (2) 画文字标签
const txt = `${gender} (${(genderProbability * 100).toFixed(0)}%) | ` +
`${Math.round(age)} 岁 | ${emotion} ${(emoProb * 100).toFixed(0)}%`;
ctx.font = '16px sans-serif';
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(x, y - 22, ctx.measureText(txt).width + 8, 20);
ctx.fillStyle = '#fff';
ctx.fillText(txt, x + 4, y - 6);
});
// 页面底部信息(首张脸示例)
if (resized[0]) {
const { age, gender, expressions } = resized[0];
const topEmotion = Object.keys(expressions).reduce((a, b) => expressions[a] > expressions[b] ? a : b);
infoBox.innerHTML = `
<span class="tag">性别</span>${gender}
<span class="tag">年龄</span>${Math.round(age)}
<span class="tag">情绪</span>${topEmotion}
`;
} else {
infoBox.textContent = '暂无人脸';
}
}, 100); // 10 FPS
}
// 添加点击事件使视频容器进入全屏
videoWrapper.addEventListener('click', () => {
if (videoWrapper.requestFullscreen) {
videoWrapper.requestFullscreen();
} else if (videoWrapper.mozRequestFullScreen) { // Firefox
videoWrapper.mozRequestFullScreen();
} else if (videoWrapper.webkitRequestFullscreen) { // Chrome, Safari and Opera
videoWrapper.webkitRequestFullscreen();
} else if (videoWrapper.msRequestFullscreen) { // IE/Edge
videoWrapper.msRequestFullscreen();
}
});
// 监听全屏状态改变,确保界面适应
document.addEventListener('fullscreenchange', () => {
if (document.fullscreenElement) {
// 在全屏时调整样式
video.style.width = '100%';
video.style.height = '100%';
} else {
// 退出全屏时恢复样式
video.style.width = '100%';
video.style.height = 'auto';
}
});
init();
</script>
</body>
</html>
- 上一篇: CSS支持 if / else 了
- 下一篇: 手把手带你完成OpenHarmony藏头诗App
猜你喜欢
- 2025-08-02 React Native 常见问题集合
- 2025-08-02 React-Native 样式指南
- 2025-08-02 SpringBoot集成DeepSeek
- 2025-08-02 课堂点名总尴尬?试试 DeepSeek,或能实现点名自由!(附教程)
- 2025-08-02 手把手带你完成OpenHarmony藏头诗App
- 2025-08-02 CSS支持 if / else 了
- 2025-08-02 CSS粘性页脚布局:从Flexbox到Grid的现代实现指南
- 2025-08-02 CSS 2025新特性解析:容器查询与嵌套选择器的高级应用案例
- 2025-08-02 如何设置Flexbox项目之间的距离
- 2025-08-02 教师如何制作随机点名系统,活跃课堂气氛
- 08-02C|在一个结构体嵌套一个共用体实现一体多用
- 08-02C++中,常用的强制类型转换函数
- 08-02如何使用C语言编程实现一个推箱子游戏?技术核心和算法实现
- 08-02C++20 新特性(24):模板访问权限和typename的放宽
- 08-02C++零基础到工程实践
- 08-02[深度学习] Python人脸识别库face_recognition使用教程
- 08-02AI算法之怎么利用Python实现支持向量机SVM算法
- 08-02【机器学习】SVM支持向量机
- 1520℃桌面软件开发新体验!用 Blazor Hybrid 打造简洁高效的视频处理工具
- 623℃Dify工具使用全场景:dify-sandbox沙盒的原理(源码篇·第2期)
- 526℃MySQL service启动脚本浅析(r12笔记第59天)
- 492℃启用MySQL查询缓存(mysql8.0查询缓存)
- 491℃服务器异常重启,导致mysql启动失败,问题解决过程记录
- 479℃「赵强老师」MySQL的闪回(赵强iso是哪个大学毕业的)
- 460℃mysql服务怎么启动和关闭?(mysql服务怎么启动和关闭)
- 458℃MySQL server PID file could not be found!失败
- 最近发表
- 标签列表
-
- cmd/c (90)
- c++中::是什么意思 (84)
- 标签用于 (71)
- 主键只能有一个吗 (77)
- c#console.writeline不显示 (95)
- pythoncase语句 (88)
- es6includes (74)
- sqlset (76)
- windowsscripthost (69)
- apt-getinstall-y (100)
- node_modules怎么生成 (87)
- chromepost (71)
- flexdirection (73)
- c++int转char (80)
- static函数和普通函数 (76)
- el-date-picker开始日期早于结束日期 (70)
- c语言min函数头文件 (68)
- asynccallback (71)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)