前端实现活体人脸检测

22,410 阅读3分钟

我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!

前言

在网页中使用活体人脸检测大部分都是前端录制一段视频,让后端调用第三方接口去判断,今天我们就用纯前端方式来实现这个功能。

点击右上角“查看详情”查看!

复制链接预览
https://code.juejin.cn/pen/7143888053151465480

创建人脸模型

引入tensorflow训练好的人脸特征点检测模型,预测 486 个 3D 人脸特征点,推断出人脸的近似面部几何图形。

async createDetector(){
    const model = faceLandmarksDetection.SupportedModels.MediaPipeFaceMesh;
    const detectorConfig = {
        maxFaces:1, //检测到的最大面部数量
        refineLandmarks:true, //可以完善眼睛和嘴唇周围的地标坐标,并在虹膜周围输出其他地标
        runtime: 'mediapipe',
        solutionPath: 'https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh', //WASM二进制文件和模型文件所在的路径
    };
    this.detector = await faceLandmarksDetection.createDetector(model, detectorConfig);
}
mesh_map.jpg

人脸识别

async renderPrediction() {
    var video = this.$refs['video'];
    var canvas = this.$refs['canvas'];
    var context = canvas.getContext('2d');
    context.clearRect(0, 0, canvas.width, canvas.height);
    const Faces = await this.detector.estimateFaces(video, {
        flipHorizontal:false, //镜像
    });
    if (Faces.length > 0) {
        this.log(`检测到人脸`);
    } else {
        this.log(`没有检测到人脸`);
    }
}

image.png

特征检测

人脸特征提取就是针对人脸的某些特征进行判断(以下的动作判断仅供参考,实际情况下需要多个特征点来判断某个动作)

人脸的远近

取4帧 人脸占画面的比例,判断这组值是递增或递减,取第一帧和最后最后一帧的占比,根据阈值判断人脸的远近。

ScreenCapture_2022-9-21 14.07.47.gif

isFarAndNear(face) {
        const proportion = this.GetPercent(face.box.width * face.box.height, this.width * this.height);
        this.isFarArr.push(proportion);
        //计算4帧的动态变化
        if (this.isFarArr.length > 4) {
            this.isFarArr.shift();
            //递增 或 递减
            if (this.Increment(this.isFarArr) || this.Decrease(this.isFarArr)) {
                const first = this.isFarArr[0];
                const last = this.isFarArr[this.isFarArr.length - 1];
                const diff = this.GetPercent(first - last, first + last);
                if (diff <= -5) {
                    this.log(`【动作】靠近`, `info`);
                };
                if (diff >= 5) {
                    this.log(`【动作】远离`, `primary`);
                };
            }
        };
    },
张嘴

取2帧 [10,152][0,17]的比例,判断递增,取第一帧和最后最后一帧的距离,根据阈值判断张嘴。

ScreenCapture_2022-9-21 14.37.49.CUT.00'13-00'32.gif

isOpenMouth(face, ctx) {
    const featureIndex1 = [0, 17];
    const featureLocation1 = [];
    const featureIndex2 = [10, 152];
    const featureLocation2 = [];

    (face.keypoints || []).forEach((element, index) => {
        if (featureIndex1.includes(index)) {
            featureLocation1.push([element.x, element.y])
        }
        if (featureIndex2.includes(index)) {
            featureLocation2.push([element.x, element.y])
        }
    });

    // 10,152占0,17的比例
    const proportion = this.GetPercent(this.getDistance(
        featureLocation1[0][0],
        featureLocation1[0][1],
        featureLocation1[1][0],
        featureLocation1[1][1],
    ), this.getDistance(
        featureLocation2[0][0],
        featureLocation2[0][1],
        featureLocation2[1][0],
        featureLocation2[1][1],
    ));
    this.isOpenMouthArr.push(proportion);

    //计算2帧的动态变化
    if (this.isOpenMouthArr.length > 2) {
        this.isOpenMouthArr.shift();
        if (this.Increment(this.isOpenMouthArr)) {
            const first = this.isOpenMouthArr[0];
            const last = this.isOpenMouthArr[this.isOpenMouthArr.length - 1];
            const diff = this.GetPercent(first - last, first + last);
            if (diff <= -5) {
                this.log(`【动作】张嘴`, `info`);
            };
        }
    }
}
眨眼

根据左眼[159, 144] 右眼[385, 374]的距离,判断连续4帧小于阈值,即可判断眨眼了。

ScreenCapture_2022-9-21 15.00.53 (1).gif

    isWink(face, ctx) {
            const leftEye = [159, 144];
            const leftEyeLocation = [];
            const rightEye = [385, 374];
            const rightEyeLocation = [];
            (face.keypoints || []).forEach((element, index) => {
                if (leftEye.includes(index)) {
                    leftEyeLocation.push([element.x, element.y]);
                }
                if (rightEye.includes(index)) {
                    rightEyeLocation.push([element.x, element.y]);
                }
            });
            let leftProportion = this.getDistance(
                leftEyeLocation[0][0],
                leftEyeLocation[0][1],
                leftEyeLocation[1][0],
                leftEyeLocation[1][1],
            );
            let rightProportion = this.getDistance(
                rightEyeLocation[0][0],
                rightEyeLocation[0][1],
                rightEyeLocation[1][0],
                rightEyeLocation[1][1],
            );
            if (leftProportion <= 5 || rightProportion <= 5) {
                this.isWinkArr.push([leftProportion, rightProportion]);
                //连续4帧一次
                if (this.isWinkArr.length >= 4) {
                    this.log(`【动作】眨眼`, `info`);
                    this.isWinkArr = [];
                }
            } else {
                this.isWinkArr = [];
            }
        }
左右摇头

根据左脸[195, 93] 右脸[195, 323]的相差距离,取4帧数据,根据距离和正负数,来判断向左转和向右转。

ScreenCapture_2022-9-21 15.12.52.gif

    isShakingHisHead(face, ctx) {
        const leftFace = [195, 93];
        const leftFaceLocation = [];
        const rightFace = [195, 323];
        const rightFaceLocation = [];

        (face.keypoints || []).forEach((element, index) => {
            if (leftFace.includes(index)) {
                leftFaceLocation.push([element.x, element.y]);
            }
            if (rightFace.includes(index)) {
                if (rightFaceLocation.length === 0) {
                    ctx.moveTo(element.x, element.y)
                } else {
                    ctx.lineTo(element.x, element.y)
                }
                rightFaceLocation.push([element.x, element.y]);

            }
        });

        let leftProportion = this.getDistance(
            leftFaceLocation[0][0],
            leftFaceLocation[0][1],
            leftFaceLocation[1][0],
            leftFaceLocation[1][1],
        );
        let rightProportion = this.getDistance(
            rightFaceLocation[0][0],
            rightFaceLocation[0][1],
            rightFaceLocation[1][0],
            rightFaceLocation[1][1],
        );

        const diff = this.GetPercent(leftProportion - rightProportion, leftProportion + rightProportion);
        this.isShakingHisHeadArr.push(diff); //左 -40 右 40

        //计算4帧的动态变化
        if (this.isShakingHisHeadArr.length > 4) {
            this.isShakingHisHeadArr.shift();
            const isL = this.isShakingHisHeadArr.every(e => e >= -60);
            const isR = this.isShakingHisHeadArr.every(e => e <= 60);
            if (isL) {
                this.log(`【动作】向左转`, `info`);
            }
            if (isR) {
                this.log(`【动作】向右转`, `info`);
            }
        };


    }

文档

tensorflow.google.cn/js?hl=zh-cn

github.com/tensorflow/…