前端人脸识别,简单的活体检测(张张嘴...),vue3使用tracking.js,face.js,face-api.js实现

实现的逻辑是先检测是否有人脸,然后再检测是否张嘴了,最后生成照片传给后端比对人脸数据。本人是在工作中的项目需要考勤,前端需要活体检测。想到了这个简单的方法,在纯前端实现这些逻辑,当然精度不高,想要更好的实现,还得靠后端使用好的检测模型。

首先下载好相关的tracking.js,face.js,face-api.js(这个可以直接npm install face-api.js),以及一些静态的资源放public/models下

html部分(提示文字、video、canvas),css部分就不展示了,样式另行设计:

 

{{ tip }}

人脸识别部分:人脸识别使用 tracking.js和face.js,检测到人脸后即可进入面部动作识别区域

const borderColor=ref(false)
// import { useRouter } from "vue-router";
const show = ref(false);
const isIndex = ref(true);
const isPhoto = ref(false);
const videoEl = ref();
const localStream = ref();
const timer = ref();
const options = ref([]);
const showPicker = ref(false);
const form = reactive({
  workerName: "",
  programName: "",
  programId: "",
});
const vedioOpen = ref(true)
const reJiance = ref(false)
const state = reactive({
  options: '',
  nose_x_anv: "",
  nose_y_anv: "",
  mouth_Y: '',
  action: "张嘴",
  isCheck: true,
})
//人脸检测
const tip = ref("请正对摄像头");
const mediaStreamTrack = ref();
const video = ref(); //播放器实例
const trackerTask = ref(); //tracking实例
const uploadLock = true; //上传锁
const faceflag = ref(false); //是否进行拍照
const src = ref();
const getUserMedia = ref("");
const canvas = ref();
const LorR = computed(() => store.state.UserInfo.workerName != undefined ? true : false
);
const selectPeople = computed(() => store.state.SalarySelectPeople);
const store = useStore();
//人脸检测区域
const init = async () => {
  toast("正在申请摄像头权限", "info")
  vedioOpen.value = false;
  initTracker(canvas.value);
};
const initTracker = async () => {
  const _this = this;
  // 固定写法
  let tracker = new window.tracking.ObjectTracker("face");
  tracker.setInitialScale(4);
  tracker.setStepSize(2);
  tracker.setEdgesDensity(0.1);
  //摄像头初始化
  trackerTask.value = window.tracking.track(video.value, tracker, {
    camera: true,
  });
  tracker.on("track", async (event) => {
    if (event.data.length == 0) {
      if (!faceflag.value) {
        tip.value = "未检测到人脸";
      }
    } else if (event.data.length > 0) {
      console.log(event.data.length);
      event.data.forEach(async (rect, index) => {
        if (index != 0) {
          return;
        }
        // 防抖
        if (!faceflag.value) {
          faceflag.value = true;
          tip.value = "请张张嘴";
          //背景更改,提示作用
          borderColor.value=true
          //进入人脸动作检测方法
          initHuoti(rect)
        }
      });
    }
  });
};

面部动作识别部分:

const jiance = ref();
const initHuoti = async (rect) => {
  await faceApi.nets.tinyFaceDetector.loadFromUri('/models');
  await faceApi.loadFaceLandmarkModel("/models");
  state.options = new faceApi.TinyFaceDetectorOptions({
    inputSize: 320,
    scoreThreshold: 0.4
  });
  //设定定时器,重复获取当前摄像头的图片
  jiance.value = setInterval(() => {
    let context = canvas.value.getContext("2d", { willReadFrequently: true });
    context.strokeStyle = "#a64ceb";
    context.strokeRect(rect.x, rect.y, rect.width, rect.height);
    canvas.value.width = video.value.videoWidth;
    canvas.value.height = video.value.videoHeight;
    context.drawImage(video.value, 0, 0, canvas.value.width, canvas.value.height);
    let base64Img = canvas.value.toDataURL("image/jpeg");
    face_test(base64Img)
  }, 400)
}
  //使用faceapi检测人脸图片的特征值
