发布于 2026-01-06 2 阅读
0

趣味帽子日!👒🎩 如何使用网络摄像头和 JavaScript 进行人脸检测 📸🧠

趣味帽子日!👒🎩 如何使用网络摄像头和 JavaScript 进行人脸检测 📸🧠

(封面图片由Dall-E mini制作,标题为“一个戴着滑稽高顶礼帽的人工智能”——你知道,因为我们今天要讲机器学习方面的内容。)

距离我上次发帖已经有一段时间了。我正在筹备一个相当大的项目;很快就会有消息公布!

但今天,我们要好好看看你们。没错,就是你们。确切地说,是你们美丽的脸庞。我们会让你们戴上帽子。为此,我们会用到face-api.jsMedia Stream API 。

不过别担心。所有数据都不会在云端或您电脑以外的任何地方进行处理,您的图片将保留下来,所有操作都在您的浏览器中完成。

我们开始吧!

样板

首先,我们需要一些 HTML 代码:一个 ` <video><video>` 元素、一个帽子图标、两个用于开始和停止视频的按钮,以及两个<select>用于选择帽子图标和设备的按钮。要知道,你可能有两个网络摄像头。

<div class="container">
  <div id="hat">
    🎩
  </div>
  <!-- autoplay is important here, otherwise it doesn't immediately show the camera input. -->
  <video id="video" width="1280" height="720" autoplay></video>
</div>

<div>
  <label for="deviceSelector">
    Select device
  </label>
  <select id="deviceSelector"></select>
</div>

<div>
  <label for="hatSelector">
    Select hat
  </label>
  <select id="hatSelector"></select>
</div>

<button id="start">
  Start video
</button>

<button id="stop">
  Stop video
</button>
Enter fullscreen mode Exit fullscreen mode

接下来,是一些用于定位帽子的 CSS 代码:

#hat {
  position: absolute;
  display: none;
  text-align: center;
}
#hat.visible {
  display: block;
}
.container {
  position: relative;
}
Enter fullscreen mode Exit fullscreen mode

太棒了。接下来,我们使用 npm 安装 face-api.js,并创建一个index.js文件供我们工作:

npm i face-api.js && touch index.js
Enter fullscreen mode Exit fullscreen mode

最后,对于样板代码,我们从 HTML 中选择所有需要的元素:

/**
 * All of the necessary HTML elements
 */
const videoEl = document.querySelector('#video')
const startButtonEl = document.querySelector('#start')
const stopButtonEl = document.querySelector('#stop')
const deviceDropdownEl = document.querySelector('#deviceSelector')
const hatSelectorEl = document.querySelector('#hatSelector')
const hatEl = document.querySelector('#hat')
Enter fullscreen mode Exit fullscreen mode

太棒了!让我们进入有趣的部分吧。

访问网络摄像头

要访问网络摄像头,我们将使用媒体流 API。该 API 允许我们访问视频和音频设备,但我们只对视频设备感兴趣。此外,我们将把这些设备缓存到一个全局变量中,这样就无需再次获取它们。让我们来看一下:

const listDevices = async () => {
  if (devices.length > 0) {
    return
  }

  devices = await navigator.mediaDevices.enumerateDevices()
  // ...
}
Enter fullscreen mode Exit fullscreen mode

该对象允许我们访问所有设备,包括视频和音频设备。每个设备都是一个类mediaDevices对象。这些对象大致如下所示:InputDeviceInfoMediaDeviceInfo

{
  deviceId: "someHash",
  groupId: "someOtherHash"
  kind: "videoinput", // or "audioinput"
  label: "Some human readable name (some identifier)"
}
Enter fullscreen mode Exit fullscreen mode

kind正是我们感兴趣的。我们可以利用这一点来筛选所有videoinput设备,从而得到可用网络摄像头的列表。我们还会将这些设备添加到<select>我们预先编写的模板代码中,并将遇到的第一个设备标记为选定设备:

/**
 * List all available camera devices in the select
 */
let selectedDevice = null

let devices = []

const listDevices = async () => {
  if (devices.length > 0) {
    return
  }

  devices = (await navigator.mediaDevices.enumerateDevices())
    .filter(d => d.kind === 'videoinput')

  if (devices.length > 0) {
    deviceDropdownEl.innerHTML = devices.map(d => `
      <option value="${d.deviceId}">${d.label}</option>
    `).join('')

    // Select first device
    selectedDevice = devices[0].deviceId
  }
}
Enter fullscreen mode Exit fullscreen mode

