趣味帽子日!👒🎩 如何使用网络摄像头和 JavaScript 进行人脸检测 📸🧠
(封面图片由Dall-E mini制作,标题为“一个戴着滑稽高顶礼帽的人工智能”——你知道,因为我们今天要讲机器学习方面的内容。)
距离我上次发帖已经有一段时间了。我正在筹备一个相当大的项目;很快就会有消息公布!
但今天,我们要好好看看你们。没错,就是你们。确切地说,是你们美丽的脸庞。我们会让你们戴上帽子。为此,我们会用到face-api.js和Media 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>
接下来,是一些用于定位帽子的 CSS 代码:
#hat {
position: absolute;
display: none;
text-align: center;
}
#hat.visible {
display: block;
}
.container {
position: relative;
}
太棒了。接下来,我们使用 npm 安装 face-api.js,并创建一个index.js文件供我们工作:
npm i face-api.js && touch index.js
最后,对于样板代码,我们从 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')
太棒了!让我们进入有趣的部分吧。
访问网络摄像头
要访问网络摄像头,我们将使用媒体流 API。该 API 允许我们访问视频和音频设备,但我们只对视频设备感兴趣。此外,我们将把这些设备缓存到一个全局变量中,这样就无需再次获取它们。让我们来看一下:
const listDevices = async () => {
if (devices.length > 0) {
return
}
devices = await navigator.mediaDevices.enumerateDevices()
// ...
}
该对象允许我们访问所有设备,包括视频和音频设备。每个设备都是一个类或mediaDevices对象。这些对象大致如下所示:InputDeviceInfoMediaDeviceInfo
{
deviceId: "someHash",
groupId: "someOtherHash"
kind: "videoinput", // or "audioinput"
label: "Some human readable name (some identifier)"
}
这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
}
}
现在,我们将向用户显示网络摄像头输入。为此,媒体流 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
}
这样应该就行了。既然它<video>有autoplay属性,就应该立即显示摄像头看到的画面。当然,除非我们不允许浏览器访问摄像头。但我们为什么要阻止呢,对吧?毕竟,我们想戴帽子。
如果戴帽子的画面显得过于诡异,我们也想停止视频播放。我们可以先分别停止每个源对象的轨迹,然后再清除其srcObject自身数据来实现这一点。
const stopVideo = () => {
// Some face detection stuff later on
if (videoEl.srcObject) {
videoEl.srcObject.getTracks().forEach(t => {
t.stop()
})
videoEl.srcObject = null
}
}
现在我们可以开始和停止视频了。接下来:
进行人脸识别
让我们开始引入机器学习。在配置过程中,我们安装了face-api.js,这是一个非常棒的库,可以执行各种与人脸识别、检测和解释相关的机器学习任务。它还可以检测情绪,告诉我们脸部不同部位(例如下颌线或眼睛)的位置,并且能够使用不同的模型权重。最棒的是:它不需要任何远程服务;我们只需要提供正确的模型权重!当然,这些权重可能相当大,但我们只需要加载一次,就可以在剩余的会话期间进行人脸识别。
不过,首先我们需要模型。face -api.js 代码库包含了我们所需的所有预训练模型:
face_landmark_68_model-shard1face_landmark_68_model-weights_manifest.jsonssd_mobilenetv1_model-shard1ssd_mobilenetv1_model-shard2ssd_mobilenetv1_model-weights_manifest.jsontiny_face_detector_model-shard1tiny_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
}
}
我们需要的是面部特征点:它们代表一个具有 x 和 y 坐标、宽度和高度值的立方体。虽然我们可以使用面部特征来获得更高的精度,但为了简单起见,我们将使用面部特征点。
借助 face-api.js,我们可以创建一个异步函数来检测视频流中的人脸。face-api.js 会帮我们完成所有必要的工作,我们只需要告诉它要在哪个元素中查找人脸以及使用哪个模型即可。不过,我们需要先初始化 API。
const detectFace = async () => {
await initFaceApi()
return await faceapi.detectSingleFace(videoEl, new faceapi.TinyFaceDetectorOptions())
}
这将返回一个名为 `<box>` 的对象,dd其中包含一个名为 `<box>` 的属性_box。这个盒子包含各种信息,包括每个角的坐标、左上角的 x 和 y 坐标、宽度和高度。要定位包含帽子的盒子,我们需要 `<box>`、`<box>`top和left` 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,
}),
},
}
主要原因是
因为我们还没有用到<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('')
}
如何戴帽子
现在我们可以开始把各个部分组合起来了。利用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;
`)
}
如你所见,我们用的是 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
}
}
如您所见,我们在这里使用了一个时间间隔来定位所有物体。由于人脸检测的特性,如果频率再高一些,画面就会抖动得厉害。现在画面已经有些抖动了,但大约 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
})
结果
结果如下:
根据您的系统配置,表情符号可能无法正常显示,因为每个系统渲染表情符号的方式都不同。此外,请稍等片刻,模型权重需要几秒钟才能加载完成。为了获得最佳效果,请在大屏幕上查看,并在新标签页中打开沙盒。当然,该标签页需要访问摄像头权限。
如果你愿意的话,不妨在评论区分享一张你戴着你最喜欢的帽子表情符号的截图?
希望您喜欢这篇文章,就像我喜欢写这篇文章一样!如果喜欢,请点个赞❤️或🦄 !我会在空闲时间写一些科技文章,偶尔也喜欢喝杯咖啡。
如果你想支持我的工作, 可以请我喝杯咖啡☕ 或者 在推特上关注我🐦 ! 你也可以直接通过PayPal支持我!
文章来源:https://dev.to/thormeier/funny-hat-day-how-to-do-face-detection-with-your-webcam-and-javascript-4gkf
