《高阶前端指北》之Web人脸识别技术实现

3,628 阅读6分钟

Hi,大家好,我是扫地盲僧,负责教你闭着眼睛写代码,面壁十年功成名就~

逻辑构思

我们需要了解几个概念(仅做了解即可):

WebRTC,是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。WebRTC 包含的这些标准使用户在无需安装任何插件或者第三方的软件的情况下,创建点对点(Peer-to-Peer)的数据分享和电话会议成为可能。

MediaDevices,接口提供访问连接媒体输入的设备,如照相机和麦克风,以及屏幕共享等。它可以使你取得任何硬件资源的媒体数据。 老规矩,我们来思考一下人脸插件需要实现哪些能力?首先,我们要分析有那么些场景?

  • 扫码人脸识别
  • 摄像头识别万物
  • 视频会议
  • 监控
  • ……

以上这些差不多是当前使用较多的场景了。功能比较简单,设计起来也不会太复杂。

最后,我们要重点考虑兼容性,尤其是移动端保持优先兼容,PC兼顾。特别在PC条件下,越来越多的人参与直播,会议等。

架构设计


以上便是我们要解决的基础功能,本插件区别于普通的插件,我们尽可能不提供样式,或者提供可高度自定义的样式,以满足开发者更多的使用场景。你可以结合自己的喜好选择不同的插件设计机制,这里我们选择第三种:在挂在结点下生成高度可定义的DOM

Event事件

构思最终展示形态

本次插件看着好像没有太多的事件,按照最新的规范只需要10行代码即可完成。但我们要做的是一个零门槛使用的人脸插件,就需要考虑更多兼容场景和能力。

  • renderDOM,绘制我们需要的DIV,思考Canvas放在什么位置?为人脸绘制圆形,便于零门槛使用。
  • startCamera,开启摄像头,思考如何解决摄像头资源占用?思考前置摄像头还是后置?思考兼容判断放在哪里?
  • startFlip,是否开启镜像,因为我们知道人脸一般是前置,反向录制,比较别扭。通常情况下人脸识别都会默认开启镜像,思考镜像如何实现?
  • drawImage,人脸识别的底层原理通常是人像识别,即是机器对于图片的识别,我们的插件应提供生成图片的能力
  • downloadImg,对于一些场景下我们需要保存图片到本地,比如自拍,识别等。
  • uploadImg,出于非必要不引入冗余代码的原则,我们无需引入或封装request请求,而是发给使用者imgFile,根据自己的框架进行上传。
  • openBeauty?,这是一个黑科技,有很多大神开源了此类的图像算法,转黑白,调亮度,磨皮等均有涉及,你能够将其原理运用到这里呢?
  • openClintFace?,通常人脸识别的原理是不停的对人脸进行检测,直到成功为止。目前各平台人脸识别的价格降低了很多,不过由于频次高依旧是项不小的开支。为减轻云端压力,能够客户端进行人脸识别?
  • resetclearclose等此类方法就不提了,对于聪慧你来说,小菜一盘。

渲染机制-Render

我们为了让使用者实现零门槛上手,所以这里选择由插件创建DOM的方式。

根据上面的架构布局的构思,我们从这里开始创建DOMstyle

this.rootEl = document.querySelector(selector) as HTMLElement;
this.wrapper = document.createElement('div') as HTMLElement;
this.video = document.createElement('video') as HTMLVideoElement;
this.canvas = document.createElement('canvas') as HTMLCanvasElement;

核心事件-WebCamera

刚刚我们说了,要抽离核心代码,那么我们就单独搞一个camera.ts,然后单独处理。

不要小看这段代码,问题贼多了。至少目前你看到的Web人脸识别方案兼容代码是它的10倍以上。

// 加上判断不到10行 可以使用promise
navigator.mediaDevices.getUserMedia(constraints:containTypes).
  then(handleSuccess(stream:MediaStream)=>void).catch(handleError(error:Error));

ployFillGetUserMedia怎么写呢?大家可以尝试着写一下,我们稍后再公布代码。

核心事件-SaveImg

无论是拍照、截屏、保存图片其本质都是一个原理,即将video转为canvas,再通过canvas做离屏渲染,保存为base64URL,最后将base64URL转为img。复杂吗?不复杂