现在,我们将向用户显示网络摄像头输入。为此,媒体流 API 提供了一个getUserMedia方法。它接收一个配置对象作为参数,该对象定义了我们想要访问的内容及其访问方式。我们不需要音频,但需要来自摄像头的视频流selectedDevice。我们还可以告诉 API 我们偏好的视频尺寸。最后,我们将此方法的输出分配给摄像头<video>,即其srcObject

const startVideo = async () => {
  // Some more face detection stuff later

  videoEl.srcObject = await navigator.mediaDevices.getUserMedia({
    video: {
      width: { ideal: 1280 },
      height: { ideal: 720 },
      deviceId: selectedDevice,
    },
    audio: false,
  })

  // More face detection stuff later
}
Enter fullscreen mode Exit fullscreen mode

这样应该就行了。既然它<video>autoplay属性,就应该立即显示摄像头看到的画面。当然,除非我们不允许浏览器访问摄像头。但我们为什么要阻止呢,对吧?毕竟,我们想戴帽子。

如果戴帽子的画面显得过于诡异,我们也想停止视频播放。我们可以先分别停止每个源对象的轨迹,然后再清除其srcObject自身数据来实现这一点。

const stopVideo = () => {
  // Some face detection stuff later on

  if (videoEl.srcObject) {
    videoEl.srcObject.getTracks().forEach(t => {
      t.stop()
    })
    videoEl.srcObject = null
  }
}
Enter fullscreen mode Exit fullscreen mode

现在我们可以开始和停止视频了。接下来:

进行人脸识别

让我们开始引入机器学习。在配置过程中,我们安装了face-api.js,这是一个非常棒的库,可以执行各种与人脸识别、检测和解释相关的机器学习任务。它还可以检测情绪,告诉我们脸部不同部位(例如下颌线或眼睛)的位置,并且能够使用不同的模型权重。最棒的是:它不需要任何远程服务;我们只需要提供正确的模型权重!当然,这些权重可能相当大,但我们只需要加载一次,就可以在剩余的会话期间进行人脸识别。

不过,首先我们需要模型。face -api.js 代码库包含了我们所需的所有预训练模型:

  • face_landmark_68_model-shard1
  • face_landmark_68_model-weights_manifest.json
  • ssd_mobilenetv1_model-shard1
  • ssd_mobilenetv1_model-shard2
  • ssd_mobilenetv1_model-weights_manifest.json
  • tiny_face_detector_model-shard1
  • tiny_face_detector_model-weights_manifest.json

我们将这些文件放在一个名为“face-api”的文件夹中model,并让 face-api 加载这些文件:

let faceApiInitialized = false

const initFaceApi = async () => {
  if (!faceApiInitialized) {
    await faceapi.loadFaceLandmarkModel('/models')
    await faceapi.nets.tinyFaceDetector.loadFromUri('/models')

    faceApiInitialized = true
  }
}
Enter fullscreen mode Exit fullscreen mode

我们需要的是面部特征点:它们代表一个具有 x 和 y 坐标、宽度和高度值的立方体。虽然我们可以使用面部特征来获得更高的精度,但为了简单起见,我们将使用面部特征点。

借助 face-api.js,我们可以创建一个异步函数来检测视频流中的人脸。face-api.js 会帮我们完成所有必要的工作,我们只需要告诉它要在哪个元素中查找人脸以及使用哪个模型即可。不过,我们需要先初始化 API。

const detectFace = async () => {
  await initFaceApi()

  return await faceapi.detectSingleFace(videoEl, new faceapi.TinyFaceDetectorOptions())
}
Enter fullscreen mode Exit fullscreen mode

这将返回一个名为 `<box>` 的对象,dd其中包含一个名为 `<box>` 的属性_box。这个盒子包含各种信息,包括每个角的坐标、左上角的 x 和 y 坐标、宽度和高度。要定位包含帽子的盒子,我们需要 `<box>`、`<box>`topleft` width<box> height` 属性。由于每个帽子表情符号都略有不同,我们不能简单地将它们直接放在脸上——那样就不合适了。

所以,我们来添加帽子,以及一些自定义帽子位置的方法:

/**
 * All of the available hats
 */
