如何用JavaScript检测人脸特征并应用过滤器

258 阅读7分钟

今天我们将回答这个问题,并且我们将增加一些额外的内容,比如用蜘蛛侠滤镜来掩盖你的脸,或者经典的狗狗滤镜。在这个项目上的工作超级有趣,我希望你能喜欢它。

这篇文章将涵盖两个主要话题。

  • 脸部特征识别
  • 添加过滤器

如何检测面部特征?

与DLib的工作方式类似,对于JavaScript,我们有一个叫做clmtrackr的库,它将完成检测人脸在图像上的位置的繁重工作,也会识别人脸特征,如鼻子、嘴巴、眼睛等。

这个库提供了一些通用模型,这些模型已经预先训练好了,可以按照以下特征的编号使用。

image.png

点图

当我们用该库处理一幅图像时,它将为该地图上的每个点返回一个数组,其中每个点由其在xy 轴上的位置来识别。当我们建立过滤器时,这将变得非常重要。你可能已经猜到了,如果我们想在人的鼻子上画一些东西,我们可以使用点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;
  }
}

构建蜘蛛人面具过滤器

image.png

蜘蛛人面具过滤器

构建过滤器可以是一项简单或非常复杂的任务。这将取决于过滤器要做什么。对于蜘蛛人面具,我们只需要把蜘蛛人面具的图像要求到屏幕的中心。要做到这一点,我们首先要确保我们的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库的帮助下,识别面部特征并开始建立你自己的过滤器是非常容易的。不过,有一些注意事项是我们在本教程中没有涉及的。例如,如果脸部不直对着相机会怎样?我们如何扭曲我们的滤镜,使其遵循脸部的弧度?或者,如果我想添加三维物体而不是二维滤镜怎么办?