public saveImg(canvas: HTMLCanvasElement, imgType: string = 'jpeg', quality: number = 1): Promise<any> {
    canvas.width = this.video.videoWidth;
    canvas.height = this.video.videoHeight;
    const ctx = canvas.getContext('2d');
    return new Promise((resolve, reject) => { 
      // 废弃toDataURL方案,1同步堵塞,2占用更多内存,3获得字符体系大
      ctx?.drawImage(
        this.video,
        0,
        0,
        canvas.width,
        canvas.height
      );
      const base64URL = canvas?.toDataURL(
        "image/" + imgType,
        quality
      );
      resolve(base64URL)
    })

留个优化作业:当前主流替代toDataURL的方案?

核心事件-ParamsChange

为了保证实时监听参数变化,我们要做一个小型的对象callback机制。我们做个简单参数监听即可,无需过渡封装。

// 对象某个参数的监听
export const watchValue = (store: object, key: string, callback: (newVal: any) => void): void => {
    let value = store[key]
    Object.defineProperty(store, key, {
        enumerable: false,
        set(newVal) {
            value = newVal
            callback(value)
        },
        get() {
            return value
        }
    })
}
//对整个对象参数的监听
export const observeProxy = (obj: Object, cal: (val: any) => void) => {
    return new Proxy(obj, {
        get: function (target, prop) {
            console.log('get调用了')
            return Reflect.get(target, prop)
        },
        set: function (target, prop, val) {
            cal(val)
            console.log('set调用了')
            return Reflect.set(target, prop, val)
        },
        deleteProperty: function (target, prop) {
            console.log('deleteProperty调用了')
            return Reflect.deleteProperty(target, prop)
        }
    })
}

拓展-美颜

美颜提供的能力: 美白,磨皮,红润。

// 美颜相机
  const beautyConfig= {
    beauty: 0,
    brightness: 0,
    ruddy: 0
  }
  const run = ()=>{
  if (this.store.isBeauty) {
            this.plugins.stream.initialize().then(() => {
                console.log('初始化本地相机成功');
                this.setBeautyParam({ ...self.options.beautyConfig } as any);
                const beautyStr = this.plugins.beautyPlugin.generateBeautyStream(self.plugins.stream);
                window.stream = beautyStr?.mediaStream_;
                self.video!.srcObject = beautyStr?.mediaStream_;
                self.video!.play();
            }).catch((error: Error) => {
                console.error('failed initialize localStream ' + error);
            });
        }
  }

其原理为通过拦截媒体流,并对其做流媒体处理,通过处理后回传给video

拓展-人脸识别(本地)

我们来给其增加一个刚才提到的本地人脸识别clintFace,可以通过一个训练模型直接检测人脸数据并是实时绘制人脸窗口,大大减少后端识别人脸的压力。

// 开启本地人脸识别
    isClintFace() {
        let self = this
        let begin = Date.now();
        const { isHasFace } = this.options
        let delay = 1000 / self.plugins.FPS - (Date.now() - begin);
        let loop = () => {
            let dt = Date.now() - begin;
            //开启人脸识别
            self.plugins.picoCamera.run(this.video)
            // isHasFace && isHasFace()
            setTimeout(loop, delay)
        }
        // requestAnimationFrame(loop);
        setTimeout(loop, delay)
    }

核心原理:与美颜插件不同的是,将其video绘制到canvas,通过canvas绘制轮廓的能力,结合人脸识别库pico,可以很轻松的触发事件。

***
run(video: any) {
    const width = this.canvas.width;
    const height = this.canvas.height;
    this.ctx.drawImage(video, 0, 0);
    let rgba = this.ctx.getImageData(0, 0, width, height).data;
    let image = {
      "pixels": this.rgba_to_grayscale(rgba, width, height),
      "nrows": width,
      "ncols": height,
      "ldim": height
    }
    let params = {
      "shiftfactor": 0.1,
      "minsize": 100,
      "maxsize": 1000,
      "scalefactor": 1.1
    }
    let dets = []
    dets = pico.run_cascade(image, this.facefinder_classify_region, params);
    dets = this.update_memory(dets);
    dets = pico.cluster_detections(dets, 0.2);
    for (let i = 0; i < dets.length; ++i) {
      if (dets[i][3] > 50.0) {
        this.ctx.beginPath();
        this.ctx.arc(dets[i][1], dets[i][0], dets[i][2] / 2, 0, 2 * Math.PI, false);
        this.ctx.lineWidth = 3;
        this.ctx.strokeStyle = 'red';
        this.ctx.stroke();
        console.info(dets[i][3], '大于50代表检测到人脸')
      }
    }
  }
  ***

启动

如果你按照我的思路,完全走下来,依旧恭喜你,已完成临门一脚。接下来便是各个浏览器下的兼容测试了。

插件还在持续优化中,敬请期待~ 不过可以给大家预览地址 ,可以关注仓库预发布地址:仓库

如果喜欢我的文章,麻烦点个赞评个论收个藏关个注

手绘图,手打字,纯原创,摘自未发布的书籍:《高阶前端指北》,转载请获得本人同意。

book-slogan.gif