const hats = {
  tophat: {
    hat: '🎩',
    positioning: box => ({
      top: box.top - (box.height * 1.1),
      left: box.left,
      fontSize: box.height,
    }),
  },
  bowhat: {
    hat: '👒',
    positioning: box => ({
      top: box.top - box.height,
      left: box.left + box.width * 0.1,
      width: box.width,
      fontSize: box.height,
    }),
  },
  cap: {
    hat: '🧢',
    positioning: box => ({
      top: box.top - box.height * 0.8,
      left: box.left - box.width * 0.10,
      fontSize: box.height * 0.9,
    }),
  },
  graduationcap: {
    hat: '🎓',
    positioning: box => ({
      top: box.top - box.height,
      left: box.left,
      fontSize: box.height,
    }),
  },
  rescuehelmet: {
    hat: '⛑️',
    positioning: box => ({
      top: box.top - box.height * 0.75,
      left: box.left,
      fontSize: box.height * 0.9,
    }),
  },
}
Enter fullscreen mode Exit fullscreen mode

主要原因是

因为我们还没有用到<select>帽子,接下来就加上这个:

let selectedHat = 'tophat'

const listHats = () => {
  hatSelectorEl.innerHTML = Object.keys(hats).map(hatKey => {
    const hat = hats[hatKey]

    return `<option value="${hatKey}">${hat.hat}</option>`
  }).join('')
}
Enter fullscreen mode Exit fullscreen mode

如何戴帽子

现在我们可以开始把各个部分组合起来了。利用selectedHat变量和方框,我们现在可以将选定的帽子定位到检测到的脸上:

/**
 * Positions the hat by a given box
 */
const positionHat = (box) => {
  const hatConfig = hats[selectedHat]
  const positioning = hatConfig.positioning(box)

  hatEl.classList.add('visible')
  hatEl.innerHTML = hatConfig.hat
  hatEl.setAttribute('style', `
    top: ${positioning.top}px; 
    left: ${positioning.left}px; 
    width: ${box.width}px; 
    height: ${box.height}px; 
    font-size: ${positioning.fontSize}px;
  `)
}
Enter fullscreen mode Exit fullscreen mode

如你所见,我们用的是 CSS。当然,我们也可以用 canvas 之类的工具来绘制,但 CSS 让事情变得更简单,也更流畅。

现在我们需要将人脸检测功能集成到 ` startVideoand`stopVideo函数中。为了完整起见,我将在这里展示这些函数的全部代码。

/**
 * Start and stop the video
 */
let faceDetectionInterval = null

const startVideo = async () => {
  listHats()
  await listDevices()

  stopVideo()

  try {
    videoEl.srcObject = await navigator.mediaDevices.getUserMedia({
      video: {
        width: { ideal: 1280 },
        height: { ideal: 720 },
        deviceId: selectedDevice,
      },
      audio: false
    })

    faceDetectionInterval = setInterval(async () => {
      const positioning = await detectFace()

      if (positioning) {
        positionHat(positioning._box)
      }
    }, 60)
  } catch(e) {
    console.error(e)
  }
}

const stopVideo = () => {
  clearInterval(faceDetectionInterval)
  hatEl.classList.remove('visible')

  if (videoEl.srcObject) {
    videoEl.srcObject.getTracks().forEach(t => {
      t.stop()
    })
    videoEl.srcObject = null
  }
}
Enter fullscreen mode Exit fullscreen mode

如您所见,我们在这里使用了一个时间间隔来定位所有物体。由于人脸检测的特性,如果频率再高一些,画面就会抖动得厉害。现在画面已经有些抖动了,但大约 60 毫秒的间隔至少可以接受。

最后,我们添加一些事件监听器,一切就绪:

/**
 * Event listeners
 */
startButtonEl.addEventListener('click', startVideo)

stopButtonEl.addEventListener('click', stopVideo)

deviceDropdownEl.addEventListener('change', e => {
  selectedDevice = e.target.value
  startVideo()
})

hatSelectorEl.addEventListener('change', e => {
  selectedHat = e.target.value
})
Enter fullscreen mode Exit fullscreen mode

结果

结果如下:

根据您的系统配置,表情符号可能无法正常显示,因为每个系统渲染表情符号的方式都不同。此外,请稍等片刻,模型权重需要几秒钟才能加载完成。为了获得最佳效果,请在大屏幕上查看,并在新标签页中打开沙盒。当然,该标签页需要访问摄像头权限。

如果你愿意的话,不妨在评论区分享一张你戴着你最喜欢的帽子表情符号的截图?


希望您喜欢这篇文章,就像我喜欢写这篇文章一样!如果喜欢,请点个赞❤️🦄 !我会在空闲时间写一些科技文章,偶尔也喜欢喝杯咖啡。

如果你想支持我的工作, 可以请我喝杯咖啡 或者 在推特上关注我🐦 你也可以直接通过PayPal支持我!

给我买杯咖啡按钮

文章来源:https://dev.to/thormeier/funny-hat-day-how-to-do-face-detection-with-your-webcam-and-javascript-4gkf