优秀的编程知识分享平台

网站首页 > 技术文章 正文

Web设计练习:制作人脸识别网页(基于TensorFlow的开源模型)

nanyue 2025-08-02 20:42:46 技术文章 1 ℃

宇哥闲着没事做了一个人脸识别网页,练练手。(懒人看过来:完整代码贴在最后,想要的拿走直接用不必客气,反正都是开源的)

这个人脸识别模型的语言是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>

Tags:

最近发表
标签列表