const face_test = async (base64Img) => {
  const detections1 = await faceApi.detectSingleFace(canvas.value, state.options).withFaceLandmarks();
  if (detections1) {
    const landmarks = detections1.landmarks
    const jawOutline = landmarks.getJawOutline()
    const nose = landmarks.getNose()
    const mouth = landmarks.getMouth()
    const leftEye = landmarks.getLeftEye()
    const rightEye = landmarks.getRightEye()
    const leftEyeBbrow = landmarks.getLeftEyeBrow()
    const rightEyeBrow = landmarks.getRightEyeBrow()
    //将特征值给这个方法检测是否张嘴了
    isOpenMouth(mouth, base64Img)
    const resizedDetections = faceApi.resizeResults(detections1, { width: 280, height: 280 })
    faceApi.draw.drawDetections(canvas.value, resizedDetections)
  }
}
//是否张嘴,精度取决于其中的检测方法
const isOpenMouth = (mouth, base64Img) => {
  const mouth_Y_list = mouth.map((item) => {
    return item.y
  })
  const max = Math.max(...mouth_Y_list)
  const min = Math.min(...mouth_Y_list)
  const _y = max - min
  if (state.mouth_Y === "") {
    state.mouth_Y = _y
  }
  if (Math.abs(state.mouth_Y - _y) > 10) {
    clearTimeout(jiance.value);
    toast('检测成功,正在拍照', 'info', 1000);
    tip.value = "正在拍照,请勿乱动";
    borderColor.value=false
    //上传照片
    uploadimg(base64Img);
  }
  state.mouth_Y = _y
}

更好的获取摄像头方法,在tracking.js中做修改tracking.initUserMedia_方法,使用window.stream = MediaStream;存储相关流,方便后面页面关闭。

function getMediaDevices() {
    
    try {
        navigator.mediaDevices.enumerateDevices().then(function (devices) {
            devices.forEach(function (device) {
                switch (device?.kind) {
                    case 'audioinput':
                        console.log('音频输入设备(麦克风|话筒):', device);
                        break;
                    case 'audiooutput':
                        console.log('音频输出设备(扬声器|音响):', device);
                        break;
                    case 'videoinput':
                        console.log('视频输入设备(摄像头|相机):', device);
                        break;
                    default:
                        console.log('当前可用的媒体设备: ', device);
                        break;
                }
            });
        }).catch(function (err) {
            console.error(err);
        });
    } catch (err) {
        console.error(err);
    } finally {
        if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
            console.log("不支持mediaDevices.enumerateDevices(), 未识别到多媒体设备!");
        }
    }
};
  tracking.initUserMedia_ = function(element, opt_options) {
    getMediaDevices();
    try {
      var options = {
          audio: true,    // 注:这里 audio、video 默认都为false【一定要确保有麦克风或摄像头(有啥设备就开启啥设备)硬件设备的情况下才设为true 或 {...},否则会报DOMException: Requested device not found 等错!!】
          video: true,  // 获取视频 默认video: { facingMode: 'user' } }前置相机
      }
      if (navigator.mediaDevices.getUserMedia) {
          // 访问用户媒体设备的 新标准API
          navigator.mediaDevices.getUserMedia(options).then(function (MediaStream) {
            element.srcObject=MediaStream
            window.stream = MediaStream;
          }).catch(function (err) {
              console.error("访问用户媒体设备失败:权限被拒绝 或 未识别到多媒体设备!", err);
          }).finally(() => {
              console.log('navigator.mediaDevices.getUserMedia API')
          });
      }
      else if (navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia) {
          try {
              //访问用户媒体设备的 旧标准API 兼容方法
              navigator.getUserMedia(options, function (MediaStream) {
                element.srcObject=MediaStream
                window.stream = MediaStream;
              }, function (err) {
                  console.error("访问用户媒体设备失败:权限被拒绝 或 未识别到多媒体设备!", err);
              })
          } catch (error) {
              console.error(err);
          }
          finally {
              console.log('navigator.getUserMedia API')
          };
      } else {
          if (0 > location.origin.indexOf('https://')) {
              console.error("提示:请尝试在本地localhost域名 或 https协议 的Web服务器环境中重新运行!");
          }
      }
  } catch (error) {
      console.error("访问用户媒体设备失败:", error);
  }
  };
//在使用页面关闭摄像头的方法
const stopMediaStreamTrack = function () {
  if (typeof window.stream === "object") {
    window.stream.getTracks().forEach(track => track.stop());
  }
  clearTimeout(jiance.value);
}