如何在web worker 中使用face-api.js

1,251 阅读5分钟

face-api在检测人脸的时候需要进行大量CUP的计算,而这是JS最不擅长的地方。这会导致阻塞其他任务的执行,影响用户体验。因此本文将使用 web worker 解决这个问题。

web worker

在开始之前需要大致了解 web worker,可以直接跳过web worker介绍查看后文的 开始 部分

Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。

Web Worker 有以下几个使用注意点

(1)同源限制

分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。

(2)DOM 限制

Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用documentwindowparent这些对象。但是,Worker 线程可以navigator对象和location对象。

(3)通信联系

Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。

(4)脚本限制

Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。

(5)文件限制

Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。

Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。

web worker细节可以看这些文章:阮一峰网络日志: Web Worker 使用教程MDN: Web Workers(以上介绍也摘录至这两篇文章)

开始

webpack中使用web worker。

方式一:webpack5以上的版本可以使用import.mata.url

注意以下配置并没有在我这里得到验证,参考:webpack web worker

// main
const worker = new Worker(new URL('./deep-thought.js', import.meta.url));
worker.postMessage({
  question:
    'The Answer to the Ultimate Question of Life, The Universe, and Everything.',
});
worker.onmessage = ({ data: { answer } }) => {
  console.log(answer);
};
// worker
self.onmessage = ({ data: { question } }) => {
  self.postMessage({
    answer: 42,
  });
};

这种方式我不知道什么问题任是无法正常使用,监听不到Worker 的 onmessage,如果有读者成功运行了欢迎留言。

退而求其次所以我用了下面的方法

方式二:webpack4 以下使用worker-loader或则worker-plugin

worker-loader

安装

pnpm add worker-loader -D
//or
yarn add worker-loader -D

webpack.config.js

// webpack.config.js
{
  module: {
    rules: [
      {
        test: /.worker.js$/,
        use: { loader: 'worker-loader' }
      }
    ]
  }
}

chain webpack

// chain webpack
chainWebpack:(config)=>{
  config.module.rule('worker').test(/.worker.js$/).use('webWorker').loader('worker-loader')
}

配置来源文档:worker-loader

使用:

import MyWorker from "../worker/my.worker.js"

const myWorker = new MyWorker()

worker-plugin的配置方式不过多介绍文档在这里:worker-plugin

配置web worker遇到的一些问题

我在使用umi搭建的项目中使用worker-loader遇到了如下报错:

image.png

解决方法:

覆盖webpack:

{
 "devDependencies": {
   "webpack": "npm:@bbkkbkk/umi-webpack5-export"
 }
}

具体细节查看: umi issues - 6247@bbkkbkk/umi-webpack5-export npm包

在web worker中使用face-api.js

face-api提供了browser和nodejs两种环境的使用,face-api官方的配置

image.png

实践下来两种再web worker配置方式再web worker都行不通。首先npm canvas这个库就没有Canvas Image这两个类。其次web worker 的环境无法通过face-api的要求。既不满足Browser 也不满足 Nodejs。

image.png

只要解决环境问题和API兼容性问题就能在worker中face-api使用

face-api 计算过程中会用到 canvas,在web worker的限制中提到了web worker不能操作浏览器中的对象,我们可以使用OffscreenCanvas代替Canvas使用。还需要手动设置一些face-api的环境,因此需要做如下额外操作:

// face.worker.js

import * as faceapi from 'face-api.js';

faceapi.env.setEnv(faceapi.env.createNodejsEnv()); // 设置环境

faceapi.env.monkeyPatch({ // 补丁
  Canvas: OffscreenCanvas, // 使 OffscreenCanvas 代替 Canvas
  createCanvasElement: () => {
    return new OffscreenCanvas(480, 270);
  },
});

细节参考 face-api issue - 47

解决方案代码部分

图像处理基本路径:

Main                                   Worker

image
|> OffscreenCanvas
|> buffer  ---- postMessage -------->  new OffscreenCanvas with buffer
                                       |> faceapi.detectAllFaces(canvas)
   detections <---- postMessage -----  detections

直接看下面代码:

JS主线程代码

// 主线程
const getFacesInfo = (image: HTMLCanvasElement|HTMLImageElement) => {
  return new Promise<FaceDetection[]>((resolve) => {

    const canvas = new OffscreenCanvas(image.width,image.height); //can also be normal canvas
    const context = canvas.getContext('2d');
    context?.drawImage(image,0,0,image.width,image.height);

    const imgData = context?.getImageData(0,0,image.width,image.height);

    faceWorker.postMessage({
      type: 'send-img-data',
      w: image.width,
      h: image.height,
      buffer: imgData?.data.buffer
    },[imgData?.data.buffer]);

    faceWorker.addEventListener('message',async(e: MessageEvent<FaceDetection[]>) => {
      resolve(e.data);
    });
  });
};

worker 代码

import * as faceapi from 'face-api.js';

faceapi.env.setEnv(faceapi.env.createNodejsEnv());

faceapi.env.monkeyPatch({
  Canvas: OffscreenCanvas,
  createCanvasElement: () => {
    return new OffscreenCanvas(480, 270);
  },
});

// 加载神经网络模型
const models_url = '/models'; // 链接到本地的public/models 

//获取模型地址 https://github.com/justadudewhohacks/face-api.js-models
['ssdMobilenetv1'].map((model) => (faceapi?.nets[model].loadFromUri(models_url)))

addEventListener('message',async (e)=>{
  const buffer = e.data.buffer;
  const w = e.data.w;
  const h = e.data.h;
  const canvas = new OffscreenCanvas(w, h);
  const context = canvas.getContext('2d');
  const imageData = context.createImageData(w, h);
  const arr = new Uint8ClampedArray(buffer);
  imageData.data.set(arr);
  context.putImageData(imageData, 0, 0);
  const detections = await faceapi.detectAllFaces(canvas)
  self.postMessage(detections)
})

下面是 public/models 所有的模型,可以只保存你需要模型。模型去这里下载:face-api.js-models image.png

注意:在使用face api之前models必须加载完成

总结

face-api in web worker 解决方案

  1. 通过使用OffscreenCanvas来代替Canvas
  2. 使用faceapi.env.setEnv(faceapi.env.createNodejsEnv());主动设置为Node环境

遗留的问题

最后文中还有一个未解决的问题是在webpack5中配合import.meta.url来使用web worker,欢迎留言。

关于这个问题更多的信息:

  • 已确保worker解析出来的位置地址正确,及 new URL('./deep-thought.js', import.meta.url)
  • 并在多个项目中试验使用都无法正确运行,还在较为干净的webpack脚手架运行了(是我打开方式不对?[捂脸])。

参考文献