今天我们将回答这个问题,并且我们将增加一些额外的内容,比如用蜘蛛侠滤镜来掩盖你的脸,或者经典的狗狗滤镜。在这个项目上的工作超级有趣,我希望你能喜欢它。
这篇文章将涵盖两个主要话题。
- 脸部特征识别
- 添加过滤器
如何检测面部特征?
与DLib的工作方式类似,对于JavaScript,我们有一个叫做clmtrackr的库,它将完成检测人脸在图像上的位置的繁重工作,也会识别人脸特征,如鼻子、嘴巴、眼睛等。
这个库提供了一些通用模型,这些模型已经预先训练好了,可以按照以下特征的编号使用。
点图
当我们用该库处理一幅图像时,它将为该地图上的每个点返回一个数组,其中每个点由其在x 和y 轴上的位置来识别。当我们建立过滤器时,这将变得非常重要。你可能已经猜到了,如果我们想在人的鼻子上画一些东西,我们可以使用点62 ,这是鼻子的中心。
但理论上已经足够了,让我们开始做一些很酷的东西吧!
我们要建造什么?
在这篇文章中,我们将利用clmtrackr 来识别视频流中的人脸(在我们的例子中是网络摄像头或摄像机),并应用自定义过滤器,可以通过屏幕上的下拉菜单来选择。这里是codepen上的应用程序的演示(请确保你的浏览器允许该应用程序访问摄像头,否则它将无法工作)。
let outputWidth;
let outputHeight;
let faceTracker; // Face Tracking
let videoInput;
let imgSpidermanMask; // Spiderman Mask Filter
let imgDogEarRight, imgDogEarLeft, imgDogNose; // Dog Face Filter
let selected = -1; // Default no filter
/*
* **p5.js** library automatically executes the `preload()` function. Basically, it is used to load external files. In our case, we'll use it to load the images for our filters and assign them to separate variables for later use.
*/
function preload()
{
// Spiderman Mask Filter asset
imgSpidermanMask = loadImage("https://i.ibb.co/9HB2sSv/spiderman-mask-1.png");
// Dog Face Filter assets
imgDogEarRight = loadImage("https://i.ibb.co/bFJf33z/dog-ear-right.png");
imgDogEarLeft = loadImage("https://i.ibb.co/dggwZ1q/dog-ear-left.png");
imgDogNose = loadImage("https://i.ibb.co/PWYGkw1/dog-nose.png");
}
/**
* In p5.js, `setup()` function is executed at the beginning of our program, but after the `preload()` function.
*/
function setup()
{
const maxWidth = Math.min(windowWidth, windowHeight);
pixelDensity(1);
outputWidth = maxWidth;
outputHeight = maxWidth * 0.75; // 4:3
createCanvas(outputWidth, outputHeight);
// webcam capture
videoInput = createCapture(VIDEO);
videoInput.size(outputWidth, outputHeight);
videoInput.hide();
// select filter
const sel = createSelect();
const selectList = ['Spiderman Mask', 'Dog Filter']; // list of filters
sel.option('Select Filter', -1); // Default no filter
for (let i = 0; i < selectList.length; i++)
{
sel.option(selectList[i], i);
}
sel.changed(applyFilter);
// tracker
faceTracker = new clm.tracker();
faceTracker.init();
faceTracker.start(videoInput.elt);
}
// callback function
function applyFilter()
{
selected = this.selected(); // change filter type
}
/*
* In p5.js, draw() function is executed after setup(). This function runs inside a loop until the program is stopped.
*/
function draw()
{
image(videoInput, 0, 0, outputWidth, outputHeight); // render video from webcam
// apply filter based on choice
switch(selected)
{
case '-1': break;
case '0': drawSpidermanMask(); break;
case '1': drawDogFace(); break;
}
}
// Spiderman Mask Filter
function drawSpidermanMask()
{
const positions = faceTracker.getCurrentPosition();
if (positions !== false)
{
push();
const wx = Math.abs(positions[13][0] - positions[1][0]) * 1.2; // The width is given by the face width, based on the geometry
const wy = Math.abs(positions[7][1] - Math.min(positions[16][1], positions[20][1])) * 1.2; // The height is given by the distance from nose to chin, times 2
translate(-wx/2, -wy/2);
image(imgSpidermanMask, positions[62][0], positions[62][1], wx, wy); // Show the mask at the center of the face
pop();
}
}
// Dog Face Filter
function drawDogFace()
{
const positions = faceTracker.getCurrentPosition();
if (positions !== false)
{
if (positions.length >= 20) {
push();
translate(-100, -150); // offset adjustment
image(imgDogEarRight, positions[20][0], positions[20][1]);
pop();
}
if (positions.length >= 16) {
push();
translate(-20, -150); // offset adjustment
image(imgDogEarLeft, positions[16][0], positions[16][1]);
pop();
}
if (positions.length >= 62) {
push();
translate(-57, -20); // offset adjustment
image(imgDogNose, positions[62][0], positions[62][1]);
pop();
}
}
}
function windowResized()
{
const maxWidth = Math.min(windowWidth, windowHeight);
pixelDensity(1);
outputWidth = maxWidth;
outputHeight = maxWidth * 0.75; // 4:3
resizeCanvas(outputWidth, outputHeight);
}
棒极了!它可能并不完美,但看起来很不错!我们来分解代码,并在此基础上对其进行修改。
让我们把代码分解,解释一下我们在做什么。
基本代码结构
为了构建这个应用程序,我们使用了p5.js库,这是一个主要为使用画布而设计的JavaScript库,完全适合我们的使用情况。P5JS不是你传统的UI库,相反,它与事件一起工作,定义何时建立UI,何时更新。与一些游戏引擎类似。
我想介绍的是P5的3个主要事件。
preload事件:在库加载后,在建立任何UI或在屏幕上绘制任何东西之前,立即执行。这使得它可以完美地加载资产。setup事件:也是在preload之后执行一次,是我们准备一切和建立初始UI的地方。draw渲染:这是一个循环调用的函数,每次系统需要渲染屏幕时它都会被执行。
预加载
根据定义,我们将使用preload 事件来加载我们将在后面的代码中使用的图片,如下所示。
function preload() {
// Spiderman Mask Filter asset
imgSpidermanMask = loadImage("https://i.ibb.co/9HB2sSv/spiderman-mask-1.png");
// Dog Face Filter assets
imgDogEarRight = loadImage("https://i.ibb.co/bFJf33z/dog-ear-right.png");
imgDogEarLeft = loadImage("https://i.ibb.co/dggwZ1q/dog-ear-left.png");
imgDogNose = loadImage("https://i.ibb.co/PWYGkw1/dog-nose.png");
}
非常简单。来自p5的函数loadImage ,正如你所期望的,将加载图像并使其作为P5图像对象可用。
设置
在这里,事情变得有点有趣,因为我们在这里加载用户界面。我们将把这个事件中执行的代码分成四个部分
创建画布
由于我们希望我们的代码是响应式的,我们的画布将有一个动态的大小,它将根据窗口的大小和使用4:3的长宽比来计算。在代码中使用长宽比并不理想,但我们会做一些假设,以保持代码在演示中的简洁性。在我们知道画布的尺寸后,我们可以用P5函数createCanvas 来创建一个画布,如下所示。
const maxWidth = Math.min(windowWidth, windowHeight);
pixelDensity(1);
outputWidth = maxWidth;
outputHeight = maxWidth * 0.75; // 4:3
createCanvas(outputWidth, outputHeight);
捕获视频流
在我们的画布工作后,我们需要从网络摄像头或相机中捕获视频流,并将其放入画布中,幸运的是,P5 通过videoCapture 函数使其非常容易做到这一点。
// webcam capture
videoInput = createCapture(VIDEO);
videoInput.size(outputWidth, outputHeight);
videoInput.hide();
构建过滤器选择器
我们的应用程序非常棒,可以提供一个以上的过滤器选项,所以我们需要建立一种方法来选择我们想要激活的过滤器。同样......我们可以在这里变得非常花哨,然而,为了简单起见,我们将使用一个简单的下拉菜单,我们可以使用P5createSelect() 函数来创建。
// select filter
const sel = createSelect();
const selectList = ['Spiderman Mask', 'Dog Filter']; // list of filters
sel.option('Select Filter', -1); // Default no filter
for (let i = 0; i < selectList.length; i++)
{
sel.option(selectList[i], i);
}
sel.changed(applyFilter);
创建图像跟踪器
图像跟踪器是一个可以连接到视频源的对象,它将识别每一帧的所有面孔和它们的特征。追踪器需要为一个给定的视频源设置一次。
// tracker
faceTracker = new clm.tracker();
faceTracker.init();
faceTracker.start(videoInput.elt);
绘制视频和过滤器
现在一切都设置好了,我们需要更新P5的draw 事件,将视频源输出到画布上,并应用任何被选中的过滤器。在我们的案例中,draw 函数将非常简单,将复杂性推到每个过滤器的定义中。
function draw() {
image(videoInput, 0, 0, outputWidth, outputHeight); // render video from webcam
// apply filter based on choice
switch(selected)
{
case '-1': break;
case '0': drawSpidermanMask(); break;
case '1': drawDogFace(); break;
}
}
构建蜘蛛人面具过滤器
蜘蛛人面具过滤器
构建过滤器可以是一项简单或非常复杂的任务。这将取决于过滤器要做什么。对于蜘蛛人面具,我们只需要把蜘蛛人面具的图像要求到屏幕的中心。要做到这一点,我们首先要确保我们的faceTracker对象通过使用faceTraker.getCurrentPosition() ,真正检测到一张脸。
一旦我们检测到人脸,我们就用P5渲染图像,使用人脸点62,也就是鼻子的中心作为图像的中心,宽度和高度代表人脸的大小,如下所示。
const positions = faceTracker.getCurrentPosition();
if (positions !== false)
{
push();
const wx = Math.abs(positions[13][0] - positions[1][0]) * 1.2; // The width is given by the face width, based on the geometry
const wy = Math.abs(positions[7][1] - Math.min(positions[16][1], positions[20][1])) * 1.2; // The height is given by the distance from nose to chin, times 2
translate(-wx/2, -wy/2);
image(imgSpidermanMask, positions[62][0], positions[62][1], wx, wy); // Show the mask at the center of the face
pop();
}
很酷吧?
现在,狗过滤器的工作方式与此相同,但使用了3个图像,而不是一个,一个用于耳朵,一个用于鼻子。我不会用更多相同的代码来烦扰你,但如果你想检查它,请查看代码集,其中包含演示的完整代码。
结论
在JavaScript库的帮助下,识别面部特征并开始建立你自己的过滤器是非常容易的。不过,有一些注意事项是我们在本教程中没有涉及的。例如,如果脸部不直对着相机会怎样?我们如何扭曲我们的滤镜,使其遵循脸部的弧度?或者,如果我想添加三维物体而不是二维滤镜怎么办?